pysdmx 1.10.1__py3-none-any.whl → 1.12.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 (38) hide show
  1. pysdmx/__init__.py +1 -1
  2. pysdmx/api/fmr/maintenance.py +10 -5
  3. pysdmx/io/input_processor.py +4 -0
  4. pysdmx/io/json/fusion/messages/constraint.py +22 -1
  5. pysdmx/io/json/fusion/messages/dsd.py +20 -14
  6. pysdmx/io/json/fusion/messages/msd.py +6 -9
  7. pysdmx/io/json/fusion/messages/schema.py +20 -1
  8. pysdmx/io/json/sdmxjson2/messages/core.py +12 -5
  9. pysdmx/io/json/sdmxjson2/messages/dsd.py +11 -17
  10. pysdmx/io/json/sdmxjson2/messages/msd.py +2 -5
  11. pysdmx/io/json/sdmxjson2/messages/report.py +7 -3
  12. pysdmx/io/json/sdmxjson2/messages/schema.py +38 -5
  13. pysdmx/io/json/sdmxjson2/messages/structure.py +7 -3
  14. pysdmx/io/json/sdmxjson2/reader/metadata.py +3 -3
  15. pysdmx/io/json/sdmxjson2/reader/structure.py +3 -3
  16. pysdmx/io/json/sdmxjson2/writer/_helper.py +118 -0
  17. pysdmx/io/json/sdmxjson2/writer/v2_0/__init__.py +1 -0
  18. pysdmx/io/json/sdmxjson2/writer/v2_0/metadata.py +33 -0
  19. pysdmx/io/json/sdmxjson2/writer/v2_0/structure.py +33 -0
  20. pysdmx/io/json/sdmxjson2/writer/v2_1/__init__.py +1 -0
  21. pysdmx/io/json/sdmxjson2/writer/v2_1/metadata.py +31 -0
  22. pysdmx/io/json/sdmxjson2/writer/v2_1/structure.py +33 -0
  23. pysdmx/io/reader.py +12 -3
  24. pysdmx/io/writer.py +13 -3
  25. pysdmx/io/xml/__ss_aux_reader.py +39 -17
  26. pysdmx/io/xml/__structure_aux_reader.py +221 -33
  27. pysdmx/io/xml/__structure_aux_writer.py +304 -5
  28. pysdmx/io/xml/__tokens.py +12 -0
  29. pysdmx/io/xml/__write_aux.py +9 -0
  30. pysdmx/io/xml/sdmx21/writer/generic.py +2 -2
  31. pysdmx/model/dataflow.py +11 -2
  32. pysdmx/toolkit/pd/_data_utils.py +1 -1
  33. {pysdmx-1.10.1.dist-info → pysdmx-1.12.0.dist-info}/METADATA +7 -1
  34. {pysdmx-1.10.1.dist-info → pysdmx-1.12.0.dist-info}/RECORD +36 -31
  35. {pysdmx-1.10.1.dist-info → pysdmx-1.12.0.dist-info}/WHEEL +1 -1
  36. pysdmx/io/json/sdmxjson2/writer/metadata.py +0 -60
  37. pysdmx/io/json/sdmxjson2/writer/structure.py +0 -61
  38. {pysdmx-1.10.1.dist-info → pysdmx-1.12.0.dist-info}/licenses/LICENSE +0 -0
pysdmx/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Your opinionated Python SDMX library."""
2
2
 
3
- __version__ = "1.10.1"
3
+ __version__ = "1.12.0"
@@ -64,9 +64,7 @@ class RegistryMaintenanceClient:
64
64
  timeout: The maximum number of seconds to wait before considering
65
65
  that a request timed out. Defaults to 10 seconds.
66
66
  """
67
- if api_endpoint.endswith("/"):
68
- api_endpoint = api_endpoint[0:-1]
69
- self._api_endpoint = f"{api_endpoint}"
67
+ self._api_endpoint = self.__sanitize_endpoint(api_endpoint)
70
68
  self._user = user
71
69
  self._password = password
72
70
  self._timeout = timeout
@@ -87,7 +85,6 @@ class RegistryMaintenanceClient:
87
85
  ) -> None:
88
86
  with httpx.Client(verify=self._ssl_context) as client:
89
87
  try:
90
- url = f"{endpoint}"
91
88
  auth = httpx.BasicAuth(self._user, self._password)
