Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sdmx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from sdmx.rest import Resource
from sdmx.source import add_source, get_source, list_sources
from sdmx.writer.csv import to_csv
from sdmx.writer.json import to_json
from sdmx.writer.xml import to_xml

__all__ = [
Expand All @@ -22,6 +23,7 @@
"read_sdmx",
"read_url",
"to_csv",
"to_json",
"to_pandas",
"to_xml",
"to_sdmx",
Expand Down
14 changes: 6 additions & 8 deletions sdmx/convert/common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Callable
from copy import deepcopy
from typing import Any


Expand Down Expand Up @@ -46,6 +47,10 @@ class DispatchConverter(Converter):

_registry: dict[type, Callable]

def __init_subclass__(cls, **kwargs) -> None:
super().__init_subclass__(**kwargs)
cls._registry = deepcopy(getattr(cls, "_registry", dict()))

def convert(self, obj, **kwargs):
# Use either type(obj) or a parent type to retrieve a conversion function
for i, cls in enumerate(type(obj).mro()):
Expand All @@ -72,14 +77,7 @@ def register(cls, func: "Callable"):
`func` must have an argument named `obj` that is annotated with a particular
type.
"""
try:
registry = getattr(cls, "_registry")
except AttributeError:
# First call → registry does not exist → create it
registry = dict()
setattr(cls, "_registry", registry)

# Register `func` for the class of the `obj` argument
registry[getattr(func, "__annotations__")["obj"]] = func
cls._registry[getattr(func, "__annotations__")["obj"]] = func

return func
22 changes: 22 additions & 0 deletions sdmx/format/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from dataclasses import dataclass

from sdmx.format.common import Format


@dataclass
class JSONFormat(Format):
"""Information about an SDMX-JSON format."""

suffix = "json"


class JSON_v10(JSONFormat):
version = "1.0"


class JSON_v20(JSONFormat):
version = "2.0.0"


class JSON_v21(JSONFormat):
version = "2.1.0"
18 changes: 18 additions & 0 deletions sdmx/tests/writer/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from datetime import datetime

import pytest

from sdmx import message
from sdmx.model import common
from sdmx.model.common import Agency, Codelist
from sdmx.model.v21 import Annotation

Expand All @@ -11,6 +14,21 @@
]


@pytest.fixture(scope="module")
def agency() -> common.Agency:
return common.Agency(id="TEST")


@pytest.fixture(scope="module")
def header(agency) -> message.Header:
return message.Header(
id="N_A",
prepared=datetime.now(),
receiver=common.Agency(id="N_A"),
sender=agency,
)


@pytest.fixture
def codelist():
"""A Codelist for writer testing."""
Expand Down
32 changes: 32 additions & 0 deletions sdmx/tests/writer/test_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest

import sdmx
from sdmx import message
from sdmx.format.json import JSON_v10, JSON_v20, JSON_v21, JSONFormat


@pytest.mark.parametrize(
"message_class",
[
message.DataMessage,
message.ErrorMessage,
message.MetadataMessage,
message.StructureMessage,
],
)
@pytest.mark.parametrize(
"json_format",
[
pytest.param(JSON_v10, marks=pytest.mark.xfail(raises=NotImplementedError)),
JSON_v20,
JSON_v21,
],
)
def test_empty_message(
header: message.Header, message_class, json_format: JSONFormat
) -> None:
msg = message_class(header=header)

result = sdmx.to_json(msg, format=json_format)

assert '{"meta": {"test": false, "id": "N_A"}}' == result
18 changes: 18 additions & 0 deletions sdmx/writer/json/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import TYPE_CHECKING

from sdmx.format.json import JSON_v20, JSON_v21

if TYPE_CHECKING:
from sdmx.message import DataMessage, StructureMessage


def to_json(obj: "DataMessage | StructureMessage", **kwargs) -> str:
format = kwargs.get("format", JSON_v20)
if format in (JSON_v20, JSON_v21):
from . import v2

return v2.JSONWriter(**kwargs).convert(obj)
else:
from . import v1

return v1.JSONWriter(**kwargs).convert(obj)
11 changes: 11 additions & 0 deletions sdmx/writer/json/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing import TYPE_CHECKING

from sdmx.convert.common import DispatchConverter

if TYPE_CHECKING:
from sdmx.format.json import JSONFormat


class BaseJSONWriter(DispatchConverter):
def __init__(self, format: "JSONFormat") -> None:
pass
12 changes: 12 additions & 0 deletions sdmx/writer/json/v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from sdmx import message

from .base import BaseJSONWriter


class JSONWriter(BaseJSONWriter):
pass


@JSONWriter.register
def _message(w: "JSONWriter", obj: message.Message):
raise NotImplementedError("Write SDMX-JSON 1.0")
43 changes: 43 additions & 0 deletions sdmx/writer/json/v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import json
from typing import Any

from sdmx import message

from .base import BaseJSONWriter


class JSONWriter(BaseJSONWriter):
pass


@JSONWriter.register
def _data_message(w: "JSONWriter", obj: message.DataMessage):
result = {"meta": w.convert(obj.header)}
return json.dumps(result)


@JSONWriter.register
def _error_message(w: "JSONWriter", obj: message.ErrorMessage):
result = {"meta": w.convert(obj.header)}
return json.dumps(result)


@JSONWriter.register
def _metadata_message(w: "JSONWriter", obj: message.MetadataMessage):
result = {"meta": w.convert(obj.header)}
return json.dumps(result)


@JSONWriter.register
def _structure_message(w: "JSONWriter", obj: message.StructureMessage):
result = {"meta": w.convert(obj.header)}
return json.dumps(result)


@JSONWriter.register
def _header(w: "JSONWriter", obj: message.Header):
result: dict[str, Any] = {"test": obj.test}
if obj.id:
result.update(id=obj.id)

return result