pysdmx 1.8.0__py3-none-any.whl → 1.9.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/__extras_check.py CHANGED
@@ -55,3 +55,17 @@ def __check_vtl_extra() -> None:
55
55
  " and prettify",
56
56
  )
57
57
  ) from None
58
+
59
+
60
+ def __check_json_extra() -> None:
61
+ try:
62
+ import jsonschema # noqa: F401
63
+ import sdmxschemas # noqa: F401
64
+ except ImportError:
65
+ raise ImportError(
66
+ ERROR_MESSAGE.format(
67
+ extra_name="json",
68
+ extra_desc="the validation of SDMX-JSON Structure Messages "
69
+ "(hint, use validate=False if you don't need validation)",
70
+ )
71
+ ) from None
pysdmx/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Your opinionated Python SDMX library."""
2
2
 
3
- __version__ = "1.8.0"
3
+ __version__ = "1.9.0"
@@ -1,7 +1,7 @@
1
1
  """Collection of Fusion-JSON schemas for categories and category schemes."""
2
2
 
3
3
  from collections import defaultdict
4
- from typing import Dict, Optional, Sequence
4
+ from typing import Dict, Optional, Sequence, Tuple, Union
5
5
 
6
6
  from msgspec import Struct
7
7
 
@@ -11,6 +11,8 @@ from pysdmx.model import (
11
11
  Agency,
12
12
  Category,
13
13
  DataflowRef,
14
+ ItemReference,
15
+ Reference,
14
16
  )
15
17
  from pysdmx.model import (
16
18
  Categorisation as CT,
@@ -21,7 +23,7 @@ from pysdmx.model import (
21
23
  from pysdmx.model import (
22
24
  Dataflow as DF,
23
25
  )
24
- from pysdmx.util import find_by_urn
26
+ from pysdmx.util import find_by_urn, parse_urn
25
27
 
26
28
 
27
29
  class FusionCategorisation(Struct, frozen=True, rename={"agency": "agencyId"}):
@@ -57,14 +59,46 @@ class FusionCategory(Struct, frozen=True):
57
59
  descriptions: Optional[Sequence[FusionString]] = None
58
60
  items: Sequence["FusionCategory"] = ()
59
61
 
60
- def to_model(self) -> Category:
62
+ def __add_flows(
63
+ self, cni: str, cf: Dict[str, list[DF]]
64
+ ) -> Sequence[DataflowRef]:
65
+ if cni in cf:
66
+ return [
67
+ DataflowRef(
68
+ (
69
+ df.agency.id
70
+ if isinstance(df.agency, Agency)
71
+ else df.agency
72
+ ),
73
+ df.id,
74
+ df.version,
75
+ df.name,
76
+ )
77
+ for df in cf[cni]
78
+ ]
79
+ else:
80
+ return ()
81
+
82
+ def to_model(
83
+ self,
84
+ cat_flows: dict[str, list[DF]],
85
+ cat_other: dict[str, list[Union[ItemReference, Reference]]],
86
+ parent_id: Optional[str] = None,
87
+ ) -> Category:
61
88
  """Converts a FusionCode to a standard code."""
62
89
  description = self.descriptions[0].value if self.descriptions else None
90
+ cni = f"{parent_id}.{self.id}" if parent_id else self.id
91
+ dataflows = self.__add_flows(cni, cat_flows)
92
+ others = cat_other.get(cni, ())
63
93
  return Category(
64
94
  id=self.id,
65
95
  name=self.names[0].value,
66
96
  description=description,
67
- categories=[c.to_model() for c in self.items],
97
+ categories=[
98
+ c.to_model(cat_flows, cat_other, cni) for c in self.items
99
+ ],
100
+ dataflows=dataflows,
101
+ other_references=others,
68
102
  )
69
103
 
70
104
 
@@ -78,16 +112,42 @@ class FusionCategoryScheme(Struct, frozen=True, rename={"agency": "agencyId"}):
78
112
  version: str = "1.0"
79
113
  items: Sequence[FusionCategory] = ()
80
114
 
81
- def to_model(self) -> CS:
115
+ def __group_refs(
116
+ self,
117
+ categorisations: Sequence[FusionCategorisation],
118
+ dataflows: Sequence[FusionDataflow],
119
+ ) -> Tuple[
120
+ dict[str, list[DF]], dict[str, list[Union[ItemReference, Reference]]]
121
+ ]:
122
+ flows: defaultdict[str, list[DF]] = defaultdict(list)
123
+ other: defaultdict[str, list[Union[ItemReference, Reference]]] = (
124
+ defaultdict(list)
125
+ )
126
+ for c in categorisations:
127
+ ref = parse_urn(c.structureReference)
128
+ src = c.categoryReference[c.categoryReference.find(")") + 2 :]
129
+ if ref.sdmx_type == "Dataflow":
130
+ d = find_by_urn(dataflows, c.structureReference)
131
+ flows[src].append(d.to_model())
132
+ else:
133
+ other[src].append(ref)
134
+ return (flows, other)
135
+
136
+ def to_model(
137
+ self,
138
+ categorisations: Sequence[FusionCategorisation] = (),
139
+ dataflows: Sequence[FusionDataflow] = (),
140
+ ) -> CS:
82
141
  """Converts a JsonCodelist to a standard codelist."""
83
142
  description = self.descriptions[0].value if self.descriptions else None
143
+ cat_flows, cat_others = self.__group_refs(categorisations, dataflows)
84
144
  return CS(
85
145
  id=self.id,
86
146
  name=self.names[0].value,
87
147
  agency=self.agency,
88
148
  description=description,
89
149
  version=self.version,
90
- items=[c.to_model() for c in self.items],
150
+ items=[c.to_model(cat_flows, cat_others) for c in self.items],
91
151
  )
92
152
 
93
153
 
@@ -98,43 +158,11 @@ class FusionCategorySchemeMessage(Struct, frozen=True):
98
158
  Categorisation: Sequence[FusionCategorisation] = ()
99
159
  Dataflow: Sequence[FusionDataflow] = ()
100
160
 
101
- def __group_flows(self) -> defaultdict[str, list[DF]]:
102
- out: defaultdict[str, list[DF]] = defaultdict(list)
103
- for c in self.Categorisation:
104
- d = find_by_urn(self.Dataflow, c.structureReference)
105
- src = c.categoryReference[c.categoryReference.find(")") + 2 :]
106
- out[src].append(d.to_model())
107
- return out
108
-
109
- def __add_flows(
110
- self, cat: Category, cni: str, cf: Dict[str, list[DF]]
111
- ) -> None:
112
- if cat.categories:
113
- for c in cat.categories:
114
- self.__add_flows(c, f"{cni}.{c.id}", cf)
115
- if cni in cf:
116
- dfrefs = [
117
- DataflowRef(
118
- (
119
- df.agency.id
120
- if isinstance(df.agency, Agency)
121
- else df.agency
122
- ),
123
- df.id,
124
- df.version,
125
- df.name,
126
- )
127
- for df in cf[cni]
128
- ]
129
- cat.dataflows = dfrefs
130
-
131
161
  def to_model(self) -> CS:
132
162
  """Returns the requested category scheme."""
133
- cf = self.__group_flows()
134
- cs = self.CategoryScheme[0].to_model()
135
- for c in cs:
136
- self.__add_flows(c, c.id, cf)
137
- return cs
163
+ return self.CategoryScheme[0].to_model(
164
+ self.Categorisation, self.Dataflow
165
+ )
138
166
 
139
167
 
140
168
  class FusionCategorisationMessage(Struct, frozen=True):
@@ -29,7 +29,7 @@ class FusionMetadataAttribute(Struct, frozen=True):
29
29
  id: str
30
30
  concept: str
31
31
  minOccurs: int
32
- maxOccurs: Union[int, Literal["unbounded"]]
32
+ maxOccurs: Union[int, Literal["unbounded"]] = "unbounded"
33
33
  presentational: Optional[bool] = False
34
34
  representation: Optional[FusionRepresentation] = None
35
35
  metadataAttributes: Sequence["FusionMetadataAttribute"] = ()
@@ -1,7 +1,7 @@
1
1
  """Collection of SDMX-JSON schemas for categories and category schemes."""
2
2
 
3
3
  from collections import defaultdict
4
- from typing import Dict, Sequence
4
+ from typing import Dict, Optional, Sequence, Tuple, Union
5
5
 
6
6
  from msgspec import Struct
7
7
 
@@ -20,8 +20,10 @@ from pysdmx.model import (
20
20
  CategoryScheme,
21
21
  Dataflow,
22
22
  DataflowRef,
23
+ ItemReference,
24
+ Reference,
23
25
  )
24
- from pysdmx.util import find_by_urn
26
+ from pysdmx.util import find_by_urn, parse_urn
25
27
 
26
28
 
27
29
  class JsonCategorisation(
@@ -84,14 +86,46 @@ class JsonCategory(NameableType, frozen=True, omit_defaults=True):
84
86
 
85
87
  categories: Sequence["JsonCategory"] = ()
86
88
 
87
- def to_model(self) -> Category:
89
+ def __add_flows(
90
+ self, cni: str, cf: Dict[str, list[Dataflow]]
91
+ ) -> Sequence[DataflowRef]:
92
+ if cni in cf:
93
+ return [
94
+ DataflowRef(
95
+ (
96
+ df.agency.id
97
+ if isinstance(df.agency, Agency)
98
+ else df.agency
99
+ ),
100
+ df.id,
101
+ df.version,
102
+ df.name,
103
+ )
104
+ for df in cf[cni]
105
+ ]
106
+ else:
107
+ return ()
108
+
109
+ def to_model(
110
+ self,
111
+ cat_flows: dict[str, list[Dataflow]],
112
+ cat_other: dict[str, list[Union[ItemReference, Reference]]],
113
+ parent_id: Optional[str] = None,
114
+ ) -> Category:
88
115
  """Converts a FusionCode to a standard code."""
116
+ cni = f"{parent_id}.{self.id}" if parent_id else self.id
117
+ dataflows = self.__add_flows(cni, cat_flows)
118
+ others = cat_other.get(cni, ())
89
119
  return Category(
90
120
  id=self.id,
91
121
  name=self.name,
92
122
  description=self.description,
93
- categories=[c.to_model() for c in self.categories],
123
+ categories=[
124
+ c.to_model(cat_flows, cat_other, cni) for c in self.categories
125
+ ],
94
126
  annotations=[a.to_model() for a in self.annotations],
127
+ dataflows=dataflows,
128
+ other_references=others,
95
129
  )
96
130
 
97
131
  @classmethod
@@ -126,15 +160,42 @@ class JsonCategoryScheme(
126
160
 
127
161
  categories: Sequence[JsonCategory] = ()
128
162
 
129
- def to_model(self) -> CategoryScheme:
130
- """Converts a JsonCodelist to a standard codelist."""
163
+ def __group_refs(
164
+ self,
165
+ categorisations: Sequence[JsonCategorisation] = (),
166
+ dataflows: Sequence[JsonDataflow] = (),
167
+ ) -> Tuple[
168
+ dict[str, list[Dataflow]],
169
+ dict[str, list[Union[ItemReference, Reference]]],
170
+ ]:
171
+ flows: defaultdict[str, list[Dataflow]] = defaultdict(list)
172
+ other: defaultdict[str, list[Union[ItemReference, Reference]]] = (
173
+ defaultdict(list)
174
+ )
175
+ for c in categorisations:
176
+ ref = parse_urn(c.source)
177
+ src = c.target[c.target.find(")") + 2 :]
178
+ if ref.sdmx_type == "Dataflow":
179
+ d = find_by_urn(dataflows, c.source)
180
+ flows[src].append(d.to_model())
181
+ else:
182
+ other[src].append(ref)
183
+ return (flows, other)
184
+
185
+ def to_model(
186
+ self,
187
+ categorisations: Sequence[JsonCategorisation] = (),
188
+ dataflows: Sequence[JsonDataflow] = (),
189
+ ) -> CategoryScheme:
190
+ """Converts a JsonCategoryScheme to a standard one."""
191
+ cat_flows, cat_other = self.__group_refs(categorisations, dataflows)
131
192
  return CategoryScheme(
132
193
  id=self.id,
133
194
  name=self.name,
134
195
  agency=self.agency,
135
196
  description=self.description,
136
197
  version=self.version,
137
- items=[c.to_model() for c in self.categories],
198
+ items=[c.to_model(cat_flows, cat_other) for c in self.categories],
138
199
  is_external_reference=self.isExternalReference,
139
200
  is_partial=self.isPartial,
140
201
  valid_from=self.validFrom,
@@ -179,49 +240,21 @@ class JsonCategorySchemes(Struct, frozen=True, omit_defaults=True):
179
240
  categorisations: Sequence[JsonCategorisation] = ()
180
241
  dataflows: Sequence[JsonDataflow] = ()
181
242
 
243
+ def to_model(self) -> CategoryScheme:
244
+ """Returns the requested codelist."""
245
+ return self.categorySchemes[0].to_model(
246
+ self.categorisations, self.dataflows
247
+ )
248
+
182
249
 
183
250
  class JsonCategorySchemeMessage(Struct, frozen=True, omit_defaults=True):
184
251
  """SDMX-JSON payload for /categoryscheme queries."""
185
252
 
186
253
  data: JsonCategorySchemes
187
254
 
188
- def __group_flows(self) -> defaultdict[str, list[Dataflow]]:
189
- out: defaultdict[str, list[Dataflow]] = defaultdict(list)
190
- for c in self.data.categorisations:
191
- d = find_by_urn(self.data.dataflows, c.source)
192
- src = c.target[c.target.find(")") + 2 :]
193
- out[src].append(d.to_model())
194
- return out
195
-
196
- def __add_flows(
197
- self, cat: Category, cni: str, cf: Dict[str, list[Dataflow]]
198
- ) -> None:
199
- if cat.categories:
200
- for c in cat.categories:
201
- self.__add_flows(c, f"{cni}.{c.id}", cf)
202
- if cni in cf:
203
- dfrefs = [
204
- DataflowRef(
205
- (
206
- df.agency.id
207
- if isinstance(df.agency, Agency)
208
- else df.agency
209
- ),
210
- df.id,
211
- df.version,
212
- df.name,
213
- )
214
- for df in cf[cni]
215
- ]
216
- cat.dataflows = dfrefs
217
-
218
255
  def to_model(self) -> CategoryScheme:
219
- """Returns the requested codelist."""
220
- cf = self.__group_flows()
221
- cs = self.data.categorySchemes[0].to_model()
222
- for c in cs:
223
- self.__add_flows(c, c.id, cf)
224
- return cs
256
+ """Returns the requested category scheme."""
257
+ return self.data.to_model()
225
258
 
226
259
 
227
260
  class JsonCategorisations(Struct, frozen=True, omit_defaults=True):
@@ -299,6 +299,7 @@ class JsonHeader(msgspec.Struct, frozen=True, omit_defaults=True):
299
299
  test=self.test,
300
300
  prepared=self.prepared,
301
301
  sender=self.sender,
302
+ receiver=self.receivers if self.receivers else (),
302
303
  )
303
304
 
304
305
  @classmethod
@@ -313,7 +314,7 @@ class JsonHeader(msgspec.Struct, frozen=True, omit_defaults=True):
313
314
  header.prepared,
314
315
  header.sender,
315
316
  header.test,
316
- receivers=(header.receiver,) if header.receiver else None,
317
+ receivers=header.receiver if header.receiver else None,
317
318
  schema=(
318
319
  "https://raw.githubusercontent.com/sdmx-twg/sdmx-json/"
319
320
  "develop/structure-message/tools/schemas/2.0.0/"
@@ -37,6 +37,7 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
37
37
  maxOccurs: Union[int, Literal["unbounded"]]
38
38
  isPresentational: bool
39
39
  localRepresentation: Optional[JsonRepresentation] = None
40
+ metadataAttributes: Sequence["JsonMetadataAttribute"] = ()
40
41
 
41
42
  def to_model(
42
43
  self, cs: Sequence[JsonConceptScheme], cls: Sequence[Codelist]
@@ -72,7 +73,7 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
72
73
  local_codes=codes,
73
74
  array_def=ab,
74
75
  local_enum_ref=local_enum_ref,
75
- components=(),
76
+ components=[a.to_model(cs, cls) for a in self.metadataAttributes],
76
77
  )
77
78
 
78
79
  @classmethod
@@ -94,6 +95,9 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
94
95
  minOccurs=min_occurs,
95
96
  maxOccurs=max_occurs,
96
97
  isPresentational=cmp.is_presentational,
98
+ metadataAttributes=[
99
+ JsonMetadataAttribute.from_model(c) for c in cmp.components
100
+ ],
97
101
  )
98
102
 
99
103
 
@@ -0,0 +1,108 @@
1
+ """SDMX-JSON document validation against JSON schemas."""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any, Callable, Mapping, Match, Optional
7
+
8
+ from jsonschema import Draft202012Validator
9
+ from jsonschema.exceptions import ValidationError
10
+ from sdmxschemas import SDMX_JSON_20_DATA_PATH as SCHEMA_PATH_JSON20_DATA
11
+ from sdmxschemas import (
12
+ SDMX_JSON_20_METADATA_PATH as SCHEMA_PATH_JSON20_METADATA,
13
+ )
14
+ from sdmxschemas import (
15
+ SDMX_JSON_20_STRUCTURE_PATH as SCHEMA_PATH_JSON20_STRUCTURE,
16
+ )
17
+
18
+ from pysdmx import errors
19
+
20
+ _SCHEMA_FILES: Mapping[str, Path] = {
21
+ "structure": SCHEMA_PATH_JSON20_STRUCTURE,
22
+ "metadata": SCHEMA_PATH_JSON20_METADATA,
23
+ "data": SCHEMA_PATH_JSON20_DATA,
24
+ }
25
+
26
+
27
+ def _schema_for(instance: Mapping[str, Any]) -> dict[str, Any]:
28
+ schema_url = instance.get("meta", {}).get("schema")
29
+ p = next(p for p in _SCHEMA_FILES.values() if p.name in schema_url)
30
+ with p.open("r", encoding="utf-8") as f:
31
+ schema = json.load(f)
32
+ return schema
33
+
34
+
35
+ def validate_sdmx_json(input_str: str) -> None:
36
+ """Validates an SDMX-JSON message against the appropriate JSON schema.
37
+
38
+ Args: input_str: The SDMX-JSON message to validate.
39
+ Raises:
40
+ invalid: If the SDMX-JSON message does not validate against the schema.
41
+
42
+ """
43
+ instance = json.loads(input_str)
44
+ schema = _schema_for(instance)
45
+ validator = Draft202012Validator(schema)
46
+
47
+ failures = sorted(
48
+ validator.iter_errors(instance),
49
+ key=lambda e: (list(e.path), e.message),
50
+ )
51
+ if failures:
52
+
53
+ def compact(e: ValidationError) -> str:
54
+ path = "$" if not e.path else "$." + ".".join(map(str, e.path))
55
+ sub = " | ".join(
56
+ getattr(e, "context", [])
57
+ and [c.message for c in e.context]
58
+ or []
59
+ )
60
+ raw = f"{e.message} | {sub}"
61
+
62
+ patterns: list[tuple[str, Callable[[Match[str]], str]]] = [
63
+ (
64
+ r"Additional properties are not allowed.*'([^']+)'",
65
+ lambda m: f"unexpected property '{m.group(1)}'",
66
+ ),
67
+ (
68
+ r"is not of type '([^']+)'",
69
+ lambda m: f"invalid type (expected {m.group(1)})",
70
+ ),
71
+ (
72
+ r"""['"]?([^'"\n]+)['"]?\s+is not one of\s+\[([^\]]+)\]""",
73
+ lambda m: "invalid value {!r}"
74
+ " (expected one of: {})".format(
75
+ m.group(1),
76
+ ", ".join(
77
+ s.strip().strip("'\"")
78
+ for s in m.group(2).split(",")
79
+ ),
80
+ ),
81
+ ),
82
+ (
83
+ r"'([^']+)' is a required property",
84
+ lambda m: f"missing property '{m.group(1)}'",
85
+ ),
86
+ (
87
+ r"""does not match ['"]([^'"]+)['"]""",
88
+ lambda m: f"does not match required"
89
+ f" pattern {m.group(1)!r}",
90
+ ),
91
+ ]
92
+
93
+ msg: Optional[str] = next(
94
+ (
95
+ fmt(re.search(rx, raw)) # type: ignore[arg-type]
96
+ for rx, fmt in patterns
97
+ if re.search(rx, raw)
98
+ ),
99
+ None,
100
+ )
101
+ msg = msg or e.message
102
+ return f"{path}: {msg}"
103
+
104
+ summary = "; ".join(compact(e) for e in failures[:3])
105
+ more = (
106
+ f" (+{len(failures) - 3} more errors)" if len(failures) > 3 else ""
107
+ )
108
+ raise errors.Invalid("Validation Error", f"{summary}{more}")
@@ -3,20 +3,27 @@
3
3
  import msgspec
4
4
 
5
5
  from pysdmx import errors
6
+ from pysdmx.__extras_check import __check_json_extra
6
7
  from pysdmx.io.json.sdmxjson2.messages import JsonMetadataMessage
8
+ from pysdmx.io.json.sdmxjson2.reader.doc_validation import validate_sdmx_json
7
9
  from pysdmx.model import decoders
8
10
  from pysdmx.model.message import MetadataMessage
9
11
 
10
12
 
11
- def read(input_str: str) -> MetadataMessage:
13
+ def read(input_str: str, validate: bool = True) -> MetadataMessage:
12
14
  """Read an SDMX-JSON 2.0.0 Metadata Message.
