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
@@ -0,0 +1,32 @@
1
+ """Writer interface for SDMX-JSON 2.0.0 Reference Metadata messages."""
2
+
3
+ import msgspec
4
+
5
+ from pysdmx import errors
6
+ from pysdmx.io.json.sdmxjson2.messages import JsonMetadataMessage
7
+ from pysdmx.model import decoders
8
+ from pysdmx.model.message import MetadataMessage
9
+
10
+
11
+ def read(input_str: str) -> MetadataMessage:
12
+ """Read an SDMX-JSON 2.0.0 Metadata Message.
13
+
14
+ Args:
15
+ input_str: SDMX-JSON reference metadata message to read.
16
+
17
+ Returns:
18
+ A pysdmx MetadataMessage
19
+ """
20
+ try:
21
+ msg = msgspec.json.Decoder(
22
+ JsonMetadataMessage, dec_hook=decoders
23
+ ).decode(input_str)
24
+ return msg.to_model()
25
+ except msgspec.DecodeError as de:
26
+ raise errors.Invalid(
27
+ "Invalid message",
28
+ (
29
+ "The supplied file could not be read as SDMX-JSON 2.0.0 "
30
+ "reference metadata message."
31
+ ),
32
+ ) from de
@@ -0,0 +1,32 @@
1
+ """Reader interface for SDMX-JSON 2.0.0 Structure messages."""
2
+
3
+ import msgspec
4
+
5
+ from pysdmx import errors
6
+ from pysdmx.io.json.sdmxjson2.messages import JsonStructureMessage
7
+ from pysdmx.model import decoders
8
+ from pysdmx.model.message import StructureMessage
9
+
10
+
11
+ def read(input_str: str) -> StructureMessage:
12
+ """Read an SDMX-JSON 2.0.0 Stucture Message.
13
+
14
+ Args:
15
+ input_str: SDMX-JSON structure message to read.
16
+
17
+ Returns:
18
+ A pysdmx StructureMessage
19
+ """
20
+ try:
21
+ msg = msgspec.json.Decoder(
22
+ JsonStructureMessage, dec_hook=decoders
23
+ ).decode(input_str)
24
+ return msg.to_model()
25
+ except msgspec.DecodeError as de:
26
+ raise errors.Invalid(
27
+ "Invalid message",
28
+ (
29
+ "The supplied file could not be read as SDMX-JSON 2.0.0 "
30
+ "structure message."
31
+ ),
32
+ ) from de
@@ -0,0 +1,9 @@
1
+ """Collection of writers for SDMX-JSON messages."""
2
+
3
+ from pysdmx.io.json.sdmxjson2 import messages as msg
4
+ from pysdmx.io.serde import Serializers
5
+
6
+ serializers = Serializers(
7
+ metadata_message=msg.JsonMetadataMessage,
8
+ structure_message=msg.JsonStructureMessage,
9
+ )
@@ -0,0 +1,60 @@
1
+ """Writer interface for SDMX-JSON 2.0.0 Reference Metadata messages."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Sequence, Union
5
+
6
+ import msgspec
7
+
8
+ from pysdmx.io.json.sdmxjson2.messages import JsonMetadataMessage
9
+ from pysdmx.model import MetadataReport, encoders
10
+ from pysdmx.model.message import Header, MetadataMessage
11
+
12
+
13
+ def write(
14
+ reports: Sequence[MetadataReport],
15
+ output_path: Optional[Union[str, Path]] = None,
16
+ prettyprint: bool = True,
17
+ header: Optional[Header] = None,
18
+ ) -> Optional[str]:
19
+ """Write metadata reports in SDMX-JSON 2.0.0.
20
+
21
+ Args:
22
+ reports: The reference metadata reports to be serialized.
23
+ output_path: The path to save the JSON file. If None or empty, the
24
+ serialized content is returned as a string instead.
25
+ prettyprint: Whether to format the JSON output with indentation (True)
26
+ or output compact JSON without extra whitespace (False).
27
+ header: The header to be used in the SDMX-JSON message
28
+ (will be generated if no header is supplied).
29
+
30
+ Returns:
31
+ The JSON string if output_path is None or empty, None otherwise.
32
+ """
33
+ if not header:
34
+ header = Header()
35
+ sm = MetadataMessage(header, reports)
36
+ jsm = JsonMetadataMessage.from_model(sm)
37
+
38
+ encoder = msgspec.json.Encoder(enc_hook=encoders)
39
+ serialized_data = encoder.encode(jsm)
40
+
41
+ # Apply pretty-printing if requested
42
+ if prettyprint:
43
+ serialized_data = msgspec.json.format(serialized_data, indent=4)
44
+
45
+ # If output_path is provided, write to file
46
+ if output_path:
47
+ # Convert to Path object if string
48
+ if isinstance(output_path, str):
49
+ output_path = Path(output_path)
50
+
51
+ # Create parent directories if they don't exist
52
+ output_path.parent.mkdir(parents=True, exist_ok=True)
53
+
54
+ # Write to file
55
+ with open(output_path, "wb") as f:
56
+ f.write(serialized_data)
57
+ return None
58
+ else:
59
+ # Return as string
60
+ return serialized_data.decode("utf-8")
@@ -0,0 +1,61 @@
1
+ """Writer interface for SDMX-JSON 2.0.0 Structure messages."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Sequence, Union
5
+
6
+ import msgspec
7
+
8
+ from pysdmx.io.json.sdmxjson2.messages import JsonStructureMessage
9
+ from pysdmx.model import encoders
10
+ from pysdmx.model.__base import MaintainableArtefact
11
+ from pysdmx.model.message import Header, StructureMessage
12
+
13
+
14
+ def write(
15
+ structures: Sequence[MaintainableArtefact],
16
+ output_path: Optional[Union[str, Path]] = None,
17
+ prettyprint: bool = True,
18
+ header: Optional[Header] = None,
19
+ ) -> Optional[str]:
20
+ """Write maintainable SDMX artefacts in SDMX-JSON 2.0.0.
21
+
22
+ Args:
23
+ structures: The maintainable SDMX artefacts to be serialized.
24
+ output_path: The path to save the JSON file. If None or empty, the
25
+ serialized content is returned as a string instead.
26
+ prettyprint: Whether to format the JSON output with indentation (True)
27
+ or output compact JSON without extra whitespace (False).
28
+ header: The header to be used in the SDMX-JSON message
29
+ (will be generated if no header is supplied).
30
+
31
+ Returns:
32
+ The JSON string if output_path is None or empty, None otherwise.
33
+ """
34
+ if not header:
35
+ header = Header()
36
+ sm = StructureMessage(header, structures)
37
+ jsm = JsonStructureMessage.from_model(sm)
38
+
39
+ encoder = msgspec.json.Encoder(enc_hook=encoders)
40
+ serialized_data = encoder.encode(jsm)
41
+
42
+ # Apply pretty-printing if requested
43
+ if prettyprint:
44
+ serialized_data = msgspec.json.format(serialized_data, indent=4)
45
+
46
+ # If output_path is provided, write to file
47
+ if output_path:
48
+ # Convert to Path object if string
49
+ if isinstance(output_path, str):
50
+ output_path = Path(output_path)
51
+
52
+ # Create parent directories if they don't exist
53
+ output_path.parent.mkdir(parents=True, exist_ok=True)
54
+
55
+ # Write to file
56
+ with open(output_path, "wb") as f:
57
+ f.write(serialized_data)
58
+ return None
59
+ else:
60
+ # Return as string
61
+ return serialized_data.decode("utf-8")
pysdmx/io/reader.py CHANGED
@@ -8,10 +8,10 @@ from pysdmx.errors import Invalid
8
8
  from pysdmx.io.format import Format
