pysdmx 1.5.2__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pysdmx/__init__.py +1 -1
- pysdmx/api/fmr/__init__.py +8 -3
- pysdmx/api/fmr/maintenance.py +158 -0
- pysdmx/api/qb/structure.py +1 -0
- pysdmx/api/qb/util.py +1 -0
- pysdmx/io/csv/__csv_aux_reader.py +99 -0
- pysdmx/io/csv/__csv_aux_writer.py +118 -0
- pysdmx/io/csv/sdmx10/reader/__init__.py +9 -14
- pysdmx/io/csv/sdmx10/writer/__init__.py +28 -2
- pysdmx/io/csv/sdmx20/__init__.py +0 -9
- pysdmx/io/csv/sdmx20/reader/__init__.py +8 -61
- pysdmx/io/csv/sdmx20/writer/__init__.py +32 -25
- pysdmx/io/csv/sdmx21/__init__.py +1 -0
- pysdmx/io/csv/sdmx21/reader/__init__.py +86 -0
- pysdmx/io/csv/sdmx21/writer/__init__.py +70 -0
- pysdmx/io/format.py +8 -0
- pysdmx/io/input_processor.py +16 -2
- pysdmx/io/json/fusion/messages/code.py +21 -4
- pysdmx/io/json/fusion/messages/concept.py +16 -8
- pysdmx/io/json/fusion/messages/dataflow.py +8 -1
- pysdmx/io/json/fusion/messages/dsd.py +15 -0
- pysdmx/io/json/fusion/messages/schema.py +8 -1
- pysdmx/io/json/sdmxjson2/messages/agency.py +43 -7
- pysdmx/io/json/sdmxjson2/messages/category.py +92 -7
- pysdmx/io/json/sdmxjson2/messages/code.py +239 -18
- pysdmx/io/json/sdmxjson2/messages/concept.py +78 -13
- pysdmx/io/json/sdmxjson2/messages/constraint.py +5 -5
- pysdmx/io/json/sdmxjson2/messages/core.py +121 -14
- pysdmx/io/json/sdmxjson2/messages/dataflow.py +63 -8
- pysdmx/io/json/sdmxjson2/messages/dsd.py +215 -20
- pysdmx/io/json/sdmxjson2/messages/map.py +200 -24
- pysdmx/io/json/sdmxjson2/messages/pa.py +36 -5
- pysdmx/io/json/sdmxjson2/messages/provider.py +35 -7
- pysdmx/io/json/sdmxjson2/messages/report.py +85 -7
- pysdmx/io/json/sdmxjson2/messages/schema.py +11 -12
- pysdmx/io/json/sdmxjson2/messages/structure.py +150 -2
- pysdmx/io/json/sdmxjson2/messages/vtl.py +547 -17
- pysdmx/io/json/sdmxjson2/reader/metadata.py +32 -0
- pysdmx/io/json/sdmxjson2/reader/structure.py +32 -0
- pysdmx/io/json/sdmxjson2/writer/__init__.py +9 -0
- pysdmx/io/json/sdmxjson2/writer/metadata.py +60 -0
- pysdmx/io/json/sdmxjson2/writer/structure.py +61 -0
- pysdmx/io/reader.py +28 -9
- pysdmx/io/serde.py +17 -0
- pysdmx/io/writer.py +45 -9
- pysdmx/io/xml/__write_data_aux.py +1 -54
- pysdmx/io/xml/__write_structure_specific_aux.py +1 -1
- pysdmx/io/xml/sdmx21/writer/generic.py +1 -1
- pysdmx/model/code.py +11 -1
- pysdmx/model/dataflow.py +23 -0
- pysdmx/model/map.py +12 -4
- pysdmx/model/message.py +9 -1
- pysdmx/toolkit/pd/_data_utils.py +100 -0
- pysdmx/toolkit/vtl/_validations.py +2 -3
- {pysdmx-1.5.2.dist-info → pysdmx-1.6.0.dist-info}/METADATA +3 -2
- {pysdmx-1.5.2.dist-info → pysdmx-1.6.0.dist-info}/RECORD +58 -46
- {pysdmx-1.5.2.dist-info → pysdmx-1.6.0.dist-info}/WHEEL +1 -1
- {pysdmx-1.5.2.dist-info → pysdmx-1.6.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Writer interface for SDMX-JSON 2.0.0 Reference Metadata messages."""
|
|
2
|
+
|
|
3
|
+
import msgspec
|
|
4
|
+
|
|
5
|
+
from pysdmx import errors
|
|
6
|
+
from pysdmx.io.json.sdmxjson2.messages import JsonMetadataMessage
|
|
7
|
+
from pysdmx.model import decoders
|
|
8
|
+
from pysdmx.model.message import MetadataMessage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read(input_str: str) -> MetadataMessage:
|
|
12
|
+
"""Read an SDMX-JSON 2.0.0 Metadata Message.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
input_str: SDMX-JSON reference metadata message to read.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
A pysdmx MetadataMessage
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
msg = msgspec.json.Decoder(
|
|
22
|
+
JsonMetadataMessage, dec_hook=decoders
|
|
23
|
+
).decode(input_str)
|
|
24
|
+
return msg.to_model()
|
|
25
|
+
except msgspec.DecodeError as de:
|
|
26
|
+
raise errors.Invalid(
|
|
27
|
+
"Invalid message",
|
|
28
|
+
(
|
|
29
|
+
"The supplied file could not be read as SDMX-JSON 2.0.0 "
|
|
30
|
+
"reference metadata message."
|
|
31
|
+
),
|
|
32
|
+
) from de
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Reader interface for SDMX-JSON 2.0.0 Structure messages."""
|
|
2
|
+
|
|
3
|
+
import msgspec
|
|
4
|
+
|
|
5
|
+
from pysdmx import errors
|
|
6
|
+
from pysdmx.io.json.sdmxjson2.messages import JsonStructureMessage
|
|
7
|
+
from pysdmx.model import decoders
|
|
8
|
+
from pysdmx.model.message import StructureMessage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read(input_str: str) -> StructureMessage:
|
|
12
|
+
"""Read an SDMX-JSON 2.0.0 Stucture Message.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
input_str: SDMX-JSON structure message to read.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
A pysdmx StructureMessage
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
msg = msgspec.json.Decoder(
|
|
22
|
+
JsonStructureMessage, dec_hook=decoders
|
|
23
|
+
).decode(input_str)
|
|
24
|
+
return msg.to_model()
|
|
25
|
+
except msgspec.DecodeError as de:
|
|
26
|
+
raise errors.Invalid(
|
|
27
|
+
"Invalid message",
|
|
28
|
+
(
|
|
29
|
+
"The supplied file could not be read as SDMX-JSON 2.0.0 "
|
|
30
|
+
"structure message."
|
|
31
|
+
),
|
|
32
|
+
) from de
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Collection of writers for SDMX-JSON messages."""
|
|
2
|
+
|
|
3
|
+
from pysdmx.io.json.sdmxjson2 import messages as msg
|
|
4
|
+
from pysdmx.io.serde import Serializers
|
|
5
|
+
|
|
6
|
+
serializers = Serializers(
|
|
7
|
+
metadata_message=msg.JsonMetadataMessage,
|
|
8
|
+
structure_message=msg.JsonStructureMessage,
|
|
9
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Writer interface for SDMX-JSON 2.0.0 Reference Metadata messages."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Sequence, Union
|
|
5
|
+
|
|
6
|
+
import msgspec
|
|
7
|
+
|
|
8
|
+
from pysdmx.io.json.sdmxjson2.messages import JsonMetadataMessage
|
|
9
|
+
from pysdmx.model import MetadataReport, encoders
|
|
10
|
+
from pysdmx.model.message import Header, MetadataMessage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write(
|
|
14
|
+
reports: Sequence[MetadataReport],
|
|
15
|
+
output_path: Optional[Union[str, Path]] = None,
|
|
16
|
+
prettyprint: bool = True,
|
|
17
|
+
header: Optional[Header] = None,
|
|
18
|
+
) -> Optional[str]:
|
|
19
|
+
"""Write metadata reports in SDMX-JSON 2.0.0.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
reports: The reference metadata reports to be serialized.
|
|
23
|
+
output_path: The path to save the JSON file. If None or empty, the
|
|
24
|
+
serialized content is returned as a string instead.
|
|
25
|
+
prettyprint: Whether to format the JSON output with indentation (True)
|
|
26
|
+
or output compact JSON without extra whitespace (False).
|
|
27
|
+
header: The header to be used in the SDMX-JSON message
|
|
28
|
+
(will be generated if no header is supplied).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The JSON string if output_path is None or empty, None otherwise.
|
|
32
|
+
"""
|
|
33
|
+
if not header:
|
|
34
|
+
header = Header()
|
|
35
|
+
sm = MetadataMessage(header, reports)
|
|
36
|
+
jsm = JsonMetadataMessage.from_model(sm)
|
|
37
|
+
|
|
38
|
+
encoder = msgspec.json.Encoder(enc_hook=encoders)
|
|
39
|
+
serialized_data = encoder.encode(jsm)
|
|
40
|
+
|
|
41
|
+
# Apply pretty-printing if requested
|
|
42
|
+
if prettyprint:
|
|
43
|
+
serialized_data = msgspec.json.format(serialized_data, indent=4)
|
|
44
|
+
|
|
45
|
+
# If output_path is provided, write to file
|
|
46
|
+
if output_path:
|
|
47
|
+
# Convert to Path object if string
|
|
48
|
+
if isinstance(output_path, str):
|
|
49
|
+
output_path = Path(output_path)
|
|
50
|
+
|
|
51
|
+
# Create parent directories if they don't exist
|
|
52
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# Write to file
|
|
55
|
+
with open(output_path, "wb") as f:
|
|
56
|
+
f.write(serialized_data)
|
|
57
|
+
return None
|
|
58
|
+
else:
|
|
59
|
+
# Return as string
|
|
60
|
+
return serialized_data.decode("utf-8")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Writer interface for SDMX-JSON 2.0.0 Structure messages."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Sequence, Union
|
|
5
|
+
|
|
6
|
+
import msgspec
|
|
7
|
+
|
|
8
|
+
from pysdmx.io.json.sdmxjson2.messages import JsonStructureMessage
|
|
9
|
+
from pysdmx.model import encoders
|
|
10
|
+
from pysdmx.model.__base import MaintainableArtefact
|
|
11
|
+
from pysdmx.model.message import Header, StructureMessage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write(
|
|
15
|
+
structures: Sequence[MaintainableArtefact],
|
|
16
|
+
output_path: Optional[Union[str, Path]] = None,
|
|
17
|
+
prettyprint: bool = True,
|
|
18
|
+
header: Optional[Header] = None,
|
|
19
|
+
) -> Optional[str]:
|
|
20
|
+
"""Write maintainable SDMX artefacts in SDMX-JSON 2.0.0.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
structures: The maintainable SDMX artefacts to be serialized.
|
|
24
|
+
output_path: The path to save the JSON file. If None or empty, the
|
|
25
|
+
serialized content is returned as a string instead.
|
|
26
|
+
prettyprint: Whether to format the JSON output with indentation (True)
|
|
27
|
+
or output compact JSON without extra whitespace (False).
|
|
28
|
+
header: The header to be used in the SDMX-JSON message
|
|
29
|
+
(will be generated if no header is supplied).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The JSON string if output_path is None or empty, None otherwise.
|
|
33
|
+
"""
|
|
34
|
+
if not header:
|
|
35
|
+
header = Header()
|
|
36
|
+
sm = StructureMessage(header, structures)
|
|
37
|
+
jsm = JsonStructureMessage.from_model(sm)
|
|
38
|
+
|
|
39
|
+
encoder = msgspec.json.Encoder(enc_hook=encoders)
|
|
40
|
+
serialized_data = encoder.encode(jsm)
|
|
41
|
+
|
|
42
|
+
# Apply pretty-printing if requested
|
|
43
|
+
if prettyprint:
|
|
44
|
+
serialized_data = msgspec.json.format(serialized_data, indent=4)
|
|
45
|
+
|
|
46
|
+
# If output_path is provided, write to file
|
|
47
|
+
if output_path:
|
|
48
|
+
# Convert to Path object if string
|
|
49
|
+
if isinstance(output_path, str):
|
|
50
|
+
output_path = Path(output_path)
|
|
51
|
+
|
|
52
|
+
# Create parent directories if they don't exist
|
|
53
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
# Write to file
|
|
56
|
+
with open(output_path, "wb") as f:
|
|
57
|
+
f.write(serialized_data)
|
|
58
|
+
return None
|
|
59
|
+
else:
|
|
60
|
+
# Return as string
|
|
61
|
+
return serialized_data.decode("utf-8")
|
pysdmx/io/reader.py
CHANGED
|
@@ -8,10 +8,10 @@ from pysdmx.errors import Invalid
|
|
|
8
8
|
from pysdmx.io.format import Format
|
|
9
9
|
from pysdmx.io.input_processor import process_string_to_read
|
|
10
10
|
from pysdmx.model import Schema
|
|
11
|
-
from pysdmx.model.__base import
|
|
12
|
-
from pysdmx.model.dataflow import Dataflow, DataStructureDefinition
|
|
11
|
+
from pysdmx.model.__base import MaintainableArtefact
|
|
13
12
|
from pysdmx.model.dataset import Dataset
|
|
14
13
|
from pysdmx.model.message import Message
|
|
14
|
+
from pysdmx.model.metadata import MetadataReport
|
|
15
15
|
from pysdmx.model.submission import SubmissionResult
|
|
16
16
|
from pysdmx.util import parse_short_urn
|
|
17
17
|
from pysdmx.util._model_utils import schema_generator
|
|
@@ -43,10 +43,9 @@ def read_sdmx( # noqa: C901
|
|
|
43
43
|
|
|
44
44
|
header = None
|
|
45
45
|
result_data: Sequence[Dataset] = []
|
|
46
|
-
result_structures: Sequence[
|
|
47
|
-
Union[ItemScheme, Dataflow, DataStructureDefinition]
|
|
48
|
-
] = []
|
|
46
|
+
result_structures: Sequence[MaintainableArtefact] = []
|
|
49
47
|
result_submission: Sequence[SubmissionResult] = []
|
|
48
|
+
reports: Sequence[MetadataReport] = []
|
|
50
49
|
if read_format == Format.STRUCTURE_SDMX_ML_2_1:
|
|
51
50
|
from pysdmx.io.xml.header import read as read_header
|
|
52
51
|
from pysdmx.io.xml.sdmx21.reader.structure import (
|
|
@@ -74,6 +73,24 @@ def read_sdmx( # noqa: C901
|
|
|
74
73
|
header = read_header(input_str, validate=validate)
|
|
75
74
|
# SDMX-ML 3.1 Structure
|
|
76
75
|
result_structures = read_structure(input_str, validate=validate)
|
|
76
|
+
elif read_format == Format.STRUCTURE_SDMX_JSON_2_0_0:
|
|
77
|
+
from pysdmx.io.json.sdmxjson2.reader.structure import (
|
|
78
|
+
read as read_struct,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
struct_msg = read_struct(input_str)
|
|
82
|
+
header = struct_msg.header
|
|
83
|
+
result_structures = (
|
|
84
|
+
struct_msg.structures if struct_msg.structures else []
|
|
85
|
+
)
|
|
86
|
+
elif read_format == Format.REFMETA_SDMX_JSON_2_0_0:
|
|
87
|
+
from pysdmx.io.json.sdmxjson2.reader.metadata import (
|
|
88
|
+
read as read_refmeta,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
ref_msg = read_refmeta(input_str)
|
|
92
|
+
header = ref_msg.header
|
|
93
|
+
reports = ref_msg.get_reports()
|
|
77
94
|
elif read_format == Format.DATA_SDMX_ML_2_1_GEN:
|
|
78
95
|
from pysdmx.io.xml.header import read as read_header
|
|
79
96
|
from pysdmx.io.xml.sdmx21.reader.generic import read as read_generic
|
|
@@ -127,18 +144,19 @@ def read_sdmx( # noqa: C901
|
|
|
127
144
|
# SDMX-CSV 1.0
|
|
128
145
|
result_data = read_csv_v1(input_str)
|
|
129
146
|
else:
|
|
130
|
-
# SDMX-CSV 2.
|
|
131
|
-
from pysdmx.io.csv.
|
|
147
|
+
# SDMX-CSV 2.1
|
|
148
|
+
from pysdmx.io.csv.sdmx21.reader import read as read_csv_v2
|
|
132
149
|
|
|
133
150
|
result_data = read_csv_v2(input_str)
|
|
134
151
|
|
|
135
|
-
if not (result_data or result_structures or result_submission):
|
|
152
|
+
if not (result_data or result_structures or result_submission or reports):
|
|
136
153
|
raise Invalid("Empty SDMX Message")
|
|
137
154
|
|
|
138
155
|
# Returning a Message class
|
|
139
156
|
if read_format in (
|
|
140
157
|
Format.DATA_SDMX_CSV_1_0_0,
|
|
141
158
|
Format.DATA_SDMX_CSV_2_0_0,
|
|
159
|
+
Format.DATA_SDMX_CSV_2_1_0,
|
|
142
160
|
Format.DATA_SDMX_ML_2_1_GEN,
|
|
143
161
|
Format.DATA_SDMX_ML_2_1_STR,
|
|
144
162
|
Format.DATA_SDMX_ML_3_0,
|
|
@@ -149,7 +167,8 @@ def read_sdmx( # noqa: C901
|
|
|
149
167
|
return Message(header=header, data=result_data)
|
|
150
168
|
elif read_format == Format.REGISTRY_SDMX_ML_2_1:
|
|
151
169
|
return Message(header=header, submission=result_submission)
|
|
152
|
-
|
|
170
|
+
elif read_format == Format.REFMETA_SDMX_JSON_2_0_0:
|
|
171
|
+
return Message(header=header, reports=reports)
|
|
153
172
|
# TODO: Ensure we have changed the signature of the structure readers
|
|
154
173
|
return Message(header=header, structures=result_structures)
|
|
155
174
|
|
pysdmx/io/serde.py
CHANGED
|
@@ -12,6 +12,15 @@ class Deserializer(Protocol):
|
|
|
12
12
|
"""Returns the domain objects."""
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class Serializer(Protocol):
|
|
17
|
+
"""Creates an SDMX message from domain objects."""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_model(self, message: Any) -> Any:
|
|
21
|
+
"""Returns the SDMX message."""
|
|
22
|
+
|
|
23
|
+
|
|
15
24
|
@dataclass
|
|
16
25
|
class Deserializers:
|
|
17
26
|
"""Collection of deserializers for a format."""
|
|
@@ -34,6 +43,14 @@ class Deserializers:
|
|
|
34
43
|
transformation_scheme: Deserializer
|
|
35
44
|
|
|
36
45
|
|
|
46
|
+
@dataclass
|
|
47
|
+
class Serializers:
|
|
48
|
+
"""Collection of serializers for a format."""
|
|
49
|
+
|
|
50
|
+
structure_message: Serializer
|
|
51
|
+
metadata_message: Serializer
|
|
52
|
+
|
|
53
|
+
|
|
37
54
|
@dataclass
|
|
38
55
|
class GdsDeserializers:
|
|
39
56
|
"""Collection of GDS deserializers for a format."""
|
pysdmx/io/writer.py
CHANGED
|
@@ -7,11 +7,14 @@ from typing import Any, Optional, Sequence
|
|
|
7
7
|
|
|
8
8
|
from pysdmx.errors import Invalid
|
|
9
9
|
from pysdmx.io.format import Format
|
|
10
|
+
from pysdmx.model import MetadataReport
|
|
11
|
+
from pysdmx.model.__base import MaintainableArtefact
|
|
10
12
|
from pysdmx.model.dataset import Dataset
|
|
11
13
|
|
|
12
14
|
WRITERS = {
|
|
13
15
|
Format.DATA_SDMX_CSV_1_0_0: "pysdmx.io.csv.sdmx10.writer",
|
|
14
16
|
Format.DATA_SDMX_CSV_2_0_0: "pysdmx.io.csv.sdmx20.writer",
|
|
17
|
+
Format.DATA_SDMX_CSV_2_1_0: "pysdmx.io.csv.sdmx21.writer",
|
|
15
18
|
Format.DATA_SDMX_ML_2_1_GEN: "pysdmx.io.xml.sdmx21.writer.generic",
|
|
16
19
|
Format.DATA_SDMX_ML_2_1_STR: "pysdmx.io.xml.sdmx21.writer."
|
|
17
20
|
"structure_specific",
|
|
@@ -22,14 +25,23 @@ WRITERS = {
|
|
|
22
25
|
Format.DATA_SDMX_ML_3_1: "pysdmx.io.xml.sdmx31.writer."
|
|
23
26
|
"structure_specific",
|
|
24
27
|
Format.STRUCTURE_SDMX_ML_3_1: "pysdmx.io.xml.sdmx31.writer.structure",
|
|
28
|
+
Format.STRUCTURE_SDMX_JSON_2_0_0: (
|
|
29
|
+
"pysdmx.io.json.sdmxjson2.writer.structure"
|
|
30
|
+
),
|
|
31
|
+
Format.REFMETA_SDMX_JSON_2_0_0: (
|
|
32
|
+
"pysdmx.io.json.sdmxjson2.writer.metadata"
|
|
33
|
+
),
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
STRUCTURE_WRITERS = (
|
|
28
37
|
Format.STRUCTURE_SDMX_ML_2_1,
|
|
29
38
|
Format.STRUCTURE_SDMX_ML_3_0,
|
|
30
39
|
Format.STRUCTURE_SDMX_ML_3_1,
|
|
40
|
+
Format.STRUCTURE_SDMX_JSON_2_0_0,
|
|
31
41
|
)
|
|
32
42
|
|
|
43
|
+
REFMETA_WRITERS = (Format.REFMETA_SDMX_JSON_2_0_0,)
|
|
44
|
+
|
|
33
45
|
|
|
34
46
|
def write_sdmx(
|
|
35
47
|
sdmx_objects: Any,
|
|
@@ -97,20 +109,36 @@ def write_sdmx(
|
|
|
97
109
|
writer = module.write
|
|
98
110
|
|
|
99
111
|
is_structure = sdmx_format in STRUCTURE_WRITERS
|
|
112
|
+
is_ref_meta = sdmx_format in REFMETA_WRITERS
|
|
100
113
|
is_xml = "xml" in WRITERS[sdmx_format]
|
|
101
|
-
|
|
114
|
+
is_json = "json" in WRITERS[sdmx_format]
|
|
115
|
+
if is_structure:
|
|
116
|
+
key = "structures"
|
|
117
|
+
elif is_ref_meta:
|
|
118
|
+
key = "reports"
|
|
119
|
+
else:
|
|
120
|
+
key = "datasets"
|
|
102
121
|
value = sdmx_objects if isinstance(sdmx_objects, list) else [sdmx_objects]
|
|
103
122
|
|
|
104
|
-
if is_structure and
|
|
123
|
+
if is_structure and not all(
|
|
124
|
+
isinstance(x, MaintainableArtefact) for x in value
|
|
125
|
+
):
|
|
105
126
|
raise Invalid(
|
|
106
|
-
"
|
|
107
|
-
"Use data formats instead."
|
|
127
|
+
"Only maintainable artefacts can be written to structure formats."
|
|
108
128
|
)
|
|
109
|
-
elif
|
|
129
|
+
elif is_ref_meta and not all(isinstance(x, MetadataReport) for x in value):
|
|
110
130
|
raise Invalid(
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
(
|
|
132
|
+
"Only metadata reports can be written to reference "
|
|
133
|
+
"metadata formats."
|
|
134
|
+
)
|
|
113
135
|
)
|
|
136
|
+
elif (
|
|
137
|
+
not is_structure
|
|
138
|
+
and not is_ref_meta
|
|
139
|
+
and not all(isinstance(x, Dataset) for x in value)
|
|
140
|
+
):
|
|
141
|
+
raise Invalid("Only Datasets can be written to data formats.")
|
|
114
142
|
|
|
115
143
|
args = {
|
|
116
144
|
key: value,
|
|
@@ -120,7 +148,7 @@ def write_sdmx(
|
|
|
120
148
|
"prettyprint": kwargs.get("prettyprint"),
|
|
121
149
|
"header": kwargs.get("header"),
|
|
122
150
|
}
|
|
123
|
-
if is_xml
|
|
151
|
+
if is_xml or is_json
|
|
124
152
|
else {}
|
|
125
153
|
),
|
|
126
154
|
**(
|
|
@@ -132,7 +160,15 @@ def write_sdmx(
|
|
|
132
160
|
if is_xml and not is_structure
|
|
133
161
|
else {}
|
|
134
162
|
),
|
|
163
|
+
**(
|
|
164
|
+
{
|
|
165
|
+
"labels": kwargs.get("labels"),
|
|
166
|
+
"keys": kwargs.get("keys"),
|
|
167
|
+
"time_format": kwargs.get("time_format"),
|
|
168
|
+
}
|
|
169
|
+
if not is_xml
|
|
170
|
+
else {}
|
|
171
|
+
),
|
|
135
172
|
}
|
|
136
173
|
args = {k: v for k, v in args.items() if v is not None}
|
|
137
|
-
|
|
138
174
|
return writer(**args)
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from typing import
|
|
2
|
-
|
|
3
|
-
import pandas as pd
|
|
1
|
+
from typing import Dict, Optional, Sequence
|
|
4
2
|
|
|
5
3
|
from pysdmx.errors import Invalid
|
|
6
4
|
from pysdmx.io.pd import PandasDataset
|
|
@@ -87,54 +85,3 @@ def writing_validation(dataset: PandasDataset) -> None:
|
|
|
87
85
|
)
|
|
88
86
|
if not dataset.structure.components.measures:
|
|
89
87
|
raise Invalid("The dataset structure must have at least one measure.")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def get_codes(
|
|
93
|
-
dimension_code: str, structure: Schema, data: pd.DataFrame
|
|
94
|
-
) -> Tuple[List[str], List[str], List[Dict[str, Any]]]:
|
|
95
|
-
"""This function divides the components in Series and Obs."""
|
|
96
|
-
series_codes = []
|
|
97
|
-
groups = structure.groups
|
|
98
|
-
group_codes = []
|
|
99
|
-
obs_codes = [dimension_code, structure.components.measures[0].id]
|
|
100
|
-
|
|
101
|
-
# Getting the series and obs codes
|
|
102
|
-
for dim in structure.components.dimensions:
|
|
103
|
-
if dim.id != dimension_code:
|
|
104
|
-
series_codes.append(dim.id)
|
|
105
|
-
|
|
106
|
-
# Adding the attributes based on the attachment level
|
|
107
|
-
for att in structure.components.attributes:
|
|
108
|
-
matching_group = next(
|
|
109
|
-
(
|
|
110
|
-
group
|
|
111
|
-
for group in groups or []
|
|
112
|
-
if set(group.dimensions)
|
|
113
|
-
== set(att.attachment_level.split(",")) # type: ignore[union-attr]
|
|
114
|
-
),
|
|
115
|
-
None,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
att.attachment_level != "D"
|
|
120
|
-
and att.id in data.columns
|
|
121
|
-
and groups is not None
|
|
122
|
-
and matching_group
|
|
123
|
-
):
|
|
124
|
-
group_codes.append(
|
|
125
|
-
{
|
|
126
|
-
"group_id": matching_group.id,
|
|
127
|
-
"attribute": att.id,
|
|
128
|
-
"dimensions": matching_group.dimensions,
|
|
129
|
-
}
|
|
130
|
-
)
|
|
131
|
-
elif att.attachment_level == "O" and att.id in data.columns:
|
|
132
|
-
obs_codes.append(att.id)
|
|
133
|
-
elif (
|
|
134
|
-
att.attachment_level is not None
|
|
135
|
-
and att.attachment_level != "D"
|
|
136
|
-
and att.id in data.columns
|
|
137
|
-
):
|
|
138
|
-
series_codes.append(att.id)
|
|
139
|
-
|
|
140
|
-
return series_codes, obs_codes, group_codes
|
|
@@ -13,10 +13,10 @@ from pysdmx.io.xml.__write_aux import (
|
|
|
13
13
|
get_structure,
|
|
14
14
|
)
|
|
15
15
|
from pysdmx.io.xml.__write_data_aux import (
|
|
16
|
-
get_codes,
|
|
17
16
|
writing_validation,
|
|
18
17
|
)
|
|
19
18
|
from pysdmx.io.xml.config import CHUNKSIZE
|
|
19
|
+
from pysdmx.toolkit.pd._data_utils import get_codes
|
|
20
20
|
from pysdmx.util import parse_short_urn
|
|
21
21
|
|
|
22
22
|
|
|
@@ -21,11 +21,11 @@ from pysdmx.io.xml.__write_aux import (
|
|
|
21
21
|
from pysdmx.io.xml.__write_data_aux import (
|
|
22
22
|
check_content_dataset,
|
|
23
23
|
check_dimension_at_observation,
|
|
24
|
-
get_codes,
|
|
25
24
|
writing_validation,
|
|
26
25
|
)
|
|
27
26
|
from pysdmx.io.xml.config import CHUNKSIZE
|
|
28
27
|
from pysdmx.model.message import Header
|
|
28
|
+
from pysdmx.toolkit.pd._data_utils import get_codes
|
|
29
29
|
from pysdmx.util import parse_short_urn
|
|
30
30
|
|
|
31
31
|
|
pysdmx/model/code.py
CHANGED
|
@@ -21,7 +21,13 @@ from typing import Iterator, Literal, Optional, Sequence, Union
|
|
|
21
21
|
|
|
22
22
|
from msgspec import Struct
|
|
23
23
|
|
|
24
|
-
from pysdmx.model.__base import
|
|
24
|
+
from pysdmx.model.__base import (
|
|
25
|
+
Agency,
|
|
26
|
+
Annotation,
|
|
27
|
+
Item,
|
|
28
|
+
ItemScheme,
|
|
29
|
+
MaintainableArtefact,
|
|
30
|
+
)
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
class Code(Item, frozen=True, omit_defaults=True):
|
|
@@ -139,6 +145,8 @@ class HierarchicalCode(
|
|
|
139
145
|
rel_valid_from: Start of the hierarchical relationship validity.
|
|
140
146
|
rel_valid_to: End of the hierarchical relationship validity.
|
|
141
147
|
codes: The child codes.
|
|
148
|
+
annotations: Annotations attached to the code.
|
|
149
|
+
urn: The URN of the code.
|
|
142
150
|
"""
|
|
143
151
|
|
|
144
152
|
id: str
|
|
@@ -149,6 +157,8 @@ class HierarchicalCode(
|
|
|
149
157
|
rel_valid_from: Optional[datetime] = None
|
|
150
158
|
rel_valid_to: Optional[datetime] = None
|
|
151
159
|
codes: Sequence["HierarchicalCode"] = ()
|
|
160
|
+
annotations: Sequence[Annotation] = ()
|
|
161
|
+
urn: Optional[str] = None
|
|
152
162
|
|
|
153
163
|
def __iter__(self) -> Iterator["HierarchicalCode"]:
|
|
154
164
|
"""Return an iterator over the list of codes."""
|
pysdmx/model/dataflow.py
CHANGED
|
@@ -130,6 +130,9 @@ class Component(
|
|
|
130
130
|
commas, for series- and group-level attributes).
|
|
131
131
|
A post_init check makes this attribute mandatory for attributes.
|
|
132
132
|
array_def: Any additional constraints for array types.
|
|
133
|
+
urn: The URN of the component.
|
|
134
|
+
local_enum_ref: The URN of the enumeration (codelist or valuelist) from
|
|
135
|
+
which the local codes are taken.
|
|
133
136
|
"""
|
|
134
137
|
|
|
135
138
|
id: str
|
|
@@ -144,6 +147,7 @@ class Component(
|
|
|
144
147
|
attachment_level: Optional[str] = None
|
|
145
148
|
array_def: Optional[ArrayBoundaries] = None
|
|
146
149
|
urn: Optional[str] = None
|
|
150
|
+
local_enum_ref: Optional[str] = None
|
|
147
151
|
|
|
148
152
|
def __post_init__(self) -> None:
|
|
149
153
|
"""Validate the component."""
|
|
@@ -217,6 +221,20 @@ class Component(
|
|
|
217
221
|
else:
|
|
218
222
|
return None
|
|
219
223
|
|
|
224
|
+
@property
|
|
225
|
+
def enum_ref(self) -> Optional[str]:
|
|
226
|
+
"""Returns the URN of the enumeration from which the codes are taken.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
The URN of the enumeration from which the codes are taken.
|
|
230
|
+
"""
|
|
231
|
+
if self.local_enum_ref:
|
|
232
|
+
return self.local_enum_ref
|
|
233
|
+
elif isinstance(self.concept, Concept) and self.concept.enum_ref:
|
|
234
|
+
return self.concept.enum_ref
|
|
235
|
+
else:
|
|
236
|
+
return None
|
|
237
|
+
|
|
220
238
|
def __str__(self) -> str:
|
|
221
239
|
"""Custom string representation without the class name."""
|
|
222
240
|
processed_output = []
|
|
@@ -393,6 +411,7 @@ class DataflowInfo(
|
|
|
393
411
|
end_period: The oldest period for which data are available.
|
|
394
412
|
last_updated: When the dataflow was last updated.
|
|
395
413
|
dsd_ref: The URN of the data structure used by the dataflow.
|
|
414
|
+
groups: The sequence of groups defined in the data structure.
|
|
396
415
|
"""
|
|
397
416
|
|
|
398
417
|
id: str
|
|
@@ -408,6 +427,7 @@ class DataflowInfo(
|
|
|
408
427
|
end_period: Optional[str] = None
|
|
409
428
|
last_updated: Optional[datetime] = None
|
|
410
429
|
dsd_ref: Optional[str] = None
|
|
430
|
+
groups: Optional[Sequence[Group]] = None
|
|
411
431
|
|
|
412
432
|
def __str__(self) -> str:
|
|
413
433
|
"""Custom string representation without the class name."""
|
|
@@ -472,6 +492,7 @@ class Schema(Struct, frozen=True, omit_defaults=True, repr_omit_defaults=True):
|
|
|
472
492
|
version: str = "1.0"
|
|
473
493
|
artefacts: Sequence[str] = ()
|
|
474
494
|
generated: datetime = datetime.now(timezone.utc)
|
|
495
|
+
name: Optional[str] = None
|
|
475
496
|
groups: Optional[Sequence[Group]] = None
|
|
476
497
|
|
|
477
498
|
def __str__(self) -> str:
|
|
@@ -533,6 +554,7 @@ class DataStructureDefinition(MaintainableArtefact, frozen=True, kw_only=True):
|
|
|
533
554
|
valid_from: The date from which the data structure is valid.
|
|
534
555
|
valid_to: The date until which the data structure is valid.
|
|
535
556
|
version: The version of the data structure.
|
|
557
|
+
components: The list of relevant components for this data structure.
|
|
536
558
|
evolving_structure: Whether new dimensions may be added under a
|
|
537
559
|
minor version update.
|
|
538
560
|
"""
|
|
@@ -572,6 +594,7 @@ class DataStructureDefinition(MaintainableArtefact, frozen=True, kw_only=True):
|
|
|
572
594
|
components=self.components,
|
|
573
595
|
version=self.version,
|
|
574
596
|
artefacts=self.__extract_artefacts(),
|
|
597
|
+
name=self.name,
|
|
575
598
|
)
|
|
576
599
|
|
|
577
600
|
@property
|
pysdmx/model/map.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import Any, Iterator, Literal, Optional, Sequence, Tuple, Union
|
|
|
6
6
|
|
|
7
7
|
from msgspec import Struct
|
|
8
8
|
|
|
9
|
-
from pysdmx.model.__base import MaintainableArtefact
|
|
9
|
+
from pysdmx.model.__base import Agency, MaintainableArtefact
|
|
10
10
|
from pysdmx.util._date_pattern_map import convert_dpm
|
|
11
11
|
|
|
12
12
|
|
|
@@ -77,7 +77,7 @@ class DatePatternMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
|
|
|
77
77
|
the target structure (e.g. `FREQ`). In this case, the input date
|
|
78
78
|
can be converted to a different format, depending on the
|
|
79
79
|
frequency of the converted data.
|
|
80
|
-
|
|
80
|
+
resolve_period: The point in time to resolve to when mapping from low
|
|
81
81
|
frequency to higher frequency periods.
|
|
82
82
|
"""
|
|
83
83
|
|
|
@@ -88,7 +88,7 @@ class DatePatternMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
|
|
|
88
88
|
id: Optional[str] = None
|
|
89
89
|
locale: str = "en"
|
|
90
90
|
pattern_type: Literal["fixed", "variable"] = "fixed"
|
|
91
|
-
|
|
91
|
+
resolve_period: Optional[
|
|
92
92
|
Literal["startOfPeriod", "endOfPeriod", "midPeriod"]
|
|
93
93
|
] = None
|
|
94
94
|
|
|
@@ -264,6 +264,14 @@ class MultiRepresentationMap(
|
|
|
264
264
|
target: Sequence[str] = []
|
|
265
265
|
maps: Sequence[MultiValueMap] = []
|
|
266
266
|
|
|
267
|
+
@property
|
|
268
|
+
def short_urn(self) -> str:
|
|
269
|
+
"""Returns the short URN for the MultiRepresentationMap."""
|
|
270
|
+
agency = (
|
|
271
|
+
self.agency.id if isinstance(self.agency, Agency) else self.agency
|
|
272
|
+
)
|
|
273
|
+
return f"RepresentationMap={agency}:{self.id}({self.version})"
|
|
274
|
+
|
|
267
275
|
def __iter__(
|
|
268
276
|
self,
|
|
269
277
|
) -> Iterator[MultiValueMap]:
|
|
@@ -300,7 +308,7 @@ class MultiComponentMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
|
|
|
300
308
|
|
|
301
309
|
source: Sequence[str]
|
|
302
310
|
target: Sequence[str]
|
|
303
|
-
values: MultiRepresentationMap
|
|
311
|
+
values: Union[MultiRepresentationMap, str]
|
|
304
312
|
|
|
305
313
|
|
|
306
314
|
class RepresentationMap(MaintainableArtefact, frozen=True, omit_defaults=True):
|