pysdmx 1.10.0rc1__py3-none-any.whl → 1.10.1__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.
@@ -15,7 +15,6 @@ from pysdmx.io.json.sdmxjson2.messages.core import (
15
15
  from pysdmx.io.json.sdmxjson2.messages.dsd import (
16
16
  _find_concept,
17
17
  _get_concept_reference,
18
- _get_json_representation,
19
18
  _get_representation,
20
19
  )
21
20
  from pysdmx.model import (
@@ -28,6 +27,13 @@ from pysdmx.model import (
28
27
  from pysdmx.util import parse_item_urn
29
28
 
30
29
 
30
+ def _get_attr_repr(comp: MetadataComponent) -> Optional[JsonRepresentation]:
31
+ enum = comp.local_enum_ref if comp.local_enum_ref else None
32
+ return JsonRepresentation.from_model(
33
+ comp.local_dtype, enum, comp.local_facets, None
34
+ )
35
+
36
+
31
37
  class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
32
38
  """SDMX-JSON payload for an attribute."""
33
39
 
@@ -80,11 +86,13 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
80
86
  def from_model(self, cmp: MetadataComponent) -> "JsonMetadataAttribute":
81
87
  """Converts a pysdmx metadata attribute to an SDMX-JSON one."""
82
88
  concept = _get_concept_reference(cmp)
83
- repr = _get_json_representation(cmp)
89
+ repr = _get_attr_repr(cmp)
84
90
 
85
91
  min_occurs = cmp.array_def.min_size if cmp.array_def else 0
86
- if cmp.array_def is None or cmp.array_def.max_size is None:
87
- max_occurs: Union[int, Literal["unbounded"]] = "unbounded"
92
+ if cmp.array_def is None:
93
+ max_occurs: Union[int, Literal["unbounded"]] = 1
94
+ elif cmp.array_def.max_size is None:
95
+ max_occurs = "unbounded"
88
96
  else:
89
97
  max_occurs = cmp.array_def.max_size
90
98
 
@@ -95,9 +103,9 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
95
103
  minOccurs=min_occurs,
96
104
  maxOccurs=max_occurs,
97
105
  isPresentational=cmp.is_presentational,
98
- metadataAttributes=[
99
- JsonMetadataAttribute.from_model(c) for c in cmp.components
100
- ],
106
+ metadataAttributes=tuple(
107
+ [JsonMetadataAttribute.from_model(c) for c in cmp.components]
108
+ ),
101
109
  )
102
110
 
103
111
 
@@ -119,9 +127,9 @@ class JsonMetadataAttributes(Struct, frozen=True, omit_defaults=True):
119
127
  ) -> "JsonMetadataAttributes":
120
128
  """Converts a pysdmx list of metadata attributes to SDMX-JSON."""
121
129
  return JsonMetadataAttributes(
122
- metadataAttributes=[
123
- JsonMetadataAttribute.from_model(a) for a in attributes
124
- ]
130
+ metadataAttributes=tuple(
131
+ [JsonMetadataAttribute.from_model(a) for a in attributes]
132
+ )
125
133
  )
126
134
 
127
135
 
@@ -21,7 +21,7 @@ class JsonSchemas(msgspec.Struct, frozen=True, omit_defaults=True):
21
21
  dataStructures: Sequence[JsonDataStructure]
22
22
  valuelists: Sequence[JsonValuelist] = ()
23
23
  codelists: Sequence[JsonCodelist] = ()
24
- contentConstraints: Sequence[JsonDataConstraint] = ()
24
+ dataConstraints: Sequence[JsonDataConstraint] = ()
25
25
 
26
26
  def to_model(
27
27
  self,
@@ -32,7 +32,7 @@ class JsonSchemas(msgspec.Struct, frozen=True, omit_defaults=True):
32
32
  self.conceptSchemes,
33
33
  self.codelists,
34
34
  self.valuelists,
35
- self.contentConstraints,
35
+ self.dataConstraints,
36
36
  )
37
37
  return comps, grps # type: ignore[return-value]
38
38
 
@@ -17,6 +17,7 @@ from pysdmx.io.json.sdmxjson2.messages.code import (
17
17
  JsonValuelist,
18
18
  )
19
19
  from pysdmx.io.json.sdmxjson2.messages.concept import JsonConceptScheme
20
+ from pysdmx.io.json.sdmxjson2.messages.constraint import JsonDataConstraint
20
21
  from pysdmx.io.json.sdmxjson2.messages.core import JsonHeader
21
22
  from pysdmx.io.json.sdmxjson2.messages.dataflow import JsonDataflow
22
23
  from pysdmx.io.json.sdmxjson2.messages.dsd import JsonDataStructure
@@ -49,90 +50,95 @@ from pysdmx.model.message import StructureMessage
49
50
  class JsonStructures(Struct, frozen=True, omit_defaults=True):
50
51
  """The allowed strutures."""
51
52
 
52
- dataStructures: Sequence[JsonDataStructure] = ()
53
+ agencySchemes: Sequence[JsonAgencyScheme] = ()
54
+ categorisations: Sequence[JsonCategorisation] = ()
53
55
  categorySchemes: Sequence[JsonCategoryScheme] = ()
54
- conceptSchemes: Sequence[JsonConceptScheme] = ()
55
56
  codelists: Sequence[JsonCodelist] = ()
56
- valueLists: Sequence[JsonValuelist] = ()
57
+ conceptSchemes: Sequence[JsonConceptScheme] = ()
58
+ customTypeSchemes: Sequence[JsonCustomTypeScheme] = ()
59
+ dataConstraints: Sequence[JsonDataConstraint] = ()
60
+ dataflows: Sequence[JsonDataflow] = ()
61
+ dataProviderSchemes: Sequence[JsonDataProviderScheme] = ()
62
+ dataStructures: Sequence[JsonDataStructure] = ()
57
63
  hierarchies: Sequence[JsonHierarchy] = ()
58
64
  hierarchyAssociations: Sequence[JsonHierarchyAssociation] = ()
59
- agencySchemes: Sequence[JsonAgencyScheme] = ()
60
- dataProviderSchemes: Sequence[JsonDataProviderScheme] = ()
61
- metadataProviderSchemes: Sequence[JsonMetadataProviderScheme] = ()
62
- dataflows: Sequence[JsonDataflow] = ()
63
- provisionAgreements: Sequence[JsonProvisionAgreement] = ()
64
65
  metadataflows: Sequence[JsonMetadataflow] = ()
66
+ metadataProviderSchemes: Sequence[JsonMetadataProviderScheme] = ()
65
67
  metadataProvisionAgreements: Sequence[JsonMetadataProvisionAgreement] = ()
66
68
  metadataStructures: Sequence[JsonMetadataStructure] = ()
67
- structureMaps: Sequence[JsonStructureMap] = ()
68
- representationMaps: Sequence[JsonRepresentationMap] = ()
69
- categorisations: Sequence[JsonCategorisation] = ()
70
- customTypeSchemes: Sequence[JsonCustomTypeScheme] = ()
71
- vtlMappingSchemes: Sequence[JsonVtlMappingScheme] = ()
72
69
  namePersonalisationSchemes: Sequence[JsonNamePersonalisationScheme] = ()
70
+ provisionAgreements: Sequence[JsonProvisionAgreement] = ()
71
+ representationMaps: Sequence[JsonRepresentationMap] = ()
73
72
  rulesetSchemes: Sequence[JsonRulesetScheme] = ()
73
+ structureMaps: Sequence[JsonStructureMap] = ()
74
74
  transformationSchemes: Sequence[JsonTransformationScheme] = ()
75
75
  userDefinedOperatorSchemes: Sequence[JsonUserDefinedOperatorScheme] = ()
76
+ valueLists: Sequence[JsonValuelist] = ()
77
+ vtlMappingSchemes: Sequence[JsonVtlMappingScheme] = ()
76
78
 
77
79
  def to_model(self) -> Sequence[MaintainableArtefact]:
78
80
  """Map to pysdmx artefacts."""
79
81
  structures = [] # type: ignore[var-annotated]
80
82
  structures.extend(
81
- i.to_model(
82
- self.conceptSchemes, self.codelists, self.valueLists, ()
83
- )
84
- for i in self.dataStructures
83
+ i.to_model(self.dataflows) for i in self.agencySchemes
85
84
  )
85
+ structures.extend(i.to_model() for i in self.categorisations)
86
86
  structures.extend(i.to_model() for i in self.categorySchemes)
87
- structures.extend(
88
- i.to_model(self.codelists) for i in self.conceptSchemes
89
- )
90
87
  structures.extend(i.to_model() for i in self.codelists)
91
- structures.extend(i.to_model() for i in self.valueLists)
92
- structures.extend(i.to_model(self.codelists) for i in self.hierarchies)
93
88
  structures.extend(
94
- i.to_model(self.hierarchies, self.codelists)
95
- for i in self.hierarchyAssociations
89
+ i.to_model(self.codelists) for i in self.conceptSchemes
96
90
  )
91
+ structures.extend(i.to_model() for i in self.customTypeSchemes)
92
+ structures.extend(i.to_model() for i in self.dataConstraints)
97
93
  structures.extend(
98
- i.to_model(self.dataflows) for i in self.agencySchemes
94
+ i.to_model(
95
+ self.dataStructures,
96
+ self.conceptSchemes,
97
+ self.valueLists,
98
+ self.codelists,
99
+ )
100
+ for i in self.dataflows
99
101
  )
100
102
  structures.extend(
101
103
  i.to_model(self.provisionAgreements)
102
104
  for i in self.dataProviderSchemes
103
105
  )
106
+ structures.extend(
107
+ i.to_model(
108
+ self.conceptSchemes, self.codelists, self.valueLists, ()
109
+ )
110
+ for i in self.dataStructures
111
+ )
112
+ structures.extend(i.to_model(self.codelists) for i in self.hierarchies)
113
+ structures.extend(
114
+ i.to_model(self.hierarchies, self.codelists)
115
+ for i in self.hierarchyAssociations
116
+ )
117
+
118
+ structures.extend(i.to_model() for i in self.metadataflows)
104
119
  structures.extend(
105
120
  i.to_model(self.metadataProvisionAgreements)
106
121
  for i in self.metadataProviderSchemes
107
122
  )
123
+ structures.extend(
124
+ i.to_model() for i in self.metadataProvisionAgreements
125
+ )
108
126
  structures.extend(
109
127
  i.to_model(self.conceptSchemes, self.codelists, self.valueLists)
110
128
  for i in self.metadataStructures
111
129
  )
112
130
  structures.extend(
113
- i.to_model(
114
- self.dataStructures,
115
- self.conceptSchemes,
116
- self.valueLists,
117
- self.codelists,
118
- )
119
- for i in self.dataflows
131
+ i.to_model() for i in self.namePersonalisationSchemes
120
132
  )
121
133
  structures.extend(i.to_model() for i in self.provisionAgreements)
122
- structures.extend(i.to_model() for i in self.metadataflows)
123
134
  structures.extend(
124
- i.to_model() for i in self.metadataProvisionAgreements
135
+ i.to_model(bool(len(i.source) > 1 or len(i.target) > 1))
136
+ for i in self.representationMaps
125
137
  )
138
+ structures.extend(i.to_model() for i in self.rulesetSchemes)
126
139
  structures.extend(
127
140
  i.to_model(self.representationMaps) for i in self.structureMaps
128
141
  )
129
- structures.extend(i.to_model() for i in self.categorisations)
130
- structures.extend(i.to_model() for i in self.customTypeSchemes)
131
- structures.extend(i.to_model() for i in self.vtlMappingSchemes)
132
- structures.extend(
133
- i.to_model() for i in self.namePersonalisationSchemes
134
- )
135
- structures.extend(i.to_model() for i in self.rulesetSchemes)
136
142
  structures.extend(
137
143
  i.to_model(
138
144
  self.customTypeSchemes,
@@ -146,9 +152,8 @@ class JsonStructures(Struct, frozen=True, omit_defaults=True):
146
152
  structures.extend(
147
153
  i.to_model() for i in self.userDefinedOperatorSchemes
148
154
  )
149
- for rm in self.representationMaps:
150
- multi = bool(len(rm.source) > 1 or len(rm.target) > 1)
151
- structures.append(rm.to_model(multi))
155
+ structures.extend(i.to_model() for i in self.valueLists)
156
+ structures.extend(i.to_model() for i in self.vtlMappingSchemes)
152
157
  return structures
153
158
 
154
159
  @classmethod
@@ -264,6 +269,33 @@ class JsonStructures(Struct, frozen=True, omit_defaults=True):
264
269
  hierarchies = tuple(
265
270
  [JsonHierarchy.from_model(h) for h in msg.get_hierarchies()]
266
271
  )
272
+ constraints = tuple(
273
+ [
274
+ JsonDataConstraint.from_model(c)
275
+ for c in msg.get_data_constraints()
276
+ ]
277
+ )
278
+ mpas = tuple(
279
+ [
280
+ JsonMetadataProvisionAgreement.from_model(c)
281
+ for c in msg.get_metadata_provision_agreements()
282
+ ]
283
+ )
284
+ mprvs = tuple(
285
+ [
286
+ JsonMetadataProviderScheme.from_model(c)
287
+ for c in msg.get_metadata_provider_schemes()
288
+ ]
289
+ )
290
+ mdfs = tuple(
291
+ [JsonMetadataflow.from_model(c) for c in msg.get_metadataflows()]
292
+ )
293
+ msds = tuple(
294
+ [
295
+ JsonMetadataStructure.from_model(c)
296
+ for c in msg.get_metadata_structures()
297
+ ]
298
+ )
267
299
  return JsonStructures(
268
300
  agencySchemes=agencies,
269
301
  categorisations=categorisations,
@@ -271,11 +303,16 @@ class JsonStructures(Struct, frozen=True, omit_defaults=True):
271
303
  codelists=codelists,
272
304
  conceptSchemes=concept_schemes,
273
305
  customTypeSchemes=custom_types,
306
+ dataConstraints=constraints,
274
307
  dataflows=dataflows,
275
308
  dataProviderSchemes=data_providers,
276
309
  dataStructures=data_structures,
277
310
  hierarchies=hierarchies,
278
311
  hierarchyAssociations=hier_associations,
312
+ metadataflows=mdfs,
313
+ metadataProviderSchemes=mprvs,
314
+ metadataProvisionAgreements=mpas,
315
+ metadataStructures=msds,
279
316
  namePersonalisationSchemes=name_personalisations,
280
317
  provisionAgreements=agreements,
281
318
  representationMaps=representations_maps,
@@ -1,6 +1,6 @@
1
1
  """Collection of SDMX-JSON schemas for VTL artefacts."""
2
2
 
3
- from typing import Literal, Optional, Sequence
3
+ from typing import Dict, Literal, Optional, Sequence
4
4
 
5
5
  from msgspec import Struct
6
6
 
@@ -506,33 +506,37 @@ class JsonRulesetScheme(ItemSchemeType, frozen=True, omit_defaults=True):
506
506
  class JsonToVtlMapping(Struct, frozen=True, omit_defaults=True):
507
507
  """SDMX-JSON payload for To VTL mappings."""
508
508
 
509
- toVtlSubSpace: Sequence[str]
510
- type: Optional[str] = None
509
+ toVtlSubSpace: Dict[str, Sequence[str]]
510
+ method: Optional[str] = None
511
511
 
512
512
  def to_model(self) -> ToVtlMapping:
513
513
  """Converts deserialized class to pysdmx model class."""
514
- return ToVtlMapping(self.toVtlSubSpace, self.type)
514
+ return ToVtlMapping(self.toVtlSubSpace["keys"], self.method)
515
515
 
516
516
  @classmethod
517
517
  def from_model(cls, mapping: ToVtlMapping) -> "JsonToVtlMapping":
518
518
  """Converts a pysdmx "to VTL" mapping to an SDMX-JSON one."""
519
- return JsonToVtlMapping(mapping.to_vtl_sub_space, mapping.method)
519
+ return JsonToVtlMapping(
520
+ {"keys": mapping.to_vtl_sub_space}, mapping.method
521
+ )
520
522
 
521
523
 
522
524
  class JsonFromVtlMapping(Struct, frozen=True, omit_defaults=True):
523
525
  """SDMX-JSON payload for from VTL mappings."""
524
526
 
525
- fromVtlSuperSpace: Sequence[str]
526
- type: Optional[str] = None
527
+ fromVtlSuperSpace: Dict[str, Sequence[str]]
528
+ method: Optional[str] = None
527
529
 
528
530
  def to_model(self) -> FromVtlMapping:
529
531
  """Converts deserialized class to pysdmx model class."""
530
- return FromVtlMapping(self.fromVtlSuperSpace, self.type)
532
+ return FromVtlMapping(self.fromVtlSuperSpace["keys"], self.method)
531
533
 
532
534
  @classmethod
533
535
  def from_model(cls, mapping: FromVtlMapping) -> "JsonFromVtlMapping":
534
536
  """Converts a pysdmx "from VTL" mapping to an SDMX-JSON one."""
535
- return JsonFromVtlMapping(mapping.from_vtl_sub_space, mapping.method)
537
+ return JsonFromVtlMapping(
538
+ {"keys": mapping.from_vtl_sub_space}, mapping.method
539
+ )
536
540
 
537
541
 
538
542
  class JsonVtlMapping(NameableType, frozen=True, omit_defaults=True):
@@ -88,6 +88,10 @@ def validate_sdmx_json(input_str: str) -> None:
88
88
  lambda m: f"does not match required"
89
89
  f" pattern {m.group(1)!r}",
90
90
  ),
91
+ (
92
+ r"\[\]\s+is\s+too\s+short",
93
+ lambda _m: "[] should be non-empty",
94
+ ),
91
95
  ]
92
96
 
93
97
  msg: Optional[str] = next(
@@ -26,6 +26,8 @@ def check_dimension_at_observation(
26
26
  for key, value in dimension_at_observation.items():
27
27
  if key not in datasets:
28
28
  raise Invalid(f"Dataset {key} not found in Message content.")
29
+ if value == ALL_DIM:
30
+ continue
29
31
  writing_validation(datasets[key])
30
32
  dataset = datasets[key]
31
33
  components = dataset.structure.components # type: ignore[union-attr]
pysdmx/model/__base.py CHANGED
@@ -1,5 +1,6 @@
1
+ import re
1
2
  from datetime import datetime
2
- from typing import Any, Optional, Sequence, Union
3
+ from typing import Any, Literal, Optional, Sequence, Union
3
4
 
4
5
  from msgspec import Struct
5
6
 
@@ -327,6 +328,50 @@ class ItemScheme(MaintainableArtefact, frozen=True, omit_defaults=True):
327
328
  items: Sequence[Item] = ()
328
329
  is_partial: bool = False
329
330
 
331
+ def search(
332
+ self,
333
+ query: str,
334
+ use_regex: bool = False,
335
+ fields: Literal["name", "description", "all"] = "all",
336
+ ) -> Sequence[Item]:
337
+ """Search for items matching the query.
338
+
339
+ Args:
340
+ query: The substring or regex pattern to search for.
341
+ use_regex: Whether to treat the query as a regex (default: False).
342
+ fields: The fields to search in (default: all textual fields).
343
+
344
+ Returns:
345
+ Items that match the query.
346
+ """
347
+ if not query:
348
+ raise Invalid(
349
+ "Invalid search", "The query string cannot be empty."
350
+ )
351
+
352
+ # Determine which fields to search in
353
+ search_fields = (
354
+ ["name", "description"] if fields == "all" else [fields]
355
+ )
356
+
357
+ # Transform plain text queries into a regex
358
+ if not use_regex:
359
+ query = re.escape(query)
360
+
361
+ pattern = re.compile(query, re.IGNORECASE if not use_regex else 0)
362
+
363
+ all_items = getattr(self, "all_items", "")
364
+ items = all_items if all_items else self.items
365
+
366
+ return [
367
+ item # type: ignore[misc]
368
+ for item in items
369
+ if any(
370
+ pattern.search(str(getattr(item, field, "")))
371
+ for field in search_fields
372
+ )
373
+ ]
374
+
330
375
 
331
376
  class DataflowRef(
332
377
  Struct, frozen=True, omit_defaults=True, repr_omit_defaults=True, tag=True
pysdmx/model/__init__.py CHANGED
@@ -29,6 +29,16 @@ from pysdmx.model.code import (
29
29
  HierarchyAssociation,
30
30
  )
31
31
  from pysdmx.model.concept import Concept, ConceptScheme, DataType, Facets
32
+ from pysdmx.model.constraint import (
33
+ ConstraintAttachment,
34
+ CubeKeyValue,
35
+ CubeRegion,
36
+ CubeValue,
37
+ DataConstraint,
38
+ DataKey,
39
+ DataKeyValue,
40
+ KeySet,
41
+ )
32
42
  from pysdmx.model.dataflow import (
33
43
  ArrayBoundaries,
34
44
  Component,
@@ -161,9 +171,16 @@ __all__ = [
161
171
  "ComponentMap",
162
172
  "Concept",
163
173
  "ConceptScheme",
174
+ "ConstraintAttachment",
164
175
  "Contact",
176
+ "CubeKeyValue",
177
+ "CubeRegion",
178
+ "CubeValue",
165
179
  "DataConsumer",
166
180
  "DataConsumerScheme",
181
+ "DataConstraint",
182
+ "DataKey",
183
+ "DataKeyValue",
167
184
  "Dataflow",
168
185
  "DataflowInfo",
169
186
  "DataflowRef",
@@ -180,6 +197,7 @@ __all__ = [
180
197
  "HierarchyAssociation",
181
198
  "ImplicitComponentMap",
182
199
  "ItemReference",
200
+ "KeySet",
183
201
  "MetadataAttribute",
184
202
  "MetadataComponent",
185
203
  "Metadataflow",
pysdmx/model/category.py CHANGED
@@ -91,6 +91,15 @@ class CategoryScheme(ItemScheme, frozen=True, omit_defaults=True):
91
91
  flows.update(self.__extract_flows(cat))
92
92
  return list(flows)
93
93
 
94
+ @property
95
+ def all_items(self) -> Sequence[Category]:
96
+ """Get all the categories in the category scheme as a flat list.
97
+
98
+ Returns:
99
+ A flat list of all the categories present in the category scheme.
100
+ """
101
+ return self.__get_categories(self.categories)
102
+
94
103
  def __iter__(self) -> Iterator[Category]:
95
104
  """Return an iterator over the list of categories."""
96
105
  yield from self.categories
@@ -160,6 +169,14 @@ class CategoryScheme(ItemScheme, frozen=True, omit_defaults=True):
160
169
  processed_output.append(f"{attr}: {value}")
161
170
  return f"{', '.join(processed_output)}"
162
171
 
172
+ def __get_categories(self, cats: Sequence[Category]) -> Sequence[Category]:
173
+ out = []
174
+ for cat in cats:
175
+ out.append(cat)
176
+ if cat.categories:
177
+ out.extend(self.__get_categories(cat.categories))
178
+ return out
179
+
163
180
 
164
181
  class Categorisation(
165
182
  MaintainableArtefact, frozen=True, omit_defaults=True, kw_only=True
@@ -0,0 +1,69 @@
1
+ """Model for SDMX Data Constraints."""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional, Sequence
5
+
6
+ from msgspec import Struct
7
+
8
+ from pysdmx.model.__base import MaintainableArtefact
9
+
10
+
11
+ class CubeValue(Struct, frozen=True, omit_defaults=True):
12
+ """A value of the cube, with optional business validity."""
13
+
14
+ value: str
15
+ valid_from: Optional[datetime] = None
16
+ valid_to: Optional[datetime] = None
17
+
18
+
19
+ class CubeKeyValue(Struct, frozen=True, omit_defaults=True):
20
+ """The list of values for a cube's component."""
21
+
22
+ id: str
23
+ values: Sequence[CubeValue]
24
+
25
+
26
+ class CubeRegion(Struct, frozen=True, omit_defaults=True):
27
+ """A cube region, with its associated values (by default, included)."""
28
+
29
+ key_values: Sequence[CubeKeyValue]
30
+ is_included: bool = True
31
+
32
+
33
+ class ConstraintAttachment(Struct, frozen=True, omit_defaults=True):
34
+ """The artefacts to which the data constraint is attached."""
35
+
36
+ data_provider: Optional[str]
37
+ data_structures: Optional[Sequence[str]] = None
38
+ dataflows: Optional[Sequence[str]] = None
39
+ provision_agreements: Optional[Sequence[str]] = None
40
+
41
+
42
+ class DataKeyValue(Struct, frozen=True, omit_defaults=True):
43
+ """A key value, i.e. a component of the key (e.g. FREQ=M)."""
44
+
45
+ id: str
46
+ value: str
47
+
48
+
49
+ class DataKey(Struct, frozen=True, omit_defaults=True):
50
+ """A data key, i.e. one value per dimension in the data key."""
51
+
52
+ keys_values: Sequence[DataKeyValue]
53
+ valid_from: Optional[datetime] = None
54
+ valid_to: Optional[datetime] = None
55
+
56
+
57
+ class KeySet(Struct, frozen=True, omit_defaults=True):
58
+ """A set of keys, inluded by default."""
59
+
60
+ keys: Sequence[DataKey]
61
+ is_included: bool
62
+
63
+
64
+ class DataConstraint(MaintainableArtefact, frozen=True, omit_defaults=True):
65
+ """A data constraint, defining the allowed or available values."""
66
+
67
+ constraint_attachment: Optional[ConstraintAttachment] = None
68
+ cube_regions: Sequence[CubeRegion] = ()
69
+ key_sets: Sequence[KeySet] = ()
pysdmx/model/dataflow.py CHANGED
@@ -101,10 +101,11 @@ class Component(
101
101
  one of: *D* (for Dataset), *O* (for Observation), any string identifying a
102
102
  component ID (FREQ) or comma-separated list of component IDs
103
103
  (FREQ,REF_AREA). The latter can be used to identify the dimension, group
104
- or series to which the attribute is attached. The attachment level of a
105
- component may vary with the statistical domain, i.e. a component attached
106
- to a series in a particular domain may be attached to, say, the dataset in
107
- another domain.
104
+ or series to which the attribute is attached. It can also be used to
105
+ identify the measure(s) to which the attribute relates, in case multiple
106
+ measures are defined. The attachment level of a component may vary with the
107
+ statistical domain, i.e. a component attached to a series in a particular
108
+ domain may be attached to, say, the dataset in another domain.
108
109
 
109
110
  The *codes* field indicates the expected (i.e. allowed) set of values a
110
111
  component can take within a particular domain. In addition to
@@ -128,7 +129,9 @@ class Component(
128
129
  Attributes can be attached at different levels such as
129
130
  D (for dataset-level attributes), O (for observation-level
130
131
  attributes) or a combination of dimension IDs, separated by
131
- commas, for series- and group-level attributes).
132
+ commas, for series- and group-level attributes, as well as for
133
+ attributes attached to one or more measures, when multiple
134
+ measures are defined).
132
135
  A post_init check makes this attribute mandatory for attributes.
133
136
  array_def: Any additional constraints for array types.
134
137
  urn: The URN of the component.