wool 0.1rc3__py3-none-any.whl → 0.1rc7__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 wool might be problematic. Click here for more details.
- wool/__init__.py +37 -27
- wool/_cli.py +93 -40
- wool/_event.py +109 -0
- wool/_future.py +82 -11
- wool/_logging.py +14 -1
- wool/_manager.py +36 -20
- wool/_mempool/__init__.py +3 -0
- wool/_mempool/_mempool.py +204 -0
- wool/_mempool/_metadata/__init__.py +41 -0
- wool/_pool.py +357 -149
- wool/_protobuf/.gitkeep +0 -0
- wool/_protobuf/_mempool/_metadata/_metadata_pb2.py +36 -0
- wool/_protobuf/_mempool/_metadata/_metadata_pb2.pyi +17 -0
- wool/_queue.py +2 -1
- wool/_session.py +429 -0
- wool/_task.py +174 -113
- wool/_typing.py +5 -1
- wool/_utils.py +10 -17
- wool/_worker.py +120 -73
- wool-0.1rc7.dist-info/METADATA +343 -0
- wool-0.1rc7.dist-info/RECORD +23 -0
- {wool-0.1rc3.dist-info → wool-0.1rc7.dist-info}/WHEEL +1 -2
- wool/_client.py +0 -206
- wool-0.1rc3.dist-info/METADATA +0 -137
- wool-0.1rc3.dist-info/RECORD +0 -17
- wool-0.1rc3.dist-info/top_level.txt +0 -1
- {wool-0.1rc3.dist-info → wool-0.1rc7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from google.protobuf import descriptor as _descriptor
|
|
2
|
+
from google.protobuf import message as _message
|
|
3
|
+
from typing import ClassVar as _ClassVar, Optional as _Optional
|
|
4
|
+
|
|
5
|
+
DESCRIPTOR: _descriptor.FileDescriptor
|
|
6
|
+
|
|
7
|
+
class _MetadataMessage(_message.Message):
|
|
8
|
+
__slots__ = ("ref", "mutable", "size", "md5")
|
|
9
|
+
REF_FIELD_NUMBER: _ClassVar[int]
|
|
10
|
+
MUTABLE_FIELD_NUMBER: _ClassVar[int]
|
|
11
|
+
SIZE_FIELD_NUMBER: _ClassVar[int]
|
|
12
|
+
MD5_FIELD_NUMBER: _ClassVar[int]
|
|
13
|
+
ref: str
|
|
14
|
+
mutable: bool
|
|
15
|
+
size: int
|
|
16
|
+
md5: bytes
|
|
17
|
+
def __init__(self, ref: _Optional[str] = ..., mutable: bool = ..., size: _Optional[int] = ..., md5: _Optional[bytes] = ...) -> None: ...
|
wool/_queue.py
CHANGED
wool/_session.py
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from contextvars import Token
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
from typing import Coroutine
|
|
12
|
+
from typing import TypeVar
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
from weakref import WeakValueDictionary
|
|
15
|
+
|
|
16
|
+
import wool
|
|
17
|
+
from wool._future import Future
|
|
18
|
+
from wool._future import fulfill
|
|
19
|
+
from wool._future import poll
|
|
20
|
+
from wool._manager import Manager
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from wool._task import AsyncCallable
|
|
24
|
+
from wool._task import Task
|
|
25
|
+
from wool._typing import Positive
|
|
26
|
+
from wool._typing import Zero
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def command(fn):
|
|
30
|
+
"""
|
|
31
|
+
Decorator to wrap a function with connection checks and automatic
|
|
32
|
+
reconnection logic.
|
|
33
|
+
|
|
34
|
+
:param fn: The function to wrap.
|
|
35
|
+
:return: The wrapped function.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@functools.wraps(fn)
|
|
39
|
+
def wrapper(self: BaseSession, *args, **kwargs):
|
|
40
|
+
if not self.connected:
|
|
41
|
+
raise RuntimeError("Client not connected to manager")
|
|
42
|
+
assert self.manager
|
|
43
|
+
try:
|
|
44
|
+
return fn(self, *args, **kwargs)
|
|
45
|
+
except (ConnectionRefusedError, ConnectionResetError):
|
|
46
|
+
logging.warning(
|
|
47
|
+
f"Connection to manager at {self._address} lost. "
|
|
48
|
+
"Attempting to reconnect..."
|
|
49
|
+
)
|
|
50
|
+
self.connect()
|
|
51
|
+
logging.debug(
|
|
52
|
+
f"Reconnected to manager at {self._address}. "
|
|
53
|
+
"Retrying command..."
|
|
54
|
+
)
|
|
55
|
+
return fn(self, *args, **kwargs)
|
|
56
|
+
|
|
57
|
+
return wrapper
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BaseSession:
|
|
61
|
+
"""
|
|
62
|
+
Base class for managing a session with a worker pool.
|
|
63
|
+
|
|
64
|
+
Provides methods to connect to the manager and shut down the worker pool.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
address: tuple[str, int],
|
|
70
|
+
*,
|
|
71
|
+
authkey: bytes | None = None,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Initialize the session with the specified address and authentication
|
|
75
|
+
key.
|
|
76
|
+
|
|
77
|
+
:param address: The address of the manager (host, port).
|
|
78
|
+
:param authkey: Optional authentication key for the manager.
|
|
79
|
+
"""
|
|
80
|
+
self._address: tuple[str, int] = address
|
|
81
|
+
self._authkey: bytes | None = authkey
|
|
82
|
+
self._manager: Manager | None = None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def manager(self) -> Manager | None:
|
|
86
|
+
"""
|
|
87
|
+
Get the manager instance for the session.
|
|
88
|
+
|
|
89
|
+
:return: The manager instance.
|
|
90
|
+
"""
|
|
91
|
+
return self._manager
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def connected(self) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Check if the session is connected to the manager.
|
|
97
|
+
|
|
98
|
+
:return: True if connected, False otherwise.
|
|
99
|
+
"""
|
|
100
|
+
return self._manager is not None
|
|
101
|
+
|
|
102
|
+
def connect(
|
|
103
|
+
self: Self,
|
|
104
|
+
*,
|
|
105
|
+
retries: Zero | Positive[int] = 2,
|
|
106
|
+
interval: Positive[float] = 1,
|
|
107
|
+
) -> Self:
|
|
108
|
+
"""
|
|
109
|
+
Connect to the manager process for the worker pool.
|
|
110
|
+
|
|
111
|
+
:param retries: Number of retry attempts if the connection fails.
|
|
112
|
+
:param interval: Interval in seconds between retry attempts.
|
|
113
|
+
:return: The session instance.
|
|
114
|
+
:raises ConnectionError: If the connection fails after all retries.
|
|
115
|
+
"""
|
|
116
|
+
if retries < 0:
|
|
117
|
+
raise ValueError("Retries must be a positive integer")
|
|
118
|
+
if not interval > 0:
|
|
119
|
+
raise ValueError("Interval must be a positive float")
|
|
120
|
+
if not self._manager:
|
|
121
|
+
self._manager = Manager(
|
|
122
|
+
address=self._address, authkey=self._authkey
|
|
123
|
+
)
|
|
124
|
+
attempts = threading.Semaphore(retries + 1)
|
|
125
|
+
error = None
|
|
126
|
+
i = 1
|
|
127
|
+
while attempts.acquire(blocking=False):
|
|
128
|
+
logging.debug(
|
|
129
|
+
f"Attempt {i} of {retries + 1} to connect to manager at "
|
|
130
|
+
f"{self._address}..."
|
|
131
|
+
)
|
|
132
|
+
try:
|
|
133
|
+
self._manager.connect()
|
|
134
|
+
except (ConnectionRefusedError, ConnectionResetError) as e:
|
|
135
|
+
error = e
|
|
136
|
+
i += 1
|
|
137
|
+
time.sleep(interval)
|
|
138
|
+
else:
|
|
139
|
+
break
|
|
140
|
+
else:
|
|
141
|
+
if error:
|
|
142
|
+
self._manager = None
|
|
143
|
+
raise error
|
|
144
|
+
logging.debug(
|
|
145
|
+
f"Successfully connected to manager at {self._address}"
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
logging.warning(f"Already connected to manager at {self._address}")
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def stop(self, wait: bool = True) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Shut down the worker pool and close the connection to the manager.
|
|
154
|
+
|
|
155
|
+
:param wait: Whether to wait for in-flight tasks to complete before
|
|
156
|
+
shutting down.
|
|
157
|
+
:raises AssertionError: If the manager is not connected.
|
|
158
|
+
"""
|
|
159
|
+
assert self._manager
|
|
160
|
+
try:
|
|
161
|
+
if wait and not (waiting := self._manager.waiting()).is_set():
|
|
162
|
+
waiting.set()
|
|
163
|
+
if not (stopped := self._manager.stopping()).is_set():
|
|
164
|
+
stopped.set()
|
|
165
|
+
finally:
|
|
166
|
+
self._manager = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
Self = TypeVar("Self", bound=BaseSession)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class WorkerSession(BaseSession):
|
|
173
|
+
"""
|
|
174
|
+
A session for interacting with a worker pool.
|
|
175
|
+
|
|
176
|
+
Provides methods to retrieve task futures and interact with the task queue.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
_futures = None
|
|
180
|
+
_queue = None
|
|
181
|
+
|
|
182
|
+
@command
|
|
183
|
+
def futures(self) -> WeakValueDictionary[UUID, Future]:
|
|
184
|
+
"""
|
|
185
|
+
Retrieve the dictionary of task futures.
|
|
186
|
+
|
|
187
|
+
:return: A dictionary of task futures.
|
|
188
|
+
:raises AssertionError: If the manager is not connected.
|
|
189
|
+
"""
|
|
190
|
+
assert self.manager
|
|
191
|
+
if not self._futures:
|
|
192
|
+
self._futures = self.manager.futures()
|
|
193
|
+
return self._futures
|
|
194
|
+
|
|
195
|
+
@command
|
|
196
|
+
def get(self, *args, **kwargs) -> Task:
|
|
197
|
+
"""
|
|
198
|
+
Retrieve a task from the task queue.
|
|
199
|
+
|
|
200
|
+
:param args: Additional positional arguments for the queue's `get`
|
|
201
|
+
method.
|
|
202
|
+
:param kwargs: Additional keyword arguments for the queue's `get`
|
|
203
|
+
method.
|
|
204
|
+
:return: The next task in the queue.
|
|
205
|
+
:raises AssertionError: If the manager is not connected.
|
|
206
|
+
"""
|
|
207
|
+
assert self.manager
|
|
208
|
+
if not self._queue:
|
|
209
|
+
self._queue = self.manager.queue()
|
|
210
|
+
return self._queue.get(*args, **kwargs)
|
|
211
|
+
|
|
212
|
+
def __enter__(self):
|
|
213
|
+
"""
|
|
214
|
+
Enter the context of the worker session.
|
|
215
|
+
|
|
216
|
+
:return: The session instance.
|
|
217
|
+
"""
|
|
218
|
+
if not self.connected:
|
|
219
|
+
self.connect()
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
def __exit__(self, *_):
|
|
223
|
+
"""
|
|
224
|
+
Exit the context of the worker session.
|
|
225
|
+
"""
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# PUBLIC
|
|
230
|
+
def session(
|
|
231
|
+
host: str = "localhost",
|
|
232
|
+
port: int = 48800,
|
|
233
|
+
*,
|
|
234
|
+
authkey: bytes | None = None,
|
|
235
|
+
) -> WorkerPoolSession:
|
|
236
|
+
"""
|
|
237
|
+
Convenience function to declare a worker pool session context.
|
|
238
|
+
|
|
239
|
+
:param host: The hostname of the worker pool.
|
|
240
|
+
:param port: The port of the worker pool.
|
|
241
|
+
:param authkey: Optional authentication key for the worker pool.
|
|
242
|
+
:return: A decorator that wraps the function to execute within the session.
|
|
243
|
+
|
|
244
|
+
Usage:
|
|
245
|
+
|
|
246
|
+
.. code-block:: python
|
|
247
|
+
|
|
248
|
+
import wool
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@wool.session(host="localhost", port=48800)
|
|
252
|
+
async def foo(): ...
|
|
253
|
+
|
|
254
|
+
...is functionally equivalent to...
|
|
255
|
+
|
|
256
|
+
.. code-block:: python
|
|
257
|
+
|
|
258
|
+
import wool
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async def foo():
|
|
262
|
+
with wool.session(host="localhost", port=48800):
|
|
263
|
+
...
|
|
264
|
+
|
|
265
|
+
This can be used with the ``@wool.task`` decorator to declare a task that
|
|
266
|
+
is tightly coupled with the specified session:
|
|
267
|
+
|
|
268
|
+
.. code-block:: python
|
|
269
|
+
|
|
270
|
+
import wool
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@wool.session(host="localhost", port=48800)
|
|
274
|
+
@wool.task
|
|
275
|
+
async def foo(): ...
|
|
276
|
+
|
|
277
|
+
.. note::
|
|
278
|
+
|
|
279
|
+
The order of decorators matters. In order for invocations of the
|
|
280
|
+
declared task to be dispatched to the pool specified by
|
|
281
|
+
``@wool.session``, the ``@wool.task`` decorator must be applied after
|
|
282
|
+
``@wool.session``.
|
|
283
|
+
"""
|
|
284
|
+
return WorkerPoolSession((host, port), authkey=authkey)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# PUBLIC
|
|
288
|
+
def current_session() -> WorkerPoolSession:
|
|
289
|
+
"""
|
|
290
|
+
Get the current client session.
|
|
291
|
+
|
|
292
|
+
:return: The current client session, or None if no session is active.
|
|
293
|
+
"""
|
|
294
|
+
return wool.__wool_session__.get()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# PUBLIC
|
|
298
|
+
class WorkerPoolSession(BaseSession):
|
|
299
|
+
"""
|
|
300
|
+
A session for managing a pool of workers.
|
|
301
|
+
|
|
302
|
+
Provides methods to interact with the worker pool and manage tasks.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
_token: Token | None = None
|
|
306
|
+
|
|
307
|
+
def __init__(
|
|
308
|
+
self,
|
|
309
|
+
address: tuple[str, int],
|
|
310
|
+
*,
|
|
311
|
+
authkey: bytes | None = None,
|
|
312
|
+
):
|
|
313
|
+
"""
|
|
314
|
+
Initialize the pool session with the specified address and
|
|
315
|
+
authentication key.
|
|
316
|
+
|
|
317
|
+
:param address: The address of the worker pool (host, port).
|
|
318
|
+
:param authkey: Optional authentication key for the worker pool.
|
|
319
|
+
"""
|
|
320
|
+
super().__init__(address, authkey=authkey)
|
|
321
|
+
|
|
322
|
+
def __call__(self, fn: AsyncCallable) -> AsyncCallable:
|
|
323
|
+
"""
|
|
324
|
+
Decorator to execute a function within the context of the pool session.
|
|
325
|
+
|
|
326
|
+
:param fn: The function to wrap.
|
|
327
|
+
:return: The wrapped function.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
@functools.wraps(fn)
|
|
331
|
+
async def wrapper(*args, **kwargs) -> Coroutine:
|
|
332
|
+
with self:
|
|
333
|
+
return await fn(*args, **kwargs)
|
|
334
|
+
|
|
335
|
+
return wrapper
|
|
336
|
+
|
|
337
|
+
def __enter__(self):
|
|
338
|
+
"""
|
|
339
|
+
Enter the context of the pool session.
|
|
340
|
+
|
|
341
|
+
:return: The session instance.
|
|
342
|
+
"""
|
|
343
|
+
if not self.connected:
|
|
344
|
+
self.connect()
|
|
345
|
+
self._token = self.session.set(self)
|
|
346
|
+
return self
|
|
347
|
+
|
|
348
|
+
def __exit__(self, *_):
|
|
349
|
+
"""
|
|
350
|
+
Exit the context of the pool session.
|
|
351
|
+
"""
|
|
352
|
+
assert self._token
|
|
353
|
+
self.session.reset(self._token)
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def session(self) -> ContextVar[WorkerPoolSession]:
|
|
357
|
+
"""
|
|
358
|
+
Get the context variable for the pool session.
|
|
359
|
+
|
|
360
|
+
:return: The context variable for the pool session.
|
|
361
|
+
"""
|
|
362
|
+
return wool.__wool_session__
|
|
363
|
+
|
|
364
|
+
@command
|
|
365
|
+
def put(self, /, wool_task: Task) -> Future:
|
|
366
|
+
"""
|
|
367
|
+
Submit a task to the worker pool.
|
|
368
|
+
|
|
369
|
+
:param wool_task: The task to submit.
|
|
370
|
+
:return: A future representing the result of the task.
|
|
371
|
+
:raises AssertionError: If the manager is not connected.
|
|
372
|
+
"""
|
|
373
|
+
assert self._manager
|
|
374
|
+
return self._manager.put(wool_task)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# PUBLIC
|
|
378
|
+
class LocalSession(WorkerPoolSession):
|
|
379
|
+
"""
|
|
380
|
+
A session for managing local tasks without a worker pool.
|
|
381
|
+
|
|
382
|
+
Provides methods to execute tasks locally and retrieve their results.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
def __init__(self):
|
|
386
|
+
"""
|
|
387
|
+
Initialize the local session.
|
|
388
|
+
"""
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def manager(self) -> Manager | None:
|
|
393
|
+
"""
|
|
394
|
+
Get the manager instance for the local session.
|
|
395
|
+
|
|
396
|
+
:return: None, as the local session does not use a manager.
|
|
397
|
+
"""
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def connected(self) -> bool:
|
|
402
|
+
"""
|
|
403
|
+
Check if the local session is connected.
|
|
404
|
+
|
|
405
|
+
:return: True, as the local session is always connected.
|
|
406
|
+
"""
|
|
407
|
+
return True
|
|
408
|
+
|
|
409
|
+
def connect(self, *args, **kwargs) -> LocalSession:
|
|
410
|
+
"""
|
|
411
|
+
Connect to the local session.
|
|
412
|
+
|
|
413
|
+
:return: The local session instance.
|
|
414
|
+
"""
|
|
415
|
+
return self
|
|
416
|
+
|
|
417
|
+
def put(self, /, wool_task: wool.Task) -> Future:
|
|
418
|
+
"""
|
|
419
|
+
Execute a task locally and retrieve its result.
|
|
420
|
+
|
|
421
|
+
:param wool_task: The task to execute.
|
|
422
|
+
:return: A future representing the result of the task.
|
|
423
|
+
"""
|
|
424
|
+
wool_future = Future()
|
|
425
|
+
loop = asyncio.get_event_loop()
|
|
426
|
+
future = asyncio.run_coroutine_threadsafe(wool_task.run(), loop)
|
|
427
|
+
future.add_done_callback(fulfill(wool_future))
|
|
428
|
+
asyncio.run_coroutine_threadsafe(poll(wool_future, future), loop)
|
|
429
|
+
return wool_future
|