pysdmx 1.10.0rc1__py3-none-any.whl → 1.10.0rc2__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 (33) hide show
  1. pysdmx/__init__.py +1 -1
  2. pysdmx/api/fmr/__init__.py +3 -2
  3. pysdmx/io/_pd_utils.py +83 -0
  4. pysdmx/io/csv/__csv_aux_writer.py +23 -0
  5. pysdmx/io/csv/sdmx10/reader/__init__.py +1 -1
  6. pysdmx/io/csv/sdmx10/writer/__init__.py +15 -9
  7. pysdmx/io/csv/sdmx20/reader/__init__.py +1 -1
  8. pysdmx/io/csv/sdmx20/writer/__init__.py +1 -1
  9. pysdmx/io/csv/sdmx21/reader/__init__.py +1 -1
  10. pysdmx/io/csv/sdmx21/writer/__init__.py +1 -1
  11. pysdmx/io/json/sdmxjson2/messages/__init__.py +4 -0
  12. pysdmx/io/json/sdmxjson2/messages/code.py +16 -6
  13. pysdmx/io/json/sdmxjson2/messages/constraint.py +235 -16
  14. pysdmx/io/json/sdmxjson2/messages/dsd.py +35 -7
  15. pysdmx/io/json/sdmxjson2/messages/map.py +5 -4
  16. pysdmx/io/json/sdmxjson2/messages/metadataflow.py +1 -0
  17. pysdmx/io/json/sdmxjson2/messages/msd.py +18 -10
  18. pysdmx/io/json/sdmxjson2/messages/schema.py +2 -2
  19. pysdmx/io/json/sdmxjson2/messages/structure.py +81 -44
  20. pysdmx/io/json/sdmxjson2/messages/vtl.py +13 -9
  21. pysdmx/io/xml/__write_data_aux.py +20 -7
  22. pysdmx/io/xml/__write_structure_specific_aux.py +71 -54
  23. pysdmx/io/xml/sdmx21/writer/generic.py +31 -19
  24. pysdmx/model/__base.py +46 -1
  25. pysdmx/model/__init__.py +18 -0
  26. pysdmx/model/category.py +17 -0
  27. pysdmx/model/concept.py +16 -0
  28. pysdmx/model/constraint.py +69 -0
  29. pysdmx/model/message.py +80 -71
  30. {pysdmx-1.10.0rc1.dist-info → pysdmx-1.10.0rc2.dist-info}/METADATA +1 -1
  31. {pysdmx-1.10.0rc1.dist-info → pysdmx-1.10.0rc2.dist-info}/RECORD +33 -31
  32. {pysdmx-1.10.0rc1.dist-info → pysdmx-1.10.0rc2.dist-info}/WHEEL +0 -0
  33. {pysdmx-1.10.0rc1.dist-info → pysdmx-1.10.0rc2.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.0rc1"
3
+ __version__ = "1.10.0rc2"
@@ -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:
pysdmx/io/_pd_utils.py ADDED
@@ -0,0 +1,83 @@
1
+ import pandas as pd
2
+
3
+ from pysdmx.errors import Invalid
4
+ from pysdmx.model.concept import DataType
5
+ from pysdmx.model.dataflow import Schema
6
+
7
+ NUMERIC_TYPES = {
8
+ DataType.BIG_INTEGER,
9
+ DataType.COUNT,
10
+ DataType.DECIMAL,
11
+ DataType.DOUBLE,
12
+ DataType.FLOAT,
13
+ DataType.INCREMENTAL,
14
+ DataType.INTEGER,
15
+ DataType.LONG,
16
+ DataType.SHORT,
17
+ }
18
+
19
+
20
+ def _fill_na_values(data: pd.DataFrame, structure: Schema) -> pd.DataFrame:
21
+ """Fills missing values in the DataFrame based on the component type.
22
+
23
+ Numeric components are filled with "NaN".
24
+ Other components are filled with "#N/A".
25
+ If the structure does not have components,
26
+ all missing values are filled with "".
27
+
28
+ Args:
29
+ data: The DataFrame to fill.
30
+ structure: The structure definition (´Schema´).
31
+
32
+ Returns:
33
+ The DataFrame with filled missing values.
34
+
35
+ Raises:
36
+ Invalid: If the structure does not have components.
37
+ """
38
+ for component in structure.components:
39
+ if component.id in data.columns:
40
+ if component.dtype in NUMERIC_TYPES:
41
+ data[component.id] = (
42
+ data[component.id].astype(object).fillna("NaN")
43
+ )
44
+ else:
45
+ data[component.id] = (
46
+ data[component.id].astype(object).fillna("#N/A")
47
+ )
48
+
49
+ return data
50
+
51
+
52
+ def _validate_explicit_null_values(
53
+ data: pd.DataFrame, structure: Schema
54
+ ) -> None:
55
+ """Validates that explicit null values are correct for the component type.
56
+
57
+ Numeric components must not contain "#N/A".
58
+ Non-numeric components must not contain "NaN".
59
+
60
+ Args:
61
+ data: The DataFrame to validate.
62
+ structure: The structure definition (´Schema´).
63
+
64
+ Raises:
65
+ Invalid: If invalid null values are found.
66
+ """
67
+ for component in structure.components:
68
+ if component.id in data.columns:
69
+ series = data[component.id].astype(str)
70
+ if component.dtype in NUMERIC_TYPES:
71
+ # Numeric: #N/A is invalid
72
+ if series.isin(["#N/A"]).any():
73
+ raise Invalid(
74
+ f"Invalid null value '#N/A' in numeric component "
75
+ f"'{component.id}'."
76
+ )
77
+ else:
78
+ # Non-numeric: NaN is invalid
79
+ if series.isin(["NaN"]).any():
80
+ raise Invalid(
81
+ f"Invalid null value 'NaN' in non-numeric component "
82
+ f"'{component.id}'."
83
+ )
@@ -3,6 +3,8 @@ from typing import List, Literal, Optional, Sequence
3
3
 
4
4
  import pandas as pd
5
5
 
6
+ from pysdmx.errors import Invalid
7
+ from pysdmx.io._pd_utils import _fill_na_values
6
8
  from pysdmx.io.pd import PandasDataset
7
9
  from pysdmx.model import Schema
8
10
  from pysdmx.model.dataset import ActionType
@@ -16,6 +18,25 @@ SDMX_CSV_ACTION_MAPPER = {
16
18
  }
17
19
 
18
20
 
21
+ def _validate_schema_exists(dataset: PandasDataset) -> Schema:
22
+ """Validates that the dataset has a Schema defined.
23
+
24
+ Args:
25
+ dataset: The dataset to validate.
26
+
27
+ Returns:
28
+ The `Schema` from the dataset.
29
+
30
+ Raises:
31
+ Invalid: If the structure is not a `Schema`.
32
+ """
33
+ if not isinstance(dataset.structure, Schema):
34
+ raise Invalid(
35
+ "Dataset Structure is not a Schema. Cannot perform operation."
36
+ )
37
+ return dataset.structure
38
+
39
+
19
40
  def __write_time_period(df: pd.DataFrame, time_format: str) -> None:
20
41
  # TODO: Correct handle of normalized time format
21
42
  raise NotImplementedError("Normalized time format is not implemented yet.")
@@ -70,8 +91,10 @@ def _write_csv_2_aux(
70
91
  ) -> List[pd.DataFrame]:
71
92
  dataframes = []
72
93
  for dataset in datasets:
94
+ schema = _validate_schema_exists(dataset)
73
95
  # Create a copy of the dataset
74
96
  df: pd.DataFrame = copy(dataset.data)
97
+ df = _fill_na_values(df, schema)
75
98
  structure_ref, unique_id = dataset.short_urn.split("=", maxsplit=1)
76
99
 
77
100
  # Add additional attributes to the dataset
@@ -24,7 +24,7 @@ def read(input_str: str) -> Sequence[PandasDataset]:
24
24
  """
25
25
  # Get Dataframe from CSV file
26
26
  df_csv = pd.read_csv(
27
- StringIO(input_str), keep_default_na=False, na_values=[""]
27
+ StringIO(input_str), keep_default_na=False, na_values=[]
28
28
  )
29
29
  # Drop empty columns
30
30
  df_csv = df_csv.dropna(axis=1, how="all")
@@ -6,9 +6,12 @@ from typing import Literal, Optional, Sequence, Union
6
6
 
7
7
  import pandas as pd
8
8
 
9
- from pysdmx.io.csv.__csv_aux_writer import __write_time_period
9
+ from pysdmx.io._pd_utils import _fill_na_values
10
+ from pysdmx.io.csv.__csv_aux_writer import (
11
+ __write_time_period,
12
+ _validate_schema_exists,
13
+ )
10
14
  from pysdmx.io.pd import PandasDataset
11
- from pysdmx.model import Schema
12
15
  from pysdmx.toolkit.pd._data_utils import format_labels
13
16
 
14
17
 
@@ -44,22 +47,26 @@ def write(
44
47
  # Create a copy of the dataset
45
48
  dataframes = []
46
49
  for dataset in datasets:
50
+ # Validate that dataset has a proper Schema
51
+ schema = _validate_schema_exists(dataset)
52
+
47
53
  df: pd.DataFrame = copy(dataset.data)
48
54
 
55
+ # Fill missing values
56
+ df = _fill_na_values(df, schema)
57
+
49
58
  # Add additional attributes to the dataset
50
59
  for k, v in dataset.attributes.items():
51
60
  df[k] = v
52
61
  structure_id = dataset.short_urn.split("=")[1]
53
62
  if time_format is not None and time_format != "original":
54
63
  __write_time_period(df, time_format)
55
- if labels is not None and isinstance(dataset.structure, Schema):
56
- format_labels(df, labels, dataset.structure.components)
64
+ if labels is not None:
65
+ format_labels(df, labels, schema.components)
57
66
  if labels == "id":
58
67
  df.insert(0, "DATAFLOW", structure_id)
59
68
  else:
60
- df.insert(
61
- 0, "DATAFLOW", f"{structure_id}:{dataset.structure.name}"
62
- )
69
+ df.insert(0, "DATAFLOW", f"{structure_id}:{schema.name}")
63
70
  else:
64
71
  df.insert(0, "DATAFLOW", structure_id)
65
72
 
@@ -68,8 +75,7 @@ def write(
68
75
  # Concatenate the dataframes
69
76
  all_data = pd.concat(dataframes, ignore_index=True, axis=0)
70
77
 
71
- # Ensure null values are represented as empty strings
72
- all_data = all_data.astype(str).replace({"nan": "", "<NA>": ""})
78
+ all_data = all_data.astype(str)
73
79
  # If the output path is an empty string we use None
74
80
  output_path = (
75
81
  None
@@ -24,7 +24,7 @@ def read(input_str: str) -> Sequence[PandasDataset]:
24
24
  """
25
25
  # Get Dataframe from CSV file
26
26
  df_csv = pd.read_csv(
27
- StringIO(input_str), keep_default_na=False, na_values=[""]
27
+ StringIO(input_str), keep_default_na=False, na_values=[]
28
28
  )
29
29
  # Drop empty columns
30
30
  df_csv = df_csv.dropna(axis=1, how="all")
@@ -60,7 +60,7 @@ def write(
60
60
 
61
61
  all_data = pd.concat(dataframes, ignore_index=True, axis=0)
62
62
 
63
- all_data = all_data.astype(str).replace({"nan": "", "<NA>": ""})
63
+ all_data = all_data.astype(str)
64
64
 
65
65
  # If the output path is an empty string we use None
66
66
  output_path = (
@@ -24,7 +24,7 @@ def read(input_str: str) -> Sequence[PandasDataset]:
24
24
  """
25
25
  # Get Dataframe from CSV file
26
26
  df_csv = pd.read_csv(
27
- StringIO(input_str), keep_default_na=False, na_values=[""]
27
+ StringIO(input_str), keep_default_na=False, na_values=[]
28
28
  )
29
29
  # Drop empty columns
30
30
  df_csv = df_csv.dropna(axis=1, how="all")
@@ -57,7 +57,7 @@ def write(
57
57
 
58
58
  all_data = pd.concat(dataframes, ignore_index=True, axis=0)
59
59
 
60
- all_data = all_data.astype(str).replace({"nan": "", "<NA>": ""})
60
+ all_data = all_data.astype(str)
61
61
 
62
62
  # If the output path is an empty string we use None
63
63
  output_path = (
@@ -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",
@@ -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
 
@@ -1,16 +1,43 @@
1
1
  """Collection of SDMX-JSON schemas for content constraints."""
2
2
 
3
- from typing import Dict, Literal, Optional, Sequence
3
+ from datetime import datetime
4
+ from typing import Optional, Sequence
4
5
 
5
6
  from msgspec import Struct
6
7
 
7
- from pysdmx.io.json.sdmxjson2.messages.core import MaintainableType
8
+ from pysdmx import errors
9
+ from pysdmx.io.json.sdmxjson2.messages.core import (
10
+ JsonAnnotation,
11
+ MaintainableType,
12
+ )
13
+ from pysdmx.model import (
14
+ Agency,
15
+ ConstraintAttachment,
16
+ CubeKeyValue,
17
+ CubeRegion,
18
+ CubeValue,
19
+ DataConstraint,
20
+ DataKey,
21
+ DataKeyValue,
22
+ KeySet,
23
+ )
8
24
 
9
25
 
10
26
  class JsonValue(Struct, frozen=True, omit_defaults=True):
11
- """SDMX-JSON payload for an allowed value."""
27
+ """SDMX-JSON payload for a cube value."""
12
28
 
13
29
  value: str
30
+ validFrom: Optional[datetime] = None
31
+ validTo: Optional[datetime] = None
32
+
33
+ def to_model(self) -> CubeValue:
34
+ """Converts a JsonValue to a CubeValue."""
35
+ return CubeValue(self.value, self.validFrom, self.validTo)
36
+
37
+ @classmethod
38
+ def from_model(self, cv: CubeValue) -> "JsonValue":
39
+ """Converts a pysdmx cube value to an SDMX-JSON one."""
40
+ return JsonValue(cv.value, cv.valid_from, cv.valid_to)
14
41
 
15
42
 
16
43
  class JsonKeyValue(Struct, frozen=True, omit_defaults=True):
@@ -18,36 +45,228 @@ class JsonKeyValue(Struct, frozen=True, omit_defaults=True):
18
45
 
19
46
  id: str
20
47
  values: Sequence[JsonValue]
48
+ # Additional properties are supported in the model (include,
49
+ # removePrefix, validFrom, validTo, timeRange) but not by the FMR.
50
+ # Therefore, they are ignored for now.
51
+
52
+ def to_model(self) -> CubeKeyValue:
53
+ """Converts a JsonKeyValue to a CubeKeyValue."""
54
+ return CubeKeyValue(self.id, [v.to_model() for v in self.values])
21
55
 
22
- def to_model(self) -> Sequence[str]:
23
- """Returns the requested list of values."""
24
- return [v.value for v in self.values]
56
+ @classmethod
57
+ def from_model(self, key_value: CubeKeyValue) -> "JsonKeyValue":
58
+ """Converts a pysdmx cube key value to an SDMX-JSON one."""
59
+ return JsonKeyValue(
60
+ key_value.id, [JsonValue.from_model(v) for v in key_value.values]
61
+ )
25
62
 
26
63
 
27
64
  class JsonCubeRegion(Struct, frozen=True, omit_defaults=True):
28
65
  """SDMX-JSON payload for a cube region."""
29
66
 
67
+ # The property `components` is ignored as it's not used in the FMR`
30
68
  keyValues: Sequence[JsonKeyValue]
69
+ include: bool = True
31
70
 
32
- def to_map(self) -> Dict[str, Sequence[str]]:
33
- """Gets the list of allowed values for a component."""
34
- return {kv.id: kv.to_model() for kv in self.keyValues}
71
+ def to_model(self) -> CubeRegion:
72
+ """Converts a JsonCubeRegion to a CubeRegion."""
73
+ return CubeRegion(
74
+ [kv.to_model() for kv in self.keyValues], self.include
75
+ )
76
+
77
+ @classmethod
78
+ def from_model(self, region: CubeRegion) -> "JsonCubeRegion":
79
+ """Converts a pysdmx cube region to an SDMX-JSON one."""
80
+ return JsonCubeRegion(
81
+ [JsonKeyValue.from_model(kv) for kv in region.key_values],
82
+ region.is_included,
83
+ )
35
84
 
36
85
 
37
86
  class JsonConstraintAttachment(Struct, frozen=True, omit_defaults=True):
38
87
  """SDMX-JSON payload for a constraint attachment."""
39
88
 
40
- dataProvider: Optional[str]
41
- simpleDataSources: Optional[Sequence[str]] = None
42
- dataStructures: Optional[Sequence[str]] = None
43
- dataflows: Optional[Sequence[str]] = None
44
- provisionAgreements: Optional[Sequence[str]] = None
45
- queryableDataSources: Optional[Sequence[str]] = None
89
+ dataProvider: Optional[str] = None
90
+ dataStructures: Sequence[str] = ()
91
+ dataflows: Sequence[str] = ()
92
+ provisionAgreements: Sequence[str] = ()
93
+
94
+ def to_model(self) -> ConstraintAttachment:
95
+ """Converts a JsonConstraintAttachment to a ConstraintAttachment."""
96
+ return ConstraintAttachment(
97
+ self.dataProvider,
98
+ self.dataStructures,
99
+ self.dataflows,
100
+ self.provisionAgreements,
101
+ )
102
+
103
+ @classmethod
104
+ def from_model(
105
+ self, attachment: ConstraintAttachment
106
+ ) -> "JsonConstraintAttachment":
107
+ """Converts a pysdmx constraint attachment to an SDMX-JSON one."""
108
+ ds = attachment.data_structures if attachment.data_structures else ()
109
+ df = attachment.dataflows if attachment.dataflows else ()
110
+ pa = (
111
+ attachment.provision_agreements
112
+ if attachment.provision_agreements
113
+ else ()
114
+ )
115
+ return JsonConstraintAttachment(attachment.data_provider, ds, df, pa)
116
+
117
+
118
+ class JsonDataKeyValue(Struct, frozen=True, omit_defaults=True):
119
+ """SDMX-JSON payload for a data key value."""
120
+
121
+ id: str
122
+ value: str
123
+
124
+ def to_model(self) -> DataKeyValue:
125
+ """Converts a JsonDataKeyValue to a DataKeyValue."""
126
+ return DataKeyValue(self.id, self.value)
127
+
128
+ @classmethod
129
+ def from_model(self, kv: DataKeyValue) -> "JsonDataKeyValue":
130
+ """Converts a pysdmx key value to an SDMX-JSON one."""
131
+ return JsonDataKeyValue(kv.id, kv.value)
132
+
133
+
134
+ class JsonDataKey(Struct, frozen=True, omit_defaults=True):
135
+ """SDMX-JSON payload for a data key."""
136
+
137
+ keyValues: Sequence[JsonDataKeyValue]
138
+ validFrom: Optional[datetime] = None
139
+ validTo: Optional[datetime] = None
140
+
141
+ def to_model(self) -> DataKey:
142
+ """Converts a JsonDataKey to a DataKey."""
143
+ return DataKey(
144
+ [kv.to_model() for kv in self.keyValues],
145
+ self.validFrom,
146
+ self.validTo,
147
+ )
148
+
149
+ @classmethod
150
+ def from_model(self, kv: DataKey) -> "JsonDataKey":
151
+ """Converts a pysdmx key constraint to an SDMX-JSON one."""
152
+ return JsonDataKey(
153
+ [JsonDataKeyValue.from_model(val) for val in kv.keys_values],
154
+ kv.valid_from,
155
+ kv.valid_to,
156
+ )
157
+
158
+
159
+ class JsonKeySet(Struct, frozen=True, omit_defaults=True):
160
+ """SDMX-JSON payload for a keyset."""
161
+
162
+ keys: Sequence[JsonDataKey]
163
+ isIncluded: bool
164
+
165
+ def to_model(self) -> KeySet:
166
+ """Converts a JsonKeySet to a KeySet."""
167
+ return KeySet([k.to_model() for k in self.keys], self.isIncluded)
168
+
169
+ @classmethod
170
+ def from_model(self, ks: KeySet) -> "JsonKeySet":
171
+ """Converts a pysdmx key set constraint to an SDMX-JSON one."""
172
+ return JsonKeySet(
173
+ [JsonDataKey.from_model(k) for k in ks.keys], ks.is_included
174
+ )
46
175
 
47
176
 
48
177
  class JsonDataConstraint(MaintainableType, frozen=True, omit_defaults=True):
49
178
  """SDMX-JSON payload for a content constraint."""
50
179
 
51
- role: Optional[Literal["Allowed", "Actual"]] = None
52
180
  constraintAttachment: Optional[JsonConstraintAttachment] = None
53
181
  cubeRegions: Optional[Sequence[JsonCubeRegion]] = None
182
+ dataKeySets: Optional[Sequence[JsonKeySet]] = None
183
+
184
+ def to_model(self) -> DataConstraint:
185
+ """Converts a JsonDataConstraint to a pysdmx Data Constraint."""
186
+ at = self.constraintAttachment.to_model() # type: ignore[union-attr]
187
+ return DataConstraint(
188
+ id=self.id,
189
+ name=self.name,
190
+ agency=self.agency,
191
+ description=self.description,
192
+ version=self.version,
193
+ annotations=tuple([a.to_model() for a in self.annotations]),
194
+ is_external_reference=self.isExternalReference,
195
+ valid_from=self.validFrom,
196
+ valid_to=self.validTo,
197
+ constraint_attachment=at,
198
+ cube_regions=[r.to_model() for r in self.cubeRegions]
199
+ if self.cubeRegions
200
+ else (),
201
+ key_sets=[s.to_model() for s in self.dataKeySets]
202
+ if self.dataKeySets
203
+ else (),
204
+ )
205
+
206
+ @classmethod
207
+ def from_model(self, cons: DataConstraint) -> "JsonDataConstraint":
208
+ """Converts a pysdmx constraint to an SDMX-JSON one."""
209
+ crs = (
210
+ [JsonCubeRegion.from_model(r) for r in cons.cube_regions]
211
+ if cons.cube_regions
212
+ else None
213
+ )
214
+ dks = (
215
+ [JsonKeySet.from_model(s) for s in cons.key_sets]
216
+ if cons.key_sets
217
+ else None
218
+ )
219
+ if not cons.name:
220
+ raise errors.Invalid(
221
+ "Invalid input",
222
+ "SDMX-JSON data constraints must have a name",
223
+ {"data_constraint": cons.id},
224
+ )
225
+ if not cons.constraint_attachment:
226
+ raise errors.Invalid(
227
+ "Invalid input",
228
+ "SDMX-JSON data constraints must have a constraint attachment",
229
+ {"data_constraint": cons.id},
230
+ )
231
+ return JsonDataConstraint(
232
+ id=cons.id,
233
+ name=cons.name,
234
+ agency=(
235
+ cons.agency.id
236
+ if isinstance(cons.agency, Agency)
237
+ else cons.agency
238
+ ),
239
+ description=cons.description,
240
+ version=cons.version,
241
+ annotations=tuple(
242
+ [JsonAnnotation.from_model(a) for a in cons.annotations]
243
+ ),
244
+ isExternalReference=cons.is_external_reference,
245
+ validFrom=cons.valid_from,
246
+ validTo=cons.valid_to,
247
+ constraintAttachment=JsonConstraintAttachment.from_model(
248
+ cons.constraint_attachment
249
+ ),
250
+ cubeRegions=crs,
251
+ dataKeySets=dks,
252
+ )
253
+
254
+
255
+ class JsonDataConstraints(Struct, frozen=True, omit_defaults=True):
256
+ """SDMX-JSON payload for data constraints."""
257
+
258
+ dataConstraints: Sequence[JsonDataConstraint] = ()
259
+
260
+ def to_model(self) -> Sequence[DataConstraint]:
261
+ """Returns the requested data constraints."""
262
+ return [cc.to_model() for cc in self.dataConstraints]
263
+
264
+
265
+ class JsonDataConstraintMessage(Struct, frozen=True, omit_defaults=True):
266
+ """SDMX-JSON payload for /dataconstraint queries."""
267
+
268
+ data: JsonDataConstraints
269
+
270
+ def to_model(self) -> Sequence[DataConstraint]:
271
+ """Returns the requested data constraints."""
272
+ return self.data.to_model()