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.
- chainset-0.1.0/PKG-INFO +17 -0
- chainset-0.1.0/README.md +2 -0
- chainset-0.1.0/pyproject.toml +71 -0
- chainset-0.1.0/pyproject.toml.orig +69 -0
- chainset-0.1.0/src/chainset/__init__.py +7 -0
- chainset-0.1.0/src/chainset/chain.py +251 -0
- chainset-0.1.0/src/chainset/py.typed +0 -0
- chainset-0.1.0/src/chainset/storage.py +52 -0
chainset-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
chainset-0.1.0/README.md
ADDED
|
@@ -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,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)
|