Skip to content

Commit

Permalink
Merge pull request #3 from khaeru/feature/multiple-writers
Browse files Browse the repository at this point in the history
Support multiple writers
  • Loading branch information
khaeru authored May 5, 2020
2 parents 479742f + 1e92dc7 commit 4891810
Show file tree
Hide file tree
Showing 22 changed files with 613 additions and 199 deletions.
3 changes: 0 additions & 3 deletions .coveragerc

This file was deleted.

6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
*.py[cod]
__pycache__

.eggs
*.cache
*.egg-info
*.pdf
*.py[cod]

# Development and build files
.coverage
.coverage*
.pytest_cache
build
coverage.xml
Expand Down
88 changes: 62 additions & 26 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,41 +68,77 @@ SDMX-JSON
:undoc-members:


``writer``: Convert SDMX to pandas objects
------------------------------------------
``writer``: Convert ``sdmx`` objects to other formats
-----------------------------------------------------

.. _writer-pandas:

``writer.pandas``: Convert to ``pandas`` objects
::::::::::::::::::::::::::::::::::::::::::::::::

.. currentmodule:: sdmx.writer.pandas

.. versionchanged:: 1.0

:meth:`sdmx.to_pandas` (via :meth:`write <sdmx.writer.write>`)
handles all types of objects, replacing the earlier, separate
``data2pandas`` and ``structure2pd`` writers.
:meth:`sdmx.to_pandas` handles all types of objects, replacing the earlier, separate ``data2pandas`` and ``structure2pd`` writers.

.. automodule:: sdmx.writer
:members:
:exclude-members: write

.. automethod:: sdmx.writer.write

.. autosummary::
write_component
write_datamessage
write_dataset
write_dict
write_dimensiondescriptor
write_itemscheme
write_list
write_membervalue
write_nameableartefact
write_serieskeys
write_structuremessage

.. autodata:: DEFAULT_RTYPE
:noindex:
:func:`.to_pandas` implements a dispatch pattern according to the type of *obj*.
Some of the internal methods take specific arguments and return varying values.
These arguments can be passed to :meth:`to_pandas` when `obj` is of the appropriate type:

.. autosummary::
sdmx.writer.pandas.write_dataset
sdmx.writer.pandas.write_datamessage
sdmx.writer.pandas.write_itemscheme
sdmx.writer.pandas.write_structuremessage
sdmx.writer.pandas.DEFAULT_RTYPE

Other objects are converted as follows:

:class:`.Component`
The :attr:`~.Concept.id` attribute of the :attr:`~.Component.concept_identity` is returned.

:class:`.DataMessage`
The :class:`.DataSet` or data sets within the Message are converted to pandas objects.
Returns:

- :class:`pandas.Series` or :class:`pandas.DataFrame`, if `obj` has only one data set.
- list of (Series or DataFrame), if `obj` has more than one data set.

:class:`.dict`
The values of the mapping are converted individually.
If the resulting values are :class:`str` or Series *with indexes that share the same name*, then they are converted to a Series, possibly with a :class:`pandas.MultiIndex`.
Otherwise, a :class:`.DictLike` is returned.

:class:`.DimensionDescriptor`
The :attr:`~.DimensionDescriptor.components` of the DimensionDescriptor are written.

:class:`list`
For the following *obj*, returns Series instead of a :class:`list`:

- a list of :class:`.Observation`: the Observations are written using :meth:`write_dataset`.
- a list with only 1 :class:`.DataSet` (e.g. the :attr:`~.DataMessage.data` attribute of :class:`.DataMessage`): the Series for the single element is returned.
- a list of :class:`.SeriesKey`: the key values (but no data) are returned.

:class:`.NameableArtefact`
The :attr:`~.NameableArtefact.name` attribute of `obj` is returned.

.. automodule:: sdmx.writer.pandas
:members: DEFAULT_RTYPE, write_dataset, write_datamessage, write_itemscheme, write_structuremessage