92
89
  headers = {
93
90
  "Content-Type": "application/text",
@@ -99,7 +96,7 @@ class RegistryMaintenanceClient:
99
96
  serializer = serializers.structure_message
100
97
  bodyjs = self._encoder.encode(serializer.from_model(message))
101
98
  r = client.post(
102
- url,
99
+ endpoint,
103
100
  headers=headers,
104
101
  content=bodyjs,
105
102
  timeout=self._timeout,
@@ -156,3 +153,11 @@ class RegistryMaintenanceClient:
156
153
  message = MetadataMessage(header=header, reports=reports)
157
154
  endpoint = f"{self._api_endpoint}/ws/secure/sdmx/v2/metadata"
158
155
  return self.__post(message, action, endpoint)
156
+
157
+ def __sanitize_endpoint(self, endpoint: str) -> str:
158
+ if endpoint.endswith("/"):
159
+ endpoint = endpoint[0:-1]
160
+ endpoint = endpoint.replace("/ws/secure/sdmx/v2/metadata", "")
161
+ endpoint = endpoint.replace("/sdmx/v2", "")
162
+ endpoint = endpoint.replace("/ws/secure/sdmxapi/rest", "")
163
+ return endpoint
@@ -97,6 +97,10 @@ def __get_sdmx_json_flavour(input_str: str) -> Tuple[str, Format]:
97
97
  return input_str, Format.STRUCTURE_SDMX_JSON_2_0_0
98
98
  elif "2.0.0/sdmx-json-metadata-schema.json" in flavour_check:
99
99
  return input_str, Format.REFMETA_SDMX_JSON_2_0_0
100
+ elif "2.1/sdmx-json-structure-schema.json" in flavour_check:
101
+ return input_str, Format.STRUCTURE_SDMX_JSON_2_1_0
102
+ elif "2.1/sdmx-json-metadata-schema.json" in flavour_check:
103
+ return input_str, Format.REFMETA_SDMX_JSON_2_1_0
100
104
  elif "sdmx-json" in flavour_check:
101
105
  raise NotImplemented(
102
106
  "Unsupported format", "This flavour of SDMX-JSON is not supported."
@@ -1,6 +1,6 @@
1
1
  """Collection of Fusion-JSON schemas for content constraints."""
2
2
 
3
- from typing import Dict, Sequence
3
+ from typing import Dict, List, Optional, Sequence
4
4
 
5
5
  from msgspec import Struct
6
6
 
@@ -11,11 +11,32 @@ class FusionKeyValue(Struct, frozen=True):
11
11
  values: Sequence[str]
12
12
 
13
13
 
14
+ class FusionKeySet(Struct, frozen=True):
15
+ """Fusion-JSON payload for the list of allowed values per component."""
16
+
17
+ dims: Sequence[str]
18
+ rows: Sequence[Sequence[str]]
19
+
20
+
14
21
  class FusionContentConstraint(Struct, frozen=True):
15
22
  """Fusion-JSON payload for a content constraint."""
16
23
 
17
24
  includeCube: Dict[str, FusionKeyValue] = {}
25
+ includeSeries: Optional[FusionKeySet] = None
18
26
 
19
27
  def to_map(self) -> Dict[str, Sequence[str]]:
20
28
  """Gets the list of allowed values for a component."""
21
29
  return {k: v.values for k, v in self.includeCube.items()}
30
+
31
+ def get_series(self, dimensions: List[str]) -> Sequence[str]:
32
+ """Get the list of series defined in the keyset."""
33
+ if self.includeSeries:
34
+ series = []
35
+ for r in self.includeSeries.rows:
36
+ s = dict.fromkeys(dimensions, "*")
37
+ for idx, cd in enumerate(self.includeSeries.dims):
38
+ s[cd] = r[idx]
39
+ series.append(".".join(s.values()))
40
+ return series
41
+ else:
42
+ return []
@@ -20,6 +20,7 @@ from pysdmx.model import (
20
20
  Codelist,
21
21
  Component,
22
22
  Components,
23
+ Concept,
23
24
  DataStructureDefinition,
24
25
  DataType,
25
26
  Facets,
@@ -32,14 +33,15 @@ from pysdmx.util import parse_item_urn
32
33
  def _find_concept(
33
34
  cs: Sequence[FusionConceptScheme],
34
35
  urn: str,
35
- ) -> FusionConcept:
36
+ ) -> Optional[FusionConcept]:
36
37
  r = parse_item_urn(urn)
37
38
  f = [
38
39
  m
39
40
  for m in cs
40
41
  if (m.agency == r.agency and m.id == r.id and m.version == r.version)
41
42
  ]
42
- return [c for c in f[0].items if c.id == r.item_id][0]
43
+ m = [c for c in f[0].items if c.id == r.item_id]
44
+ return m[0] if m else None
43
45
 
44
46
 
45
47
  def _get_representation(
@@ -112,12 +114,13 @@ class FusionAttribute(Struct, frozen=True):
112
114
  groups: Sequence[FusionGroup],
113
115
  ) -> Component:
114
116
  """Returns an attribute."""
115
- c = _find_concept(cs, self.concept)
117
+ m = _find_concept(cs, self.concept) if cs else None
118
+ c = m.to_model(cls) if m else parse_item_urn(self.concept)
116
119
  dt, facets, codes, ab = _get_representation(
117
120
  self.id, self.representation, cls, cons
118
121
  )
119
122
  lvl = self.__derive_level(groups)
120
- desc = c.descriptions[0].value if c.descriptions else None
123
+ desc = c.description if isinstance(c, Concept) else None
121
124
  if self.representation and self.representation.representation:
122
125
  local_enum_ref = self.representation.representation
123
126
  else:
@@ -126,10 +129,10 @@ class FusionAttribute(Struct, frozen=True):
126
129
  id=self.id,
127
130
  required=self.mandatory,
128
131
  role=Role.ATTRIBUTE,
129
- concept=c.to_model(cls),
132
+ concept=c,
130
133
  local_dtype=dt,
131
134
  local_facets=facets,
132
- name=c.names[0].value,
135
+ name=c.name if isinstance(c, Concept) else None,
133
136
  description=desc,
134
137
  local_codes=codes,
135
138
  attachment_level=lvl,
@@ -160,6 +163,7 @@ class FusionDimension(Struct, frozen=True):
160
163
  id: str
161
164
  concept: str
162
165
  representation: Optional[FusionRepresentation] = None
166
+ isTimeDimension: bool = False
163
167
 
164
168
  def to_model(
165
169
  self,
@@ -168,11 +172,12 @@ class FusionDimension(Struct, frozen=True):
168
172
  cons: Dict[str, Sequence[str]],
169
173
  ) -> Component:
170
174
  """Returns a dimension."""
171
- c = _find_concept(cs, self.concept)
175
+ m = _find_concept(cs, self.concept) if cs else None
176
+ c = m.to_model(cls) if m else parse_item_urn(self.concept)
172
177
  dt, facets, codes, ab = _get_representation(
173
178
  self.id, self.representation, cls, cons
174
179
  )
175
- desc = c.descriptions[0].value if c.descriptions else None
180
+ desc = c.description if isinstance(c, Concept) else None
176
181
  if self.representation and self.representation.representation:
177
182
  local_enum_ref = self.representation.representation
178
183
  else:
@@ -181,10 +186,10 @@ class FusionDimension(Struct, frozen=True):
181
186
  id=self.id,
182
187
  required=True,
183
188
  role=Role.DIMENSION,
184
- concept=c.to_model(cls),
189
+ concept=c,
185
190
  local_dtype=dt,
186
191
  local_facets=facets,
187
- name=c.names[0].value,
192
+ name=c.name if isinstance(c, Concept) else None,
188
193
  description=desc,
189
194
  local_codes=codes,
190
195
  array_def=ab,
@@ -222,11 +227,12 @@ class FusionMeasure(Struct, frozen=True):
222
227
  cons: Dict[str, Sequence[str]],
223
228
  ) -> Component:
224
229
  """Returns a measure."""
225
- c = _find_concept(cs, self.concept)
230
+ m = _find_concept(cs, self.concept) if cs else None
231
+ c = m.to_model(cls) if m else parse_item_urn(self.concept)
226
232
  dt, facets, codes, ab = _get_representation(
227
233
  self.id, self.representation, cls, cons
228
234
  )
229
- desc = c.descriptions[0].value if c.descriptions else None
235
+ desc = c.description if isinstance(c, Concept) else None
230
236
  if self.representation and self.representation.representation:
231
237
  local_enum_ref = self.representation.representation
232
238
  else:
@@ -235,10 +241,10 @@ class FusionMeasure(Struct, frozen=True):
235
241
  id=self.id,
236
242
  required=self.mandatory,
237
243
  role=Role.MEASURE,
238
- concept=c.to_model(cls),
244
+ concept=c,
239
245
  local_dtype=dt,
240
246
  local_facets=facets,
241
- name=c.names[0].value,
247
+ name=c.name if isinstance(c, Concept) else None,
242
248
  description=desc,
243
249
  local_codes=codes,
244
250
  array_def=ab,
@@ -14,13 +14,9 @@ from pysdmx.io.json.fusion.messages.dsd import (
14
14
  _find_concept,
15
15
  _get_representation,
16
16
  )
17
- from pysdmx.model import (
18
- ArrayBoundaries,
19
- MetadataComponent,
20
- )
21
- from pysdmx.model import (
22
- MetadataStructure as MSD,
23
- )
17
+ from pysdmx.model import ArrayBoundaries, MetadataComponent
18
+ from pysdmx.model import MetadataStructure as MSD
19
+ from pysdmx.util import parse_item_urn
24
20
 
25
21
 
26
22
  class FusionMetadataAttribute(Struct, frozen=True):
@@ -40,7 +36,8 @@ class FusionMetadataAttribute(Struct, frozen=True):
40
36
  cls: Sequence[FusionCodelist],
41
37
  ) -> MetadataComponent:
42
38
  """Returns an attribute."""
43
- c = _find_concept(cs, self.concept)
39
+ m = _find_concept(cs, self.concept) if cs else None
40
+ c = m.to_model(cls) if m else parse_item_urn(self.concept)
44
41
  dt, facets, codes, _ = _get_representation(
45
42
  self.id, self.representation, cls, {}
46
43
  )
@@ -60,7 +57,7 @@ class FusionMetadataAttribute(Struct, frozen=True):
60
57
  return MetadataComponent(
61
58
  self.id,
62
59
  is_presentational=self.presentational, # type: ignore[arg-type]
63
- concept=c.to_model(cls),
60
+ concept=c,
64
61
  local_dtype=dt,
65
62
  local_facets=facets,
66
63
  local_codes=codes,
@@ -65,6 +65,25 @@ class FusionSchemaMessage(msgspec.Struct, frozen=True):
65
65
  mapped_grps = [
66
66
  Group(g.id, dimensions=g.dimensionReferences) for g in grps
67
67
  ]
68
+ keys: List[str] = []
69
+ for dc in self.DataConstraint:
70
+ keys.extend(
71
+ dc.get_series(
72
+ [
73
+ d.id
74
+ for d in self.DataStructure[0].dimensionList.dimensions
75
+ if not d.isTimeDimension
76
+ ]
77
+ )
78
+ )
79
+ keys = list(set(keys)) if keys else None # type: ignore[assignment]
68
80
  return Schema(
69
- context, agency, id_, comps, version, urns, groups=mapped_grps
81
+ context,
82
+ agency,
83
+ id_,
84
+ comps,
85
+ version,
86
+ urns,
87
+ groups=mapped_grps,
88
+ keys=keys,
70
89
  )
@@ -307,17 +307,24 @@ class JsonHeader(msgspec.Struct, frozen=True, omit_defaults=True):
307
307
  self,
308
308
  header: Header,
309
309
  msg_type: Literal["structure", "metadata"] = "structure",
310
+ msg_version: Literal["2.0.0", "2.1"] = "2.0.0",
310
311
  ) -> "JsonHeader":
311
312
  """Create an SDMX-JSON header from a pysdmx Header."""
313
+ if msg_version == "2.0.0":
314
+ schema = (
315
+ "https://raw.githubusercontent.com/sdmx-twg/sdmx-json/"
316
+ f"develop/structure-message/tools/schemas/{msg_version}/"
317
+ f"sdmx-json-{msg_type}-schema.json"
318
+ )
319
+ else:
320
+ schema = (
321
+ f"https://json.sdmx.org/2.1/sdmx-json-{msg_type}-schema.json"
322
+ )
312
323
  return JsonHeader(
313
324
  header.id,
314
325
  header.prepared,
315
326
  header.sender,
316
327
  header.test,
317
328
  receivers=header.receiver if header.receiver else None,
318
- schema=(
319
- "https://raw.githubusercontent.com/sdmx-twg/sdmx-json/"
320
- "develop/structure-message/tools/schemas/2.0.0/"
321
- f"sdmx-json-{msg_type}-schema.json"
322
- ),
329
+ schema=schema,
323
330
  )
@@ -34,14 +34,17 @@ from pysdmx.model.dataflow import Group
34
34
  from pysdmx.util import parse_item_urn
35
35
 
36
36
 
37
- def _find_concept(cs: Sequence[JsonConceptScheme], urn: str) -> JsonConcept:
37
+ def _find_concept(
38
+ cs: Sequence[JsonConceptScheme], urn: str
39
+ ) -> Optional[JsonConcept]:
38
40
  r = parse_item_urn(urn)
39
41
  f = [
40
42
  m
41
43
  for m in cs
42
44
  if (m.agency == r.agency and m.id == r.id and m.version == r.version)
43
45
  ]
44
- return [c for c in f[0].concepts if c.id == r.item_id][0]
46
+ m = [c for c in f[0].concepts if c.id == r.item_id]
47
+ return m[0] if m else None
45
48
 
46
49
 
47
50
  def _get_type(repr_: JsonRepresentation) -> Optional[str]:
@@ -168,11 +171,8 @@ class JsonDimension(Struct, frozen=True, omit_defaults=True):
168
171
  cons: Dict[str, Sequence[str]],
169
172
  ) -> Component:
170
173
  """Returns a component."""
171
- c = (
172
- _find_concept(cs, self.conceptIdentity).to_model(cls)
173
- if cs
174
- else parse_item_urn(self.conceptIdentity)
175
- )
174
+ m = _find_concept(cs, self.conceptIdentity) if cs else None
175
+ c = m.to_model(cls) if m else parse_item_urn(self.conceptIdentity)
176
176
  name = c.name if isinstance(c, Concept) else None
177
177
  desc = c.description if isinstance(c, Concept) else None
178
178
  dt, facets, codes, ab = _get_representation(
@@ -227,11 +227,8 @@ class JsonAttribute(Struct, frozen=True, omit_defaults=True):
227
227
  groups: Sequence[JsonGroup],
228
228
  ) -> Component:
229
229
  """Returns a component."""
230
- c = (
231
- _find_concept(cs, self.conceptIdentity).to_model(cls)
232
- if cs
233
- else parse_item_urn(self.conceptIdentity)
234
- )
230
+ m = _find_concept(cs, self.conceptIdentity) if cs else None
231
+ c = m.to_model(cls) if m else parse_item_urn(self.conceptIdentity)
235
232
  name = c.name if isinstance(c, Concept) else None
236
233
  desc = c.description if isinstance(c, Concept) else None
237
234
  dt, facets, codes, ab = _get_representation(
@@ -312,11 +309,8 @@ class JsonMeasure(Struct, frozen=True, omit_defaults=True):
312
309
  cons: Dict[str, Sequence[str]],
313
310
  ) -> Component:
314
311
  """Returns a component."""
315
- c = (
316
- _find_concept(cs, self.conceptIdentity).to_model(cls)
317
- if cs
318
- else parse_item_urn(self.conceptIdentity)
319
- )
312
+ m = _find_concept(cs, self.conceptIdentity) if cs else None
313
+ c = m.to_model(cls) if m else parse_item_urn(self.conceptIdentity)
320
314
  name = c.name if isinstance(c, Concept) else None
321
315
  desc = c.description if isinstance(c, Concept) else None
322
316
  dt, facets, codes, ab = _get_representation(
@@ -49,11 +49,8 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
49
49
  self, cs: Sequence[JsonConceptScheme], cls: Sequence[Codelist]
50
50
  ) -> MetadataComponent:
51
51
  """Returns a metadata component."""
52
- c = (
53
- _find_concept(cs, self.conceptIdentity).to_model(cls)
54
- if cs
55
- else parse_item_urn(self.conceptIdentity)
56
- )
52
+ m = _find_concept(cs, self.conceptIdentity) if cs else None
53
+ c = m.to_model(cls) if m else parse_item_urn(self.conceptIdentity)
57
54
  dt, facets, codes, _ = _get_representation(
58
55
  self.id, self.localRepresentation, cls, {}
59
56
  )
