pysdmx 1.8.1__py3-none-any.whl → 1.10.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.
Files changed (42) hide show
  1. pysdmx/__extras_check.py +15 -1
  2. pysdmx/__init__.py +1 -1
  3. pysdmx/api/fmr/__init__.py +3 -2
  4. pysdmx/io/input_processor.py +9 -6
  5. pysdmx/io/json/fusion/messages/category.py +69 -41
  6. pysdmx/io/json/sdmxjson2/messages/__init__.py +4 -0
  7. pysdmx/io/json/sdmxjson2/messages/category.py +76 -43
  8. pysdmx/io/json/sdmxjson2/messages/code.py +16 -6
  9. pysdmx/io/json/sdmxjson2/messages/constraint.py +235 -16
  10. pysdmx/io/json/sdmxjson2/messages/core.py +2 -1
  11. pysdmx/io/json/sdmxjson2/messages/dsd.py +35 -7
  12. pysdmx/io/json/sdmxjson2/messages/map.py +5 -4
  13. pysdmx/io/json/sdmxjson2/messages/metadataflow.py +1 -0
  14. pysdmx/io/json/sdmxjson2/messages/msd.py +18 -10
  15. pysdmx/io/json/sdmxjson2/messages/schema.py +2 -2
  16. pysdmx/io/json/sdmxjson2/messages/structure.py +81 -44
  17. pysdmx/io/json/sdmxjson2/messages/vtl.py +13 -9
  18. pysdmx/io/json/sdmxjson2/reader/doc_validation.py +112 -0
  19. pysdmx/io/json/sdmxjson2/reader/metadata.py +8 -1
  20. pysdmx/io/json/sdmxjson2/reader/structure.py +9 -2
  21. pysdmx/io/reader.py +18 -4
  22. pysdmx/io/xml/__data_aux.py +9 -4
  23. pysdmx/io/xml/__parse_xml.py +2 -0
  24. pysdmx/io/xml/__structure_aux_reader.py +70 -0
  25. pysdmx/io/xml/__structure_aux_writer.py +63 -9
  26. pysdmx/io/xml/__tokens.py +3 -0
  27. pysdmx/io/xml/__write_aux.py +35 -30
  28. pysdmx/io/xml/header.py +48 -35
  29. pysdmx/model/__base.py +47 -2
  30. pysdmx/model/__init__.py +18 -0
  31. pysdmx/model/category.py +23 -1
  32. pysdmx/model/constraint.py +69 -0
  33. pysdmx/model/message.py +97 -72
  34. pysdmx/toolkit/vtl/__init__.py +10 -1
  35. pysdmx/toolkit/vtl/_validations.py +8 -12
  36. pysdmx/toolkit/vtl/convert.py +333 -0
  37. pysdmx/toolkit/vtl/script_generation.py +1 -1
  38. pysdmx/util/_model_utils.py +40 -3
  39. {pysdmx-1.8.1.dist-info → pysdmx-1.10.0.dist-info}/METADATA +6 -3
  40. {pysdmx-1.8.1.dist-info → pysdmx-1.10.0.dist-info}/RECORD +42 -39
  41. {pysdmx-1.8.1.dist-info → pysdmx-1.10.0.dist-info}/WHEEL +0 -0
  42. {pysdmx-1.8.1.dist-info → pysdmx-1.10.0.dist-info}/licenses/LICENSE +0 -0
pysdmx/__extras_check.py CHANGED
@@ -46,7 +46,7 @@ def __check_xml_extra() -> None:
46
46
 
47
47
  def __check_vtl_extra() -> None:
48
48
  try:
49
- import vtlengine # type: ignore[import-untyped] # noqa: F401
49
+ import vtlengine # noqa: F401
50
50
  except ImportError:
