pysdmx 1.5.2__py3-none-any.whl → 1.7.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 (63) 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 +20 -6
  18. pysdmx/io/json/fusion/messages/code.py +21 -4
  19. pysdmx/io/json/fusion/messages/concept.py +10 -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 +265 -22
  26. pysdmx/io/json/sdmxjson2/messages/concept.py +75 -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/__ss_aux_reader.py +1 -2
  47. pysdmx/io/xml/__structure_aux_reader.py +15 -10
  48. pysdmx/io/xml/__structure_aux_writer.py +15 -13
  49. pysdmx/io/xml/__write_data_aux.py +6 -57
  50. pysdmx/io/xml/__write_structure_specific_aux.py +7 -3
  51. pysdmx/io/xml/doc_validation.py +1 -3
  52. pysdmx/io/xml/sdmx21/writer/generic.py +6 -4
  53. pysdmx/model/__init__.py +1 -3
  54. pysdmx/model/code.py +11 -1
  55. pysdmx/model/dataflow.py +23 -0
  56. pysdmx/model/map.py +19 -13
  57. pysdmx/model/message.py +10 -5
  58. pysdmx/toolkit/pd/_data_utils.py +99 -0
  59. pysdmx/toolkit/vtl/_validations.py +2 -3
  60. {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/METADATA +4 -3
  61. {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/RECORD +63 -51
  62. {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/WHEEL +1 -1
  63. {pysdmx-1.5.2.dist-info → pysdmx-1.7.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)
@@ -30,8 +30,7 @@ def _reading_str_series(dataset: Dict[str, Any]) -> pd.DataFrame:
30
30
  if OBS in data:
31
31
  del keys[OBS]
32
32
  data[OBS] = add_list(data[OBS])
33
- for j in data[OBS]:
34
- test_list.append({**keys, **j})
33
+ test_list.extend([{**keys, **j} for j in data[OBS]])
35
34
  else:
36
35
  test_list.append(keys)
37
36
  test_list, df = __process_df(test_list, df)
@@ -397,7 +397,7 @@ class StructureParser(Struct):
397
397
  """
398
398
  if json_fac is None:
399
399
  return
400
- for key, _value in json_fac.items():
400
+ for key in json_fac:
401
401
  if key == TEXT_TYPE and json_fac[TEXT_TYPE] in list(DataType):
402
402
  json_obj["dtype"] = DataType(json_fac[TEXT_TYPE])
403
403
 
@@ -907,9 +907,9 @@ class StructureParser(Struct):
907
907
  item_json_info = self.__format_name_description(item_json_info)
908
908
  if CONTACT in item_json_info and item_name_class == AGENCY:
909
909
  item_json_info[CONTACT] = add_list(item_json_info[CONTACT])
910
- contacts = []
911
- for e in item_json_info[CONTACT]:
912
- contacts.append(self.__format_contact(e))
910
+ contacts = [
911
+ self.__format_contact(e) for e in item_json_info[CONTACT]
912
+ ]
913
913
  item_json_info[CONTACT.lower() + "s"] = contacts
914
914
  del item_json_info[CONTACT]
915
915
 
@@ -949,9 +949,11 @@ class StructureParser(Struct):
949
949
  group_dimensions = [group_dimensions]
950
950
 
951
951
  group["dimensions"] = [
952
- d[DIM_REF]
953
- if isinstance(d[DIM_REF], str)
954
- else d[DIM_REF][REF][ID]
952
+ (
953
+ d[DIM_REF]
954
+ if isinstance(d[DIM_REF], str)
955
+ else d[DIM_REF][REF][ID]
956
+ )
955
957
  for d in group_dimensions
956
958
  ]
957
959
 
@@ -994,9 +996,12 @@ class StructureParser(Struct):
994
996
  items = []
995
997
  if item in element:
996
998
  element[item] = add_list(element[item])
997
- for item_elem in element[item]:
998
- # Dynamic
999
- items.append(self.__format_item(item_elem, item))
999
+ items.extend(
1000
+ [
1001
+ self.__format_item(item_elem, item)
1002
+ for item_elem in element[item]
1003
+ ]
1004
+ )
1000
1005
  del element[item]
1001
1006
  element["items"] = items
1002
1007
  element = self.__format_agency(element)
@@ -310,9 +310,9 @@ def __write_maintainable(
310
310
  f"{str(maintainable.is_external_reference).lower()!r}"
311
311
  )
312
312
  if not references_30 and not (isinstance(maintainable, AgencyScheme)):
313
- outfile["Attributes"] += (
314
- f" isFinal={str(maintainable.is_final).lower()!r}"
315
- )
313
+ outfile[
314
+ "Attributes"
315
+ ] += f" isFinal={str(maintainable.is_final).lower()!r}"
316
316
 
317
317
  if isinstance(maintainable.agency, str):
318
318
  outfile["Attributes"] += f" agencyID={maintainable.agency!r}"
@@ -462,7 +462,7 @@ def __write_components( # noqa: C901
462
462
  )
463
463
 
464
464
  position = 1
465
- for _, comps in components.items():
465
+ for comps in components.values():
466
466
  if comps:
467
467
  role_name = ROLE_MAPPING[comps[0].role]
468
468
  if role_name == MEASURE:
@@ -809,9 +809,9 @@ def __write_scheme( # noqa: C901
809
809
  DSD,
810
810
  DFW,
811
811
  ]:
812
- data["Attributes"] += (
813
- f" isPartial={str(item_scheme.is_partial).lower()!r}"
814
- )
812
+ data[
813
+ "Attributes"
814
+ ] += f" isPartial={str(item_scheme.is_partial).lower()!r}"
815
815
  if scheme in [
816
816
  RULE_SCHEME,
817
817
  UDO_SCHEME,
@@ -820,9 +820,9 @@ def __write_scheme( # noqa: C901
820
820
  CUSTOM_TYPE_SCHEME,
821
821
  NAME_PER_SCHEME,
822
822
  ]:
823
- data["Attributes"] += (
824
- f" {_write_vtl(item_scheme, indent, references_30)}"
825
- )
823
+ data[
824
+ "Attributes"
825
+ ] += f" {_write_vtl(item_scheme, indent, references_30)}"
826
826
 
827
827
  outfile = ""
828
828
 
@@ -1129,9 +1129,11 @@ def _write_vtl( # noqa: C901
1129
1129
  ref_codelist = (
1130
1130
  item_or_scheme.codelist
1131
1131
  if isinstance(item_or_scheme.codelist, Reference)
1132
- else parse_urn(item_or_scheme.codelist)
1133
- if isinstance(item_or_scheme.codelist, str)
1134
- else parse_short_urn(item_or_scheme.codelist.short_urn)
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
+ )
1135
1137
  )
1136
1138
  if references_30:
1137
1139
  data += (
@@ -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
@@ -56,13 +54,15 @@ def writing_validation(dataset: PandasDataset) -> None:
56
54
  for comp in dataset.structure.components
57
55
  if comp.role in (Role.DIMENSION, Role.MEASURE)
58
56
  ]
59
- for att in dataset.structure.components.attributes:
57
+ required_components.extend(
58
+ att.id
59
+ for att in dataset.structure.components.attributes
60
60
  if (
61
61
  att.required
62
62
  and att.attachment_level is not None
63
63
  and att.attachment_level != "D"
64
- ):
65
- required_components.append(att.id)
64
+ )
65
+ )
66
66
  non_required = [
67
67
  comp.id
68
68
  for comp in dataset.structure.components
@@ -87,54 +87,3 @@ def writing_validation(dataset: PandasDataset) -> None:
87
87
  )
88
88
  if not dataset.structure.components.measures:
89
89
  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
 
@@ -198,8 +198,12 @@ def __group_processing(
198
198
  .to_dict(orient="records")
199
199
  )
200
200
 
201
- for record in grouped_data:
202
- out_list.append(__format_group_str(record, group["group_id"]))
201
+ out_list.extend(
202
+ [
203
+ __format_group_str(record, group["group_id"])
204
+ for record in grouped_data
205
+ ]
206
+ )
203
207
 
204
208
  return "".join(out_list)
205
209
 
@@ -39,9 +39,7 @@ def validate_doc(input_str: str) -> None:
39
39
  doc = etree.parse(bytes_infile, parser=parser)
40
40
  if not xmlschema.validate(doc):
41
41
  log_errors = list(xmlschema.error_log) # type: ignore[call-overload]
42
- unhandled_errors = []
43
- for e in log_errors:
44
- unhandled_errors.append(e.message)
42
+ unhandled_errors = [e.message for e in log_errors]
45
43
  severe_errors = unhandled_errors.copy()
46
44
  for e in unhandled_errors:
47
45
  for allowed_error in ALLOWED_ERRORS_CONTENT: