pyuncommonkit 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sean D. True
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyuncommonkit
3
+ Version: 0.1.0
4
+ Summary: Named shared-memory segments for numpy arrays: publish once, attach from any process.
5
+ Keywords: numpy,npy,mmap,shared-memory,cache
6
+ Author: Sean D. True
7
+ Author-email: Sean D. True <sean.true@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Filesystems
24
+ Requires-Dist: numpy>=1.26
25
+ Requires-Dist: pytest>=8 ; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6 ; extra == 'dev'
27
+ Requires-Python: >=3.11
28
+ Project-URL: Homepage, https://github.com/seantrue/pycommonkit
29
+ Project-URL: Repository, https://github.com/seantrue/pycommonkit
30
+ Project-URL: Issues, https://github.com/seantrue/pycommonkit/issues
31
+ Project-URL: Contract, https://github.com/seantrue/pycommonkit/blob/main/docs/contract.md
32
+ Project-URL: Rust crate, https://crates.io/crates/commonkit
33
+ Provides-Extra: dev
34
+ Description-Content-Type: text/markdown
35
+
36
+ <p align="center">
37
+ <img src="https://raw.githubusercontent.com/seantrue/pycommonkit/main/docs/images/commons-banner.jpg"
38
+ alt="Illustrated aerial view of farmland: square fields and center-pivot circles on a shared road grid"
39
+ width="100%">
40
+ </p>
41
+
42
+ # pycommonkit
43
+
44
+ Named shared-memory segments for numpy arrays. One process decodes and
45
+ publishes; every other process attaches the same physical pages.
46
+
47
+ Installed as **`pyuncommonkit`**, imported as **`commonkit`** (the PyPI name `commonkit` belongs
48
+ to an unrelated project, and PyPI refused `pycommonkit` as too similar to it). A Rust crate, `commonkit`, implementing the same on-disk contract is
49
+ in this repository under `rust/` (`docs/rust-crate-plan.md`): keys, segments, the store and the
50
+ reaper are done, and a cross-language conformance suite runs both on one tree.
51
+
52
+ The name is for Fortran's **named `COMMON` block**. `COMMON /GRID/ X, Y` in one
53
+ subroutine and the same name in another, compiled separately, give both the same
54
+ storage: the linker joins them by the name alone, and no unit owns the block. The
55
+ name is the whole interface. A commonkit key is the same idea: any process that
56
+ can spell the key reaches the same bytes, without knowing who wrote them.
57
+
58
+ Where the analogy stops is where commonkit had to be stricter:
59
+
60
+ * **Across processes, not within one program.** A `COMMON` block is shared by the
61
+ program units of one executable; a segment is shared by separate processes.
62
+ * **The layout travels with the data.** Each Fortran unit declares its own view
63
+ of a block, and a unit that declares it differently silently reads other
64
+ variables' bits. A segment carries its dtype, shape and order in its `.npy`
65
+ header, so every reader sees the same layout, and publishing a different array
66
+ under an existing key is refused loudly (`KeyCollision`).
67
+ * **Written once, then read-only.** Any unit may assign into a `COMMON` block at
68
+ any time. A segment is written once, whole, and readers map it read-only.
69
+ * **It may not be there.** A `COMMON` block exists for the whole run. A segment
70
+ exists only until the reaper needs the space, which is why every caller must be
71
+ able to recompute it (below).
72
+
73
+ ## The contract
74
+
75
+ `docs/contract.md` (version 1) is the authority on the on-disk format: keys, layout, the `.npy`
76
+ segment, publishing, the write-once rules, touch, the reaper, policy and counters. Python is the
77
+ reference implementation and `conformance/vectors/` decides any disagreement
78
+ (`conformance/generate.py` regenerates the fixtures byte for byte). Both the Python package and
79
+ the Rust crate pass every vector.
80
+
81
+ ## Status
82
+
83
+ See `docs/STATUS.md`. Nothing is published to PyPI or crates.io yet.
84
+
85
+ ## What it is
86
+
87
+ * `SharedArrayStore` — publish/attach `.npy` segments under a root directory,
88
+ with admission gates, a TTL reaper and a byte budget.
89
+ * `KeyScheme` — the addressing vocabulary for one *domain*. A scheme fixes the
90
+ domain, a version, the legal forms and the legal variants, and turns
91
+ `(digest, variant, form)` into both a key and a relative path.
92
+ * `shared_store()` — the ONE store per host that every user shares. Its byte budget, TTL,
93
+ free-space floor, fsync and republish verification come only from the tree's `.policy.json`
94
+ (which wins), then `COMMONKIT_STORE_*` environment variables, then the defaults (macOS:
95
+ `~/Library/Caches/commonkit/store`, 2 GB, 900 s, 8 GB free), because the reaping process applies
96
+ its own policy to the whole tree; the root comes from the environment or the default. A caller
97
+ chooses only `mode` and the smallest array worth storing.
98
+ * `TieredPixelCache` — an in-process L1 memo in front of a store, so a loop
99
+ that looks up the same digest repeatedly pays one attach rather than one per
100
+ call.
101
+
102
+ ## A cache, not a store of record
103
+
104
+ **A missing segment is never an error, only a slower path.** The store promises
105
+ that a key which is present names the right bytes. It never promises that a key
106
+ is present. So every caller must be able to compute the value again when
107
+ `attach` returns `None`, and must treat a `publish` that returns `False` as
108
+ normal.
109
+
110
+ A segment can be absent at any moment, for reasons the caller doesn't control:
111
+
112
+ * **Its publish was declined.** Under pressure the store refuses rather than
113
+ blocks: the array was smaller than worth storing or larger than allowed, the
114
+ volume was below its free-space floor, or the tree was over its byte budget.
115
+ A decline is how the store survives back-pressure, not a fault.
116
+ * **The reaper removed it.** Segments are evicted when they expire, least
117
+ recently used first to get back under the byte budget, and to restore the
118
+ free-space floor. The reaper doesn't know who might still want a segment, and
119
+ doesn't ask: no reference counts, no coordination between processes.
120
+ * **It was torn.** A file shorter than its header promises, after a crash with
121
+ `fsync` off (the default) or damage from outside the contract, is a miss.
122
+ * **The store is disabled.** The mode is `off`, or the root can't be created or
123
+ written: every attach misses and every publish declines.
124
+ * **The tree is gone.** On Linux it lives in `/dev/shm` and does not survive a
125
+ reboot; any root can be cleared; another machine has its own tree.
126
+
127
+ That one rule is what makes the rest cheap. The reaper can unlink freely,
128
+ publish never blocks, writes need no durability, and a process that fails
129
+ costs the others only time: the next reader recomputes.
130
+
131
+ **What does raise.** Exactly two things, both bugs at the call site rather than
132
+ conditions of the store: building a key that isn't valid for its scheme
133
+ (`ValueError`), and publishing a *different* array under a key that already
134
+ holds one (`KeyCollision`, a `ValueError` subclass), which means a key scheme or
135
+ a digest is broken. Everything else (an absent root, a full filesystem, a
136
+ reaped or torn segment, a hostile `.usage.json` or `.policy.json`) returns
137
+ `None` or `False` and ticks a counter. The constructor never raises: an
138
+ unusable root leaves the store `disabled` and every method a no-op.
139
+
140
+ **Replacing a value under the same key.** A segment is written once: publishing
141
+ the same array again only refreshes it, and a different array raises
142
+ `KeyCollision`. A caller that must replace a value whose key hasn't changed (a
143
+ recompute after a fix the key doesn't capture) calls `remove(key)` first, then
144
+ publishes. `remove` only unlinks, so a reader still holding the old segment keeps
145
+ its bytes; there is no in-use check.
146
+
147
+ **A value that can't be recomputed doesn't belong here alone.** Keep its
148
+ durable copy somewhere you own and use the store to share it fast. (A pinned
149
+ subtree the reaper skips is a possible later addition; it isn't built.)
150
+
151
+ **Callers never need `.usage.json`.** It is the reaper's record of how much the
152
+ tree holds, read only by the byte-budget gate. When it is missing or stale,
153
+ usage is unknown, that gate passes, and the free-space floor still applies;
154
+ the next complete reap writes a fresh one.
155
+
156
+ ## Keys
157
+
158
+ {domain}:{version}:{form}:{digest}:{variant}
159
+
160
+ and the path it names:
161
+
162
+ {root}/{form}/{digest[:2]}/{digest}.{variant}.npy
163
+
164
+ Two-level sharding, so no directory holds a million entries. The `variant` axis
165
+ is whatever distinguishes two renderings of the same source: for images it is
166
+ orientation (`o0`, `o1`, `oa`), for audio it would be sample rate (`sr16000`).
167
+ Schemes are registered by domain so `parse()` and `relpath()` can dispatch on a
168
+ key alone, which is what lets one store hold several domains.
169
+
170
+ ## Tests
171
+
172
+ uv run --extra dev pytest -q # includes the conformance vectors
173
+
174
+ The suite addresses audio and video rather than images, to show the package is
175
+ not pixel-specific. Two modules run end to end against real media and skip
176
+ themselves when their tool is not on `PATH`:
177
+
178
+ * `tests/test_sox.py` — `sox` synthesizes a sine wave and dumps it as raw
179
+ int16 (and float32) samples. The samples are published, attached back as a
180
+ read-only `np.memmap`, attached again from a separate process, and stored at
181
+ two sample rates as two variants (`sr16000`, `sr44100`) of one digest.
182
+ * `tests/test_ffmpeg.py` — `ffmpeg` renders its `testsrc` pattern to
183
+ `rawvideo` frames, stored as one `(T, H, W, C)` array. The same checks, with
184
+ the frame rate as the variant (`fr10`, `fr25`), plus a clip over
185
+ `max_array_bytes` being declined whole rather than truncated.
186
+
187
+ Both use their tool's built-in generators, so no media files are checked in.
188
+ Both also exercise per-form dtypes: `dtypes` maps a *form* to the dtype it
189
+ requires (`{"wav16k": "int16"}`), and a float dump is refused under the
190
+ integer form and accepted under the float one. A form's name is only a label —
191
+ the store enforces a dtype only for forms listed in `dtypes`.
192
+
193
+ ## Origin
194
+
195
+ Lifted on 2026-10-27 from `seantrue/commonkit` (`main` at `4dbfc7d`, plus the contract and
196
+ vectors from its `rust-crate-plan` branch at `2b6bcf9` and after), with a fresh history. That
197
+ package was extracted from the pixel-cache layer of a private image-corpus
198
+ application, where it ran in production first. The measurements in the module
199
+ docstrings were taken there; the framework-coupled half (the settings wiring
200
+ and the decode entry point) deliberately stayed behind.
@@ -0,0 +1,165 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/seantrue/pycommonkit/main/docs/images/commons-banner.jpg"
3
+ alt="Illustrated aerial view of farmland: square fields and center-pivot circles on a shared road grid"
4
+ width="100%">
5
+ </p>
6
+
7
+ # pycommonkit
8
+
9
+ Named shared-memory segments for numpy arrays. One process decodes and
10
+ publishes; every other process attaches the same physical pages.
11
+
12
+ Installed as **`pyuncommonkit`**, imported as **`commonkit`** (the PyPI name `commonkit` belongs
13
+ to an unrelated project, and PyPI refused `pycommonkit` as too similar to it). A Rust crate, `commonkit`, implementing the same on-disk contract is
14
+ in this repository under `rust/` (`docs/rust-crate-plan.md`): keys, segments, the store and the
15
+ reaper are done, and a cross-language conformance suite runs both on one tree.
16
+
17
+ The name is for Fortran's **named `COMMON` block**. `COMMON /GRID/ X, Y` in one
18
+ subroutine and the same name in another, compiled separately, give both the same
19
+ storage: the linker joins them by the name alone, and no unit owns the block. The
20
+ name is the whole interface. A commonkit key is the same idea: any process that
21
+ can spell the key reaches the same bytes, without knowing who wrote them.
22
+
23
+ Where the analogy stops is where commonkit had to be stricter:
24
+
25
+ * **Across processes, not within one program.** A `COMMON` block is shared by the
26
+ program units of one executable; a segment is shared by separate processes.
27
+ * **The layout travels with the data.** Each Fortran unit declares its own view
28
+ of a block, and a unit that declares it differently silently reads other
29
+ variables' bits. A segment carries its dtype, shape and order in its `.npy`
30
+ header, so every reader sees the same layout, and publishing a different array
31
+ under an existing key is refused loudly (`KeyCollision`).
32
+ * **Written once, then read-only.** Any unit may assign into a `COMMON` block at
33
+ any time. A segment is written once, whole, and readers map it read-only.
34
+ * **It may not be there.** A `COMMON` block exists for the whole run. A segment
35
+ exists only until the reaper needs the space, which is why every caller must be
36
+ able to recompute it (below).
37
+
38
+ ## The contract
39
+
40
+ `docs/contract.md` (version 1) is the authority on the on-disk format: keys, layout, the `.npy`
41
+ segment, publishing, the write-once rules, touch, the reaper, policy and counters. Python is the
42
+ reference implementation and `conformance/vectors/` decides any disagreement
43
+ (`conformance/generate.py` regenerates the fixtures byte for byte). Both the Python package and
44
+ the Rust crate pass every vector.
45
+
46
+ ## Status
47
+
48
+ See `docs/STATUS.md`. Nothing is published to PyPI or crates.io yet.
49
+
50
+ ## What it is
51
+
52
+ * `SharedArrayStore` — publish/attach `.npy` segments under a root directory,
53
+ with admission gates, a TTL reaper and a byte budget.
54
+ * `KeyScheme` — the addressing vocabulary for one *domain*. A scheme fixes the
55
+ domain, a version, the legal forms and the legal variants, and turns
56
+ `(digest, variant, form)` into both a key and a relative path.
57
+ * `shared_store()` — the ONE store per host that every user shares. Its byte budget, TTL,
58
+ free-space floor, fsync and republish verification come only from the tree's `.policy.json`
59
+ (which wins), then `COMMONKIT_STORE_*` environment variables, then the defaults (macOS:
60
+ `~/Library/Caches/commonkit/store`, 2 GB, 900 s, 8 GB free), because the reaping process applies
61
+ its own policy to the whole tree; the root comes from the environment or the default. A caller
62
+ chooses only `mode` and the smallest array worth storing.
63
+ * `TieredPixelCache` — an in-process L1 memo in front of a store, so a loop
64
+ that looks up the same digest repeatedly pays one attach rather than one per
65
+ call.
66
+
67
+ ## A cache, not a store of record
68
+
69
+ **A missing segment is never an error, only a slower path.** The store promises
70
+ that a key which is present names the right bytes. It never promises that a key
71
+ is present. So every caller must be able to compute the value again when
72
+ `attach` returns `None`, and must treat a `publish` that returns `False` as
73
+ normal.
74
+
75
+ A segment can be absent at any moment, for reasons the caller doesn't control:
76
+
77
+ * **Its publish was declined.** Under pressure the store refuses rather than
78
+ blocks: the array was smaller than worth storing or larger than allowed, the
79
+ volume was below its free-space floor, or the tree was over its byte budget.
80
+ A decline is how the store survives back-pressure, not a fault.
81
+ * **The reaper removed it.** Segments are evicted when they expire, least
82
+ recently used first to get back under the byte budget, and to restore the
83
+ free-space floor. The reaper doesn't know who might still want a segment, and
84
+ doesn't ask: no reference counts, no coordination between processes.
85
+ * **It was torn.** A file shorter than its header promises, after a crash with
86
+ `fsync` off (the default) or damage from outside the contract, is a miss.
87
+ * **The store is disabled.** The mode is `off`, or the root can't be created or
88
+ written: every attach misses and every publish declines.
89
+ * **The tree is gone.** On Linux it lives in `/dev/shm` and does not survive a
90
+ reboot; any root can be cleared; another machine has its own tree.
91
+
92
+ That one rule is what makes the rest cheap. The reaper can unlink freely,
93
+ publish never blocks, writes need no durability, and a process that fails
94
+ costs the others only time: the next reader recomputes.
95
+
96
+ **What does raise.** Exactly two things, both bugs at the call site rather than
97
+ conditions of the store: building a key that isn't valid for its scheme
98
+ (`ValueError`), and publishing a *different* array under a key that already
99
+ holds one (`KeyCollision`, a `ValueError` subclass), which means a key scheme or
100
+ a digest is broken. Everything else (an absent root, a full filesystem, a
101
+ reaped or torn segment, a hostile `.usage.json` or `.policy.json`) returns
102
+ `None` or `False` and ticks a counter. The constructor never raises: an
103
+ unusable root leaves the store `disabled` and every method a no-op.
104
+
105
+ **Replacing a value under the same key.** A segment is written once: publishing
106
+ the same array again only refreshes it, and a different array raises
107
+ `KeyCollision`. A caller that must replace a value whose key hasn't changed (a
108
+ recompute after a fix the key doesn't capture) calls `remove(key)` first, then
109
+ publishes. `remove` only unlinks, so a reader still holding the old segment keeps
110
+ its bytes; there is no in-use check.
111
+
112
+ **A value that can't be recomputed doesn't belong here alone.** Keep its
113
+ durable copy somewhere you own and use the store to share it fast. (A pinned
114
+ subtree the reaper skips is a possible later addition; it isn't built.)
115
+
116
+ **Callers never need `.usage.json`.** It is the reaper's record of how much the
117
+ tree holds, read only by the byte-budget gate. When it is missing or stale,
118
+ usage is unknown, that gate passes, and the free-space floor still applies;
119
+ the next complete reap writes a fresh one.
120
+
121
+ ## Keys
122
+
123
+ {domain}:{version}:{form}:{digest}:{variant}
124
+
125
+ and the path it names:
126
+
127
+ {root}/{form}/{digest[:2]}/{digest}.{variant}.npy
128
+
129
+ Two-level sharding, so no directory holds a million entries. The `variant` axis
130
+ is whatever distinguishes two renderings of the same source: for images it is
131
+ orientation (`o0`, `o1`, `oa`), for audio it would be sample rate (`sr16000`).
132
+ Schemes are registered by domain so `parse()` and `relpath()` can dispatch on a
133
+ key alone, which is what lets one store hold several domains.
134
+
135
+ ## Tests
136
+
137
+ uv run --extra dev pytest -q # includes the conformance vectors
138
+
139
+ The suite addresses audio and video rather than images, to show the package is
140
+ not pixel-specific. Two modules run end to end against real media and skip
141
+ themselves when their tool is not on `PATH`:
142
+
143
+ * `tests/test_sox.py` — `sox` synthesizes a sine wave and dumps it as raw
144
+ int16 (and float32) samples. The samples are published, attached back as a
145
+ read-only `np.memmap`, attached again from a separate process, and stored at
146
+ two sample rates as two variants (`sr16000`, `sr44100`) of one digest.
147
+ * `tests/test_ffmpeg.py` — `ffmpeg` renders its `testsrc` pattern to
148
+ `rawvideo` frames, stored as one `(T, H, W, C)` array. The same checks, with
149
+ the frame rate as the variant (`fr10`, `fr25`), plus a clip over
150
+ `max_array_bytes` being declined whole rather than truncated.
151
+
152
+ Both use their tool's built-in generators, so no media files are checked in.
153
+ Both also exercise per-form dtypes: `dtypes` maps a *form* to the dtype it
154
+ requires (`{"wav16k": "int16"}`), and a float dump is refused under the
155
+ integer form and accepted under the float one. A form's name is only a label —
156
+ the store enforces a dtype only for forms listed in `dtypes`.
157
+
158
+ ## Origin
159
+
160
+ Lifted on 2026-10-27 from `seantrue/commonkit` (`main` at `4dbfc7d`, plus the contract and
161
+ vectors from its `rust-crate-plan` branch at `2b6bcf9` and after), with a fresh history. That
162
+ package was extracted from the pixel-cache layer of a private image-corpus
163
+ application, where it ran in production first. The measurements in the module
164
+ docstrings were taken there; the framework-coupled half (the settings wiring
165
+ and the decode entry point) deliberately stayed behind.
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "pyuncommonkit"
3
+ version = "0.1.0"
4
+ description = "Named shared-memory segments for numpy arrays: publish once, attach from any process."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ dependencies = ["numpy>=1.26"]
10
+ keywords = [
11
+ "numpy",
12
+ "npy",
13
+ "mmap",
14
+ "shared-memory",
15
+ "cache",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: Science/Research",
21
+ "Operating System :: MacOS",
22
+ "Operating System :: POSIX :: Linux",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Scientific/Engineering",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ "Topic :: System :: Filesystems",
32
+ ]
33
+
34
+ [[project.authors]]
35
+ name = "Sean D. True"
36
+ email = "sean.true@gmail.com"
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/seantrue/pycommonkit"
40
+ Repository = "https://github.com/seantrue/pycommonkit"
41
+ Issues = "https://github.com/seantrue/pycommonkit/issues"
42
+ Contract = "https://github.com/seantrue/pycommonkit/blob/main/docs/contract.md"
43
+ "Rust crate" = "https://crates.io/crates/commonkit"
44
+
45
+ [project.optional-dependencies]
46
+ dev = [
47
+ "pytest>=8",
48
+ "ruff>=0.6",
49
+ ]
50
+
51
+ [build-system]
52
+ requires = ["uv_build>=0.11.21,<0.12.0"]
53
+ build-backend = "uv_build"
54
+
55
+ [tool.uv.build-backend]
56
+ module-name = "commonkit"
57
+
58
+ [tool.ruff]
59
+ line-length = 100
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "pyuncommonkit"
3
+ version = "0.1.0"
4
+ description = "Named shared-memory segments for numpy arrays: publish once, attach from any process."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Sean D. True", email = "sean.true@gmail.com" }
8
+ ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
11
+ # 3.11, NOT 3.12: the model venvs this exists to serve are python 3.11, and
12
+ # being importable there is the whole point of the package.
13
+ requires-python = ">=3.11"
14
+ dependencies = [
15
+ "numpy>=1.26",
16
+ ]
17
+ keywords = ["numpy", "npy", "mmap", "shared-memory", "cache"]
18
+ # No License :: classifier: PEP 639's `license` expression replaces it. Linux and macOS only:
19
+ # Windows is supported in the contract (§12) but has no Python default root or CI yet.
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Developers",
23
+ "Intended Audience :: Science/Research",
24
+ "Operating System :: MacOS",
25
+ "Operating System :: POSIX :: Linux",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3 :: Only",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Programming Language :: Python :: 3.14",
32
+ "Topic :: Scientific/Engineering",
33
+ "Topic :: Software Development :: Libraries :: Python Modules",
34
+ "Topic :: System :: Filesystems",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/seantrue/pycommonkit"
39
+ Repository = "https://github.com/seantrue/pycommonkit"
40
+ Issues = "https://github.com/seantrue/pycommonkit/issues"
41
+ Contract = "https://github.com/seantrue/pycommonkit/blob/main/docs/contract.md"
42
+ "Rust crate" = "https://crates.io/crates/commonkit"
43
+
44
+ [project.optional-dependencies]
45
+ dev = [
46
+ "pytest>=8",
47
+ "ruff>=0.6",
48
+ ]
49
+
50
+ [build-system]
51
+ requires = ["uv_build>=0.11.21,<0.12.0"]
52
+ build-backend = "uv_build"
53
+
54
+ [tool.uv.build-backend]
55
+ # Published as pyuncommonkit, imported as commonkit, so no consumer's code changes; only its
56
+ # dependency line does. PyPI `commonkit` belongs to an unrelated project, and PyPI refused
57
+ # `pycommonkit` as too similar to it (2026-10-28).
58
+ module-name = "commonkit"
59
+
60
+ [tool.ruff]
61
+ line-length = 100
@@ -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
+ ]
@@ -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}>"