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.
- sdmxlib/__init__.py +155 -0
- sdmxlib/api/__init__.py +7 -0
- sdmxlib/api/client.py +246 -0
- sdmxlib/api/filters.py +67 -0
- sdmxlib/api/providers.py +66 -0
- sdmxlib/api/query.py +428 -0
- sdmxlib/api/registry.py +666 -0
- sdmxlib/api/session.py +124 -0
- sdmxlib/formats/__init__.py +166 -0
- sdmxlib/formats/sdmx_csv/__init__.py +5 -0
- sdmxlib/formats/sdmx_csv/reader.py +147 -0
- sdmxlib/formats/sdmx_json/__init__.py +1 -0
- sdmxlib/formats/sdmx_json/reader.py +897 -0
- sdmxlib/formats/sdmx_json/writer.py +698 -0
- sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
- sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
- sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
- sdmxlib/formats/sdmx_ml21/writer.py +709 -0
- sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
- sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
- sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
- sdmxlib/formats/sdmx_ml30/writer.py +730 -0
- sdmxlib/local/__init__.py +6 -0
- sdmxlib/local/_registry.py +368 -0
- sdmxlib/model/__init__.py +122 -0
- sdmxlib/model/annotations.py +103 -0
- sdmxlib/model/base.py +61 -0
- sdmxlib/model/category.py +88 -0
- sdmxlib/model/codelist.py +91 -0
- sdmxlib/model/collections.py +382 -0
- sdmxlib/model/concept.py +121 -0
- sdmxlib/model/constraint.py +92 -0
- sdmxlib/model/convert.py +268 -0
- sdmxlib/model/dataflow.py +49 -0
- sdmxlib/model/dataset.py +337 -0
- sdmxlib/model/datastructure.py +390 -0
- sdmxlib/model/expr.py +120 -0
- sdmxlib/model/hierarchy.py +111 -0
- sdmxlib/model/istring.py +81 -0
- sdmxlib/model/mapping.py +134 -0
- sdmxlib/model/message.py +70 -0
- sdmxlib/model/organisation.py +149 -0
- sdmxlib/model/provision.py +45 -0
- sdmxlib/model/ref.py +195 -0
- sdmxlib/model/registry.py +191 -0
- sdmxlib/model/representation.py +99 -0
- sdmxlib/model/urn.py +130 -0
- sdmxlib/model/validation.py +338 -0
- sdmxlib/polars.py +392 -0
- sdmxlib/py.typed +0 -0
- sdmxlib/storage/__init__.py +28 -0
- sdmxlib/storage/lazy.py +329 -0
- sdmxlib/storage/readers.py +310 -0
- sdmxlib/storage/schema.py +289 -0
- sdmxlib/storage/writers.py +503 -0
- sdmxlib-0.8.0.dist-info/METADATA +102 -0
- sdmxlib-0.8.0.dist-info/RECORD +58 -0
- sdmxlib-0.8.0.dist-info/WHEEL +4 -0
sdmxlib/model/istring.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""InternationalString — dict-like multilingual string."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InternationalString:
|
|
12
|
+
"""A multilingual string where language access is always explicit.
|
|
13
|
+
|
|
14
|
+
Stores key-value pairs of language code → text. Immutable after
|
|
15
|
+
construction (no ``__setitem__``).
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
s = InternationalString(en="Annual", fr="Annuel")
|
|
19
|
+
s["en"] # "Annual"
|
|
20
|
+
s.get("de", "en") # "Annual" (fallback)
|
|
21
|
+
"fr" in s # True
|
|
22
|
+
s.langs # ["en", "fr"]
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__slots__ = ("_data",)
|
|
26
|
+
|
|
27
|
+
def __init__(self, **kwargs: str) -> None:
|
|
28
|
+
self._data: dict[str, str] = dict(kwargs)
|
|
29
|
+
|
|
30
|
+
# ── Construction helpers ───────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def of(cls, pairs: dict[str, str]) -> InternationalString:
|
|
34
|
+
"""Build from an existing dict."""
|
|
35
|
+
obj = cls.__new__(cls)
|
|
36
|
+
obj._data = dict(pairs) # noqa: SLF001
|
|
37
|
+
return obj
|
|
38
|
+
|
|
39
|
+
# ── Access ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
def __getitem__(self, lang: str) -> str:
|
|
42
|
+
"""Return the string for *lang*, raising KeyError if absent."""
|
|
43
|
+
return self._data[lang]
|
|
44
|
+
|
|
45
|
+
def get(self, *langs: str) -> str | None:
|
|
46
|
+
"""Return the first available language in priority order, or None."""
|
|
47
|
+
for lang in langs:
|
|
48
|
+
if lang in self._data:
|
|
49
|
+
return self._data[lang]
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def __contains__(self, lang: object) -> bool:
|
|
53
|
+
return lang in self._data
|
|
54
|
+
|
|
55
|
+
def __iter__(self) -> Iterator[str]:
|
|
56
|
+
return iter(self._data)
|
|
57
|
+
|
|
58
|
+
def __len__(self) -> int:
|
|
59
|
+
return len(self._data)
|
|
60
|
+
|
|
61
|
+
def __bool__(self) -> bool:
|
|
62
|
+
return bool(self._data)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def langs(self) -> list[str]:
|
|
66
|
+
"""List of available language codes."""
|
|
67
|
+
return list(self._data)
|
|
68
|
+
|
|
69
|
+
# ── Equality & repr ───────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
def __eq__(self, other: object) -> bool:
|
|
72
|
+
if isinstance(other, InternationalString):
|
|
73
|
+
return self._data == other._data
|
|
74
|
+
return NotImplemented
|
|
75
|
+
|
|
76
|
+
def __hash__(self) -> int:
|
|
77
|
+
return hash(tuple(sorted(self._data.items())))
|
|
78
|
+
|
|
79
|
+
def __repr__(self) -> str:
|
|
80
|
+
pairs = ", ".join(f"{k}={v!r}" for k, v in self._data.items())
|
|
81
|
+
return f"InternationalString({pairs})"
|
sdmxlib/model/mapping.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""StructureSet, CodelistMap, and CodeMap — SDMX structural mapping types."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from attrs import define, field
|
|
7
|
+
|
|
8
|
+
from sdmxlib.model.annotations import Annotations
|
|
9
|
+
from sdmxlib.model.base import MaintainableArtefact
|
|
10
|
+
from sdmxlib.model.istring import InternationalString
|
|
11
|
+
from sdmxlib.model.organisation import Agency, _to_agency, _to_annotations, _to_istring
|
|
12
|
+
from sdmxlib.model.ref import AnyRef
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@define
|
|
16
|
+
class CodeMap:
|
|
17
|
+
"""Single code-level mapping: source code id → target code id."""
|
|
18
|
+
|
|
19
|
+
source_id: str
|
|
20
|
+
target_id: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@define
|
|
24
|
+
class CodelistMap:
|
|
25
|
+
"""Maps one codelist onto another, code by code."""
|
|
26
|
+
|
|
27
|
+
id: str
|
|
28
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
29
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
30
|
+
source: AnyRef | None = None
|
|
31
|
+
target: AnyRef | None = None
|
|
32
|
+
maps: tuple[CodeMap, ...] = ()
|
|
33
|
+
|
|
34
|
+
def translate(self, source_code: str) -> str | None:
|
|
35
|
+
"""Return the target code id for a source code id, or None if unmapped."""
|
|
36
|
+
for m in self.maps:
|
|
37
|
+
if m.source_id == source_code:
|
|
38
|
+
return m.target_id
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@define
|
|
43
|
+
class ConceptMap:
|
|
44
|
+
"""Single concept-level mapping: source concept id → target concept id."""
|
|
45
|
+
|
|
46
|
+
source_id: str
|
|
47
|
+
target_id: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@define
|
|
51
|
+
class ConceptSchemeMap:
|
|
52
|
+
"""Maps one concept scheme onto another, concept by concept."""
|
|
53
|
+
|
|
54
|
+
id: str
|
|
55
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
56
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
57
|
+
source: AnyRef | None = None
|
|
58
|
+
target: AnyRef | None = None
|
|
59
|
+
maps: tuple[ConceptMap, ...] = ()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@define
|
|
63
|
+
class CategoryMap:
|
|
64
|
+
"""Single category-level mapping: source category id → target category id."""
|
|
65
|
+
|
|
66
|
+
source_id: str
|
|
67
|
+
target_id: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@define
|
|
71
|
+
class CategorySchemeMap:
|
|
72
|
+
"""Maps one category scheme onto another, category by category."""
|
|
73
|
+
|
|
74
|
+
id: str
|
|
75
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
76
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
77
|
+
source: AnyRef | None = None
|
|
78
|
+
target: AnyRef | None = None
|
|
79
|
+
maps: tuple[CategoryMap, ...] = ()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@define
|
|
83
|
+
class ComponentMap:
|
|
84
|
+
"""Single component-level mapping: source component id → target component id."""
|
|
85
|
+
|
|
86
|
+
source_id: str
|
|
87
|
+
target_id: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@define
|
|
91
|
+
class DataStructureMap:
|
|
92
|
+
"""Maps one DSD onto another, component by component."""
|
|
93
|
+
|
|
94
|
+
id: str
|
|
95
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
96
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
97
|
+
source: AnyRef | None = None
|
|
98
|
+
target: AnyRef | None = None
|
|
99
|
+
maps: tuple[ComponentMap, ...] = ()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@define
|
|
103
|
+
class DataflowMap:
|
|
104
|
+
"""Maps one dataflow onto another (artefact-level only, no item maps)."""
|
|
105
|
+
|
|
106
|
+
id: str
|
|
107
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
108
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
109
|
+
source: AnyRef | None = None
|
|
110
|
+
target: AnyRef | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@define
|
|
114
|
+
class StructureSet(MaintainableArtefact):
|
|
115
|
+
"""Maintainable container for structural mappings between artefacts."""
|
|
116
|
+
|
|
117
|
+
sdmx_package: ClassVar[str] = "mapping"
|
|
118
|
+
sdmx_class: ClassVar[str] = "StructureSet"
|
|
119
|
+
msg_field: ClassVar[str] = "structure_sets"
|
|
120
|
+
|
|
121
|
+
id: str = ""
|
|
122
|
+
maintainer: Agency = field(factory=lambda: Agency(id=""), converter=_to_agency)
|
|
123
|
+
version: str = "1.0"
|
|
124
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
125
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
126
|
+
is_final: bool = False
|
|
127
|
+
codelist_maps: tuple[CodelistMap, ...] = ()
|
|
128
|
+
concept_scheme_maps: tuple[ConceptSchemeMap, ...] = ()
|
|
129
|
+
category_scheme_maps: tuple[CategorySchemeMap, ...] = ()
|
|
130
|
+
data_structure_maps: tuple[DataStructureMap, ...] = ()
|
|
131
|
+
dataflow_maps: tuple[DataflowMap, ...] = ()
|
|
132
|
+
annotations: Annotations = field(factory=Annotations, converter=_to_annotations)
|
|
133
|
+
valid_from: datetime | None = None
|
|
134
|
+
valid_to: datetime | None = None
|
sdmxlib/model/message.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""StructureMessage — version-neutral envelope for SDMX structural artefacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from attrs import field, frozen
|
|
8
|
+
|
|
9
|
+
from sdmxlib.model.collections import ArtefactList
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from sdmxlib.model.category import Categorisation, CategoryScheme
|
|
13
|
+
from sdmxlib.model.codelist import Codelist
|
|
14
|
+
from sdmxlib.model.concept import ConceptScheme
|
|
15
|
+
from sdmxlib.model.constraint import ContentConstraint
|
|
16
|
+
from sdmxlib.model.dataflow import Dataflow
|
|
17
|
+
from sdmxlib.model.datastructure import DataStructure
|
|
18
|
+
from sdmxlib.model.hierarchy import Hierarchy
|
|
19
|
+
from sdmxlib.model.mapping import StructureSet
|
|
20
|
+
from sdmxlib.model.organisation import AgencyScheme, DataProviderScheme
|
|
21
|
+
from sdmxlib.model.provision import ProvisionAgreement
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _to_artefact_list(v: Any) -> ArtefactList: # type: ignore[type-arg]
|
|
25
|
+
return v if isinstance(v, ArtefactList) else ArtefactList(v)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@frozen
|
|
29
|
+
class StructureMessage:
|
|
30
|
+
"""Version-neutral envelope returned by the format layer.
|
|
31
|
+
|
|
32
|
+
The format reader populates whichever collections are present in the
|
|
33
|
+
parsed message. Cross-references within the message are auto-resolved
|
|
34
|
+
by the reader.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
msg = parse(xml_bytes)
|
|
38
|
+
dsd = msg.data_structures[0]
|
|
39
|
+
cl = msg.codelists["SDMX:CL_FREQ(1.0)"]
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
codelists: ArtefactList[Codelist] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
43
|
+
concept_schemes: ArtefactList[ConceptScheme] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
44
|
+
data_structures: ArtefactList[DataStructure] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
45
|
+
dataflows: ArtefactList[Dataflow] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
46
|
+
category_schemes: ArtefactList[CategoryScheme] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
47
|
+
categorisations: ArtefactList[Categorisation] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
48
|
+
provision_agreements: ArtefactList[ProvisionAgreement] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
49
|
+
agency_schemes: ArtefactList[AgencyScheme] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
50
|
+
data_provider_schemes: ArtefactList[DataProviderScheme] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
51
|
+
constraints: ArtefactList[ContentConstraint] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
52
|
+
structure_sets: ArtefactList[StructureSet] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
53
|
+
hierarchies: ArtefactList[Hierarchy] = field(factory=ArtefactList, converter=_to_artefact_list)
|
|
54
|
+
|
|
55
|
+
def all_artefacts(self) -> list[Any]:
|
|
56
|
+
"""Flat list of every artefact in the message."""
|
|
57
|
+
return [
|
|
58
|
+
*self.codelists,
|
|
59
|
+
*self.concept_schemes,
|
|
60
|
+
*self.data_structures,
|
|
61
|
+
*self.dataflows,
|
|
62
|
+
*self.category_schemes,
|
|
63
|
+
*self.categorisations,
|
|
64
|
+
*self.provision_agreements,
|
|
65
|
+
*self.agency_schemes,
|
|
66
|
+
*self.data_provider_schemes,
|
|
67
|
+
*self.constraints,
|
|
68
|
+
*self.structure_sets,
|
|
69
|
+
*self.hierarchies,
|
|
70
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Organisation types — Agency, DataProvider, AgencyScheme, DataProviderScheme."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from attrs import define, evolve, field
|
|
7
|
+
|
|
8
|
+
from sdmxlib.model.annotations import Annotation, Annotations
|
|
9
|
+
from sdmxlib.model.base import MaintainableArtefact
|
|
10
|
+
from sdmxlib.model.collections import ItemList
|
|
11
|
+
from sdmxlib.model.istring import InternationalString
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _to_istring(v: InternationalString | dict[str, str]) -> InternationalString:
|
|
15
|
+
return InternationalString.of(v) if isinstance(v, dict) else v
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _to_annotations(v: Annotations | list[Annotation]) -> Annotations:
|
|
19
|
+
return Annotations(v) if isinstance(v, list) else v
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@define
|
|
23
|
+
class Contact:
|
|
24
|
+
"""Contact information for an agency or data provider."""
|
|
25
|
+
|
|
26
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
27
|
+
department: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
28
|
+
role: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
29
|
+
phones: tuple[str, ...] = ()
|
|
30
|
+
faxes: tuple[str, ...] = ()
|
|
31
|
+
uris: tuple[str, ...] = ()
|
|
32
|
+
emails: tuple[str, ...] = ()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@define
|
|
36
|
+
class Agency:
|
|
37
|
+
"""An organisation that maintains SDMX artefacts."""
|
|
38
|
+
|
|
39
|
+
id: str
|
|
40
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
41
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
42
|
+
contacts: tuple[Contact, ...] = ()
|
|
43
|
+
parent: "Agency | None" = None
|
|
44
|
+
annotations: Annotations = field(factory=Annotations, converter=_to_annotations)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _to_agency(v: "Agency | str") -> "Agency":
|
|
48
|
+
return Agency(id=v) if isinstance(v, str) else v
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _to_item_list_agency(v: "ItemList[Agency] | list[Agency]") -> "ItemList[Agency]":
|
|
52
|
+
return v if isinstance(v, ItemList) else ItemList(v)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _to_item_list_provider(v: "ItemList[DataProvider] | list[DataProvider]") -> "ItemList[DataProvider]":
|
|
56
|
+
return v if isinstance(v, ItemList) else ItemList(v)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@define
|
|
60
|
+
class DataProvider:
|
|
61
|
+
"""An organisation that provides data under a ProvisionAgreement."""
|
|
62
|
+
|
|
63
|
+
id: str
|
|
64
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
65
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
66
|
+
contacts: tuple[Contact, ...] = ()
|
|
67
|
+
annotations: Annotations = field(factory=Annotations, converter=_to_annotations)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@define
|
|
71
|
+
class AgencyScheme(MaintainableArtefact):
|
|
72
|
+
"""Maintainable container for Agency items."""
|
|
73
|
+
|
|
74
|
+
sdmx_package: ClassVar[str] = "base"
|
|
75
|
+
sdmx_class: ClassVar[str] = "AgencyScheme"
|
|
76
|
+
msg_field: ClassVar[str] = "agency_schemes"
|
|
77
|
+
|
|
78
|
+
id: str
|
|
79
|
+
maintainer: Agency = field(converter=_to_agency)
|
|
80
|
+
version: str = "1.0"
|
|
81
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
82
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
83
|
+
is_final: bool = False
|
|
84
|
+
agencies: ItemList[Agency] = field(factory=ItemList, converter=_to_item_list_agency)
|
|
85
|
+
annotations: Annotations = field(factory=Annotations, converter=_to_annotations)
|
|
86
|
+
valid_from: datetime | None = None
|
|
87
|
+
valid_to: datetime | None = None
|
|
88
|
+
|
|
89
|
+
def add(self, agencies: "Agency | list[Agency]") -> "AgencyScheme":
|
|
90
|
+
"""Return a new AgencyScheme with *agencies* added.
|
|
91
|
+
|
|
92
|
+
Accepts a single Agency or a list of Agencies.
|
|
93
|
+
"""
|
|
94
|
+
new = [agencies] if isinstance(agencies, Agency) else list(agencies)
|
|
95
|
+
return evolve(self, agencies=[*self.agencies, *new])
|
|
96
|
+
|
|
97
|
+
def drop(self, agency_ids: str | list[str]) -> "AgencyScheme":
|
|
98
|
+
"""Return a new AgencyScheme with the given agency(ies) removed.
|
|
99
|
+
|
|
100
|
+
Accepts a single id string or a list of id strings.
|
|
101
|
+
Raises KeyError if any id is not present.
|
|
102
|
+
"""
|
|
103
|
+
ids = [agency_ids] if isinstance(agency_ids, str) else list(agency_ids)
|
|
104
|
+
missing = [i for i in ids if i not in self.agencies]
|
|
105
|
+
if missing:
|
|
106
|
+
raise KeyError(missing[0] if len(missing) == 1 else missing)
|
|
107
|
+
id_set = set(ids)
|
|
108
|
+
return evolve(self, agencies=[a for a in self.agencies if a.id not in id_set])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@define
|
|
112
|
+
class DataProviderScheme(MaintainableArtefact):
|
|
113
|
+
"""Maintainable container for DataProvider items."""
|
|
114
|
+
|
|
115
|
+
sdmx_package: ClassVar[str] = "base"
|
|
116
|
+
sdmx_class: ClassVar[str] = "DataProviderScheme"
|
|
117
|
+
msg_field: ClassVar[str] = "data_provider_schemes"
|
|
118
|
+
|
|
119
|
+
id: str
|
|
120
|
+
maintainer: Agency = field(converter=_to_agency)
|
|
121
|
+
version: str = "1.0"
|
|
122
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
123
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
124
|
+
is_final: bool = False
|
|
125
|
+
providers: ItemList[DataProvider] = field(factory=ItemList, converter=_to_item_list_provider)
|
|
126
|
+
annotations: Annotations = field(factory=Annotations, converter=_to_annotations)
|
|
127
|
+
valid_from: datetime | None = None
|
|
128
|
+
valid_to: datetime | None = None
|
|
129
|
+
|
|
130
|
+
def add(self, providers: "DataProvider | list[DataProvider]") -> "DataProviderScheme":
|
|
131
|
+
"""Return a new DataProviderScheme with *providers* added.
|
|
132
|
+
|
|
133
|
+
Accepts a single DataProvider or a list of DataProviders.
|
|
134
|
+
"""
|
|
135
|
+
new = [providers] if isinstance(providers, DataProvider) else list(providers)
|
|
136
|
+
return evolve(self, providers=[*self.providers, *new])
|
|
137
|
+
|
|
138
|
+
def drop(self, provider_ids: str | list[str]) -> "DataProviderScheme":
|
|
139
|
+
"""Return a new DataProviderScheme with the given provider(s) removed.
|
|
140
|
+
|
|
141
|
+
Accepts a single id string or a list of id strings.
|
|
142
|
+
Raises KeyError if any id is not present.
|
|
143
|
+
"""
|
|
144
|
+
ids = [provider_ids] if isinstance(provider_ids, str) else list(provider_ids)
|
|
145
|
+
missing = [i for i in ids if i not in self.providers]
|
|
146
|
+
if missing:
|
|
147
|
+
raise KeyError(missing[0] if len(missing) == 1 else missing)
|
|
148
|
+
id_set = set(ids)
|
|
149
|
+
return evolve(self, providers=[p for p in self.providers if p.id not in id_set])
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""ProvisionAgreement — binds a data provider to a dataflow."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from attrs import define, field
|
|
7
|
+
|
|
8
|
+
from sdmxlib.model.annotations import Annotations
|
|
9
|
+
from sdmxlib.model.base import MaintainableArtefact
|
|
10
|
+
from sdmxlib.model.dataflow import Dataflow
|
|
11
|
+
from sdmxlib.model.istring import InternationalString
|
|
12
|
+
from sdmxlib.model.organisation import Agency, _to_agency, _to_annotations, _to_istring
|
|
13
|
+
from sdmxlib.model.ref import AnyRef, Ref
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _to_dataflow_ref(v: "Ref[Dataflow] | Dataflow | None") -> "Ref[Dataflow] | None":
|
|
17
|
+
if isinstance(v, Dataflow):
|
|
18
|
+
return Ref.to(v)
|
|
19
|
+
return v
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@define
|
|
23
|
+
class ProvisionAgreement(MaintainableArtefact):
|
|
24
|
+
"""Binds a data provider to a dataflow.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
pa.dataflow() # Dataflow (if resolved)
|
|
28
|
+
pa.dataflow().structure() # DataStructure
|
|
29
|
+
pa.dataflow().structure.dimensions # ItemList[Dimension]
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
sdmx_package: ClassVar[str] = "registry"
|
|
33
|
+
sdmx_class: ClassVar[str] = "ProvisionAgreement"
|
|
34
|
+
msg_field: ClassVar[str] = "provision_agreements"
|
|
35
|
+
|
|
36
|
+
id: str
|
|
37
|
+
maintainer: Agency = field(converter=_to_agency)
|
|
38
|
+
version: str = "1.0"
|
|
39
|
+
name: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
40
|
+
description: InternationalString = field(factory=InternationalString, converter=_to_istring)
|
|
41
|
+
annotations: Annotations = field(factory=Annotations, converter=_to_annotations)
|
|
42
|
+
valid_from: datetime | None = None
|
|
43
|
+
valid_to: datetime | None = None
|
|
44
|
+
dataflow: Ref[Dataflow] | None = field(default=None, converter=_to_dataflow_ref)
|
|
45
|
+
data_provider: AnyRef | None = None
|
sdmxlib/model/ref.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Ref[T], HasUrn, UnresolvedRef, SdmxRef — SDMX reference abstraction."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, ClassVar, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from attrs import define, field, frozen
|
|
6
|
+
|
|
7
|
+
from sdmxlib.model.urn import SdmxUrn
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class HasUrn(Protocol):
|
|
12
|
+
"""Protocol for SDMX artefacts that can be referenced by URN.
|
|
13
|
+
|
|
14
|
+
Any MaintainableArtefact satisfies this protocol. Accepts anywhere
|
|
15
|
+
a Ref target is needed — allows ``Ref(cl_freq)`` instead of
|
|
16
|
+
``Ref.unresolved(cl_freq.urn)``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
sdmx_package: ClassVar[str]
|
|
20
|
+
sdmx_class: ClassVar[str]
|
|
21
|
+
agency_id: str
|
|
22
|
+
id: str
|
|
23
|
+
version: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@define
|
|
27
|
+
class Ref[T]:
|
|
28
|
+
"""A reference to an SDMX artefact.
|
|
29
|
+
|
|
30
|
+
Always carries the URN. May also carry the resolved object once
|
|
31
|
+
the artefact has been added to a Registry.
|
|
32
|
+
|
|
33
|
+
``None`` on a parent field means "no reference exists."
|
|
34
|
+
``Ref[T]`` means "a reference exists, resolved or not."
|
|
35
|
+
|
|
36
|
+
Access tiers:
|
|
37
|
+
|
|
38
|
+
- ``ref.urn``, ``ref.id``, ``ref.agency`` — URN metadata, always available
|
|
39
|
+
- ``ref.resolved`` — bool check
|
|
40
|
+
- ``ref.codes``, ``ref.name`` etc — proxy to resolved object, raises
|
|
41
|
+
``UnresolvedRef`` if not yet resolved
|
|
42
|
+
|
|
43
|
+
Construction:
|
|
44
|
+
|
|
45
|
+
Ref(cl_freq) # from domain object — resolved
|
|
46
|
+
Ref(cl_freq.urn) # from SdmxUrn — unresolved
|
|
47
|
+
Ref.unresolved(urn) # explicit unresolved factory
|
|
48
|
+
Ref.of(urn, obj) # explicit resolved factory
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
ref = Ref(cl_freq_urn)
|
|
52
|
+
ref.id # "CL_FREQ"
|
|
53
|
+
ref.resolved # False
|
|
54
|
+
ref.codes # raises UnresolvedRef
|
|
55
|
+
|
|
56
|
+
reg.add(cl_freq)
|
|
57
|
+
ref.resolved # True (if interned through registry)
|
|
58
|
+
ref.codes # ItemList[Code]
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
urn: SdmxUrn[T]
|
|
62
|
+
_resolved: T | None = field(default=None, repr=False, alias="_resolved")
|
|
63
|
+
|
|
64
|
+
def __attrs_post_init__(self) -> None:
|
|
65
|
+
# Allow passing a HasUrn (MaintainableArtefact) directly
|
|
66
|
+
if not isinstance(self.urn, SdmxUrn):
|
|
67
|
+
obj = self.urn # type: ignore[assignment]
|
|
68
|
+
self.urn = SdmxUrn.make( # type: ignore[assignment]
|
|
69
|
+
type(obj),
|
|
70
|
+
package=obj.sdmx_package, # type: ignore[union-attr]
|
|
71
|
+
artefact_class=obj.sdmx_class, # type: ignore[union-attr]
|
|
72
|
+
agency=obj.agency_id, # type: ignore[union-attr]
|
|
73
|
+
id=obj.id, # type: ignore[union-attr]
|
|
74
|
+
version=obj.version, # type: ignore[union-attr]
|
|
75
|
+
)
|
|
76
|
+
if self._resolved is None:
|
|
77
|
+
self._resolved = obj # type: ignore[assignment]
|
|
78
|
+
|
|
79
|
+
# ── Resolution ────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def resolved(self) -> bool:
|
|
83
|
+
"""True if the referenced object has been resolved."""
|
|
84
|
+
return self._resolved is not None
|
|
85
|
+
|
|
86
|
+
def __call__(self) -> T:
|
|
87
|
+
"""Unwrap to the resolved object. Raises UnresolvedRef if not resolved."""
|
|
88
|
+
if self._resolved is None:
|
|
89
|
+
raise UnresolvedRef(self.urn)
|
|
90
|
+
return self._resolved
|
|
91
|
+
|
|
92
|
+
def __getattr__(self, name: str) -> Any:
|
|
93
|
+
"""Proxy attribute access to the resolved object.
|
|
94
|
+
|
|
95
|
+
Allows ``ref.codes`` instead of unwrapping manually.
|
|
96
|
+
Raises ``UnresolvedRef`` if the ref has not been resolved.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
resolved = self._resolved
|
|
100
|
+
except AttributeError:
|
|
101
|
+
raise AttributeError(name) from None
|
|
102
|
+
if resolved is None:
|
|
103
|
+
raise UnresolvedRef(self.urn)
|
|
104
|
+
return getattr(resolved, name)
|
|
105
|
+
|
|
106
|
+
# ── URN metadata ──────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def id(self) -> str:
|
|
110
|
+
"""Artefact id from the URN."""
|
|
111
|
+
return self.urn.id
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def agency(self) -> str:
|
|
115
|
+
"""Agency id from the URN."""
|
|
116
|
+
return self.urn.agency
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def version(self) -> str:
|
|
120
|
+
"""Version from the URN."""
|
|
121
|
+
return self.urn.version
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def item_id(self) -> str | None:
|
|
125
|
+
"""Item id from the URN (e.g. the concept id within a ConceptScheme)."""
|
|
126
|
+
return self.urn.item_id
|
|
127
|
+
|
|
128
|
+
# ── Factories ─────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def unresolved(cls, urn: SdmxUrn[T]) -> "Ref[T]":
|
|
132
|
+
"""Create an unresolved Ref carrying the URN only."""
|
|
133
|
+
return cls(urn=urn)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def of(cls, urn: SdmxUrn[T], obj: T) -> "Ref[T]":
|
|
137
|
+
"""Create a resolved Ref. Useful in tests and format readers."""
|
|
138
|
+
return cls(urn=urn, _resolved=obj)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def to(cls, obj: T) -> "Ref[T]":
|
|
142
|
+
"""Create a resolved Ref from a MaintainableArtefact instance.
|
|
143
|
+
|
|
144
|
+
The object must satisfy HasUrn — it needs ``agency_id``, ``id``,
|
|
145
|
+
``version``, ``sdmx_package``, and ``sdmx_class`` attributes.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
cl = Codelist(id="CL_FREQ", maintainer="SDMX", version="1.0", ...)
|
|
149
|
+
ref = Ref.to(cl)
|
|
150
|
+
ref.resolved # True
|
|
151
|
+
"""
|
|
152
|
+
return cls(urn=obj) # type: ignore[arg-type] # delegates to __post_init__
|
|
153
|
+
|
|
154
|
+
# ── Boolean ───────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
def __bool__(self) -> bool:
|
|
157
|
+
"""Always True — the Ref exists (use .resolved to check resolution)."""
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
# ── Repr ──────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
def __repr__(self) -> str:
|
|
163
|
+
if self._resolved is not None:
|
|
164
|
+
return f"Ref({self.urn.id} → resolved)"
|
|
165
|
+
return f"Ref({self.urn.id} [unresolved])"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class UnresolvedRef(Exception): # noqa: N818
|
|
169
|
+
"""Raised when accessing an unresolved Ref."""
|
|
170
|
+
|
|
171
|
+
def __init__(self, urn: SdmxUrn[Any]) -> None:
|
|
172
|
+
self.urn = urn
|
|
173
|
+
super().__init__(f"{urn} has not been resolved — add the artefact to a Registry first.")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@frozen
|
|
177
|
+
class SdmxRef:
|
|
178
|
+
"""Partial artefact identity for filtering and lookup.
|
|
179
|
+
|
|
180
|
+
All fields optional. ``None`` means "match any."
|
|
181
|
+
Used as ``ArtefactList`` key and in ``Query``.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
SdmxRef(agency="SDMX", id="CL_FREQ")
|
|
185
|
+
SdmxRef(id="CL_FREQ", version="1.0")
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
agency: str | None = None
|
|
189
|
+
id: str | None = None
|
|
190
|
+
version: str | None = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# Type alias for Ref[Any] — used in format layers and internal code where the
|
|
194
|
+
# specific artefact type is not yet known or not relevant.
|
|
195
|
+
AnyRef = Ref[Any]
|