matview-cache 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matview/__init__.py +78 -0
- matview/cache.py +750 -0
- matview/canon.py +116 -0
- matview/cli.py +900 -0
- matview/codecs/__init__.py +159 -0
- matview/codecs/_arrow.py +104 -0
- matview/codecs/_arrow_ipc.py +89 -0
- matview/codecs/_arrow_pandas.py +76 -0
- matview/codecs/_arrow_parts.py +99 -0
- matview/codecs/_json.py +36 -0
- matview/codecs/_npy.py +53 -0
- matview/context.py +87 -0
- matview/errors.py +96 -0
- matview/fingerprint.py +99 -0
- matview/foreign.py +436 -0
- matview/graph.py +876 -0
- matview/policy.py +67 -0
- matview/py.typed +0 -0
- matview/registry.py +293 -0
- matview/store/__init__.py +6 -0
- matview/store/base.py +99 -0
- matview/store/sqlite.py +1188 -0
- matview/types.py +341 -0
- matview_cache-0.1.0.dist-info/METADATA +262 -0
- matview_cache-0.1.0.dist-info/RECORD +28 -0
- matview_cache-0.1.0.dist-info/WHEEL +4 -0
- matview_cache-0.1.0.dist-info/entry_points.txt +2 -0
- matview_cache-0.1.0.dist-info/licenses/LICENSE +21 -0
matview/canon.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Key canonicalization, per `SPEC.md`.
|
|
2
|
+
|
|
3
|
+
This is half of the interoperability contract. Two implementations that disagree here
|
|
4
|
+
do not error; they miss on every lookup and look like an ordinary cold cache. Change
|
|
5
|
+
it only by bumping `fingerprint_spec_version`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from typing import Any, Final
|
|
13
|
+
|
|
14
|
+
import rfc8785
|
|
15
|
+
|
|
16
|
+
from matview.errors import InvalidKey
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"RESERVED_KEY_FIELDS",
|
|
20
|
+
"Scalar",
|
|
21
|
+
"canonicalize_key",
|
|
22
|
+
"index_value",
|
|
23
|
+
"jcs",
|
|
24
|
+
"utf16_sort_key",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
Scalar = str | int | float | bool | None
|
|
28
|
+
|
|
29
|
+
#: ECMAScript's MAX_SAFE_INTEGER. JCS numbers are doubles, so this is the range in
|
|
30
|
+
#: which an int and an integral float are unambiguously the same number and render
|
|
31
|
+
#: identically. RFC 8785 serializers reject integers beyond it, so matview does too,
|
|
32
|
+
#: rather than letting the rejection surface from inside a dependency.
|
|
33
|
+
SAFE_INTEGER: Final = 2**53 - 1
|
|
34
|
+
|
|
35
|
+
#: Policy arguments to `get`, which the spec's own API mixes with key fields in one
|
|
36
|
+
#: `**kwargs`. A recipe parameter with one of these names would be silently swallowed
|
|
37
|
+
#: by the policy layer, so it is rejected at registration instead. See DECISIONS.md D18.
|
|
38
|
+
RESERVED_KEY_FIELDS: Final = frozenset({"on_miss", "force", "force_deps", "deadline", "part"})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_scalar(field: str, value: Any) -> Scalar:
|
|
42
|
+
"""Validate and normalize one key field.
|
|
43
|
+
|
|
44
|
+
Key fields must be scalars. This is a restriction and it is deliberate: nested
|
|
45
|
+
params make canonicalization far harder to get right across languages.
|
|
46
|
+
"""
|
|
47
|
+
if value is None or isinstance(value, (str, bool)):
|
|
48
|
+
return value # bool first: it is a subclass of int, and JCS renders it `true`.
|
|
49
|
+
if isinstance(value, int):
|
|
50
|
+
if abs(value) > SAFE_INTEGER:
|
|
51
|
+
raise InvalidKey(
|
|
52
|
+
f"key field {field!r} is {value}, outside the exact-integer range of a "
|
|
53
|
+
f"double (±(2^53 - 1)); no JCS implementation can render it identically"
|
|
54
|
+
)
|
|
55
|
+
return value
|
|
56
|
+
if isinstance(value, float):
|
|
57
|
+
if math.isnan(value) or math.isinf(value):
|
|
58
|
+
raise InvalidKey(f"key field {field!r} is {value!r}; NaN and infinities are rejected")
|
|
59
|
+
# An integral float within the safe range *is* the corresponding int, and the
|
|
60
|
+
# two must hash the same. Getting this wrong silently splits a cache in two.
|
|
61
|
+
if value.is_integer() and abs(value) <= SAFE_INTEGER:
|
|
62
|
+
return int(value)
|
|
63
|
+
return value
|
|
64
|
+
raise InvalidKey(
|
|
65
|
+
f"key field {field!r} has type {type(value).__name__}; key fields must be "
|
|
66
|
+
f"string, int, float, bool or null, with no nested structures"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def canonicalize_key(key: Mapping[str, Any]) -> dict[str, Scalar]:
|
|
71
|
+
"""Normalize a key dict. Defaults must already have been filled in by the caller."""
|
|
72
|
+
out: dict[str, Scalar] = {}
|
|
73
|
+
for field, value in key.items():
|
|
74
|
+
if not isinstance(field, str):
|
|
75
|
+
raise InvalidKey(f"key field names must be strings, got {field!r}")
|
|
76
|
+
out[field] = normalize_scalar(field, value)
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def jcs(value: Any) -> bytes:
|
|
81
|
+
"""Serialize with the JSON Canonicalization Scheme (RFC 8785).
|
|
82
|
+
|
|
83
|
+
Object keys sorted by UTF-16 code unit, no insignificant whitespace, UTF-8 output,
|
|
84
|
+
ES6 number rendering. Delegated rather than hand-rolled: the hard part is number
|
|
85
|
+
formatting, and getting that wrong shows up as a cold cache, not an exception.
|
|
86
|
+
See DECISIONS.md D3.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
return rfc8785.dumps(value)
|
|
90
|
+
except Exception as exc: # pragma: no cover - rfc8785 raises several types
|
|
91
|
+
raise InvalidKey(f"value is not JCS-serializable: {exc}") from exc
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def utf16_sort_key(value: str | None) -> tuple[int, bytes]:
|
|
95
|
+
"""Order strings by UTF-16 code unit, matching JCS, with `None` sorting first.
|
|
96
|
+
|
|
97
|
+
Comparing UTF-16BE encodings byte-wise is the same ordering as comparing the code
|
|
98
|
+
unit sequences numerically, surrogate pairs included. The leading int is what puts
|
|
99
|
+
a NULL part before every string part. See DECISIONS.md D15.
|
|
100
|
+
"""
|
|
101
|
+
if value is None:
|
|
102
|
+
return (0, b"")
|
|
103
|
+
return (1, value.encode("utf-16-be"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def index_value(value: Scalar) -> str:
|
|
107
|
+
"""Render a key field for promotion into `artifact_key`.
|
|
108
|
+
|
|
109
|
+
The column is `TEXT NOT NULL`, but indexed fields may be ints, floats or bools, so
|
|
110
|
+
they are stored as their canonical JCS scalar rendering: `200` not `"200"`, `true`
|
|
111
|
+
not `"True"`. That keeps `query()` consistent with `key_json` and the promotion
|
|
112
|
+
lossless. See DECISIONS.md D8.
|
|
113
|
+
"""
|
|
114
|
+
if isinstance(value, str):
|
|
115
|
+
return value
|
|
116
|
+
return jcs(value).decode("utf-8")
|