13
15
 
14
16
  Args:
15
17
  input_str: SDMX-JSON reference metadata message to read.
18
+ validate: If True, the JSON data will be validated against the schemas.
16
19
 
17
20
  Returns:
18
21
  A pysdmx MetadataMessage
19
22
  """
23
+ if validate:
24
+ __check_json_extra()
25
+ validate_sdmx_json(input_str)
26
+
20
27
  try:
21
28
  msg = msgspec.json.Decoder(
22
29
  JsonMetadataMessage, dec_hook=decoders
@@ -3,20 +3,27 @@
3
3
  import msgspec
4
4
 
5
5
  from pysdmx import errors
6
+ from pysdmx.__extras_check import __check_json_extra
6
7
  from pysdmx.io.json.sdmxjson2.messages import JsonStructureMessage
8
+ from pysdmx.io.json.sdmxjson2.reader.doc_validation import validate_sdmx_json
7
9
  from pysdmx.model import decoders
8
10
  from pysdmx.model.message import StructureMessage
9
11
 
10
12
 
11
- def read(input_str: str) -> StructureMessage:
12
- """Read an SDMX-JSON 2.0.0 Stucture Message.
13
+ def read(input_str: str, validate: bool = True) -> StructureMessage:
14
+ """Read an SDMX-JSON 2.0.0 Structure Message.
13
15
 
14
16
  Args:
15
17
  input_str: SDMX-JSON structure message to read.
18
+ validate: If True, the JSON data will be validated against the schemas.
16
19
 
17
20
  Returns:
18
21
  A pysdmx StructureMessage
19
22
  """
