pysdmx 1.4.0rc1__py3-none-any.whl → 1.5.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/qb/service.py +3 -34
- pysdmx/io/format.py +13 -1
- pysdmx/io/input_processor.py +41 -12
- pysdmx/io/reader.py +33 -3
- pysdmx/io/writer.py +4 -0
- pysdmx/io/xml/__parse_xml.py +24 -1
- pysdmx/io/xml/__structure_aux_reader.py +94 -25
- pysdmx/io/xml/__structure_aux_writer.py +134 -43
- pysdmx/io/xml/__tokens.py +15 -3
- pysdmx/io/xml/__write_aux.py +33 -1
- pysdmx/io/xml/__write_data_aux.py +29 -4
- pysdmx/io/xml/__write_structure_specific_aux.py +42 -2
- pysdmx/io/xml/doc_validation.py +6 -2
- pysdmx/io/xml/sdmx21/reader/generic.py +29 -0
- pysdmx/io/xml/sdmx21/writer/generic.py +67 -1
- pysdmx/io/xml/sdmx31/__init__.py +1 -0
- pysdmx/io/xml/sdmx31/reader/__init__.py +1 -0
- pysdmx/io/xml/sdmx31/reader/structure.py +39 -0
- pysdmx/io/xml/sdmx31/reader/structure_specific.py +39 -0
- pysdmx/io/xml/sdmx31/writer/__init__.py +1 -0
- pysdmx/io/xml/sdmx31/writer/structure.py +67 -0
- pysdmx/io/xml/sdmx31/writer/structure_specific.py +108 -0
- pysdmx/model/code.py +19 -1
- pysdmx/model/dataflow.py +12 -0
- pysdmx/util/__init__.py +2 -0
- pysdmx/util/_model_utils.py +1 -0
- pysdmx/util/_net_utils.py +39 -0
- {pysdmx-1.4.0rc1.dist-info → pysdmx-1.5.0.dist-info}/METADATA +3 -3
- {pysdmx-1.4.0rc1.dist-info → pysdmx-1.5.0.dist-info}/RECORD +32 -24
- {pysdmx-1.4.0rc1.dist-info → pysdmx-1.5.0.dist-info}/LICENSE +0 -0
- {pysdmx-1.4.0rc1.dist-info → pysdmx-1.5.0.dist-info}/WHEEL +0 -0
pysdmx/__init__.py
CHANGED
pysdmx/api/qb/service.py
CHANGED
|
@@ -24,6 +24,7 @@ from pysdmx.api.qb.schema import SchemaFormat, SchemaQuery
|
|
|
24
24
|
from pysdmx.api.qb.structure import StructureFormat, StructureQuery
|
|
25
25
|
from pysdmx.api.qb.util import ApiVersion
|
|
26
26
|
from pysdmx.io.format import GDS_FORMAT
|
|
27
|
+
from pysdmx.util._net_utils import map_httpx_errors
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class _CoreRestService:
|
|
@@ -66,38 +67,6 @@ class _CoreRestService:
|
|
|
66
67
|
}
|
|
67
68
|
self._timeout = timeout
|
|
68
69
|
|
|
69
|
-
def _map_error(
|
|
70
|
-
self, e: Union[httpx.RequestError, httpx.HTTPStatusError]
|
|
71
|
-
) -> NoReturn:
|
|
72
|
-
q = e.request.url
|
|
73
|
-
if isinstance(e, httpx.HTTPStatusError):
|
|
74
|
-
s = e.response.status_code
|
|
75
|
-
t = e.response.text
|
|
76
|
-
if s == 404:
|
|
77
|
-
msg = (
|
|
78
|
-
"The requested resource(s) could not be found in the "
|
|
79
|
-
f"targeted service. The query was `{q}`"
|
|
80
|
-
)
|
|
81
|
-
raise errors.NotFound("Not found", msg) from e
|
|
82
|
-
elif s < 500:
|
|
83
|
-
msg = (
|
|
84
|
-
f"The query returned a {s} error code. The query "
|
|
85
|
-
f"was `{q}`. The error message was: `{t}`."
|
|
86
|
-
)
|
|
87
|
-
raise errors.Invalid(f"Client error {s}", msg) from e
|
|
88
|
-
else:
|
|
89
|
-
msg = (
|
|
90
|
-
f"The service returned a {s} error code. The query "
|
|
91
|
-
f"was `{q}`. The error message was: `{t}`."
|
|
92
|
-
)
|
|
93
|
-
raise errors.InternalError(f"Service error {s}", msg) from e
|
|
94
|
-
else:
|
|
95
|
-
msg = (
|
|
96
|
-
f"There was an issue connecting to the targeted service. "
|
|
97
|
-
f"The query was `{q}`. The error message was: `{e}`."
|
|
98
|
-
)
|
|
99
|
-
raise errors.Unavailable("Connection error", msg) from e
|
|
100
|
-
|
|
101
70
|
|
|
102
71
|
class RestService(_CoreRestService):
|
|
103
72
|
"""Synchronous connector to SDMX-REST services.
|
|
@@ -206,7 +175,7 @@ class RestService(_CoreRestService):
|
|
|
206
175
|
r.raise_for_status()
|
|
207
176
|
return r.content
|
|
208
177
|
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
|
209
|
-
|
|
178
|
+
map_httpx_errors(e)
|
|
210
179
|
|
|
211
180
|
|
|
212
181
|
class AsyncRestService(_CoreRestService):
|
|
@@ -322,7 +291,7 @@ class AsyncRestService(_CoreRestService):
|
|
|
322
291
|
r.raise_for_status()
|
|
323
292
|
return r.content
|
|
324
293
|
except (httpx.RequestError, httpx.HTTPStatusError) as e:
|
|
325
|
-
|
|
294
|
+
map_httpx_errors(e)
|
|
326
295
|
|
|
327
296
|
|
|
328
297
|
class _CoreGdsRestService:
|
pysdmx/io/format.py
CHANGED
|
@@ -19,20 +19,25 @@ class Format(Enum):
|
|
|
19
19
|
f"{_BASE}structurespecifictimeseriesdata+xml;version=2.1"
|
|
20
20
|
)
|
|
21
21
|
DATA_SDMX_ML_3_0 = f"{_BASE}data+xml;version=3.0.0"
|
|
22
|
+
DATA_SDMX_ML_3_1 = f"{_BASE}data+xml;version=3.1.0"
|
|
22
23
|
GDS_JSON = "application/json"
|
|
23
24
|
REFMETA_SDMX_CSV_2_0_0 = f"{_BASE}metadata+csv;version=2.0.0"
|
|
24
25
|
REFMETA_SDMX_JSON_2_0_0 = f"{_BASE}metadata+json;version=2.0.0"
|
|
25
26
|
REFMETA_SDMX_ML_3_0 = f"{_BASE}metadata+xml;version=3.0.0"
|
|
27
|
+
REFMETA_SDMX_ML_3_1 = f"{_BASE}metadata+xml;version=3.1.0"
|
|
26
28
|
REGISTRY_SDMX_ML_2_1 = f"{_BASE}registry+xml;version=2.1"
|
|
27
|
-
REGISTRY_SDMX_ML_3_0 = f"{_BASE}registry+xml;version=3.0"
|
|
29
|
+
REGISTRY_SDMX_ML_3_0 = f"{_BASE}registry+xml;version=3.0.0"
|
|
30
|
+
REGISTRY_SDMX_ML_3_1 = f"{_BASE}registry+xml;version=3.1.0"
|
|
28
31
|
SCHEMA_SDMX_JSON_1_0_0 = f"{_BASE}schema+json;version=1.0.0"
|
|
29
32
|
SCHEMA_SDMX_JSON_2_0_0 = f"{_BASE}schema+json;version=2.0.0"
|
|
30
33
|
SCHEMA_SDMX_ML_2_1 = f"{_BASE}schema+xml;version=2.1"
|
|
31
34
|
SCHEMA_SDMX_ML_3_0 = f"{_BASE}schema+xml;version=3.0.0"
|
|
35
|
+
SCHEMA_SDMX_ML_3_1 = f"{_BASE}schema+xml;version=3.1.0"
|
|
32
36
|
STRUCTURE_SDMX_JSON_1_0_0 = f"{_BASE}structure+json;version=1.0.0"
|
|
33
37
|
STRUCTURE_SDMX_JSON_2_0_0 = f"{_BASE}structure+json;version=2.0.0"
|
|
34
38
|
STRUCTURE_SDMX_ML_2_1 = f"{_BASE}structure+xml;version=2.1"
|
|
35
39
|
STRUCTURE_SDMX_ML_3_0 = f"{_BASE}structure+xml;version=3.0.0"
|
|
40
|
+
STRUCTURE_SDMX_ML_3_1 = f"{_BASE}structure+xml;version=3.1.0"
|
|
36
41
|
ERROR_SDMX_ML_2_1 = "application/error.xml"
|
|
37
42
|
FUSION_JSON = "application/vnd.fusion.json"
|
|
38
43
|
|
|
@@ -44,6 +49,7 @@ class AvailabilityFormat(Enum):
|
|
|
44
49
|
SDMX_JSON_2_0_0 = Format.STRUCTURE_SDMX_JSON_2_0_0.value
|
|
45
50
|
SDMX_ML_2_1 = Format.STRUCTURE_SDMX_ML_2_1.value
|
|
46
51
|
SDMX_ML_3_0 = Format.STRUCTURE_SDMX_ML_3_0.value
|
|
52
|
+
SDMX_ML_3_1 = Format.STRUCTURE_SDMX_ML_3_1.value
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
class DataFormat(Enum):
|
|
@@ -58,6 +64,7 @@ class DataFormat(Enum):
|
|
|
58
64
|
SDMX_ML_2_1_STR = Format.DATA_SDMX_ML_2_1_STR.value
|
|
59
65
|
SDMX_ML_2_1_STRTS = Format.DATA_SDMX_ML_2_1_STRTS.value
|
|
60
66
|
SDMX_ML_3_0 = Format.DATA_SDMX_ML_3_0.value
|
|
67
|
+
SDMX_ML_3_1 = Format.DATA_SDMX_ML_3_1.value
|
|
61
68
|
|
|
62
69
|
|
|
63
70
|
class RefMetaFormat(Enum):
|
|
@@ -66,6 +73,7 @@ class RefMetaFormat(Enum):
|
|
|
66
73
|
SDMX_CSV_2_0_0 = Format.REFMETA_SDMX_CSV_2_0_0.value
|
|
67
74
|
SDMX_JSON_2_0_0 = Format.REFMETA_SDMX_JSON_2_0_0.value
|
|
68
75
|
SDMX_ML_3_0 = Format.REFMETA_SDMX_ML_3_0.value
|
|
76
|
+
SDMX_ML_3_1 = Format.REFMETA_SDMX_ML_3_1.value
|
|
69
77
|
FUSION_JSON = Format.FUSION_JSON.value
|
|
70
78
|
|
|
71
79
|
|
|
@@ -80,6 +88,8 @@ class SchemaFormat(Enum):
|
|
|
80
88
|
SDMX_ML_3_0_SCHEMA = Format.SCHEMA_SDMX_ML_3_0.value
|
|
81
89
|
SDMX_ML_2_1_STRUCTURE = Format.STRUCTURE_SDMX_ML_2_1.value
|
|
82
90
|
SDMX_ML_3_0_STRUCTURE = Format.STRUCTURE_SDMX_ML_3_0.value
|
|
91
|
+
SDMX_ML_3_1_STRUCTURE = Format.STRUCTURE_SDMX_ML_3_1.value
|
|
92
|
+
|
|
83
93
|
FUSION_JSON = Format.FUSION_JSON.value
|
|
84
94
|
|
|
85
95
|
|
|
@@ -90,6 +100,7 @@ class StructureFormat(Enum):
|
|
|
90
100
|
SDMX_JSON_2_0_0 = Format.STRUCTURE_SDMX_JSON_2_0_0.value
|
|
91
101
|
SDMX_ML_2_1 = Format.STRUCTURE_SDMX_ML_2_1.value
|
|
92
102
|
SDMX_ML_3_0 = Format.STRUCTURE_SDMX_ML_3_0.value
|
|
103
|
+
SDMX_ML_3_1 = Format.STRUCTURE_SDMX_ML_3_1.value
|
|
93
104
|
FUSION_JSON = Format.FUSION_JSON.value
|
|
94
105
|
|
|
95
106
|
|
|
@@ -98,6 +109,7 @@ class RegistryFormat(Enum):
|
|
|
98
109
|
|
|
99
110
|
SDMX_ML_2_1 = Format.REGISTRY_SDMX_ML_2_1.value
|
|
100
111
|
SDMX_ML_3_0 = Format.REGISTRY_SDMX_ML_3_0.value
|
|
112
|
+
SDMX_ML_3_1 = Format.REGISTRY_SDMX_ML_3_1.value
|
|
101
113
|
FUSION_JSON = Format.FUSION_JSON.value
|
|
102
114
|
|
|
103
115
|
|
pysdmx/io/input_processor.py
CHANGED
|
@@ -6,12 +6,15 @@ from io import BytesIO, StringIO, TextIOWrapper
|
|
|
6
6
|
from json import JSONDecodeError, loads
|
|
7
7
|
from os import PathLike
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Tuple, Union
|
|
9
|
+
from typing import Optional, Tuple, Union
|
|
10
10
|
|
|
11
|
-
from httpx import
|
|
11
|
+
from httpx import Client as httpx_Client
|
|
12
|
+
from httpx import HTTPStatusError, create_ssl_context
|
|
12
13
|
|
|
13
14
|
from pysdmx.errors import Invalid, NotImplemented
|
|
14
15
|
from pysdmx.io.format import Format
|
|
16
|
+
from pysdmx.io.xml.__parse_xml import SCHEMA_ROOT_31
|
|
17
|
+
from pysdmx.util import map_httpx_errors
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
def __remove_bom(input_string: str) -> str:
|
|
@@ -58,11 +61,15 @@ def __get_sdmx_ml_flavour(input_str: str) -> Tuple[str, Format]:
|
|
|
58
61
|
if ":structurespecificdata" in flavour_check:
|
|
59
62
|
if SCHEMA_ROOT_30 in flavour_check:
|
|
60
63
|
return input_str, Format.DATA_SDMX_ML_3_0
|
|
64
|
+
elif SCHEMA_ROOT_31 in flavour_check:
|
|
65
|
+
return input_str, Format.DATA_SDMX_ML_3_1
|
|
61
66
|
else:
|
|
62
67
|
return input_str, Format.DATA_SDMX_ML_2_1_STR
|
|
63
68
|
if ":structure" in flavour_check:
|
|
64
69
|
if SCHEMA_ROOT_30 in flavour_check:
|
|
65
70
|
return input_str, Format.STRUCTURE_SDMX_ML_3_0
|
|
71
|
+
elif SCHEMA_ROOT_31 in flavour_check:
|
|
72
|
+
return input_str, Format.STRUCTURE_SDMX_ML_3_1
|
|
66
73
|
else:
|
|
67
74
|
return input_str, Format.STRUCTURE_SDMX_ML_2_1
|
|
68
75
|
if ":registryinterface" in flavour_check:
|
|
@@ -92,8 +99,9 @@ def __check_sdmx_str(input_str: str) -> Tuple[str, Format]:
|
|
|
92
99
|
raise Invalid("Validation Error", "Cannot parse input as SDMX.")
|
|
93
100
|
|
|
94
101
|
|
|
95
|
-
def process_string_to_read(
|
|
102
|
+
def process_string_to_read( # noqa: C901
|
|
96
103
|
sdmx_document: Union[str, Path, BytesIO],
|
|
104
|
+
pem: Optional[Union[str, Path]] = None,
|
|
97
105
|
) -> Tuple[str, Format]:
|
|
98
106
|
"""Processes the input that comes into read_sdmx function.
|
|
99
107
|
|
|
@@ -102,6 +110,7 @@ def process_string_to_read(
|
|
|
102
110
|
|
|
103
111
|
Args:
|
|
104
112
|
sdmx_document: Path to file, URL, or string.
|
|
113
|
+
pem: Path to a PEM file for SSL verification when reading from a URL.
|
|
105
114
|
|
|
106
115
|
Returns:
|
|
107
116
|
tuple: Tuple containing the parsed input and the format of the input.
|
|
@@ -127,19 +136,39 @@ def process_string_to_read(
|
|
|
127
136
|
|
|
128
137
|
elif isinstance(sdmx_document, str):
|
|
129
138
|
if sdmx_document.startswith("http"):
|
|
139
|
+
if pem is not None and isinstance(pem, Path):
|
|
140
|
+
pem = str(pem)
|
|
141
|
+
if pem is not None and pem and not os.path.exists(pem):
|
|
142
|
+
raise Invalid(
|
|
143
|
+
"Validation Error",
|
|
144
|
+
f"PEM file {pem} does not exist.",
|
|
145
|
+
)
|
|
146
|
+
# Read from URL
|
|
147
|
+
ssl_context = (
|
|
148
|
+
create_ssl_context(
|
|
149
|
+
verify=pem,
|
|
150
|
+
)
|
|
151
|
+
if pem
|
|
152
|
+
else create_ssl_context()
|
|
153
|
+
)
|
|
130
154
|
try:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
response.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
out_str = ""
|
|
156
|
+
with httpx_Client(verify=ssl_context) as client:
|
|
157
|
+
response = client.get(sdmx_document, timeout=60)
|
|
158
|
+
try:
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
except HTTPStatusError as e:
|
|
161
|
+
if "<?xml" in e.response.text:
|
|
162
|
+
out_str = e.response.text
|
|
163
|
+
else:
|
|
164
|
+
map_httpx_errors(e)
|
|
165
|
+
else:
|
|
166
|
+
out_str = response.text
|
|
167
|
+
except Exception as e:
|
|
139
168
|
raise Invalid(
|
|
140
169
|
"Validation Error",
|
|
141
170
|
f"Cannot retrieve a SDMX Message "
|
|
142
|
-
f"from URL: {sdmx_document}.",
|
|
171
|
+
f"from URL: {sdmx_document}. Error message: {e}",
|
|
143
172
|
) from None
|
|
144
173
|
else:
|
|
145
174
|
out_str = sdmx_document
|
pysdmx/io/reader.py
CHANGED
|
@@ -20,6 +20,7 @@ from pysdmx.util._model_utils import schema_generator
|
|
|
20
20
|
def read_sdmx( # noqa: C901
|
|
21
21
|
sdmx_document: Union[str, Path, BytesIO],
|
|
22
22
|
validate: bool = True,
|
|
23
|
+
pem: Optional[Union[str, Path]] = None,
|
|
23
24
|
) -> Message:
|
|
24
25
|
"""Reads any SDMX message and extracts its content.
|
|
25
26
|
|
|
@@ -30,11 +31,15 @@ def read_sdmx( # noqa: C901
|
|
|
30
31
|
(`pathlib.Path <https://docs.python.org/3/library/pathlib.html>`_),
|
|
31
32
|
URL, or string.
|
|
32
33
|
validate: Validate the input file (only for SDMX-ML).
|
|
34
|
+
pem: When using a URL, in case the service exposed
|
|
35
|
+
a certificate created by an unknown certificate
|
|
36
|
+
authority, you can pass a PEM file for this
|
|
37
|
+
authority using this parameter.
|
|
33
38
|
|
|
34
39
|
Raises:
|
|
35
40
|
Invalid: If the file is empty or the format is not supported.
|
|
36
41
|
"""
|
|
37
|
-
input_str, read_format = process_string_to_read(sdmx_document)
|
|
42
|
+
input_str, read_format = process_string_to_read(sdmx_document, pem=pem)
|
|
38
43
|
|
|
39
44
|
header = None
|
|
40
45
|
result_data: Sequence[Dataset] = []
|
|
@@ -60,6 +65,15 @@ def read_sdmx( # noqa: C901
|
|
|
60
65
|
header = read_header(input_str, validate=validate)
|
|
61
66
|
# SDMX-ML 3.0 Structure
|
|
62
67
|
result_structures = read_structure(input_str, validate=validate)
|
|
68
|
+
elif read_format == Format.STRUCTURE_SDMX_ML_3_1:
|
|
69
|
+
from pysdmx.io.xml.header import read as read_header
|
|
70
|
+
from pysdmx.io.xml.sdmx31.reader.structure import (
|
|
71
|
+
read as read_structure,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
header = read_header(input_str, validate=validate)
|
|
75
|
+
# SDMX-ML 3.1 Structure
|
|
76
|
+
result_structures = read_structure(input_str, validate=validate)
|
|
63
77
|
elif read_format == Format.DATA_SDMX_ML_2_1_GEN:
|
|
64
78
|
from pysdmx.io.xml.header import read as read_header
|
|
65
79
|
from pysdmx.io.xml.sdmx21.reader.generic import read as read_generic
|
|
@@ -97,6 +111,16 @@ def read_sdmx( # noqa: C901
|
|
|
97
111
|
|
|
98
112
|
# SDMX-ML 3.0 Structure Specific Data
|
|
99
113
|
result_data = read_str_spe(input_str, validate=validate)
|
|
114
|
+
elif read_format == Format.DATA_SDMX_ML_3_1:
|
|
115
|
+
from pysdmx.io.xml.header import read as read_header
|
|
116
|
+
from pysdmx.io.xml.sdmx31.reader.structure_specific import (
|
|
117
|
+
read as read_str_spe,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
header = read_header(input_str, validate=validate)
|
|
121
|
+
|
|
122
|
+
# SDMX-ML 3.1 Structure Specific Data
|
|
123
|
+
result_data = read_str_spe(input_str, validate=validate)
|
|
100
124
|
elif read_format == Format.DATA_SDMX_CSV_1_0_0:
|
|
101
125
|
from pysdmx.io.csv.sdmx10.reader import read as read_csv_v1
|
|
102
126
|
|
|
@@ -118,6 +142,7 @@ def read_sdmx( # noqa: C901
|
|
|
118
142
|
Format.DATA_SDMX_ML_2_1_GEN,
|
|
119
143
|
Format.DATA_SDMX_ML_2_1_STR,
|
|
120
144
|
Format.DATA_SDMX_ML_3_0,
|
|
145
|
+
Format.DATA_SDMX_ML_3_1,
|
|
121
146
|
):
|
|
122
147
|
# TODO: Add here the Schema download for Datasets, based on structure
|
|
123
148
|
# TODO: Ensure we have changed the signature of the data readers
|
|
@@ -170,6 +195,7 @@ def get_datasets(
|
|
|
170
195
|
data: Union[str, Path, BytesIO],
|
|
171
196
|
structure: Optional[Union[str, Path, BytesIO]] = None,
|
|
172
197
|
validate: bool = True,
|
|
198
|
+
pem: Optional[Union[str, Path]] = None,
|
|
173
199
|
) -> Sequence[Dataset]:
|
|
174
200
|
"""Reads a data message and a structure message and returns a dataset.
|
|
175
201
|
|
|
@@ -194,6 +220,10 @@ def get_datasets(
|
|
|
194
220
|
(`pathlib.Path <https://docs.python.org/3/library/pathlib.html>`_),
|
|
195
221
|
URL, or string for the structure message, if needed.
|
|
196
222
|
validate: Validate the input file (only for SDMX-ML).
|
|
223
|
+
pem: When using a URL, in case the service exposed
|
|
224
|
+
a certificate created by an unknown certificate
|
|
225
|
+
authority, you can pass a PEM file for this
|
|
226
|
+
authority using this parameter.
|
|
197
227
|
|
|
198
228
|
Raises:
|
|
199
229
|
Invalid:
|
|
@@ -203,13 +233,13 @@ def get_datasets(
|
|
|
203
233
|
If the related data structure (or dataflow with its children)
|
|
204
234
|
is not found.
|
|
205
235
|
"""
|
|
206
|
-
data_msg = read_sdmx(data, validate=validate)
|
|
236
|
+
data_msg = read_sdmx(data, validate=validate, pem=pem)
|
|
207
237
|
if not data_msg.data:
|
|
208
238
|
raise Invalid("No data found in the data message")
|
|
209
239
|
|
|
210
240
|
if structure is None:
|
|
211
241
|
return data_msg.data
|
|
212
|
-
structure_msg = read_sdmx(structure, validate=validate)
|
|
242
|
+
structure_msg = read_sdmx(structure, validate=validate, pem=pem)
|
|
213
243
|
if structure_msg.structures is None:
|
|
214
244
|
raise Invalid("No structure found in the structure message")
|
|
215
245
|
|
pysdmx/io/writer.py
CHANGED
|
@@ -19,11 +19,15 @@ WRITERS = {
|
|
|
19
19
|
Format.DATA_SDMX_ML_3_0: "pysdmx.io.xml.sdmx30.writer."
|
|
20
20
|
"structure_specific",
|
|
21
21
|
Format.STRUCTURE_SDMX_ML_3_0: "pysdmx.io.xml.sdmx30.writer.structure",
|
|
22
|
+
Format.DATA_SDMX_ML_3_1: "pysdmx.io.xml.sdmx31.writer."
|
|
23
|
+
"structure_specific",
|
|
24
|
+
Format.STRUCTURE_SDMX_ML_3_1: "pysdmx.io.xml.sdmx31.writer.structure",
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
STRUCTURE_WRITERS = (
|
|
25
28
|
Format.STRUCTURE_SDMX_ML_2_1,
|
|
26
29
|
Format.STRUCTURE_SDMX_ML_3_0,
|
|
30
|
+
Format.STRUCTURE_SDMX_ML_3_1,
|
|
27
31
|
)
|
|
28
32
|
|
|
29
33
|
|
pysdmx/io/xml/__parse_xml.py
CHANGED
|
@@ -41,6 +41,24 @@ XML_OPTIONS_30 = {
|
|
|
41
41
|
"dict_constructor": dict,
|
|
42
42
|
"attr_prefix": "",
|
|
43
43
|
}
|
|
44
|
+
SCHEMA_ROOT_31 = "http://www.sdmx.org/resources/sdmxml/schemas/v3_1/"
|
|
45
|
+
NAMESPACES_31 = {
|
|
46
|
+
SCHEMA_ROOT_31 + "message": None,
|
|
47
|
+
SCHEMA_ROOT_31 + "common": None,
|
|
48
|
+
SCHEMA_ROOT_31 + "structure": None,
|
|
49
|
+
"http://www.w3.org/2001/XMLSchema-instance": "xsi",
|
|
50
|
+
"http://www.w3.org/XML/1998/namespace": None,
|
|
51
|
+
SCHEMA_ROOT_31 + "data/structurespecific": None,
|
|
52
|
+
SCHEMA_ROOT_31 + "registry": None,
|
|
53
|
+
"http://schemas.xmlsoap.org/soap/envelope/": None,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
XML_OPTIONS_31 = {
|
|
57
|
+
"process_namespaces": True,
|
|
58
|
+
"namespaces": NAMESPACES_31,
|
|
59
|
+
"dict_constructor": dict,
|
|
60
|
+
"attr_prefix": "",
|
|
61
|
+
}
|
|
44
62
|
|
|
45
63
|
|
|
46
64
|
def parse_xml(
|
|
@@ -61,7 +79,12 @@ def parse_xml(
|
|
|
61
79
|
"""
|
|
62
80
|
if validate:
|
|
63
81
|
validate_doc(input_str)
|
|
64
|
-
if
|
|
82
|
+
if SCHEMA_ROOT_31 in input_str:
|
|
83
|
+
dict_info = xmltodict.parse(
|
|
84
|
+
input_str,
|
|
85
|
+
**XML_OPTIONS_31, # type: ignore[arg-type]
|
|
86
|
+
)
|
|
87
|
+
elif SCHEMA_ROOT_30 in input_str:
|
|
65
88
|
dict_info = xmltodict.parse(
|
|
66
89
|
input_str,
|
|
67
90
|
**XML_OPTIONS_30, # type: ignore[arg-type]
|
|
@@ -51,6 +51,7 @@ from pysdmx.io.xml.__tokens import (
|
|
|
51
51
|
DFWS,
|
|
52
52
|
DIM,
|
|
53
53
|
DIM_LIST,
|
|
54
|
+
DIM_REF,
|
|
54
55
|
DSD,
|
|
55
56
|
DSD_COMPS,
|
|
56
57
|
DSDS,
|
|
@@ -64,6 +65,7 @@ from pysdmx.io.xml.__tokens import (
|
|
|
64
65
|
FAXES,
|
|
65
66
|
GROUP,
|
|
66
67
|
GROUP_DIM,
|
|
68
|
+
GROUPS_LOW,
|
|
67
69
|
ID,
|
|
68
70
|
IS_EXTERNAL_REF,
|
|
69
71
|
IS_EXTERNAL_REF_LOW,
|
|
@@ -82,6 +84,7 @@ from pysdmx.io.xml.__tokens import (
|
|
|
82
84
|
ME_REL,
|
|
83
85
|
MEASURE,
|
|
84
86
|
METADATA,
|
|
87
|
+
MSR,
|
|
85
88
|
NAME,
|
|
86
89
|
NAME_PER,
|
|
87
90
|
NAME_PER_SCHEME,
|
|
@@ -91,7 +94,6 @@ from pysdmx.io.xml.__tokens import (
|
|
|
91
94
|
ORGS,
|
|
92
95
|
PAR_ID,
|
|
93
96
|
PAR_VER,
|
|
94
|
-
PRIM_MEASURE,
|
|
95
97
|
REF,
|
|
96
98
|
REQUIRED,
|
|
97
99
|
ROLE,
|
|
@@ -131,6 +133,7 @@ from pysdmx.io.xml.__tokens import (
|
|
|
131
133
|
VALID_TO_LOW,
|
|
132
134
|
VALUE_ITEM,
|
|
133
135
|
VALUE_LIST,
|
|
136
|
+
VALUE_LIST_LOW,
|
|
134
137
|
VALUE_LISTS,
|
|
135
138
|
VERSION,
|
|
136
139
|
VTL_CL_MAPP,
|
|
@@ -167,6 +170,7 @@ from pysdmx.model.dataflow import (
|
|
|
167
170
|
Components,
|
|
168
171
|
Dataflow,
|
|
169
172
|
DataStructureDefinition,
|
|
173
|
+
GroupDimension,
|
|
170
174
|
Role,
|
|
171
175
|
)
|
|
172
176
|
from pysdmx.model.vtl import (
|
|
@@ -216,13 +220,13 @@ ITEMS_CLASSES = {
|
|
|
216
220
|
CUSTOM_TYPE: CustomType,
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
COMP_TYPES = [DIM, ATT,
|
|
223
|
+
COMP_TYPES = [DIM, ATT, MEASURE, MSR, GROUP_DIM]
|
|
220
224
|
|
|
221
225
|
ROLE_MAPPING = {
|
|
222
226
|
DIM: Role.DIMENSION,
|
|
223
227
|
ATT: Role.ATTRIBUTE,
|
|
224
|
-
PRIM_MEASURE: Role.MEASURE,
|
|
225
228
|
MEASURE: Role.MEASURE,
|
|
229
|
+
MSR: Role.MEASURE,
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
FACETS_MAPPING = {
|
|
@@ -291,6 +295,7 @@ class StructureParser(Struct):
|
|
|
291
295
|
|
|
292
296
|
agencies: Dict[str, AgencyScheme] = {}
|
|
293
297
|
codelists: Dict[str, Codelist] = {}
|
|
298
|
+
valuelists: Dict[str, Codelist] = {}
|
|
294
299
|
concepts: Dict[str, ConceptScheme] = {}
|
|
295
300
|
datastructures: Dict[str, DataStructureDefinition] = {}
|
|
296
301
|
dataflows: Dict[str, Dataflow] = {}
|
|
@@ -466,20 +471,28 @@ class StructureParser(Struct):
|
|
|
466
471
|
if TEXT_FORMAT in json_rep:
|
|
467
472
|
self.__format_facets(json_rep[TEXT_FORMAT], json_obj)
|
|
468
473
|
|
|
469
|
-
if ENUM in json_rep and
|
|
474
|
+
if ENUM in json_rep and (
|
|
475
|
+
len(self.codelists) > 0 or len(self.valuelists) > 0
|
|
476
|
+
):
|
|
470
477
|
enum = json_rep[ENUM]
|
|
471
478
|
if isinstance(enum, str):
|
|
472
479
|
ref = parse_urn(enum)
|
|
473
480
|
else:
|
|
474
481
|
ref = enum.get(REF, enum)
|
|
475
|
-
|
|
476
482
|
if isinstance(ref, dict) and "URN" in ref:
|
|
477
483
|
codelist = find_by_urn(
|
|
478
484
|
list(self.codelists.values()), ref["URN"]
|
|
479
485
|
)
|
|
480
486
|
|
|
481
487
|
elif isinstance(ref, Reference):
|
|
482
|
-
codelist = find_by_urn(
|
|
488
|
+
codelist = find_by_urn(
|
|
489
|
+
list(
|
|
490
|
+
self.codelists.values()
|
|
491
|
+
if ref.sdmx_type == CL
|
|
492
|
+
else self.valuelists.values()
|
|
493
|
+
),
|
|
494
|
+
str(ref),
|
|
495
|
+
)
|
|
483
496
|
else:
|
|
484
497
|
short_urn = str(
|
|
485
498
|
Reference(
|
|
@@ -541,7 +554,7 @@ class StructureParser(Struct):
|
|
|
541
554
|
for con in concept_scheme.concepts:
|
|
542
555
|
if isinstance(concept_ref, str):
|
|
543
556
|
if con.id == item_reference.item_id:
|
|
544
|
-
rep[CON] =
|
|
557
|
+
rep[CON] = parse_urn(concept_ref)
|
|
545
558
|
break
|
|
546
559
|
elif con.id == concept_ref[ID]:
|
|
547
560
|
rep[CON] = con
|
|
@@ -551,7 +564,9 @@ class StructureParser(Struct):
|
|
|
551
564
|
return rep
|
|
552
565
|
|
|
553
566
|
@staticmethod
|
|
554
|
-
def __get_attachment_level(
|
|
567
|
+
def __get_attachment_level( # noqa: C901
|
|
568
|
+
attribute: Dict[str, Any], element_info: Dict[str, Any]
|
|
569
|
+
) -> str:
|
|
555
570
|
if DIM in attribute:
|
|
556
571
|
dims = add_list(attribute[DIM])
|
|
557
572
|
if dims and isinstance(dims[0], dict):
|
|
@@ -561,15 +576,39 @@ class StructureParser(Struct):
|
|
|
561
576
|
# therefore we need to check first if a Dimension is present,
|
|
562
577
|
# then the AttachmentGroup
|
|
563
578
|
if ATTACH_GROUP in attribute:
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
579
|
+
att_grp = add_list(attribute[ATTACH_GROUP])
|
|
580
|
+
att_grp = [att[REF][ID] for att in att_grp]
|
|
581
|
+
for grp in att_grp:
|
|
582
|
+
group_dims: List[str] = next(
|
|
583
|
+
(
|
|
584
|
+
g.dimensions
|
|
585
|
+
for g in element_info[GROUPS_LOW]
|
|
586
|
+
if g.id == grp
|
|
587
|
+
),
|
|
588
|
+
[],
|
|
589
|
+
)
|
|
590
|
+
att_level += (
|
|
591
|
+
"," + ",".join(group_dims)
|
|
592
|
+
if len(group_dims) > 0
|
|
593
|
+
else ""
|
|
594
|
+
)
|
|
568
595
|
elif GROUP in attribute:
|
|
569
|
-
|
|
570
|
-
|
|
596
|
+
if REF in attribute[GROUP]:
|
|
597
|
+
group_id = attribute[GROUP][REF][ID]
|
|
598
|
+
else:
|
|
599
|
+
group_id = attribute[GROUP]
|
|
600
|
+
group_dimensions: List[str] = next(
|
|
601
|
+
(
|
|
602
|
+
g.dimensions
|
|
603
|
+
for g in element_info[GROUPS_LOW]
|
|
604
|
+
if g.id == group_id
|
|
605
|
+
),
|
|
606
|
+
[],
|
|
571
607
|
)
|
|
572
|
-
|
|
608
|
+
att_level = (
|
|
609
|
+
",".join(group_dimensions) if len(group_dimensions) > 0 else ""
|
|
610
|
+
)
|
|
611
|
+
elif OBSERVATION in attribute or MEASURE in attribute:
|
|
573
612
|
att_level = "O"
|
|
574
613
|
else:
|
|
575
614
|
# For None (SDMX-2.1) or Dataflow (SDMX-3.0), attribute is
|
|
@@ -682,7 +721,7 @@ class StructureParser(Struct):
|
|
|
682
721
|
json_obj[DFW_LOW] = dataflow
|
|
683
722
|
|
|
684
723
|
def __format_component(
|
|
685
|
-
self, comp: Dict[str, Any], role: Role
|
|
724
|
+
self, comp: Dict[str, Any], role: Role, element_info: Dict[str, Any]
|
|
686
725
|
) -> Component:
|
|
687
726
|
comp[ROLE.lower()] = role
|
|
688
727
|
comp[REQUIRED] = True
|
|
@@ -700,7 +739,9 @@ class StructureParser(Struct):
|
|
|
700
739
|
|
|
701
740
|
# Attribute Handling
|
|
702
741
|
if ATT_REL in comp:
|
|
703
|
-
comp[ATT_LVL] = self.__get_attachment_level(
|
|
742
|
+
comp[ATT_LVL] = self.__get_attachment_level(
|
|
743
|
+
comp[ATT_REL], element_info
|
|
744
|
+
)
|
|
704
745
|
del comp[ATT_REL]
|
|
705
746
|
|
|
706
747
|
if ME_REL in comp:
|
|
@@ -727,7 +768,7 @@ class StructureParser(Struct):
|
|
|
727
768
|
return Component(**comp)
|
|
728
769
|
|
|
729
770
|
def __format_component_lists(
|
|
730
|
-
self, element: Dict[str, Any]
|
|
771
|
+
self, element: Dict[str, Any], element_info: Dict[str, Any]
|
|
731
772
|
) -> List[Component]:
|
|
732
773
|
comp_list = []
|
|
733
774
|
|
|
@@ -744,6 +785,7 @@ class StructureParser(Struct):
|
|
|
744
785
|
formatted_comp = self.__format_component(
|
|
745
786
|
comp,
|
|
746
787
|
role,
|
|
788
|
+
element_info,
|
|
747
789
|
)
|
|
748
790
|
comp_list.append(formatted_comp)
|
|
749
791
|
|
|
@@ -754,12 +796,11 @@ class StructureParser(Struct):
|
|
|
754
796
|
element[COMPS] = []
|
|
755
797
|
comps = element[DSD_COMPS]
|
|
756
798
|
|
|
757
|
-
for comp_list in [DIM_LIST, ME_LIST,
|
|
758
|
-
if comp_list
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
fmt_comps = self.__format_component_lists(comps[comp_list])
|
|
799
|
+
for comp_list in [DIM_LIST, ME_LIST, ATT_LIST]:
|
|
800
|
+
if comp_list in comps:
|
|
801
|
+
fmt_comps = self.__format_component_lists(
|
|
802
|
+
comps[comp_list], element
|
|
803
|
+
)
|
|
763
804
|
element[COMPS].extend(fmt_comps)
|
|
764
805
|
|
|
765
806
|
element[COMPS] = Components(element[COMPS])
|
|
@@ -892,6 +933,32 @@ class StructureParser(Struct):
|
|
|
892
933
|
|
|
893
934
|
return ITEMS_CLASSES[item_name_class](**item_json_info)
|
|
894
935
|
|
|
936
|
+
@staticmethod
|
|
937
|
+
def __format_groups(element: Dict[str, Any]) -> Dict[str, Any]:
|
|
938
|
+
if DSD_COMPS in element:
|
|
939
|
+
dsd_comps = element[DSD_COMPS]
|
|
940
|
+
if GROUP in dsd_comps:
|
|
941
|
+
groups = (
|
|
942
|
+
dsd_comps[GROUP]
|
|
943
|
+
if isinstance(dsd_comps[GROUP], list)
|
|
944
|
+
else [dsd_comps[GROUP]]
|
|
945
|
+
)
|
|
946
|
+
for group in groups:
|
|
947
|
+
group_dimensions = group.pop(GROUP_DIM, [])
|
|
948
|
+
if isinstance(group_dimensions, dict):
|
|
949
|
+
group_dimensions = [group_dimensions]
|
|
950
|
+
|
|
951
|
+
group["dimensions"] = [
|
|
952
|
+
d[DIM_REF]
|
|
953
|
+
if isinstance(d[DIM_REF], str)
|
|
954
|
+
else d[DIM_REF][REF][ID]
|
|
955
|
+
for d in group_dimensions
|
|
956
|
+
]
|
|
957
|
+
|
|
958
|
+
element[GROUPS_LOW] = [GroupDimension(**g) for g in groups]
|
|
959
|
+
del element[DSD_COMPS][GROUP]
|
|
960
|
+
return element
|
|
961
|
+
|
|
895
962
|
def __format_is_final_30(
|
|
896
963
|
self, json_elem: Dict[str, Any]
|
|
897
964
|
) -> Dict[str, Any]:
|
|
@@ -940,7 +1007,7 @@ class StructureParser(Struct):
|
|
|
940
1007
|
del element["xmlns"]
|
|
941
1008
|
# Dynamic creation with specific class
|
|
942
1009
|
if scheme == VALUE_LIST:
|
|
943
|
-
element["sdmx_type"] =
|
|
1010
|
+
element["sdmx_type"] = VALUE_LIST_LOW
|
|
944
1011
|
element = self.__format_is_final_30(element)
|
|
945
1012
|
result: ItemScheme = STRUCTURES_MAPPING[scheme](**element)
|
|
946
1013
|
elements[result.short_urn] = result
|
|
@@ -980,6 +1047,7 @@ class StructureParser(Struct):
|
|
|
980
1047
|
element = self.__format_urls(element)
|
|
981
1048
|
element = self.__format_agency(element)
|
|
982
1049
|
element = self.__format_validity(element)
|
|
1050
|
+
element = self.__format_groups(element)
|
|
983
1051
|
element = self.__format_components(element)
|
|
984
1052
|
|
|
985
1053
|
if "xmlns" in element:
|
|
@@ -1060,6 +1128,7 @@ class StructureParser(Struct):
|
|
|
1060
1128
|
lambda data: self.__format_scheme(
|
|
1061
1129
|
data, VALUE_LIST, VALUE_ITEM
|
|
1062
1130
|
),
|
|
1131
|
+
"valuelists",
|
|
1063
1132
|
),
|
|
1064
1133
|
CON_SCHEMES: process_structure(
|
|
1065
1134
|
CON_SCHEMES,
|