pysdmx 1.5.2__py3-none-any.whl → 1.6.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 (58) hide show
  1. pysdmx/__init__.py +1 -1
  2. pysdmx/api/fmr/__init__.py +8 -3
  3. pysdmx/api/fmr/maintenance.py +158 -0
  4. pysdmx/api/qb/structure.py +1 -0
  5. pysdmx/api/qb/util.py +1 -0
  6. pysdmx/io/csv/__csv_aux_reader.py +99 -0
  7. pysdmx/io/csv/__csv_aux_writer.py +118 -0
  8. pysdmx/io/csv/sdmx10/reader/__init__.py +9 -14
  9. pysdmx/io/csv/sdmx10/writer/__init__.py +28 -2
  10. pysdmx/io/csv/sdmx20/__init__.py +0 -9
  11. pysdmx/io/csv/sdmx20/reader/__init__.py +8 -61
  12. pysdmx/io/csv/sdmx20/writer/__init__.py +32 -25
  13. pysdmx/io/csv/sdmx21/__init__.py +1 -0
  14. pysdmx/io/csv/sdmx21/reader/__init__.py +86 -0
  15. pysdmx/io/csv/sdmx21/writer/__init__.py +70 -0
  16. pysdmx/io/format.py +8 -0
  17. pysdmx/io/input_processor.py +16 -2
  18. pysdmx/io/json/fusion/messages/code.py +21 -4
  19. pysdmx/io/json/fusion/messages/concept.py +16 -8
  20. pysdmx/io/json/fusion/messages/dataflow.py +8 -1
  21. pysdmx/io/json/fusion/messages/dsd.py +15 -0
  22. pysdmx/io/json/fusion/messages/schema.py +8 -1
  23. pysdmx/io/json/sdmxjson2/messages/agency.py +43 -7
  24. pysdmx/io/json/sdmxjson2/messages/category.py +92 -7
  25. pysdmx/io/json/sdmxjson2/messages/code.py +239 -18
  26. pysdmx/io/json/sdmxjson2/messages/concept.py +78 -13
  27. pysdmx/io/json/sdmxjson2/messages/constraint.py +5 -5
  28. pysdmx/io/json/sdmxjson2/messages/core.py +121 -14
  29. pysdmx/io/json/sdmxjson2/messages/dataflow.py +63 -8
  30. pysdmx/io/json/sdmxjson2/messages/dsd.py +215 -20
  31. pysdmx/io/json/sdmxjson2/messages/map.py +200 -24
  32. pysdmx/io/json/sdmxjson2/messages/pa.py +36 -5
  33. pysdmx/io/json/sdmxjson2/messages/provider.py +35 -7
  34. pysdmx/io/json/sdmxjson2/messages/report.py +85 -7
  35. pysdmx/io/json/sdmxjson2/messages/schema.py +11 -12
  36. pysdmx/io/json/sdmxjson2/messages/structure.py +150 -2
  37. pysdmx/io/json/sdmxjson2/messages/vtl.py +547 -17
  38. pysdmx/io/json/sdmxjson2/reader/metadata.py +32 -0
  39. pysdmx/io/json/sdmxjson2/reader/structure.py +32 -0
  40. pysdmx/io/json/sdmxjson2/writer/__init__.py +9 -0
  41. pysdmx/io/json/sdmxjson2/writer/metadata.py +60 -0
  42. pysdmx/io/json/sdmxjson2/writer/structure.py +61 -0
  43. pysdmx/io/reader.py +28 -9
  44. pysdmx/io/serde.py +17 -0
  45. pysdmx/io/writer.py +45 -9
  46. pysdmx/io/xml/__write_data_aux.py +1 -54
  47. pysdmx/io/xml/__write_structure_specific_aux.py +1 -1
  48. pysdmx/io/xml/sdmx21/writer/generic.py +1 -1
  49. pysdmx/model/code.py +11 -1
  50. pysdmx/model/dataflow.py +23 -0
  51. pysdmx/model/map.py +12 -4
  52. pysdmx/model/message.py +9 -1
  53. pysdmx/toolkit/pd/_data_utils.py +100 -0
  54. pysdmx/toolkit/vtl/_validations.py +2 -3
  55. {pysdmx-1.5.2.dist-info → pysdmx-1.6.0.dist-info}/METADATA +3 -2
  56. {pysdmx-1.5.2.dist-info → pysdmx-1.6.0.dist-info}/RECORD +58 -46
  57. {pysdmx-1.5.2.dist-info → pysdmx-1.6.0.dist-info}/WHEEL +1 -1
  58. {pysdmx-1.5.2.dist-info → pysdmx-1.6.0.dist-info/licenses}/LICENSE +0 -0