9
9
  from pysdmx.io.input_processor import process_string_to_read
10
10
  from pysdmx.model import Schema
11
- from pysdmx.model.__base import ItemScheme
12
- from pysdmx.model.dataflow import Dataflow, DataStructureDefinition
11
+ from pysdmx.model.__base import MaintainableArtefact
13
12
  from pysdmx.model.dataset import Dataset
14
13
  from pysdmx.model.message import Message
14
+ from pysdmx.model.metadata import MetadataReport
15
15
  from pysdmx.model.submission import SubmissionResult
16
16
  from pysdmx.util import parse_short_urn
17
17
  from pysdmx.util._model_utils import schema_generator
@@ -43,10 +43,9 @@ def read_sdmx( # noqa: C901
43
43
 
44
44
  header = None
45
45
  result_data: Sequence[Dataset] = []
46
- result_structures: Sequence[
47
- Union[ItemScheme, Dataflow, DataStructureDefinition]
48
- ] = []
46
+ result_structures: Sequence[MaintainableArtefact] = []
49
47
  result_submission: Sequence[SubmissionResult] = []
48
+ reports: Sequence[MetadataReport] = []
50
49
  if read_format == Format.STRUCTURE_SDMX_ML_2_1:
51
50
  from pysdmx.io.xml.header import read as read_header
52
51
  from pysdmx.io.xml.sdmx21.reader.structure import (
@@ -74,6 +73,24 @@ def read_sdmx( # noqa: C901
74
73
  header = read_header(input_str, validate=validate)
75
74
  # SDMX-ML 3.1 Structure
76
75
  result_structures = read_structure(input_str, validate=validate)
76
+ elif read_format == Format.STRUCTURE_SDMX_JSON_2_0_0:
77
+ from pysdmx.io.json.sdmxjson2.reader.structure import (
78
+ read as read_struct,
79
+ )
80
+
81
+ struct_msg = read_struct(input_str)
82
+ header = struct_msg.header
83
+ result_structures = (
84
+ struct_msg.structures if struct_msg.structures else []
85
+ )
86
+ elif read_format == Format.REFMETA_SDMX_JSON_2_0_0:
87
+ from pysdmx.io.json.sdmxjson2.reader.metadata import (
88
+ read as read_refmeta,
89
+ )
90
+
91
+ ref_msg = read_refmeta(input_str)
92
+ header = ref_msg.header
93
+ reports = ref_msg.get_reports()
77
94
  elif read_format == Format.DATA_SDMX_ML_2_1_GEN:
78
95
  from pysdmx.io.xml.header import read as read_header
79
96
  from pysdmx.io.xml.sdmx21.reader.generic import read as read_generic
@@ -127,18 +144,19 @@ def read_sdmx( # noqa: C901
127
144
  # SDMX-CSV 1.0
128
145
  result_data = read_csv_v1(input_str)
129
146
  else:
130
- # SDMX-CSV 2.0
131
- from pysdmx.io.csv.sdmx20.reader import read as read_csv_v2
147
+ # SDMX-CSV 2.1
148
+ from pysdmx.io.csv.sdmx21.reader import read as read_csv_v2
132
149
 
133
150
  result_data = read_csv_v2(input_str)
134
151
 
135
- if not (result_data or result_structures or result_submission):
152
+ if not (result_data or result_structures or result_submission or reports):
136
153
  raise Invalid("Empty SDMX Message")
137
154
 
138
155
  # Returning a Message class
139
156
  if read_format in (
140
157
  Format.DATA_SDMX_CSV_1_0_0,
141
158
  Format.DATA_SDMX_CSV_2_0_0,
159
+ Format.DATA_SDMX_CSV_2_1_0,
142
160
  Format.DATA_SDMX_ML_2_1_GEN,
143
161
  Format.DATA_SDMX_ML_2_1_STR,
144
162
  Format.DATA_SDMX_ML_3_0,
@@ -149,7 +167,8 @@ def read_sdmx( # noqa: C901
149
167
  return Message(header=header, data=result_data)
150
168
  elif read_format == Format.REGISTRY_SDMX_ML_2_1:
151
169
  return Message(header=header, submission=result_submission)
152
-
170
+ elif read_format == Format.REFMETA_SDMX_JSON_2_0_0:
171
+ return Message(header=header, reports=reports)
153
172
  # TODO: Ensure we have changed the signature of the structure readers
154
173
  return Message(header=header, structures=result_structures)
155
174
 
pysdmx/io/serde.py CHANGED
@@ -12,6 +12,15 @@ class Deserializer(Protocol):
12
12
  """Returns the domain objects."""
13
13
 
14
14
 
15
+ @runtime_checkable
16
+ class Serializer(Protocol):
17
+ """Creates an SDMX message from domain objects."""
18
+
19
+ @classmethod
20
+ def from_model(self, message: Any) -> Any:
21
+ """Returns the SDMX message."""
22
+
23
+
15
24
  @dataclass
16
25
  class Deserializers:
17
26
  """Collection of deserializers for a format."""
@@ -34,6 +43,14 @@ class Deserializers:
34
43
  transformation_scheme: Deserializer
35
44
 
36
45
 
46
+ @dataclass
47
+ class Serializers:
48
+ """Collection of serializers for a format."""
49
+
50
+ structure_message: Serializer
51
+ metadata_message: Serializer
52
+
53
+
37
54
  @dataclass
38
55
  class GdsDeserializers:
39
56
  """Collection of GDS deserializers for a format."""
pysdmx/io/writer.py CHANGED
@@ -7,11 +7,14 @@ from typing import Any, Optional, Sequence
7
7
 
8
8
  from pysdmx.errors import Invalid
9
9
  from pysdmx.io.format import Format
10
+ from pysdmx.model import MetadataReport
11
+ from pysdmx.model.__base import MaintainableArtefact
10
12
  from pysdmx.model.dataset import Dataset
11
13
 
12
14
  WRITERS = {
13
15
  Format.DATA_SDMX_CSV_1_0_0: "pysdmx.io.csv.sdmx10.writer",
14
16
  Format.DATA_SDMX_CSV_2_0_0: "pysdmx.io.csv.sdmx20.writer",
17
+ Format.DATA_SDMX_CSV_2_1_0: "pysdmx.io.csv.sdmx21.writer",
15
18
  Format.DATA_SDMX_ML_2_1_GEN: "pysdmx.io.xml.sdmx21.writer.generic",
16
19
  Format.DATA_SDMX_ML_2_1_STR: "pysdmx.io.xml.sdmx21.writer."
17
20
  "structure_specific",
@@ -22,14 +25,23 @@ WRITERS = {
22
25
  Format.DATA_SDMX_ML_3_1: "pysdmx.io.xml.sdmx31.writer."
23
26
  "structure_specific",
24
27
  Format.STRUCTURE_SDMX_ML_3_1: "pysdmx.io.xml.sdmx31.writer.structure",
28
+ Format.STRUCTURE_SDMX_JSON_2_0_0: (
29
+ "pysdmx.io.json.sdmxjson2.writer.structure"
30
+ ),
31
+ Format.REFMETA_SDMX_JSON_2_0_0: (
32
+ "pysdmx.io.json.sdmxjson2.writer.metadata"
33
+ ),
25
34
  }
26
35
 
27
36
  STRUCTURE_WRITERS = (
28
37
  Format.STRUCTURE_SDMX_ML_2_1,
29
38
  Format.STRUCTURE_SDMX_ML_3_0,
30
39
  Format.STRUCTURE_SDMX_ML_3_1,
40
+ Format.STRUCTURE_SDMX_JSON_2_0_0,
31
41
  )
32
42
 
43
+ REFMETA_WRITERS = (Format.REFMETA_SDMX_JSON_2_0_0,)
44
+
33
45
 
34
46
  def write_sdmx(
35
47
  sdmx_objects: Any,
@@ -97,20 +109,36 @@ def write_sdmx(
97
109
  writer = module.write
98
110
 
99
111
  is_structure = sdmx_format in STRUCTURE_WRITERS
112
+ is_ref_meta = sdmx_format in REFMETA_WRITERS
100
113
  is_xml = "xml" in WRITERS[sdmx_format]
101
- key = "structures" if is_structure else "datasets"
114
+ is_json = "json" in WRITERS[sdmx_format]
115
+ if is_structure:
116
+ key = "structures"
117
+ elif is_ref_meta:
118
+ key = "reports"
119
+ else:
120
+ key = "datasets"
102
121
  value = sdmx_objects if isinstance(sdmx_objects, list) else [sdmx_objects]
103
122
 
104
- if is_structure and any(isinstance(x, Dataset) for x in value):
123
+ if is_structure and not all(
124
+ isinstance(x, MaintainableArtefact) for x in value
125
+ ):
105
126
  raise Invalid(
106
- "Datasets cannot be written to structure formats. "
107
- "Use data formats instead."
127
+ "Only maintainable artefacts can be written to structure formats."
108
128
  )
109
- elif not is_structure and not all(isinstance(x, Dataset) for x in value):
129
+ elif is_ref_meta and not all(isinstance(x, MetadataReport) for x in value):
110
130
  raise Invalid(
111
- "Only Datasets can be written to data formats. "
112
- "Use structure formats for other SDMX objects."
131
+ (
132
+ "Only metadata reports can be written to reference "
133
+ "metadata formats."
134
+ )
113
135
  )
136
+ elif (
137
+ not is_structure
138
+ and not is_ref_meta
139
+ and not all(isinstance(x, Dataset) for x in value)
140
+ ):
141
+ raise Invalid("Only Datasets can be written to data formats.")
114
142
 
115
143
  args = {
116
144
  key: value,
@@ -120,7 +148,7 @@ def write_sdmx(
120
148
  "prettyprint": kwargs.get("prettyprint"),
121
149
  "header": kwargs.get("header"),
122
150
  }
123
- if is_xml
151
+ if is_xml or is_json
124
152
  else {}
125
153
  ),
126
154
  **(
@@ -132,7 +160,15 @@ def write_sdmx(
132
160
  if is_xml and not is_structure
133
161
  else {}
134
162
  ),
163
+ **(
164
+ {
165
+ "labels": kwargs.get("labels"),
166
+ "keys": kwargs.get("keys"),
167
+ "time_format": kwargs.get("time_format"),
168
+ }
169
+ if not is_xml
170
+ else {}
171
+ ),
135
172
  }
136
173
  args = {k: v for k, v in args.items() if v is not None}
137
-
138
174
  return writer(**args)
@@ -1,6 +1,4 @@
1
- from typing import Any, Dict, List, Optional, Sequence, Tuple
2
-
3
- import pandas as pd
1
+ from typing import Dict, Optional, Sequence
4
2
 
5
3
  from pysdmx.errors import Invalid
6
4
  from pysdmx.io.pd import PandasDataset
@@ -87,54 +85,3 @@ def writing_validation(dataset: PandasDataset) -> None:
87
85
  )
88
86
  if not dataset.structure.components.measures:
89
87
  raise Invalid("The dataset structure must have at least one measure.")
90
-
91
-
92
- def get_codes(
93
- dimension_code: str, structure: Schema, data: pd.DataFrame
94
- ) -> Tuple[List[str], List[str], List[Dict[str, Any]]]:
95
- """This function divides the components in Series and Obs."""
96
- series_codes = []
97
- groups = structure.groups
98
- group_codes = []
99
- obs_codes = [dimension_code, structure.components.measures[0].id]
100
-
101
- # Getting the series and obs codes
102
- for dim in structure.components.dimensions:
103
- if dim.id != dimension_code:
104
- series_codes.append(dim.id)
105
-
106
- # Adding the attributes based on the attachment level
107
- for att in structure.components.attributes:
108
- matching_group = next(
109
- (
110
- group
111
- for group in groups or []
112
- if set(group.dimensions)
113
- == set(att.attachment_level.split(",")) # type: ignore[union-attr]
114
- ),
115
- None,
116
- )
117
-
118
- if (
119
- att.attachment_level != "D"
120
- and att.id in data.columns
121
- and groups is not None
122
- and matching_group
123
- ):
124
- group_codes.append(
125
- {
126
- "group_id": matching_group.id,
127
- "attribute": att.id,
128
- "dimensions": matching_group.dimensions,
129
- }
130
- )
131
- elif att.attachment_level == "O" and att.id in data.columns:
132
- obs_codes.append(att.id)
133
- elif (
134
- att.attachment_level is not None
135
- and att.attachment_level != "D"
136
- and att.id in data.columns
137
- ):
138
- series_codes.append(att.id)
139
-
140
- return series_codes, obs_codes, group_codes
@@ -13,10 +13,10 @@ from pysdmx.io.xml.__write_aux import (
13
13
  get_structure,
14
14
  )
15
15
  from pysdmx.io.xml.__write_data_aux import (
16
- get_codes,
17
16
  writing_validation,
18
17
  )
19
18
  from pysdmx.io.xml.config import CHUNKSIZE
19
+ from pysdmx.toolkit.pd._data_utils import get_codes
20
20
  from pysdmx.util import parse_short_urn
21
21
 
22
22
 
@@ -21,11 +21,11 @@ from pysdmx.io.xml.__write_aux import (
21
21
  from pysdmx.io.xml.__write_data_aux import (
22
22
  check_content_dataset,
23
23
  check_dimension_at_observation,
24
- get_codes,
25
24
  writing_validation,
26
25
  )
27
26
  from pysdmx.io.xml.config import CHUNKSIZE
28
27
  from pysdmx.model.message import Header
28
+ from pysdmx.toolkit.pd._data_utils import get_codes
29
29
  from pysdmx.util import parse_short_urn
30
30
 
31
31
 
pysdmx/model/code.py CHANGED
@@ -21,7 +21,13 @@ from typing import Iterator, Literal, Optional, Sequence, Union
21
21
 
22
22
  from msgspec import Struct
23
23
 
24
- from pysdmx.model.__base import Agency, Item, ItemScheme, MaintainableArtefact
24
+ from pysdmx.model.__base import (
25
+ Agency,
26
+ Annotation,
27
+ Item,
28
+ ItemScheme,
29
+ MaintainableArtefact,
30
+ )
25
31
 
26
32
 
27
33
  class Code(Item, frozen=True, omit_defaults=True):
@@ -139,6 +145,8 @@ class HierarchicalCode(
139
145
  rel_valid_from: Start of the hierarchical relationship validity.
140
146
  rel_valid_to: End of the hierarchical relationship validity.
141
147
  codes: The child codes.
148
+ annotations: Annotations attached to the code.
149
+ urn: The URN of the code.
142
150
  """
143
151
 
144
152
  id: str
@@ -149,6 +157,8 @@ class HierarchicalCode(
149
157
  rel_valid_from: Optional[datetime] = None
150
158
  rel_valid_to: Optional[datetime] = None
151
159
  codes: Sequence["HierarchicalCode"] = ()
160
+ annotations: Sequence[Annotation] = ()
161
+ urn: Optional[str] = None
152
162
 
153
163
  def __iter__(self) -> Iterator["HierarchicalCode"]:
154
164
  """Return an iterator over the list of codes."""
pysdmx/model/dataflow.py CHANGED
@@ -130,6 +130,9 @@ class Component(
130
130
  commas, for series- and group-level attributes).
131
131
  A post_init check makes this attribute mandatory for attributes.
132
132
  array_def: Any additional constraints for array types.
133
+ urn: The URN of the component.
134
+ local_enum_ref: The URN of the enumeration (codelist or valuelist) from
135
+ which the local codes are taken.
133
136
  """
134
137
 
135
138
  id: str
@@ -144,6 +147,7 @@ class Component(
144
147
  attachment_level: Optional[str] = None
145
148
  array_def: Optional[ArrayBoundaries] = None
146
149
  urn: Optional[str] = None
150
+ local_enum_ref: Optional[str] = None
147
151
 
148
152
  def __post_init__(self) -> None:
149
153
  """Validate the component."""
@@ -217,6 +221,20 @@ class Component(
217
221
  else:
218
222
  return None
219
223
 
224
+ @property
225
+ def enum_ref(self) -> Optional[str]:
226
+ """Returns the URN of the enumeration from which the codes are taken.
227
+
228
+ Returns:
229
+ The URN of the enumeration from which the codes are taken.
230
+ """
231
+ if self.local_enum_ref:
232
+ return self.local_enum_ref
233
+ elif isinstance(self.concept, Concept) and self.concept.enum_ref:
234
+ return self.concept.enum_ref
235
+ else:
236
+ return None
237
+
220
238
  def __str__(self) -> str:
221
239
  """Custom string representation without the class name."""
222
240
  processed_output = []
@@ -393,6 +411,7 @@ class DataflowInfo(
393
411
  end_period: The oldest period for which data are available.
394
412
  last_updated: When the dataflow was last updated.
395
413
  dsd_ref: The URN of the data structure used by the dataflow.
414
+ groups: The sequence of groups defined in the data structure.
396
415
  """
397
416
 
398
417
  id: str
@@ -408,6 +427,7 @@ class DataflowInfo(
408
427
  end_period: Optional[str] = None
409
428
  last_updated: Optional[datetime] = None
410
429
  dsd_ref: Optional[str] = None
430
+ groups: Optional[Sequence[Group]] = None
411
431
 
412
432
  def __str__(self) -> str:
413
433
  """Custom string representation without the class name."""
@@ -472,6 +492,7 @@ class Schema(Struct, frozen=True, omit_defaults=True, repr_omit_defaults=True):
472
492
  version: str = "1.0"
473
493
  artefacts: Sequence[str] = ()
474
494
  generated: datetime = datetime.now(timezone.utc)
495
+ name: Optional[str] = None
475
496
  groups: Optional[Sequence[Group]] = None
476
497
 
477
498
  def __str__(self) -> str:
@@ -533,6 +554,7 @@ class DataStructureDefinition(MaintainableArtefact, frozen=True, kw_only=True):
533
554
  valid_from: The date from which the data structure is valid.
534
555
  valid_to: The date until which the data structure is valid.
535
556
  version: The version of the data structure.
557
+ components: The list of relevant components for this data structure.
536
558
  evolving_structure: Whether new dimensions may be added under a
537
559
  minor version update.
538
560
  """
@@ -572,6 +594,7 @@ class DataStructureDefinition(MaintainableArtefact, frozen=True, kw_only=True):
572
594
  components=self.components,
573
595
  version=self.version,
574
596
  artefacts=self.__extract_artefacts(),
597
+ name=self.name,
575
598
  )
576
599
 
577
600
  @property
pysdmx/model/map.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any, Iterator, Literal, Optional, Sequence, Tuple, Union
6
6
 
7
7
  from msgspec import Struct
8
8
 
9
- from pysdmx.model.__base import MaintainableArtefact
9
+ from pysdmx.model.__base import Agency, MaintainableArtefact
10
10
  from pysdmx.util._date_pattern_map import convert_dpm
11
11
 
12
12
 
@@ -77,7 +77,7 @@ class DatePatternMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
77
77
  the target structure (e.g. `FREQ`). In this case, the input date
78
78
  can be converted to a different format, depending on the
79
79
  frequency of the converted data.
80
- resolvePeriod: The point in time to resolve to when mapping from low
80
+ resolve_period: The point in time to resolve to when mapping from low
81
81
  frequency to higher frequency periods.
82
82
  """
83
83
 
@@ -88,7 +88,7 @@ class DatePatternMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
88
88
  id: Optional[str] = None
89
89
  locale: str = "en"
90
90
  pattern_type: Literal["fixed", "variable"] = "fixed"
91
- resolvePeriod: Optional[
91
+ resolve_period: Optional[
92
92
  Literal["startOfPeriod", "endOfPeriod", "midPeriod"]
93
93
  ] = None
94
94
 
@@ -264,6 +264,14 @@ class MultiRepresentationMap(
264
264
  target: Sequence[str] = []
265
265
  maps: Sequence[MultiValueMap] = []
266
266
 
267
+ @property
268
+ def short_urn(self) -> str:
269
+ """Returns the short URN for the MultiRepresentationMap."""
270
+ agency = (
271
+ self.agency.id if isinstance(self.agency, Agency) else self.agency
272
+ )
273
+ return f"RepresentationMap={agency}:{self.id}({self.version})"
274
+
267
275
  def __iter__(
268
276
  self,
269
277
  ) -> Iterator[MultiValueMap]:
@@ -300,7 +308,7 @@ class MultiComponentMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
300
308
 
301
309
  source: Sequence[str]
302
310
  target: Sequence[str]
303
- values: MultiRepresentationMap
311
+ values: Union[MultiRepresentationMap, str]
304
312
 
305
313
 
306
314
  class RepresentationMap(MaintainableArtefact, frozen=True, omit_defaults=True):