chainset 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,17 @@
1
+ Metadata-Version: 2.3
2
+ Name: chainset
3
+ Version: 0.1.0
4
+ Summary: Chainable dataset processing
5
+ Author: Vladislav A. Proskurov
6
+ Author-email: Vladislav A. Proskurov <rilshok@pm.me>
7
+ License: MIT
8
+ Requires-Dist: typing-extensions>=4.8.0
9
+ Requires-Dist: iokit>=0.4.2
10
+ Requires-Python: >=3.10
11
+ Project-URL: Homepage, https://github.com/rilshok/chainset
12
+ Project-URL: Repository, https://github.com/rilshok/chainset.git
13
+ Project-URL: Issues, https://github.com/rilshok/chainset/issues
14
+ Description-Content-Type: text/markdown
15
+
16
+ # chainset
17
+ Chainable dataset processing
@@ -0,0 +1,2 @@
1
+ # chainset
2
+ Chainable dataset processing
@@ -0,0 +1,71 @@
1
+ [project]
2
+ name = "chainset"
3
+ version = "0.1.0"
4
+ description = "Chainable dataset processing"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "typing-extensions>=4.8.0",
9
+ "iokit>=0.4.2",
10
+ ]
11
+
12
+ [project.license]
13
+ text = "MIT"
14
+
15
+ [[project.authors]]
16
+ name = "Vladislav A. Proskurov"
17
+ email = "rilshok@pm.me"
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/rilshok/chainset"
21
+ Repository = "https://github.com/rilshok/chainset.git"
22
+ Issues = "https://github.com/rilshok/chainset/issues"
23
+
24
+ [dependency-groups]
25
+ lint = [
26
+ "mypy>=1.7.1",
27
+ "pre-commit>=4.0.0",
28
+ "ruff>=0.6.3",
29
+ ]
30
+ test = [
31
+ "pytest>=8.2.2",
32
+ "pytest-cov>=6.0.0",
33
+ "pytest-xdist>=3.6.1",
34
+ ]
35
+
36
+ [[dependency-groups.dev]]
37
+ include-group = "lint"
38
+
39
+ [[dependency-groups.dev]]
40
+ include-group = "test"
41
+
42
+ [build-system]
43
+ requires = ["uv_build>=0.11.29,<0.12.0"]
44
+ build-backend = "uv_build"
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+
49
+ [tool.mypy]
50
+ strict = true
51
+ python_version = "3.10"
52
+
53
+ [tool.ruff]
54
+ line-length = 100
55
+
56
+ [tool.ruff.format]
57
+ docstring-code-format = true
58
+
59
+ [tool.ruff.lint]
60
+ select = ["ALL"]
61
+ ignore = ["CPY001"]
62
+
63
+ [tool.ruff.lint.per-file-ignores]
64
+ "tests/*" = [
65
+ "PLR2004",
66
+ "PLR0915",
67
+ "S101",
68
+ "S106",
69
+ "ANN201",
70
+ "SLF001",
71
+ ]
@@ -0,0 +1,69 @@
1
+ [project]
2
+ name = "chainset"
3
+ version = "0.1.0"
4
+ description = "Chainable dataset processing"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = {text = "MIT"}
8
+ authors = [
9
+ {name = "Vladislav A. Proskurov", email = "rilshok@pm.me"},
10
+ ]
11
+ dependencies = [
12
+ "typing-extensions>=4.8.0",
13
+ "iokit>=0.4.2",
14
+ ]
15
+
16
+
17
+ [dependency-groups]
18
+ dev = [
19
+ {include-group = "lint"},
20
+ {include-group = "test"},
21
+ ]
22
+ lint = [
23
+ "mypy>=1.7.1",
24
+ "pre-commit>=4.0.0",
25
+ "ruff>=0.6.3",
26
+ ]
27
+ test = [
28
+ "pytest>=8.2.2",
29
+ "pytest-cov>=6.0.0",
30
+ "pytest-xdist>=3.6.1",
31
+ ]
32
+
33
+
34
+ [build-system]
35
+ requires = ["uv_build>=0.11.29,<0.12.0"]
36
+ build-backend = "uv_build"
37
+
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/rilshok/chainset"
41
+ Repository = "https://github.com/rilshok/chainset.git"
42
+ Issues = "https://github.com/rilshok/chainset/issues"
43
+
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+
48
+
49
+ [tool.mypy]
50
+ strict = true
51
+ python_version = "3.10"
52
+
53
+
54
+ [tool.ruff]
55
+ line-length = 100
56
+
57
+
58
+ [tool.ruff.format]
59
+ docstring-code-format = true
60
+
61
+
62
+ [tool.ruff.lint]
63
+ select = ["ALL"]
64
+ ignore = [
65
+ "CPY001", # copyright lives in LICENSE, not in every file
66
+ ]
67
+
68
+ [tool.ruff.lint.per-file-ignores]
69
+ "tests/*" = ["PLR2004", "PLR0915", "S101", "S106", "ANN201", "SLF001"]
@@ -0,0 +1,7 @@
1
+ """Chainable dataset processing."""
2
+
3
+ __all__: list[str] = []
4
+
5
+ from importlib.metadata import version
6
+
7
+ __version__ = version("chainset")
@@ -0,0 +1,251 @@
1
+ """Chain of dataset processing steps."""
2
+
3
+ from collections.abc import Callable
4
+ from types import FunctionType
5
+ from typing import Generic, TypeVar, overload
6
+
7
+ from iokit import FormatState, State
8
+ from iokit.utils.time import Timestamp
9
+ from typing_extensions import Self
10
+
11
+ from .storage import DatasetStorage, StorageBackend
12
+
13
+
14
+ class Chain:
15
+ """Process a dataset by combining a storage backend with a data provider."""
16
+
17
+ def __init__(self, storage: StorageBackend, origin: str) -> None:
18
+ """Initialize the chain.
19
+
20
+ Args:
21
+ storage: Backend used to persist the dataset.
22
+ origin: Origin of chain.
23
+
24
+ """
25
+ self._storage = DatasetStorage(storage)
26
+ self._origin = origin
27
+
28
+ @property
29
+ def storage(self) -> DatasetStorage:
30
+ """Backend used to persist the dataset."""
31
+ return self._storage
32
+
33
+ @property
34
+ def origin(self) -> str:
35
+ """Origin of chain."""
36
+ return self._origin
37
+
38
+
39
+ T = TypeVar("T", bound=object)
40
+
41
+
42
+ class StateCodec(Generic[T]):
43
+ """Convert records to and from states of a single storage format."""
44
+
45
+ def __init__(self, state_t: type[FormatState[T]], **config: object) -> None:
46
+ """Initialize the codec.
47
+
48
+ Args:
49
+ state_t: State type used to encode and decode the records.
50
+ config: Options forwarded to `state_t` on encoding and decoding.
51
+
52
+ """
53
+ self.state_t = state_t
54
+ self.config = config
55
+
56
+ def encode(self, data: T, key: str) -> FormatState[T]:
57
+ """Encode `data` into a state stored under `key`.
58
+
59
+ Args:
60
+ data: Record produced by a chain method.
61
+ key: Name of the record within the storage.
62
+
63
+ Returns:
64
+ The state holding the encoded `data`.
65
+
66
+ """
67
+ return self.state_t(
68
+ data,
69
+ stem=None,
70
+ path=key + self.state_t.extension(),
71
+ timestamp=Timestamp.now(),
72
+ **self.config,
73
+ )
74
+
75
+ def decode(self, state: State[T]) -> T:
76
+ """Decode a record from `state`.
77
+
78
+ Args:
79
+ state: State previously produced by `encode`.
80
+
81
+ Returns:
82
+ The record held by `state`.
83
+
84
+ """
85
+ return state.load(**self.config)
86
+
87
+
88
+ C = TypeVar("C", bound=Chain)
89
+
90
+
91
+ class BoundStoredMethod(Generic[C, T]):
92
+ """Stored method bound to a chain instance."""
93
+
94
+ def __init__(
95
+ self,
96
+ obj: C,
97
+ func: Callable[[C, str], T],
98
+ codec: StateCodec[T],
99
+ ) -> None:
100
+ """Initialize the bound stored method.
101
+
102
+ Args:
103
+ obj: Chain instance owning the records.
104
+ func: Chain method computing a record from an item identifier.
105
+ codec: Codec used to encode and decode the records of `func`.
106
+
107
+ """
108
+ self._obj = obj
109
+ self._func = func
110
+ self._codec = codec
111
+ self._origin = obj.origin
112
+ self._key = self._func.__name__
113
+
114
+ def __call__(self, uid: str) -> T:
115
+ """Return the record of the item `uid`, computing and storing it if needed.
116
+
117
+ Args:
118
+ uid: Identifier of the dataset item.
119
+
120
+ Returns:
121
+ The stored record of the item `uid`.
122
+
123
+ """
124
+ if self.exists(uid):
125
+ return self.pull(uid)
126
+ record = self._func(self._obj, uid)
127
+ self.push(uid, record=record, force=False)
128
+ return record
129
+
130
+ def pull(self, uid: str) -> T:
131
+ """Read the record of the item `uid` from the storage.
132
+
133
+ Args:
134
+ uid: Identifier of the dataset item.
135
+
136
+ Returns:
137
+ The stored record of the item `uid`.
138
+
139
+ """
140
+ state = self._obj.storage.pull(
141
+ uid=uid,
142
+ origin=self._origin,
143
+ key=self._key,
144
+ state_t=self._codec.state_t,
145
+ )
146
+ return self._codec.decode(state)
147
+
148
+ def push(self, uid: str, record: T, *, force: bool = False) -> None:
149
+ """Write the `record` of the item `uid` to the storage.
150
+
151
+ Args:
152
+ uid: Identifier of the dataset item.
153
+ record: Record to store.
154
+ force: Whether to overwrite an already stored record.
155
+
156
+ """
157
+ state = self._codec.encode(record, key=self._key)
158
+ self._obj.storage.push(uid=uid, origin=self._origin, state=state, force=force)
159
+
160
+ def remove(self, uid: str) -> None:
161
+ """Delete the record of the item `uid` from the storage.
162
+
163
+ Args:
164
+ uid: Identifier of the dataset item.
165
+
166
+ """
167
+ self._obj.storage.remove(uid=uid, origin=self._origin, key=self._key)
168
+
169
+ def exists(self, uid: str) -> bool:
170
+ """Check whether the record of the item `uid` is stored.
171
+
172
+ Args:
173
+ uid: Identifier of the dataset item.
174
+
175
+ Returns:
176
+ True if the record is present in the storage.
177
+
178
+ """
179
+ return self._obj.storage.exists(uid, origin=self._origin, key=self._key)
180
+
181
+
182
+ class StoredMethod(Generic[C, T]):
183
+ """Descriptor persisting the results of a chain method in the chain storage."""
184
+
185
+ def __init__(
186
+ self,
187
+ func: Callable[[C, str], T],
188
+ codec: StateCodec[T],
189
+ ) -> None:
190
+ """Initialize the stored method.
191
+
192
+ Args:
193
+ func: Chain method computing a record from an item identifier.
194
+ codec: Codec used to encode and decode the records of `func`.
195
+
196
+ Raises:
197
+ TypeError: If `func` is not a plain function.
198
+
199
+ """
200
+ if not isinstance(func, FunctionType):
201
+ msg = f"Stored methods must be plain functions, got {type(func).__name__!r}"
202
+ raise TypeError(msg)
203
+ self._func = func
204
+ self._codec = codec
205
+
206
+ @overload
207
+ def __get__(self, obj: None, objtype: type[C] | None = None) -> Self: ...
208
+
209
+ @overload
210
+ def __get__(self, obj: C, objtype: type[C] | None = None) -> BoundStoredMethod[C, T]: ...
211
+
212
+ def __get__(
213
+ self,
214
+ obj: C | None,
215
+ objtype: type[C] | None = None,
216
+ ) -> Self | BoundStoredMethod[C, T]:
217
+ """Bind the stored method to a chain instance.
218
+
219
+ Args:
220
+ obj: Chain instance the method is accessed on, or None on class access.
221
+ objtype: Chain class the method is accessed through.
222
+
223
+ Returns:
224
+ The descriptor itself on class access, otherwise the bound method.
225
+
226
+ """
227
+ if obj is None:
228
+ return self
229
+ return BoundStoredMethod(obj=obj, func=self._func, codec=self._codec)
230
+
231
+
232
+ def store_as(
233
+ state_t: type[FormatState[T]],
234
+ **config: object,
235
+ ) -> Callable[[Callable[[C, str], T]], StoredMethod[C, T]]:
236
+ """Store the results of the decorated chain method in the given state format.
237
+
238
+ Args:
239
+ state_t: State type used to encode and decode the records.
240
+ config: Options forwarded to `state_t` on encoding and decoding.
241
+
242
+ Returns:
243
+ A decorator turning a chain method into a `StoredMethod`.
244
+
245
+ """
246
+ codec = StateCodec(state_t, **config)
247
+
248
+ def decorator(func: Callable[[C, str], T]) -> StoredMethod[C, T]:
249
+ return StoredMethod(func=func, codec=codec)
250
+
251
+ return decorator
File without changes
@@ -0,0 +1,52 @@
1
+ """Storage backend for datasets."""
2
+
3
+ from typing import Any, BinaryIO, TypeVar
4
+
5
+ from iokit import BufferedState, FormatState, Storage
6
+
7
+ StorageBackend = Storage[BinaryIO]
8
+
9
+ S = TypeVar("S", bound=FormatState[Any])
10
+
11
+
12
+ class DatasetStorage:
13
+ """Store dataset items in a binary storage backend."""
14
+
15
+ def __init__(self, backend: StorageBackend) -> None:
16
+ """Initialize the dataset storage.
17
+
18
+ Args:
19
+ backend: Binary storage used to persist the dataset.
20
+
21
+ """
22
+ self._backend = backend
23
+
24
+ def _key(self, uid: str, origin: str, key: str) -> str:
25
+ return f"{uid}/{origin}/{key}"
26
+
27
+ def push(self, uid: str, origin: str, state: FormatState[Any], *, force: bool = False) -> None:
28
+ """Write a state to the storage."""
29
+ key = self._key(uid, origin, state.path)
30
+ self._backend.push(key, state.buffer, force=force)
31
+
32
+ def pull(self, uid: str, origin: str, key: str, state_t: type[S]) -> S:
33
+ """Read a state from the storage."""
34
+ key = self._key(uid, origin, key) + state_t.extension()
35
+ buffer = self._backend.pull(key)
36
+ state: BufferedState[Any] = BufferedState(buffer, path=key)
37
+ return state_t.from_state(state)
38
+
39
+ def remove(self, uid: str, origin: str, key: str) -> None:
40
+ """Delete a state from the storage."""
41
+ key = self._key(uid, origin, key)
42
+ return self._backend.remove(key)
43
+
44
+ def exists(self, uid: str, origin: str, key: str) -> bool:
45
+ """Check whether a state is present in the storage."""
46
+ key = self._key(uid, origin, key)
47
+ return self._backend.exists(key)
48
+
49
+ def size(self, uid: str, origin: str, key: str) -> int:
50
+ """Report the stored size of a state."""
51
+ key = self._key(uid, origin, key)
52
+ return self._backend.size(key)