wool 0.1rc9__py3-none-any.whl → 0.1rc11__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.

Files changed (44) hide show
  1. wool/__init__.py +71 -50
  2. wool/_protobuf/__init__.py +12 -5
  3. wool/_protobuf/exception.py +3 -0
  4. wool/_protobuf/task.py +11 -0
  5. wool/_protobuf/task_pb2.py +42 -0
  6. wool/_protobuf/task_pb2.pyi +43 -0
  7. wool/_protobuf/{mempool/metadata_pb2_grpc.py → task_pb2_grpc.py} +2 -2
  8. wool/_protobuf/worker.py +24 -0
  9. wool/_protobuf/worker_pb2.py +47 -0
  10. wool/_protobuf/worker_pb2.pyi +39 -0
  11. wool/_protobuf/worker_pb2_grpc.py +141 -0
  12. wool/_resource_pool.py +376 -0
  13. wool/_typing.py +0 -10
  14. wool/_work.py +553 -0
  15. wool/_worker.py +843 -169
  16. wool/_worker_discovery.py +1223 -0
  17. wool/_worker_pool.py +337 -0
  18. wool/_worker_proxy.py +515 -0
  19. {wool-0.1rc9.dist-info → wool-0.1rc11.dist-info}/METADATA +7 -7
  20. wool-0.1rc11.dist-info/RECORD +22 -0
  21. wool-0.1rc11.dist-info/entry_points.txt +2 -0
  22. wool/_cli.py +0 -262
  23. wool/_event.py +0 -109
  24. wool/_future.py +0 -171
  25. wool/_logging.py +0 -44
  26. wool/_manager.py +0 -181
  27. wool/_mempool/__init__.py +0 -4
  28. wool/_mempool/_client.py +0 -167
  29. wool/_mempool/_mempool.py +0 -311
  30. wool/_mempool/_metadata.py +0 -35
  31. wool/_mempool/_service.py +0 -227
  32. wool/_pool.py +0 -524
  33. wool/_protobuf/mempool/metadata_pb2.py +0 -36
  34. wool/_protobuf/mempool/metadata_pb2.pyi +0 -17
  35. wool/_protobuf/mempool/service_pb2.py +0 -66
  36. wool/_protobuf/mempool/service_pb2.pyi +0 -108
  37. wool/_protobuf/mempool/service_pb2_grpc.py +0 -355
  38. wool/_queue.py +0 -32
  39. wool/_session.py +0 -429
  40. wool/_task.py +0 -366
  41. wool/_utils.py +0 -63
  42. wool-0.1rc9.dist-info/RECORD +0 -29
  43. wool-0.1rc9.dist-info/entry_points.txt +0 -2
  44. {wool-0.1rc9.dist-info → wool-0.1rc11.dist-info}/WHEEL +0 -0