@@ -3,14 +3,25 @@
3
3
  from collections import defaultdict
4
4
  from typing import Dict, Sequence, Set
5
5
 
6
- from msgspec import Struct
6
+ import msgspec
7
7
 
8
- from pysdmx.io.json.sdmxjson2.messages.core import ItemSchemeType
8
+ from pysdmx.io.json.sdmxjson2.messages.core import (
9
+ ItemSchemeType,
10
+ JsonAnnotation,
11
+ )
9
12
  from pysdmx.io.json.sdmxjson2.messages.dataflow import JsonDataflow
10
13
  from pysdmx.model import Agency, AgencyScheme, DataflowRef
11
14
 
12
15
 
13
- class JsonAgencyScheme(ItemSchemeType, frozen=True):
16
+ def _sanitize_agency(agency: Agency, is_sdmx_scheme: bool) -> Agency:
17
+ if is_sdmx_scheme:
18
+ nid = agency.id
19
+ else:
20
+ nid = agency.id[agency.id.rindex(".") + 1 :]
21
+ return msgspec.structs.replace(agency, id=nid, dataflows=())
22
+
23
+
24
+ class JsonAgencyScheme(ItemSchemeType, frozen=True, omit_defaults=True):
14
25
  """SDMX-JSON payload for an agency scheme."""
15
26
 
16
27
  agencies: Sequence[Agency] = ()
@@ -26,7 +37,7 @@ class JsonAgencyScheme(ItemSchemeType, frozen=True):
26
37
  description=a.description,
27
38
  contacts=a.contacts,
28
39
  dataflows=flows,
29
- annotations=[a.to_model() for a in self.annotations],
40
+ annotations=tuple([a.to_model() for a in self.annotations]),
30
41
  )
31
42
 
32
43
  def to_model(self, dataflows: Sequence[JsonDataflow]) -> AgencyScheme:
@@ -43,15 +54,40 @@ class JsonAgencyScheme(ItemSchemeType, frozen=True):
43
54
  description=self.description,
44
55
  agency=self.agency,
45
56
  items=agencies,
46
- annotations=[a.to_model() for a in self.annotations],
57
+ annotations=tuple([a.to_model() for a in self.annotations]),
47
58
  is_external_reference=self.isExternalReference,
48
59
  is_partial=self.isPartial,
49
60
  valid_from=self.validFrom,
50
61
  valid_to=self.validTo,
51
62
  )
52
63
 
64
+ @classmethod
65
+ def from_model(self, asc: AgencyScheme) -> "JsonAgencyScheme":
66
+ """Converts a pysdmx agency scheme to an SDMX-JSON one."""
67
+ agency = (
68
+ asc.agency.id if isinstance(asc.agency, Agency) else asc.agency
69
+ )
70
+ is_sdmx_scheme = agency == "SDMX"
71
+ children = [_sanitize_agency(a, is_sdmx_scheme) for a in asc.items]
72
+
73
+ return JsonAgencyScheme(
74
+ id="AGENCIES",
75
+ name="AGENCIES",
76
+ agency=agency,
77
+ description=asc.description,
78
+ version="1.0",
79
+ agencies=children,
80
+ annotations=tuple(
81
+ [JsonAnnotation.from_model(a) for a in asc.annotations]
82
+ ),
83
+ isExternalReference=asc.is_external_reference,
84
+ isPartial=asc.is_partial,
85
+ validFrom=asc.valid_from,
86
+ validTo=asc.valid_to,
87
+ )
88
+
53
89
 
54
- class JsonAgencySchemes(Struct, frozen=True):
90
+ class JsonAgencySchemes(msgspec.Struct, frozen=True, omit_defaults=True):
55
91
  """SDMX-JSON payload for the list of agency schemes."""
56
92
 
57
93
  agencySchemes: Sequence[JsonAgencyScheme]
@@ -62,7 +98,7 @@ class JsonAgencySchemes(Struct, frozen=True):
62
98
  return [a.to_model(self.dataflows) for a in self.agencySchemes]
63
99
 
64
100
 
65
- class JsonAgencyMessage(Struct, frozen=True):
101
+ class JsonAgencyMessage(msgspec.Struct, frozen=True, omit_defaults=True):
66
102
  """SDMX-JSON payload for /agencyscheme queries."""
67
103
 
68
104
  data: JsonAgencySchemes
@@ -5,8 +5,10 @@ from typing import Dict, Sequence
5
5
 
6
6
  from msgspec import Struct
7
7
 
8
+ from pysdmx import errors
8
9
  from pysdmx.io.json.sdmxjson2.messages.core import (
9
10
  ItemSchemeType,
11
+ JsonAnnotation,
10
12
  MaintainableType,
11
13
  NameableType,
12
14
  )
@@ -23,7 +25,10 @@ from pysdmx.util import find_by_urn
23
25
 
24
26
 
25
27
  class JsonCategorisation(
26
- MaintainableType, frozen=True, rename={"agency": "agencyID"}
28
+ MaintainableType,
29
+ frozen=True,
30
+ rename={"agency": "agencyID"},
31
+ omit_defaults=True,
27
32
  ):
28
33
  """SDMX-JSON payload for a categorisation."""
29
34
 
@@ -46,8 +51,35 @@ class JsonCategorisation(
46
51
  annotations=[a.to_model() for a in self.annotations],
47
52
  )
48
53
 
54
+ @classmethod
55
+ def from_model(self, cat: Categorisation) -> "JsonCategorisation":
56
+ """Converts a pysdmx categorisation to an SDMX-JSON one."""
57
+ if not cat.name:
58
+ raise errors.Invalid(
59
+ "Invalid input",
60
+ "SDMX-JSON categorisations must have a name",
61
+ {"categorisation": cat.id},
62
+ )
63
+ return JsonCategorisation(
64
+ agency=(
65
+ cat.agency.id if isinstance(cat.agency, Agency) else cat.agency
66
+ ),
67
+ id=cat.id,
68
+ name=cat.name,
69
+ version=cat.version,
70
+ isExternalReference=cat.is_external_reference,
71
+ validFrom=cat.valid_from,
72
+ validTo=cat.valid_to,
73
+ description=cat.description,
74
+ annotations=tuple(
75
+ [JsonAnnotation.from_model(a) for a in cat.annotations]
76
+ ),
77
+ source=cat.source,
78
+ target=cat.target,
79
+ )
80
+
49
81
 
50
- class JsonCategory(NameableType, frozen=True):
82
+ class JsonCategory(NameableType, frozen=True, omit_defaults=True):
51
83
  """SDMX-JSON payload for a category."""
52
84
 
53
85
  categories: Sequence["JsonCategory"] = ()
@@ -62,9 +94,33 @@ class JsonCategory(NameableType, frozen=True):
62
94
  annotations=[a.to_model() for a in self.annotations],
63
95
  )
64
96
 
97
+ @classmethod
98
+ def from_model(self, cat: Category) -> "JsonCategory":
99
+ """Converts a pysdmx category to an SDMX-JSON one."""
100
+ if not cat.name:
101
+ raise errors.Invalid(
102
+ "Invalid input",
103
+ "SDMX-JSON category must have a name",
104
+ {"category": cat.id},
105
+ )
106
+ return JsonCategory(
107
+ id=cat.id,
108
+ name=cat.name,
109
+ description=cat.description,
110
+ annotations=tuple(
111
+ [JsonAnnotation.from_model(a) for a in cat.annotations]
112
+ ),
113
+ categories=tuple(
114
+ [JsonCategory.from_model(c) for c in cat.categories]
115
+ ),
116
+ )
117
+
65
118
 
66
119
  class JsonCategoryScheme(
67
- ItemSchemeType, frozen=True, rename={"agency": "agencyID"}
120
+ ItemSchemeType,
121
+ frozen=True,
122
+ rename={"agency": "agencyID"},
123
+ omit_defaults=True,
68
124
  ):
69
125
  """SDMX-JSON payload for a category scheme."""
70
126
 
@@ -86,8 +142,37 @@ class JsonCategoryScheme(
86
142
  annotations=[a.to_model() for a in self.annotations],
87
143
  )
88
144
 
145
+ @classmethod
146
+ def from_model(self, cs: CategoryScheme) -> "JsonCategoryScheme":
147
+ """Converts a pysdmx category scheme to an SDMX-JSON one."""
148
+ if not cs.name:
149
+ raise errors.Invalid(
150
+ "Invalid input",
151
+ "SDMX-JSON category schemes must have a name",
152
+ {"category_scheme": cs.id},
153
+ )
154
+ return JsonCategoryScheme(
155
+ agency=(
156
+ cs.agency.id if isinstance(cs.agency, Agency) else cs.agency
157
+ ),
158
+ id=cs.id,
159
+ name=cs.name,
160
+ version=cs.version,
161
+ isExternalReference=cs.is_external_reference,
162
+ validFrom=cs.valid_from,
163
+ validTo=cs.valid_to,
164
+ description=cs.description,
165
+ annotations=tuple(
166
+ [JsonAnnotation.from_model(a) for a in cs.annotations]
167
+ ),
168
+ isPartial=cs.is_partial,
169
+ categories=tuple(
170
+ [JsonCategory.from_model(c) for c in cs.categories]
171
+ ),
172
+ )
173
+
89
174
 
90
- class JsonCategorySchemes(Struct, frozen=True):
175
+ class JsonCategorySchemes(Struct, frozen=True, omit_defaults=True):
91
176
  """SDMX-JSON payload for the list of category schemes."""
92
177
 
93
178
  categorySchemes: Sequence[JsonCategoryScheme]
@@ -95,7 +180,7 @@ class JsonCategorySchemes(Struct, frozen=True):
95
180
  dataflows: Sequence[JsonDataflow] = ()
96
181
 
97
182
 
98
- class JsonCategorySchemeMessage(Struct, frozen=True):
183
+ class JsonCategorySchemeMessage(Struct, frozen=True, omit_defaults=True):
99
184
  """SDMX-JSON payload for /categoryscheme queries."""
100
185
 
101
186
  data: JsonCategorySchemes
@@ -139,13 +224,13 @@ class JsonCategorySchemeMessage(Struct, frozen=True):
139
224
  return cs
140
225
 
141
226
 
142
- class JsonCategorisations(Struct, frozen=True):
227
+ class JsonCategorisations(Struct, frozen=True, omit_defaults=True):
143
228
  """SDMX-JSON payload for the list of categorisations."""
144
229
 
145
230
  categorisations: Sequence[JsonCategorisation]
146
231
 
147
232
 
148
- class JsonCategorisationMessage(Struct, frozen=True):
233
+ class JsonCategorisationMessage(Struct, frozen=True, omit_defaults=True):
149
234
  """SDMX-JSON payload for /categorisation queries."""
150
235
 
151
236
  data: JsonCategorisations
@@ -6,6 +6,7 @@ from typing import Optional, Sequence, Tuple
6
6
 
7
7
  from msgspec import Struct
8
8
 
