pysdmx 1.5.2__py3-none-any.whl → 1.7.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 +20 -6
- pysdmx/io/json/fusion/messages/code.py +21 -4
- pysdmx/io/json/fusion/messages/concept.py +10 -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 +265 -22
- pysdmx/io/json/sdmxjson2/messages/concept.py +75 -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/__ss_aux_reader.py +1 -2
- pysdmx/io/xml/__structure_aux_reader.py +15 -10
- pysdmx/io/xml/__structure_aux_writer.py +15 -13
- pysdmx/io/xml/__write_data_aux.py +6 -57
- pysdmx/io/xml/__write_structure_specific_aux.py +7 -3
- pysdmx/io/xml/doc_validation.py +1 -3
- pysdmx/io/xml/sdmx21/writer/generic.py +6 -4
- pysdmx/model/__init__.py +1 -3
- pysdmx/model/code.py +11 -1
- pysdmx/model/dataflow.py +23 -0
- pysdmx/model/map.py +19 -13
- pysdmx/model/message.py +10 -5
- pysdmx/toolkit/pd/_data_utils.py +99 -0
- pysdmx/toolkit/vtl/_validations.py +2 -3
- {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/METADATA +4 -3
- {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/RECORD +63 -51
- {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/WHEEL +1 -1
- {pysdmx-1.5.2.dist-info → pysdmx-1.7.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)
|
pysdmx/io/xml/__ss_aux_reader.py
CHANGED
|
@@ -30,8 +30,7 @@ def _reading_str_series(dataset: Dict[str, Any]) -> pd.DataFrame:
|
|
|
30
30
|
if OBS in data:
|
|
31
31
|
del keys[OBS]
|
|
32
32
|
data[OBS] = add_list(data[OBS])
|
|
33
|
-
for j in data[OBS]
|
|
34
|
-
test_list.append({**keys, **j})
|
|
33
|
+
test_list.extend([{**keys, **j} for j in data[OBS]])
|
|
35
34
|
else:
|
|
36
35
|
test_list.append(keys)
|
|
37
36
|
test_list, df = __process_df(test_list, df)
|
|
@@ -397,7 +397,7 @@ class StructureParser(Struct):
|
|
|
397
397
|
"""
|
|
398
398
|
if json_fac is None:
|
|
399
399
|
return
|
|
400
|
-
for key
|
|
400
|
+
for key in json_fac:
|
|
401
401
|
if key == TEXT_TYPE and json_fac[TEXT_TYPE] in list(DataType):
|
|
402
402
|
json_obj["dtype"] = DataType(json_fac[TEXT_TYPE])
|
|
403
403
|
|
|
@@ -907,9 +907,9 @@ class StructureParser(Struct):
|
|
|
907
907
|
item_json_info = self.__format_name_description(item_json_info)
|
|
908
908
|
if CONTACT in item_json_info and item_name_class == AGENCY:
|
|
909
909
|
item_json_info[CONTACT] = add_list(item_json_info[CONTACT])
|
|
910
|
-
contacts = [
|
|
911
|
-
|
|
912
|
-
|
|
910
|
+
contacts = [
|
|
911
|
+
self.__format_contact(e) for e in item_json_info[CONTACT]
|
|
912
|
+
]
|
|
913
913
|
item_json_info[CONTACT.lower() + "s"] = contacts
|
|
914
914
|
del item_json_info[CONTACT]
|
|
915
915
|
|
|
@@ -949,9 +949,11 @@ class StructureParser(Struct):
|
|
|
949
949
|
group_dimensions = [group_dimensions]
|
|
950
950
|
|
|
951
951
|
group["dimensions"] = [
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
952
|
+
(
|
|
953
|
+
d[DIM_REF]
|
|
954
|
+
if isinstance(d[DIM_REF], str)
|
|
955
|
+
else d[DIM_REF][REF][ID]
|
|
956
|
+
)
|
|
955
957
|
for d in group_dimensions
|
|
956
958
|
]
|
|
957
959
|
|
|
@@ -994,9 +996,12 @@ class StructureParser(Struct):
|
|
|
994
996
|
items = []
|
|
995
997
|
if item in element:
|
|
996
998
|
element[item] = add_list(element[item])
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
999
|
+
items.extend(
|
|
1000
|
+
[
|
|
1001
|
+
self.__format_item(item_elem, item)
|
|
1002
|
+
for item_elem in element[item]
|
|
1003
|
+
]
|
|
1004
|
+
)
|
|
1000
1005
|
del element[item]
|
|
1001
1006
|
element["items"] = items
|
|
1002
1007
|
element = self.__format_agency(element)
|
|
@@ -310,9 +310,9 @@ def __write_maintainable(
|
|
|
310
310
|
f"{str(maintainable.is_external_reference).lower()!r}"
|
|
311
311
|
)
|
|
312
312
|
if not references_30 and not (isinstance(maintainable, AgencyScheme)):
|
|
313
|
-
outfile[
|
|
314
|
-
|
|
315
|
-
)
|
|
313
|
+
outfile[
|
|
314
|
+
"Attributes"
|
|
315
|
+
] += f" isFinal={str(maintainable.is_final).lower()!r}"
|
|
316
316
|
|
|
317
317
|
if isinstance(maintainable.agency, str):
|
|
318
318
|
outfile["Attributes"] += f" agencyID={maintainable.agency!r}"
|
|
@@ -462,7 +462,7 @@ def __write_components( # noqa: C901
|
|
|
462
462
|
)
|
|
463
463
|
|
|
464
464
|
position = 1
|
|
465
|
-
for
|
|
465
|
+
for comps in components.values():
|
|
466
466
|
if comps:
|
|
467
467
|
role_name = ROLE_MAPPING[comps[0].role]
|
|
468
468
|
if role_name == MEASURE:
|
|
@@ -809,9 +809,9 @@ def __write_scheme( # noqa: C901
|
|
|
809
809
|
DSD,
|
|
810
810
|
DFW,
|
|
811
811
|
]:
|
|
812
|
-
data[
|
|
813
|
-
|
|
814
|
-
)
|
|
812
|
+
data[
|
|
813
|
+
"Attributes"
|
|
814
|
+
] += f" isPartial={str(item_scheme.is_partial).lower()!r}"
|
|
815
815
|
if scheme in [
|
|
816
816
|
RULE_SCHEME,
|
|
817
817
|
UDO_SCHEME,
|
|
@@ -820,9 +820,9 @@ def __write_scheme( # noqa: C901
|
|
|
820
820
|
CUSTOM_TYPE_SCHEME,
|
|
821
821
|
NAME_PER_SCHEME,
|
|
822
822
|
]:
|
|
823
|
-
data[
|
|
824
|
-
|
|
825
|
-
)
|
|
823
|
+
data[
|
|
824
|
+
"Attributes"
|
|
825
|
+
] += f" {_write_vtl(item_scheme, indent, references_30)}"
|
|
826
826
|
|
|
827
827
|
outfile = ""
|
|
828
828
|
|
|
@@ -1129,9 +1129,11 @@ def _write_vtl( # noqa: C901
|
|
|
1129
1129
|
ref_codelist = (
|
|
1130
1130
|
item_or_scheme.codelist
|
|
1131
1131
|
if isinstance(item_or_scheme.codelist, Reference)
|
|
1132
|
-
else
|
|
1133
|
-
|
|
1134
|
-
|
|
1132
|
+
else (
|
|
1133
|
+
parse_urn(item_or_scheme.codelist)
|
|
1134
|
+
if isinstance(item_or_scheme.codelist, str)
|
|
1135
|
+
else parse_short_urn(item_or_scheme.codelist.short_urn)
|
|
1136
|
+
)
|
|
1135
1137
|
)
|
|
1136
1138
|
if references_30:
|
|
1137
1139
|
data += (
|
|
@@ -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
|
|
@@ -56,13 +54,15 @@ def writing_validation(dataset: PandasDataset) -> None:
|
|
|
56
54
|
for comp in dataset.structure.components
|
|
57
55
|
if comp.role in (Role.DIMENSION, Role.MEASURE)
|
|
58
56
|
]
|
|
59
|
-
|
|
57
|
+
required_components.extend(
|
|
58
|
+
att.id
|
|
59
|
+
for att in dataset.structure.components.attributes
|
|
60
60
|
if (
|
|
61
61
|
att.required
|
|
62
62
|
and att.attachment_level is not None
|
|
63
63
|
and att.attachment_level != "D"
|
|
64
|
-
)
|
|
65
|
-
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
66
|
non_required = [
|
|
67
67
|
comp.id
|
|
68
68
|
for comp in dataset.structure.components
|
|
@@ -87,54 +87,3 @@ def writing_validation(dataset: PandasDataset) -> None:
|
|
|
87
87
|
)
|
|
88
88
|
if not dataset.structure.components.measures:
|
|
89
89
|
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
|
|
|
@@ -198,8 +198,12 @@ def __group_processing(
|
|
|
198
198
|
.to_dict(orient="records")
|
|
199
199
|
)
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
out_list.extend(
|
|
202
|
+
[
|
|
203
|
+
__format_group_str(record, group["group_id"])
|
|
204
|
+
for record in grouped_data
|
|
205
|
+
]
|
|
206
|
+
)
|
|
203
207
|
|
|
204
208
|
return "".join(out_list)
|
|
205
209
|
|
pysdmx/io/xml/doc_validation.py
CHANGED
|
@@ -39,9 +39,7 @@ def validate_doc(input_str: str) -> None:
|
|
|
39
39
|
doc = etree.parse(bytes_infile, parser=parser)
|
|
40
40
|
if not xmlschema.validate(doc):
|
|
41
41
|
log_errors = list(xmlschema.error_log) # type: ignore[call-overload]
|
|
42
|
-
unhandled_errors = []
|
|
43
|
-
for e in log_errors:
|
|
44
|
-
unhandled_errors.append(e.message)
|
|
42
|
+
unhandled_errors = [e.message for e in log_errors]
|
|
45
43
|
severe_errors = unhandled_errors.copy()
|
|
46
44
|
for e in unhandled_errors:
|
|
47
45
|
for allowed_error in ALLOWED_ERRORS_CONTENT:
|