sdmxlib 0.8.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. sdmxlib/__init__.py +155 -0
  2. sdmxlib/api/__init__.py +7 -0
  3. sdmxlib/api/client.py +246 -0
  4. sdmxlib/api/filters.py +67 -0
  5. sdmxlib/api/providers.py +66 -0
  6. sdmxlib/api/query.py +428 -0
  7. sdmxlib/api/registry.py +666 -0
  8. sdmxlib/api/session.py +124 -0
  9. sdmxlib/formats/__init__.py +166 -0
  10. sdmxlib/formats/sdmx_csv/__init__.py +5 -0
  11. sdmxlib/formats/sdmx_csv/reader.py +147 -0
  12. sdmxlib/formats/sdmx_json/__init__.py +1 -0
  13. sdmxlib/formats/sdmx_json/reader.py +897 -0
  14. sdmxlib/formats/sdmx_json/writer.py +698 -0
  15. sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
  16. sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
  17. sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
  18. sdmxlib/formats/sdmx_ml21/writer.py +709 -0
  19. sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
  20. sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
  21. sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
  22. sdmxlib/formats/sdmx_ml30/writer.py +730 -0
  23. sdmxlib/local/__init__.py +6 -0
  24. sdmxlib/local/_registry.py +368 -0
  25. sdmxlib/model/__init__.py +122 -0
  26. sdmxlib/model/annotations.py +103 -0
  27. sdmxlib/model/base.py +61 -0
  28. sdmxlib/model/category.py +88 -0
  29. sdmxlib/model/codelist.py +91 -0
  30. sdmxlib/model/collections.py +382 -0
  31. sdmxlib/model/concept.py +121 -0
  32. sdmxlib/model/constraint.py +92 -0
  33. sdmxlib/model/convert.py +268 -0
  34. sdmxlib/model/dataflow.py +49 -0
  35. sdmxlib/model/dataset.py +337 -0
  36. sdmxlib/model/datastructure.py +390 -0
  37. sdmxlib/model/expr.py +120 -0
  38. sdmxlib/model/hierarchy.py +111 -0
  39. sdmxlib/model/istring.py +81 -0
  40. sdmxlib/model/mapping.py +134 -0
  41. sdmxlib/model/message.py +70 -0
  42. sdmxlib/model/organisation.py +149 -0
  43. sdmxlib/model/provision.py +45 -0
  44. sdmxlib/model/ref.py +195 -0
  45. sdmxlib/model/registry.py +191 -0
  46. sdmxlib/model/representation.py +99 -0
  47. sdmxlib/model/urn.py +130 -0
  48. sdmxlib/model/validation.py +338 -0
  49. sdmxlib/polars.py +392 -0
  50. sdmxlib/py.typed +0 -0
  51. sdmxlib/storage/__init__.py +28 -0
  52. sdmxlib/storage/lazy.py +329 -0
  53. sdmxlib/storage/readers.py +310 -0
  54. sdmxlib/storage/schema.py +289 -0
  55. sdmxlib/storage/writers.py +503 -0
  56. sdmxlib-0.8.0.dist-info/METADATA +102 -0
  57. sdmxlib-0.8.0.dist-info/RECORD +58 -0
  58. sdmxlib-0.8.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,191 @@
