sdmxlib 0.69.0__tar.gz → 0.70.0__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.69.0 → sdmxlib-0.70.0}/PKG-INFO +18 -13
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/README.md +7 -2
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/pyproject.toml +12 -12
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/pyproject.toml.orig +24 -25
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/__init__.py +2 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/__init__.py +68 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_structure_dto.py +20 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_structure_ingest.py +2 -2
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/_scan.py +3 -2
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/data_store.py +5 -5
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/sql.py +9 -4
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/rollup.py +2 -2
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/validation.py +27 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/query.py +39 -10
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/raw.py +3 -1
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/session.py +63 -6
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/sql.py +1 -1
- sdmxlib-0.70.0/src/sdmxlib/storage/_parsed.py +104 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/readers.py +17 -4
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/resolve.py +9 -2
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/writers.py +3 -2
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/_duckdb.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/_singleflight.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/_time.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/_freshness.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/client.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/federated.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/policy.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/providers.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/registry.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/session.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/builders/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/builders/contentconstraint.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/builders/metadata_annotation.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/data_store.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_arrow.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_attachment.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_coerce.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_index.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_positions.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_refuse.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/data_format.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/gaps.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_csv/writer.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_data_common.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_dto.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_header.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_losses.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_periods.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_resolve.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/data.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/data_v1.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/data_v2.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/structure_values.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/writer.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json_1_0/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json_1_0/writer.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_allowed_children.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_closed_world.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_common.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_header.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_losses.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_v3_reader.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_v3_writer.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/_providers.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/_synthetic.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml31/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml31/namespaces.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml31/reader.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml31/writer.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/registry.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_binding.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_container.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_invariants.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_items.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_label_match.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_rebind.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_referenceable.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_refshape.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_resolvers.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_rules.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_tree.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/annotations.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/base.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/binding.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/category.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/codelist.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/collections.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/concept.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/constraint.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/convert.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/dataflow.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/datastructure.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/errors.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/expr.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/header.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/hierarchy.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/in_memory_registry.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/istring.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/link.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/mapping.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/message.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/metadata_provision.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/metadataflow.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/metadataset.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/metadatastructure.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/organisation.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/provision.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/ref.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/registry.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/representation.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/rest_api.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/urn.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/valuelist.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/version.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/version_rewrite.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/polars.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/py.typed +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_closure.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_common.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_compile.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_edges.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_execute.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_expr.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_joins.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_plan.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_pushdown.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_rebase.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_rest.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/accessors.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/observations.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/schema.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/rest.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/__init__.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/_checked.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/_kinds.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/_projections.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/lazy.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/schema.py +0 -0
- {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/versions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sdmxlib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.70.0
|
|
4
4
|
Summary: SDMX structural metadata library for Python
|
|
5
5
|
Keywords: sdmx,statistics,metadata,datastructure
|
|
6
6
|
Author: gabrielgellner
|
|
@@ -13,18 +13,18 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
15
15
|
Requires-Dist: attrs>=26.1.0
|
|
16
|
-
Requires-Dist: duckdb>=
|
|
16
|
+
Requires-Dist: duckdb>=2.0.0
|
|
17
17
|
Requires-Dist: httpx>=0.28.1
|
|
18
|
-
Requires-Dist: lxml>=6.
|
|
19
|
-
Requires-Dist: msgspec>=0.
|
|
20
|
-
Requires-Dist: polars>=
|
|
21
|
-
Requires-Dist: pyarrow>=
|
|
22
|
-
Requires-Dist: typing-extensions>=4.
|
|
18
|
+
Requires-Dist: lxml>=6.1.3
|
|
19
|
+
Requires-Dist: msgspec>=0.22.0
|
|
20
|
+
Requires-Dist: polars>=2.0.0
|
|
21
|
+
Requires-Dist: pyarrow>=26.0.0
|
|
22
|
+
Requires-Dist: typing-extensions>=4.16.0
|
|
23
23
|
Requires-Python: >=3.14
|
|
24
|
-
Project-URL: Homepage, https://
|
|
25
|
-
Project-URL: Repository, https://
|
|
26
|
-
Project-URL: Documentation, https://
|
|
27
|
-
Project-URL: Changelog, https://
|
|
24
|
+
Project-URL: Homepage, https://github.com/StatCat-Suite/sdmxlib
|
|
25
|
+
Project-URL: Repository, https://github.com/StatCat-Suite/sdmxlib
|
|
26
|
+
Project-URL: Documentation, https://statcat-suite.github.io/sdmxlib
|
|
27
|
+
Project-URL: Changelog, https://github.com/StatCat-Suite/sdmxlib/blob/main/CHANGELOG.md
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
30
|
# sdmxlib
|
|
@@ -120,7 +120,7 @@ library's actual surface. Point your assistant at them.
|
|
|
120
120
|
## Upgrading
|
|
121
121
|
|
|
122
122
|
sdmxlib is pre-stability and breaks cleanly rather than carrying two spellings
|
|
123
|
-
of the same idea. [Upgrading](https://
|
|
123
|
+
of the same idea. [Upgrading](https://statcat-suite.github.io/sdmxlib/upgrading/)
|
|
124
124
|
lists every breaking change since 0.48, with the old and new spelling of each —
|
|
125
125
|
`sl.Registry` is now `sl.LocalRegistry`, `get()` takes a URN and a resolution
|
|
126
126
|
depth, the four query DSLs are one path language, and every model class is
|
|
@@ -128,7 +128,7 @@ frozen.
|
|
|
128
128
|
|
|
129
129
|
## Documentation
|
|
130
130
|
|
|
131
|
-
See the [docs](https://
|
|
131
|
+
See the [docs](https://statcat-suite.github.io/sdmxlib/) for the full cookbook,
|
|
132
132
|
including:
|
|
133
133
|
|
|
134
134
|
- Connecting to registries and choosing a resolution depth
|
|
@@ -148,3 +148,8 @@ just test # unit tests
|
|
|
148
148
|
just test-integration # end-to-end tests, replayed from recorded cassettes
|
|
149
149
|
just ci # the full gate: lint, typecheck, unit, docs, integration
|
|
150
150
|
```
|
|
151
|
+
|
|
152
|
+
Issues are tracked on [GitHub](https://github.com/StatCat-Suite/sdmxlib/issues).
|
|
153
|
+
The project started on GitLab, so a bare `#492` in code, comments or docs means
|
|
154
|
+
an issue in the [GitLab tracker](https://gitlab.com/pinax-suite/sdmxlib/-/issues),
|
|
155
|
+
which stays readable as an archive. GitHub issues are written `GH-12`.
|
|
@@ -91,7 +91,7 @@ library's actual surface. Point your assistant at them.
|
|
|
91
91
|
## Upgrading
|
|
92
92
|
|
|
93
93
|
sdmxlib is pre-stability and breaks cleanly rather than carrying two spellings
|
|
94
|
-
of the same idea. [Upgrading](https://
|
|
94
|
+
of the same idea. [Upgrading](https://statcat-suite.github.io/sdmxlib/upgrading/)
|
|
95
95
|
lists every breaking change since 0.48, with the old and new spelling of each —
|
|
96
96
|
`sl.Registry` is now `sl.LocalRegistry`, `get()` takes a URN and a resolution
|
|
97
97
|
depth, the four query DSLs are one path language, and every model class is
|
|
@@ -99,7 +99,7 @@ frozen.
|
|
|
99
99
|
|
|
100
100
|
## Documentation
|
|
101
101
|
|
|
102
|
-
See the [docs](https://
|
|
102
|
+
See the [docs](https://statcat-suite.github.io/sdmxlib/) for the full cookbook,
|
|
103
103
|
including:
|
|
104
104
|
|
|
105
105
|
- Connecting to registries and choosing a resolution depth
|
|
@@ -119,3 +119,8 @@ just test # unit tests
|
|
|
119
119
|
just test-integration # end-to-end tests, replayed from recorded cassettes
|
|
120
120
|
just ci # the full gate: lint, typecheck, unit, docs, integration
|
|
121
121
|
```
|
|
122
|
+
|
|
123
|
+
Issues are tracked on [GitHub](https://github.com/StatCat-Suite/sdmxlib/issues).
|
|
124
|
+
The project started on GitLab, so a bare `#492` in code, comments or docs means
|
|
125
|
+
an issue in the [GitLab tracker](https://gitlab.com/pinax-suite/sdmxlib/-/issues),
|
|
126
|
+
which stays readable as an archive. GitHub issues are written `GH-12`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sdmxlib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.70.0"
|
|
4
4
|
description = "SDMX structural metadata library for Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -20,13 +20,13 @@ classifiers = [
|
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
22
|
"attrs>=26.1.0",
|
|
23
|
-
"duckdb>=
|
|
23
|
+
"duckdb>=2.0.0",
|
|
24
24
|
"httpx>=0.28.1",
|
|
25
|
-
"lxml>=6.
|
|
26
|
-
"msgspec>=0.
|
|
27
|
-
"polars>=
|
|
28
|
-
"pyarrow>=
|
|
29
|
-
"typing-extensions>=4.
|
|
25
|
+
"lxml>=6.1.3",
|
|
26
|
+
"msgspec>=0.22.0",
|
|
27
|
+
"polars>=2.0.0",
|
|
28
|
+
"pyarrow>=26.0.0",
|
|
29
|
+
"typing-extensions>=4.16.0",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.license]
|
|
@@ -37,10 +37,10 @@ name = "gabrielgellner"
|
|
|
37
37
|
email = "gabrielgellner@gmail.com"
|
|
38
38
|
|
|
39
39
|
[project.urls]
|
|
40
|
-
Homepage = "https://
|
|
41
|
-
Repository = "https://
|
|
42
|
-
Documentation = "https://
|
|
43
|
-
Changelog = "https://
|
|
40
|
+
Homepage = "https://github.com/StatCat-Suite/sdmxlib"
|
|
41
|
+
Repository = "https://github.com/StatCat-Suite/sdmxlib"
|
|
42
|
+
Documentation = "https://statcat-suite.github.io/sdmxlib"
|
|
43
|
+
Changelog = "https://github.com/StatCat-Suite/sdmxlib/blob/main/CHANGELOG.md"
|
|
44
44
|
|
|
45
45
|
[build-system]
|
|
46
46
|
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
@@ -105,7 +105,7 @@ dev = [
|
|
|
105
105
|
"types-PyYAML>=6.0.12",
|
|
106
106
|
"mkdocs-material>=9.7.6",
|
|
107
107
|
"mkdocstrings[python]>=1.0.3",
|
|
108
|
-
"polars>=
|
|
108
|
+
"polars>=2.0.0",
|
|
109
109
|
"pytest>=9.0.2",
|
|
110
110
|
"jsonschema[format-nongpl]>=4.21.0",
|
|
111
111
|
"pytest-benchmark>=4.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sdmxlib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.70.0"
|
|
4
4
|
description = "SDMX structural metadata library for Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Apache-2.0" }
|
|
@@ -17,37 +17,36 @@ classifiers = [
|
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
19
|
"attrs>=26.1.0",
|
|
20
|
-
# >=
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
# taken on their own merits. It is **not** a fix for #400: 1.5.0, 1.5.1,
|
|
26
|
-
# 1.5.2, 1.5.3, 1.5.4, 1.5.5 and 1.6.0.dev379 all read the affected bundle
|
|
27
|
-
# identically, so that defect is in the file rather than in the reader.
|
|
28
|
-
#
|
|
29
|
-
# >=1.5.6: test_annotation_subtree_read (#426) asserts the annotation
|
|
30
|
-
# subtree read uses the owner index. 1.5.5's planner chooses a SEQ_SCAN
|
|
31
|
-
# over `owner_artefact_urn = ?` (502 rows read) and 1.5.6 uses the index,
|
|
32
|
-
# so on 1.5.5 the read scans every annotation row.
|
|
33
|
-
"duckdb>=1.5.6",
|
|
20
|
+
# >=2.0: an `execute()` result has no `pl()` any more, so frames come from
|
|
21
|
+
# a relation (`sql().pl()`), and a relation call on a handle holding an
|
|
22
|
+
# unfinished result raises rather than clobbering it (`session.completed`).
|
|
23
|
+
# The tests assert 2.0's profile JSON, error text and sort costs.
|
|
24
|
+
"duckdb>=2.0.0",
|
|
34
25
|
"httpx>=0.28.1",
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
26
|
+
# >=6.1: `resolve_entities` defaults to 'internal' (CVE-2026-41066), so no
|
|
27
|
+
# parse expands an external entity.
|
|
28
|
+
"lxml>=6.1.3",
|
|
29
|
+
# >=0.22: on 3.14 an older msgspec cannot resolve a Struct field's forward
|
|
30
|
+
# reference (`list[SentinelValueJson]` in `_structure_dto`), and importing
|
|
31
|
+
# sdmxlib raises NameError.
|
|
32
|
+
"msgspec>=0.22.0",
|
|
33
|
+
# >=2.0: a cast no longer parses text into a date, so `_typed` parses it
|
|
34
|
+
# (`query._converted`); and `collect()` streams, so an order is sorted for.
|
|
35
|
+
"polars>=2.0.0",
|
|
36
|
+
# >=22 is the first with Python 3.14 wheels; 26 is what the suite runs.
|
|
37
|
+
"pyarrow>=26.0.0",
|
|
39
38
|
# PEP 728's `closed=True` TypedDict, which `typing` gains only in 3.15: a
|
|
40
39
|
# TypedDict unpacked into a constructor (`**read_reference_attrs(el)`) must
|
|
41
40
|
# be closed for a checker to know it holds no other key. httpx already
|
|
42
41
|
# brings it in, through anyio; declared because we now import it.
|
|
43
|
-
"typing-extensions>=4.
|
|
42
|
+
"typing-extensions>=4.16.0",
|
|
44
43
|
]
|
|
45
44
|
|
|
46
45
|
[project.urls]
|
|
47
|
-
Homepage = "https://
|
|
48
|
-
Repository = "https://
|
|
49
|
-
Documentation = "https://
|
|
50
|
-
Changelog = "https://
|
|
46
|
+
Homepage = "https://github.com/StatCat-Suite/sdmxlib"
|
|
47
|
+
Repository = "https://github.com/StatCat-Suite/sdmxlib"
|
|
48
|
+
Documentation = "https://statcat-suite.github.io/sdmxlib"
|
|
49
|
+
Changelog = "https://github.com/StatCat-Suite/sdmxlib/blob/main/CHANGELOG.md"
|
|
51
50
|
|
|
52
51
|
[build-system]
|
|
53
52
|
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
@@ -119,7 +118,7 @@ dev = [
|
|
|
119
118
|
"types-PyYAML>=6.0.12",
|
|
120
119
|
"mkdocs-material>=9.7.6",
|
|
121
120
|
"mkdocstrings[python]>=1.0.3",
|
|
122
|
-
"polars>=
|
|
121
|
+
"polars>=2.0.0",
|
|
123
122
|
"pytest>=9.0.2",
|
|
124
123
|
"jsonschema[format-nongpl]>=4.21.0",
|
|
125
124
|
"pytest-benchmark>=4.0.0",
|
|
@@ -54,6 +54,7 @@ from sdmxlib.model.validation import (
|
|
|
54
54
|
ReferenceIssue,
|
|
55
55
|
ReferenceValidationError,
|
|
56
56
|
SdmxConformanceError,
|
|
57
|
+
SdmxErrorMessage,
|
|
57
58
|
SdmxParseError,
|
|
58
59
|
ValidationError,
|
|
59
60
|
ValidationIssue,
|
|
@@ -380,6 +381,7 @@ __all__ = [
|
|
|
380
381
|
"RowKind",
|
|
381
382
|
"SchemaInspector",
|
|
382
383
|
"SdmxConformanceError",
|
|
384
|
+
"SdmxErrorMessage",
|
|
383
385
|
"SdmxParseError",
|
|
384
386
|
"SdmxRef",
|
|
385
387
|
"SdmxUrn",
|
|
@@ -58,6 +58,7 @@ from sdmxlib.model.registry import SupportsAddAll
|
|
|
58
58
|
from sdmxlib.model.validation import (
|
|
59
59
|
LossyReadError,
|
|
60
60
|
SdmxConformanceError,
|
|
61
|
+
SdmxErrorMessage,
|
|
61
62
|
SdmxParseError,
|
|
62
63
|
ValidationError,
|
|
63
64
|
check_metadata_serialisable,
|
|
@@ -163,6 +164,9 @@ def parse(
|
|
|
163
164
|
refusals and carries them as ``.refused``, with everything that
|
|
164
165
|
was read as ``.message``. When ``lossless`` and anything was
|
|
165
166
|
lost, `LossyReadError`, carrying ``.lost`` and ``.message``.
|
|
167
|
+
When the document is an SDMX error message (``<mes:Error>``,
|
|
168
|
+
``{"errors": [...]}``), `SdmxErrorMessage`, carrying each
|
|
169
|
+
error's code and text as ``.errors``.
|
|
166
170
|
"""
|
|
167
171
|
try:
|
|
168
172
|
# Read straight into an in-memory registry: its references are bound
|
|
@@ -201,6 +205,7 @@ def parse(
|
|
|
201
205
|
def _dispatch(data: bytes, *, version: str | None) -> StructureMessage | MetadataMessage:
|
|
202
206
|
"""Detect format and message kind, then hand off to the matching reader."""
|
|
203
207
|
detected = version or _detect_version(data)
|
|
208
|
+
_refuse_error_message(data, detected)
|
|
204
209
|
kind = _detect_message_kind(data, detected)
|
|
205
210
|
|
|
206
211
|
if kind == "metadata":
|
|
@@ -334,6 +339,69 @@ _MESSAGE_NS_VERSION = {_MES_21: "2.1", _MES_30: "3.0", _MES_31: "3.1"}
|
|
|
334
339
|
#: Local name of the SDMX-ML root element that carries reported metadata.
|
|
335
340
|
_METADATA_ROOT = "GenericMetadata"
|
|
336
341
|
|
|
342
|
+
#: Local name of the SDMX-ML root element of an error message, and of each error in it.
|
|
343
|
+
_ERROR_ROOT = "Error"
|
|
344
|
+
_ERROR_ENTRY = "ErrorMessage"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class _ErrorProbeEntry(msgspec.Struct):
|
|
348
|
+
"""One SDMX-JSON error. ``message`` is not in the schema; FMR 12.4 writes it."""
|
|
349
|
+
|
|
350
|
+
code: int | float | str
|
|
351
|
+
title: str | None = None
|
|
352
|
+
detail: str | None = None
|
|
353
|
+
message: str | None = None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class _ErrorProbe(msgspec.Struct):
|
|
357
|
+
"""Just enough of an SDMX-JSON document to tell an error message from the rest."""
|
|
358
|
+
|
|
359
|
+
errors: list[_ErrorProbeEntry] | None = None
|
|
360
|
+
data: msgspec.Raw | None = None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _refuse_error_message(data: bytes, version: str) -> None:
|
|
364
|
+
"""Raise `SdmxErrorMessage` if *data* is an SDMX error message (GH-31).
|
|
365
|
+
|
|
366
|
+
SDMX-ML: the root is ``<mes:Error>``. SDMX-JSON: ``errors`` is non-empty
|
|
367
|
+
and there is no ``data``, since a message may also carry errors beside
|
|
368
|
+
what it answers. A JSON payload this probe cannot decode is left to the
|
|
369
|
+
reader, so the reader's own error is the one raised.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
SdmxErrorMessage: Carrying each error's code and text.
|
|
373
|
+
"""
|
|
374
|
+
if version == "json":
|
|
375
|
+
try:
|
|
376
|
+
probe = msgspec.json.decode(data, type=_ErrorProbe)
|
|
377
|
+
except msgspec.MsgspecError:
|
|
378
|
+
return
|
|
379
|
+
if probe.errors and probe.data is None:
|
|
380
|
+
raise SdmxErrorMessage(tuple((_code(e.code), e.title or e.detail or e.message or "") for e in probe.errors))
|
|
381
|
+
return
|
|
382
|
+
root = _xml_root(data)
|
|
383
|
+
if root is None or root.localname != _ERROR_ROOT or root.namespace not in _MESSAGE_NS_VERSION:
|
|
384
|
+
return
|
|
385
|
+
entries = [
|
|
386
|
+
el for el in etree.fromstring(data) if isinstance(el.tag, str) and etree.QName(el).localname == _ERROR_ENTRY
|
|
387
|
+
]
|
|
388
|
+
raise SdmxErrorMessage(tuple((el.get("code", ""), _first_text(el)) for el in entries))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _code(code: float | str) -> str:
|
|
392
|
+
"""An SDMX-JSON error code as SDMX-ML writes it: ``404``, not ``404.0``."""
|
|
393
|
+
if isinstance(code, float) and code.is_integer():
|
|
394
|
+
return str(int(code))
|
|
395
|
+
return str(code)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _first_text(entry: etree._Element) -> str:
|
|
399
|
+
"""The first ``<com:Text>`` of an ``<ErrorMessage>``; a message may give several languages."""
|
|
400
|
+
for child in entry:
|
|
401
|
+
if isinstance(child.tag, str) and etree.QName(child).localname == "Text":
|
|
402
|
+
return (child.text or "").strip()
|
|
403
|
+
return ""
|
|
404
|
+
|
|
337
405
|
|
|
338
406
|
def _xml_root(data: bytes) -> etree.QName | None:
|
|
339
407
|
"""The qualified name of the XML root element, or None if *data* is not XML.
|
|
@@ -367,6 +367,14 @@ class Format(WireStruct, rename=_camel_rename):
|
|
|
367
367
|
end_time: str | None = None
|
|
368
368
|
#: Values with a special meaning (#422) — a text format's alone.
|
|
369
369
|
sentinel_values: list[SentinelValueJson] | None = None
|
|
370
|
+
#: Read tolerance: SDMX-JSON 1.0's name for `data_type` (GH-30), moved
|
|
371
|
+
#: there as it is read so nothing downstream sees two spellings.
|
|
372
|
+
text_type: str | None = None
|
|
373
|
+
|
|
374
|
+
def __post_init__(self) -> None:
|
|
375
|
+
"""Read 1.0's `text_type` as `data_type`."""
|
|
376
|
+
if self.text_type is not None and self.data_type is None:
|
|
377
|
+
self.data_type, self.text_type = self.text_type, None
|
|
370
378
|
|
|
371
379
|
|
|
372
380
|
class SentinelValueJson(WireStruct, rename=_camel_rename):
|
|
@@ -404,6 +412,9 @@ class Representation(WireStruct, rename=_camel_rename):
|
|
|
404
412
|
#: or ``"unbounded"``.
|
|
405
413
|
min_occurs: int | None = None
|
|
406
414
|
max_occurs: int | str | None = None
|
|
415
|
+
#: Read tolerance: SDMX-JSON 1.0's name for `format` (GH-30), moved there
|
|
416
|
+
#: as it is read, before the ``oneOf`` below counts it.
|
|
417
|
+
text_format: Format | None = None
|
|
407
418
|
|
|
408
419
|
def __post_init__(self) -> None:
|
|
409
420
|
"""Every representation type's ``oneOf``: an enumeration or a format (#474).
|
|
@@ -411,6 +422,8 @@ class Representation(WireStruct, rename=_camel_rename):
|
|
|
411
422
|
Raises:
|
|
412
423
|
ValueError: If it has neither, or both.
|
|
413
424
|
"""
|
|
425
|
+
if self.text_format is not None and self.format is None:
|
|
426
|
+
self.format, self.text_format = self.text_format, None
|
|
414
427
|
# Tested inline before `one_of` builds its arguments: this runs for
|
|
415
428
|
# every component and concept, and the call alone was 2-5% of a read.
|
|
416
429
|
if (self.enumeration is None) == (self.format is None):
|
|
@@ -871,6 +884,11 @@ class AttributeRelationship(WireStruct, rename=_camel_rename):
|
|
|
871
884
|
are_dimensions_optional: list[bool] | None = None
|
|
872
885
|
group: str | None = None
|
|
873
886
|
dataflow: EmptyObject | None = None
|
|
887
|
+
#: Read tolerance, SDMX-JSON 1.0's two spellings (GH-30): ``none`` is
|
|
888
|
+
#: `dataflow` (2.1's ``<str:None/>``, a dataset-level attribute), and
|
|
889
|
+
#: ``primaryMeasure`` is `observation`, naming the measure 2.1 relates it to.
|
|
890
|
+
none: EmptyObject | None = None
|
|
891
|
+
primary_measure: str | None = None
|
|
874
892
|
|
|
875
893
|
def __post_init__(self) -> None:
|
|
876
894
|
"""The schema's ``oneOf`` (#474).
|
|
@@ -884,6 +902,8 @@ class AttributeRelationship(WireStruct, rename=_camel_rename):
|
|
|
884
902
|
dimensions=self.dimensions,
|
|
885
903
|
group=self.group,
|
|
886
904
|
observation=self.observation,
|
|
905
|
+
none=self.none,
|
|
906
|
+
primaryMeasure=self.primary_measure,
|
|
887
907
|
)
|
|
888
908
|
|
|
889
909
|
|
|
@@ -540,9 +540,9 @@ def from_attachment(
|
|
|
540
540
|
# Required by the schema; one a document omits varies with nothing,
|
|
541
541
|
# read as dataset-level as the SDMX-ML readers read it (#421).
|
|
542
542
|
return AttachmentLevel.DATASET, None, frozenset(), None
|
|
543
|
-
if ar.observation is not None:
|
|
543
|
+
if ar.observation is not None or ar.primary_measure is not None:
|
|
544
544
|
return AttachmentLevel.OBSERVATION, None, frozenset(), None
|
|
545
|
-
if ar.dataflow is not None:
|
|
545
|
+
if ar.dataflow is not None or ar.none is not None:
|
|
546
546
|
return AttachmentLevel.DATASET, None, frozenset(), None
|
|
547
547
|
if ar.dimensions:
|
|
548
548
|
# `areDimensionsOptional` is parallel to `dimensions` (#422).
|
|
@@ -38,6 +38,7 @@ from sdmxlib.model.datastructure import DataStructure
|
|
|
38
38
|
from sdmxlib.model.ref import Ref
|
|
39
39
|
from sdmxlib.model.urn import SdmxUrn
|
|
40
40
|
from sdmxlib.query.observations import ObsQuery
|
|
41
|
+
from sdmxlib.session import completed
|
|
41
42
|
|
|
42
43
|
#: The catalog every scan's relation lives in — see the module docstring.
|
|
43
44
|
SCANS = "sdmx_scans"
|
|
@@ -99,11 +100,11 @@ def scan_frame_into(
|
|
|
99
100
|
if column in frame.columns
|
|
100
101
|
)
|
|
101
102
|
name = relation.rpartition(".")[2]
|
|
102
|
-
plane.connection.register(name, typed.to_arrow())
|
|
103
|
+
completed(plane.connection).register(name, typed.to_arrow())
|
|
103
104
|
try:
|
|
104
105
|
plane.connection.execute(f"CREATE TABLE {relation} AS SELECT * FROM {name}")
|
|
105
106
|
finally:
|
|
106
|
-
plane.connection.unregister(name)
|
|
107
|
+
completed(plane.connection).unregister(name)
|
|
107
108
|
return plane.obs(binding, dsd=dsd)
|
|
108
109
|
|
|
109
110
|
|
|
@@ -31,7 +31,7 @@ from sdmxlib.model.ref import Ref
|
|
|
31
31
|
from sdmxlib.model.representation import Facet
|
|
32
32
|
from sdmxlib.query.observations import Observations, ObsQuery, Sidecar
|
|
33
33
|
from sdmxlib.query.query import build
|
|
34
|
-
from sdmxlib.session import SessionFactory
|
|
34
|
+
from sdmxlib.session import SessionFactory, completed
|
|
35
35
|
|
|
36
36
|
if TYPE_CHECKING:
|
|
37
37
|
# `local.registry` imports this module at runtime: a cycle.
|
|
@@ -676,10 +676,10 @@ class DuckDBDataStore(SessionFactory):
|
|
|
676
676
|
raise ValueError(msg)
|
|
677
677
|
|
|
678
678
|
conn = self.connection
|
|
679
|
-
#
|
|
679
|
+
# Registered for this session, not created as a view: a view is
|
|
680
|
+
# catalog state, and DuckDB refuses one over a Python object.
|
|
680
681
|
view_name = f"__sdmxlib_write_obs_{id(source):x}"
|
|
681
|
-
|
|
682
|
-
rel.create_view(view_name, replace=True)
|
|
682
|
+
completed(conn).register(view_name, _as_relation(conn, source))
|
|
683
683
|
try:
|
|
684
684
|
target = f"{lake_alias}.{schema}.{table}"
|
|
685
685
|
conn.execute(f"CREATE SCHEMA IF NOT EXISTS {lake_alias}.{schema}")
|
|
@@ -696,7 +696,7 @@ class DuckDBDataStore(SessionFactory):
|
|
|
696
696
|
msg = f"Unknown mode: {mode!r} (expected 'overwrite' or 'append')"
|
|
697
697
|
raise ValueError(msg)
|
|
698
698
|
finally:
|
|
699
|
-
conn.
|
|
699
|
+
completed(conn).unregister(view_name)
|
|
700
700
|
# A rewrite can change the target table's schema; drop cached column
|
|
701
701
|
# lists so the inspector (internal query path + custom SQL) re-reflects.
|
|
702
702
|
# The target may be cached under a differently-qualified name than the
|
|
@@ -52,6 +52,7 @@ import duckdb
|
|
|
52
52
|
from attrs import frozen
|
|
53
53
|
|
|
54
54
|
from sdmxlib.model.binding import DataflowBinding
|
|
55
|
+
from sdmxlib.session import cursor_for
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
class SidecarNotUniqueError(ValueError):
|
|
@@ -152,7 +153,9 @@ class SchemaInspector:
|
|
|
152
153
|
cached = self._columns_cache.get(table)
|
|
153
154
|
if cached is not None:
|
|
154
155
|
return cached
|
|
155
|
-
|
|
156
|
+
# On this thread's handle, never `_conn` itself: concurrent
|
|
157
|
+
# `execute()` on one handle overwrite each other's rows (#75).
|
|
158
|
+
rows = cursor_for(self._conn).execute(f"DESCRIBE {table}").fetchall()
|
|
156
159
|
described = tuple((str(r[0]), str(r[1])) for r in rows)
|
|
157
160
|
self._columns_cache[table] = described
|
|
158
161
|
return described
|
|
@@ -190,9 +193,11 @@ class SchemaInspector:
|
|
|
190
193
|
return cached
|
|
191
194
|
tuple_sql = ", ".join(keys)
|
|
192
195
|
not_null = " AND ".join(f"{k} IS NOT NULL" for k in keys)
|
|
193
|
-
row =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
row = (
|
|
197
|
+
cursor_for(self._conn)
|
|
198
|
+
.execute(f"SELECT count(*), count(DISTINCT ({tuple_sql})) FROM {table} WHERE {not_null}")
|
|
199
|
+
.fetchone()
|
|
200
|
+
)
|
|
196
201
|
counts = (int(row[0]), int(row[1])) if row else (0, 0)
|
|
197
202
|
self._key_counts_cache[cache_key] = counts
|
|
198
203
|
return counts
|
|
@@ -16,7 +16,7 @@ import attrs
|
|
|
16
16
|
import duckdb
|
|
17
17
|
import polars as pl
|
|
18
18
|
|
|
19
|
-
from sdmxlib.session import cursor_for
|
|
19
|
+
from sdmxlib.session import completed, cursor_for, frame_of
|
|
20
20
|
from sdmxlib.sql import SqlRelation
|
|
21
21
|
|
|
22
22
|
|
|
@@ -105,7 +105,7 @@ class AncestorMap:
|
|
|
105
105
|
def collect(self) -> pl.DataFrame:
|
|
106
106
|
"""Execute and return the mapping as a Polars frame."""
|
|
107
107
|
sql, params = self.to_sql()
|
|
108
|
-
return cursor_for(self._conn).
|
|
108
|
+
return frame_of(completed(cursor_for(self._conn)).sql(sql, params=params))
|
|
109
109
|
|
|
110
110
|
def to_dict(self) -> dict[str, str]:
|
|
111
111
|
"""Execute and return ``{path: rollup_path}``.
|
|
@@ -222,6 +222,33 @@ class SdmxConformanceError(SdmxParseError):
|
|
|
222
222
|
return cls("Structure", detail, refused=refused, message=message)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
class SdmxErrorMessage(SdmxParseError): # noqa: N818 - SDMX's own element name, `ErrorMessage`
|
|
226
|
+
"""Raised by `parse` when the document is an SDMX error message (GH-31).
|
|
227
|
+
|
|
228
|
+
A registry answers a query it cannot serve with an error message —
|
|
229
|
+
``<mes:Error>`` in SDMX-ML, ``{"errors": [...]}`` in SDMX-JSON — rather
|
|
230
|
+
than a structure or metadata message. `parse` used to read one as an empty
|
|
231
|
+
`StructureMessage`, so a saved "100 No Results Found" was indistinguishable
|
|
232
|
+
from a query that matched nothing. The REST client never gets this far: it
|
|
233
|
+
raises on the HTTP status first. This is for bytes read from a file.
|
|
234
|
+
|
|
235
|
+
A `SdmxParseError`, because the payload is still not the message asked
|
|
236
|
+
for; catching the base handles it with every other unusable document.
|
|
237
|
+
|
|
238
|
+
Attributes:
|
|
239
|
+
errors: One ``(code, text)`` pair per error the message reports, in
|
|
240
|
+
document order. The code is text, as SDMX-ML types it; the text
|
|
241
|
+
is the first one given, or empty when there is none.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
errors: tuple[tuple[str, str], ...]
|
|
245
|
+
|
|
246
|
+
def __init__(self, errors: tuple[tuple[str, str], ...]) -> None:
|
|
247
|
+
self.errors = errors
|
|
248
|
+
reported = "; ".join(f"{code} {text}".strip() for code, text in errors) or "no error given"
|
|
249
|
+
super().__init__(f"the document is an SDMX error message, not a structure or metadata message: {reported}")
|
|
250
|
+
|
|
251
|
+
|
|
225
252
|
class LossyReadError(SdmxParseError):
|
|
226
253
|
"""Raised by ``parse(..., lossless=True)`` when the message is not exactly the document (#422, #474).
|
|
227
254
|
|
|
@@ -73,7 +73,7 @@ from sdmxlib.query._expr import (
|
|
|
73
73
|
)
|
|
74
74
|
from sdmxlib.query._plan import Plan, scanned
|
|
75
75
|
from sdmxlib.query._pushdown import DUCKDB, Program, Push, plan_program
|
|
76
|
-
from sdmxlib.session import cursor_for
|
|
76
|
+
from sdmxlib.session import completed, cursor_for, frame_of
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def _partition_prefix(root: AnyCols, order: Sequence[OrderKey]) -> tuple[str, ...]:
|
|
@@ -127,7 +127,7 @@ def _stream_by_partition[R: RowKind, A](
|
|
|
127
127
|
# does not pull the next partition until this one is finished — which
|
|
128
128
|
# is exactly what keeps peak at one partition rather than their sum.
|
|
129
129
|
cursor = query._cursor().cursor()
|
|
130
|
-
yield cursor.
|
|
130
|
+
yield completed(cursor).sql(compiled.sql, params=list(compiled.params)).to_arrow_reader(chunk_rows)
|
|
131
131
|
|
|
132
132
|
stream = readers()
|
|
133
133
|
first = next(stream)
|
|
@@ -174,9 +174,9 @@ def _typed(frame: pl.DataFrame, root: AnyCols, columns: Mapping[str, str] | None
|
|
|
174
174
|
|
|
175
175
|
Raises:
|
|
176
176
|
ValueError: If a value cannot take its component's type — a code its
|
|
177
|
-
codelist does not list, text where the DSD declares a number
|
|
178
|
-
fast; the message names the way to the raw rows and
|
|
179
|
-
of every bad value.
|
|
177
|
+
codelist does not list, text where the DSD declares a number or
|
|
178
|
+
a date. Fail fast; the message names the way to the raw rows and
|
|
179
|
+
to the report of every bad value.
|
|
180
180
|
"""
|
|
181
181
|
dsd = getattr(root, "dsd", None)
|
|
182
182
|
if not isinstance(dsd, DataStructure):
|
|
@@ -193,7 +193,7 @@ def _typed(frame: pl.DataFrame, root: AnyCols, columns: Mapping[str, str] | None
|
|
|
193
193
|
if not casts:
|
|
194
194
|
return frame
|
|
195
195
|
try:
|
|
196
|
-
return frame.with_columns(
|
|
196
|
+
return frame.with_columns(_converted(name, frame.schema[name], dtype) for name, dtype in casts.items())
|
|
197
197
|
except pl.exceptions.PolarsError as exc:
|
|
198
198
|
msg = (
|
|
199
199
|
f"an observation column does not fit its component's type in {dsd.id}: {exc}. Results are "
|
|
@@ -203,6 +203,35 @@ def _typed(frame: pl.DataFrame, root: AnyCols, columns: Mapping[str, str] | None
|
|
|
203
203
|
raise ValueError(msg) from exc
|
|
204
204
|
|
|
205
205
|
|
|
206
|
+
#: `xs:boolean`'s lexical forms.
|
|
207
|
+
_BOOLEANS = {"true": True, "false": False, "1": True, "0": False}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _converted(name: str, stored: pl.DataType, dtype: pl.DataType) -> pl.Expr:
|
|
211
|
+
"""Column `name`, stored as `stored`, as `dtype` — parsed when it is text.
|
|
212
|
+
|
|
213
|
+
A cast is not a parse: polars 2.0 casts no text to a date, a datetime or a
|
|
214
|
+
time (1.x did dates only), and no version casts text to a boolean. A value
|
|
215
|
+
stored as ``VARCHAR`` — every observation column is — is parsed instead. A
|
|
216
|
+
datetime with an offset lands in UTC, and every datetime is naive UTC, the
|
|
217
|
+
store's convention (`sdmxlib._time`).
|
|
218
|
+
"""
|
|
219
|
+
column = pl.col(name)
|
|
220
|
+
if stored != pl.String():
|
|
221
|
+
return column.cast(dtype)
|
|
222
|
+
match dtype:
|
|
223
|
+
case pl.Date():
|
|
224
|
+
return column.str.to_date()
|
|
225
|
+
case pl.Datetime():
|
|
226
|
+
return column.str.to_datetime(time_unit=dtype.time_unit, time_zone="UTC").dt.replace_time_zone(None)
|
|
227
|
+
case pl.Time():
|
|
228
|
+
return column.str.to_time()
|
|
229
|
+
case pl.Boolean():
|
|
230
|
+
return column.replace_strict(_BOOLEANS, return_dtype=pl.Boolean)
|
|
231
|
+
case _:
|
|
232
|
+
return column.cast(dtype)
|
|
233
|
+
|
|
234
|
+
|
|
206
235
|
def _declared_components(dsd: DataStructure) -> frozenset[str]:
|
|
207
236
|
"""The components whose type the DSD states — see `_typed`."""
|
|
208
237
|
found = {
|
|
@@ -1613,7 +1642,7 @@ class Query[R: RowKind, A = object]:
|
|
|
1613
1642
|
their `Facet` declares.
|
|
1614
1643
|
"""
|
|
1615
1644
|
compiled = self.to_sql()
|
|
1616
|
-
frame = _typed(self._cursor().
|
|
1645
|
+
frame = _typed(frame_of(completed(self._cursor()).sql(compiled.sql, params=list(compiled.params))), self.root)
|
|
1617
1646
|
if not labels:
|
|
1618
1647
|
return frame
|
|
1619
1648
|
return _labelled(frame, self.root, labels, label_suffix)
|
|
@@ -1699,7 +1728,7 @@ class Query[R: RowKind, A = object]:
|
|
|
1699
1728
|
# on one connection would overwrite each other's row buffer (#75), and
|
|
1700
1729
|
# the reader holds this one until it is exhausted or closed.
|
|
1701
1730
|
cursor = self._cursor().cursor()
|
|
1702
|
-
return cursor.
|
|
1731
|
+
return completed(cursor).sql(compiled.sql, params=list(compiled.params)).to_arrow_reader(chunk_rows)
|
|
1703
1732
|
|
|
1704
1733
|
def _partition_values(self, partitions: tuple[str, ...]) -> list[tuple[object, ...]]:
|
|
1705
1734
|
"""Every partition, in key order — one small query before the stream.
|
|
@@ -2355,7 +2384,7 @@ class Select[R: RowKind, A = object]:
|
|
|
2355
2384
|
# component id -> the name it was projected under, so an aliased column
|
|
2356
2385
|
# is typed and labelled under the alias.
|
|
2357
2386
|
aliases = {item.projected_name: name for name, item in self.items if isinstance(item, AnyCol)}
|
|
2358
|
-
frame = _as_utc(cursor.
|
|
2387
|
+
frame = _as_utc(frame_of(completed(cursor).sql(compiled.sql, params=list(compiled.params))), self.items)
|
|
2359
2388
|
frame = _typed(frame, self._query.root, aliases)
|
|
2360
2389
|
if not labels:
|
|
2361
2390
|
return frame
|
|
@@ -2450,7 +2479,7 @@ class Select[R: RowKind, A = object]:
|
|
|
2450
2479
|
# on one connection would overwrite each other's row buffer (#75), and
|
|
2451
2480
|
# the reader holds this one until it is exhausted or closed.
|
|
2452
2481
|
cursor = self._query._cursor(self._query._projection_columns(self.items)).cursor()
|
|
2453
|
-
return cursor.
|
|
2482
|
+
return completed(cursor).sql(compiled.sql, params=list(compiled.params)).to_arrow_reader(chunk_rows)
|
|
2454
2483
|
|
|
2455
2484
|
@override
|
|
2456
2485
|
def __repr__(self) -> str:
|