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.
Files changed (100) hide show
  1. pysdmx/__extras_check.py +3 -2
  2. pysdmx/__init__.py +1 -1
  3. pysdmx/api/fmr/__init__.py +4 -4
  4. pysdmx/api/gds/__init__.py +328 -0
  5. pysdmx/api/qb/gds.py +153 -0
  6. pysdmx/api/qb/service.py +91 -3
  7. pysdmx/api/qb/structure.py +1 -0
  8. pysdmx/api/qb/util.py +1 -0
  9. pysdmx/io/__init__.py +2 -1
  10. pysdmx/io/csv/sdmx10/reader/__init__.py +4 -2
  11. pysdmx/io/csv/sdmx10/writer/__init__.py +15 -2
  12. pysdmx/io/csv/sdmx20/reader/__init__.py +5 -2
  13. pysdmx/io/csv/sdmx20/writer/__init__.py +13 -2
  14. pysdmx/io/format.py +4 -0
  15. pysdmx/io/input_processor.py +12 -3
  16. pysdmx/io/json/fusion/messages/core.py +2 -0
  17. pysdmx/io/json/fusion/messages/report.py +13 -7
  18. pysdmx/io/json/gds/messages/__init__.py +35 -0
  19. pysdmx/io/json/gds/messages/agencies.py +41 -0
  20. pysdmx/io/json/gds/messages/catalog.py +79 -0
  21. pysdmx/io/json/gds/messages/sdmx_api.py +23 -0
  22. pysdmx/io/json/gds/messages/services.py +49 -0
  23. pysdmx/io/json/gds/messages/urn_resolver.py +43 -0
  24. pysdmx/io/json/gds/reader/__init__.py +12 -0
  25. pysdmx/io/json/sdmxjson2/messages/__init__.py +12 -4
  26. pysdmx/io/json/sdmxjson2/messages/agency.py +72 -0
  27. pysdmx/io/json/sdmxjson2/messages/category.py +22 -29
  28. pysdmx/io/json/sdmxjson2/messages/code.py +68 -64
  29. pysdmx/io/json/sdmxjson2/messages/concept.py +9 -18
  30. pysdmx/io/json/sdmxjson2/messages/constraint.py +2 -13
  31. pysdmx/io/json/sdmxjson2/messages/core.py +113 -21
  32. pysdmx/io/json/sdmxjson2/messages/dataflow.py +51 -21
  33. pysdmx/io/json/sdmxjson2/messages/dsd.py +110 -36
  34. pysdmx/io/json/sdmxjson2/messages/map.py +61 -49
  35. pysdmx/io/json/sdmxjson2/messages/pa.py +9 -17
  36. pysdmx/io/json/sdmxjson2/messages/provider.py +88 -0
  37. pysdmx/io/json/sdmxjson2/messages/report.py +84 -14
  38. pysdmx/io/json/sdmxjson2/messages/schema.py +14 -5
  39. pysdmx/io/json/sdmxjson2/messages/structure.py +105 -36
  40. pysdmx/io/json/sdmxjson2/messages/vtl.py +42 -96
  41. pysdmx/io/pd.py +2 -9
  42. pysdmx/io/reader.py +72 -27
  43. pysdmx/io/serde.py +11 -0
  44. pysdmx/io/writer.py +134 -0
  45. pysdmx/io/xml/{sdmx21/reader/__data_aux.py → __data_aux.py} +9 -2
  46. pysdmx/io/xml/{sdmx21/reader/__parse_xml.py → __parse_xml.py} +30 -6
  47. pysdmx/io/xml/__ss_aux_reader.py +96 -0
  48. pysdmx/io/xml/__structure_aux_reader.py +1174 -0
  49. pysdmx/io/xml/__structure_aux_writer.py +1233 -0
  50. pysdmx/io/xml/{sdmx21/__tokens.py → __tokens.py} +33 -1
  51. pysdmx/io/xml/{sdmx21/writer/__write_aux.py → __write_aux.py} +129 -37
  52. pysdmx/io/xml/{sdmx21/writer/__write_data_aux.py → __write_data_aux.py} +1 -1
  53. pysdmx/io/xml/__write_structure_specific_aux.py +254 -0
  54. pysdmx/io/xml/{sdmx21/reader/doc_validation.py → doc_validation.py} +10 -2
  55. pysdmx/io/xml/{sdmx21/reader/header.py → header.py} +11 -3
  56. pysdmx/io/xml/sdmx21/reader/error.py +2 -2
  57. pysdmx/io/xml/sdmx21/reader/generic.py +12 -8
  58. pysdmx/io/xml/sdmx21/reader/structure.py +5 -840
  59. pysdmx/io/xml/sdmx21/reader/structure_specific.py +13 -97
  60. pysdmx/io/xml/sdmx21/reader/submission.py +2 -2
  61. pysdmx/io/xml/sdmx21/writer/error.py +1 -1
  62. pysdmx/io/xml/sdmx21/writer/generic.py +13 -7
  63. pysdmx/io/xml/sdmx21/writer/structure.py +16 -828
  64. pysdmx/io/xml/sdmx21/writer/structure_specific.py +13 -238
  65. pysdmx/io/xml/sdmx30/__init__.py +1 -0
  66. pysdmx/io/xml/sdmx30/reader/__init__.py +1 -0
  67. pysdmx/io/xml/sdmx30/reader/structure.py +39 -0
  68. pysdmx/io/xml/sdmx30/reader/structure_specific.py +39 -0
  69. pysdmx/io/xml/sdmx30/writer/__init__.py +1 -0
  70. pysdmx/io/xml/sdmx30/writer/structure.py +67 -0
  71. pysdmx/io/xml/sdmx30/writer/structure_specific.py +108 -0
  72. pysdmx/model/__base.py +99 -34
  73. pysdmx/model/__init__.py +4 -0
  74. pysdmx/model/category.py +20 -0
  75. pysdmx/model/code.py +29 -8
  76. pysdmx/model/concept.py +52 -11
  77. pysdmx/model/dataflow.py +117 -33
  78. pysdmx/model/dataset.py +66 -14
  79. pysdmx/model/gds.py +161 -0
  80. pysdmx/model/map.py +51 -8
  81. pysdmx/model/message.py +235 -55
  82. pysdmx/model/metadata.py +79 -16
  83. pysdmx/model/submission.py +12 -7
  84. pysdmx/model/vtl.py +30 -13
  85. pysdmx/toolkit/__init__.py +1 -1
  86. pysdmx/toolkit/pd/__init__.py +85 -0
  87. pysdmx/toolkit/vtl/__init__.py +2 -1
  88. pysdmx/toolkit/vtl/_validations.py +1 -1
  89. pysdmx/toolkit/vtl/{generate_vtl_script.py → script_generation.py} +30 -4
  90. pysdmx/toolkit/vtl/validation.py +119 -0
  91. pysdmx/util/_model_utils.py +1 -1
  92. pysdmx-1.4.0rc1.dist-info/METADATA +119 -0
  93. pysdmx-1.4.0rc1.dist-info/RECORD +140 -0
  94. pysdmx/io/json/sdmxjson2/messages/org.py +0 -140
  95. pysdmx/toolkit/vtl/model_validations.py +0 -50
  96. pysdmx-1.3.0.dist-info/METADATA +0 -76
  97. pysdmx-1.3.0.dist-info/RECORD +0 -116
  98. /pysdmx/io/xml/{sdmx21/writer/config.py → config.py} +0 -0
  99. {pysdmx-1.3.0.dist-info → pysdmx-1.4.0rc1.dist-info}/LICENSE +0 -0
  100. {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
- class Component(Struct, frozen=True, omit_defaults=True):
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 attachement level (if role = A only).
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
- """Returns a human-friendly description."""
190
- out = []
191
- for k in self.__annotations__:
192
- v = self.__getattribute__(k)
193
- if v:
194
- if k == "concept":
195
- out.append(f"{k}=({str(v)})")
196
- else:
197
- out.append(f"{k}={str(v)}")
198
- return ", ".join(out)
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
- class DataflowInfo(Struct, frozen=True, omit_defaults=True):
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
- """Returns a human-friendly description."""
359
- out = []
360
- for k in self.__annotations__:
361
- v = self.__getattribute__(k)
362
- if v:
363
- out.append(f"{k}={v}")
364
- return ", ".join(out)
365
-
366
-
367
- class Schema(Struct, frozen=True, omit_defaults=True):
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
- """Returns a human-friendly description."""
407
- out = []
408
- for k in self.__annotations__:
409
- v = self.__getattribute__(k)
410
- if v:
411
- out.append(f"{k}={v}")
412
- return ", ".join(out)
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
- """ActionType enumeration.
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
- class SeriesInfo(Struct, frozen=True):
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
- """Returns a human-friendly description."""
48
- out = []
49
- for k in self.__annotations__:
50
- v = self.__getattribute__(k)
51
- if v:
52
- out.append(f"{k}={v}")
53
- return ", ".join(out)
54
-
55
-
56
- class Dataset(Struct, frozen=False, kw_only=True):
57
- """Core Dataset class."""
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 DatePatternMap(Struct, frozen=True, omit_defaults=True, tag=True):
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(Struct, frozen=True, omit_defaults=True, tag=True):
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(Struct, frozen=True, omit_defaults=True, tag=True):
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(Struct, frozen=True, omit_defaults=True, kw_only=True):
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(Struct, frozen=True, omit_defaults=True, kw_only=True):
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(Struct, frozen=True, omit_defaults=True, tag=True):
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(Struct, frozen=True, omit_defaults=True, tag=True):
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):