@@ -1,6 +1,6 @@
1
1
  """Collection of SDMX-JSON schemas for reference metadata queries."""
2
2
 
3
- from typing import Any, Optional, Sequence
3
+ from typing import Any, Literal, Optional, Sequence
4
4
 
5
5
  from msgspec import Struct
6
6
 
@@ -162,7 +162,11 @@ class JsonMetadataMessage(Struct, frozen=True, omit_defaults=True):
162
162
  return MetadataMessage(header, reports)
163
163
 
164
164
  @classmethod
165
- def from_model(self, msg: MetadataMessage) -> "JsonMetadataMessage":
165
+ def from_model(
166
+ self,
167
+ msg: MetadataMessage,
168
+ msg_version: Literal["2.0.0", "2.1"] = "2.0.0",
169
+ ) -> "JsonMetadataMessage":
166
170
  """Converts a pysdmx metadata message to an SDMX-JSON one."""
167
171
  if not msg.header:
168
172
  raise errors.Invalid(
@@ -175,6 +179,6 @@ class JsonMetadataMessage(Struct, frozen=True, omit_defaults=True):
175
179
  "SDMX-JSON metadata messages must have metadata reports.",
176
180
  )
177
181
 
178
- header = JsonHeader.from_model(msg.header, "metadata")
182
+ header = JsonHeader.from_model(msg.header, "metadata", msg_version)
179
183
  reports = [JsonMetadataReport.from_model(r) for r in msg.reports]
