Skip to content

Commit

Permalink
Merge pull request #6 from khaeru/issue/5
Browse files Browse the repository at this point in the history
Enhance writer.xml to round-trip ItemSchemes to XML
  • Loading branch information
khaeru authored Jun 4, 2020
2 parents bbf3bbb + 74ae038 commit d7e4f44
Show file tree
Hide file tree
Showing 17 changed files with 8,292 additions and 235 deletions.
2 changes: 1 addition & 1 deletion doc/walkthrough.rst
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ If given, the response from the web service is written to the specified file, *a

.. versionadded:: 0.2.1

:func:`.read_sdmx` can be used to load SDMX messages stored in local files:
:func:`~.sdmx.read_sdmx` can be used to load SDMX messages stored in local files:

.. ipython:: python
Expand Down
6 changes: 6 additions & 0 deletions doc/whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ What's new?
Next release (vX.Y.0)
=====================

New features
------------

- Methods like :meth:`.IdentifiableArtefact.compare` are added for recursive comparison of :mod:`.model` objects (:pull:`6`).
- :func:`.to_xml` covers a larger subset of SDMX-ML, including almost all contents of a :class:`.StructureMessage` (:pull:`6`).


v1.1.0 (2020-05-18)
===================
Expand Down
94 changes: 80 additions & 14 deletions sdmx/format/xml.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import logging
from functools import lru_cache
from operator import itemgetter

from lxml.etree import QName

from sdmx import message
from sdmx import message, model

log = logging.getLogger(__name__)


# XML Namespaces
_base_ns = "http://www.sdmx.org/resources/sdmxml/schemas/v2_1"
Expand All @@ -22,16 +27,77 @@
@lru_cache()
def qname(ns_or_name, name=None):
"""Return a fully-qualified tag *name* in namespace *ns*."""
ns, name = ns_or_name.split(":") if name is None else (ns_or_name, name)
return QName(NS[ns], name)


# Mapping tag names → Message classes
MESSAGE = {
"Structure": message.StructureMessage,
"GenericData": message.DataMessage,
"GenericTimeSeriesData": message.DataMessage,
"StructureSpecificData": message.DataMessage,
"StructureSpecificTimeSeriesData": message.DataMessage,
"Error": message.ErrorMessage,
}
if isinstance(ns_or_name, QName):
# Already a QName; do nothing
return ns_or_name
else:
ns, name = ns_or_name.split(":") if name is None else (ns_or_name, name)
return QName(NS[ns], name)


# Correspondence of message and model classes with XML tag names
_CLS_TAG = [
(message.DataMessage, qname("mes:GenericData")),
(message.DataMessage, qname("mes:GenericTimeSeriesData")),
(message.DataMessage, qname("mes:StructureSpecificData")),
(message.DataMessage, qname("mes:StructureSpecificTimeSeriesData")),
(message.ErrorMessage, qname("mes:Error")),
(message.StructureMessage, qname("mes:Structure")),
(model.Agency, qname("str:Agency")),
(model.Agency, qname("mes:Receiver")),
(model.Agency, qname("mes:Sender")),
(model.AttributeDescriptor, qname("str:AttributeList")),
(model.Categorisation, qname("str:Categorisation")),
(model.DataAttribute, qname("str:Attribute")),
(model.DataflowDefinition, qname("str:Dataflow")),
(model.DataStructureDefinition, qname("str:DataStructure")),
(model.DataStructureDefinition, qname("com:Structure")),
(model.DataStructureDefinition, qname("str:Structure")),
(model.Dimension, qname("str:Dimension")),
(model.Dimension, qname("str:DimensionReference")),
(model.Dimension, qname("str:GroupDimension")),
(model.DimensionDescriptor, qname("str:DimensionList")),
(model.GroupDimensionDescriptor, qname("str:Group")),
(model.GroupDimensionDescriptor, qname("str:AttachmentGroup")),
(model.GroupKey, qname("gen:GroupKey")),
(model.Key, qname("gen:ObsKey")),
(model.MeasureDescriptor, qname("str:MeasureList")),
(model.SeriesKey, qname("gen:SeriesKey")),
(model.StructureUsage, qname("com:StructureUsage")),
] + [
(getattr(model, name), qname("str", name))
for name in (
"AgencyScheme",
"Category",
"CategoryScheme",
"Code",
"Codelist",
"Concept",
"ConceptScheme",
"ContentConstraint",
"DataProvider",
"DataProviderScheme",
"PrimaryMeasure",
"TimeDimension",
)
]


@lru_cache()
def class_for_tag(tag):
"""Return a message or model class for an XML tag."""
results = map(itemgetter(0), filter(lambda ct: ct[1] == tag, _CLS_TAG))
try:
return next(results)
except StopIteration:
return None


@lru_cache()
def tag_for_class(cls):
"""Return an XML tag for a message or model class."""
results = map(itemgetter(1), filter(lambda ct: ct[0] is cls, _CLS_TAG))
try:
return next(results)
except StopIteration:
return None
31 changes: 31 additions & 0 deletions sdmx/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
:mod:`sdmx` also uses :class:`DataMessage` to encapsulate SDMX-JSON data
returned by data sources.
"""
import logging
from typing import List, Optional, Text, Union

from requests import Response

from sdmx import model
from sdmx.util import BaseModel, DictLike, summarize_dictlike

log = logging.getLogger(__name__)


def _summarize(obj, fields):
"""Helper method for __repr__ on Header and Message (sub)classes."""
Expand Down Expand Up @@ -98,6 +101,8 @@ class ErrorMessage(Message):


class StructureMessage(Message):
#: Collection of :class:`.Categorisation`.
categorisation: DictLike[str, model.Categorisation] = DictLike()
#: Collection of :class:`.CategoryScheme`.
category_scheme: DictLike[str, model.CategoryScheme] = DictLike()
#: Collection of :class:`.Codelist`.
Expand All @@ -115,6 +120,32 @@ class StructureMessage(Message):
#: Collection of :class:`.ProvisionAgreement`.
provisionagreement: DictLike[str, model.ProvisionAgreement] = DictLike()

def compare(self, other, strict=True):
"""Return :obj:`True` if `self` is the same as `other`.
Two StructureMessages compare equal if :meth:`.DictLike.compare` is :obj:`True`
for each of the object collection attributes.
Parameters
----------
strict : bool, optional
Passed to :meth:`.DictLike.compare`.
"""
return all(
getattr(self, attr).compare(getattr(other, attr), strict)
for attr in (
"categorisation",
"category_scheme",
"codelist",
"concept_scheme",
"constraint",
"dataflow",
"structure",
"organisation_scheme",
"provisionagreement",
)
)

def __repr__(self):
"""String representation."""
lines = [super().__repr__()]
Expand Down
Loading

0 comments on commit d7e4f44

Please sign in to comment.