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.

@@ -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
@@ -1,6 +1,7 @@
1
1
  from queue import Queue
2
2
  from time import time
3
- from typing import Generic, TypeVar
3
+ from typing import Generic
4
+ from typing import TypeVar
4
5
 
5
6
  T = TypeVar("T")
6
7
 
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