180
184
  return JsonMetadataMessage(header, JsonMetadataSets(reports))
@@ -1,12 +1,15 @@
1
1
  """Collection of SDMX-JSON schemas for SDMX-REST schema queries."""
2
2
 
3
- from typing import Literal, Optional, Sequence, Tuple
3
+ from typing import Dict, Literal, Optional, Sequence, Tuple
4
4
 
5
5
  import msgspec
6
6
 
7
7
  from pysdmx.io.json.sdmxjson2.messages.code import JsonCodelist, JsonValuelist
8
8
  from pysdmx.io.json.sdmxjson2.messages.concept import JsonConceptScheme
9
- from pysdmx.io.json.sdmxjson2.messages.constraint import JsonDataConstraint
9
+ from pysdmx.io.json.sdmxjson2.messages.constraint import (
10
+ JsonDataConstraint,
11
+ JsonKeySet,
12
+ )
10
13
  from pysdmx.io.json.sdmxjson2.messages.core import JsonHeader
11
14
  from pysdmx.io.json.sdmxjson2.messages.dsd import JsonDataStructure
12
15
  from pysdmx.model import Components, HierarchyAssociation, Schema
@@ -25,7 +28,7 @@ class JsonSchemas(msgspec.Struct, frozen=True, omit_defaults=True):
25
28
 
26
29
  def to_model(
27
30
  self,
28
- ) -> Tuple[Components, Optional[Sequence[Group]]]:
31
+ ) -> Tuple[Components, Optional[Sequence[Group]], Optional[Sequence[str]]]:
29
32
  """Returns the requested schema."""
30
33
  comps = self.dataStructures[0].dataStructureComponents
31
34
  comps, grps = comps.to_model( # type: ignore[union-attr,assignment]
@@ -34,7 +37,36 @@ class JsonSchemas(msgspec.Struct, frozen=True, omit_defaults=True):
34
37
  self.valuelists,
35
38
  self.dataConstraints,
36
39
  )
37
- return comps, grps # type: ignore[return-value]
40
+ keys = self.__process_keys()
41
+ return comps, grps, keys # type: ignore[return-value]
42
+
43
+ def __extract_keys_dict(
44
+ self, keysets: Sequence[JsonKeySet]
45
+ ) -> Sequence[Dict[str, str]]:
46
+ keys = []
47
+ for ks in keysets:
48
+ keys.extend(
49
+ [{kv.id: kv.value for kv in k.keyValues} for k in ks.keys]
50
+ )
51
+ return keys
52
+
53
+ def __infer_keys(self, keys_dict: Dict[str, str]) -> str:
54
+ dimensions = [
55
+ d.id
56
+ for d in self.dataStructures[ # type: ignore[union-attr]
57
+ 0
58
+ ].dataStructureComponents.dimensionList.dimensions
59
+ ]
60
+ dim_values = [keys_dict.get(d, "*") for d in dimensions]
61
+ return ".".join(dim_values)
62
+
63
+ def __process_keys(self) -> Optional[Sequence[str]]:
64
+ keys = []
65
+ for c in self.dataConstraints:
66
+ if c.dataKeySets:
67
+ keys_dicts = self.__extract_keys_dict(c.dataKeySets)
68
+ keys.extend([self.__infer_keys(d) for d in keys_dicts])
69
+ return list(set(keys))
38
70
 
39
71
 