.. todo::
Support selection of language for conversion of
:class:`InternationalString <sdmx.model.InternationalString>`.


``writer.xml``: Write to SDMX-ML
::::::::::::::::::::::::::::::::

.. versionadded:: 1.1

See :func:`.to_xml`.


``remote``: Access SDMX REST web services
-----------------------------------------
.. autoclass:: sdmx.remote.Session
Expand Down
6 changes: 5 additions & 1 deletion doc/whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ What's new?
Next release (vX.Y.0)
=====================

- Test suite improvements
- New features:

- :pull:`3`: Add :meth:`to_xml` to generate SDMX-ML for a subset of the IM.

- Test suite:

- :pull:`2`: Add tests of data queries for source(s): OECD

Expand Down
3 changes: 2 additions & 1 deletion sdmx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sdmx.api import Request, read_sdmx, read_url
from sdmx.source import add_source, list_sources
from sdmx.util import Resource
from sdmx.writer import write as to_pandas
from sdmx.writer import to_pandas, to_xml
import logging

__all__ = [
Expand All @@ -15,6 +15,7 @@
'read_sdmx',
'read_url',
'to_pandas',
'to_xml',
]


Expand Down
23 changes: 23 additions & 0 deletions sdmx/format/xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from functools import lru_cache

from lxml.etree import QName


# XML Namespaces
_base_ns = 'http://www.sdmx.org/resources/sdmxml/schemas/v2_1'
NS = {
'com': f'{_base_ns}/common',
'data': f'{_base_ns}/data/structurespecific',
'str': f'{_base_ns}/structure',
'mes': f'{_base_ns}/message',
'gen': f'{_base_ns}/data/generic',
'footer': f'{_base_ns}/message/footer',
'xml': 'http://www.w3.org/XML/1998/namespace',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
}


@lru_cache()
def qname(ns, name):
"""Return a fully-qualified tag *name* in namespace *ns*."""
return QName(NS[ns], name)
48 changes: 41 additions & 7 deletions sdmx/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@ def __init__(self, value=None, **kwargs):
and isinstance(value[0], str)):
# 2-tuple of str is (locale, label)
value = {value[0]: value[1]}
elif isinstance(value, dict):
# dict; use directly
pass
elif isinstance(value, IterableABC):
# Iterable of 2-tuples
value = {locale: label for (locale, label) in value}
elif value is None:
# Keyword arguments → dict, possibly empty
value = dict(kwargs)
elif isinstance(value, dict):
# dict; use directly
pass
else:
raise ValueError(value, kwargs)

Expand All @@ -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]
Expand Down Expand Up @@ -311,19 +311,24 @@ 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."""
for c in self.child:
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:
Expand Down Expand Up @@ -1621,3 +1626,32 @@ class ProvisionAgreement(MaintainableArtefact, ConstrainableArtefact):
structure_usage: StructureUsage = None
#:
data_provider: DataProvider = None


#: The SDMX-IM defines 'packages'; these are used in URNs.
PACKAGE = dict()

_PACKAGE_CLASS = {
'base': {Agency, AgencyScheme, DataProvider},
'categoryscheme': {Category, Categorisation, CategoryScheme},
'codelist': {Code, Codelist},
'conceptscheme': {Concept, ConceptScheme},
'datastructure': {DataflowDefinition, DataStructureDefinition},
'registry': {ContentConstraint, ProvisionAgreement},
}

for package, classes in _PACKAGE_CLASS.items():
PACKAGE.update({cls: package for cls in classes})


def get_class(cls, package=None):
"""Return a class object for string *cls* and *package* names."""
if isinstance(cls, str):
if cls in 'Dataflow DataStructure':
cls += 'Definition'
cls = globals()[cls]

if package and package != PACKAGE[cls]:
raise ValueError(f'Package {repr(package)} invalid for {cls}')

return cls
Loading

0 comments on commit 4891810

Please sign in to comment.