From 1e92dc70571fc599c0caa60acdf76bcc33e26999 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 5 May 2020 19:55:38 +0200 Subject: [PATCH] Extend writer.xml for hierarchical ItemSchemes, Annotations --- sdmx/model.py | 13 ++++-- sdmx/tests/writer/conftest.py | 13 +++++- sdmx/writer/xml.py | 74 +++++++++++++++++++++++++++-------- 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/sdmx/model.py b/sdmx/model.py index 5c95c7a7e..71ca9b4d3 100644 --- a/sdmx/model.py +++ b/sdmx/model.py @@ -141,7 +141,7 @@ def __add__(self, other): result.localizations.update(other.localizations) return result - def localized_default(self, locale): + def localized_default(self, locale=None): """Return the string in *locale*, or else the first defined.""" try: return self.localizations[locale] @@ -311,12 +311,12 @@ def __init__(self, *args, **kwargs): # Add this Item as a child of its parent parent = kwargs.get('parent', None) - if parent and self not in parent.child: - parent.child.append(self) + if parent: + parent.append_child(self) # Add this Item as a parent of its children for c in kwargs.get('child', []): - c.parent = self + self.append_child(c) def __contains__(self, item): """Recursive containment.""" @@ -324,6 +324,11 @@ def __contains__(self, item): if item == c or item in c: return True + def append_child(self, other): + if other not in self.child: + self.child.append(other) + other.parent = self + def get_child(self, id): """Return the child with the given *id*.""" for c in self.child: diff --git a/sdmx/tests/writer/conftest.py b/sdmx/tests/writer/conftest.py index 84ddee643..e415056d4 100644 --- a/sdmx/tests/writer/conftest.py +++ b/sdmx/tests/writer/conftest.py @@ -1,6 +1,6 @@ import pytest from sdmx.message import StructureMessage -from sdmx.model import Agency, Code, Codelist +from sdmx.model import Agency, Annotation, Code, Codelist @pytest.fixture @@ -25,6 +25,17 @@ def codelist(): id='B', name={'en': 'Beginning of period'}, ) + cl.items['B1'] = Code( + id='B1', + name={'en': 'Child code of B'}, + ) + cl.items['B'].append_child(cl.items['B1']) + + cl.items['A'].annotations.append(Annotation( + id='A1', + type='NOTE', + text={'en': 'Text annotation on Code A.'}, + )) return cl diff --git a/sdmx/writer/xml.py b/sdmx/writer/xml.py index 9e9f791fd..1a06f9c3d 100644 --- a/sdmx/writer/xml.py +++ b/sdmx/writer/xml.py @@ -11,7 +11,9 @@ def Element(name, *args, **kwargs): - return _element_maker(qname(*name.split(':')), *args, **kwargs) + name = name.split(':') + name = qname(*name) if len(name) == 2 else name[0] + return _element_maker(name, *args, **kwargs) Writer = BaseWriter('XML') @@ -37,20 +39,50 @@ def write(obj, **kwargs): # Utility functions -def nameable(obj, elem): - for locale, label in obj.name.localizations.items(): - child = Element('com:Name', label) +def i11lstring(obj, name): + """InternationalString. + + Returns a list of elements with name `name`. + """ + elems = [] + + for locale, label in obj.localizations.items(): + child = Element(name, label) child.set(qname('xml', 'lang'), locale) - elem.append(child) + elems.append(child) + + return elems -def maintainable(obj): - urn = sdmx.urn.make(obj) - elem = Element(f'str:{obj.__class__.__name__}', urn=urn) - nameable(obj, elem) +def annotable(obj, name, *args, **kwargs): + elem = Element(name, *args, **kwargs) + + if len(obj.annotations): + e_anno = Element('com:Annotations') + e_anno.extend(Writer.recurse(a) for a in obj.annotations) + elem.append(e_anno) + return elem +def identifiable(obj, name, *args, **kwargs): + return annotable(obj, name, *args, id=obj.id, **kwargs) + + +def nameable(obj, name, *args, **kwargs): + elem = identifiable(obj, name, *args, **kwargs) + elem.extend(i11lstring(obj.name, 'com:Name')) + return elem + + +def maintainable(obj, parent=None): + return nameable( + obj, + f'str:{obj.__class__.__name__}', + urn=sdmx.urn.make(obj, parent), + ) + + @Writer.register def _(obj: message.StructureMessage): msg = Element('mes:Structure') @@ -77,11 +109,21 @@ def _(obj: model.ItemScheme): @Writer.register def _(obj: model.Item, parent): - # NB this isn't correct: produces .Codelist instead of .Code - elem = Element( - f'str:{obj.__class__.__name__}', - id=obj.id, - urn=sdmx.urn.make(obj, parent), - ) - nameable(obj, elem) + elem = maintainable(obj, parent=parent) + + if obj.parent: + # Reference to parent code + e_parent = Element('str:Parent') + e_parent.append(Element('Ref', id=obj.parent.id)) + elem.append(e_parent) + + return elem + + +@Writer.register +def _(obj: model.Annotation): + elem = Element('com:Annotation') + if obj.id: + elem.attrib['id'] = obj.id + elem.extend(i11lstring(obj.text, 'com:AnnotationText')) return elem