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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """Your opinionated Python SDMX library."""
2
2
 
3
- __version__ = "1.4.0rc1"
3
+ __version__ = "1.5.0"
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
- self._map_error(e)
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
- self._map_error(e)
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
 
@@ -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 get as httpx_get
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
- response = httpx_get(sdmx_document, timeout=60)
132
- if (
133
- response.status_code != 200
134
- and "<?xml" not in response.text
135
- ):
136
- raise Exception("Invalid URL, no SDMX Error found")
137
- out_str = response.text
138
- except Exception:
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
 
@@ -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 SCHEMA_ROOT_30 in input_str:
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, PRIM_MEASURE, MEASURE, GROUP_DIM]
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 len(self.codelists) > 0:
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(list(self.codelists.values()), str(ref))
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] = 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(attribute: Dict[str, Any]) -> str:
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
- raise NotImplementedError(
565
- "Attribute relationships with Dimension "
566
- "and AttachmentGroup is not supported."
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
- raise NotImplementedError(
570
- "Attribute relationships with Group is not supported."
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
- elif OBSERVATION in attribute or PRIM_MEASURE in attribute:
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(comp[ATT_REL])
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, GROUP, ATT_LIST]:
758
- if comp_list == GROUP and comp_list in comps:
759
- del comps[GROUP]
760
-
761
- elif comp_list in comps:
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"] = "valuelist"
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,