40
72
  class JsonSchemaMessage(msgspec.Struct, frozen=True, omit_defaults=True):
@@ -52,7 +84,7 @@ class JsonSchemaMessage(msgspec.Struct, frozen=True, omit_defaults=True):
52
84
  hierarchies: Sequence[HierarchyAssociation],
53
85
  ) -> Schema:
54
86
  """Returns the requested schema."""
55
- components, groups = self.data.to_model()
87
+ components, groups, keys = self.data.to_model()
56
88
  comp_dict = {c.id: c for c in components}
57
89
  urns = [a.urn for a in self.meta.links]
58
90
  for ha in hierarchies:
@@ -77,4 +109,5 @@ class JsonSchemaMessage(msgspec.Struct, frozen=True, omit_defaults=True):
77
109
  version,
78
110
  urns, # type: ignore[arg-type]
79
111
  groups=groups,
112
+ keys=keys,
80
113
  )
@@ -1,6 +1,6 @@
1
1
  """Collection of SDMX-JSON schemas for generic structure messages."""
2
2
 
3
- from typing import Sequence
3
+ from typing import Literal, Sequence
4
4
 
5
5
  from msgspec import Struct
6
6
 
@@ -338,12 +338,16 @@ class JsonStructureMessage(Struct, frozen=True, omit_defaults=True):
338
338
  return StructureMessage(header, structures)
339
339
 
340
340
  @classmethod
341
- def from_model(cls, message: StructureMessage) -> "JsonStructureMessage":
341
+ def from_model(
342
+ cls,
343
+ message: StructureMessage,
344
+ msg_version: Literal["2.0.0", "2.1"] = "2.0.0",
345
+ ) -> "JsonStructureMessage":
342
346
  """Creates an SDMX-JSON payload from a pysdmx StructureMessage."""
343
347
  if not message.header:
344
348
  raise errors.Invalid(
345
349
  "Invalid input", "SDMX-JSON messages must have a header."
346
350
  )
347
- header = JsonHeader.from_model(message.header)
351
+ header = JsonHeader.from_model(message.header, msg_version=msg_version)
348
352
  structs = JsonStructures.from_model(message)
349
353
  return JsonStructureMessage(header, structs)
@@ -1,4 +1,4 @@
1
- """Writer interface for SDMX-JSON 2.0.0 Reference Metadata messages."""
1
+ """Reader interface for SDMX-JSON 2.0.0 and 2.1.0 Reference Metadata."""
2
2
 
3
3
  import msgspec
4
4
 
@@ -11,7 +11,7 @@ from pysdmx.model.message import MetadataMessage
11
11
 
12
12
 
13
13
  def read(input_str: str, validate: bool = True) -> MetadataMessage:
14
- """Read an SDMX-JSON 2.0.0 Metadata Message.
14
+ """Read SDMX-JSON 2.0.0 and 2.1.0 Metadata messages.
15
15
 
16
16
  Args:
17
17
  input_str: SDMX-JSON reference metadata message to read.
@@ -34,6 +34,6 @@ def read(input_str: str, validate: bool = True) -> MetadataMessage:
34
34
  "Invalid message",
35
35
  (
36
36
  "The supplied file could not be read as SDMX-JSON 2.0.0 "
37
- "reference metadata message."
37
+ "or 2.1.0 reference metadata message."
38
38
  ),
39
39
  ) from de
@@ -1,4 +1,4 @@
1
- """Reader interface for SDMX-JSON 2.0.0 Structure messages."""
1
+ """Reader interface for SDMX-JSON 2.0.0 and 2.1.0 Structure messages."""
2
2
 
3
3
  import msgspec
4
4
 
@@ -11,7 +11,7 @@ from pysdmx.model.message import StructureMessage
11
11
 
12
12
 
13
13
  def read(input_str: str, validate: bool = True) -> StructureMessage:
14
- """Read an SDMX-JSON 2.0.0 Structure Message.
14
+ """Read SDMX-JSON 2.0.0 and 2.1.0 Structure messages.
15
15
 
16
16
  Args:
17
17
  input_str: SDMX-JSON structure message to read.
@@ -34,6 +34,6 @@ def read(input_str: str, validate: bool = True) -> StructureMessage:
34
34
  "Invalid message",
35
35
  (
36
36
  "The supplied file could not be read as SDMX-JSON 2.0.0 "
37
- "structure message."
37
+ "or 2.1.0 structure message."
38
38
  ),
39
39
  ) from de