thds.core 1.44.20250722150732__py3-none-any.whl → 1.44.20250722163652__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.
- thds/core/__init__.py +0 -1
- {thds_core-1.44.20250722150732.dist-info → thds_core-1.44.20250722163652.dist-info}/METADATA +1 -1
- {thds_core-1.44.20250722150732.dist-info → thds_core-1.44.20250722163652.dist-info}/RECORD +6 -8
- thds/core/futures.py +0 -227
- thds/core/refcount.py +0 -99
- {thds_core-1.44.20250722150732.dist-info → thds_core-1.44.20250722163652.dist-info}/WHEEL +0 -0
- {thds_core-1.44.20250722150732.dist-info → thds_core-1.44.20250722163652.dist-info}/entry_points.txt +0 -0
- {thds_core-1.44.20250722150732.dist-info → thds_core-1.44.20250722163652.dist-info}/top_level.txt +0 -0
thds/core/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
thds/core/__init__.py,sha256=
|
|
1
|
+
thds/core/__init__.py,sha256=BUX2dBk_xTCX-E_jXnDiIK4jxY97iOxEmwC1oRRz7Z4,955
|
|
2
2
|
thds/core/ansi_esc.py,sha256=QZ3CptZbX4N_hyP2IgqfTbNt9tBPaqy7ReTMQIzGbrc,870
|
|
3
3
|
thds/core/cache.py,sha256=nL0oAyZrhPqyBBLevnOWSWVoEBrftaG3aE6Qq6tvmAA,7153
|
|
4
4
|
thds/core/calgitver.py,sha256=6ioH5MGE65l_Dp924oD5CWrLyxKgmhtn46YwGxFpHfM,2497
|
|
@@ -14,7 +14,6 @@ thds/core/exit_after.py,sha256=0lz63nz2NTiIdyBDYyRa9bQShxQKe7eISy8VhXeW4HU,3485
|
|
|
14
14
|
thds/core/files.py,sha256=NJlPXj7BejKd_Pa06MOywVv_YapT4bVedfsJHrWX8nI,4579
|
|
15
15
|
thds/core/fp.py,sha256=S9hM7YmjbmaYbe4l5jSGnzf3HWhEaItmUOv6GMQpHo8,508
|
|
16
16
|
thds/core/fretry.py,sha256=PKgOxCMjcF4zsFfXFvPXpomv5J6KU6llB1EaKukugig,6942
|
|
17
|
-
thds/core/futures.py,sha256=JgqEP9TFC5UVr4tpfaVePHuI-pTsKAFHDWLxkE4aDb0,7372
|
|
18
17
|
thds/core/generators.py,sha256=rcdFpPj0NMJWSaSZTnBfTeZxTTORNB633Lng-BW1284,1939
|
|
19
18
|
thds/core/git.py,sha256=cfdN1oXyfz7k7T2XaseTqL6Ng53B9lfKtzDLmFjojRs,2947
|
|
20
19
|
thds/core/hash_cache.py,sha256=jSFijG33UUQjVSkbuACdg4KzIBaf28i7hSQXCO49Qh0,4066
|
|
@@ -36,7 +35,6 @@ thds/core/progress.py,sha256=4YGbxliDl1i-k-88w4s86uy1E69eQ6xJySGPSkpH1QM,3358
|
|
|
36
35
|
thds/core/project_root.py,sha256=K18U3MLthZnzmdrWmKKtHLd6iu7am9b2vNAThqknpfo,891
|
|
37
36
|
thds/core/protocols.py,sha256=4na2EeWUDWfLn5-SxfMmKegDSndJ5z-vwMhDavhCpEM,409
|
|
38
37
|
thds/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
-
thds/core/refcount.py,sha256=KWhF5vULrCUCJPoqh3X58JkbfYo8HmYpUrmCHjrUx64,4020
|
|
40
38
|
thds/core/scaling.py,sha256=f7CtdgK0sN6nroTq5hLAkG8xwbWhbCZUULstSKjoxO0,1615
|
|
41
39
|
thds/core/scope.py,sha256=9RWWCFRqsgjTyH6rzRm_WnO69N_sEBRaykarc2PAnBY,10834
|
|
42
40
|
thds/core/source_serde.py,sha256=X4c7LiT3VidejqtTel9YB6dWGB3x-ct39KF9E50Nbx4,139
|
|
@@ -73,8 +71,8 @@ thds/core/sqlite/structured.py,sha256=SvZ67KcVcVdmpR52JSd52vMTW2ALUXmlHEeD-VrzWV
|
|
|
73
71
|
thds/core/sqlite/types.py,sha256=oUkfoKRYNGDPZRk29s09rc9ha3SCk2SKr_K6WKebBFs,1308
|
|
74
72
|
thds/core/sqlite/upsert.py,sha256=BmKK6fsGVedt43iY-Lp7dnAu8aJ1e9CYlPVEQR2pMj4,5827
|
|
75
73
|
thds/core/sqlite/write.py,sha256=z0219vDkQDCnsV0WLvsj94keItr7H4j7Y_evbcoBrWU,3458
|
|
76
|
-
thds_core-1.44.
|
|
77
|
-
thds_core-1.44.
|
|
78
|
-
thds_core-1.44.
|
|
79
|
-
thds_core-1.44.
|
|
80
|
-
thds_core-1.44.
|
|
74
|
+
thds_core-1.44.20250722163652.dist-info/METADATA,sha256=wV2K6BylZOMJPiho5YLtWe8WXYVrG9I5lApfq81dxX8,2216
|
|
75
|
+
thds_core-1.44.20250722163652.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
76
|
+
thds_core-1.44.20250722163652.dist-info/entry_points.txt,sha256=bOCOVhKZv7azF3FvaWX6uxE6yrjK6FcjqhtxXvLiFY8,161
|
|
77
|
+
thds_core-1.44.20250722163652.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
|
|
78
|
+
thds_core-1.44.20250722163652.dist-info/RECORD,,
|
thds/core/futures.py
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import concurrent.futures
|
|
2
|
-
import typing as ty
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from functools import partial
|
|
5
|
-
|
|
6
|
-
from typing_extensions import ParamSpec
|
|
7
|
-
|
|
8
|
-
from . import lazy
|
|
9
|
-
|
|
10
|
-
R = ty.TypeVar("R")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class PFuture(ty.Protocol[R]):
|
|
14
|
-
"""
|
|
15
|
-
A Protocol defining the behavior of a future-like object.
|
|
16
|
-
|
|
17
|
-
This defines an interface for an object that acts as a placeholder
|
|
18
|
-
for a result that will be available later. It is structurally
|
|
19
|
-
compatible with concurrent.futures.Future but omits cancellation.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def running(self) -> bool:
|
|
23
|
-
"""Return True if the future is currently executing."""
|
|
24
|
-
...
|
|
25
|
-
|
|
26
|
-
def done(self) -> bool:
|
|
27
|
-
"""Return True if the future is done (finished)."""
|
|
28
|
-
...
|
|
29
|
-
|
|
30
|
-
def result(self, timeout: ty.Optional[float] = None) -> R:
|
|
31
|
-
"""Return the result of the work item.
|
|
32
|
-
|
|
33
|
-
If the work item raised an exception, this method raises the same exception.
|
|
34
|
-
If the timeout is reached, it raises TimeoutError.
|
|
35
|
-
Other exceptions (e.g. CancelledException) may also be raised depending on the
|
|
36
|
-
implementation.
|
|
37
|
-
"""
|
|
38
|
-
...
|
|
39
|
-
|
|
40
|
-
def exception(self, timeout: ty.Optional[float] = None) -> ty.Optional[BaseException]:
|
|
41
|
-
"""
|
|
42
|
-
Return the exception raised by the work item.
|
|
43
|
-
|
|
44
|
-
Returns None if the work item completed without raising.
|
|
45
|
-
If the timeout is reached, it raises TimeoutError.
|
|
46
|
-
Other exceptions (e.g. CancelledException) may also be raised depending on the
|
|
47
|
-
implementation.
|
|
48
|
-
"""
|
|
49
|
-
...
|
|
50
|
-
|
|
51
|
-
def add_done_callback(self, fn: ty.Callable[["PFuture[R]"], None]) -> None:
|
|
52
|
-
"""
|
|
53
|
-
Attaches a callable that will be called when the future is done.
|
|
54
|
-
|
|
55
|
-
The callable will be called with the future object as its only
|
|
56
|
-
argument.
|
|
57
|
-
"""
|
|
58
|
-
...
|
|
59
|
-
|
|
60
|
-
def set_result(self, result: R) -> None:
|
|
61
|
-
"""Set the result of the future, marking it as done."""
|
|
62
|
-
...
|
|
63
|
-
|
|
64
|
-
def set_exception(self, exception: BaseException) -> None:
|
|
65
|
-
"""Set the exception of the future, marking it as done."""
|
|
66
|
-
...
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class LazyFuture(PFuture[R]):
|
|
70
|
-
def __init__(self, mk_future: ty.Callable[[], PFuture[R]]) -> None:
|
|
71
|
-
"""mk_future should generally be serializable.
|
|
72
|
-
|
|
73
|
-
It also needs to be repeatable - i.e. if it were resolved in two different
|
|
74
|
-
processes, it should return the same 'result' (value or exception) in both.
|
|
75
|
-
"""
|
|
76
|
-
self._mk_future = mk_future
|
|
77
|
-
self._lazy_future = lazy.lazy(mk_future)
|
|
78
|
-
|
|
79
|
-
def __getstate__(self) -> dict[str, ty.Any]:
|
|
80
|
-
"""Return the state of the LazyFuture for serialization."""
|
|
81
|
-
return {"_mk_future": self._mk_future}
|
|
82
|
-
|
|
83
|
-
def __setstate__(self, state: dict[str, ty.Any]) -> None:
|
|
84
|
-
"""Restore the state of the LazyFuture from serialization."""
|
|
85
|
-
self._mk_future = state["_mk_future"]
|
|
86
|
-
self._lazy_future = lazy.lazy(self._mk_future)
|
|
87
|
-
|
|
88
|
-
def running(self) -> bool:
|
|
89
|
-
"""Return True if the future is currently executing."""
|
|
90
|
-
return self._lazy_future().running()
|
|
91
|
-
|
|
92
|
-
def done(self) -> bool:
|
|
93
|
-
"""Return True if the future is done (finished)."""
|
|
94
|
-
return self._lazy_future().done()
|
|
95
|
-
|
|
96
|
-
def result(self, timeout: ty.Optional[float] = None) -> R:
|
|
97
|
-
"""
|
|
98
|
-
Return the result of the work item.
|
|
99
|
-
|
|
100
|
-
If the work item raised an exception, this method raises the same
|
|
101
|
-
exception. If the timeout is reached, it raises TimeoutError.
|
|
102
|
-
"""
|
|
103
|
-
return self._lazy_future().result(timeout)
|
|
104
|
-
|
|
105
|
-
def exception(self, timeout: ty.Optional[float] = None) -> ty.Optional[BaseException]:
|
|
106
|
-
"""
|
|
107
|
-
Return the exception raised by the work item.
|
|
108
|
-
|
|
109
|
-
Returns None if the work item completed without raising.
|
|
110
|
-
If the timeout is reached, it raises TimeoutError.
|
|
111
|
-
"""
|
|
112
|
-
return self._lazy_future().exception(timeout)
|
|
113
|
-
|
|
114
|
-
def add_done_callback(self, fn: ty.Callable[["PFuture[R]"], None]) -> None:
|
|
115
|
-
"""
|
|
116
|
-
Attaches a callable that will be called when the future is done.
|
|
117
|
-
|
|
118
|
-
The callable will be called with the future object as its only
|
|
119
|
-
argument.
|
|
120
|
-
"""
|
|
121
|
-
self._lazy_future().add_done_callback(fn)
|
|
122
|
-
|
|
123
|
-
def set_result(self, result: R) -> None:
|
|
124
|
-
"""Set the result of the future, marking it as done."""
|
|
125
|
-
self._lazy_future().set_result(result)
|
|
126
|
-
|
|
127
|
-
def set_exception(self, exception: BaseException) -> None:
|
|
128
|
-
"""Set the exception of the future, marking it as done."""
|
|
129
|
-
self._lazy_future().set_exception(exception)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
P = ParamSpec("P")
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def make_lazy(mk_future: ty.Callable[P, PFuture[R]]) -> ty.Callable[P, LazyFuture[R]]:
|
|
136
|
-
"""Create a LazyFuture that will lazily resolve the given callable."""
|
|
137
|
-
|
|
138
|
-
def mk_future_with_params(*args: P.args, **kwargs: P.kwargs) -> LazyFuture[R]:
|
|
139
|
-
return LazyFuture(partial(mk_future, *args, **kwargs))
|
|
140
|
-
|
|
141
|
-
return mk_future_with_params
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@dataclass(frozen=True)
|
|
145
|
-
class ResolvedFuture(PFuture[R]):
|
|
146
|
-
_result: R
|
|
147
|
-
_done: bool = True
|
|
148
|
-
|
|
149
|
-
def running(self) -> bool:
|
|
150
|
-
return False
|
|
151
|
-
|
|
152
|
-
def done(self) -> bool:
|
|
153
|
-
return self._done
|
|
154
|
-
|
|
155
|
-
def result(self, timeout: ty.Optional[float] = None) -> R:
|
|
156
|
-
return self._result
|
|
157
|
-
|
|
158
|
-
def exception(self, timeout: ty.Optional[float] = None) -> ty.Optional[BaseException]:
|
|
159
|
-
return None
|
|
160
|
-
|
|
161
|
-
def add_done_callback(self, fn: ty.Callable[["PFuture[R]"], None]) -> None:
|
|
162
|
-
fn(self)
|
|
163
|
-
|
|
164
|
-
def set_result(self, result: R) -> None:
|
|
165
|
-
raise RuntimeError("Cannot set result on a resolved future.")
|
|
166
|
-
|
|
167
|
-
def set_exception(self, exception: BaseException) -> None:
|
|
168
|
-
raise RuntimeError("Cannot set exception on a resolved future.")
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def resolved(result: R) -> ResolvedFuture[R]:
|
|
172
|
-
return ResolvedFuture(result)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
# what's below doesn't absolutely have to exist here, as it adds a 'dependency' on
|
|
176
|
-
# concurrent.futures... but practically speaking I think that's a pretty safe default
|
|
177
|
-
# 'actual Future' type.
|
|
178
|
-
|
|
179
|
-
R1 = ty.TypeVar("R1")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def identity(x: R) -> R:
|
|
183
|
-
return x
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def translate_done(
|
|
187
|
-
out_future: PFuture[R1],
|
|
188
|
-
translate_result: ty.Callable[[R], R1],
|
|
189
|
-
done_fut: PFuture[R],
|
|
190
|
-
) -> None:
|
|
191
|
-
try:
|
|
192
|
-
result = done_fut.result()
|
|
193
|
-
out_future.set_result(translate_result(result))
|
|
194
|
-
except Exception as e:
|
|
195
|
-
out_future.set_exception(e)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
OF_R1 = ty.TypeVar("OF_R1", bound=PFuture)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def chain_futures(
|
|
202
|
-
inner_future: PFuture[R],
|
|
203
|
-
outer_future: OF_R1,
|
|
204
|
-
translate_future: ty.Callable[[R], R1],
|
|
205
|
-
) -> OF_R1:
|
|
206
|
-
"""Chain two futures together with a translator in the middle."""
|
|
207
|
-
outer_done_cb = partial(translate_done, outer_future, translate_future)
|
|
208
|
-
inner_future.add_done_callback(outer_done_cb)
|
|
209
|
-
return outer_future
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def reify_future(future: PFuture[R]) -> concurrent.futures.Future[R]:
|
|
213
|
-
"""Reify a PFuture into a concurrent.futures.Future."""
|
|
214
|
-
if isinstance(future, concurrent.futures.Future):
|
|
215
|
-
return future
|
|
216
|
-
return chain_futures(future, concurrent.futures.Future[R](), identity)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def as_completed(
|
|
220
|
-
futures: ty.Iterable[PFuture[R]],
|
|
221
|
-
) -> ty.Iterator[concurrent.futures.Future[R]]:
|
|
222
|
-
"""Return an iterator that yields futures as they complete.
|
|
223
|
-
|
|
224
|
-
We do need to actually create Future objects here, using the add_done_callback method
|
|
225
|
-
so that the these things actually work with concurrent.futures.as_completed.
|
|
226
|
-
"""
|
|
227
|
-
yield from concurrent.futures.as_completed(map(reify_future, futures))
|
thds/core/refcount.py
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""There are times when what you want is a lazy-ish resource, but you don't want it to live forever - just for the duration of the current stack's usage.
|
|
2
|
-
At the same time, if another thread or stack is currently using the resource, you want to reuse it.
|
|
3
|
-
|
|
4
|
-
A good example of using this might be a ThreadPoolExecutor where multiple other threads might be doing similar work concurrently, and it would help if they could all share the same pool:
|
|
5
|
-
|
|
6
|
-
```
|
|
7
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
-
from thds.core import refcount
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
shared_thread_pool = refcount.Resource(lambda: ThreadPoolExecutor())
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def do_work(thunks):
|
|
15
|
-
with shared_thread_pool.get() as thread_pool:
|
|
16
|
-
for res in parallel.yield_results(thunks, executor_cm=thread_pool):
|
|
17
|
-
print(res)
|
|
18
|
-
...
|
|
19
|
-
```
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
import threading
|
|
23
|
-
import typing as ty
|
|
24
|
-
from contextlib import contextmanager
|
|
25
|
-
|
|
26
|
-
from . import log
|
|
27
|
-
|
|
28
|
-
R = ty.TypeVar("R")
|
|
29
|
-
logger = log.getLogger(__name__)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class _ContextProxy(ty.Generic[R]):
|
|
33
|
-
"""A proxy to wrap a resource and neuter its context management methods."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, resource: R):
|
|
36
|
-
self._resource = resource
|
|
37
|
-
|
|
38
|
-
def __getattr__(self, name: str) -> ty.Any:
|
|
39
|
-
return getattr(self._resource, name)
|
|
40
|
-
|
|
41
|
-
def __enter__(self) -> R:
|
|
42
|
-
# The user might still do `with proxy: ...`. We allow it,
|
|
43
|
-
# but it does nothing and just returns the underlying resource.
|
|
44
|
-
return self._resource
|
|
45
|
-
|
|
46
|
-
def __exit__(self, *args: ty.Any) -> None:
|
|
47
|
-
# This is a no-op; the real cleanup is handled by _RefCountResource
|
|
48
|
-
pass
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class _RefCountResource(ty.Generic[R]):
|
|
52
|
-
def __init__(self, factory: ty.Callable[[], ty.ContextManager[R]]) -> None:
|
|
53
|
-
self._factory = factory
|
|
54
|
-
self._rcm__exit__: ty.Optional[ty.Callable[[ty.Any, ty.Any, ty.Any], ty.Optional[bool]]] = None
|
|
55
|
-
self._resource: ty.Optional[R] = None
|
|
56
|
-
self._ref_count: int = 0
|
|
57
|
-
self._lock = threading.RLock()
|
|
58
|
-
|
|
59
|
-
@contextmanager
|
|
60
|
-
def get(self) -> ty.Iterator[R]:
|
|
61
|
-
with self._lock:
|
|
62
|
-
assert self._ref_count >= 0, "Reference count should not be negative prior to incrementing"
|
|
63
|
-
if self._ref_count == 0:
|
|
64
|
-
assert (
|
|
65
|
-
self._rcm__exit__ is None
|
|
66
|
-
), "Resource CM __exit__ should be None when ref count is zero"
|
|
67
|
-
assert self._resource is None, "Resource should be None when ref count is zero"
|
|
68
|
-
resource_cm = self._factory()
|
|
69
|
-
resource = resource_cm.__enter__()
|
|
70
|
-
self._rcm__exit__ = resource_cm.__exit__
|
|
71
|
-
if id(resource) == id(resource_cm):
|
|
72
|
-
logger.info("Patching self-managing resource to avoid double exit: %s", resource)
|
|
73
|
-
# this is one of those context managers that returns itself
|
|
74
|
-
# since we manage this resource, we need to prevent others from trying to enter or exit it.
|
|
75
|
-
resource = _ContextProxy(resource) # type: ignore[assignment]
|
|
76
|
-
self._resource = resource
|
|
77
|
-
self._ref_count += 1
|
|
78
|
-
assert self._resource is not None, "Resource should not be None after incrementing ref count"
|
|
79
|
-
resource = self._resource
|
|
80
|
-
try:
|
|
81
|
-
yield resource
|
|
82
|
-
|
|
83
|
-
finally:
|
|
84
|
-
with self._lock:
|
|
85
|
-
self._ref_count -= 1
|
|
86
|
-
assert self._ref_count >= 0, "Reference count should not be negative after decrementing"
|
|
87
|
-
if self._ref_count == 0:
|
|
88
|
-
assert (
|
|
89
|
-
self._rcm__exit__ is not None
|
|
90
|
-
), "Resource CM __exit__ should not be None when ref count is zero"
|
|
91
|
-
assert (
|
|
92
|
-
self._resource is not None
|
|
93
|
-
), "Resource should not be None when ref count is zero"
|
|
94
|
-
self._rcm__exit__(None, None, None)
|
|
95
|
-
self._resource = None
|
|
96
|
-
self._rcm__exit__ = None
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Resource = _RefCountResource # probably preferable to use this name
|
|
File without changes
|
{thds_core-1.44.20250722150732.dist-info → thds_core-1.44.20250722163652.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{thds_core-1.44.20250722150732.dist-info → thds_core-1.44.20250722163652.dist-info}/top_level.txt
RENAMED
|
File without changes
|