pysdmx 1.3.0__py3-none-any.whl → 1.4.0rc1__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.
- pysdmx/__extras_check.py +3 -2
- pysdmx/__init__.py +1 -1
- pysdmx/api/fmr/__init__.py +4 -4
- pysdmx/api/gds/__init__.py +328 -0
- pysdmx/api/qb/gds.py +153 -0
- pysdmx/api/qb/service.py +91 -3
- pysdmx/api/qb/structure.py +1 -0
- pysdmx/api/qb/util.py +1 -0
- pysdmx/io/__init__.py +2 -1
- pysdmx/io/csv/sdmx10/reader/__init__.py +4 -2
- pysdmx/io/csv/sdmx10/writer/__init__.py +15 -2
- pysdmx/io/csv/sdmx20/reader/__init__.py +5 -2
- pysdmx/io/csv/sdmx20/writer/__init__.py +13 -2
- pysdmx/io/format.py +4 -0
- pysdmx/io/input_processor.py +12 -3
- pysdmx/io/json/fusion/messages/core.py +2 -0
- pysdmx/io/json/fusion/messages/report.py +13 -7
- pysdmx/io/json/gds/messages/__init__.py +35 -0
- pysdmx/io/json/gds/messages/agencies.py +41 -0
- pysdmx/io/json/gds/messages/catalog.py +79 -0
- pysdmx/io/json/gds/messages/sdmx_api.py +23 -0
- pysdmx/io/json/gds/messages/services.py +49 -0
- pysdmx/io/json/gds/messages/urn_resolver.py +43 -0
- pysdmx/io/json/gds/reader/__init__.py +12 -0
- pysdmx/io/json/sdmxjson2/messages/__init__.py +12 -4
- pysdmx/io/json/sdmxjson2/messages/agency.py +72 -0
- pysdmx/io/json/sdmxjson2/messages/category.py +22 -29
- pysdmx/io/json/sdmxjson2/messages/code.py +68 -64
- pysdmx/io/json/sdmxjson2/messages/concept.py +9 -18
- pysdmx/io/json/sdmxjson2/messages/constraint.py +2 -13
- pysdmx/io/json/sdmxjson2/messages/core.py +113 -21
- pysdmx/io/json/sdmxjson2/messages/dataflow.py +51 -21
- pysdmx/io/json/sdmxjson2/messages/dsd.py +110 -36
- pysdmx/io/json/sdmxjson2/messages/map.py +61 -49
- pysdmx/io/json/sdmxjson2/messages/pa.py +9 -17
- pysdmx/io/json/sdmxjson2/messages/provider.py +88 -0
- pysdmx/io/json/sdmxjson2/messages/report.py +84 -14
- pysdmx/io/json/sdmxjson2/messages/schema.py +14 -5
- pysdmx/io/json/sdmxjson2/messages/structure.py +105 -36
- pysdmx/io/json/sdmxjson2/messages/vtl.py +42 -96
- pysdmx/io/pd.py +2 -9
- pysdmx/io/reader.py +72 -27
- pysdmx/io/serde.py +11 -0
- pysdmx/io/writer.py +134 -0
- pysdmx/io/xml/{sdmx21/reader/__data_aux.py → __data_aux.py} +9 -2
- pysdmx/io/xml/{sdmx21/reader/__parse_xml.py → __parse_xml.py} +30 -6
- pysdmx/io/xml/__ss_aux_reader.py +96 -0
- pysdmx/io/xml/__structure_aux_reader.py +1174 -0
- pysdmx/io/xml/__structure_aux_writer.py +1233 -0
- pysdmx/io/xml/{sdmx21/__tokens.py → __tokens.py} +33 -1
- pysdmx/io/xml/{sdmx21/writer/__write_aux.py → __write_aux.py} +129 -37
- pysdmx/io/xml/{sdmx21/writer/__write_data_aux.py → __write_data_aux.py} +1 -1
- pysdmx/io/xml/__write_structure_specific_aux.py +254 -0
- pysdmx/io/xml/{sdmx21/reader/doc_validation.py → doc_validation.py} +10 -2
- pysdmx/io/xml/{sdmx21/reader/header.py → header.py} +11 -3
- pysdmx/io/xml/sdmx21/reader/error.py +2 -2
- pysdmx/io/xml/sdmx21/reader/generic.py +12 -8
- pysdmx/io/xml/sdmx21/reader/structure.py +5 -840
- pysdmx/io/xml/sdmx21/reader/structure_specific.py +13 -97
- pysdmx/io/xml/sdmx21/reader/submission.py +2 -2
- pysdmx/io/xml/sdmx21/writer/error.py +1 -1
- pysdmx/io/xml/sdmx21/writer/generic.py +13 -7
- pysdmx/io/xml/sdmx21/writer/structure.py +16 -828
- pysdmx/io/xml/sdmx21/writer/structure_specific.py +13 -238
- pysdmx/io/xml/sdmx30/__init__.py +1 -0
- pysdmx/io/xml/sdmx30/reader/__init__.py +1 -0
- pysdmx/io/xml/sdmx30/reader/structure.py +39 -0
- pysdmx/io/xml/sdmx30/reader/structure_specific.py +39 -0
- pysdmx/io/xml/sdmx30/writer/__init__.py +1 -0
- pysdmx/io/xml/sdmx30/writer/structure.py +67 -0
- pysdmx/io/xml/sdmx30/writer/structure_specific.py +108 -0
- pysdmx/model/__base.py +99 -34
- pysdmx/model/__init__.py +4 -0
- pysdmx/model/category.py +20 -0
- pysdmx/model/code.py +29 -8
- pysdmx/model/concept.py +52 -11
- pysdmx/model/dataflow.py +117 -33
- pysdmx/model/dataset.py +66 -14
- pysdmx/model/gds.py +161 -0
- pysdmx/model/map.py +51 -8
- pysdmx/model/message.py +235 -55
- pysdmx/model/metadata.py +79 -16
- pysdmx/model/submission.py +12 -7
- pysdmx/model/vtl.py +30 -13
- pysdmx/toolkit/__init__.py +1 -1
- pysdmx/toolkit/pd/__init__.py +85 -0
- pysdmx/toolkit/vtl/__init__.py +2 -1
- pysdmx/toolkit/vtl/_validations.py +1 -1
- pysdmx/toolkit/vtl/{generate_vtl_script.py → script_generation.py} +30 -4
- pysdmx/toolkit/vtl/validation.py +119 -0
- pysdmx/util/_model_utils.py +1 -1
- pysdmx-1.4.0rc1.dist-info/METADATA +119 -0
- pysdmx-1.4.0rc1.dist-info/RECORD +140 -0
- pysdmx/io/json/sdmxjson2/messages/org.py +0 -140
- pysdmx/toolkit/vtl/model_validations.py +0 -50
- pysdmx-1.3.0.dist-info/METADATA +0 -76
- pysdmx-1.3.0.dist-info/RECORD +0 -116
- /pysdmx/io/xml/{sdmx21/writer/config.py → config.py} +0 -0
- {pysdmx-1.3.0.dist-info → pysdmx-1.4.0rc1.dist-info}/LICENSE +0 -0
- {pysdmx-1.3.0.dist-info → pysdmx-1.4.0rc1.dist-info}/WHEEL +0 -0
pysdmx/model/dataflow.py
CHANGED
|
@@ -35,15 +35,39 @@ class Role(str, Enum):
|
|
|
35
35
|
ATTRIBUTE = "A"
|
|
36
36
|
"""The component provides descriptive information about the data."""
|
|
37
37
|
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
"""Return the role as a string."""
|
|
40
|
+
return self.name.capitalize()
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
"""Role String representation."""
|
|
44
|
+
return f"{self.__class__.__name__}.{self._name_}"
|
|
45
|
+
|
|
38
46
|
|
|
39
|
-
class ArrayBoundaries(Struct, frozen=True):
|
|
47
|
+
class ArrayBoundaries(Struct, frozen=True, repr_omit_defaults=True):
|
|
40
48
|
"""The minimum and maximum number of items in the SDMX array."""
|
|
41
49
|
|
|
42
50
|
min_size: int = 0
|
|
43
51
|
max_size: Optional[int] = None
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
"""Custom string representation without the class name."""
|
|
55
|
+
processed_output = []
|
|
56
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
57
|
+
processed_output.append(f"{attr}: {value}")
|
|
58
|
+
return f"{', '.join(processed_output)}"
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
62
|
+
attrs = []
|
|
63
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
64
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
65
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Component(
|
|
69
|
+
Struct, frozen=True, omit_defaults=True, repr_omit_defaults=True
|
|
70
|
+
):
|
|
47
71
|
"""A component of a dataset (aka **variable**), such the frequency.
|
|
48
72
|
|
|
49
73
|
Concepts are used to **describe the relevant characteristics** of a
|
|
@@ -98,11 +122,12 @@ class Component(Struct, frozen=True, omit_defaults=True):
|
|
|
98
122
|
description: Additional descriptive information about the component.
|
|
99
123
|
local_codes: The expected local values for the component (e.g. currency
|
|
100
124
|
codes).
|
|
101
|
-
attachment_level: The
|
|
125
|
+
attachment_level: The attachment level (if role = A only).
|
|
102
126
|
Attributes can be attached at different levels such as
|
|
103
127
|
D (for dataset-level attributes), O (for observation-level
|
|
104
128
|
attributes) or a combination of dimension IDs, separated by
|
|
105
129
|
commas, for series- and group-level attributes).
|
|
130
|
+
A post_init check makes this attribute mandatory for attributes.
|
|
106
131
|
array_def: Any additional constraints for array types.
|
|
107
132
|
"""
|
|
108
133
|
|
|
@@ -129,6 +154,12 @@ class Component(Struct, frozen=True, omit_defaults=True):
|
|
|
129
154
|
"only allowed for attribute components"
|
|
130
155
|
),
|
|
131
156
|
)
|
|
157
|
+
if self.role == Role.ATTRIBUTE and self.attachment_level is None:
|
|
158
|
+
raise Invalid(
|
|
159
|
+
"Validation Error",
|
|
160
|
+
"The attachment_level field is mandatory "
|
|
161
|
+
"for attribute components",
|
|
162
|
+
)
|
|
132
163
|
|
|
133
164
|
@property
|
|
134
165
|
def dtype(self) -> DataType:
|
|
@@ -186,16 +217,18 @@ class Component(Struct, frozen=True, omit_defaults=True):
|
|
|
186
217
|
return None
|
|
187
218
|
|
|
188
219
|
def __str__(self) -> str:
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
for
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
"""Custom string representation without the class name."""
|
|
221
|
+
processed_output = []
|
|
222
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
223
|
+
processed_output.append(f"{attr}: {value}")
|
|
224
|
+
return f"{', '.join(processed_output)}"
|
|
225
|
+
|
|
226
|
+
def __repr__(self) -> str:
|
|
227
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
228
|
+
attrs = []
|
|
229
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
230
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
231
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
|
199
232
|
|
|
200
233
|
|
|
201
234
|
class Components(UserList[Component]):
|
|
@@ -313,8 +346,21 @@ class Components(UserList[Component]):
|
|
|
313
346
|
f"There is already a component with ID: {fld.id}",
|
|
314
347
|
)
|
|
315
348
|
|
|
349
|
+
def __str__(self) -> str:
|
|
350
|
+
"""Custom string representation without the class name."""
|
|
351
|
+
return f"data: {len(self)} components"
|
|
352
|
+
|
|
353
|
+
def __repr__(self) -> str:
|
|
354
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
355
|
+
attrs = []
|
|
356
|
+
for attr, value in self.__dict__.items():
|
|
357
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
358
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
|
316
359
|
|
|
317
|
-
|
|
360
|
+
|
|
361
|
+
class DataflowInfo(
|
|
362
|
+
Struct, frozen=True, omit_defaults=True, repr_omit_defaults=True
|
|
363
|
+
):
|
|
318
364
|
"""Extended information about a dataflow.
|
|
319
365
|
|
|
320
366
|
The information includes:
|
|
@@ -355,16 +401,32 @@ class DataflowInfo(Struct, frozen=True, omit_defaults=True):
|
|
|
355
401
|
dsd_ref: Optional[str] = None
|
|
356
402
|
|
|
357
403
|
def __str__(self) -> str:
|
|
358
|
-
"""
|
|
359
|
-
|
|
360
|
-
for
|
|
361
|
-
|
|
362
|
-
if
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
404
|
+
"""Custom string representation without the class name."""
|
|
405
|
+
processed_output = []
|
|
406
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
407
|
+
# str is taken as a Sequence, so we need to check it's not a str
|
|
408
|
+
if isinstance(value, Sequence) and not isinstance(value, str):
|
|
409
|
+
# Handle non-empty lists
|
|
410
|
+
if not value:
|
|
411
|
+
continue
|
|
412
|
+
class_name = value[0].__class__.__name__
|
|
413
|
+
value = f"{len(value)} {class_name.lower()}s"
|
|
414
|
+
|
|
415
|
+
processed_output.append(f"{attr}: {value}")
|
|
416
|
+
return f"{', '.join(processed_output)}"
|
|
417
|
+
|
|
418
|
+
def __repr__(self) -> str:
|
|
419
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
420
|
+
attrs = []
|
|
421
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
422
|
+
# Omit empty sequences
|
|
423
|
+
if isinstance(value, (list, tuple, set)) and not value:
|
|
424
|
+
continue
|
|
425
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
426
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class Schema(Struct, frozen=True, omit_defaults=True, repr_omit_defaults=True):
|
|
368
430
|
"""The allowed content within a certain context.
|
|
369
431
|
|
|
370
432
|
This is the equivalent to the result of a schema query in the
|
|
@@ -403,13 +465,32 @@ class Schema(Struct, frozen=True, omit_defaults=True):
|
|
|
403
465
|
generated: datetime = datetime.now(timezone.utc)
|
|
404
466
|
|
|
405
467
|
def __str__(self) -> str:
|
|
406
|
-
"""
|
|
407
|
-
|
|
408
|
-
for
|
|
409
|
-
|
|
410
|
-
if
|
|
411
|
-
|
|
412
|
-
|
|
468
|
+
"""Custom string representation without the class name."""
|
|
469
|
+
processed_output = []
|
|
470
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
471
|
+
# str is taken as a Sequence, so we need to check it's not a str
|
|
472
|
+
if isinstance(value, Sequence) and not isinstance(value, str):
|
|
473
|
+
# Handle non-empty lists
|
|
474
|
+
if not value:
|
|
475
|
+
continue
|
|
476
|
+
class_name = value[0].__class__.__name__
|
|
477
|
+
# If the value is a list of artefacts, we can summarize it
|
|
478
|
+
if attr == "artefacts":
|
|
479
|
+
class_name = "Artefact"
|
|
480
|
+
value = f"{len(value)} {class_name.lower()}s"
|
|
481
|
+
|
|
482
|
+
processed_output.append(f"{attr}: {value}")
|
|
483
|
+
return f"{', '.join(processed_output)}"
|
|
484
|
+
|
|
485
|
+
def __repr__(self) -> str:
|
|
486
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
487
|
+
attrs = []
|
|
488
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
489
|
+
# Omit empty sequences
|
|
490
|
+
if isinstance(value, (list, tuple, set)) and not value:
|
|
491
|
+
continue
|
|
492
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
493
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
|
413
494
|
|
|
414
495
|
@property
|
|
415
496
|
def short_urn(self) -> str:
|
|
@@ -442,9 +523,12 @@ class DataStructureDefinition(MaintainableArtefact, frozen=True, kw_only=True):
|
|
|
442
523
|
valid_from: The date from which the data structure is valid.
|
|
443
524
|
valid_to: The date until which the data structure is valid.
|
|
444
525
|
version: The version of the data structure.
|
|
526
|
+
evolving_structure: Whether new dimensions may be added under a
|
|
527
|
+
minor version update.
|
|
445
528
|
"""
|
|
446
529
|
|
|
447
530
|
components: Components
|
|
531
|
+
evolving_structure: bool = False
|
|
448
532
|
|
|
449
533
|
def __extract_artefacts(self) -> Sequence[str]:
|
|
450
534
|
"""Extract the artefacts used to generate the schema."""
|
|
@@ -492,7 +576,7 @@ class Dataflow(
|
|
|
492
576
|
):
|
|
493
577
|
"""A flow of data that providers will provide."""
|
|
494
578
|
|
|
495
|
-
structure: Optional[str] = None
|
|
579
|
+
structure: Optional[Union[DataStructureDefinition, str]] = None
|
|
496
580
|
|
|
497
581
|
|
|
498
582
|
class ProvisionAgreement(
|
pysdmx/model/dataset.py
CHANGED
|
@@ -10,9 +10,15 @@ from pysdmx.model import Schema
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class ActionType(Enum):
|
|
13
|
-
"""
|
|
13
|
+
"""Enumeration that defines the Dataset Action.
|
|
14
|
+
|
|
15
|
+
Arguments:
|
|
16
|
+
Append: Append data to an existing dataset.
|
|
17
|
+
Replace: Replace the existing dataset with new data.
|
|
18
|
+
Delete: Delete the data provided from the data source.
|
|
19
|
+
Information: Provide information about the dataset
|
|
20
|
+
without modifying it.
|
|
14
21
|
|
|
15
|
-
Enumeration that withholds the Action type for writing purposes.
|
|
16
22
|
"""
|
|
17
23
|
|
|
18
24
|
Append = "Append"
|
|
@@ -20,8 +26,16 @@ class ActionType(Enum):
|
|
|
20
26
|
Delete = "Delete"
|
|
21
27
|
Information = "Information"
|
|
22
28
|
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
"""Return the action as a string."""
|
|
31
|
+
return self.name.capitalize()
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
"""Action String representation."""
|
|
35
|
+
return f"{self.__class__.__name__}.{self._name_}"
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
|
|
38
|
+
class SeriesInfo(Struct, frozen=True, repr_omit_defaults=True):
|
|
25
39
|
"""A group of related data, such as a time series, or a case series.
|
|
26
40
|
|
|
27
41
|
Attributes:
|
|
@@ -44,17 +58,41 @@ class SeriesInfo(Struct, frozen=True):
|
|
|
44
58
|
is_active: bool = True
|
|
45
59
|
|
|
46
60
|
def __str__(self) -> str:
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
for
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
"""Custom string representation without the class name."""
|
|
62
|
+
processed_output = []
|
|
63
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
64
|
+
processed_output.append(f"{attr}: {value}")
|
|
65
|
+
return f"{', '.join(processed_output)}"
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
69
|
+
attrs = []
|
|
70
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
71
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
72
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Dataset(Struct, frozen=False, repr_omit_defaults=True, kw_only=True):
|
|
76
|
+
"""An organised collection of data.
|
|
77
|
+
|
|
78
|
+
It includes metadata such as the structure of the dataset, attributes,
|
|
79
|
+
action type, reporting periods and publication details.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
structure: The structure referenced from a dataset,
|
|
83
|
+
which can be a string (short_urn) or a Schema object.
|
|
84
|
+
attributes: dictionary of attributes at dataset level, with its values.
|
|
85
|
+
action: Defines the :class:`Action <pysdmx.model.dataset.ActionType>`
|
|
86
|
+
of the dataset, default is ActionType.Information.
|
|
87
|
+
reporting_begin: The start date for reporting, if applicable.
|
|
88
|
+
reporting_end: The end date for reporting, if applicable.
|
|
89
|
+
data_extraction_date: The date when the data was extracted.
|
|
90
|
+
valid_from: The start date for the validity of the dataset.
|
|
91
|
+
valid_to: The end date for the validity of the dataset.
|
|
92
|
+
publication_year: The year of publication of the dataset.
|
|
93
|
+
publication_period: The period of publication of the dataset.
|
|
94
|
+
set_id: An optional identifier for the dataset.
|
|
95
|
+
"""
|
|
58
96
|
|
|
59
97
|
structure: Union[str, Schema]
|
|
60
98
|
attributes: Dict[str, Any] = {}
|
|
@@ -79,3 +117,17 @@ class Dataset(Struct, frozen=False, kw_only=True):
|
|
|
79
117
|
return self.structure
|
|
80
118
|
else:
|
|
81
119
|
return self.structure.short_urn
|
|
120
|
+
|
|
121
|
+
def __str__(self) -> str:
|
|
122
|
+
"""Custom string representation without the class name."""
|
|
123
|
+
processed_output = []
|
|
124
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
125
|
+
processed_output.append(f"{attr}: {value}")
|
|
126
|
+
return f"{', '.join(processed_output)}"
|
|
127
|
+
|
|
128
|
+
def __repr__(self) -> str:
|
|
129
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
130
|
+
attrs = []
|
|
131
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
132
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
133
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
pysdmx/model/gds.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Models for GDS data.
|
|
2
|
+
|
|
3
|
+
This module defines classes for representing GDS-specific data,
|
|
4
|
+
such as agencies, in the SDMX data model.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional, Sequence, Union
|
|
8
|
+
|
|
9
|
+
from msgspec import Struct
|
|
10
|
+
|
|
11
|
+
from pysdmx.model.__base import MaintainableArtefact
|
|
12
|
+
from pysdmx.util import parse_maintainable_urn
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GdsBase(Struct, repr_omit_defaults=True, frozen=True):
|
|
16
|
+
"""Base class for all GDS models with a custom __str__ method."""
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
"""Custom string representation without the class name."""
|
|
20
|
+
processed_output = []
|
|
21
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
22
|
+
# str is taken as a Sequence, so we need to check it's not a str
|
|
23
|
+
if isinstance(value, Sequence) and not isinstance(value, str):
|
|
24
|
+
# Handle non-empty lists
|
|
25
|
+
if not value:
|
|
26
|
+
continue
|
|
27
|
+
class_name = value[0].__class__.__name__
|
|
28
|
+
value = f"{len(value)} {class_name.lower()}s"
|
|
29
|
+
|
|
30
|
+
processed_output.append(f"{attr}: {value}")
|
|
31
|
+
return f"{', '.join(processed_output)}"
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
35
|
+
attrs = []
|
|
36
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
37
|
+
# Omit empty sequences
|
|
38
|
+
if isinstance(value, (list, tuple, set)) and not value:
|
|
39
|
+
continue
|
|
40
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
41
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GdsEndpoint(GdsBase, frozen=True):
|
|
45
|
+
"""Represents a GDS endpoint.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
api_version: The API version of the endpoint.
|
|
49
|
+
url: The URL of the endpoint.
|
|
50
|
+
comments: Comments about the endpoint.
|
|
51
|
+
message_formats: List of message formats supported by the endpoint.
|
|
52
|
+
rest_resources: List of REST resources available at the endpoint.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
api_version: str
|
|
56
|
+
url: str
|
|
57
|
+
comments: str
|
|
58
|
+
message_formats: List[str]
|
|
59
|
+
rest_resources: List[str]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GdsServiceReference(GdsBase, frozen=True):
|
|
63
|
+
"""Represents a GDS service reference.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
id: The ID of the service reference.
|
|
67
|
+
name: The name of the service reference.
|
|
68
|
+
urn: The URN of the service reference.
|
|
69
|
+
service: The service associated with the reference.
|
|
70
|
+
description: An optional description of the service reference.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
id: str
|
|
74
|
+
name: str
|
|
75
|
+
urn: str
|
|
76
|
+
service: str
|
|
77
|
+
description: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def short_urn(self) -> str:
|
|
81
|
+
"""Returns a short URN for the ServiceReference."""
|
|
82
|
+
return parse_maintainable_urn(self.service).__str__()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class GdsService(GdsBase, MaintainableArtefact, frozen=True, kw_only=True):
|
|
86
|
+
"""Represents a GDS service.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
agency: The AgencyID of the service's owner.
|
|
90
|
+
base: The base URL of the service.
|
|
91
|
+
endpoints: List of GDS endpoints available at the service.
|
|
92
|
+
authentication: Optional authentication method for the service.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
base: str
|
|
96
|
+
endpoints: List[GdsEndpoint]
|
|
97
|
+
authentication: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class GdsCatalog(GdsBase, MaintainableArtefact, frozen=True, kw_only=True):
|
|
101
|
+
"""Represents a GDS catalog.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
agency: The AgencyID of the catalog's owner.
|
|
105
|
+
id: The ID of the catalog.
|
|
106
|
+
urn: The URN of the catalog.
|
|
107
|
+
version: The version of the catalog.
|
|
108
|
+
services: Optional list of GdsService or GdsServiceReference,
|
|
109
|
+
depending on the references parameter, that are associated with the
|
|
110
|
+
catalog.
|
|
111
|
+
endpoints: List of GDS endpoints available at the catalog.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
services: Optional[List[Union[GdsService, GdsServiceReference]]] = None
|
|
115
|
+
endpoints: Optional[List[GdsEndpoint]] = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GdsSdmxApi(GdsBase, frozen=True):
|
|
119
|
+
"""Represents an SDMX API version.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
release: The release version of the SDMX API.
|
|
123
|
+
description: A description of the release.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
release: str
|
|
127
|
+
description: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ResolverResult(
|
|
131
|
+
GdsBase, frozen=True, rename={"query_response_status_code": "status_code"}
|
|
132
|
+
):
|
|
133
|
+
"""Represents a single resolver result.
|
|
134
|
+
|
|
135
|
+
Attributes:
|
|
136
|
+
api_version: The API version of the resolver result.
|
|
137
|
+
query: The query URL for the resource.
|
|
138
|
+
status_code: The HTTP response code for the query.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
api_version: str
|
|
142
|
+
query: str
|
|
143
|
+
status_code: int
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class GdsUrnResolver(GdsBase, frozen=True):
|
|
147
|
+
"""Represents the response for a URN resolver query.
|
|
148
|
+
|
|
149
|
+
Attributes:
|
|
150
|
+
agency: The agency maintaining the resource.
|
|
151
|
+
resource_id: The ID of the resource.
|
|
152
|
+
version: The version of the resource.
|
|
153
|
+
sdmx_type: The type of SDMX resource.
|
|
154
|
+
resolver_results: A list of resolver results.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
agency: str
|
|
158
|
+
resource_id: str
|
|
159
|
+
version: str
|
|
160
|
+
sdmx_type: str
|
|
161
|
+
resolver_results: List[ResolverResult]
|
pysdmx/model/map.py
CHANGED
|
@@ -10,7 +10,43 @@ from pysdmx.model.__base import MaintainableArtefact
|
|
|
10
10
|
from pysdmx.util._date_pattern_map import convert_dpm
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class _BaseMap(
|
|
14
|
+
Struct,
|
|
15
|
+
frozen=True,
|
|
16
|
+
omit_defaults=True,
|
|
17
|
+
repr_omit_defaults=True,
|
|
18
|
+
):
|
|
19
|
+
"""Base class for mapping definitions."""
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
"""Custom string representation without the class name."""
|
|
23
|
+
processed_output = []
|
|
24
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
25
|
+
# str is taken as a Sequence, so we need to check it's not a str
|
|
26
|
+
if isinstance(value, Sequence) and not isinstance(value, str):
|
|
27
|
+
# Handle non-empty lists
|
|
28
|
+
if value:
|
|
29
|
+
class_name = value[0].__class__.__name__
|
|
30
|
+
value = f"{len(value)} {class_name.lower()}s"
|
|
31
|
+
# redundant if check for python 3.9 and lower versions cov
|
|
32
|
+
if not value:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
processed_output.append(f"{attr}: {value}")
|
|
36
|
+
return f"{', '.join(processed_output)}"
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
"""Custom __repr__ that omits empty sequences."""
|
|
40
|
+
attrs = []
|
|
41
|
+
for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
|
|
42
|
+
# Omit empty sequences
|
|
43
|
+
if isinstance(value, (list, tuple, set)) and not value:
|
|
44
|
+
continue
|
|
45
|
+
attrs.append(f"{attr}={repr(value)}")
|
|
46
|
+
return f"{self.__class__.__name__}({', '.join(attrs)})"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DatePatternMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
|
|
14
50
|
"""A mapping based on a date pattern.
|
|
15
51
|
|
|
16
52
|
Examples:
|
|
@@ -41,6 +77,8 @@ class DatePatternMap(Struct, frozen=True, omit_defaults=True, tag=True):
|
|
|
41
77
|
the target structure (e.g. `FREQ`). In this case, the input date
|
|
42
78
|
can be converted to a different format, depending on the
|
|
43
79
|
frequency of the converted data.
|
|
80
|
+
resolvePeriod: The point in time to resolve to when mapping from low
|
|
81
|
+
frequency to higher frequency periods.
|
|
44
82
|
"""
|
|
45
83
|
|
|
46
84
|
source: str
|
|
@@ -50,6 +88,9 @@ class DatePatternMap(Struct, frozen=True, omit_defaults=True, tag=True):
|
|
|
50
88
|
id: Optional[str] = None
|
|
51
89
|
locale: str = "en"
|
|
52
90
|
pattern_type: Literal["fixed", "variable"] = "fixed"
|
|
91
|
+
resolvePeriod: Optional[
|
|
92
|
+
Literal["startOfPeriod", "endOfPeriod", "midPeriod"]
|
|
93
|
+
] = None
|
|
53
94
|
|
|
54
95
|
@property
|
|
55
96
|
def py_pattern(self) -> str:
|
|
@@ -57,7 +98,7 @@ class DatePatternMap(Struct, frozen=True, omit_defaults=True, tag=True):
|
|
|
57
98
|
return convert_dpm(self.pattern)
|
|
58
99
|
|
|
59
100
|
|
|
60
|
-
class FixedValueMap(
|
|
101
|
+
class FixedValueMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
|
|
61
102
|
"""Set a component to a fixed value.
|
|
62
103
|
|
|
63
104
|
Examples:
|
|
@@ -81,7 +122,9 @@ class FixedValueMap(Struct, frozen=True, omit_defaults=True, tag=True):
|
|
|
81
122
|
located_in: Literal["source", "target"] = "target"
|
|
82
123
|
|
|
83
124
|
|
|
84
|
-
class ImplicitComponentMap(
|
|
125
|
+
class ImplicitComponentMap(
|
|
126
|
+
_BaseMap, frozen=True, omit_defaults=True, tag=True
|
|
127
|
+
):
|
|
85
128
|
"""A mapping where the value in the source is copied to the target.
|
|
86
129
|
|
|
87
130
|
Examples:
|
|
@@ -103,7 +146,7 @@ class ImplicitComponentMap(Struct, frozen=True, omit_defaults=True, tag=True):
|
|
|
103
146
|
target: str
|
|
104
147
|
|
|
105
148
|
|
|
106
|
-
class MultiValueMap(
|
|
149
|
+
class MultiValueMap(_BaseMap, frozen=True, omit_defaults=True, kw_only=True):
|
|
107
150
|
"""Provides the values for a mapping between one or more components.
|
|
108
151
|
|
|
109
152
|
Examples:
|
|
@@ -158,7 +201,7 @@ class MultiValueMap(Struct, frozen=True, omit_defaults=True, kw_only=True):
|
|
|
158
201
|
return tuple(out)
|
|
159
202
|
|
|
160
203
|
|
|
161
|
-
class ValueMap(
|
|
204
|
+
class ValueMap(_BaseMap, frozen=True, omit_defaults=True, kw_only=True):
|
|
162
205
|
"""Maps the values of two components together.
|
|
163
206
|
|
|
164
207
|
Examples:
|
|
@@ -232,7 +275,7 @@ class MultiRepresentationMap(
|
|
|
232
275
|
return len(self.maps)
|
|
233
276
|
|
|
234
277
|
|
|
235
|
-
class MultiComponentMap(
|
|
278
|
+
class MultiComponentMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
|
|
236
279
|
"""Maps one or more source components to one or more target components.
|
|
237
280
|
|
|
238
281
|
Examples:
|
|
@@ -293,7 +336,7 @@ class RepresentationMap(MaintainableArtefact, frozen=True, omit_defaults=True):
|
|
|
293
336
|
return len(self.maps)
|
|
294
337
|
|
|
295
338
|
|
|
296
|
-
class ComponentMap(
|
|
339
|
+
class ComponentMap(_BaseMap, frozen=True, omit_defaults=True, tag=True):
|
|
297
340
|
"""Maps a source component to a target component.
|
|
298
341
|
|
|
299
342
|
Examples:
|
|
@@ -314,7 +357,7 @@ class ComponentMap(Struct, frozen=True, omit_defaults=True, tag=True):
|
|
|
314
357
|
|
|
315
358
|
source: str
|
|
316
359
|
target: str
|
|
317
|
-
values: RepresentationMap
|
|
360
|
+
values: Union[RepresentationMap, str]
|
|
318
361
|
|
|
319
362
|
|
|
320
363
|
class StructureMap(MaintainableArtefact, frozen=True, omit_defaults=True):
|