9
+ from pysdmx import errors
9
10
  from pysdmx.io.json.sdmxjson2.messages.core import (
10
11
  ItemSchemeType,
11
12
  JsonAnnotation,
@@ -14,6 +15,8 @@ from pysdmx.io.json.sdmxjson2.messages.core import (
14
15
  NameableType,
15
16
  )
16
17
  from pysdmx.model import (
18
+ Agency,
19
+ Annotation,
17
20
  Code,
18
21
  Codelist,
19
22
  HierarchicalCode,
@@ -22,14 +25,16 @@ from pysdmx.model import (
22
25
  )
23
26
  from pysdmx.util import find_by_urn, parse_item_urn
24
27
 
28
+ _VAL_FMT = "%Y-%m-%dT%H:%M:%S%z"
25
29
 
26
- class JsonCode(NameableType, frozen=True):
30
+
31
+ class JsonCode(NameableType, frozen=True, omit_defaults=True):
27
32
  """SDMX-JSON payload for codes."""
28
33
 
29
34
  parent: Optional[str] = None
30
35
 
31
36
  def __handle_date(self, datestr: str) -> datetime:
32
- return datetime.strptime(datestr, "%Y-%m-%dT%H:%M:%S%z")
37
+ return datetime.strptime(datestr, _VAL_FMT)
33
38
 
34
39
  def __get_val(
35
40
  self, a: JsonAnnotation
@@ -57,11 +62,51 @@ class JsonCode(NameableType, frozen=True):
57
62
  description=self.description,
58
63
  valid_from=vf,
59
64
  valid_to=vt,
60
- annotations=[a.to_model() for a in self.annotations],
65
+ annotations=tuple(
66
+ [
67
+ a.to_model()
68
+ for a in self.annotations
69
+ if a.type != "FR_VALIDITY_PERIOD"
70
+ ]
71
+ ),
72
+ )
73
+
74
+ @classmethod
75
+ def from_model(self, code: Code) -> "JsonCode":
76
+ """Converts a pysdmx code to an SDMX-JSON one."""
77
+ if not code.name:
78
+ raise errors.Invalid(
79
+ "Invalid input",
80
+ "SDMX-JSON codes must have a name",
81
+ {"code": code.id},
82
+ )
83
+
84
+ annotations = [JsonAnnotation.from_model(a) for a in code.annotations]
85
+ if code.valid_from and code.valid_to:
86
+ vp = (
87
+ f"{datetime.strftime(code.valid_from, _VAL_FMT)}/"
88
+ f"{datetime.strftime(code.valid_to, _VAL_FMT)}"
89
+ )
90
+ elif code.valid_from:
91
+ vp = f"{datetime.strftime(code.valid_from, _VAL_FMT)}/"
92
+ elif code.valid_to:
93
+ vp = f"/{datetime.strftime(code.valid_to, _VAL_FMT)}"
94
+ else:
95
+ vp = ""
96
+ if vp:
97
+ annotations.append(
98
+ JsonAnnotation(title=vp, type="FR_VALIDITY_PERIOD")
99
+ )
100
+
101
+ return JsonCode(
102
+ id=code.id,
103
+ name=code.name,
104
+ description=code.description,
105
+ annotations=tuple(annotations),
61
106
  )
62
107
 
63
108
 
64
- class JsonCodelist(ItemSchemeType, frozen=True):
109
+ class JsonCodelist(ItemSchemeType, frozen=True, omit_defaults=True):
65
110
  """SDMX-JSON payload for a codelist."""
66
111
 
67
112
  codes: Sequence[JsonCode] = ()
@@ -74,16 +119,43 @@ class JsonCodelist(ItemSchemeType, frozen=True):
74
119
  agency=self.agency,
75
120
  description=self.description,
76
121
  version=self.version,
77
- items=[i.to_model() for i in self.codes],
78
- annotations=[a.to_model() for a in self.annotations],
122
+ items=tuple([i.to_model() for i in self.codes]),
123
+ annotations=tuple([a.to_model() for a in self.annotations]),
79
124
  is_external_reference=self.isExternalReference,
80
125
  is_partial=self.isPartial,
81
126
  valid_from=self.validFrom,
82
127
  valid_to=self.validTo,
83
128
  )
84
129
 
130
+ @classmethod
131
+ def from_model(self, cl: Codelist) -> "JsonCodelist":
132
+ """Converts a pysdmx codelist to an SDMX-JSON one."""
133
+ if not cl.name:
134
+ raise errors.Invalid(
135
+ "Invalid input",
136
+ "SDMX-JSON codelists must have a name",
137
+ {"codelist": cl.id},
138
+ )
139
+ return JsonCodelist(
140
+ id=cl.id,
141
+ name=cl.name,
142
+ agency=(
143
+ cl.agency.id if isinstance(cl.agency, Agency) else cl.agency
144
+ ),
145
+ description=cl.description,
146
+ version=cl.version,
147
+ codes=tuple([JsonCode.from_model(i) for i in cl.items]),
148
+ annotations=tuple(
149
+ [JsonAnnotation.from_model(a) for a in cl.annotations]
150
+ ),
151
+ isExternalReference=cl.is_external_reference,
152
+ isPartial=cl.is_partial,
153
+ validFrom=cl.valid_from,
154
+ validTo=cl.valid_to,
155
+ )
156
+
85
157
 
86
- class JsonValuelist(ItemSchemeType, frozen=True):
158
+ class JsonValuelist(ItemSchemeType, frozen=True, omit_defaults=True):
87
159
  """SDMX-JSON payload for a valuelist."""
88
160
 
89
161
  valueItems: Sequence[JsonCode] = ()
@@ -105,15 +177,42 @@ class JsonValuelist(ItemSchemeType, frozen=True):
105
177
  sdmx_type="valuelist",
106
178
  )
