sdmxlib 0.41.2__tar.gz → 0.41.3__tar.gz
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-0.41.2 → sdmxlib-0.41.3}/PKG-INFO +2 -2
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/pyproject.toml +6 -2
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/__init__.py +2 -1
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/local/data_store.py +54 -7
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/polars.py +21 -49
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/storage/lazy.py +30 -4
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/README.md +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/_duckdb.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/client.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/federated.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/filters.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/policy.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/providers.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/query.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/registry.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/api/session.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/builders/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/builders/contentconstraint.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/catalog.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/data_store.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_csv/writer.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json/_data_common.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json/data_v1.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json/data_v2.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json/structure_values.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json/writer.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json_1_0/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_json_1_0/writer.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml/_common.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml31/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml31/namespaces.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml31/reader.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/formats/sdmx_ml31/writer.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/local/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/local/registry.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/annotations.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/base.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/binding.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/category.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/code_query.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/codelist.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/collections.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/concept.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/constraint.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/convert.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/dataflow.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/dataset.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/datastructure.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/expr.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/hierarchy.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/hierarchy_query.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/in_memory_registry.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/istring.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/mapping.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/message.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/metadataflow.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/metadataset.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/metadatastructure.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/organisation.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/provision.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/ref.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/registry.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/representation.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/urn.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/model/validation.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/py.typed +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/rest.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/sql.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/storage/__init__.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/storage/_kinds.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/storage/readers.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/storage/schema.py +0 -0
- {sdmxlib-0.41.2 → sdmxlib-0.41.3}/src/sdmxlib/storage/writers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sdmxlib
|
|
3
|
-
Version: 0.41.
|
|
3
|
+
Version: 0.41.3
|
|
4
4
|
Summary: SDMX structural metadata library for Python
|
|
5
5
|
Keywords: sdmx,statistics,metadata,datastructure
|
|
6
6
|
Author: gabrielgellner
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
15
15
|
Requires-Dist: attrs>=26.1.0
|
|
16
|
-
Requires-Dist: duckdb>=1.
|
|
16
|
+
Requires-Dist: duckdb>=1.5.3
|
|
17
17
|
Requires-Dist: httpx>=0.28.1
|
|
18
18
|
Requires-Dist: lxml>=6.0.2
|
|
19
19
|
Requires-Dist: orjson>=3.9
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sdmxlib"
|
|
3
|
-
version = "0.41.
|
|
3
|
+
version = "0.41.3"
|
|
4
4
|
description = "SDMX structural metadata library for Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Apache-2.0" }
|
|
@@ -17,7 +17,11 @@ classifiers = [
|
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
19
|
"attrs>=26.1.0",
|
|
20
|
-
|
|
20
|
+
# >=1.5.3: ducklake extension must support DuckLake catalog format 1.0,
|
|
21
|
+
# which is what statcan-catalog-builder (duckdb 1.5.3) writes into
|
|
22
|
+
# published bundles. duckdb 1.5.1's ducklake tops out at format 0.4 and
|
|
23
|
+
# fails to ATTACH such bundles with "DuckLake catalog version mismatch".
|
|
24
|
+
"duckdb>=1.5.3",
|
|
21
25
|
"httpx>=0.28.1",
|
|
22
26
|
"lxml>=6.0.2",
|
|
23
27
|
"orjson>=3.9",
|
|
@@ -11,7 +11,7 @@ from sdmxlib.data_store import DataStore
|
|
|
11
11
|
from sdmxlib.sql import CompileContext, Predicate, SqlRelation
|
|
12
12
|
from sdmxlib.formats import parse, read, serialize, to_json, to_xml
|
|
13
13
|
from sdmxlib.local import LazyCodelist, LazyHierarchy, Registry
|
|
14
|
-
from sdmxlib.local.data_store import DuckDBDataStore
|
|
14
|
+
from sdmxlib.local.data_store import DuckDBDataStore, DuckLakeVersionError
|
|
15
15
|
from sdmxlib.rest import ComponentFilter, parse_components
|
|
16
16
|
from sdmxlib.model.convert import from_dict, to_dict
|
|
17
17
|
from sdmxlib.model.validation import (
|
|
@@ -167,6 +167,7 @@ __all__ = [
|
|
|
167
167
|
"DriftError",
|
|
168
168
|
"DriftIssue",
|
|
169
169
|
"DuckDBDataStore",
|
|
170
|
+
"DuckLakeVersionError",
|
|
170
171
|
"Facet",
|
|
171
172
|
"FailurePolicy",
|
|
172
173
|
"FederatedCatalog",
|
|
@@ -37,6 +37,29 @@ if TYPE_CHECKING:
|
|
|
37
37
|
WriteSource = pl.DataFrame | pl.LazyFrame | pa.Table | duckdb.DuckDBPyRelation
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class DuckLakeVersionError(Exception): # mirrors RegistryUnavailable etc.
|
|
41
|
+
"""A DuckLake catalog's format is newer than the installed extension.
|
|
42
|
+
|
|
43
|
+
Raised when ATTACH-ing a bundle whose DuckLake catalog format exceeds
|
|
44
|
+
what the loaded ``ducklake`` extension supports. The fix is to align
|
|
45
|
+
versions (upgrade ``duckdb`` >= 1.5.3 and/or the ``ducklake``
|
|
46
|
+
extension), not to migrate the bundle in place. See issue #156.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _is_ducklake_version_mismatch(exc: Exception) -> bool:
|
|
51
|
+
"""True if ``exc`` is a DuckDB error about an unsupported DuckLake format.
|
|
52
|
+
|
|
53
|
+
Matches the two shapes DuckDB raises depending on the skew:
|
|
54
|
+
|
|
55
|
+
- ``DuckLake catalog version mismatch: catalog version is X, but the
|
|
56
|
+
extension requires version Y``
|
|
57
|
+
- ``Only DuckLake versions ... are supported``
|
|
58
|
+
"""
|
|
59
|
+
msg = str(exc).lower()
|
|
60
|
+
return "ducklake" in msg and ("catalog version mismatch" in msg or "are supported" in msg)
|
|
61
|
+
|
|
62
|
+
|
|
40
63
|
class DuckDBDataStore:
|
|
41
64
|
"""A `DataStore` backed by DuckDB.
|
|
42
65
|
|
|
@@ -215,13 +238,37 @@ class DuckDBDataStore:
|
|
|
215
238
|
sql = _azure_create_secret_sql(self._azure_credentials)
|
|
216
239
|
self._conn.execute(sql)
|
|
217
240
|
if self._attach:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
241
|
+
self._run_attaches()
|
|
242
|
+
|
|
243
|
+
def _run_attaches(self) -> None:
|
|
244
|
+
"""ATTACH each configured catalog, translating DuckLake skew errors.
|
|
245
|
+
|
|
246
|
+
A DuckLake catalog whose format is newer than the installed
|
|
247
|
+
``ducklake`` extension surfaces as an opaque DuckDB exception; we
|
|
248
|
+
re-raise it as `DuckLakeVersionError` with the fix. Any other
|
|
249
|
+
ATTACH failure propagates unchanged. See issue #156.
|
|
250
|
+
"""
|
|
251
|
+
conn = self._conn
|
|
252
|
+
if conn is None: # guarded by caller; narrows the type here too
|
|
253
|
+
return
|
|
254
|
+
for alias, spec in (self._attach or {}).items():
|
|
255
|
+
url, options = _split_attach_spec(spec)
|
|
256
|
+
sql = f"ATTACH {url} AS {_sql_identifier(alias)}"
|
|
257
|
+
if options:
|
|
258
|
+
sql += f" {options}"
|
|
259
|
+
try:
|
|
260
|
+
conn.execute(sql)
|
|
261
|
+
except duckdb.Error as exc:
|
|
262
|
+
if _is_ducklake_version_mismatch(exc):
|
|
263
|
+
msg = (
|
|
264
|
+
f"cannot attach DuckLake catalog {alias!r}: its catalog format is "
|
|
265
|
+
"newer than the installed ducklake extension supports. Upgrade "
|
|
266
|
+
"duckdb (>=1.5.3) and/or the ducklake extension, then retry. "
|
|
267
|
+
f"(underlying: {exc})"
|
|
268
|
+
)
|
|
269
|
+
raise DuckLakeVersionError(msg) from exc
|
|
270
|
+
raise
|
|
271
|
+
conn.execute("SET search_path = 'sdmx'")
|
|
225
272
|
|
|
226
273
|
def _needs_azure(self) -> bool:
|
|
227
274
|
"""True if Azure secret/extension setup is required for this session.
|
|
@@ -19,7 +19,6 @@ directly for standalone use::
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
-
import weakref
|
|
23
22
|
from typing import TYPE_CHECKING, Any
|
|
24
23
|
|
|
25
24
|
import polars as pl
|
|
@@ -175,25 +174,15 @@ def validation_report(
|
|
|
175
174
|
# slpl.label() and _label_view() are hot paths. Building the id→label
|
|
176
175
|
# dict by iterating ``codelist.codes`` materialises a Code object per
|
|
177
176
|
# row, which dominates on large codelists (CL_GEOGRAPHY: 78k codes →
|
|
178
|
-
# ~16s per call).
|
|
177
|
+
# ~16s per call).
|
|
179
178
|
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def clear_label_cache() -> None:
|
|
190
|
-
"""Drop the cached id→label maps used by ``label`` / ``_label_view``.
|
|
191
|
-
|
|
192
|
-
The cache is normally invalidated automatically when the underlying
|
|
193
|
-
codelist instance is garbage-collected. Call this explicitly in
|
|
194
|
-
tests or other contexts that want to force a rebuild.
|
|
195
|
-
"""
|
|
196
|
-
_LABEL_MAP_CACHE.clear()
|
|
179
|
+
# Caching lives at the *source*, not here (#155): `LazyCodelist.code_map`
|
|
180
|
+
# memoises per instance per lang with a single-flight build lock, so every
|
|
181
|
+
# code_map caller — not just label() — dedups, and concurrent first-time
|
|
182
|
+
# callers share one build instead of stampeding the DuckDB connection.
|
|
183
|
+
# Plain in-memory `Codelist` has no code_map and is rebuilt each call, but
|
|
184
|
+
# that path is pure-Python dict construction (no SQL) — cheap enough not to
|
|
185
|
+
# warrant a second cache. See docs/design/caching.md.
|
|
197
186
|
|
|
198
187
|
|
|
199
188
|
def _component_label_map(component: object, lang: str) -> dict[str, str] | None:
|
|
@@ -202,8 +191,9 @@ def _component_label_map(component: object, lang: str) -> dict[str, str] | None:
|
|
|
202
191
|
Fast path: when ``component.representation()`` is a
|
|
203
192
|
`LazyCodelist` (or anything with a
|
|
204
193
|
``code_map(*, lang)`` method), use direct-SQL projection — single
|
|
205
|
-
DuckDB query
|
|
206
|
-
to in-memory
|
|
194
|
+
DuckDB query (cached + single-flight at the source), no
|
|
195
|
+
Code/InternationalString construction. Falls back to in-memory
|
|
196
|
+
iteration for plain `Codelist` instances.
|
|
207
197
|
|
|
208
198
|
Returns ``None`` when the component is not enumerated or its
|
|
209
199
|
representation Ref is unresolved.
|
|
@@ -214,33 +204,13 @@ def _component_label_map(component: object, lang: str) -> dict[str, str] | None:
|
|
|
214
204
|
if not (isinstance(rep, Ref) and rep.resolved):
|
|
215
205
|
return None
|
|
216
206
|
cl: object = rep()
|
|
217
|
-
try:
|
|
218
|
-
per_lang = _LABEL_MAP_CACHE.get(cl)
|
|
219
|
-
except TypeError:
|
|
220
|
-
# Some codelist instances (e.g. attrs frozen) aren't weakly
|
|
221
|
-
# referenceable. Skip caching for those.
|
|
222
|
-
per_lang = None
|
|
223
|
-
if per_lang is not None and lang in per_lang:
|
|
224
|
-
return per_lang[lang]
|
|
225
207
|
code_map = getattr(cl, "code_map", None)
|
|
226
|
-
mapping: dict[str, str]
|
|
227
208
|
if callable(code_map):
|
|
228
|
-
# Fast path: direct SQL projection (LazyCodelist).
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
codes = cast("Any", cl).codes
|
|
234
|
-
mapping = {c.id: c.name.get(lang) or c.id for c in codes}
|
|
235
|
-
try:
|
|
236
|
-
if per_lang is None:
|
|
237
|
-
_LABEL_MAP_CACHE[cl] = {lang: mapping}
|
|
238
|
-
else:
|
|
239
|
-
per_lang[lang] = mapping
|
|
240
|
-
except TypeError:
|
|
241
|
-
# Codelist isn't weakly referenceable — return uncached.
|
|
242
|
-
pass
|
|
243
|
-
return mapping
|
|
209
|
+
# Fast path: direct SQL projection (LazyCodelist), cached at source.
|
|
210
|
+
return cast("dict[str, str]", code_map(lang=lang))
|
|
211
|
+
# Fallback: in-memory codelist iteration (no SQL, cheap).
|
|
212
|
+
codes = cast("Any", cl).codes
|
|
213
|
+
return {c.id: c.name.get(lang) or c.id for c in codes}
|
|
244
214
|
|
|
245
215
|
|
|
246
216
|
def _label_view(
|
|
@@ -298,9 +268,11 @@ def label(
|
|
|
298
268
|
appends a ``{col}{suffix}`` column with the code label in ``lang``.
|
|
299
269
|
Codes with no label fall back to the code id.
|
|
300
270
|
|
|
301
|
-
Label maps are cached per
|
|
302
|
-
|
|
303
|
-
|
|
271
|
+
Label maps are cached per (codelist instance, lang) at the source
|
|
272
|
+
(`LazyCodelist.code_map`) with a single-flight build lock, so the
|
|
273
|
+
first caller builds the map and concurrent callers share it rather
|
|
274
|
+
than each re-querying. A fresh ingest produces a new codelist
|
|
275
|
+
instance with a fresh cache, so re-ingestion is automatically correct.
|
|
304
276
|
|
|
305
277
|
Example:
|
|
306
278
|
labelled = slpl.label(pl.scan_csv("data.csv"), dsd, lang="en").collect()
|
|
@@ -92,8 +92,10 @@ class LazyCodelist:
|
|
|
92
92
|
"""
|
|
93
93
|
|
|
94
94
|
__slots__ = (
|
|
95
|
-
"__weakref__",
|
|
95
|
+
"__weakref__",
|
|
96
96
|
"_agency",
|
|
97
|
+
"_code_map_cache",
|
|
98
|
+
"_code_map_lock",
|
|
97
99
|
"_conn",
|
|
98
100
|
"_description",
|
|
99
101
|
"_id",
|
|
@@ -123,6 +125,13 @@ class LazyCodelist:
|
|
|
123
125
|
self._name = name
|
|
124
126
|
self._description = description
|
|
125
127
|
self._is_final = is_final
|
|
128
|
+
# Per-instance, per-language label-map cache + single-flight build
|
|
129
|
+
# lock (#155). The lock collapses the thundering herd: N threads
|
|
130
|
+
# asking the same instance for the same lang run the SQL once, not
|
|
131
|
+
# N times. Cache lifetime is the instance's — a fresh ingest yields
|
|
132
|
+
# a new LazyCodelist, so re-ingestion is automatically correct.
|
|
133
|
+
self._code_map_cache: dict[str, dict[str, str]] = {}
|
|
134
|
+
self._code_map_lock = threading.Lock()
|
|
126
135
|
|
|
127
136
|
# ── MaintainableArtefact-compatible properties ────────────────────────
|
|
128
137
|
|
|
@@ -393,10 +402,27 @@ class LazyCodelist:
|
|
|
393
402
|
# ── Direct SQL helpers ───────────────────────────────────────────────
|
|
394
403
|
|
|
395
404
|
def code_map(self, *, lang: str) -> dict[str, str]:
|
|
396
|
-
"""Direct SQL: ``{code_id: label}`` without creating Code objects.
|
|
397
|
-
from sdmxlib.storage.readers import code_map # noqa: PLC0415
|
|
405
|
+
"""Direct SQL: ``{code_id: label}`` without creating Code objects.
|
|
398
406
|
|
|
399
|
-
|
|
407
|
+
Memoised per instance per ``lang`` with a single-flight build lock
|
|
408
|
+
(#155): the first caller runs the SQL projection, concurrent
|
|
409
|
+
callers for the same ``lang`` block on the lock and then return the
|
|
410
|
+
hot cache rather than each re-running the query. The returned dict
|
|
411
|
+
is the cached instance — treat it as read-only.
|
|
412
|
+
"""
|
|
413
|
+
cached = self._code_map_cache.get(lang)
|
|
414
|
+
if cached is not None:
|
|
415
|
+
return cached
|
|
416
|
+
with self._code_map_lock:
|
|
417
|
+
# Double-checked: a prior holder may have built it while we waited.
|
|
418
|
+
cached = self._code_map_cache.get(lang)
|
|
419
|
+
if cached is not None:
|
|
420
|
+
return cached
|
|
421
|
+
from sdmxlib.storage.readers import code_map # noqa: PLC0415
|
|
422
|
+
|
|
423
|
+
mapping = code_map(self._conn, self._urn, lang=lang)
|
|
424
|
+
self._code_map_cache[lang] = mapping
|
|
425
|
+
return mapping
|
|
400
426
|
|
|
401
427
|
def materialize(self) -> Codelist:
|
|
402
428
|
"""Load all codes and return a real `Codelist`."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|