thds.core 1.43.20250721144545__py3-none-any.whl → 1.44.20250721231022__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.

Potentially problematic release.


This version of thds.core might be problematic. Click here for more details.

thds/core/__init__.py CHANGED
@@ -13,6 +13,7 @@ from . import ( # noqa: F401
13
13
  exit_after,
14
14
  files,
15
15
  fretry,
16
+ futures,
16
17
  generators,
17
18
  git,
18
19
  hash_cache,
thds/core/futures.py ADDED
@@ -0,0 +1,227 @@
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thds.core
3
- Version: 1.43.20250721144545
3
+ Version: 1.44.20250721231022
4
4
  Summary: Core utilities.
5
5
  Author-email: Trilliant Health <info@trillianthealth.com>
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
- thds/core/__init__.py,sha256=BUX2dBk_xTCX-E_jXnDiIK4jxY97iOxEmwC1oRRz7Z4,955
1
+ thds/core/__init__.py,sha256=p5ZTz0yURvtiJlUspnI6aMb9nIekTkzU8sqr8HrnyiM,968
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,6 +14,7 @@ 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
17
18
  thds/core/generators.py,sha256=rcdFpPj0NMJWSaSZTnBfTeZxTTORNB633Lng-BW1284,1939
18
19
  thds/core/git.py,sha256=cfdN1oXyfz7k7T2XaseTqL6Ng53B9lfKtzDLmFjojRs,2947
19
20
  thds/core/hash_cache.py,sha256=jSFijG33UUQjVSkbuACdg4KzIBaf28i7hSQXCO49Qh0,4066
@@ -72,8 +73,8 @@ thds/core/sqlite/structured.py,sha256=SvZ67KcVcVdmpR52JSd52vMTW2ALUXmlHEeD-VrzWV
72
73
  thds/core/sqlite/types.py,sha256=oUkfoKRYNGDPZRk29s09rc9ha3SCk2SKr_K6WKebBFs,1308
73
74
  thds/core/sqlite/upsert.py,sha256=BmKK6fsGVedt43iY-Lp7dnAu8aJ1e9CYlPVEQR2pMj4,5827
74
75
  thds/core/sqlite/write.py,sha256=z0219vDkQDCnsV0WLvsj94keItr7H4j7Y_evbcoBrWU,3458
75
- thds_core-1.43.20250721144545.dist-info/METADATA,sha256=_RqGFrTodIEiPHVauPeVXFZY7NCHr0DpCmQppCb6F8A,2216
76
- thds_core-1.43.20250721144545.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
- thds_core-1.43.20250721144545.dist-info/entry_points.txt,sha256=bOCOVhKZv7azF3FvaWX6uxE6yrjK6FcjqhtxXvLiFY8,161
78
- thds_core-1.43.20250721144545.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
79
- thds_core-1.43.20250721144545.dist-info/RECORD,,
76
+ thds_core-1.44.20250721231022.dist-info/METADATA,sha256=ZmyLepclldKh4i06UYo3vgJHgPUVHmlDW9vFpA0Simk,2216
77
+ thds_core-1.44.20250721231022.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
+ thds_core-1.44.20250721231022.dist-info/entry_points.txt,sha256=bOCOVhKZv7azF3FvaWX6uxE6yrjK6FcjqhtxXvLiFY8,161
79
+ thds_core-1.44.20250721231022.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
80
+ thds_core-1.44.20250721231022.dist-info/RECORD,,