thunk 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.
thunk-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) binado
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.
thunk-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: thunk
3
+ Version: 0.1.0
4
+ Summary: Generate automatic save and load methods for functions that manipulate arrays
5
+ Keywords: hdf5,json,numpy,persistence,serialization,type-annotations
6
+ Author: binado
7
+ Author-email: binado <bernardopveronese@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Typing :: Typed
21
+ Requires-Dist: h5py>=3.16.0
22
+ Requires-Dist: numpy>=2.5.3
23
+ Requires-Dist: pydantic>=2.13.5
24
+ Requires-Dist: jax>=0.11.2 ; extra == 'jax'
25
+ Requires-Python: >=3.12
26
+ Project-URL: Homepage, https://github.com/binado/thunk
27
+ Project-URL: Repository, https://github.com/binado/thunk.git
28
+ Project-URL: Issues, https://github.com/binado/thunk/issues
29
+ Provides-Extra: jax
30
+ Description-Content-Type: text/markdown
31
+
32
+ # thunk
33
+
34
+ Generate automatic save and load methods for functions that manipulate arrays
35
+
36
+ ## Installation
37
+
38
+ ```console
39
+ uv add thunk
40
+ # or
41
+ pip install thunk
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ `thunk.fn` derives save/load methods from a callable's type annotations. It
47
+ inspects the signature and never runs the function body.
48
+
49
+ ```python
50
+ from dataclasses import dataclass
51
+ from typing import Annotated
52
+
53
+ import numpy as np
54
+ import thunk
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class Params:
59
+ y: np.ndarray
60
+ labels: list[str]
61
+
62
+
63
+ def simulator(
64
+ x: np.ndarray,
65
+ /,
66
+ params: Params,
67
+ *,
68
+ seed: int = 0,
69
+ chunk_size: Annotated[int, thunk.Skip()] = 4096,
70
+ ) -> np.ndarray: ...
71
+
72
+
73
+ pfn = thunk.fn(simulator)
74
+
75
+ # Bind arguments and split them into persisted groups (does not run simulator).
76
+ inputs, opts = pfn.flatten(x, params, seed=42)
77
+
78
+ pfn.save_inputs("inputs.h5", inputs)
79
+ pfn.save_opts("opts.json", opts)
80
+ # ... or flatten and save both in one step:
81
+ pfn.save("inputs.h5", "opts.json", x, params, seed=42)
82
+
83
+ # Restore, optionally edit the configuration, and run.
84
+ inputs = pfn.load_inputs("inputs.h5")
85
+ opts = pfn.load_opts("opts.json")
86
+ result = pfn(inputs, opts)
87
+
88
+ # Or restore a legal Python call without executing simulator.
89
+ args, kwargs = pfn.load(inputs="inputs.h5", opts="opts.json")
90
+ result = simulator(*args, **kwargs)
91
+
92
+ pfn.save_output("output.h5", result, inputs=inputs, opts=opts)
93
+ restored = pfn.load_output("output.h5")
94
+ ```
95
+
96
+ Roles decide where each parameter is stored:
97
+
98
+ | Role | Group | Storage |
99
+ | --- | --- | --- |
100
+ | `Data()` (default when the annotation contains arrays) | `inputs` | HDF5 |
101
+ | `Static()` (default otherwise) | `opts` | JSON, via pydantic |
102
+ | `Skip()` | neither | supplied at call time |
103
+
104
+ Supported annotations: `bool`, `int`, `float`, `str`, `None`, `np.ndarray`,
105
+ `jax.Array` (with the JAX extra), `Literal[...]`, `T | None`, `list[T]`, `tuple[...]`, `dict[str, T]`, and
106
+ dataclasses of those. Values are validated strictly (no `int` → `float`, no
107
+ `list` ↔ `tuple`). Types outside that set can use
108
+ `Data(DataSerializer(...), DataValidator(...))` or
109
+ `Static(PlainSerializer(...), PlainValidator(...))`.
110
+
111
+ Loading takes `extras="forbid" | "ignore"` for stored names that are no longer
112
+ parameters and `missing="raise" | "default"` for parameters absent from a file
113
+ (defaults are filled with a warning).
114
+
115
+ ## JAX arrays and random keys
116
+
117
+ Install the optional integration (JAX 0.11.2 or newer):
118
+
119
+ ```console
120
+ uv add 'thunk[jax]'
121
+ # or
122
+ pip install 'thunk[jax]'
123
+ ```
124
+
125
+ Annotate arrays and typed PRNG keys with `jax.Array`. Both infer `Data`,
126
+ including inside the supported containers and dataclasses. NumPy and JAX
127
+ annotations require their respective array types; `jax.typing.ArrayLike` and
128
+ mixed-array unions are unsupported.
129
+
130
+ ```python
131
+ import jax
132
+ import jax.numpy as jnp
133
+ import thunk
134
+
135
+
136
+ def simulate(x: jax.Array, key: jax.Array, *, scale: float = 0.1) -> jax.Array:
137
+ return x + scale * jax.random.normal(key, x.shape, dtype=x.dtype)
138
+
139
+
140
+ pfn = thunk.fn(simulate) # jax.jit(simulate) also works
141
+ x = jnp.zeros((100, 3), dtype=jnp.float32)
142
+ key = jax.random.key(42, impl="threefry2x32")
143
+ pfn.save("inputs.h5", "opts.json", x, key, scale=0.2)
144
+ inputs = pfn.load_inputs("inputs.h5")
145
+ opts = pfn.load_opts("opts.json")
146
+ result = pfn(inputs, opts)
147
+ pfn.save_output("output.h5", result, inputs=inputs, opts=opts)
148
+ ```
149
+
150
+ Values, shapes, numeric dtypes, and typed-key implementations are preserved.
151
+ Supported numeric dtypes are bool, int8/16/32/64, uint8/16/32/64,
152
+ float16/32/64, and complex64/128. Extended dtypes such as bfloat16 and float8
153
+ are rejected. Loading int64, uint64, float64, or complex128 requires
154
+ `JAX_ENABLE_X64=1` or `jax.config.update("jax_enable_x64", True)`; thunk raises
155
+ `ValueTypeError` rather than narrowing the stored dtype.
156
+
157
+ Typed keys support `threefry2x32`, `threefry4x32`, `philox2x32`, `philox4x32`,
158
+ `rbg`, and `unsafe_rbg`, including batched and empty key arrays. Restoration
159
+ uses the stored implementation regardless of JAX's current default RNG.
160
+ Legacy `uint32` keys are stored as ordinary numeric arrays. Custom RNG
161
+ implementations are unsupported.
162
+
163
+ Weakly typed arrays (for example `jnp.asarray(1)`) are rejected: supply an
164
+ explicit dtype such as `jnp.asarray(1, dtype=jnp.int32)`. Tracers, deleted
165
+ arrays, and arrays not fully addressable by the current process are also
166
+ rejected with a parameter or nested-field path. Persist outside JAX
167
+ transformations, before deletion or buffer donation, and gather distributed
168
+ data on the saving process first.
169
+
170
+ `flatten()` retains array identity and performs no transfers. Saving and
171
+ input content hashing synchronize and transfer array data to the host;
172
+ loading places arrays on JAX's default device. Device placement and sharding
173
+ are not restored. Custom pytrees and distributed checkpointing are outside
174
+ this integration's scope. Importing thunk or compiling NumPy annotations
175
+ does not import JAX, and existing NumPy files remain compatible.
176
+
177
+ ## Contributing
178
+
179
+ ### Setup
180
+
181
+ [Install uv](https://docs.astral.sh/uv/getting-started/installation/) and
182
+ [Just](https://just.systems/man/en/packages.html), then run:
183
+
184
+ ```console
185
+ just setup
186
+ uv run prek install
187
+ ```
188
+
189
+ The underlying command is `uv sync --all-groups` if Just is unavailable.
190
+
191
+ ### Development
192
+
193
+ ```console
194
+ just fmt # uv run ruff check --fix . && uv run ruff format .
195
+ just lint # uv run ruff check .
196
+ just typecheck # uv run ty check
197
+ just test # uv run pytest
198
+ just hooks # uv run prek run --all-files
199
+ just check # full validation, tests, and package build
200
+ ```
201
+
202
+ ### Update from the template
203
+
204
+ The template runs `uv lock`, so updates must explicitly trust it:
205
+
206
+ ```console
207
+ uvx copier@9.18.2 update --trust
208
+ just check
209
+ ```
210
+
211
+ ### Publishing
212
+
213
+ Configure `pypi` and `testpypi` GitHub environments with trusted publishers.
214
+ Push a tag matching the version in `pyproject.toml`, such as `v0.1.0`, to
215
+ publish to PyPI. Run the Release workflow manually to publish to TestPyPI.
216
+
217
+ ## License
218
+
219
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
thunk-0.1.0/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # thunk
2
+
3
+ Generate automatic save and load methods for functions that manipulate arrays
4
+
5
+ ## Installation
6
+
7
+ ```console
8
+ uv add thunk
9
+ # or
10
+ pip install thunk
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ `thunk.fn` derives save/load methods from a callable's type annotations. It
16
+ inspects the signature and never runs the function body.
17
+
18
+ ```python
19
+ from dataclasses import dataclass
20
+ from typing import Annotated
21
+
22
+ import numpy as np
23
+ import thunk
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Params:
28
+ y: np.ndarray
29
+ labels: list[str]
30
+
31
+
32
+ def simulator(
33
+ x: np.ndarray,
34
+ /,
35
+ params: Params,
36
+ *,
37
+ seed: int = 0,
38
+ chunk_size: Annotated[int, thunk.Skip()] = 4096,
39
+ ) -> np.ndarray: ...
40
+
41
+
42
+ pfn = thunk.fn(simulator)
43
+
44
+ # Bind arguments and split them into persisted groups (does not run simulator).
45
+ inputs, opts = pfn.flatten(x, params, seed=42)
46
+
47
+ pfn.save_inputs("inputs.h5", inputs)
48
+ pfn.save_opts("opts.json", opts)
49
+ # ... or flatten and save both in one step:
50
+ pfn.save("inputs.h5", "opts.json", x, params, seed=42)
51
+
52
+ # Restore, optionally edit the configuration, and run.
53
+ inputs = pfn.load_inputs("inputs.h5")
54
+ opts = pfn.load_opts("opts.json")
55
+ result = pfn(inputs, opts)
56
+
57
+ # Or restore a legal Python call without executing simulator.
58
+ args, kwargs = pfn.load(inputs="inputs.h5", opts="opts.json")
59
+ result = simulator(*args, **kwargs)
60
+
61
+ pfn.save_output("output.h5", result, inputs=inputs, opts=opts)
62
+ restored = pfn.load_output("output.h5")
63
+ ```
64
+
65
+ Roles decide where each parameter is stored:
66
+
67
+ | Role | Group | Storage |
68
+ | --- | --- | --- |
69
+ | `Data()` (default when the annotation contains arrays) | `inputs` | HDF5 |
70
+ | `Static()` (default otherwise) | `opts` | JSON, via pydantic |
71
+ | `Skip()` | neither | supplied at call time |
72
+
73
+ Supported annotations: `bool`, `int`, `float`, `str`, `None`, `np.ndarray`,
74
+ `jax.Array` (with the JAX extra), `Literal[...]`, `T | None`, `list[T]`, `tuple[...]`, `dict[str, T]`, and
75
+ dataclasses of those. Values are validated strictly (no `int` → `float`, no
76
+ `list` ↔ `tuple`). Types outside that set can use
77
+ `Data(DataSerializer(...), DataValidator(...))` or
78
+ `Static(PlainSerializer(...), PlainValidator(...))`.
79
+
80
+ Loading takes `extras="forbid" | "ignore"` for stored names that are no longer
81
+ parameters and `missing="raise" | "default"` for parameters absent from a file
82
+ (defaults are filled with a warning).
83
+
84
+ ## JAX arrays and random keys
85
+
86
+ Install the optional integration (JAX 0.11.2 or newer):
87
+
88
+ ```console
89
+ uv add 'thunk[jax]'
90
+ # or
91
+ pip install 'thunk[jax]'
92
+ ```
93
+
94
+ Annotate arrays and typed PRNG keys with `jax.Array`. Both infer `Data`,
95
+ including inside the supported containers and dataclasses. NumPy and JAX
96
+ annotations require their respective array types; `jax.typing.ArrayLike` and
97
+ mixed-array unions are unsupported.
98
+
99
+ ```python
100
+ import jax
101
+ import jax.numpy as jnp
102
+ import thunk
103
+
104
+
105
+ def simulate(x: jax.Array, key: jax.Array, *, scale: float = 0.1) -> jax.Array:
106
+ return x + scale * jax.random.normal(key, x.shape, dtype=x.dtype)
107
+
108
+
109
+ pfn = thunk.fn(simulate) # jax.jit(simulate) also works
110
+ x = jnp.zeros((100, 3), dtype=jnp.float32)
111
+ key = jax.random.key(42, impl="threefry2x32")
112
+ pfn.save("inputs.h5", "opts.json", x, key, scale=0.2)
113
+ inputs = pfn.load_inputs("inputs.h5")
114
+ opts = pfn.load_opts("opts.json")
115
+ result = pfn(inputs, opts)
116
+ pfn.save_output("output.h5", result, inputs=inputs, opts=opts)
117
+ ```
118
+
119
+ Values, shapes, numeric dtypes, and typed-key implementations are preserved.
120
+ Supported numeric dtypes are bool, int8/16/32/64, uint8/16/32/64,
121
+ float16/32/64, and complex64/128. Extended dtypes such as bfloat16 and float8
122
+ are rejected. Loading int64, uint64, float64, or complex128 requires
123
+ `JAX_ENABLE_X64=1` or `jax.config.update("jax_enable_x64", True)`; thunk raises
124
+ `ValueTypeError` rather than narrowing the stored dtype.
125
+
126
+ Typed keys support `threefry2x32`, `threefry4x32`, `philox2x32`, `philox4x32`,
127
+ `rbg`, and `unsafe_rbg`, including batched and empty key arrays. Restoration
128
+ uses the stored implementation regardless of JAX's current default RNG.
129
+ Legacy `uint32` keys are stored as ordinary numeric arrays. Custom RNG
130
+ implementations are unsupported.
131
+
132
+ Weakly typed arrays (for example `jnp.asarray(1)`) are rejected: supply an
133
+ explicit dtype such as `jnp.asarray(1, dtype=jnp.int32)`. Tracers, deleted
134
+ arrays, and arrays not fully addressable by the current process are also
135
+ rejected with a parameter or nested-field path. Persist outside JAX
136
+ transformations, before deletion or buffer donation, and gather distributed
137
+ data on the saving process first.
138
+
139
+ `flatten()` retains array identity and performs no transfers. Saving and
140
+ input content hashing synchronize and transfer array data to the host;
141
+ loading places arrays on JAX's default device. Device placement and sharding
142
+ are not restored. Custom pytrees and distributed checkpointing are outside
143
+ this integration's scope. Importing thunk or compiling NumPy annotations
144
+ does not import JAX, and existing NumPy files remain compatible.
145
+
146
+ ## Contributing
147
+
148
+ ### Setup
149
+
150
+ [Install uv](https://docs.astral.sh/uv/getting-started/installation/) and
151
+ [Just](https://just.systems/man/en/packages.html), then run:
152
+
153
+ ```console
154
+ just setup
155
+ uv run prek install
156
+ ```
157
+
158
+ The underlying command is `uv sync --all-groups` if Just is unavailable.
159
+
160
+ ### Development
161
+
162
+ ```console
163
+ just fmt # uv run ruff check --fix . && uv run ruff format .
164
+ just lint # uv run ruff check .
165
+ just typecheck # uv run ty check
166
+ just test # uv run pytest
167
+ just hooks # uv run prek run --all-files
168
+ just check # full validation, tests, and package build
169
+ ```
170
+
171
+ ### Update from the template
172
+
173
+ The template runs `uv lock`, so updates must explicitly trust it:
174
+
175
+ ```console
176
+ uvx copier@9.18.2 update --trust
177
+ just check
178
+ ```
179
+
180
+ ### Publishing
181
+
182
+ Configure `pypi` and `testpypi` GitHub environments with trusted publishers.
183
+ Push a tag matching the version in `pyproject.toml`, such as `v0.1.0`, to
184
+ publish to PyPI. Run the Release workflow manually to publish to TestPyPI.
185
+
186
+ ## License
187
+
188
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,83 @@
1
+ [project]
2
+ name = "thunk"
3
+ version = "0.1.0"
4
+ description = "Generate automatic save and load methods for functions that manipulate arrays"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ "h5py>=3.16.0",
10
+ "numpy>=2.5.3",
11
+ "pydantic>=2.13.5",
12
+ ]
13
+ license-files = ["LICENSE"]
14
+ keywords = [
15
+ "hdf5",
16
+ "json",
17
+ "numpy",
18
+ "persistence",
19
+ "serialization",
20
+ "type-annotations",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Intended Audience :: Developers",
25
+ "Intended Audience :: Science/Research",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Programming Language :: Python :: 3.14",
32
+ "Topic :: Scientific/Engineering",
33
+ "Typing :: Typed",
34
+ ]
35
+
36
+ [[project.authors]]
37
+ name = "binado"
38
+ email = "bernardopveronese@gmail.com"
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/binado/thunk"
42
+ Repository = "https://github.com/binado/thunk.git"
43
+ Issues = "https://github.com/binado/thunk/issues"
44
+
45
+ [project.optional-dependencies]
46
+ jax = ["jax>=0.11.2"]
47
+
48
+ [build-system]
49
+ requires = ["uv_build>=0.12.18,<0.13"]
50
+ build-backend = "uv_build"
51
+
52
+ [dependency-groups]
53
+ dev = [
54
+ "jax>=0.11.2",
55
+ "prek>=0.5.3",
56
+ "ruff>=0.16.0",
57
+ "ty>=0.0.83",
58
+ { include-group = "test" },
59
+ ]
60
+ test = ["pytest>=9.1.1"]
61
+
62
+ [tool.pytest.ini_options]
63
+ addopts = "-ra"
64
+ testpaths = ["tests"]
65
+
66
+ [tool.ruff]
67
+ target-version = "py312"
68
+
69
+ [tool.ruff.lint]
70
+ select = [
71
+ "E4",
72
+ "E7",
73
+ "E9",
74
+ "F",
75
+ "I",
76
+ "UP",
77
+ ]
78
+
79
+ [tool.ty.src]
80
+ include = [
81
+ "src",
82
+ "tests",
83
+ ]
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "thunk"
3
+ version = "0.1.0"
4
+ description = "Generate automatic save and load methods for functions that manipulate arrays"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "binado", email = "bernardopveronese@gmail.com" },
9
+ ]
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "h5py>=3.16.0",
13
+ "numpy>=2.5.3",
14
+ "pydantic>=2.13.5",
15
+ ]
16
+ license-files = ["LICENSE"]
17
+ keywords = ["hdf5", "json", "numpy", "persistence", "serialization", "type-annotations"]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Developers",
21
+ "Intended Audience :: Science/Research",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Scientific/Engineering",
29
+ "Typing :: Typed",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/binado/thunk"
34
+ Repository = "https://github.com/binado/thunk.git"
35
+ Issues = "https://github.com/binado/thunk/issues"
36
+
37
+ [project.optional-dependencies]
38
+ jax = [
39
+ "jax>=0.11.2",
40
+ ]
41
+
42
+
43
+ [build-system]
44
+ requires = ["uv_build>=0.12.18,<0.13"]
45
+ build-backend = "uv_build"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "jax>=0.11.2",
50
+ "prek>=0.5.3",
51
+ "ruff>=0.16.0",
52
+ "ty>=0.0.83",
53
+ { include-group = "test" },
54
+ ]
55
+ test = [
56
+ "pytest>=9.1.1",
57
+ ]
58
+
59
+ [tool.pytest.ini_options]
60
+ addopts = "-ra"
61
+ testpaths = ["tests"]
62
+
63
+ [tool.ruff]
64
+ target-version = "py312"
65
+
66
+ [tool.ruff.lint]
67
+ select = ["E4", "E7", "E9", "F", "I", "UP"]
68
+
69
+ [tool.ty.src]
70
+ include = ["src", "tests"]
@@ -0,0 +1,37 @@
1
+ """Generate automatic save and load methods for functions that manipulate arrays"""
2
+
3
+ from importlib.metadata import version
4
+
5
+ from ._errors import (
6
+ OutputCodecError,
7
+ SchemaMismatchError,
8
+ SerializerContractError,
9
+ SpecError,
10
+ StorageFormatError,
11
+ ThunkError,
12
+ ValueTypeError,
13
+ )
14
+ from ._markers import Data, DataSerializer, DataValidator, Skip, Static
15
+ from ._persistence import Extras, FunctionPersistence, Missing, fn
16
+
17
+ __version__ = version("thunk")
18
+
19
+ __all__ = [
20
+ "Data",
21
+ "DataSerializer",
22
+ "DataValidator",
23
+ "Extras",
24
+ "FunctionPersistence",
25
+ "Missing",
26
+ "OutputCodecError",
27
+ "SchemaMismatchError",
28
+ "SerializerContractError",
29
+ "Skip",
30
+ "SpecError",
31
+ "Static",
32
+ "StorageFormatError",
33
+ "ThunkError",
34
+ "ValueTypeError",
35
+ "__version__",
36
+ "fn",
37
+ ]
@@ -0,0 +1,59 @@
1
+ """Atomic file publication via a temporary file in the destination directory."""
2
+
3
+ import contextlib
4
+ import os
5
+ import tempfile
6
+ from collections.abc import Callable, Sequence
7
+ from pathlib import Path
8
+
9
+ type Writer = Callable[[Path], None]
10
+
11
+
12
+ def _umask() -> int:
13
+ mask = os.umask(0)
14
+ os.umask(mask)
15
+ return mask
16
+
17
+
18
+ def _new_temp(dest: Path) -> Path:
19
+ fd, name = tempfile.mkstemp(dir=dest.parent, prefix=f".{dest.name}.", suffix=".tmp")
20
+ os.close(fd)
21
+ return Path(name)
22
+
23
+
24
+ def _publish(temp: Path, dest: Path) -> None:
25
+ try:
26
+ mode = dest.stat().st_mode & 0o777
27
+ except FileNotFoundError:
28
+ mode = 0o666 & ~_umask()
29
+ os.chmod(temp, mode)
30
+ os.replace(temp, dest)
31
+
32
+
33
+ def atomic_write(path: os.PathLike[str] | str, writer: Writer) -> None:
34
+ publish_all([(path, writer)])
35
+
36
+
37
+ def publish_all(items: Sequence[tuple[os.PathLike[str] | str, Writer]]) -> None:
38
+ """Encode every item to a temp file, then replace the destinations.
39
+
40
+ No destination is touched unless every writer succeeds. The replacements
41
+ themselves are sequential, so they are not one transaction.
42
+ """
43
+ dests = [Path(p) for p, _ in items]
44
+ resolved = [d.resolve() for d in dests]
45
+ if len(set(resolved)) != len(resolved):
46
+ raise ValueError("destination paths must be distinct")
47
+
48
+ temps: list[Path] = []
49
+ try:
50
+ for dest, (_, writer) in zip(dests, items, strict=True):
51
+ temp = _new_temp(dest)
52
+ temps.append(temp)
53
+ writer(temp)
54
+ for temp, dest in zip(temps, dests, strict=True):
55
+ _publish(temp, dest)
56
+ finally:
57
+ for temp in temps:
58
+ with contextlib.suppress(FileNotFoundError):
59
+ temp.unlink()