51
51
  raise ImportError(
52
52
  ERROR_MESSAGE.format(
@@ -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.1"
3
+ __version__ = "1.10.0"
@@ -44,6 +44,7 @@ from pysdmx.model import (
44
44
  Metadataflow,
45
45
  MetadataProvisionAgreement,
46
46
  MetadataReport,
47
+ MetadataStructure,
47
48
  MultiRepresentationMap,
48
49
  ProvisionAgreement,
49
50
  RepresentationMap,
@@ -767,7 +768,7 @@ class RegistryClient(__BaseRegistryClient):
767
768
  agency: str = "*",
768
769
  id: str = "*",
769
770
  version: str = "+",
770
- ) -> Sequence[Dataflow]:
771
+ ) -> Sequence[MetadataStructure]:
771
772
  """Get the metadata structures (MSD) matching the supplied parameters.
772
773
 
773
774
  Args:
@@ -1341,7 +1342,7 @@ class AsyncRegistryClient(__BaseRegistryClient):
1341
1342
  agency: str = "*",
1342
1343
  id: str = "*",
1343
1344
  version: str = "+",
1344
- ) -> Sequence[Dataflow]:
1345
+ ) -> Sequence[MetadataStructure]:
1345
1346
  """Get the metadata structures (MSD) matching the supplied parameters.
1346
1347
 
1347
1348
  Args:
@@ -29,16 +29,19 @@ def __check_xml(input_str: str) -> bool:
29
29
 
30
30
  def __check_csv(input_str: str) -> bool:
31
31
  try:
32
- max_length = min(2048, len(input_str))
33
- dialect = csv.Sniffer().sniff(input_str[:max_length])
32
+ lines = input_str.splitlines()
33
+
34
+ # Use the first N complete lines
35
+ # (1 should be enough)
36
+ max_lines = 1
37
+ sample = "\n".join(lines[:max_lines])
38
+
39
+ dialect = csv.Sniffer().sniff(sample)
34
40
  control_csv_format = (
35
41
  dialect.delimiter == "," and dialect.quotechar == '"'
36
42
  )
37
43
  # Check we can access the data and it is not empty
38
- if (
39
- len(input_str.splitlines()) > 1
40
- or input_str.splitlines()[0].count(",") > 1
41
- ) and control_csv_format:
44
+ if (len(lines) > 1 or lines[0].count(",") > 1) and control_csv_format:
42
45
  return True
43
46
  except Exception:
44
47
  return False
@@ -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):
@@ -12,6 +12,9 @@ from pysdmx.io.json.sdmxjson2.messages.code import (
12
12
  JsonHierarchyMessage,
13
13
  )
14
14
  from pysdmx.io.json.sdmxjson2.messages.concept import JsonConceptSchemeMessage
15
+ from pysdmx.io.json.sdmxjson2.messages.constraint import (
16
+ JsonDataConstraintMessage,
17
+ )
15
18
  from pysdmx.io.json.sdmxjson2.messages.dataflow import (
16
19
  JsonDataflowMessage,
17
20
  JsonDataflowsMessage,
@@ -50,6 +53,7 @@ __all__ = [
50
53
  "JsonCategorySchemeMessage",
51
54
  "JsonCodelistMessage",
52
55
  "JsonConceptSchemeMessage",
56
+ "JsonDataConstraintMessage",
53
57
  "JsonDataflowMessage",
54
58
  "JsonDataflowsMessage",
55
59
  "JsonDataStructuresMessage",
@@ -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):
@@ -231,7 +231,7 @@ class JsonCodelists(Struct, frozen=True, omit_defaults=True):
231
231
  """SDMX-JSON payload for lists of codes."""
232
232
 
233
233
  codelists: Sequence[JsonCodelist] = ()
234
- valuelists: Sequence[JsonValuelist] = ()
234
+ valueLists: Sequence[JsonValuelist] = ()
235
235
 
236
236
 
237
237
  class JsonCodelistMessage(Struct, frozen=True, omit_defaults=True):
@@ -244,7 +244,7 @@ class JsonCodelistMessage(Struct, frozen=True, omit_defaults=True):
244
244
  if self.data.codelists:
245
245
  return self.data.codelists[0].to_model()
246
246
  else:
247
- return self.data.valuelists[0].to_model()
247
+ return self.data.valueLists[0].to_model()
248
248
 
249
249
 
250
250
  class JsonHierarchicalCode(Struct, frozen=True, omit_defaults=True):
@@ -329,10 +329,10 @@ class JsonHierarchicalCode(Struct, frozen=True, omit_defaults=True):
329
329
  code=code.urn,
330
330
  validFrom=code.rel_valid_from,
331
331
  validTo=code.rel_valid_to,
332
- annotations=tuple(annotations),
333
- hierarchicalCodes=[
334
- JsonHierarchicalCode.from_model(c) for c in code.codes
335
- ],
332
+ annotations=tuple(annotations) if annotations else None,
333
+ hierarchicalCodes=tuple(
334
+ [JsonHierarchicalCode.from_model(c) for c in code.codes]
335
+ ),
336
336
  )
337
337
 
338
338
 
@@ -475,6 +475,15 @@ class JsonHierarchyAssociation(
475
475
  "SDMX-JSON hierarchy associations must reference a context",
476
476
  {"hierarchy_association": ha.id},
477
477
  )
478
+ lnk = (
479
+ JsonLink(
480
+ rel="UserDefinedOperator",
481
+ type="sdmx_artefact",
482
+ urn=ha.operator,
483
+ )
484
+ if ha.operator
485
+ else None
486
+ )
478
487
  return JsonHierarchyAssociation(
479
488
  agency=(
480
489
  ha.agency.id if isinstance(ha.agency, Agency) else ha.agency
@@ -492,6 +501,7 @@ class JsonHierarchyAssociation(
492
501
  linkedHierarchy=href,
493
502
  linkedObject=ha.component_ref,
494
503
  contextObject=ha.context_ref,
504
+ links=[lnk] if lnk else (),
495
505
  )
496
506
 
497
507