23
+ if validate:
24
+ __check_json_extra()
25
+ validate_sdmx_json(input_str)
26
+
20
27
  try:
21
28
  msg = msgspec.json.Decoder(
22
29
  JsonStructureMessage, dec_hook=decoders
pysdmx/io/reader.py CHANGED
@@ -26,11 +26,18 @@ def read_sdmx( # noqa: C901
26
26
 
27
27
  Check the :ref:`formats supported <io-reader-formats-supported>`
28
28
 
29
+ .. important::
30
+ When reading a SDMX-JSON structure message, you can read it
31
+ without installing the pysdmx[json] extra
32
+ by passing the parameter ``validate=False`` to this function.
33
+ Otherwise, if not installed, an error will be raised
34
+ if using ``validate=True``, as it is the default value.
35
+
29
36
  Args:
30
37
  sdmx_document: Path to file
31
38
  (`pathlib.Path <https://docs.python.org/3/library/pathlib.html>`_),
32
39
  URL, or string.
33
- validate: Validate the input file (only for SDMX-ML).
40
+ validate: Validate the input file (only for SDMX-ML and SDMX-JSON).
34
41
  pem: When using a URL, in case the service exposed
35
42
  a certificate created by an unknown certificate
36
43
  authority, you can pass a PEM file for this
@@ -78,7 +85,7 @@ def read_sdmx( # noqa: C901
78
85
  read as read_struct,
79
86
  )
80
87
 
81
- struct_msg = read_struct(input_str)
88
+ struct_msg = read_struct(input_str, validate=validate)
82
89
  header = struct_msg.header
83
90
  result_structures = (
84
91
  struct_msg.structures if struct_msg.structures else []
@@ -88,7 +95,7 @@ def read_sdmx( # noqa: C901
88
95
  read as read_refmeta,
89
96
  )
90
97
 
91
- ref_msg = read_refmeta(input_str)
98
+ ref_msg = read_refmeta(input_str, validate=validate)
92
99
  header = ref_msg.header
93
100
  reports = ref_msg.get_reports()
94
101
  elif read_format == Format.DATA_SDMX_ML_2_1_GEN:
@@ -230,6 +237,13 @@ def get_datasets(
230
237
  not equal to AllDimensions or Generic formats
231
238
  - Execution of VTL scripts over PandasDataset
232
239
 
240
+ .. important::
241
+ When reading a SDMX-JSON structure message (as the structure argument),
242
+ you can read it without installing the pysdmx[json] extra
243
+ by passing the parameter ``validate=False`` to this function.
244
+ Otherwise, if not installed, an error will be raised
245
+ if using ``validate=True``, as it is the default value.
246
+
233
247
  Args:
234
248
  data: Path to file
235
249
  (`pathlib.Path <https://docs.python.org/3/library/pathlib.html>`_),
@@ -238,7 +252,7 @@ def get_datasets(
238
252
  Path to file
239
253
  (`pathlib.Path <https://docs.python.org/3/library/pathlib.html>`_),
240
254
  URL, or string for the structure message, if needed.
241
- validate: Validate the input file (only for SDMX-ML).
255
+ validate: Validate the input file (only for SDMX-ML and SDMX-JSON).
242
256
  pem: When using a URL, in case the service exposed
243
257
  a certificate created by an unknown certificate
244
258
  authority, you can pass a PEM file for this
@@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional, Tuple
2
2
 
3
3
  import pandas as pd
4
4
 
5
- from pysdmx.errors import Invalid, NotImplemented
5
+ from pysdmx.errors import Invalid
6
6
  from pysdmx.io.xml.__tokens import (
7
7
  AGENCY_ID,
8
8
  DATASET,
@@ -10,6 +10,7 @@ from pysdmx.io.xml.__tokens import (
10
10
  HEADER,
11
11
  ID,
12
12
  PROV_AGREEMENT,
13
+ PROV_AGREMENT,
13
14
  REF,
14
15
  STR_ID,
15
16
  STR_REF,
@@ -92,9 +93,13 @@ def __get_elements_from_structure(structure: Dict[str, Any]) -> Any:
92
93
  structure_type = "ProvisionAgreement"
93
94
  tuple_ids = __get_ids_from_structure(structure[PROV_AGREEMENT])
94
95
  else:
95
- raise NotImplemented(
96
- "Unsupported", "ProvisionAgrement not implemented"
97
- )
96
+ # This section handles ProvisionAgrement.
97
+ # IMPORTANT: This is a typo in the SDMX standard XML schema 2.1
98
+ # fixed in 3.0 version.
99
+ # We intentionally read that exact key to be compatible
100
+ # with such files.
101
+ structure_type = "ProvisionAgreement"
102
+ tuple_ids = __get_ids_from_structure(structure[PROV_AGREMENT])
98
103
  return tuple_ids + (structure_type,)
99
104
 
100
105