1
+ """Registry — pure-domain SDMX artefact store.
2
+
3
+ The ``Registry`` is the SDMX Information Model concept of a local artefact
4
+ graph: a URN-keyed store that connects artefacts through interned ``Ref``
5
+ objects.
6
+
7
+ Key behaviours:
8
+
9
+ - **One Ref per URN** — interning means every holder of a given Ref shares
10
+ the same object. Resolving it once propagates everywhere automatically.
11
+ - **Eager accept, lazy resolve** — add artefacts in any order; Refs get
12
+ resolved as their targets arrive.
13
+ - **Pure domain** — no network, no HTTP, no serialisation.
14
+
15
+ Example::
16
+
17
+ reg = Registry()
18
+ reg.add(cl_freq)
19
+ reg.add(dsd) # Refs to cl_freq resolve here
20
+ dsd.dimensions["FREQ"].codelist() # Codelist — no fetch needed
21
+ """
22
+
23
+ from collections.abc import Iterable
24
+ from typing import Any
25
+
26
+ import attrs
27
+
28
+ from sdmxlib.model.collections import ItemList
29
+ from sdmxlib.model.ref import AnyRef, HasUrn, Ref
30
+ from sdmxlib.model.urn import SdmxUrn
31
+
32
+
33
+ class Registry:
34
+ """In-memory SDMX artefact store with URN identity and Ref interning.
35
+
36
+ Example::
37
+
38
+ reg = Registry()
39
+ reg.add_all([cl_freq, concept_scheme, dsd])
40
+
41
+ dsd = reg.get(DataStructure.urn(agency="ESTAT", id="CPI", version="1.0"))
42
+ dims = reg.get_all(Dimension) # not typically used; prefer dsd.dimensions
43
+ """
44
+
45
+ def __init__(self) -> None:
46
+ self._store: dict[str, Any] = {} # str(urn) → artefact
47
+ self._refs: dict[str, AnyRef] = {} # str(urn) → canonical Ref
48
+ self._by_type: dict[type, list[Any]] = {} # Python type → [artefact, ...]
49
+
50
+ # ── Public API ────────────────────────────────────────────────────────
51
+
52
+ def add(self, artefact: Any) -> None:
53
+ """Add an artefact to the registry.
54
+
55
+ - Interns the artefact under its URN.
56
+ - Resolves the canonical ``Ref`` for this URN so all existing
57
+ holders see the artefact immediately.
58
+ - Walks all outgoing ``Ref`` fields, replaces them with canonical
59
+ interned Refs, and resolves any that point to artefacts already
60
+ present.
61
+
62
+ Raises ``TypeError`` if the artefact does not satisfy ``HasUrn``.
63
+ """
64
+ if not isinstance(artefact, HasUrn):
65
+ msg = (
66
+ f"{type(artefact).__name__} does not satisfy HasUrn — "
67
+ "it needs sdmx_package, sdmx_class, agency_id, id, version"
68
+ )
69
+ raise TypeError(msg)
70
+
71
+ urn = _artefact_urn(artefact)
72
+ urn_str = str(urn)
73
+
74
+ # Store under exact URN
75
+ self._store[urn_str] = artefact
76
+
77
+ # Track by Python type for get_all()
78
+ t = type(artefact)
79
+ if t not in self._by_type:
80
+ self._by_type[t] = []
81
+ if artefact not in self._by_type[t]:
82
+ self._by_type[t].append(artefact)
83
+
84
+ # Resolve the canonical Ref for this artefact (propagates to all holders)
85
+ canonical = self._canonical_ref(urn)
86
+ canonical._resolved = artefact # noqa: SLF001
87
+
88
+ # Walk outgoing Refs on the artefact and intern them
89
+ self._walk_refs(artefact, set())
90
+
91
+ def add_all(self, artefacts: Iterable[Any]) -> None:
92
+ """Add multiple artefacts in any order."""
93
+ for artefact in artefacts:
94
+ self.add(artefact)
95
+
96
+ def get[T](self, urn: "SdmxUrn[T]") -> "T | None":
97
+ """Look up an artefact by exact URN. Returns ``None`` if absent."""
98
+ return self._store.get(str(urn)) # type: ignore[return-value]
99
+
100
+ def get_all[T](self, artefact_type: "type[T]") -> "list[T]":
101
+ """All artefacts of the given Python type (exact match, not subclasses)."""
102
+ return list(self._by_type.get(artefact_type, [])) # type: ignore[return-value]
103
+
104
+ def ref[T](self, urn: "SdmxUrn[T]") -> "Ref[T]":
105
+ """Return the canonical interned Ref for this URN.
106
+
107
+ Creates an unresolved Ref if this URN has not been seen before.
108
+ If the target artefact is already in the registry, the returned
109
+ Ref will be resolved.
110
+ """
111
+ return self._canonical_ref(urn) # type: ignore[return-value]
112
+
113
+ def __contains__(self, urn: object) -> bool:
114
+ """True if an artefact with this URN is stored."""
115
+ if isinstance(urn, SdmxUrn):
116
+ return str(urn) in self._store
117
+ return False
118
+
119
+ def __len__(self) -> int:
120
+ """Number of artefacts stored."""
121
+ return len(self._store)
122
+
123
+ def __repr__(self) -> str:
124
+ counts = ", ".join(f"{t.__name__}={len(v)}" for t, v in self._by_type.items())
125
+ return f"Registry({counts or 'empty'})"
126
+
127
+ # ── Internals ─────────────────────────────────────────────────────────
128
+
129
+ def _canonical_ref(self, urn: "SdmxUrn[Any]") -> AnyRef:
130
+ """Get or create the canonical (interned) Ref for a URN."""
131
+ urn_str = str(urn)
132
+ if urn_str not in self._refs:
133
+ new_ref: AnyRef = Ref.unresolved(urn)
134
+ self._refs[urn_str] = new_ref
135
+ # Resolve immediately if the target is already stored
136
+ artefact = self._store.get(urn_str)
137
+ if artefact is not None:
138
+ new_ref._resolved = artefact # noqa: SLF001
139
+ return self._refs[urn_str]
140
+
141
+ def _walk_refs(self, obj: Any, visited: set[int]) -> None:
142
+ """Walk dataclass fields of *obj*, interning every ``Ref`` found.
143
+
144
+ Recurses into ``ItemList`` items and nested dataclass fields.
145
+ The *visited* set prevents cycles.
146
+ """
147
+ if not attrs.has(type(obj)) or isinstance(obj, type):
148
+ return
149
+ oid = id(obj)
150
+ if oid in visited:
151
+ return
152
+ visited.add(oid)
153
+
154
+ for f in attrs.fields(type(obj)):
155
+ self._process_field(obj, f.name, getattr(obj, f.name, None), visited)
156
+
157
+ def _process_field(self, obj: Any, name: str, value: Any, visited: set[int]) -> None:
158
+ """Intern a single field value; recurse if it contains more Refs."""
159
+ if value is None:
160
+ return
161
+ if isinstance(value, Ref):
162
+ self._intern_ref_field(obj, name, value)
163
+ elif isinstance(value, ItemList):
164
+ for item in value:
165
+ self._walk_refs(item, visited)
166
+ elif attrs.has(type(value)) and not isinstance(value, type):
167
+ self._walk_refs(value, visited)
168
+
169
+ def _intern_ref_field(self, obj: Any, name: str, ref: AnyRef) -> None:
170
+ """Replace *ref* on *obj.name* with the canonical interned Ref."""
171
+ interned = self._canonical_ref(ref.urn)
172
+ # Transfer resolution from incoming ref to canonical if needed
173
+ if ref._resolved is not None and interned._resolved is None: # noqa: SLF001
174
+ interned._resolved = ref._resolved # noqa: SLF001
175
+ if interned is not ref:
176
+ setattr(obj, name, interned)
177
+
178
+
179
+ # ── Module-level helper ────────────────────────────────────────────────────
180
+
181
+
182
+ def _artefact_urn(artefact: Any) -> "SdmxUrn[Any]":
183
+ """Build the canonical SdmxUrn for a HasUrn artefact."""
184
+ return SdmxUrn.make(
185
+ type(artefact),
186
+ package=artefact.sdmx_package,
187
+ artefact_class=artefact.sdmx_class,
188
+ agency=artefact.agency_id,
189
+ id=artefact.id,
190
+ version=artefact.version,
191
+ )
@@ -0,0 +1,99 @@
1
+ """Representation types — DataType, Facet, Representation alias."""
2
+
3
+ from enum import StrEnum
4
+ from typing import TYPE_CHECKING
5
+
6
+ from attrs import frozen
7
+
8
+ if TYPE_CHECKING:
9
+ from sdmxlib.model.codelist import Codelist
10
+ from sdmxlib.model.ref import Ref
11
+
12
+
13
+ class DataType(StrEnum):
14
+ """SDMX data types for non-enumerated representations.
15
+
16
+ Values match the SDMX-ML ``textType`` attribute strings exactly, so
17
+ ``DataType.String == "String"`` and existing string-passing code continues
18
+ to work unchanged.
19
+
20
+ Example:
21
+ Facet(type=DataType.String, max_length=100)
22
+ Facet(type=DataType.ObservationalTimePeriod)
23
+ Facet(type="String") # str also accepted
24
+ """
25
+
26
+ String = "String"
27
+ Alpha = "Alpha"
28
+ AlphaNumeric = "AlphaNumeric"
29
+ Numeric = "Numeric"
30
+ BigInteger = "BigInteger"
31
+ Integer = "Integer"
32
+ Long = "Long"
33
+ Short = "Short"
34
+ Decimal = "Decimal"
35
+ Float = "Float"
36
+ Double = "Double"
37
+ Boolean = "Boolean"
38
+ URI = "URI"
39
+ URL = "URL"
40
+ Count = "Count"
41
+ InclusiveValueRange = "InclusiveValueRange"
42
+ ExclusiveValueRange = "ExclusiveValueRange"
43
+ Incremental = "Incremental"
44
+ ObservationalTimePeriod = "ObservationalTimePeriod"
45
+ StandardTimePeriod = "StandardTimePeriod"
46
+ BasicTimePeriod = "BasicTimePeriod"
47
+ GregorianTimePeriod = "GregorianTimePeriod"
48
+ GregorianYear = "GregorianYear"
49
+ GregorianYearMonth = "GregorianYearMonth"
50
+ GregorianDay = "GregorianDay"
51
+ ReportingTimePeriod = "ReportingTimePeriod"
52
+ ReportingYear = "ReportingYear"
53
+ ReportingSemester = "ReportingSemester"
54
+ ReportingTrimester = "ReportingTrimester"
55
+ ReportingQuarter = "ReportingQuarter"
56
+ ReportingMonth = "ReportingMonth"
57
+ ReportingWeek = "ReportingWeek"
58
+ ReportingDay = "ReportingDay"
59
+ ReportingHour = "ReportingHour"
60
+ DateTime = "DateTime"
61
+ Time = "Time"
62
+ Duration = "Duration"
63
+ Month = "Month"
64
+ MonthDay = "MonthDay"
65
+ Day = "Day"
66
+ Year = "Year"
67
+ XHTML = "XHTML"
68
+ Attachment = "Attachment"
69
+
70
+
71
+ @frozen
72
+ class Facet:
73
+ """Non-enumerated representation with optional constraints.
74
+
75
+ ``type`` accepts a :class:`DataType` member or a raw string (for
76
+ non-standard values from registry responses).
77
+
78
+ Example:
79
+ Facet(type=DataType.String, max_length=100)
80
+ Facet(type=DataType.ObservationalTimePeriod)
81
+ Facet(type="String") # str accepted; DataType.String preferred
82
+ """
83
+
84
+ type: DataType | str | None = None
85
+ max_length: int | None = None
86
+ min_length: int | None = None
87
+ max_value: float | None = None
88
+ min_value: float | None = None
89
+ decimals: int | None = None
90
+ pattern: str | None = None
91
+ start_value: float | None = None
92
+ end_value: float | None = None
93
+ is_sequence: bool | None = None
94
+ is_multi_lingual: bool | None = None
95
+
96
+
97
+ # Type alias — a component's representation is a codelist ref (enumerated)
98
+ # or a facet (free text with optional constraints), or absent.
99
+ type Representation = "Ref[Codelist] | Facet | None"
sdmxlib/model/urn.py ADDED
@@ -0,0 +1,130 @@
1
+ """SdmxUrn[T] — typed artefact identity.
2
+
3
+ T is a phantom type parameter — no field stores it. It exists so the type
4
+ checker can carry the target type through ``reg.fetch(urn) -> T``.
5
+ """
6
+
7
+ import re
8
+ from typing import Any, ClassVar
9
+
10
+ from attrs import frozen
11
+
12
+ # urn:sdmx:org.sdmx.infomodel.<package>.<Class>=<AGENCY>:<ID>(<version>)
13
+ # optional item suffix: .<item_id>
14
+ _URN_RE = re.compile(
15
+ r"urn:sdmx:org\.sdmx\.infomodel\.(?P<package>[^.]+)\.(?P<artefact_class>[^=]+)"
16
+ r"=(?P<agency>[^:]+):(?P<id>[^.(]+)(?:\((?P<version>[^)]+)\))?(?:\.(?P<item_id>.+))?$"
17
+ )
18
+
19
+ # SDMX 2.1 / 3.0 formal type patterns from SDMXCommonReferences.xsd
20
+ # IDType: [A-Za-z0-9_@$-]+
21
+ # NestedIDType: [A-Za-z0-9_@$-]+(\.[A-Za-z0-9_@$-]+)* (dots for agency hierarchy)
22
+ _PACKAGE_RE = re.compile(r"^[a-z][a-z_]*$")
23
+ _CLASS_RE = re.compile(r"^[A-Z][A-Za-z]+$")
24
+ _AGENCY_RE = re.compile(r"^[A-Za-z0-9_@$-]+(\.[A-Za-z0-9_@$-]+)*$")
25
+ _ID_RE = re.compile(r"^[A-Za-z0-9_@$-]+$")
26
+
27
+
28
+ @frozen
29
+ class SdmxUrn[T]:
30
+ """Globally unique identifier for an SDMX artefact.
31
+
32
+ ``T`` is a phantom type — not stored at runtime, used by the type
33
+ checker to propagate the target type through
34
+ ``reg.fetch(urn: SdmxUrn[T]) -> T``.
35
+
36
+ Example:
37
+ SdmxUrn.parse("urn:sdmx:org.sdmx.infomodel.codelist.Codelist=SDMX:CL_FREQ(1.0)")
38
+ SdmxUrn.make(
39
+ Codelist,
40
+ package="codelist",
41
+ artefact_class="Codelist",
42
+ agency="SDMX",
43
+ id="CL_FREQ",
44
+ version="1.0",
45
+ )
46
+ """
47
+
48
+ _PACKAGE_PREFIX: ClassVar[str] = "urn:sdmx:org.sdmx.infomodel"
49
+
50
+ package: str
51
+ artefact_class: str
52
+ agency: str
53
+ id: str
54
+ version: str = "latest"
55
+ item_id: str | None = None
56
+
57
+ # ── Factories ─────────────────────────────────────────────────────────
58
+
59
+ @classmethod
60
+ def parse(cls, urn: str) -> "SdmxUrn[Any]":
61
+ """Parse a full URN string. Returns unparameterised SdmxUrn."""
62
+ m = _URN_RE.match(urn)
63
+ if not m:
64
+ msg = f"Invalid SDMX URN: {urn!r}"
65
+ raise ValueError(msg)
66
+ return cls(
67
+ package=m.group("package"),
68
+ artefact_class=m.group("artefact_class"),
69
+ agency=m.group("agency"),
70
+ id=m.group("id"),
71
+ version=m.group("version") or "latest",
72
+ item_id=m.group("item_id"),
73
+ )
74
+
75
+ @classmethod
76
+ def make[U](
77
+ cls,
78
+ artefact_type: type[U], # noqa: ARG003 # phantom type witness; unused at runtime
79
+ /,
80
+ *,
81
+ package: str,
82
+ artefact_class: str,
83
+ agency: str,
84
+ id: str, # noqa: A002
85
+ version: str = "latest",
86
+ item_id: str | None = None,
87
+ ) -> "SdmxUrn[U]":
88
+ """Low-level factory with basic validation.
89
+
90
+ ``artefact_type`` is a phantom type witness — unused at runtime,
91
+ used by the type checker to bind ``U`` and propagate the artefact
92
+ type through ``session.get`` and ``session.extract``.
93
+ """
94
+ if not _AGENCY_RE.match(agency):
95
+ msg = f"Invalid agency id: {agency!r}"
96
+ raise ValueError(msg)
97
+ if not _ID_RE.match(id):
98
+ msg = f"Invalid artefact id: {id!r}"
99
+ raise ValueError(msg)
100
+ return cls( # type: ignore[return-value]
101
+ package=package,
102
+ artefact_class=artefact_class,
103
+ agency=agency,
104
+ id=id,
105
+ version=version,
106
+ item_id=item_id,
107
+ )
108
+
109
+ # ── String representation ──────────────────────────────────────────────
110
+
111
+ def __str__(self) -> str:
112
+ """Round-trip to full URN string."""
113
+ base = f"{self._PACKAGE_PREFIX}.{self.package}.{self.artefact_class}={self.agency}:{self.id}"
114
+ if self.version and self.version != "latest":
115
+ base = f"{base}({self.version})"
116
+ if self.item_id:
117
+ base = f"{base}.{self.item_id}"
118
+ return base
119
+
120
+ # ── Properties ────────────────────────────────────────────────────────
121
+
122
+ @property
123
+ def is_latest(self) -> bool:
124
+ """True if version is the sentinel "latest"."""
125
+ return self.version == "latest"
126
+
127
+ @property
128
+ def is_item_urn(self) -> bool:
129
+ """True if this URN points to an item within a scheme (e.g. a Code)."""
130
+ return self.item_id is not None