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.
@@ -2,6 +2,7 @@ from typing import Any, Dict
2
2
 
3
3
  import xmltodict
4
4
 
5
+ from pysdmx.__extras_check import __check_xml_extra
5
6
  from pysdmx.io.xml.doc_validation import validate_doc
6
7
 
7
8
  SCHEMA_ROOT = "http://www.sdmx.org/resources/sdmxml/schemas/v2_1/"
@@ -77,6 +78,7 @@ def parse_xml(
77
78
  Raises:
78
79
  Invalid: If the SDMX data cannot be parsed.
79
80
  """
81
+ __check_xml_extra()
80
82
  if validate:
81
83
  validate_doc(input_str)
82
84
  if SCHEMA_ROOT_31 in input_str:
@@ -43,6 +43,7 @@ from pysdmx.io.xml.__tokens import (
43
43
  CUSTOM_TYPE_SCHEME,
44
44
  CUSTOM_TYPE_SCHEMES,
45
45
  CUSTOM_TYPES,
46
+ DATA_PROV,
46
47
  DEPARTMENT,
47
48
  DESC,
48
49
  DFW,
@@ -94,6 +95,8 @@ from pysdmx.io.xml.__tokens import (
94
95
  ORGS,
95
96
  PAR_ID,
96
97
  PAR_VER,
98
+ PROV_AGREEMENT,
99
+ PROV_AGREEMENTS,
97
100
  REF,
98
101
  REQUIRED,
99
102
  ROLE,
@@ -105,6 +108,7 @@ from pysdmx.io.xml.__tokens import (
105
108
  SER_URL_LOW,
106
109
  STR_URL,
107
110
  STR_URL_LOW,
111
+ STR_USAGE,
108
112
  STRUCTURE,
109
113
  TELEPHONE,
110
114
  TELEPHONES,
@@ -171,6 +175,7 @@ from pysdmx.model.dataflow import (
171
175
  Dataflow,
172
176
  DataStructureDefinition,
173
177
  Group,
178
+ ProvisionAgreement,
174
179
  Role,
175
180
  )
176
181
  from pysdmx.model.vtl import (
@@ -204,6 +209,7 @@ STRUCTURES_MAPPING = {
204
209
  VTL_MAPPING_SCHEME: VtlMappingScheme,
205
210
  NAME_PER_SCHEME: NamePersonalisationScheme,
206
211
  CUSTOM_TYPE_SCHEME: CustomTypeScheme,
212
+ PROV_AGREEMENTS: ProvisionAgreement,
207
213
  }
208
214
  ITEMS_CLASSES = {
209
215
  AGENCY: Agency,
@@ -808,6 +814,62 @@ class StructureParser(Struct):
808
814
 
809
815
  return element
810
816
 
817
+ def __format_prov_agreement(
818
+ self, element: Dict[str, Any]
819
+ ) -> Dict[str, Any]:
820
+ dfw = None
821
+ if STR_USAGE in element:
822
+ ref_dfw: Union[Reference, ItemReference]
823
+ if REF in element[STR_USAGE]:
824
+ str_usage = element[STR_USAGE][REF]
825
+ ref_dfw = Reference(
826
+ sdmx_type=str_usage[CLASS],
827
+ agency=str_usage[AGENCY_ID],
828
+ id=str_usage[ID],
829
+ version=str_usage[VERSION],
830
+ )
831
+ else:
832
+ ref_dfw = parse_urn(element[STR_USAGE][URN])
833
+ dfw = (
834
+ f"{ref_dfw.sdmx_type}={ref_dfw.agency}:"
835
+ f"{ref_dfw.id}({ref_dfw.version})"
836
+ )
837
+ del element[STR_USAGE]
838
+
839
+ if DFW in element:
840
+ ref_dfw = parse_urn(element[DFW])
841
+ dfw = (
842
+ f"{ref_dfw.sdmx_type}={ref_dfw.agency}:"
843
+ f"{ref_dfw.id}({ref_dfw.version})"
844
+ )
845
+ del element[DFW]
846
+
847
+ ref_data_prov: Union[Reference, ItemReference]
848
+ if REF in element[DATA_PROV]:
849
+ data_prov = element[DATA_PROV][REF]
850
+ ref_data_prov = ItemReference(
851
+ sdmx_type=data_prov[CLASS],
852
+ agency=data_prov[AGENCY_ID],
853
+ id=data_prov[PAR_ID],
854
+ version=data_prov[PAR_VER],
855
+ item_id=data_prov[ID],
856
+ )
857
+ elif URN in element[DATA_PROV]:
858
+ ref_data_prov = parse_urn(element[DATA_PROV][URN])
859
+ else:
860
+ ref_data_prov = parse_urn(element[DATA_PROV])
861
+ del element[DATA_PROV]
862
+ provider = (
863
+ f"{ref_data_prov.sdmx_type}={ref_data_prov.agency}:"
864
+ f"{ref_data_prov.id}({ref_data_prov.version})"
865
+ f".{ref_data_prov.item_id}" # type: ignore[union-attr]
866
+ )
867
+
868
+ element["dataflow"] = dfw
869
+ element["provider"] = provider
870
+
871
+ return element
872
+
811
873
  def __format_vtl(self, json_vtl: Dict[str, Any]) -> Dict[str, Any]:
812
874
  # VTL Scheme Handling
813
875
  _format_lower_key("vtlVersion", json_vtl)
@@ -1054,6 +1116,8 @@ class StructureParser(Struct):
1054
1116
  element = self.__format_validity(element)
1055
1117
  element = self.__format_groups(element)
1056
1118
  element = self.__format_components(element)
1119
+ if item == PROV_AGREEMENT:
1120
+ element = self.__format_prov_agreement(element)
1057
1121
 
1058
1122
  if "xmlns" in element:
1059
1123
  del element["xmlns"]
@@ -1155,6 +1219,12 @@ class StructureParser(Struct):
1155
1219
  lambda data: self.__format_schema(data, DFWS, DFW),
1156
1220
  "dataflows",
1157
1221
  ),
1222
+ PROV_AGREEMENTS: process_structure(
1223
+ PROV_AGREEMENTS,
1224
+ lambda data: self.__format_schema(
1225
+ data, PROV_AGREEMENTS, PROV_AGREEMENT
1226
+ ),
1227
+ ),
1158
1228
  VTLMAPPINGS: process_structure(
1159
1229
  VTLMAPPINGS,
1160
1230
  lambda data: self.__format_scheme(
@@ -21,6 +21,7 @@ from pysdmx.io.xml.__tokens import (
21
21
  CS,
22
22
  CUSTOM_TYPE,
23
23
  CUSTOM_TYPE_SCHEME,
24
+ DATA_PROV,
24
25
  DEPARTMENT,
25
26
  DFW,
26
27
  DIM,
@@ -49,10 +50,12 @@ from pysdmx.io.xml.__tokens import (
49
50
  PAR_ID,
50
51
  PAR_VER,
51
52
  POSITION,
53
+ PROV_AGREEMENT,
52
54
  REF,
53
55
  ROLE,
54
56
  RULE,
55
57
  RULE_SCHEME,
58
+ STR_USAGE,
56
59
  TELEPHONE,
57
60
  TEXT_FORMAT,
58
61
  TEXT_TYPE,
@@ -125,10 +128,12 @@ from pysdmx.model.dataflow import (
125
128
  Dataflow,
126
129
  DataStructureDefinition,
127
130
  Group,
131
+ ProvisionAgreement,
128
132
  Role,
129
133
  )
130
134
  from pysdmx.util import (
131
135
  parse_item_urn,
136
+ parse_short_item_urn,
132
137
  parse_short_urn,
133
138
  parse_urn,
134
139
  )
@@ -174,6 +179,7 @@ STR_DICT_TYPE_LIST_21 = {
174
179
  RulesetScheme: "Rulesets",
175
180
  UserDefinedOperatorScheme: "UserDefinedOperators",
176
181
  TransformationScheme: "Transformations",
182
+ ProvisionAgreement: "ProvisionAgreements",
177
183
  }
178
184
 
179
185
 
@@ -189,6 +195,7 @@ STR_DICT_TYPE_LIST_30 = {
189
195
  RulesetScheme: "RulesetSchemes",
190
196
  UserDefinedOperatorScheme: "UserDefinedOperatorSchemes",
191
197
  TransformationScheme: "TransformationSchemes",
198
+ ProvisionAgreement: "ProvisionAgreements",
192
199
  }
193
200
 
194
201
 
@@ -790,6 +797,50 @@ def __write_structure(
790
797
  return outfile
791
798
 
792
799
 
800
+ def __write_prov_agreement(
801
+ dataflow: str, provider: str, indent: str, references_30: bool = False
802
+ ) -> str:
803
+ """Writes the provision agreement structure to the XML file."""
804
+ ref_df = parse_short_urn(dataflow)
805
+ ref_pr = parse_short_item_urn(provider)
806
+ if references_30:
807
+ outfile = f"{indent}<{ABBR_STR}:{DFW}>"
808
+ outfile += (
809
+ f"urn:sdmx:org.sdmx.infomodel.datastructure.Dataflow={ref_df.agency}:{ref_df.id}({ref_df.version})"
810
+ f"</{ABBR_STR}:{DFW}>"
811
+ )
812
+ outfile += f"{indent}<{ABBR_STR}:{DATA_PROV}>"
813
+ outfile += (
814
+ f"urn:sdmx:org.sdmx.infomodel.base.DataProvider={ref_pr.agency}:{ref_pr.id}({ref_pr.version}).{ref_pr.item_id}"
815
+ f"</{ABBR_STR}:{DATA_PROV}>"
816
+ )
817
+ else:
818
+ outfile = f"{indent}<{ABBR_STR}:{STR_USAGE}>"
819
+ outfile += (
820
+ f"{add_indent(indent)}<{REF} "
821
+ f'{PACKAGE}="datastructure" '
822
+ f"{AGENCY_ID}={ref_df.agency!r} "
823
+ f"{ID}={ref_df.id!r} "
824
+ f"{VERSION}={ref_df.version!r} "
825
+ f"{CLASS}={DFW!r}/>"
826
+ )
827
+ outfile += f"{indent}</{ABBR_STR}:{STR_USAGE}>"
828
+ outfile += f"{indent}<{ABBR_STR}:{DATA_PROV}>"
829
+ outfile += (
830
+ f"{add_indent(indent)}<{REF} "
831
+ f"{PAR_ID}={ref_pr.id!r} "
832
+ f'{PACKAGE}="base" '
833
+ f"{PAR_VER}={ref_pr.version!r} "
834
+ f"{AGENCY_ID}={ref_pr.agency!r} "
835
+ f"{ID}={ref_pr.item_id!r} "
836
+ f"{CLASS}={DATA_PROV!r}/>"
837
+ )
838
+ outfile += f"{indent}</{ABBR_STR}:{DATA_PROV}>"
839
+
840
+ outfile = outfile.replace("'", '"')
841
+ return outfile
842
+
843
+
793
844
  def __write_scheme( # noqa: C901
794
845
  item_scheme: Any, indent: str, scheme: str, references_30: bool = False
795
846
  ) -> str:
@@ -805,10 +856,7 @@ def __write_scheme( # noqa: C901
805
856
  item_scheme, add_indent(indent), references_30
806
857
  )
807
858
 
808
- if scheme not in [
809
- DSD,
810
- DFW,
811
- ]:
859
+ if scheme not in [DSD, DFW, PROV_AGREEMENT]:
812
860
  data["Attributes"] += (
813
861
  f" isPartial={str(item_scheme.is_partial).lower()!r}"
814
862
  )
@@ -839,6 +887,13 @@ def __write_scheme( # noqa: C901
839
887
  outfile += __write_structure(
840
888
  item_scheme.structure, add_indent(indent), references_30
841
889
  )
890
+ if scheme == PROV_AGREEMENT:
891
+ outfile += __write_prov_agreement(
892
+ item_scheme.dataflow,
893
+ item_scheme.provider,
894
+ add_indent(indent),
895
+ references_30,
896
+ )
842
897
 
843
898
  if scheme not in [
844
899
  DSD,
@@ -849,6 +904,7 @@ def __write_scheme( # noqa: C901
849
904
  VTL_MAPPING_SCHEME,
850
905
  CUSTOM_TYPE_SCHEME,
851
906
  NAME_PER_SCHEME,
907
+ PROV_AGREEMENT,
852
908
  ]:
853
909
  for item in item_scheme.items:
854
910
  if (
@@ -1129,11 +1185,9 @@ def _write_vtl( # noqa: C901
1129
1185
  ref_codelist = (
1130
1186
  item_or_scheme.codelist
1131
1187
  if isinstance(item_or_scheme.codelist, Reference)
1132
- else (
1133
- parse_urn(item_or_scheme.codelist)
1134
- if isinstance(item_or_scheme.codelist, str)
1135
- else parse_short_urn(item_or_scheme.codelist.short_urn)
1136
- )
1188
+ else parse_urn(item_or_scheme.codelist)
1189
+ if isinstance(item_or_scheme.codelist, str)
1190
+ else parse_short_urn(item_or_scheme.codelist.short_urn)
1137
1191
  )
1138
1192
  if references_30:
1139
1193
  data += (
pysdmx/io/xml/__tokens.py CHANGED
@@ -13,6 +13,7 @@ POSITION = "position"
13
13
  CLASS = "class"
14
14
  PACKAGE = "package"
15
15
  LINK = "Link"
16
+ DATA_PROV = "DataProvider"
16
17
 
17
18
  # Structure Specific
18
19
  STR_SPE = "StructureSpecificData"
@@ -93,6 +94,8 @@ CON_SCHEMES = "ConceptSchemes"
93
94
  DSDS = "DataStructures"
94
95
  DATAFLOWS = "Dataflows"
95
96
  CONSTRAINTS = "Constraints"
97
+ PROV_AGREEMENTS = "ProvisionAgreements"
98
+
96
99
 
97
100
  # Individual
98
101
  AGENCY = "Agency"
@@ -69,6 +69,7 @@ CONCEPTS_SCHEMES = "ConceptSchemes"
69
69
  DSDS = "DataStructures"
70
70
  DATAFLOWS = "Dataflows"
71
71
  CONSTRAINTS = "Constraints"
72
+ PROV_AGREEMENTS = "ProvisionAgreements"
72
73
  ALL_DIM = "AllDimensions"
73
74
 
74
75
  BASE_URL_21 = "http://www.sdmx.org/resources/sdmxml/schemas/v2_1"
@@ -199,6 +200,7 @@ MSG_CONTENT_PKG_21 = OrderedDict(
199
200
  [
200
201
  (ORGS, "OrganisationSchemes"),
201
202
  (DATAFLOWS, "Dataflows"),
203
+ (PROV_AGREEMENTS, "ProvisionAgreements"),
202
204
  (CODELISTS, "Codelists"),
203
205
  (CONCEPTS, "Concepts"),
204
206
  (DSDS, "DataStructures"),
@@ -217,6 +219,7 @@ MSG_CONTENT_PKG_30 = OrderedDict(
217
219
  [
218
220
  (AGC, "AgencySchemes"),
219
221
  (DATAFLOWS, "Dataflows"),
222
+ (PROV_AGREEMENTS, "ProvisionAgreements"),
220
223
  (CODELISTS, "Codelists"),
221
224
  (CONCEPTS_SCHEMES, "ConceptSchemes"),
222
225
  (DSDS, "DataStructures"),
@@ -281,35 +284,32 @@ def __item(
281
284
  name = None
282
285
  org_id = None
283
286
 
284
- if isinstance(sender_receiver, Organisation):
285
- org_id = sender_receiver.id
286
- name = sender_receiver.name
287
-
288
- unexpected_keys = {
289
- URI_LOW,
290
- URN_LOW,
291
- DESC_LOW,
292
- CONTACTS_LOW,
293
- DFWS_LOW,
294
- ANNOTATIONS_LOW,
295
- }
296
-
297
- unexpected_with_values = {
298
- key
299
- for key in unexpected_keys
300
- if hasattr(sender_receiver, key)
301
- and getattr(sender_receiver, key) not in (None, (), [], {})
302
- }
303
-
304
- if unexpected_with_values:
305
- warnings.warn(
306
- f"The following attributes will be lost: "
307
- f"{', '.join(unexpected_with_values)}",
308
- UserWarning,
309
- stacklevel=2,
310
- )
311
- else:
312
- return ""
287
+ org_id = sender_receiver.id
288
+ name = sender_receiver.name
289
+
290
+ unexpected_keys = {
291
+ URI_LOW,
292
+ URN_LOW,
293
+ DESC_LOW,
294
+ CONTACTS_LOW,
295
+ DFWS_LOW,
296
+ ANNOTATIONS_LOW,
297
+ }
298
+
299
+ unexpected_with_values = {
300
+ key
301
+ for key in unexpected_keys
302
+ if hasattr(sender_receiver, key)
303
+ and getattr(sender_receiver, key) not in (None, (), [], {})
304
+ }
305
+
306
+ if unexpected_with_values:
307
+ warnings.warn(
308
+ f"The following attributes will be lost: "
309
+ f"{', '.join(unexpected_with_values)}",
310
+ UserWarning,
311
+ stacklevel=2,
312
+ )
313
313
 
314
314
  message = f"{nl}{child2}<{ABBR_MSG}:{element} id={org_id!r}"
315
315
  if name is not None:
@@ -395,6 +395,11 @@ def __reference(
395
395
  )
396
396
 
397
397
 
398
+ def __write_receivers(header: Header, nl: str, prettyprint: bool) -> str:
399
+ recs = [__item("Receiver", r, nl, prettyprint) for r in header.receiver]
400
+ return "".join(recs)
401
+
402
+
398
403
  def __write_header(
399
404
  header: Header,
400
405
  prettyprint: bool,
@@ -467,7 +472,7 @@ def __write_header(
467
472
  f"{__value('Test', test)}"
468
473
  f"{__value('Prepared', prepared)}"
469
474
  f"{__item('Sender', header.sender, nl, prettyprint)}"
470
- f"{__item('Receiver', header.receiver, nl, prettyprint)}"
475
+ f"{__write_receivers(header, nl, prettyprint)}"
471
476
  f"{references_str}"
472
477
  f"{__value('DataSetAction', action_value)}"
473
478
  f"{__value('DataSetID', header.dataset_id)}"
pysdmx/io/xml/header.py CHANGED
@@ -161,12 +161,18 @@ def __parse_header(header: Dict[str, Any]) -> Header:
161
161
  Header: The header of the SDMX message.
162
162
 
163
163
  """
164
+ if isinstance(header.get(RECEIVER), list):
165
+ receiver = [
166
+ __parse_sender_receiver(r) for r in header.get(RECEIVER, [])
167
+ ]
168
+ else:
169
+ receiver = __parse_sender_receiver(header.get(RECEIVER)) # type: ignore[assignment]
164
170
  dict_header = {
165
171
  "id": header.get(HEADER_ID),
166
172
  "test": header.get(TEST),
167
173
  "prepared": __parse_prepared(header.get(PREPARED)), # type: ignore[arg-type]
168
174
  "sender": __parse_sender_receiver(header.get(SENDER)),
169
- "receiver": __parse_sender_receiver(header.get(RECEIVER)),
175
+ "receiver": receiver,
170
176
  "source": __parse_source(header.get(SOURCE)),
171
177
  "dataset_action": __parse_dataset_action(header.get(DATASET_ACTION)),
172
178
  "structure": __parse_structure(header.get(STRUCTURE)),
pysdmx/model/__base.py CHANGED
@@ -373,7 +373,7 @@ class DataflowRef(
373
373
  return f"{self.__class__.__name__}({', '.join(attrs)})"
374
374
 
375
375
 
376
- class Reference(Struct, frozen=True, repr_omit_defaults=True):
376
+ class Reference(Struct, frozen=True, repr_omit_defaults=True, tag=True):
377
377
  """The coordinates of an SDMX maintainable artefact.
378
378
 
379
379
  Attributes:
pysdmx/model/category.py CHANGED
@@ -9,13 +9,15 @@ from typing import Iterator, Optional, Sequence, Union
9
9
  from pysdmx.model.__base import (
10
10
  DataflowRef,
11
11
  Item,
12
+ ItemReference,
12
13
  ItemScheme,
13
14
  MaintainableArtefact,
15
+ Reference,
14
16
  )
15
17
  from pysdmx.model.dataflow import Dataflow
16
18
 
17
19
 
18
- class Category(Item, frozen=False, omit_defaults=True): # type: ignore[misc]
20
+ class Category(Item, frozen=True, omit_defaults=True):
19
21
  """A category, ie a way to **organize and group** things.
20
22
 
21
23
  Categories are used to organize and group other artefacts in SDMX.
@@ -34,10 +36,13 @@ class Category(Item, frozen=False, omit_defaults=True): # type: ignore[misc]
34
36
  categories: The sub-categories, i.e. the categories belonging
35
37
  to the category.
36
38
  dataflows: The list of dataflows attached to the category.
39
+ other_references: References to other types artefacts (i.e. not
40
+ dataflows) attached to the category.
37
41
  """
38
42
 
39
43
  categories: Sequence["Category"] = ()
40
44
  dataflows: Sequence[Union[Dataflow, DataflowRef]] = ()
45
+ other_references: Sequence[Union[ItemReference, Reference]] = ()
41
46
 
42
47
  def __iter__(self) -> Iterator["Category"]:
43
48
  """Return an iterator over the list of categories."""
pysdmx/model/message.py CHANGED
@@ -19,7 +19,7 @@ from typing import Any, Dict, List, Optional, Sequence, Type, Union
19
19
  from msgspec import Struct
20
20
 
21
21
  from pysdmx.errors import Invalid, NotFound
22
- from pysdmx.model.__base import ItemScheme, MaintainableArtefact, Organisation
22
+ from pysdmx.model.__base import MaintainableArtefact, Organisation
23
23
  from pysdmx.model.category import Categorisation, CategoryScheme
24
24
  from pysdmx.model.code import Codelist, Hierarchy, HierarchyAssociation
25
25
  from pysdmx.model.concept import ConceptScheme
@@ -87,7 +87,7 @@ class Header(Struct, repr_omit_defaults=True, kw_only=True):
87
87
  test: bool = False
88
88
  prepared: datetime = datetime.now(timezone.utc)
89
89
  sender: Organisation = Organisation(id="ZZZ")
90
- receiver: Optional[Organisation] = None
90
+ receiver: Sequence[Organisation] = ()
91
91
  source: Optional[str] = None
92
92
  dataset_action: Optional[ActionType] = None
93
93
  structure: Optional[Dict[str, str]] = None
@@ -99,7 +99,13 @@ class Header(Struct, repr_omit_defaults=True, kw_only=True):
99
99
  self.sender = Organisation(id=self.sender)
100
100
 
101
101
  if isinstance(self.receiver, str):
102
- self.receiver = Organisation(id=self.receiver)
102
+ self.receiver = [Organisation(id=self.receiver)]
103
+
104
+ if isinstance(self.receiver, Organisation):
105
+ self.receiver = [self.receiver]
106
+
107
+ if self.receiver is None:
108
+ self.receiver = ()
103
109
 
104
110
  def __str__(self) -> str:
105
111
  """Custom string representation without the class name."""
@@ -171,7 +177,9 @@ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
171
177
  attrs.append(f"{attr}={repr(value)}")
172
178
  return f"{self.__class__.__name__}({', '.join(attrs)})"
173
179
 
174
- def __get_elements(self, type_: Type[Any]) -> List[Any]:
180
+ # Returns MaintainableArtefacts only, but mypy complains.
181
+ # As it is an internal method, it's acceptable.
182
+ def __get_elements(self, type_: Type[MaintainableArtefact]) -> List[Any]:
175
183
  """Returns a list of elements of a specific type."""
176
184
  if self.structures is None:
177
185
  raise NotFound(
@@ -180,6 +188,8 @@ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
180
188
  structures = [e for e in self.structures if isinstance(e, type_)]
181
189
  return structures
182
190
 
191
+ # Returns Codelist or ValueList only, but mypy complains.
192
+ # As it is an internal method, it's acceptable.
183
193
  def __get_enumerations(
184
194
  self, type_: Type[Any], is_vl: bool = False
185
195
  ) -> List[Any]:
@@ -188,9 +198,11 @@ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
188
198
  t = "valuelist" if is_vl else "codelist"
189
199
  return [e for e in enums if e.sdmx_type == t]
190
200
 
201
+ # Returns MaintainableArtefacts only,
202
+ # but mypy complains. As it is an internal method, it's acceptable.
191
203
  def __get_single_structure(
192
204
  self,
193
- type_: Type[Union[ItemScheme, DataStructureDefinition, Dataflow]],
205
+ type_: Type[MaintainableArtefact],
194
206
  short_urn: str,
195
207
  ) -> Any:
196
208
  """Returns a specific element from content."""
@@ -235,7 +247,7 @@ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
235
247
  return self.__get_elements(Metadataflow)
236
248
 
237
249
  def get_organisation_scheme(self, short_urn: str) -> AgencyScheme:
238
- """Returns a specific OrganisationScheme."""
250
+ """Returns a specific AgencyScheme."""
239
251
  return self.__get_single_structure(AgencyScheme, short_urn)
240
252
 
241
253
  def get_codelist(self, short_urn: str) -> Codelist:
@@ -256,6 +268,10 @@ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
256
268
  """Returns a specific Dataflow."""
257
269
  return self.__get_single_structure(Dataflow, short_urn)
258
270
 
271
+ def get_provision_agreement(self, short_urn: str) -> ProvisionAgreement:
272
+ """Returns a specific Provision Agreement."""
273
+ return self.__get_single_structure(ProvisionAgreement, short_urn)
274
+
259
275
  def get_transformation_schemes(self) -> List[TransformationScheme]:
260
276
  """Returns the TransformationSchemes."""
261
277
  return self.__get_elements(TransformationScheme)
@@ -5,7 +5,7 @@ from pysdmx.model.message import Message
5
5
  from pysdmx.util import parse_urn
6
6
 
7
7
 
8
- def schema_generator(message: Message, dataset_ref: Reference) -> Schema:
8
+ def schema_generator(message: Message, dataset_ref: Reference) -> Schema: # noqa: C901
9
9
  """Generates a Schema by resolving the short_urn in the message."""
10
10
  context = dataset_ref.sdmx_type.lower()
11
11
  if context == "datastructure":
@@ -47,8 +47,45 @@ def schema_generator(message: Message, dataset_ref: Reference) -> Schema:
47
47
  artefacts=dsd.to_schema().artefacts,
48
48
  )
49
49
  elif context == "provisionagreement":
50
- raise NotImplementedError(
51
- "ProvisionAgreement schema generation is not supported yet."
50
+ try:
51
+ prov_agree = message.get_provision_agreement(str(dataset_ref))
52
+ except NotFound:
53
+ raise Invalid(
54
+ f"Missing Provision Agreement {dataset_ref} "
55
+ f"in structures message.",
56
+ ) from None
57
+ if prov_agree.dataflow is None:
58
+ raise Invalid(
59
+ f"Provision Agreement {dataset_ref} does not have a "
60
+ f"Dataflow defined.",
61
+ )
62
+ dfw_ref = parse_urn(prov_agree.dataflow)
63
+ try:
64
+ dataflow = message.get_dataflow(str(dfw_ref))
65
+ except NotFound:
66
+ raise Invalid(
67
+ f"Missing Dataflow in {dataset_ref} structures message.",
68
+ ) from None
69
+ if dataflow.structure is None:
70
+ raise Invalid(
71
+ f"Dataflow {dataset_ref} does not have a structure defined.",
72
+ )
73
+ dsd_ref = parse_urn(dataflow.structure) # type: ignore[arg-type]
74
+ try:
75
+ dsd = message.get_data_structure_definition(str(dsd_ref))
76
+ except NotFound:
77
+ raise Invalid(
78
+ f"Not found referenced DataStructure {dsd_ref}"
79
+ f" from Provision Agreement {dataset_ref}.",
80
+ ) from None
81
+ return Schema(
82
+ context=context, # type: ignore[arg-type]
83
+ id=dataset_ref.id,
84
+ version=dataset_ref.version,
85
+ agency=dataset_ref.agency,
86
+ groups=dsd.groups,
87
+ components=dsd.components,
88
+ artefacts=dsd.to_schema().artefacts,
52
89
  )
53
90
  else:
54
91
  raise Invalid(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysdmx
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: Your opinionated Python SDMX library
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -16,9 +16,11 @@ Classifier: Typing :: Typed
16
16
  Provides-Extra: all
17
17
  Provides-Extra: data
18
18
  Provides-Extra: dc
19
+ Provides-Extra: json
19
20
  Provides-Extra: vtl
20
21
  Provides-Extra: xml
21
22
  Requires-Dist: httpx[http2] (>=0)
23
+ Requires-Dist: jsonschema (>=4.10) ; extra == "json"
22
24
  Requires-Dist: lxml (>=5.2) ; extra == "all"
23
25
  Requires-Dist: lxml (>=5.2) ; extra == "xml"
24
26
  Requires-Dist: msgspec (>=0)
@@ -28,6 +30,7 @@ Requires-Dist: parsy (>=2.1)
28
30
  Requires-Dist: python-dateutil (>=2.8.2) ; extra == "all"
29
31
  Requires-Dist: python-dateutil (>=2.8.2) ; extra == "dc"
30
32
  Requires-Dist: sdmxschemas (>=1.0.0) ; extra == "all"
33
+ Requires-Dist: sdmxschemas (>=1.0.0) ; extra == "json"
31
34
  Requires-Dist: sdmxschemas (>=1.0.0) ; extra == "xml"
32
35
  Requires-Dist: vtlengine (>=1.1,<2.0) ; extra == "all"
33
36
  Requires-Dist: vtlengine (>=1.1,<2.0) ; extra == "vtl"