wool/_resource_pool.py ADDED
@@ -0,0 +1,376 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+ from typing import Awaitable
7
+ from typing import Callable
8
+ from typing import Final
9
+ from typing import Generic
10
+ from typing import TypeVar
11
+ from typing import cast
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ SENTINEL: Final = object()
17
+
18
+
19
+ class Resource(Generic[T]):
20
+ """
21
+ A single-use async context manager for resource acquisition.
22
+
23
+ This class can only be used once as an async context manager. After
24
+ acquisition, it cannot be reacquired, and after release, it cannot be
25
+ released again.
26
+
27
+ :param pool:
28
+ The :py:class:`ResourcePool` this resource belongs to.
29
+ :param key:
30
+ The cache key for this resource.
31
+ """
32
+
33
+ def __init__(self, pool: ResourcePool[T], key):
34
+ self._pool = pool
35
+ self._key = key
36
+ self._resource = None
37
+ self._acquired = False
38
+ self._released = False
39
+
40
+ async def __aenter__(self) -> T:
41
+ """
42
+ Context manager entry - acquire resource.
43
+
44
+ :returns:
45
+ The cached resource object.
46
+ :raises RuntimeError:
47
+ If called on a resource that was previously acquired.
48
+ """
49
+ if self._acquired:
50
+ raise RuntimeError(
51
+ "Cannot re-acquire a resource that has already been acquired"
52
+ )
53
+
54
+ self._acquired = True
55
+ try:
56
+ self._resource = await self._pool.acquire(self._key)
57
+ return cast(T, self._resource)
58
+ except Exception:
59
+ self._acquired = False
60
+ raise
61
+
62
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
63
+ """
64
+ Context manager exit - release resource.
65
+
66
+ :param exc_type:
67
+ Exception type if an exception occurred, None otherwise.
68
+ :param exc_val:
69
+ Exception value if an exception occurred, None otherwise.
70
+ :param exc_tb:
71
+ Exception traceback if an exception occurred, None otherwise.
72
+ """
73
+ await self._release()
74
+
75
+ async def _release(self):
76
+ """
77
+ Release the resource.
78
+
79
+ :raises RuntimeError:
80
+ If attempting to release a resource that was not acquired or
81
+ already released.
82
+ """
83
+ if not self._acquired:
84
+ raise RuntimeError("Cannot release a resource that was not acquired")
85
+ if self._released:
86
+ raise RuntimeError(
87
+ "Cannot release a resource that has already been released"
88
+ )
89
+
90
+ self._released = True
91
+ if self._resource:
92
+ await self._pool.release(self._key)
93
+
94
+
95
+ class ResourcePool(Generic[T]):
96
+ """
97
+ An asynchronous reference-counted cache with TTL-based cleanup.
98
+
99
+ Objects are created on-demand via a factory function (sync or async) and
100
+ automatically cleaned up after all references are released and the TTL
101
+ expires.
102
+
103
+ :param factory:
104
+ Function to create new objects (sync or async).
105
+ :param finalizer:
106
+ Optional cleanup function (sync or async).
107
+ :param ttl:
108
+ Time-to-live in seconds after last reference is released.
109
+ """
110
+
111
+ @dataclass
112
+ class CacheEntry:
113
+ """
114
+ Internal cache entry tracking an object and its metadata.
115
+
116
+ :param obj:
117
+ The cached object.
118
+ :param reference_count:
119
+ Number of active references to this object.
120
+ :param cleanup:
121
+ Optional cleanup task scheduled when reference count reaches zero.
122
+ """
123
+
124
+ obj: Any
125
+ reference_count: int
126
+ cleanup: asyncio.Task | None = None
127
+
128
+ @dataclass
129
+ class Stats:
130
+ """
131
+ Statistics about the current state of the resource pool.
132
+
133
+ :param total_entries:
134
+ Total number of cached entries.
135
+ :param referenced_entries:
136
+ Number of entries currently being referenced (reference_count > 0).
137
+ :param pending_cleanup:
138
+ Number of cleanup tasks currently pending execution.
139
+ """
140
+
141
+ total_entries: int
142
+ referenced_entries: int
143
+ pending_cleanup: int
144
+
145
+ def __init__(
146
+ self,
147
+ factory: Callable[[Any], T | Awaitable[T]],
148
+ *,
149
+ finalizer: Callable[[T], None | Awaitable[None]] | None = None,
150
+ ttl: float = 0,
151
+ ):
152
+ self._factory = factory
153
+ self._finalizer = finalizer
154
+ self._ttl = ttl
155
+ self._cache: dict[Any, ResourcePool.CacheEntry] = {}
156
+ self._lock = asyncio.Lock()
157
+
158
+ async def __aenter__(self):
159
+ """Async context manager entry.
160
+
161
+ :returns:
162
+ The ResourcePool instance itself.
163
+ """
164
+ return self
165
+
166
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
167
+ """Async context manager exit - cleanup all resources.
168
+
169
+ :param exc_type:
170
+ Exception type if an exception occurred, None otherwise.
171
+ :param exc_val:
172
+ Exception value if an exception occurred, None otherwise.
173
+ :param exc_tb:
174
+ Exception traceback if an exception occurred, None otherwise.
175
+ """
176
+ await self.clear()
177
+
178
+ @property
179
+ def stats(self) -> Stats:
180
+ """
181
+ Return cache statistics.
182
+
183
+ .. note::
184
+ This is synchronous for convenience, but should only be called
185
+ when not concurrently modifying the cache.
186
+
187
+ :returns:
188
+ :py:class:`ResourcePool.Stats` containing current statistics.
189
+ """
190
+ pending_cleanup = sum(
191
+ 1 for c in self.pending_cleanup.values() if c is not None and not c.done()
192
+ )
193
+ return self.Stats(
194
+ total_entries=len(self._cache),
195
+ referenced_entries=sum(
196
+ 1 for e in self._cache.values() if e.reference_count > 0
197
+ ),
198
+ pending_cleanup=pending_cleanup,
199
+ )
200
+
201
+ @property
202
+ def pending_cleanup(self):
203
+ """Dictionary of cache keys with pending cleanup tasks.
204
+
205
+ :returns:
206
+ Dictionary mapping cache keys to their cleanup tasks.
207
+ """
208
+ return {
209
+ k: v.cleanup
210
+ for k, v in self._cache.items()
211
+ if v.cleanup is not None and not v.cleanup.done()
212
+ }
213
+
214
+ def get(self, key: Any) -> Resource[T]:
215
+ """
216
+ Get a resource acquisition that can be awaited or used as context
217
+ manager.
218
+
219
+ :param key:
220
+ The cache key.
221
+ :returns:
222
+ :py:class:`Resource` that can be awaited or used with 'async with'.
223
+ """
224
+ return Resource(self, key)
225
+
226
+ async def acquire(self, key: Any) -> T:
227
+ """
228
+ Internal acquire method - acquires a reference to the cached object.
229
+
230
+ Creates a new object via the factory if not cached. Increments
231
+ reference count and cancels any pending cleanup.
232
+
233
+ :param key:
234
+ The cache key.
235
+ :returns:
236
+ The cached or newly created object.
237
+ """
238
+ async with self._lock:
239
+ if key in self._cache:
240
+ entry = self._cache[key]
241
+ entry.reference_count += 1
242
+
243
+ # Cancel pending cleanup task if it exists
244
+ if entry.cleanup is not None and not entry.cleanup.done():
245
+ entry.cleanup.cancel()
246
+ try:
247
+ await entry.cleanup
248
+ except asyncio.CancelledError:
249
+ pass
250
+ entry.cleanup = None
251
+
252
+ return entry.obj
253
+ else:
254
+ # Cache miss - create new object
255
+ obj = await self._await(self._factory, key)
256
+ self._cache[key] = self.CacheEntry(obj=obj, reference_count=1)
257
+ return obj
258
+
259
+ async def release(self, key: Any) -> None:
260
+ """
261
+ Release a reference to the cached object.
262
+
263
+ Decrements reference count. If count reaches 0, schedules cleanup
264
+ after TTL expires (if TTL > 0).
265
+
266
+ :param key:
267
+ The cache key.
268
+ :raises KeyError:
269
+ If key not in cache.
270
+ """
271
+ async with self._lock:
272
+ if key not in self._cache:
273
+ raise KeyError(f"Key '{key}' not found in cache")
274
+ entry = self._cache[key]
275
+
276
+ if entry.reference_count <= 0:
277
+ raise ValueError(f"Reference count for key '{key}' is already 0")
278
+
279
+ entry.reference_count -= 1
280
+
281
+ if entry.reference_count <= 0:
282
+ if self._ttl > 0:
283
+ # Schedule cleanup after TTL
284
+ entry.cleanup = asyncio.create_task(self._schedule_cleanup(key))
285
+ else:
286
+ # Immediate cleanup
287
+ await self._cleanup(key)
288
+
289
+ async def clear(self, key=SENTINEL) -> None:
290
+ """Clear cache entries and cancel pending cleanups.
291
+
292
+ :param key:
293
+ Specific key to clear, or SENTINEL to clear all entries.
294
+ """
295
+ async with self._lock:
296
+ # Clean up all entries
297
+ if key is SENTINEL:
298
+ keys = list(self._cache.keys())
299
+ else:
300
+ keys = [key]
301
+ for key in keys:
302
+ await self._cleanup(key)
303
+
304
+ async def _schedule_cleanup(self, key: Any) -> None:
305
+ """
306
+ Schedule cleanup after TTL delay.
307
+
308
+ Only cleans up if the reference count is still 0 when TTL expires.
309
+
310
+ :param key:
311
+ The cache key to schedule cleanup for.
312
+ """
313
+ try:
314
+ await asyncio.sleep(self._ttl)
315
+
316
+ async with self._lock:
317
+ # Double-check conditions - reference might have been re-acquired
318
+ if key in self._cache:
319
+ entry = self._cache[key]
320
+ if entry.reference_count == 0:
321
+ await self._cleanup(key)
322
+
323
+ except asyncio.CancelledError:
324
+ # Cleanup was cancelled due to new reference - this is expected
325
+ pass
326
+
327
+ async def _cleanup(self, key: Any) -> None:
328
+ """
329
+ Remove entry from cache and call finalizer.
330
+
331
+ .. warning::
332
+ Must be called while holding the lock.
333
+
334
+ :param key:
335
+ The cache key to cleanup.
336
+ """
337
+ entry = self._cache[key]
338
+ try:
339
+ # Cancel cleanup task if running
340
+ if entry.cleanup is not None and not entry.cleanup.done():
341
+ entry.cleanup.cancel()
342
+ try:
343
+ await entry.cleanup
344
+ except asyncio.CancelledError:
345
+ pass
346
+ finally:
347
+ # Call finalizer
348
+ if self._finalizer:
349
+ try:
350
+ await self._await(self._finalizer, entry.obj)
351
+ except Exception:
352
+ pass
353
+ del self._cache[key]
354
+
355
+ async def _await(self, func: Callable, *args) -> Any:
356
+ """
357
+ Call a function that might be sync or async.
358
+
359
+ If the function is a coroutine function, await it. Otherwise, call it
360
+ synchronously. If the result is a coroutine, await that as well.
361
+
362
+ :param func:
363
+ The function to call.
364
+ :param args:
365
+ Arguments to pass to the function.
366
+ :returns:
367
+ The result of the function call.
368
+ """
369
+ if asyncio.iscoroutinefunction(func):
370
+ return await func(*args)
371
+ else:
372
+ result = func(*args)
373
+ # Check if the result is a coroutine and await it if so
374
+ if asyncio.iscoroutine(result):
375
+ return await result
376
+ return result
wool/_typing.py CHANGED
@@ -1,16 +1,6 @@
1
- from typing import Annotated
2
1
  from typing import Callable
3
- from typing import Literal
4
- from typing import SupportsInt
5
2
  from typing import TypeVar
6
3
 
7
- from annotated_types import Gt
8
-
9
- T = TypeVar("T", bound=SupportsInt)
10
- Positive = Annotated[T, Gt(0)]
11
-
12
- Zero = Literal[0]
13
-
14
4
  F = TypeVar("F", bound=Callable)
15
5
  W = TypeVar("W", bound=Callable)
16
6
  Decorator = Callable[[F], W]