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/api/session.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Session — identity map and artefact construction."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from sdmxlib.model.message import StructureMessage
|
|
6
|
+
from sdmxlib.model.urn import SdmxUrn
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _version_key(version: str) -> tuple[int, ...]:
|
|
10
|
+
"""Parse a version string into a tuple of ints for correct ordering.
|
|
11
|
+
|
|
12
|
+
Falls back to ``(0,)`` if the string is not a valid dotted-integer version.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
return tuple(int(x) for x in version.split("."))
|
|
16
|
+
except (ValueError, AttributeError):
|
|
17
|
+
return (0,)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Session:
|
|
21
|
+
"""Session-scoped identity map for SDMX artefacts.
|
|
22
|
+
|
|
23
|
+
Interns artefacts by URN so the same object is returned for the
|
|
24
|
+
same URN within a session. Also merges successive StructureMessages
|
|
25
|
+
into the identity map.
|
|
26
|
+
|
|
27
|
+
Four separate indices:
|
|
28
|
+
- ``_map``: versioned URN string → artefact (all ingested)
|
|
29
|
+
- ``_latest``: ``cls:agency:id`` → artefact (latest version, all)
|
|
30
|
+
- ``_resolved_map``: versioned URN string → artefact (resolved-only)
|
|
31
|
+
- ``_resolved_latest``: ``cls:agency:id`` → artefact (latest resolved)
|
|
32
|
+
|
|
33
|
+
The resolved indices are populated only when ``ingest(..., resolved=True)``
|
|
34
|
+
is called. This lets ``get_resolved()`` return a cached object only when
|
|
35
|
+
it was fetched with ``references=all``, preventing unresolved objects
|
|
36
|
+
from being returned by named methods or ``resolve()``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self._map: dict[str, Any] = {}
|
|
41
|
+
self._latest: dict[str, Any] = {}
|
|
42
|
+
self._resolved_map: dict[str, Any] = {}
|
|
43
|
+
self._resolved_latest: dict[str, Any] = {}
|
|
44
|
+
self._class_tomsg_field: dict[str, str] = {}
|
|
45
|
+
|
|
46
|
+
def ingest(self, msg: StructureMessage, *, resolved: bool = False) -> None:
|
|
47
|
+
"""Add all artefacts from a StructureMessage to the identity map.
|
|
48
|
+
|
|
49
|
+
Pass ``resolved=True`` when the message was fetched with
|
|
50
|
+
``references=all`` so that ``get_resolved()`` can return the cached
|
|
51
|
+
object on subsequent calls.
|
|
52
|
+
"""
|
|
53
|
+
for artefact in msg.all_artefacts():
|
|
54
|
+
cls_name = type(artefact).__name__
|
|
55
|
+
package = getattr(type(artefact), "sdmx_package", None)
|
|
56
|
+
if package is None:
|
|
57
|
+
continue
|
|
58
|
+
urn = SdmxUrn.make(
|
|
59
|
+
type(artefact),
|
|
60
|
+
package=package,
|
|
61
|
+
artefact_class=cls_name,
|
|
62
|
+
agency=artefact.agency_id,
|
|
63
|
+
id=artefact.id,
|
|
64
|
+
version=artefact.version,
|
|
65
|
+
)
|
|
66
|
+
msg_field = getattr(type(artefact), "msg_field", None)
|
|
67
|
+
if msg_field and cls_name not in self._class_tomsg_field:
|
|
68
|
+
self._class_tomsg_field[cls_name] = msg_field
|
|
69
|
+
key = str(urn)
|
|
70
|
+
self._map[key] = artefact
|
|
71
|
+
latest_key = f"{cls_name}:{artefact.agency_id}:{artefact.id}"
|
|
72
|
+
existing = self._latest.get(latest_key)
|
|
73
|
+
if existing is None or _version_key(artefact.version) > _version_key(existing.version):
|
|
74
|
+
self._latest[latest_key] = artefact
|
|
75
|
+
if resolved:
|
|
76
|
+
self._resolved_map[key] = artefact
|
|
77
|
+
existing_r = self._resolved_latest.get(latest_key)
|
|
78
|
+
if existing_r is None or artefact.version > existing_r.version:
|
|
79
|
+
self._resolved_latest[latest_key] = artefact
|
|
80
|
+
|
|
81
|
+
def get[T](self, urn: SdmxUrn[T]) -> T | None:
|
|
82
|
+
"""Return a cached artefact by URN, or None if not cached."""
|
|
83
|
+
hit = self._map.get(str(urn))
|
|
84
|
+
if hit is not None:
|
|
85
|
+
return hit # type: ignore[return-value]
|
|
86
|
+
if urn.version == "latest":
|
|
87
|
+
latest_key = f"{urn.artefact_class}:{urn.agency}:{urn.id}"
|
|
88
|
+
return self._latest.get(latest_key) # type: ignore[return-value]
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def get_resolved[T](self, urn: SdmxUrn[T]) -> T | None:
|
|
92
|
+
"""Return a cached artefact only if it was ingested as resolved."""
|
|
93
|
+
hit = self._resolved_map.get(str(urn))
|
|
94
|
+
if hit is not None:
|
|
95
|
+
return hit # type: ignore[return-value]
|
|
96
|
+
if urn.version == "latest":
|
|
97
|
+
latest_key = f"{urn.artefact_class}:{urn.agency}:{urn.id}"
|
|
98
|
+
return self._resolved_latest.get(latest_key) # type: ignore[return-value]
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def first_of_class[T](self, artefact_type: type[T]) -> T | None:
|
|
102
|
+
"""Return the first cached artefact whose type matches artefact_type."""
|
|
103
|
+
for v in self._map.values():
|
|
104
|
+
if isinstance(v, artefact_type):
|
|
105
|
+
return v
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def all_of_class[T](self, artefact_type: type[T]) -> list[T]:
|
|
109
|
+
"""Return all cached artefacts whose type matches artefact_type."""
|
|
110
|
+
return [v for v in self._map.values() if isinstance(v, artefact_type)]
|
|
111
|
+
|
|
112
|
+
def extract[T](self, msg: StructureMessage, urn: SdmxUrn[T]) -> T | None:
|
|
113
|
+
"""Pull a specific artefact out of a freshly-parsed message by URN.
|
|
114
|
+
|
|
115
|
+
Looks in the StructureMessage collection named by the artefact
|
|
116
|
+
type's ``msg_field`` class attribute.
|
|
117
|
+
"""
|
|
118
|
+
field = self._class_tomsg_field.get(urn.artefact_class)
|
|
119
|
+
if field is None:
|
|
120
|
+
return None
|
|
121
|
+
collection = getattr(msg, field, None)
|
|
122
|
+
if collection is None:
|
|
123
|
+
return None
|
|
124
|
+
return collection.get(urn) # type: ignore[return-value]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""sdmxlib.formats — SDMX wire format readers and writers.
|
|
2
|
+
|
|
3
|
+
Entry points:
|
|
4
|
+
|
|
5
|
+
from sdmxlib.formats import read, parse, to_xml, to_json
|
|
6
|
+
|
|
7
|
+
msg = read("path/to/file.xml")
|
|
8
|
+
msg = read(xml_bytes)
|
|
9
|
+
|
|
10
|
+
reg = sl.Registry()
|
|
11
|
+
read("codelists.xml", registry=reg)
|
|
12
|
+
read("datastructure.xml", registry=reg) # cross-file Refs resolve automatically
|
|
13
|
+
|
|
14
|
+
xml_bytes = to_xml(msg)
|
|
15
|
+
json_bytes = to_json(msg)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from sdmxlib.formats.sdmx_json.reader import read as _read_json
|
|
21
|
+
from sdmxlib.formats.sdmx_json.writer import write as _write_json
|
|
22
|
+
from sdmxlib.formats.sdmx_ml21.namespaces import MES as _MES_21
|
|
23
|
+
from sdmxlib.formats.sdmx_ml21.reader import read as _read_21
|
|
24
|
+
from sdmxlib.formats.sdmx_ml21.writer import write as _write_21
|
|
25
|
+
from sdmxlib.formats.sdmx_ml30.namespaces import MES as _MES_30
|
|
26
|
+
from sdmxlib.formats.sdmx_ml30.reader import read as _read_30
|
|
27
|
+
from sdmxlib.formats.sdmx_ml30.writer import write as _write_30
|
|
28
|
+
from sdmxlib.model.message import StructureMessage
|
|
29
|
+
from sdmxlib.model.registry import Registry
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read(
|
|
33
|
+
source: "str | Path | bytes",
|
|
34
|
+
*,
|
|
35
|
+
version: str | None = None,
|
|
36
|
+
registry: Registry | None = None,
|
|
37
|
+
) -> StructureMessage:
|
|
38
|
+
"""Read an SDMX file (or raw bytes) into a StructureMessage.
|
|
39
|
+
|
|
40
|
+
A convenience wrapper over :func:`parse` that accepts a file path in
|
|
41
|
+
addition to raw bytes. Format is auto-detected when *version* is omitted.
|
|
42
|
+
|
|
43
|
+
If *registry* is provided all parsed artefacts are added to it, enabling
|
|
44
|
+
cross-file Ref resolution when multiple files are loaded into the same
|
|
45
|
+
registry.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
source: File path (``str`` or ``Path``) or raw bytes (XML or JSON).
|
|
49
|
+
version: Optional format hint: ``"json"``, ``"2.1"``, or ``"3.0"``.
|
|
50
|
+
Auto-detected if omitted.
|
|
51
|
+
registry: Optional :class:`~sdmxlib.model.registry.Registry` to intern
|
|
52
|
+
artefacts into after parsing.
|
|
53
|
+
|
|
54
|
+
Example::
|
|
55
|
+
|
|
56
|
+
msg = sl.read("path/to/datastructure.xml")
|
|
57
|
+
|
|
58
|
+
reg = sl.Registry()
|
|
59
|
+
sl.read("codelists.xml", registry=reg)
|
|
60
|
+
sl.read("datastructure.xml", registry=reg)
|
|
61
|
+
dsd = reg.get(sl.DataStructure.urn(agency="ESTAT", id="CPI", version="1.0"))
|
|
62
|
+
"""
|
|
63
|
+
data = Path(source).read_bytes() if isinstance(source, (str, Path)) else source
|
|
64
|
+
return parse(data, version=version, registry=registry)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse(
|
|
68
|
+
data: bytes,
|
|
69
|
+
*,
|
|
70
|
+
version: str | None = None,
|
|
71
|
+
registry: Registry | None = None,
|
|
72
|
+
) -> StructureMessage:
|
|
73
|
+
"""Parse SDMX bytes into a StructureMessage.
|
|
74
|
+
|
|
75
|
+
Auto-detects the format (SDMX-JSON 2.0 or SDMX-ML 2.1/3.0) when
|
|
76
|
+
*version* is not supplied. Cross-references within the message are
|
|
77
|
+
auto-resolved.
|
|
78
|
+
|
|
79
|
+
If *registry* is provided all parsed artefacts are added to it after
|
|
80
|
+
parsing, interning their Refs and enabling cross-message resolution.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
data: Raw bytes — JSON or XML.
|
|
84
|
+
version: Optional hint: ``"json"``, ``"2.1"``, or ``"3.0"``.
|
|
85
|
+
Auto-detected if omitted.
|
|
86
|
+
registry: Optional Registry to intern artefacts into after parsing.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If the format cannot be detected or is not supported.
|
|
90
|
+
"""
|
|
91
|
+
detected = version or _detect_version(data)
|
|
92
|
+
if detected == "json":
|
|
93
|
+
return _read_json(data, registry=registry)
|
|
94
|
+
if detected == "2.1":
|
|
95
|
+
return _read_21(data, registry=registry)
|
|
96
|
+
if detected == "3.0":
|
|
97
|
+
return _read_30(data, registry=registry)
|
|
98
|
+
msg = f"Unsupported format: {detected!r}"
|
|
99
|
+
raise ValueError(msg)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def serialize(msg: StructureMessage, *, version: str = "2.1", pretty_print: bool = False) -> bytes:
|
|
103
|
+
"""Serialise a StructureMessage to bytes.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
msg: The StructureMessage to serialise.
|
|
107
|
+
version: Format to write — ``"2.1"``, ``"3.0"`` (SDMX-ML XML) or
|
|
108
|
+
``"json"`` (SDMX-JSON 2.0).
|
|
109
|
+
pretty_print: If True, indent the output.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: If the requested version is not supported.
|
|
113
|
+
"""
|
|
114
|
+
if version == "json":
|
|
115
|
+
return _write_json(msg, pretty_print=pretty_print)
|
|
116
|
+
if version == "2.1":
|
|
117
|
+
return _write_21(msg, pretty_print=pretty_print)
|
|
118
|
+
if version == "3.0":
|
|
119
|
+
return _write_30(msg, pretty_print=pretty_print)
|
|
120
|
+
err = f"Unsupported format for writing: {version!r}"
|
|
121
|
+
raise ValueError(err)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def to_xml(msg: StructureMessage, *, version: str = "2.1", pretty_print: bool = False) -> bytes:
|
|
125
|
+
"""Serialise a StructureMessage to SDMX-ML XML bytes.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
msg: The StructureMessage to serialise.
|
|
129
|
+
version: SDMX-ML version — ``"2.1"`` (default) or ``"3.0"``.
|
|
130
|
+
pretty_print: If True, indent the output.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
xml_bytes = to_xml(msg)
|
|
134
|
+
xml_bytes = to_xml(msg, version="3.0", pretty_print=True)
|
|
135
|
+
"""
|
|
136
|
+
return serialize(msg, version=version, pretty_print=pretty_print)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def to_json(msg: StructureMessage, *, pretty_print: bool = False) -> bytes:
|
|
140
|
+
"""Serialise a StructureMessage to SDMX-JSON 2.0 bytes.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
msg: The StructureMessage to serialise.
|
|
144
|
+
pretty_print: If True, indent the output.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
json_bytes = to_json(msg)
|
|
148
|
+
json_bytes = to_json(msg, pretty_print=True)
|
|
149
|
+
"""
|
|
150
|
+
return serialize(msg, version="json", pretty_print=pretty_print)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _detect_version(data: bytes) -> str:
|
|
154
|
+
"""Detect the wire format from the first bytes of the payload."""
|
|
155
|
+
# JSON messages start with '{' (possibly after whitespace)
|
|
156
|
+
chunk = data[:2048]
|
|
157
|
+
stripped = chunk.lstrip()
|
|
158
|
+
if stripped and stripped[0:1] == b"{":
|
|
159
|
+
return "json"
|
|
160
|
+
text = chunk.decode("utf-8", errors="replace")
|
|
161
|
+
if _MES_30 in text:
|
|
162
|
+
return "3.0"
|
|
163
|
+
if _MES_21 in text:
|
|
164
|
+
return "2.1"
|
|
165
|
+
msg = "Cannot detect format: not JSON and no recognised SDMX-ML namespace"
|
|
166
|
+
raise ValueError(msg)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""SDMX-CSV data reader.
|
|
2
|
+
|
|
3
|
+
Parses SDMX-CSV messages into :class:`~sdmxlib.model.dataset.Dataset` objects.
|
|
4
|
+
|
|
5
|
+
SDMX-CSV format::
|
|
6
|
+
|
|
7
|
+
DATAFLOW,FREQ,REF_AREA,INDICATOR,TIME_PERIOD,OBS_VALUE,OBS_STATUS
|
|
8
|
+
ESTAT:STS_INPR_M(1.0),M,AT,INDPRO,2024-01,98.5,A
|
|
9
|
+
|
|
10
|
+
The ``DATAFLOW`` column identifies the dataflow but is not a DSD component.
|
|
11
|
+
It is extracted and then dropped; the remaining columns are parsed using the
|
|
12
|
+
DSD schema.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import csv
|
|
16
|
+
import io
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
import polars as pl
|
|
22
|
+
|
|
23
|
+
import sdmxlib.polars as slpl
|
|
24
|
+
from sdmxlib.model.dataflow import Dataflow
|
|
25
|
+
from sdmxlib.model.dataset import Dataset
|
|
26
|
+
from sdmxlib.model.ref import Ref
|
|
27
|
+
from sdmxlib.model.urn import SdmxUrn
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from sdmxlib.model.datastructure import DataStructure
|
|
31
|
+
|
|
32
|
+
_DATAFLOW_COL = "DATAFLOW"
|
|
33
|
+
_KEY_COL = "KEY"
|
|
34
|
+
_DATAFLOW_RE = re.compile(r"^([^:]+):([^(]+)\(([^)]+)\)$")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_dataflow_id(dataflow_id: str) -> tuple[str, str, str]:
|
|
38
|
+
"""Parse an SDMX-CSV DATAFLOW value into ``(agency, id, version)``.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
parse_dataflow_id("ESTAT:STS_INPR_M(1.0)") # ("ESTAT", "STS_INPR_M", "1.0")
|
|
42
|
+
"""
|
|
43
|
+
m = _DATAFLOW_RE.match(dataflow_id)
|
|
44
|
+
if m is None:
|
|
45
|
+
msg = f"Cannot parse DATAFLOW value {dataflow_id!r} — expected 'AGENCY:ID(VERSION)'"
|
|
46
|
+
raise ValueError(msg)
|
|
47
|
+
return m.group(1), m.group(2), m.group(3)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _peek_dataflow_id(source: "str | Path | bytes") -> str | None:
|
|
51
|
+
"""Extract the DATAFLOW value from the first data row without reading the whole file."""
|
|
52
|
+
if isinstance(source, (str, Path)):
|
|
53
|
+
with Path(source).open(newline="", encoding="utf-8") as f:
|
|
54
|
+
head = f.read(4096)
|
|
55
|
+
else:
|
|
56
|
+
head = source[:4096].decode("utf-8", errors="replace")
|
|
57
|
+
|
|
58
|
+
reader = csv.reader(io.StringIO(head))
|
|
59
|
+
try:
|
|
60
|
+
header = next(reader)
|
|
61
|
+
except StopIteration:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
df_idx = header.index(_DATAFLOW_COL)
|
|
65
|
+
except ValueError:
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
first_row = next(reader)
|
|
69
|
+
except StopIteration:
|
|
70
|
+
return None
|
|
71
|
+
return first_row[df_idx] if df_idx < len(first_row) else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _dataflow_ref(dataflow_str: str) -> "Ref[Dataflow]":
|
|
75
|
+
"""Build an unresolved Ref[Dataflow] from an SDMX-CSV DATAFLOW column value."""
|
|
76
|
+
agency, id_, version = parse_dataflow_id(dataflow_str)
|
|
77
|
+
urn = SdmxUrn.make(
|
|
78
|
+
Dataflow,
|
|
79
|
+
package="datastructure",
|
|
80
|
+
artefact_class="Dataflow",
|
|
81
|
+
agency=agency,
|
|
82
|
+
id=id_,
|
|
83
|
+
version=version,
|
|
84
|
+
)
|
|
85
|
+
return Ref[Dataflow].unresolved(urn)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def scan_csv(
|
|
89
|
+
source: "str | Path | bytes",
|
|
90
|
+
structure: "DataStructure",
|
|
91
|
+
*,
|
|
92
|
+
infer_schema_length: "int | None" = 0,
|
|
93
|
+
dataflow: "Ref[Dataflow] | None" = None,
|
|
94
|
+
) -> Dataset:
|
|
95
|
+
"""Parse an SDMX-CSV message into a :class:`~sdmxlib.model.dataset.Dataset`.
|
|
96
|
+
|
|
97
|
+
File paths are scanned lazily via ``pl.scan_csv``; bytes (e.g. from an
|
|
98
|
+
HTTP response) are read eagerly and wrapped in a ``LazyFrame``. Either
|
|
99
|
+
way the returned ``Dataset.data`` is always a ``pl.LazyFrame``.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
source: File path or raw bytes from an SDMX-CSV response.
|
|
103
|
+
structure: Fully resolved :class:`~sdmxlib.model.datastructure.DataStructure`.
|
|
104
|
+
Codelists should be resolved for ``pl.Enum`` typing.
|
|
105
|
+
infer_schema_length: Passed to Polars. ``0`` (default) means
|
|
106
|
+
schema is fully DSD-driven with no row sampling,
|
|
107
|
+
which is required for true streaming on paths.
|
|
108
|
+
dataflow: Optional resolved :class:`~sdmxlib.model.dataflow.Dataflow` ref.
|
|
109
|
+
When ``None`` and a ``DATAFLOW`` column is present, an
|
|
110
|
+
unresolved ref is built from the column value.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A :class:`~sdmxlib.model.dataset.Dataset` with a lazy ``data`` frame.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
import sdmxlib.polars as slpl
|
|
117
|
+
|
|
118
|
+
ds = slpl.scan_csv("data.csv", dsd)
|
|
119
|
+
df = ds.label().collect()
|
|
120
|
+
"""
|
|
121
|
+
dataflow_str = _peek_dataflow_id(source)
|
|
122
|
+
dataflow_ref = dataflow
|
|
123
|
+
if dataflow_ref is None and dataflow_str is not None:
|
|
124
|
+
dataflow_ref = _dataflow_ref(dataflow_str)
|
|
125
|
+
|
|
126
|
+
schema_overrides = slpl.schema(structure)
|
|
127
|
+
|
|
128
|
+
if isinstance(source, (str, Path)):
|
|
129
|
+
lf = pl.scan_csv(
|
|
130
|
+
source,
|
|
131
|
+
schema_overrides=schema_overrides,
|
|
132
|
+
infer_schema_length=infer_schema_length,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
lf = pl.read_csv(
|
|
136
|
+
io.BytesIO(source),
|
|
137
|
+
schema_overrides=schema_overrides,
|
|
138
|
+
infer_schema_length=infer_schema_length,
|
|
139
|
+
).lazy()
|
|
140
|
+
|
|
141
|
+
# Drop metadata columns that are not DSD components.
|
|
142
|
+
col_names = lf.collect_schema().names()
|
|
143
|
+
to_drop = [c for c in (_DATAFLOW_COL, _KEY_COL) if c in col_names]
|
|
144
|
+
if to_drop:
|
|
145
|
+
lf = lf.drop(to_drop)
|
|
146
|
+
|
|
147
|
+
return Dataset(structure=structure, data=lf, dataflow=dataflow_ref)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""sdmxlib.formats.sdmx_json — SDMX-JSON 2.0 structure reader."""
|