107
179
 
180
+ @classmethod
181
+ def from_model(self, cl: Codelist) -> "JsonValuelist":
182
+ """Converts a pysdmx codelist to an SDMX-JSON valuelist."""
183
+ if not cl.name:
184
+ raise errors.Invalid(
185
+ "Invalid input",
186
+ "SDMX-JSON valuelists must have a name",
187
+ {"valuelist": cl.id},
188
+ )
189
+ return JsonValuelist(
190
+ id=cl.id,
191
+ name=cl.name,
192
+ agency=(
193
+ cl.agency.id if isinstance(cl.agency, Agency) else cl.agency
194
+ ),
195
+ description=cl.description,
196
+ version=cl.version,
197
+ valueItems=tuple([JsonCode.from_model(i) for i in cl.items]),
198
+ annotations=tuple(
199
+ [JsonAnnotation.from_model(a) for a in cl.annotations]
200
+ ),
201
+ isExternalReference=cl.is_external_reference,
202
+ isPartial=cl.is_partial,
203
+ validFrom=cl.valid_from,
204
+ validTo=cl.valid_to,
205
+ )
108
206
 
109
- class JsonCodelists(Struct, frozen=True):
207
+
208
+ class JsonCodelists(Struct, frozen=True, omit_defaults=True):
110
209
  """SDMX-JSON payload for lists of codes."""
111
210
 
112
211
  codelists: Sequence[JsonCodelist] = ()
113
212
  valuelists: Sequence[JsonValuelist] = ()
114
213
 
115
214
 
116
- class JsonCodelistMessage(Struct, frozen=True):
215
+ class JsonCodelistMessage(Struct, frozen=True, omit_defaults=True):
117
216
  """SDMX-JSON payload for /codelist queries."""
118
217
 
119
218
  data: JsonCodelists
@@ -126,7 +225,7 @@ class JsonCodelistMessage(Struct, frozen=True):
126
225
  return self.data.valuelists[0].to_model()
127
226
 
128
227
 
129
- class JsonHierarchicalCode(Struct, frozen=True):
228
+ class JsonHierarchicalCode(Struct, frozen=True, omit_defaults=True):
130
229
  """Fusion-JSON payload for hierarchical codes."""
131
230
 
132
231
  id: str
@@ -163,6 +262,11 @@ class JsonHierarchicalCode(Struct, frozen=True):
163
262
  codes = [c.to_model(codelists) for c in self.hierarchicalCodes]
164
263
  vf = self.validFrom.replace(tzinfo=tz.utc) if self.validFrom else None
165
264
  vt = self.validTo.replace(tzinfo=tz.utc) if self.validTo else None
265
+ if self.id != code.id:
266
+ a = Annotation(id="hcode", type="pysdmx", text=self.id)
267
+ annotations = [a]
268
+ else:
269
+ annotations = []
166
270
  return HierarchicalCode(
167
271
  code.id,
168
272
  name,
@@ -172,10 +276,45 @@ class JsonHierarchicalCode(Struct, frozen=True):
172
276
  vf,
173
277
  vt,
174
278
  codes,
279
+ tuple(annotations),
280
+ self.code,
175
281
  )
176
282
 
283
+ @classmethod
284
+ def from_model(self, code: HierarchicalCode) -> "JsonHierarchicalCode":
285
+ """Converts a pysdmx hierarchical code to an SDMX-JSON one."""
286
+ if not code.urn:
287
+ raise errors.Invalid(
288
+ "Invalid input",
289
+ "SDMX-JSON hierarchical codes must have the code urn.",
290
+ {"code": code.id},
291
+ )
292
+
293
+ annotations = [
294
+ JsonAnnotation.from_model(a)
295
+ for a in code.annotations
296
+ if a.type != "pysdmx"
297
+ ]
298
+ id_ano = [
299
+ a
300
+ for a in code.annotations
301
+ if a.type == "pysdmx" and a.id == "hcode"
302
+ ]
303
+ hid = id_ano[0].value if len(id_ano) > 0 else code.id
304
+
305
+ return JsonHierarchicalCode(
306
+ id=hid, # type: ignore[arg-type]
307
+ code=code.urn,
308
+ validFrom=code.rel_valid_from,
309
+ validTo=code.rel_valid_to,
310
+ annotations=tuple(annotations),
311
+ hierarchicalCodes=[
312
+ JsonHierarchicalCode.from_model(c) for c in code.codes
313
+ ],
314
+ )
177
315
 
178
- class JsonHierarchy(ItemSchemeType, frozen=True):
316
+
317
+ class JsonHierarchy(ItemSchemeType, frozen=True, omit_defaults=True):
179
318
  """SDMX-JSON payload for a hierarchy."""
180
319
 
181
320
  hierarchicalCodes: Sequence[JsonHierarchicalCode] = ()
@@ -189,7 +328,7 @@ class JsonHierarchy(ItemSchemeType, frozen=True):
189
328
  agency=self.agency,
190
329
  description=self.description,
191
330
  version=self.version,
192
- annotations=[a.to_model() for a in self.annotations],
331
+ annotations=tuple([a.to_model() for a in self.annotations]),
193
332
  is_external_reference=self.isExternalReference,
194
333
  is_partial=self.isPartial,
195
334
  valid_from=self.validFrom,
@@ -197,8 +336,35 @@ class JsonHierarchy(ItemSchemeType, frozen=True):
197
336
  codes=[i.to_model(cls) for i in self.hierarchicalCodes],
198
337
  )
199
338
 
339
+ @classmethod
340
+ def from_model(self, h: Hierarchy) -> "JsonHierarchy":
341
+ """Converts a pysdmx hierarchy to an SDMX-JSON one."""
342
+ if not h.name:
343
+ raise errors.Invalid(
344
+ "Invalid input",
345
+ "SDMX-JSON hierarchy must have a name",
346
+ {"hierarchy": h.id},
347
+ )
348
+ return JsonHierarchy(
349
+ id=h.id,
350
+ name=h.name,
351
+ agency=(h.agency.id if isinstance(h.agency, Agency) else h.agency),
352
+ description=h.description,
353
+ version=h.version,
354
+ hierarchicalCodes=tuple(
355
+ [JsonHierarchicalCode.from_model(i) for i in h.codes]
356
+ ),
357
+ annotations=tuple(
358
+ [JsonAnnotation.from_model(a) for a in h.annotations]
359
+ ),
360
+ isExternalReference=h.is_external_reference,
361
+ isPartial=h.is_partial,
362
+ validFrom=h.valid_from,
363
+ validTo=h.valid_to,
364
+ )
200
365
 
201
- class JsonHierarchies(Struct, frozen=True):
366
+
367
+ class JsonHierarchies(Struct, frozen=True, omit_defaults=True):
202
368
  """SDMX-JSON payload for hierarchies."""
203
369
 
204
370
  codelists: Sequence[JsonCodelist] = ()
@@ -209,7 +375,9 @@ class JsonHierarchies(Struct, frozen=True):
209
375
  return [h.to_model(self.codelists) for h in self.hierarchies]
210
376
 
211
377
 
212
- class JsonHierarchyAssociation(MaintainableType, frozen=True):
378
+ class JsonHierarchyAssociation(
379
+ MaintainableType, frozen=True, omit_defaults=True
380
+ ):
213
381
  """SDMX-JSON payload for a hierarchy association."""
214
382
 
