pyuncommonkit 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.
commonkit/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Named shared-memory segments for numpy arrays.
2
+
3
+ The name is for Fortran's ``COMMON`` block: a named region several separately
4
+ compiled programs agree to share. The name is the whole interface, and the
5
+ segment outlives whoever wrote it.
6
+
7
+ ``store`` is the mechanism and carries the measured reasons behind it, ``keys``
8
+ the addressing, ``cache`` the in-process memo that keeps a tight loop from
9
+ paying an attach per call.
10
+ """
11
+ from .cache import TieredPixelCache
12
+ from .keys import KeyScheme, parse, register, relpath, scheme_for
13
+ from .shared import shared_store
14
+ from .store import KeyCollision, SharedArrayStore
15
+
16
+ __all__ = [
17
+ "KeyCollision", "KeyScheme", "SharedArrayStore", "TieredPixelCache",
18
+ "parse", "register", "relpath", "scheme_for", "shared_store",
19
+ ]
commonkit/cache.py ADDED
@@ -0,0 +1,125 @@
1
+ """Two-tier pixel cache: an in-process memo over the shared store.
2
+
3
+ Why two tiers and not a straight replacement, in one measurement:
4
+
5
+ The consumer's face-crop path calls its pixel accessor **twice per face**
6
+ -- once for the base dimensions, once for the crop. An 18-face page is
7
+ therefore 36 lookups. Today that is one 38 ms decode plus 35 free in-process
8
+ hits. Against a shared store alone it would be 36 attaches at ~3.3 ms each =
9
+ 119 ms, **three times slower than doing nothing at all**. Attaching is cheap, but
10
+ it is not free, and a loop that attaches per crop is the one access pattern that
11
+ turns a cache into a pessimization.
12
+
13
+ So L1 memoises the ATTACHED MAPPING, not a second copy of the pixels. The first
14
+ lookup attaches; every later lookup in that process is a dict hit. The pixels
15
+ still live once in shared memory and are still shared across processes -- L1 adds
16
+ no bytes, it removes syscalls.
17
+
18
+ Tier order: L1 in-process memo -> L2 shared store -> decode.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ from collections import Counter
23
+
24
+ import numpy as np
25
+
26
+ from .store import KeyCollision
27
+
28
+
29
+ class TieredPixelCache:
30
+ """Satisfies the consumer's ``PixelCache`` protocol.
31
+
32
+ ``l1`` is anything with ``get``/``set_array`` (the original consumer
33
+ passes an LRU array cache); ``l2`` is a
34
+ :class:`~commonkit.store.SharedArrayStore`. Either may be None,
35
+ which degrades to the other -- and with both None this is a no-op cache,
36
+ leaving its caller exactly as it would be with no cache at all.
37
+ """
38
+
39
+ def __init__(self, l1=None, l2=None):
40
+ self.l1 = l1
41
+ self.l2 = l2
42
+ self.stats: Counter = Counter()
43
+
44
+ # ``_base_array`` checks for this and prefers it over the bytes path.
45
+ def get(self, key: str):
46
+ """The PixelCache protocol: the array, or None."""
47
+ return self.get_with_source(key)[0]
48
+
49
+ def get_with_source(self, key: str):
50
+ """``(array, source)`` -- which TIER answered, or ``(None, "decode")``.
51
+
52
+ The return channel this class used to lack, and the lack of which was a
53
+ real defect rather than an inelegance. ``entry.decoded`` could not ask
54
+ what happened, so it inferred it from counter DELTAS -- reading
55
+ ``shm_hit`` off this object, which deliberately does not tick it because
56
+ the store already does. The inference silently defaulted to "decode",
57
+ so every cross-process attach was reported as a decode.
58
+
59
+ Answering directly deletes the inference instead of correcting it. It is
60
+ also the only thread-safe shape: this cache is a module-global shared
61
+ across a node's CONCURRENCY threads, so recording "what I did last" on
62
+ ``self`` would be a race between them.
63
+ """
64
+ if self.l1 is not None:
65
+ arr = self.l1.get(key)
66
+ if arr is not None:
67
+ self.stats["l1_hit"] += 1
68
+ return arr, "l1"
69
+ if self.l2 is not None:
70
+ arr = self.l2.attach(key)
71
+ if arr is not None:
72
+ # The store counts the hit (and the publish, below) itself.
73
+ # Counting it here too doubled both in drain_stats, which
74
+ # sums the tier's counters with the store's.
75
+ # Promote so a crop loop pays one attach, not one per crop.
76
+ if self.l1 is not None:
77
+ try:
78
+ self.l1.set_array(key, arr)
79
+ except Exception: # noqa: BLE001, S110 -- an L1 that can't take the array costs a later attach, never an error
80
+ pass
81
+ return arr, "shm"
82
+ self.stats["miss"] += 1
83
+ return None, "decode"
84
+
85
+ def set_array(self, key: str, arr) -> None:
86
+ """Called by ``_base_array`` after a decode. Raises only :class:`KeyCollision`."""
87
+ if self.l1 is not None:
88
+ try:
89
+ self.l1.set_array(key, arr)
90
+ except Exception: # noqa: BLE001, S110 -- an L1 that can't take the array costs a later decode, never an error
91
+ pass
92
+ if self.l2 is not None:
93
+ try:
94
+ if not self.l2.publish(key, arr):
95
+ self.stats["shm_declined"] += 1
96
+ except KeyCollision:
97
+ # The one exception that must reach the caller (contract §6.4): a
98
+ # broken key scheme hidden here would serve two contents under one key.
99
+ raise
100
+ except Exception: # noqa: BLE001
101
+ self.stats["shm_publish_failed"] += 1
102
+
103
+ def set(self, key: str, value: bytes) -> None:
104
+ """Bytes path, for protocol compliance. ``_base_array`` prefers
105
+ ``set_array`` and will not normally reach this."""
106
+ try:
107
+ import io
108
+ arr = np.load(io.BytesIO(value), allow_pickle=False)
109
+ except Exception: # noqa: BLE001 -- undecodable bytes are a miss: the cache never raises
110
+ return
111
+ self.set_array(key, arr) # outside the try, so a KeyCollision gets out
112
+
113
+ def drain_stats(self) -> Counter:
114
+ """Take and reset counters, including the store's own."""
115
+ out = Counter(self.stats)
116
+ self.stats.clear()
117
+ if self.l2 is not None:
118
+ try:
119
+ out.update(self.l2.drain_stats())
120
+ except Exception: # noqa: BLE001, S110 -- a store whose stats fail still lets the tier's own counters out
121
+ pass
122
+ return out
123
+
124
+ def __repr__(self) -> str:
125
+ return f"<TieredPixelCache l1={self.l1!r} l2={self.l2!r}>"
commonkit/keys.py ADDED
@@ -0,0 +1,229 @@
1
+ """Addressing: the key for a segment, and the path that key names.
2
+
3
+ Key shape::
4
+
5
+ {domain}:{version}:{form}:{digest}:{variant}
6
+
7
+ Path shape::
8
+
9
+ {root}/{form}/{digest[:2]}/{digest}.{variant}.npy
10
+
11
+ Two-level sharding like a CAS, so no directory ever holds a million entries.
12
+
13
+ A :class:`KeyScheme` fixes one domain's vocabulary. The pieces mean:
14
+
15
+ ``domain``
16
+ Who the segments belong to -- ``pixels``, ``audio``. Also the dispatch key:
17
+ schemes register themselves, so :func:`parse` and :func:`relpath` can work
18
+ from a key alone and one store can hold several domains.
19
+ ``version``
20
+ The DECODE CONTRACT, not a release number. Bump it when what a segment
21
+ contains changes meaning, so old segments become unreachable rather than
22
+ being attached by a reader that assumes the new contract. (``pixels`` is at
23
+ ``v3`` because ``v2`` segments could hold LA/RGBA, which ``v3`` readers
24
+ assume away.)
25
+ ``form``
26
+ What KIND of array -- ``pil`` uint8 images, ``f32chw`` model tensors,
27
+ ``wav16k`` int16 audio. Deliberately NOT colour order: a BGR view is
28
+ converted from the one stored RGB segment in about a millisecond against a
29
+ 38 ms decode, and storing both would double the tree to save that
30
+ millisecond while letting the two drift.
31
+ ``variant``
32
+ What distinguishes two renderings of the SAME source bytes. This is the
33
+ axis that differs per domain and the reason the scheme is a class rather
34
+ than a constant: images vary by orientation (``o0`` raw, ``o1`` oriented,
35
+ ``oa`` Apple), a waveform would vary by sample rate (``sr16000``).
36
+
37
+ The form segment precedes the digest so that a domain whose variant suffix is
38
+ already load-bearing elsewhere keeps it at the end of the key, untouched.
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import re
43
+ from collections.abc import Iterable
44
+
45
+ #: Segments are addressed by md5, lowercase. Every pattern here is used with
46
+ #: ``fullmatch``, never ``match`` with ``$``: ``$`` also matches before a final
47
+ #: newline, which let ``"...\n"`` through as a digest or token (contract §3.1).
48
+ _DIGEST_RE = re.compile(r"[0-9a-f]{32}")
49
+
50
+ #: Every other component is a bare lowercase token. Restrictive on purpose:
51
+ #: these become PATH components, and a key is the only thing standing between
52
+ #: a caller and a path the reaper will later be asked to unlink.
53
+ _TOKEN_RE = re.compile(r"[a-z0-9]+")
54
+
55
+ #: Fold ASCII upper case only (contract §3.3): ``str.lower`` folds far more.
56
+ _ASCII_LOWER = str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")
57
+
58
+
59
+ class KeyScheme:
60
+ """One domain's key vocabulary, and the only thing that builds its keys.
61
+
62
+ ``variants`` is either an explicit tuple of legal variants or a regex
63
+ string for an open-ended axis (sample rates, say). Explicit is preferred
64
+ where the set is closed: an unknown variant is then a loud error at the
65
+ call site rather than a segment nobody will ever look for.
66
+ """
67
+
68
+ def __init__(self, domain: str, version: str, forms: Iterable[str],
69
+ variants, dtypes: dict[str, str] | None = None):
70
+ for name, value in (("domain", domain), ("version", version)):
71
+ if not _TOKEN_RE.fullmatch(str(value)):
72
+ raise ValueError(f"{name} must be [a-z0-9]+, got {value!r}")
73
+ self.domain = str(domain)
74
+ self.version = str(version)
75
+ self.forms = tuple(forms)
76
+ if not self.forms:
77
+ raise ValueError(f"{self.domain}: at least one form is required")
78
+ for form in self.forms:
79
+ if not _TOKEN_RE.fullmatch(form):
80
+ raise ValueError(f"form must be [a-z0-9]+, got {form!r}")
81
+ if len(set(self.forms)) != len(self.forms):
82
+ raise ValueError(f"{self.domain}: forms must be distinct, got {self.forms}")
83
+
84
+ if isinstance(variants, str):
85
+ self.variants: tuple[str, ...] = ()
86
+ self._variant_re = re.compile(variants)
87
+ #: The pattern's text, which is what makes two pattern schemes the
88
+ #: same or different (contract §3.4).
89
+ self.variant_pattern: str | None = variants
90
+ else:
91
+ self.variants = tuple(variants)
92
+ if not self.variants:
93
+ raise ValueError(f"{self.domain}: at least one variant")
94
+ for variant in self.variants:
95
+ if not _TOKEN_RE.fullmatch(variant):
96
+ raise ValueError(f"variant must be [a-z0-9]+, got {variant!r}")
97
+ self._variant_re = None
98
+ self.variant_pattern = None
99
+
100
+ # form -> required numpy dtype NAME, for forms whose consumers rely on
101
+ # one. Declared here rather than branched on inside the store, which
102
+ # would put one domain's policy in the generic layer. A segment that
103
+ # violates it is refused at publish: a bad segment is not one bad call,
104
+ # it is published once and then poisons every reader that attaches it,
105
+ # on a path where a miss is meant to be the worst outcome available.
106
+ self.dtypes = dict(dtypes or {})
107
+ for form in self.dtypes:
108
+ if form not in self.forms:
109
+ raise ValueError(f"{self.domain}: dtype for unknown form {form!r}")
110
+
111
+ self._key_re = re.compile(
112
+ rf"{re.escape(self.domain)}:{re.escape(self.version)}:"
113
+ r"(?P<form>[a-z0-9]+):(?P<digest>[0-9a-f]{32}):"
114
+ r"(?P<variant>[a-z0-9]+)")
115
+
116
+ # ------------------------------------------------------------- building
117
+
118
+ @property
119
+ def default_form(self) -> str:
120
+ return self.forms[0]
121
+
122
+ def dtype_for(self, form: str) -> str | None:
123
+ """The dtype ``form`` must have, or None when anything goes."""
124
+ return self.dtypes.get(form)
125
+
126
+ def valid_variant(self, variant: str) -> bool:
127
+ """A token, and in the closed set or matched WHOLE by the pattern (contract §3.2).
128
+
129
+ ``re.match`` anchored only the start, so ``sr[0-9]+`` accepted ``sr16000x``;
130
+ and a pattern like ``sr.+`` could accept ``sr-1``, which no parser reads back.
131
+ """
132
+ variant = str(variant)
133
+ if not _TOKEN_RE.fullmatch(variant):
134
+ return False
135
+ if self._variant_re is not None:
136
+ return self._variant_re.fullmatch(variant) is not None
137
+ return variant in self.variants
138
+
139
+ def key(self, digest: str, variant: str, form: str | None = None) -> str:
140
+ """The store key for one array. Raises on anything it cannot address."""
141
+ digest = str(digest).translate(_ASCII_LOWER)
142
+ if not _DIGEST_RE.fullmatch(digest):
143
+ raise ValueError(f"not an md5 digest: {digest!r}")
144
+ form = self.default_form if form is None else str(form)
145
+ if form not in self.forms:
146
+ raise ValueError(
147
+ f"{self.domain}: unknown form {form!r}; have {self.forms}")
148
+ variant = str(variant)
149
+ if not self.valid_variant(variant):
150
+ raise ValueError(f"{self.domain}: unknown variant {variant!r}")
151
+ return f"{self.domain}:{self.version}:{form}:{digest}:{variant}"
152
+
153
+ # -------------------------------------------------------------- reading
154
+
155
+ def parse(self, key: str) -> tuple[str, str, str]:
156
+ """``key -> (form, digest, variant)``. Raises on anything unparseable.
157
+
158
+ Strict on purpose: this is the only thing between a malformed key and
159
+ a path the reaper might later unlink.
160
+ """
161
+ m = self._key_re.fullmatch(str(key))
162
+ if m is None:
163
+ raise ValueError(f"unparseable {self.domain} key: {key!r}")
164
+ form, variant = m.group("form"), m.group("variant")
165
+ if form not in self.forms or not self.valid_variant(variant):
166
+ raise ValueError(f"unparseable {self.domain} key: {key!r}")
167
+ return form, m.group("digest"), variant
168
+
169
+ def relpath(self, key: str) -> str:
170
+ """``key -> "{form}/{2-hex}/{digest}.{variant}.npy"``, never absolute."""
171
+ form, digest, variant = self.parse(key)
172
+ return f"{form}/{digest[:2]}/{digest}.{variant}.npy"
173
+
174
+ def __repr__(self) -> str:
175
+ return f"<KeyScheme {self.domain}:{self.version} forms={self.forms}>"
176
+
177
+
178
+ # ------------------------------------------------------------------ registry
179
+ #
180
+ # Dispatch by domain is what lets ONE store hold several domains: the store
181
+ # parses a key without being told which scheme it belongs to, so it never has
182
+ # to grow a parameter for something the key already says.
183
+
184
+ _REGISTRY: dict[str, KeyScheme] = {}
185
+
186
+
187
+ def register(scheme: KeyScheme) -> KeyScheme:
188
+ """Register ``scheme``, refusing a second, different one for its domain.
189
+
190
+ Idempotent for an identical re-registration, because a module that binds a
191
+ scheme at import time may legitimately be imported twice under different
192
+ names. A DISAGREEING second registration is the bug worth catching: two
193
+ spellings of one domain means the publisher writes one key and the reader
194
+ asks for another, and the hit rate silently goes to zero with no error.
195
+ """
196
+ prior = _REGISTRY.get(scheme.domain)
197
+ if prior is not None:
198
+ same = (prior.version == scheme.version
199
+ and prior.forms == scheme.forms
200
+ # A closed variant SET (contract §3.4): order carries no meaning there,
201
+ # unlike forms, whose first is the default.
202
+ and set(prior.variants) == set(scheme.variants)
203
+ and prior.variant_pattern == scheme.variant_pattern
204
+ and prior.dtypes == scheme.dtypes)
205
+ if not same:
206
+ raise ValueError(
207
+ f"two disagreeing schemes for domain {scheme.domain!r}")
208
+ return prior
209
+ _REGISTRY[scheme.domain] = scheme
210
+ return scheme
211
+
212
+
213
+ def scheme_for(key: str) -> KeyScheme:
214
+ """The registered scheme that owns ``key``."""
215
+ domain = str(key).split(":", 1)[0]
216
+ scheme = _REGISTRY.get(domain)
217
+ if scheme is None:
218
+ raise ValueError(f"no registered scheme for key: {key!r}")
219
+ return scheme
220
+
221
+
222
+ def parse(key: str) -> tuple[str, str, str]:
223
+ """``key -> (form, digest, variant)``, dispatching on the key's domain."""
224
+ return scheme_for(key).parse(key)
225
+
226
+
227
+ def relpath(key: str) -> str:
228
+ """``key -> relative segment path``, dispatching on the key's domain."""
229
+ return scheme_for(key).relpath(key)
commonkit/read.py ADDED
@@ -0,0 +1,49 @@
1
+ """Counting what a lookup did, in ONE vocabulary.
2
+
3
+ A store answers three ways and the names for them are worth owning centrally,
4
+ because the alternative is what happened before this module existed: two paths
5
+ to the same pixels each decided independently what to call a hit, wrote the
6
+ SAME counter names for DIFFERENT events, and nothing downstream could tell
7
+ which one it was reading. One of them classified by counter DELTA and was
8
+ wrong for two days; a correct production measurement was retracted on the
9
+ strength of the ambiguity.
10
+
11
+ The counter PREFIX is the caller's, not ours: ``pixels_shm`` and ``audio_shm``
12
+ are different measurements and must not collide in one stats table.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ #: Every tier a lookup can be answered by. ``decode`` means "nothing had it" --
19
+ #: the caller recomputes, which is always allowed and never an error.
20
+ SOURCES = ("l1", "shm", "decode")
21
+
22
+
23
+ def note(stats: Any, source: str, *, prefix: str) -> str:
24
+ """Tick ``{prefix}{source}``. THE one place that counter is written.
25
+
26
+ Returns ``source`` so callers can use this inline. A bad source name is a
27
+ programming error and raises: these counters are the instrument the whole
28
+ exercise is judged by, and a typo would silently invent a tier.
29
+ """
30
+ if source not in SOURCES:
31
+ raise ValueError(f"unknown source {source!r}; have {SOURCES}")
32
+ if stats is not None:
33
+ stats[f"{prefix}{source}"] += 1
34
+ return source
35
+
36
+
37
+ def attach(store: Any, key: str) -> Any | None:
38
+ """The array for ``key``, or None. A miss is never an error.
39
+
40
+ An absent store, an unusable key, an absent segment and a torn file all
41
+ return None, because every caller's answer to all four is the same:
42
+ recompute it.
43
+ """
44
+ if store is None or not key:
45
+ return None
46
+ try:
47
+ return store.attach(key)
48
+ except Exception: # noqa: BLE001
49
+ return None
commonkit/shared.py ADDED
@@ -0,0 +1,227 @@
1
+ """The ONE store per host that every commonkit user shares, and its tree-wide policy.
2
+
3
+ Why one store rather than one per application, measured on an M4 Max (macOS, 16 KB pages)
4
+ by the first consumer:
5
+
6
+ * Every budget is enforced per TREE, by whichever process happens to reap. Two
7
+ applications with two roots therefore hold the SUM of their budgets, and neither
8
+ sees the other's usage. One root means one budget and one age-out for everything.
9
+ * On macOS, Time Machine's hourly local snapshots keep a reaped segment's blocks
10
+ allocated (a 539 MB segment reaped after a snapshot freed 0 MB, TM exclusion of
11
+ ~/Library/Caches notwithstanding). A tree that turns over within the hour can pin up
12
+ to ~24x its budget of purgeable space until the snapshots thin. That is why the macOS
13
+ budget is 2 GB, and why a second tree with its own budget multiplies the pinned space.
14
+
15
+ Tree-wide policy -- root, byte budget, TTL, free-space floor, reap interval, largest
16
+ array, fsync, republish verification -- comes ONLY from the tree's ``.policy.json``, the
17
+ environment (``COMMONKIT_STORE_*``) or the defaults here, never from a caller, because the
18
+ reaper applies whatever the reaping process was given to every segment in the tree: a
19
+ caller with a longer TTL or a bigger budget would silently override everyone else's. A
20
+ caller chooses only what concerns itself: ``mode``, how it reads (``attach``: ``mmap``
21
+ shares one physical copy, ``read`` gives it a private one), and the smallest array worth
22
+ storing. Tests may pass a ``root`` to stay out of the shared tree.
23
+
24
+ The policy FILE wins over the environment (C7), because the environment is per process
25
+ and the tree is not: on thresher the policy lived in ``~/.bashrc``, so an app launched
26
+ from the Finder ran with the defaults and reaped a 7 GB tree down to 2 GB. A file at the
27
+ root is seen by every process that finds the root. The environment fills only what the
28
+ file leaves out (contract §10.6).
29
+
30
+ Segments of different domains coexist because forms partition the tree
31
+ (``{form}/{digest[:2]}/...``). Form names are therefore global: pick ones no other
32
+ domain uses.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import json
37
+ import math
38
+ import os
39
+ import re
40
+ import stat
41
+ import sys
42
+
43
+ from .store import SharedArrayStore
44
+
45
+ ENV_PREFIX = "COMMONKIT_STORE_"
46
+ _DARWIN = sys.platform == "darwin"
47
+
48
+ #: Per-platform defaults. macOS: the user cache directory on the internal SSD -- it has no
49
+ #: /dev/shm, the SSD beat a RAM disk on first attach (0.69 ms against 0.76 ms), and a
50
+ #: ``ram://`` disk commits its whole size up front as dirty memory. Linux: /dev/shm, which
51
+ #: is RAM, so the budget is its only real bound.
52
+ DEFAULTS = {
53
+ "ROOT": (os.path.expanduser("~/Library/Caches/commonkit/store") if _DARWIN
54
+ else "/dev/shm/commonkit"),
55
+ "MAX_GB": 2.0 if _DARWIN else 8.0,
56
+ "MIN_FREE_GB": 8.0,
57
+ "TTL_S": 900.0,
58
+ "REAP_S": 30.0,
59
+ "MAX_ARRAY_BYTES": 128 * 2**20,
60
+ "FSYNC": False,
61
+ "VERIFY_REPUBLISH": False,
62
+ }
63
+
64
+ #: The tree's policy file, directly under the root (contract §10.5), and the most of it
65
+ #: that is read: a real one is a few hundred bytes.
66
+ POLICY_FILE = ".policy.json"
67
+ _POLICY_MAX_BYTES = 65536
68
+
69
+ #: The settings that become byte counts, and their scale. The derived integer must fit
70
+ #: in 64 signed bits (contract §10.2), or a value is malformed: ``int(float("inf"))``
71
+ #: raised OverflowError out of ``shared_store`` until 2026-10-28.
72
+ _BYTE_SCALE = {"MAX_GB": 2**30, "MIN_FREE_GB": 2**30, "MAX_ARRAY_BYTES": 1}
73
+
74
+ #: The number grammar (contract §10.2). ``float()`` is far looser: ``inf``, ``nan``,
75
+ #: ``1_000``, Arabic-Indic digits and Unicode whitespace all parse, and a Rust or C
76
+ #: reader of the same environment would disagree with every one of them.
77
+ _NUMBER_RE = re.compile(r"[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?")
78
+ _ASCII_SPACE = " \t\n\v\f\r"
79
+ _FLAG_ON = frozenset({"1", "true", "yes", "on"})
80
+
81
+
82
+ def parse_number(raw: str) -> float | None:
83
+ """A finite float from ``raw`` under the contract's grammar, or None if malformed."""
84
+ text = raw.strip(_ASCII_SPACE)
85
+ if not _NUMBER_RE.fullmatch(text):
86
+ return None
87
+ value = float(text) # the grammar is a subset of float()'s
88
+ return value if math.isfinite(value) else None
89
+
90
+
91
+ def parse_flag(raw: str) -> bool:
92
+ """Whether ``raw`` turns a flag on (contract §10.3): exact, ASCII case-insensitive.
93
+
94
+ ``isascii`` first, because ``str.lower`` folds beyond ASCII. Nothing is trimmed:
95
+ ``" 1"`` is off, so a value means one thing whichever language reads it.
96
+ """
97
+ return raw.isascii() and raw.lower() in _FLAG_ON
98
+
99
+
100
+ class _Object(dict):
101
+ """A JSON object that remembers whether its text repeated a key."""
102
+
103
+ duplicate = False
104
+
105
+
106
+ def _object_pairs(pairs):
107
+ obj = _Object(pairs)
108
+ obj.duplicate = len(obj) != len(pairs)
109
+ return obj
110
+
111
+
112
+ def _not_json(name):
113
+ raise ValueError(f"{name} is not JSON")
114
+
115
+
116
+ def read_policy(root) -> dict:
117
+ """The tree's ``.policy.json`` as a dict, or ``{}`` when it is absent or invalid.
118
+
119
+ Invalid is ENTIRELY absent (contract §10.5): not a regular file, over 64 KiB,
120
+ not UTF-8, not JSON (``NaN`` and ``Infinity`` included), not an object, or a
121
+ repeated top-level key, which JSON parsers resolve differently. Never raises.
122
+ """
123
+ path = os.path.join(os.path.abspath(str(root)), POLICY_FILE)
124
+ try:
125
+ # stat first: opening a FIFO someone named .policy.json would block forever.
126
+ st = os.stat(path)
127
+ if not stat.S_ISREG(st.st_mode) or st.st_size > _POLICY_MAX_BYTES:
128
+ return {}
129
+ with open(path, "rb") as fh:
130
+ data = fh.read(_POLICY_MAX_BYTES + 1)
131
+ if len(data) > _POLICY_MAX_BYTES:
132
+ return {}
133
+ # Decoded first: json.loads on bytes would also accept UTF-16 and UTF-32.
134
+ doc = json.loads(data.decode("utf-8"), object_pairs_hook=_object_pairs,
135
+ parse_constant=_not_json)
136
+ except Exception: # noqa: BLE001
137
+ return {}
138
+ if not isinstance(doc, _Object) or doc.duplicate:
139
+ return {}
140
+ return dict(doc)
141
+
142
+
143
+ def _from_policy(name: str, value):
144
+ """``value`` from the policy file as a setting of ``name``'s kind, or None (left out)."""
145
+ if isinstance(DEFAULTS[name], bool):
146
+ if isinstance(value, bool):
147
+ return value
148
+ return parse_flag(value) if isinstance(value, str) else None
149
+ if isinstance(value, bool): # bool is an int to Python, never a number here
150
+ return None
151
+ if isinstance(value, (int, float)):
152
+ try:
153
+ value = float(value) # an integer past the double range overflows
154
+ except OverflowError:
155
+ return None
156
+ return value if math.isfinite(value) else None
157
+ return parse_number(value) if isinstance(value, str) else None
158
+
159
+
160
+ def _from_env(name: str):
161
+ raw = os.environ.get(ENV_PREFIX + name)
162
+ if raw is None or raw == "":
163
+ return None
164
+ if isinstance(DEFAULTS[name], bool):
165
+ return parse_flag(raw)
166
+ return parse_number(raw)
167
+
168
+
169
+ def _in_range(name: str, value) -> bool:
170
+ scale = _BYTE_SCALE.get(name)
171
+ if scale is None:
172
+ return True
173
+ try:
174
+ n = int(value * scale) # trunc; inf (1e300 GiB) raises OverflowError
175
+ except (OverflowError, ValueError):
176
+ return False
177
+ return -2**63 <= n <= 2**63 - 1
178
+
179
+
180
+ def setting(name: str, policy: dict | None = None):
181
+ """The effective tree-wide value of ``name`` (a key of DEFAULTS).
182
+
183
+ The first valid one of: ``policy`` (a :func:`read_policy` result), the environment,
184
+ the default (contract §10.6). ``ROOT`` never comes from the policy file, which is
185
+ found BY the root. A malformed value is skipped rather than raised: like the store
186
+ itself, configuration must never be the reason a caller fails.
187
+ """
188
+ default = DEFAULTS[name]
189
+ if name == "ROOT":
190
+ raw = os.environ.get(ENV_PREFIX + name)
191
+ return os.path.expanduser(raw) if raw else default
192
+ candidates = []
193
+ if policy and name in policy:
194
+ candidates.append(_from_policy(name, policy[name]))
195
+ candidates.append(_from_env(name))
196
+ for value in candidates:
197
+ if value is not None and _in_range(name, value):
198
+ return type(default)(value)
199
+ return default
200
+
201
+
202
+ def shared_store(*, mode: str = "readwrite", attach: str = "mmap",
203
+ min_array_bytes: int = 256 * 1024, root=None) -> SharedArrayStore:
204
+ """The host's shared store, with the tree-wide policy from ``.policy.json`` and the
205
+ environment. The file is read once, here.
206
+
207
+ Never raises (the store's own contract): an unusable root comes up disabled.
208
+ """
209
+ root = root or setting("ROOT")
210
+ policy = read_policy(root)
211
+
212
+ def get(name):
213
+ return setting(name, policy)
214
+
215
+ return SharedArrayStore(
216
+ root,
217
+ mode=mode,
218
+ attach=attach,
219
+ max_bytes=int(get("MAX_GB") * 2**30),
220
+ min_free_bytes=int(get("MIN_FREE_GB") * 2**30),
221
+ ttl_s=get("TTL_S"),
222
+ min_array_bytes=min_array_bytes,
223
+ max_array_bytes=get("MAX_ARRAY_BYTES"),
224
+ reap_interval_s=get("REAP_S"),
225
+ fsync=get("FSYNC"),
226
+ verify_republish=get("VERIFY_REPUBLISH"),
227
+ )