sdmxlib 0.63.0__tar.gz → 0.64.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.63.0 → sdmxlib-0.64.0}/PKG-INFO +1 -1
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/pyproject.toml +2 -2
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/pyproject.toml.orig +11 -2
- {sdmxlib-0.63.0/src/sdmxlib/storage → sdmxlib-0.64.0/src/sdmxlib}/_time.py +12 -1
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/_freshness.py +1 -1
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/client.py +1 -1
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/_data_common.py +72 -60
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/data_v1.py +2 -3
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/data_v2.py +2 -3
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/codelist.py +26 -4
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_compile.py +24 -16
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_expr.py +85 -3
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/query.py +1 -1
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/session.py +37 -31
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/storage/lazy.py +13 -1
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/storage/readers.py +1 -1
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/storage/resolve.py +50 -14
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/storage/schema.py +1 -1
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/storage/writers.py +3 -3
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/README.md +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/_duckdb.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/_singleflight.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/federated.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/filters.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/policy.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/providers.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/query.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/registry.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/api/session.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/builders/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/builders/contentconstraint.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/builders/metadata_annotation.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/data_store.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/_coerce.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/_index.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/data_format.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/gaps.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_csv/writer.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/_dto.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/_resolve.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/_structure_dto.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/_structure_ingest.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/structure_values.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json/writer.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json_1_0/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_json_1_0/writer.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml/_common.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml/_v3_reader.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml/_v3_writer.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml21/_synthetic.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml31/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml31/namespaces.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml31/reader.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/formats/sdmx_ml31/writer.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/local/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/local/data_store.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/local/registry.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/local/sql.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/_interning.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/_items.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/_label_match.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/_refshape.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/annotations.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/base.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/binding.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/category.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/collections.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/concept.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/constraint.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/convert.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/dataflow.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/dataset.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/datastructure.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/errors.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/expr.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/hierarchy.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/in_memory_registry.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/istring.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/mapping.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/message.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/metadata_provision.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/metadataflow.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/metadataset.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/metadatastructure.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/organisation.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/provision.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/ref.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/registry.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/representation.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/rest_api.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/rollup.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/urn.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/validation.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/model/version.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/polars.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/py.typed +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_closure.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_common.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_edges.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_execute.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_joins.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_plan.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_pushdown.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_rebase.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/_rest.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/accessors.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/observations.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/raw.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/query/schema.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/rest.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/sql.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/storage/__init__.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/storage/_kinds.py +0 -0
- {sdmxlib-0.63.0 → sdmxlib-0.64.0}/src/sdmxlib/storage/_projections.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sdmxlib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.64.0"
|
|
4
4
|
description = "SDMX structural metadata library for Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -140,7 +140,7 @@ mutate_only_covered_lines = false
|
|
|
140
140
|
|
|
141
141
|
[dependency-groups]
|
|
142
142
|
dev = [
|
|
143
|
-
"basedpyright
|
|
143
|
+
"basedpyright==1.39.9",
|
|
144
144
|
"catppuccin[pygments]>=2.5.0",
|
|
145
145
|
"types-lxml>=2025.3.30",
|
|
146
146
|
"mkdocs-material>=9.7.6",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sdmxlib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.64.0"
|
|
4
4
|
description = "SDMX structural metadata library for Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Apache-2.0" }
|
|
@@ -220,7 +220,16 @@ dev = [
|
|
|
220
220
|
# ruff on `select = ["ALL"]` that is not hypothetical: ruff 0.16.0
|
|
221
221
|
# stabilised 12 preview rules and would have turned CI red with no commit
|
|
222
222
|
# of ours in between. Bump these deliberately, in their own MR.
|
|
223
|
-
|
|
223
|
+
#
|
|
224
|
+
# basedpyright is pinned *exactly*, because the bound above was not enough:
|
|
225
|
+
# 1.39.10 is a patch release built on pyright 1.1.412, which stopped solving
|
|
226
|
+
# a generic class passed as a `Callable[[], R]` factory from the declared
|
|
227
|
+
# type — `x: Box[int] = make(Box)` is an error there and fine on 1.1.411.
|
|
228
|
+
# Every `field(factory=ItemList, ...)` in the model is that shape, so a fresh
|
|
229
|
+
# environment failed `just check` with 44 errors and no commit of ours in
|
|
230
|
+
# between. Still broken on pyright 1.1.414 / basedpyright 1.40.2. The pin
|
|
231
|
+
# goes with basedpyright itself in the pyrefly-only migration.
|
|
232
|
+
"basedpyright==1.39.9",
|
|
224
233
|
"catppuccin[pygments]>=2.5.0",
|
|
225
234
|
"types-lxml>=2025.3.30",
|
|
226
235
|
"mkdocs-material>=9.7.6",
|
|
@@ -35,12 +35,19 @@ breaking.
|
|
|
35
35
|
from __future__ import annotations
|
|
36
36
|
|
|
37
37
|
from datetime import UTC
|
|
38
|
-
from typing import TYPE_CHECKING
|
|
38
|
+
from typing import TYPE_CHECKING, overload
|
|
39
39
|
|
|
40
40
|
if TYPE_CHECKING:
|
|
41
41
|
from datetime import datetime
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
# Overloaded so a value known to be present stays `datetime` on the way out —
|
|
45
|
+
# a query literal (#415) is never None, and widening it to `datetime | None`
|
|
46
|
+
# would push a pointless check onto every caller.
|
|
47
|
+
@overload
|
|
48
|
+
def to_storage(value: datetime) -> datetime: ...
|
|
49
|
+
@overload
|
|
50
|
+
def to_storage(value: None) -> None: ...
|
|
44
51
|
def to_storage(value: datetime | None) -> datetime | None:
|
|
45
52
|
"""A datetime as this schema stores it: UTC, with `tzinfo` stripped.
|
|
46
53
|
|
|
@@ -60,6 +67,10 @@ def to_storage(value: datetime | None) -> datetime | None:
|
|
|
60
67
|
return value.astimezone(UTC).replace(tzinfo=None)
|
|
61
68
|
|
|
62
69
|
|
|
70
|
+
@overload
|
|
71
|
+
def from_storage(value: datetime) -> datetime: ...
|
|
72
|
+
@overload
|
|
73
|
+
def from_storage(value: None) -> None: ...
|
|
63
74
|
def from_storage(value: datetime | None) -> datetime | None:
|
|
64
75
|
"""A stored datetime as the model sees it: the same instant, tagged UTC.
|
|
65
76
|
|
|
@@ -56,7 +56,7 @@ class Freshness:
|
|
|
56
56
|
"has the pointer moved?", which validators answer without one.
|
|
57
57
|
"""
|
|
58
58
|
|
|
59
|
-
#: When the response was received. Naive UTC, per `
|
|
59
|
+
#: When the response was received. Naive UTC, per `sdmxlib._time`'s
|
|
60
60
|
#: convention, so comparisons cannot shift with a reader's zone.
|
|
61
61
|
stored_at: datetime
|
|
62
62
|
#: `ETag`, for `If-None-Match`. The cheapest revalidation there is.
|
|
@@ -223,7 +223,7 @@ def _raise_for_status(response: httpx.Response, *, url: str) -> None:
|
|
|
223
223
|
def _now() -> datetime:
|
|
224
224
|
"""Now, as naive UTC.
|
|
225
225
|
|
|
226
|
-
`
|
|
226
|
+
`sdmxlib._time`'s convention, so a stored instant means the same thing to
|
|
227
227
|
every reader regardless of their machine's zone.
|
|
228
228
|
"""
|
|
229
229
|
return datetime.now(UTC).replace(tzinfo=None)
|
|
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
|
|
23
23
|
import pyarrow as pa
|
|
24
24
|
|
|
25
25
|
from sdmxlib.model.annotations import Annotations
|
|
26
|
-
from sdmxlib.model.codelist import
|
|
26
|
+
from sdmxlib.model.codelist import Code, CodelistLike
|
|
27
27
|
from sdmxlib.model.dataflow import Dataflow
|
|
28
28
|
from sdmxlib.model.datastructure import Component, DataStructure, Dimension
|
|
29
29
|
from sdmxlib.model.istring import InternationalString
|
|
@@ -194,28 +194,16 @@ def _frequency_dimension(dsd: DataStructure) -> Dimension | None:
|
|
|
194
194
|
return None
|
|
195
195
|
|
|
196
196
|
|
|
197
|
-
def
|
|
198
|
-
"""
|
|
197
|
+
def frequency_dimension_id(dsd: DataStructure) -> str | None:
|
|
198
|
+
"""The id of the DSD's frequency dimension, or None if it has none.
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
Only the id. The FREQ codes' ``duration`` annotations are read by
|
|
201
|
+
`ComponentTable` from the codes a message actually observed, in the same
|
|
202
|
+
batched lookup that labels them — this used to materialise the whole FREQ
|
|
203
|
+
codelist up front, one of the per-message full reads #410 removed.
|
|
204
204
|
"""
|
|
205
205
|
freq = _frequency_dimension(dsd)
|
|
206
|
-
if freq is None
|
|
207
|
-
return None, {}
|
|
208
|
-
overrides: dict[str, _Duration] = {}
|
|
209
|
-
codelist: CodelistLike | None = getattr(freq, "resolved_codelist", None)
|
|
210
|
-
if codelist is not None:
|
|
211
|
-
# `materialize()` because this reads each code's *annotations*, which
|
|
212
|
-
# is past what `CodelistLike` offers; free when the codelist is already
|
|
213
|
-
# eager, and a frequency codelist is a handful of codes either way.
|
|
214
|
-
for code in codelist.materialize().codes:
|
|
215
|
-
duration = _annotation_duration(code.annotations, lang)
|
|
216
|
-
if duration is not None:
|
|
217
|
-
overrides[code.id] = duration
|
|
218
|
-
return freq.id, overrides
|
|
206
|
+
return freq.id if freq is not None else None
|
|
219
207
|
|
|
220
208
|
|
|
221
209
|
def _add_duration(start: datetime, duration: _Duration) -> datetime:
|
|
@@ -316,6 +304,16 @@ class ComponentTable:
|
|
|
316
304
|
SDMX-JSON spec explicitly allows this) rather than the full
|
|
317
305
|
enumeration. The StatCan census geography codelist (579k codes)
|
|
318
306
|
would otherwise blow up a ``limit=2`` data response to >30 MB.
|
|
307
|
+
|
|
308
|
+
**The cost follows the response too, not just its size.** A value's
|
|
309
|
+
name, ``parent`` and annotations are filled in at `to_json`, by one
|
|
310
|
+
`CodelistLike.codes_for` per component over exactly the ids observed.
|
|
311
|
+
Calling `materialize` here instead read every code of every enumerated
|
|
312
|
+
component on every message once the DSD came from a `LocalRegistry` —
|
|
313
|
+
1.4 s to write one observation against a 200k-code geography, where the
|
|
314
|
+
same DSD with eager codelists took 3 ms (#410). Looking each code up as it
|
|
315
|
+
arrived would be one round trip per distinct value, which is the same
|
|
316
|
+
mistake at the other end of the scale.
|
|
319
317
|
"""
|
|
320
318
|
|
|
321
319
|
def __init__(
|
|
@@ -328,7 +326,6 @@ class ComponentTable:
|
|
|
328
326
|
annotations: AnnotationPool,
|
|
329
327
|
time_dim_id: str | None = None,
|
|
330
328
|
freq_dim_id: str | None = None,
|
|
331
|
-
freq_overrides: dict[str, _Duration] | None = None,
|
|
332
329
|
) -> None:
|
|
333
330
|
self._ids = component_ids
|
|
334
331
|
# Shared with the sibling table — see `AnnotationPool`. Required rather
|
|
@@ -342,11 +339,12 @@ class ComponentTable:
|
|
|
342
339
|
self._kind = kind
|
|
343
340
|
self._time_dim_id = time_dim_id
|
|
344
341
|
# FREQ-driven period bounds (issue #149): the frequency dimension id,
|
|
345
|
-
# its codes' annotation-derived duration overrides
|
|
346
|
-
#
|
|
347
|
-
#
|
|
342
|
+
# its observed codes' annotation-derived duration overrides (filled
|
|
343
|
+
# by `_resolve_codes`, alongside their labels), and the accumulated
|
|
344
|
+
# TIME_PERIOD id → set of co-occurring FREQ ids seen while streaming.
|
|
345
|
+
# Bounds are resolved at to_json() time from these.
|
|
348
346
|
self._freq_dim_id = freq_dim_id
|
|
349
|
-
self._freq_overrides
|
|
347
|
+
self._freq_overrides: dict[str, _Duration] = {}
|
|
350
348
|
self._time_freqs: dict[str, set[str]] = {}
|
|
351
349
|
# ``dsd.dimensions`` is ItemList[Dimension] and does NOT include
|
|
352
350
|
# the time dimension (which lives at ``dsd.time_dimension``).
|
|
@@ -370,14 +368,12 @@ class ComponentTable:
|
|
|
370
368
|
# so position N maps to the Nth-inserted entry. ``to_json``
|
|
371
369
|
# walks this directly.
|
|
372
370
|
self._codes: dict[str, dict[str, dict[str, object]]] = {cid: {} for cid in component_ids}
|
|
373
|
-
# Per-component resolved codelists,
|
|
374
|
-
#
|
|
375
|
-
#
|
|
376
|
-
#
|
|
377
|
-
#
|
|
378
|
-
|
|
379
|
-
# than once from.
|
|
380
|
-
self._codelists: dict[str, Codelist] = {}
|
|
371
|
+
# Per-component resolved codelists, held as they came — eager or lazy
|
|
372
|
+
# — and never walked: `_resolve_codes` asks each for the observed ids
|
|
373
|
+
# only. For census-scale codelists (CL_GEOGRAPHY: 579k codes) the walk
|
|
374
|
+
# dominated wall time on small data responses, first as a label cache
|
|
375
|
+
# and again as a per-message `materialize()` (#410).
|
|
376
|
+
self._codelists: dict[str, CodelistLike] = {}
|
|
381
377
|
|
|
382
378
|
for cid in component_ids:
|
|
383
379
|
comp = components.get(cid)
|
|
@@ -395,10 +391,7 @@ class ComponentTable:
|
|
|
395
391
|
# getattr returns Any; the annotation kills it on this line.
|
|
396
392
|
cl: CodelistLike | None = getattr(comp, "resolved_codelist", None)
|
|
397
393
|
if cl is not None:
|
|
398
|
-
|
|
399
|
-
# reads name, parent_id and annotations off each — the full
|
|
400
|
-
# `Code`, not the id-and-label surface `CodelistLike` shares.
|
|
401
|
-
self._codelists[cid] = cl.materialize()
|
|
394
|
+
self._codelists[cid] = cl
|
|
402
395
|
comp_anns = self._annotations.register(comp.annotations)
|
|
403
396
|
if comp_anns:
|
|
404
397
|
self._component_annotations[cid] = comp_anns
|
|
@@ -412,22 +405,39 @@ class ComponentTable:
|
|
|
412
405
|
pos = len(table)
|
|
413
406
|
table[code_id] = pos
|
|
414
407
|
self._position_str[component_id][code_id] = str(pos)
|
|
415
|
-
# Both SDMX-JSON 1.0 and 2.0 schemas require every value entry
|
|
416
|
-
#
|
|
417
|
-
# the
|
|
418
|
-
#
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
408
|
+
# Both SDMX-JSON 1.0 and 2.0 schemas require every value entry to carry
|
|
409
|
+
# id + name. The id stands in for the name until `_resolve_codes`
|
|
410
|
+
# replaces it, and stays for a value the codelist does not hold (or a
|
|
411
|
+
# component with no codelist).
|
|
412
|
+
self._codes[component_id][code_id] = {"id": code_id, "name": code_id}
|
|
413
|
+
return pos
|
|
414
|
+
|
|
415
|
+
def _resolve_codes(self) -> None:
|
|
416
|
+
"""Fill each observed value's name, ``parent`` and annotations.
|
|
417
|
+
|
|
418
|
+
One `CodelistLike.codes_for` per enumerated component, over the ids
|
|
419
|
+
this message observed — the whole point of deferring it here (#410).
|
|
420
|
+
Annotations are registered in position order, so the pool's indices
|
|
421
|
+
are deterministic for a given input.
|
|
422
|
+
"""
|
|
423
|
+
for component_id, cl in self._codelists.items():
|
|
424
|
+
entries = self._codes[component_id]
|
|
425
|
+
if not entries:
|
|
426
|
+
continue
|
|
427
|
+
found = cl.codes_for(entries.keys())
|
|
428
|
+
for code_id, entry in entries.items():
|
|
429
|
+
code = found.get(code_id)
|
|
430
|
+
if code is None:
|
|
431
|
+
continue
|
|
432
|
+
if component_id == self._freq_dim_id:
|
|
433
|
+
duration = _annotation_duration(code.annotations, self._lang)
|
|
434
|
+
if duration is not None:
|
|
435
|
+
self._freq_overrides[code_id] = duration
|
|
436
|
+
self._fill_entry(entry, code)
|
|
437
|
+
|
|
438
|
+
def _fill_entry(self, entry: dict[str, object], code: Code) -> None:
|
|
439
|
+
"""Write one resolved *code*'s name, ``parent`` and annotations into *entry*."""
|
|
440
|
+
entry["name"] = code.name.get(self._lang) or code.id
|
|
431
441
|
# A value's hierarchy edge (#365). `parent` is a first-class property of
|
|
432
442
|
# this object in the 1.0, 2.0 and 2.1 data schemas, and omitting it made
|
|
433
443
|
# every hierarchical codelist arrive flat: the .Stat Data Explorer groups
|
|
@@ -446,20 +456,17 @@ class ComponentTable:
|
|
|
446
456
|
# `IDType`, so an id it rejects was already invalid SDMX upstream. Adding
|
|
447
457
|
# a filter here would turn someone else's malformed codelist into a
|
|
448
458
|
# clean-looking document with a silently missing hierarchy.
|
|
449
|
-
if parent_id is not None:
|
|
450
|
-
entry["parent"] = parent_id
|
|
459
|
+
if code.parent_id is not None:
|
|
460
|
+
entry["parent"] = code.parent_id
|
|
451
461
|
# The code's annotations, as indices into the shared pool (#366). This
|
|
452
462
|
# is how a consumer gets a value's ORDER, NOT_DISPLAYED and the rest —
|
|
453
463
|
# the *type* vocabulary is the annotation creator's, not SDMX's ("the
|
|
454
464
|
# types are not enumerated, as these can be specified by the user or
|
|
455
465
|
# creator", per the AnnotationType documentation in every SDMX-ML
|
|
456
466
|
# version), so the writer emits all of them and filters none.
|
|
467
|
+
value_anns = self._annotations.register(code.annotations)
|
|
457
468
|
if value_anns:
|
|
458
469
|
entry["annotations"] = value_anns
|
|
459
|
-
# TIME_PERIOD start/end is resolved in to_json() once the full set of
|
|
460
|
-
# co-occurring FREQ values is known (see _resolve_time_bounds).
|
|
461
|
-
self._codes[component_id][code_id] = entry
|
|
462
|
-
return pos
|
|
463
470
|
|
|
464
471
|
def tracks_freq(self) -> bool:
|
|
465
472
|
"""True if both a time dimension and a frequency dimension are known."""
|
|
@@ -532,7 +539,12 @@ class ComponentTable:
|
|
|
532
539
|
return {cid: pos_str[cid] for cid in code_ids}
|
|
533
540
|
|
|
534
541
|
def to_json(self) -> list[dict[str, object]]:
|
|
535
|
-
"""Render the per-position metadata array for the structures block.
|
|
542
|
+
"""Render the per-position metadata array for the structures block.
|
|
543
|
+
|
|
544
|
+
Call once, after the last batch: this is where observed values are
|
|
545
|
+
resolved against their codelists.
|
|
546
|
+
"""
|
|
547
|
+
self._resolve_codes()
|
|
536
548
|
out: list[dict[str, object]] = []
|
|
537
549
|
for key_pos, cid in enumerate(self._ids):
|
|
538
550
|
code_entries = self._codes[cid]
|
|
@@ -49,7 +49,7 @@ from sdmxlib.formats.sdmx_json._data_common import (
|
|
|
49
49
|
decode_observations,
|
|
50
50
|
display_names,
|
|
51
51
|
encode_batch,
|
|
52
|
-
|
|
52
|
+
frequency_dimension_id,
|
|
53
53
|
resolve_columns,
|
|
54
54
|
rows_to_lazyframe,
|
|
55
55
|
)
|
|
@@ -121,7 +121,7 @@ def write_sdmx_json_data(
|
|
|
121
121
|
dim_cols, dim_phys, attr_cols, attr_phys, _measure_id, measure_col = resolve_columns(dsd)
|
|
122
122
|
|
|
123
123
|
time_dim_id = dsd.time_dimension.id if dsd.time_dimension is not None else "TIME_PERIOD"
|
|
124
|
-
freq_dim_id
|
|
124
|
+
freq_dim_id = frequency_dimension_id(dsd)
|
|
125
125
|
# One pool for the whole structure block — the two tables emit integer
|
|
126
126
|
# indices into it, and an index is meaningless without knowing which array
|
|
127
127
|
# it indexes (#366).
|
|
@@ -134,7 +134,6 @@ def write_sdmx_json_data(
|
|
|
134
134
|
annotations=annotations,
|
|
135
135
|
time_dim_id=time_dim_id,
|
|
136
136
|
freq_dim_id=freq_dim_id,
|
|
137
|
-
freq_overrides=freq_overrides,
|
|
138
137
|
)
|
|
139
138
|
attr_lookup = ComponentTable(attr_cols, dsd, kind="attribute", lang=lang, annotations=annotations)
|
|
140
139
|
|
|
@@ -63,7 +63,7 @@ from sdmxlib.formats.sdmx_json._data_common import (
|
|
|
63
63
|
decode_observations,
|
|
64
64
|
display_names,
|
|
65
65
|
encode_batch,
|
|
66
|
-
|
|
66
|
+
frequency_dimension_id,
|
|
67
67
|
resolve_columns,
|
|
68
68
|
rows_to_lazyframe,
|
|
69
69
|
)
|
|
@@ -142,7 +142,7 @@ def write_sdmx_json_data(
|
|
|
142
142
|
dim_cols, dim_phys, attr_cols, attr_phys, _measure_id, measure_col = resolve_columns(dsd)
|
|
143
143
|
|
|
144
144
|
time_dim_id = dsd.time_dimension.id if dsd.time_dimension is not None else "TIME_PERIOD"
|
|
145
|
-
freq_dim_id
|
|
145
|
+
freq_dim_id = frequency_dimension_id(dsd)
|
|
146
146
|
# One pool for the whole structure block — the two tables emit integer
|
|
147
147
|
# indices into it, and an index is meaningless without knowing which array
|
|
148
148
|
# it indexes (#366).
|
|
@@ -155,7 +155,6 @@ def write_sdmx_json_data(
|
|
|
155
155
|
annotations=annotations,
|
|
156
156
|
time_dim_id=time_dim_id,
|
|
157
157
|
freq_dim_id=freq_dim_id,
|
|
158
|
-
freq_overrides=freq_overrides,
|
|
159
158
|
)
|
|
160
159
|
attr_lookup = ComponentTable(attr_cols, dsd, kind="attribute", lang=lang, annotations=annotations)
|
|
161
160
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Codelist and Code — enumerated value sets."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterable
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from enum import Enum
|
|
5
6
|
from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable
|
|
@@ -186,14 +187,28 @@ class CodelistLike(Protocol):
|
|
|
186
187
|
"""``{code_id: label}`` in ``lang``, falling back to the id."""
|
|
187
188
|
...
|
|
188
189
|
|
|
190
|
+
def codes_for(self, ids: Iterable[str]) -> dict[str, Code]:
|
|
191
|
+
"""``{code_id: Code}`` for exactly *ids*, each fully built.
|
|
192
|
+
|
|
193
|
+
Ids the codelist does not hold are absent from the result rather than
|
|
194
|
+
an error — a caller resolving observed values meets values outside the
|
|
195
|
+
enumeration, and the id is then its own label.
|
|
196
|
+
|
|
197
|
+
The targeted counterpart to `materialize`: one batched read on a lazy
|
|
198
|
+
codelist, whose cost follows ``len(ids)`` rather than the codelist. The
|
|
199
|
+
SDMX-JSON data writer called `materialize` per component per message
|
|
200
|
+
and paid for every code in a 579,951-code geography to label the
|
|
201
|
+
handful a response observed (#410).
|
|
202
|
+
"""
|
|
203
|
+
...
|
|
204
|
+
|
|
189
205
|
def materialize(self) -> "Codelist":
|
|
190
206
|
"""This codelist with every `Code` built, for callers needing more.
|
|
191
207
|
|
|
192
208
|
Free on an eager `Codelist`, which returns itself; a full read on a
|
|
193
|
-
lazy one
|
|
194
|
-
annotations or ``parent_id``
|
|
195
|
-
|
|
196
|
-
be a `Query`.
|
|
209
|
+
lazy one, whose cost is the whole codelist. A caller that needs some
|
|
210
|
+
codes' annotations or ``parent_id`` rather than all of them wants
|
|
211
|
+
`codes_for` instead — that was the SDMX-JSON data writer (#410).
|
|
197
212
|
"""
|
|
198
213
|
...
|
|
199
214
|
|
|
@@ -385,6 +400,13 @@ class Codelist(MaintainableArtefact):
|
|
|
385
400
|
"""
|
|
386
401
|
return self.codes.ids()
|
|
387
402
|
|
|
403
|
+
def codes_for(self, ids: Iterable[str]) -> dict[str, Code]:
|
|
404
|
+
"""``{code_id: Code}`` for exactly *ids*; unknown ids are absent.
|
|
405
|
+
|
|
406
|
+
The eager half of `CodelistLike.codes_for` — an `ItemList.get` per id.
|
|
407
|
+
"""
|
|
408
|
+
return {cid: code for cid in ids if (code := self.codes.get(cid)) is not None}
|
|
409
|
+
|
|
388
410
|
def materialize(self) -> "Codelist":
|
|
389
411
|
"""This codelist — it is already materialised.
|
|
390
412
|
|
|
@@ -375,24 +375,32 @@ def _operator(pred: _Leaf, reference: str, lang: tuple[object, ...] = ()) -> Sql
|
|
|
375
375
|
# `In(d, [])` is False, normalised here rather than emitted as
|
|
376
376
|
# `IN ()`, which is a syntax error in most dialects.
|
|
377
377
|
return Sql("1 = 0")
|
|
378
|
-
#
|
|
378
|
+
# The whole set as *one* list parameter (#411), not a placeholder
|
|
379
|
+
# per value as #301 left it. Binding is linear either way, because
|
|
380
|
+
# DuckDB's Python client converts each value, but the list is never
|
|
381
|
+
# slower and usually much faster — duckdb 1.5.5, 1,000 / 50,000
|
|
382
|
+
# values, `IN (?, …)` against `= ANY(?)`:
|
|
379
383
|
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
# by hand. Measured, it is **not** a fix: 2489 ms against 2549 ms
|
|
384
|
-
# for 50,000 values, 1.0x at every size from 100 up. The cost is
|
|
385
|
-
# DuckDB marshalling each Python value, not the placeholder count,
|
|
386
|
-
# so collapsing the placeholders moves nothing.
|
|
384
|
+
# pandas absent 1.1x / 1.1x
|
|
385
|
+
# pandas stubbed 1.5x / 2.1x (`sys.modules["pandas"] = None`)
|
|
386
|
+
# pandas installed 2.6x / 9.3x
|
|
387
387
|
#
|
|
388
|
-
#
|
|
389
|
-
#
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
|
|
395
|
-
|
|
388
|
+
# #301 measured 1.0x and kept the placeholders. That run had pandas
|
|
389
|
+
# absent, where DuckDB retries `import pandas` twice for every bound
|
|
390
|
+
# value to check for `pandas.NA` and never remembers the failure;
|
|
391
|
+
# the probe swamped both shapes, so they tied. Strip it out and the
|
|
392
|
+
# per-placeholder overhead shows.
|
|
393
|
+
#
|
|
394
|
+
# `ANY` over a list holding NULL has `IN`'s three-valued semantics,
|
|
395
|
+
# so no row changes — the differential properties are what say so.
|
|
396
|
+
#
|
|
397
|
+
# Still linear, though. What is flat is registering an Arrow buffer,
|
|
398
|
+
# which is `Session.value_set`, explicit because doing it implicitly
|
|
399
|
+
# would make `to_sql()` output reference a relation that does not
|
|
400
|
+
# exist. #301 surveyed six consumers before leaving it explicit:
|
|
401
|
+
# none of them calls `is_in` at all, and the ones filtering by a
|
|
402
|
+
# set use a subquery, which is already free.
|
|
403
|
+
return Sql(f"{reference} = ANY(?)", (*lang, list(values)))
|
|
396
404
|
case _: # pragma: no cover - exhaustive over the leaf kinds
|
|
397
405
|
msg = f"cannot compile {type(pred).__name__}"
|
|
398
406
|
raise PathError(msg)
|
|
@@ -25,10 +25,12 @@ import builtins
|
|
|
25
25
|
import copy
|
|
26
26
|
import sys
|
|
27
27
|
from collections.abc import Callable, Iterable, Mapping
|
|
28
|
+
from datetime import UTC, date, datetime
|
|
28
29
|
from typing import NoReturn, Self, TypeIs, cast, override
|
|
29
30
|
|
|
30
31
|
import attrs
|
|
31
32
|
|
|
33
|
+
from sdmxlib._time import to_storage
|
|
32
34
|
from sdmxlib.model._label_match import Fold
|
|
33
35
|
from sdmxlib.model.version import sort_key as version_sort_key
|
|
34
36
|
from sdmxlib.query._common import Descending, canonical_key
|
|
@@ -1060,10 +1062,14 @@ def _check_literal(column: AnyCol, holds: str, value: object) -> None:
|
|
|
1060
1062
|
ok = isinstance(value, int | float) and not isinstance(value, bool)
|
|
1061
1063
|
if ok:
|
|
1062
1064
|
return
|
|
1065
|
+
raise _literal_type_error(column, holds, value)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _literal_type_error(column: AnyCol, holds: str, value: object) -> TypeError:
|
|
1069
|
+
"""The refusal `_check_literal` raises, for a column checking its own way."""
|
|
1063
1070
|
field = ".".join((*column.path, column.column)) if column.path else column.column
|
|
1064
1071
|
suggestion = f' Did you mean "{value}"?' if holds == "str" else ""
|
|
1065
|
-
|
|
1066
|
-
raise TypeError(msg)
|
|
1072
|
+
return TypeError(f"{field} holds {holds}, got {type(value).__name__}.{suggestion}")
|
|
1067
1073
|
|
|
1068
1074
|
|
|
1069
1075
|
class Str[R: RowKind](Col[R]):
|
|
@@ -1850,7 +1856,7 @@ class Temporal[R: RowKind](Num[R]):
|
|
|
1850
1856
|
The store's convention is **UTC-naive** — DuckDB's `TIMESTAMP` carries no
|
|
1851
1857
|
zone, and handing it a tz-aware value converts to the *session's* local
|
|
1852
1858
|
time, so the same instant read on two machines is two instants (#321,
|
|
1853
|
-
`
|
|
1859
|
+
`sdmxlib/_time.py`). The round-trip that module documents is therefore
|
|
1854
1860
|
`naive in -> aware out`: the wall time is unchanged, it is simply labelled.
|
|
1855
1861
|
|
|
1856
1862
|
`readers` already honour that, so `reg.get(urn).valid_from` is tz-aware.
|
|
@@ -1863,10 +1869,86 @@ class Temporal[R: RowKind](Num[R]):
|
|
|
1863
1869
|
A column class rather than `Num`, which is what these were declared as at
|
|
1864
1870
|
first: `Num` compares and orders correctly but carries no way to tell the
|
|
1865
1871
|
frame builder that a value is an instant, and that is the whole difference.
|
|
1872
|
+
|
|
1873
|
+
**Literals go in the way the writers put values in** (#415). Every one is
|
|
1874
|
+
normalised with `sdmxlib._time.to_storage` — an aware `datetime` converted
|
|
1875
|
+
to UTC and its zone dropped, a naive one taken as UTC already, a `date` as
|
|
1876
|
+
midnight UTC — before it reaches DuckDB. Without that, DuckDB converted an
|
|
1877
|
+
aware literal to the *session's* zone first: `is_in([aware])` matched
|
|
1878
|
+
nothing and `< aware` compared against an instant shifted by the local
|
|
1879
|
+
offset, so one query gave different rows on different machines. It went
|
|
1880
|
+
unseen because on a UTC session the shift is zero.
|
|
1881
|
+
|
|
1882
|
+
And the literal check is its own. `Num`'s demands an `int`, so `==`
|
|
1883
|
+
refused a `datetime` while `<` — which `Num` does not check — took one, and
|
|
1884
|
+
took an `int` just as readily.
|
|
1866
1885
|
"""
|
|
1867
1886
|
|
|
1868
1887
|
__slots__ = ()
|
|
1869
1888
|
|
|
1889
|
+
def _literal(self, value: object) -> datetime:
|
|
1890
|
+
"""*value* as the store holds it, or a `TypeError` naming the column.
|
|
1891
|
+
|
|
1892
|
+
Raises:
|
|
1893
|
+
TypeError: On anything but a `datetime` or `date` — `None`
|
|
1894
|
+
included, since `= NULL` and `< NULL` are never true.
|
|
1895
|
+
"""
|
|
1896
|
+
if value is None:
|
|
1897
|
+
msg = (
|
|
1898
|
+
f"comparing {self.column} to None matches nothing — a comparison with NULL "
|
|
1899
|
+
"is never true in SQL. Use is_null(), or is_not_null() for the complement."
|
|
1900
|
+
)
|
|
1901
|
+
raise TypeError(msg)
|
|
1902
|
+
if isinstance(value, datetime):
|
|
1903
|
+
return to_storage(value)
|
|
1904
|
+
# After `datetime`, which is a `date` too.
|
|
1905
|
+
if isinstance(value, date):
|
|
1906
|
+
return to_storage(datetime(value.year, value.month, value.day, tzinfo=UTC))
|
|
1907
|
+
raise _literal_type_error(self, "datetime", value)
|
|
1908
|
+
|
|
1909
|
+
@override
|
|
1910
|
+
def __eq__(self, other: object) -> Pred[R]:
|
|
1911
|
+
# To `Col` directly, past `Num`, whose integer check is the defect.
|
|
1912
|
+
return Col.__eq__(self, self._literal(other))
|
|
1913
|
+
|
|
1914
|
+
@override
|
|
1915
|
+
def __ne__(self, other: object) -> Pred[R]:
|
|
1916
|
+
return Col.__ne__(self, self._literal(other))
|
|
1917
|
+
|
|
1918
|
+
@override
|
|
1919
|
+
def __lt__(self, other: object) -> Pred[R]:
|
|
1920
|
+
return Compare(self, "<", self._literal(other))
|
|
1921
|
+
|
|
1922
|
+
@override
|
|
1923
|
+
def __le__(self, other: object) -> Pred[R]:
|
|
1924
|
+
return Compare(self, "<=", self._literal(other))
|
|
1925
|
+
|
|
1926
|
+
@override
|
|
1927
|
+
def __gt__(self, other: object) -> Pred[R]:
|
|
1928
|
+
return Compare(self, ">", self._literal(other))
|
|
1929
|
+
|
|
1930
|
+
@override
|
|
1931
|
+
def __ge__(self, other: object) -> Pred[R]:
|
|
1932
|
+
return Compare(self, ">=", self._literal(other))
|
|
1933
|
+
|
|
1934
|
+
@override
|
|
1935
|
+
def __hash__(self) -> int:
|
|
1936
|
+
return id(self)
|
|
1937
|
+
|
|
1938
|
+
@override
|
|
1939
|
+
def is_in(self, values: Iterable[object] | SqlRelation) -> Pred[R]:
|
|
1940
|
+
"""As `Col.is_in`, with every member checked and normalised.
|
|
1941
|
+
|
|
1942
|
+
Every member, not just the first as `Str.is_in` does: a mixed set is
|
|
1943
|
+
unlikely, but an aware member left un-normalised is a silent miss, so
|
|
1944
|
+
each one is converted anyway and the check comes free.
|
|
1945
|
+
"""
|
|
1946
|
+
if _is_relation(values) or _is_opaque(values) or not _is_iterable(values):
|
|
1947
|
+
return super().is_in(values)
|
|
1948
|
+
# `None` stays a member, as `Str.is_in` lets it: NULL in an `IN` set
|
|
1949
|
+
# is inert rather than a mistake worth refusing.
|
|
1950
|
+
return super().is_in([None if member is None else self._literal(member) for member in values])
|
|
1951
|
+
|
|
1870
1952
|
|
|
1871
1953
|
class AnyWinPred:
|
|
1872
1954
|
"""Non-generic base for windowed predicates, as `AnyPred` is for plain ones.
|
|
@@ -224,7 +224,7 @@ def _as_utc(frame: pl.DataFrame, items: Sequence[tuple[str, AnyCol | AnyAgg]]) -
|
|
|
224
224
|
compare nor subtract.
|
|
225
225
|
|
|
226
226
|
`replace_time_zone`, not `convert_time_zone`: the wall time is already
|
|
227
|
-
UTC and must not move. That is the third row of `
|
|
227
|
+
UTC and must not move. That is the third row of `sdmxlib/_time.py`'s
|
|
228
228
|
round-trip table — *same wall time, now labelled*.
|
|
229
229
|
"""
|
|
230
230
|
temporal = [name for name, item in items if isinstance(item, Temporal)]
|