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/query.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""GetQuery[T], Query[T], and DataQuery — artefact and data queries."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
6
|
+
|
|
7
|
+
import polars as pl
|
|
8
|
+
from attrs import frozen
|
|
9
|
+
|
|
10
|
+
from sdmxlib.api.filters import DimFilter
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sdmxlib.model.dataset import Dataset
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@frozen
|
|
17
|
+
class _KeySpec:
|
|
18
|
+
"""Bundled key inputs passed from DataQuery to registry closures."""
|
|
19
|
+
|
|
20
|
+
raw_key: str = "all"
|
|
21
|
+
slices: tuple[DimFilter, ...] = ()
|
|
22
|
+
measures: tuple[str, ...] | None = None
|
|
23
|
+
measure_dim_hint: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _to_references(reference_type: type) -> str:
|
|
27
|
+
"""Map a model type to its SDMX REST ``references=`` parameter string."""
|
|
28
|
+
sdmx_class: str = getattr(reference_type, "sdmx_class", reference_type.__name__)
|
|
29
|
+
return sdmx_class.lower()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GetQuery[T]:
|
|
33
|
+
"""Single-artefact lookup with configurable resolution depth.
|
|
34
|
+
|
|
35
|
+
Returned by ``reg.get()``. Call ``.fetch()`` for an unresolved skeleton
|
|
36
|
+
or ``.resolve()`` to follow references.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
reg.get(sl.DataStructure, agency="ECB", id="ECB_EXR1").fetch()
|
|
40
|
+
reg.get(sl.DataStructure, agency="ECB", id="ECB_EXR1").resolve()
|
|
41
|
+
reg.get(sl.DataStructure, agency="ECB", id="ECB_EXR1").resolve(sl.Codelist)
|
|
42
|
+
reg.get(sl.DataStructure, agency="ECB", id="ECB_EXR1").resolve(partial=False)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
fetcher: Callable[[], T],
|
|
48
|
+
resolver: Callable[[str, bool], T],
|
|
49
|
+
) -> None:
|
|
50
|
+
self._fetcher = fetcher
|
|
51
|
+
self._resolver = resolver
|
|
52
|
+
|
|
53
|
+
def fetch(self) -> T:
|
|
54
|
+
"""Fetch the artefact skeleton only (``detail=full``, ``references=none``)."""
|
|
55
|
+
return self._fetcher()
|
|
56
|
+
|
|
57
|
+
def resolve(
|
|
58
|
+
self,
|
|
59
|
+
reference_type: type | None = None,
|
|
60
|
+
*,
|
|
61
|
+
partial: bool = True,
|
|
62
|
+
) -> T:
|
|
63
|
+
"""Fetch the artefact with references resolved.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
reference_type: If given, resolve only Refs of this type.
|
|
67
|
+
If ``None`` (default), resolve all references.
|
|
68
|
+
partial: If ``True`` (default), referenced ItemSchemes are trimmed
|
|
69
|
+
to items allowed by ContentConstraints
|
|
70
|
+
(``detail=referencepartial``). Produces smaller, more
|
|
71
|
+
useful responses for the common case. Set to ``False``
|
|
72
|
+
for full metadata audits (``detail=full``).
|
|
73
|
+
|
|
74
|
+
Wire mapping:
|
|
75
|
+
resolve() → detail=referencepartial, references=descendants
|
|
76
|
+
resolve(partial=False) → detail=full, references=descendants
|
|
77
|
+
resolve(sl.Codelist) → detail=referencepartial, references=codelist
|
|
78
|
+
resolve(sl.Codelist, partial=False) → detail=full, references=codelist
|
|
79
|
+
"""
|
|
80
|
+
ref_str = "descendants" if reference_type is None else _to_references(reference_type)
|
|
81
|
+
return self._resolver(ref_str, partial)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Query[T]:
|
|
85
|
+
"""Lazy collection search. Fetches on terminal call.
|
|
86
|
+
|
|
87
|
+
Server-side constraint (agency) is set at construction.
|
|
88
|
+
Client-side refinements (filter, sort_by, limit) chain before the terminal.
|
|
89
|
+
|
|
90
|
+
Terminals:
|
|
91
|
+
browse() — identity stubs only (no items populated)
|
|
92
|
+
fetch() — full artefacts, Refs unresolved
|
|
93
|
+
resolve() — full artefacts, Refs resolved (constrained by default)
|
|
94
|
+
resolve(partial=False) — full artefacts, Refs resolved, all codes
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
reg.query(sl.Codelist, agency="ESTAT").fetch()
|
|
98
|
+
|
|
99
|
+
(
|
|
100
|
+
reg.query(sl.Dataflow, agency="ECB")
|
|
101
|
+
.filter(lambda df: "EXR" in df.id)
|
|
102
|
+
.sort_by("id")
|
|
103
|
+
.limit(5)
|
|
104
|
+
.resolve()
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
reg.query(sl.Dataflow, agency="ECB").resolve(sl.DataStructure)
|
|
108
|
+
reg.query(sl.Dataflow, agency="ECB").browse()
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
fetcher: Callable[[], list[T]],
|
|
114
|
+
resolver: Callable[[str, bool], list[T]],
|
|
115
|
+
browser: Callable[[], list[T]],
|
|
116
|
+
) -> None:
|
|
117
|
+
self._fetcher = fetcher
|
|
118
|
+
self._resolver = resolver
|
|
119
|
+
self._browser = browser
|
|
120
|
+
self._filters: list[Callable[[T], bool]] = []
|
|
121
|
+
self._order_key: str | Callable[[T], Any] | None = None
|
|
122
|
+
self._descending: bool = False
|
|
123
|
+
self._limit: int | None = None
|
|
124
|
+
|
|
125
|
+
# ── Refinement (return Self) ──────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def filter(self, predicate: Callable[[T], bool]) -> Self:
|
|
128
|
+
"""Add a client-side filter predicate."""
|
|
129
|
+
self._filters.append(predicate)
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def sort_by(self, key: str | Callable[[T], Any], *, descending: bool = False) -> Self:
|
|
133
|
+
"""Sort results by attribute name or key function."""
|
|
134
|
+
self._order_key = key
|
|
135
|
+
self._descending = descending
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def limit(self, n: int) -> Self:
|
|
139
|
+
"""Truncate results to at most n items."""
|
|
140
|
+
self._limit = n
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
# ── Internal pipeline ─────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
def _apply(self, items: list[T]) -> list[T]:
|
|
146
|
+
for pred in self._filters:
|
|
147
|
+
items = [item for item in items if pred(item)]
|
|
148
|
+
if self._order_key is not None:
|
|
149
|
+
order_key = self._order_key
|
|
150
|
+
key_fn: Callable[[T], Any] = order_key if callable(order_key) else lambda item: getattr(item, order_key)
|
|
151
|
+
items = sorted(items, key=key_fn, reverse=self._descending)
|
|
152
|
+
if self._limit is not None:
|
|
153
|
+
items = items[: self._limit]
|
|
154
|
+
return items
|
|
155
|
+
|
|
156
|
+
# ── Terminals ─────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def browse(self) -> list[T]:
|
|
159
|
+
"""Fetch lightweight identity stubs (``detail=allstubs``, ``references=none``).
|
|
160
|
+
|
|
161
|
+
Each artefact has identity fields (id, agency, version, name) but
|
|
162
|
+
item collections are empty. Use for catalogue scanning before deciding
|
|
163
|
+
which artefacts to fetch in full.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
for df in reg.query(sl.Dataflow, agency="ECB").browse():
|
|
167
|
+
print(df.id, df.name.get("en"))
|
|
168
|
+
"""
|
|
169
|
+
return self._apply(self._browser())
|
|
170
|
+
|
|
171
|
+
def fetch(self) -> list[T]:
|
|
172
|
+
"""Fetch artefacts with unresolved Refs (``detail=full``, ``references=none``)."""
|
|
173
|
+
return self._apply(self._fetcher())
|
|
174
|
+
|
|
175
|
+
def fetch_one(self) -> T:
|
|
176
|
+
"""Fetch exactly one artefact. Raises if count != 1."""
|
|
177
|
+
results = self.fetch()
|
|
178
|
+
if len(results) != 1:
|
|
179
|
+
msg = f"Expected exactly 1 result, got {len(results)}"
|
|
180
|
+
raise ValueError(msg)
|
|
181
|
+
return results[0]
|
|
182
|
+
|
|
183
|
+
def fetch_first(self) -> T:
|
|
184
|
+
"""Fetch the first matching artefact. Raises if none found."""
|
|
185
|
+
results = self.fetch()
|
|
186
|
+
if not results:
|
|
187
|
+
msg = "No results found"
|
|
188
|
+
raise ValueError(msg)
|
|
189
|
+
return results[0]
|
|
190
|
+
|
|
191
|
+
def fetch_optional(self) -> T | None:
|
|
192
|
+
"""Fetch the first matching artefact, or None if none found."""
|
|
193
|
+
results = self.fetch()
|
|
194
|
+
return results[0] if results else None
|
|
195
|
+
|
|
196
|
+
def resolve(
|
|
197
|
+
self,
|
|
198
|
+
reference_type: type | None = None,
|
|
199
|
+
*,
|
|
200
|
+
partial: bool = True,
|
|
201
|
+
) -> list[T]:
|
|
202
|
+
"""Fetch artefacts with references resolved.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
reference_type: If given, resolve only Refs of this type.
|
|
206
|
+
partial: Controls whether referenced ItemSchemes are trimmed by
|
|
207
|
+
ContentConstraints. ``True`` (default) is faster and
|
|
208
|
+
more useful for data queries; ``False`` for full audits.
|
|
209
|
+
"""
|
|
210
|
+
ref_str = "descendants" if reference_type is None else _to_references(reference_type)
|
|
211
|
+
return self._apply(self._resolver(ref_str, partial))
|
|
212
|
+
|
|
213
|
+
def resolve_one(
|
|
214
|
+
self,
|
|
215
|
+
reference_type: type | None = None,
|
|
216
|
+
*,
|
|
217
|
+
partial: bool = True,
|
|
218
|
+
) -> T:
|
|
219
|
+
"""Resolve exactly one artefact. Raises if count != 1."""
|
|
220
|
+
results = self.resolve(reference_type, partial=partial)
|
|
221
|
+
if len(results) != 1:
|
|
222
|
+
msg = f"Expected exactly 1 result, got {len(results)}"
|
|
223
|
+
raise ValueError(msg)
|
|
224
|
+
return results[0]
|
|
225
|
+
|
|
226
|
+
def resolve_first(
|
|
227
|
+
self,
|
|
228
|
+
reference_type: type | None = None,
|
|
229
|
+
*,
|
|
230
|
+
partial: bool = True,
|
|
231
|
+
) -> T:
|
|
232
|
+
"""Resolve the first matching artefact. Raises if none found."""
|
|
233
|
+
results = self.resolve(reference_type, partial=partial)
|
|
234
|
+
if not results:
|
|
235
|
+
msg = "No results found"
|
|
236
|
+
raise ValueError(msg)
|
|
237
|
+
return results[0]
|
|
238
|
+
|
|
239
|
+
def resolve_optional(
|
|
240
|
+
self,
|
|
241
|
+
reference_type: type | None = None,
|
|
242
|
+
*,
|
|
243
|
+
partial: bool = True,
|
|
244
|
+
) -> T | None:
|
|
245
|
+
"""Resolve the first matching artefact, or None if none found."""
|
|
246
|
+
results = self.resolve(reference_type, partial=partial)
|
|
247
|
+
return results[0] if results else None
|
|
248
|
+
|
|
249
|
+
def count(self) -> int:
|
|
250
|
+
"""Return the number of matching artefacts."""
|
|
251
|
+
return len(self.fetch())
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class DataQuery:
|
|
255
|
+
"""Lazy data fetch — resolves to a :class:`~sdmxlib.model.dataset.Dataset`.
|
|
256
|
+
|
|
257
|
+
Returned by :meth:`~sdmxlib.api.registry.RestRegistry.data`. Use
|
|
258
|
+
:meth:`fetch` for in-memory loading or :meth:`fetch_to` to stream
|
|
259
|
+
directly to a file for large datasets. Use :meth:`slice` and
|
|
260
|
+
:meth:`measures` to filter what comes over the wire.
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
# Raw positional key string (backwards compatible)
|
|
264
|
+
ds = reg.data(flow, key="M.USD.EUR.SP00.A").fetch()
|
|
265
|
+
|
|
266
|
+
# Named dimension filter — no positional knowledge needed
|
|
267
|
+
ds = (
|
|
268
|
+
reg.data(flow)
|
|
269
|
+
.slice(sl.dim("FREQ") == "M", sl.dim("CURRENCY") == "USD")
|
|
270
|
+
.fetch()
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Measure selection + pivot for long-format SDMX 2.1 flows
|
|
274
|
+
df = (
|
|
275
|
+
reg.data(flow)
|
|
276
|
+
.slice(sl.dim("REF_AREA").isin(["AT", "DE"]))
|
|
277
|
+
.measures(["EMP", "UNE"])
|
|
278
|
+
.pivot()
|
|
279
|
+
.collect()
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# File-backed streaming for large data
|
|
283
|
+
ds = reg.data(sl.Dataflow, agency="ESTAT", id="LFS").fetch_to("lfs.csv")
|
|
284
|
+
ds.sink_parquet("lfs.parquet")
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
def __init__(
|
|
288
|
+
self,
|
|
289
|
+
fetcher: "Callable[[_KeySpec], Dataset]",
|
|
290
|
+
streamer: "Callable[[Path, _KeySpec], Dataset]",
|
|
291
|
+
raw_key: str = "all",
|
|
292
|
+
measure_dim: str | None = None,
|
|
293
|
+
) -> None:
|
|
294
|
+
self._fetcher = fetcher
|
|
295
|
+
self._streamer = streamer
|
|
296
|
+
self._raw_key = raw_key
|
|
297
|
+
self._measure_dim = measure_dim
|
|
298
|
+
self._slices: list[DimFilter] = []
|
|
299
|
+
self._measures: list[str] | None = None
|
|
300
|
+
|
|
301
|
+
def slice(self, *filters: DimFilter) -> Self:
|
|
302
|
+
"""Add server-side dimension filters to the SDMX key.
|
|
303
|
+
|
|
304
|
+
Each filter is a :class:`~sdmxlib.api.filters.DimFilter` produced by
|
|
305
|
+
:func:`~sdmxlib.dim`. Calling ``.slice()`` multiple times accumulates
|
|
306
|
+
filters — each call is additive.
|
|
307
|
+
|
|
308
|
+
Unspecified dimensions become wildcards. Raises ``ValueError`` if
|
|
309
|
+
``key=`` was also passed to
|
|
310
|
+
:meth:`~sdmxlib.api.registry.RestRegistry.data`.
|
|
311
|
+
|
|
312
|
+
Example:
|
|
313
|
+
import sdmxlib as sl
|
|
314
|
+
|
|
315
|
+
reg.data(flow).slice(sl.dim("FREQ") == "M").fetch()
|
|
316
|
+
reg.data(flow).slice(
|
|
317
|
+
sl.dim("FREQ") == "M",
|
|
318
|
+
sl.dim("REF_AREA").isin(["AT", "DE"]),
|
|
319
|
+
).fetch()
|
|
320
|
+
|
|
321
|
+
# Chained — equivalent to a single call
|
|
322
|
+
(
|
|
323
|
+
reg.data(flow)
|
|
324
|
+
.slice(sl.dim("FREQ") == "M")
|
|
325
|
+
.slice(sl.dim("REF_AREA") == "AT")
|
|
326
|
+
.fetch()
|
|
327
|
+
)
|
|
328
|
+
"""
|
|
329
|
+
if self._raw_key != "all":
|
|
330
|
+
msg = "Cannot combine .slice() with key= — use one or the other"
|
|
331
|
+
raise ValueError(msg)
|
|
332
|
+
self._slices.extend(filters)
|
|
333
|
+
return self
|
|
334
|
+
|
|
335
|
+
def measures(self, codes: "str | list[str]") -> Self:
|
|
336
|
+
"""Select which measures or indicators to fetch.
|
|
337
|
+
|
|
338
|
+
For SDMX 2.1 flows with a ``MeasureDimension`` (e.g. ``INDIC_EM``),
|
|
339
|
+
this filters the indicator key segment — controlling which columns
|
|
340
|
+
appear after :meth:`pivot`. For SDMX 3.0 multi-measure flows this
|
|
341
|
+
will become the ``measures=`` query parameter.
|
|
342
|
+
|
|
343
|
+
``codes`` is a single code string or a list of code strings.
|
|
344
|
+
|
|
345
|
+
Example:
|
|
346
|
+
# Fetch only EMP and UNE indicators, then pivot to wide
|
|
347
|
+
reg.data(flow).measures(["EMP", "UNE"]).pivot().collect()
|
|
348
|
+
|
|
349
|
+
# Single measure
|
|
350
|
+
reg.data(flow).measures("EMP").fetch()
|
|
351
|
+
"""
|
|
352
|
+
self._measures = [codes] if isinstance(codes, str) else list(codes)
|
|
353
|
+
return self
|
|
354
|
+
|
|
355
|
+
def _spec(self) -> _KeySpec:
|
|
356
|
+
return _KeySpec(
|
|
357
|
+
raw_key=self._raw_key,
|
|
358
|
+
slices=tuple(self._slices),
|
|
359
|
+
measures=tuple(self._measures) if self._measures is not None else None,
|
|
360
|
+
measure_dim_hint=self._measure_dim,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def fetch(self) -> "Dataset":
|
|
364
|
+
"""Fetch data into memory and return a Dataset.
|
|
365
|
+
|
|
366
|
+
The full response is loaded as bytes then wrapped in a ``pl.LazyFrame``.
|
|
367
|
+
Suitable for datasets that fit comfortably in memory.
|
|
368
|
+
"""
|
|
369
|
+
return self._fetcher(self._spec())
|
|
370
|
+
|
|
371
|
+
def pivot(self) -> pl.LazyFrame:
|
|
372
|
+
"""Fetch data and pivot to wide format in one step.
|
|
373
|
+
|
|
374
|
+
Equivalent to ``reg.data(...).fetch().pivot()``. Requires a
|
|
375
|
+
``MeasureDimension`` in the DSD — raises ``ValueError`` otherwise.
|
|
376
|
+
|
|
377
|
+
Returns a ``pl.LazyFrame`` — call ``.collect()`` to materialise or
|
|
378
|
+
``.sink_parquet()`` to stream directly to a file.
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
wide = reg.data(flow).measures(["EMP", "UNE"]).pivot().collect()
|
|
382
|
+
reg.data(flow).pivot().sink_parquet("wide.parquet")
|
|
383
|
+
"""
|
|
384
|
+
return self._fetcher(self._spec()).pivot()
|
|
385
|
+
|
|
386
|
+
def pivot_to(self, path: "str | Path") -> None:
|
|
387
|
+
"""Stream data to a file and sink the pivoted wide table.
|
|
388
|
+
|
|
389
|
+
Equivalent to ``fetch_to(path).pivot().sink_parquet(...)`` but uses
|
|
390
|
+
a single temp file internally. The long-format file is written by
|
|
391
|
+
httpx chunked streaming; the pivot happens lazily when sinking.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
path: Destination Parquet file path.
|
|
395
|
+
|
|
396
|
+
Example:
|
|
397
|
+
reg.data(flow).measures(["EMP", "UNE"]).pivot_to("wide.parquet")
|
|
398
|
+
"""
|
|
399
|
+
import tempfile # noqa: PLC0415
|
|
400
|
+
|
|
401
|
+
suffix = ".csv"
|
|
402
|
+
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
|
403
|
+
tmp_path = Path(tmp.name)
|
|
404
|
+
try:
|
|
405
|
+
ds = self._streamer(tmp_path, self._spec())
|
|
406
|
+
ds.pivot().sink_parquet(Path(path))
|
|
407
|
+
finally:
|
|
408
|
+
tmp_path.unlink(missing_ok=True)
|
|
409
|
+
|
|
410
|
+
def fetch_to(self, path: "str | Path") -> "Dataset":
|
|
411
|
+
"""Stream data directly to a file and return a file-backed Dataset.
|
|
412
|
+
|
|
413
|
+
Uses httpx chunked streaming — the response is never fully loaded into
|
|
414
|
+
memory. The returned ``Dataset.data`` is a genuine ``pl.LazyFrame``
|
|
415
|
+
backed by ``pl.scan_csv``, so :meth:`~sdmxlib.model.dataset.Dataset.sink_parquet`
|
|
416
|
+
and :meth:`~sdmxlib.model.dataset.Dataset.sink_csv` are truly streaming.
|
|
417
|
+
|
|
418
|
+
The caller owns the file — it is not deleted when the Dataset is
|
|
419
|
+
garbage collected.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
path: Destination file path.
|
|
423
|
+
|
|
424
|
+
Example:
|
|
425
|
+
ds = reg.data(sl.Dataflow, agency="ESTAT", id="LFS").fetch_to("lfs.csv")
|
|
426
|
+
ds.sink_parquet("lfs.parquet")
|
|
427
|
+
"""
|
|
428
|
+
return self._streamer(Path(path), self._spec())
|