215
383
  linkedHierarchy: str = ""
@@ -251,8 +419,61 @@ class JsonHierarchyAssociation(MaintainableType, frozen=True):
251
419
  operator=lnk[0].urn if lnk else None,
252
420
  )
253
421
 
422
+ @classmethod
423
+ def from_model(
424
+ self, ha: HierarchyAssociation
425
+ ) -> "JsonHierarchyAssociation":
426
+ """Converts a pysdmx hierarchy association to an SDMX-JSON one."""
427
+ if not ha.name:
428
+ raise errors.Invalid(
429
+ "Invalid input",
430
+ "SDMX-JSON hierarchy associations must have a name",
431
+ {"hierarchy_association": ha.id},
432
+ )
433
+ if ha.hierarchy is None:
434
+ raise errors.Invalid(
435
+ "Invalid input",
436
+ "SDMX-JSON hierarchy associations must reference a hierarchy",
437
+ {"hierarchy_association": ha.id},
438
+ )
439
+ if not ha.component_ref:
440
+ raise errors.Invalid(
441
+ "Invalid input",
442
+ "SDMX-JSON hierarchy associations must reference a component",
443
+ {"hierarchy_association": ha.id},
444
+ )
445
+ if isinstance(ha.hierarchy, Hierarchy):
446
+ base = "urn:sdmx:org.sdmx.infomodel.codelist."
447
+ href = f"{base}{ha.hierarchy.short_urn}"
448
+ else:
449
+ href = ha.hierarchy
450
+ if not ha.context_ref:
451
+ raise errors.Invalid(
452
+ "Invalid input",
453
+ "SDMX-JSON hierarchy associations must reference a context",
454
+ {"hierarchy_association": ha.id},
455
+ )
456
+ return JsonHierarchyAssociation(
457
+ agency=(
458
+ ha.agency.id if isinstance(ha.agency, Agency) else ha.agency
459
+ ),
460
+ id=ha.id,
461
+ name=ha.name,
462
+ version=ha.version,
463
+ isExternalReference=ha.is_external_reference,
464
+ validFrom=ha.valid_from,
465
+ validTo=ha.valid_to,
466
+ description=ha.description,
467
+ annotations=tuple(
468
+ [JsonAnnotation.from_model(a) for a in ha.annotations]
469
+ ),
470
+ linkedHierarchy=href,
471
+ linkedObject=ha.component_ref,
472
+ contextObject=ha.context_ref,
473
+ )
474
+
254
475
 
255
- class JsonHierarchyMessage(Struct, frozen=True):
476
+ class JsonHierarchyMessage(Struct, frozen=True, omit_defaults=True):
256
477
  """SDMX-JSON payload for /hierarchy queries."""
257
478
 
258
479
  data: JsonHierarchies
@@ -262,7 +483,7 @@ class JsonHierarchyMessage(Struct, frozen=True):
262
483
  return self.data.to_model()[0]
263
484
 
264
485
 
265
- class JsonHierarchiesMessage(Struct, frozen=True):
486
+ class JsonHierarchiesMessage(Struct, frozen=True, omit_defaults=True):
266
487
  """SDMX-JSON payload for /hierarchy queries."""
267
488
 
268
489
  data: JsonHierarchies
@@ -272,7 +493,7 @@ class JsonHierarchiesMessage(Struct, frozen=True):
272
493
  return self.data.to_model()
273
494
 
274
495
 
275
- class JsonHierarchyAssociations(Struct, frozen=True):
496
+ class JsonHierarchyAssociations(Struct, frozen=True, omit_defaults=True):
276
497
  """SDMX-JSON payload for hierarchy associations."""
277
498
 
278
499
  codelists: Sequence[JsonCodelist] = ()
@@ -287,7 +508,7 @@ class JsonHierarchyAssociations(Struct, frozen=True):
287
508
  ]
288
509
 
289
510
 
290
- class JsonHierarchyAssociationMessage(Struct, frozen=True):
511
+ class JsonHierarchyAssociationMessage(Struct, frozen=True, omit_defaults=True):
291
512
  """SDMX-JSON payload for hierarchy associations messages."""
292
513
 
293
514
  data: JsonHierarchyAssociations