wool 0.1rc13__tar.gz → 0.1rc15__tar.gz

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 (29) hide show
  1. {wool-0.1rc13 → wool-0.1rc15}/PKG-INFO +153 -41
  2. wool-0.1rc15/README.md +232 -0
  3. {wool-0.1rc13 → wool-0.1rc15}/pyproject.toml +1 -1
  4. {wool-0.1rc13 → wool-0.1rc15}/wool/__init__.py +1 -26
  5. wool-0.1rc15/wool/_connection.py +247 -0
  6. wool-0.1rc15/wool/_context.py +29 -0
  7. wool-0.1rc15/wool/_loadbalancer.py +213 -0
  8. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/task_pb2_grpc.py +2 -2
  9. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/worker_pb2.py +6 -6
  10. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/worker_pb2.pyi +4 -4
  11. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/worker_pb2_grpc.py +2 -2
  12. {wool-0.1rc13 → wool-0.1rc15}/wool/_resource_pool.py +3 -3
  13. wool-0.1rc15/wool/_undefined.py +11 -0
  14. {wool-0.1rc13 → wool-0.1rc15}/wool/_work.py +5 -4
  15. wool-0.1rc15/wool/_worker.py +601 -0
  16. {wool-0.1rc13 → wool-0.1rc15}/wool/_worker_discovery.py +24 -36
  17. {wool-0.1rc13 → wool-0.1rc15}/wool/_worker_pool.py +46 -31
  18. {wool-0.1rc13 → wool-0.1rc15}/wool/_worker_proxy.py +54 -186
  19. wool-0.1rc15/wool/_worker_service.py +243 -0
  20. wool-0.1rc13/README.md +0 -120
  21. wool-0.1rc13/wool/_worker.py +0 -912
  22. {wool-0.1rc13 → wool-0.1rc15}/.gitignore +0 -0
  23. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/__init__.py +0 -0
  24. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/exception.py +0 -0
  25. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/task.py +0 -0
  26. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/task_pb2.py +0 -0
  27. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/task_pb2.pyi +0 -0
  28. {wool-0.1rc13 → wool-0.1rc15}/wool/_protobuf/worker.py +0 -0
  29. {wool-0.1rc13 → wool-0.1rc15}/wool/_typing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wool
3
- Version: 0.1rc13
3
+ Version: 0.1rc15
4
4
  Summary: A Python framework for distributed multiprocessing.
5
5
  Author-email: Conrad Bzura <conrad@wool.io>
6
6
  Maintainer-email: maintainers@wool.io
@@ -222,14 +222,14 @@ Requires-Dist: hypothesis; extra == 'dev'
222
222
  Requires-Dist: pytest; extra == 'dev'
223
223
  Requires-Dist: pytest-asyncio; extra == 'dev'
224
224
  Requires-Dist: pytest-cov; extra == 'dev'
225
- Requires-Dist: pytest-grpc-aio~=0.2.0; extra == 'dev'
225
+ Requires-Dist: pytest-grpc-aio~=0.3.0; extra == 'dev'
226
226
  Requires-Dist: pytest-mock; extra == 'dev'
227
227
  Requires-Dist: ruff; extra == 'dev'
228
228
  Description-Content-Type: text/markdown
229
229
 
230
- # Wool
230
+ ![](https://raw.githubusercontent.com/wool-labs/wool/refs/heads/main/assets/woolly-transparent-bg-2048.png)
231
231
 
232
- Wool is a native Python package for transparently executing tasks in a horizontally scalable, distributed network of agnostic worker processes. Any picklable async function or method can be converted into a task with a simple decorator and a client connection.
232
+ **Wool** is a native Python package for transparently executing tasks in a horizontally scalable, distributed network of agnostic worker processes. Any picklable async function or method can be converted into a task with a simple decorator and a client connection.
233
233
 
234
234
  ## Installation
235
235
 
@@ -253,19 +253,73 @@ cd wool
253
253
 
254
254
  ## Usage
255
255
 
256
- ### CLI Commands
256
+ ### Declaring tasks
257
257
 
258
- Wool provides a command-line interface (CLI) for managing the worker pool.
258
+ Wool tasks are coroutine functions that are executed in a remote `asyncio` event loop within a worker process. To declare a task, use the `@wool.task` decorator:
259
259
 
260
- To list the available commands:
260
+ ```python
261
+ import wool
261
262
 
262
- ```sh
263
- wool --help
263
+ @wool.task
264
+ async def sample_task(x, y):
265
+ return x + y
264
266
  ```
265
267
 
266
- #### Start the Worker Pool
268
+ Tasks must be picklable, stateless, and idempotent. Avoid passing unpicklable objects as arguments or return values.
269
+
270
+ ### Worker pools
271
+
272
+ Worker pools are responsible for executing tasks. Wool provides two types of pools:
273
+
274
+ #### Ephemeral pools
275
+
276
+ Ephemeral pools are created and destroyed within the scope of a context manager. Use `wool.pool` to declare an ephemeral pool:
277
+
278
+ ```python
279
+ import asyncio, wool
280
+
281
+ @wool.task
282
+ async def sample_task(x, y):
283
+ return x + y
284
+
285
+ async def main():
286
+ with wool.pool():
287
+ result = await sample_task(1, 2)
288
+ print(f"Result: {result}")
289
+
290
+ asyncio.run(main())
291
+ ```
292
+
293
+ #### Durable pools
294
+
295
+ Durable pools are started independently and persist beyond the scope of a single application. Use the `wool` CLI to manage durable pools:
296
+
297
+ ```bash
298
+ wool pool up --port 5050 --authkey deadbeef --module tasks
299
+ ```
300
+
301
+ Connect to a durable pool using `wool.session`:
302
+
303
+ ```python
304
+ import asyncio, wool
305
+
306
+ @wool.task
307
+ async def sample_task(x, y):
308
+ return x + y
309
+
310
+ async def main():
311
+ with wool.session(port=5050, authkey=b"deadbeef"):
312
+ result = await sample_task(1, 2)
313
+ print(f"Result: {result}")
314
+
315
+ asyncio.run(main())
316
+ ```
267
317
 
268
- To start the worker pool, use the `up` command:
318
+ ### CLI commands
319
+
320
+ Wool provides a command-line interface (CLI) for managing worker pools.
321
+
322
+ #### Start the worker pool
269
323
 
270
324
  ```sh
271
325
  wool pool up --host <host> --port <port> --authkey <authkey> --breadth <breadth> --module <module>
@@ -275,11 +329,9 @@ wool pool up --host <host> --port <port> --authkey <authkey> --breadth <breadth>
275
329
  - `--port`: The port number (default: `0`).
276
330
  - `--authkey`: The authentication key (default: `b""`).
277
331
  - `--breadth`: The number of worker processes (default: number of CPU cores).
278
- - `--module`: Python module containing Wool task definitions to be executed by this pool (optional, can be specified multiple times).
279
-
280
- #### Stop the Worker Pool
332
+ - `--module`: Python module containing Wool task definitions (optional, can be specified multiple times).
281
333
 
282
- To stop the worker pool, use the `down` command:
334
+ #### Stop the worker pool
283
335
 
284
336
  ```sh
285
337
  wool pool down --host <host> --port <port> --authkey <authkey> --wait
@@ -290,9 +342,7 @@ wool pool down --host <host> --port <port> --authkey <authkey> --wait
290
342
  - `--authkey`: The authentication key (default: `b""`).
291
343
  - `--wait`: Wait for in-flight tasks to complete before shutting down.
292
344
 
293
- #### Ping the Worker Pool
294
-
295
- To ping the worker pool, use the `ping` command:
345
+ #### Ping the worker pool
296
346
 
297
347
  ```sh
298
348
  wool ping --host <host> --port <port> --authkey <authkey>
@@ -302,46 +352,108 @@ wool ping --host <host> --port <port> --authkey <authkey>
302
352
  - `--port`: The port number (required).
303
353
  - `--authkey`: The authentication key (default: `b""`).
304
354
 
305
- ### Sample Python Application
355
+ ### Advanced usage
356
+
357
+ #### Nested pools and sessions
306
358
 
307
- Below is an example of how to create a Wool client connection, decorate an async function using the `task` decorator, and execute the function remotely:
359
+ Wool supports nesting pools and sessions to achieve complex workflows. Tasks can be dispatched to specific pools by nesting contexts:
308
360
 
309
- Module defining remote tasks:
310
- `tasks.py`
311
361
  ```python
312
362
  import asyncio, wool
313
363
 
314
- # Decorate an async function using the `task` decorator
315
364
  @wool.task
316
- async def sample_task(x, y):
365
+ async def task_a():
317
366
  await asyncio.sleep(1)
318
- return x + y
367
+
368
+ @wool.task
369
+ async def task_b():
370
+ with wool.pool(port=5051):
371
+ await task_a()
372
+
373
+ async def main():
374
+ with wool.pool(port=5050):
375
+ await task_a()
376
+ await task_b()
377
+
378
+ asyncio.run(main())
319
379
  ```
320
380
 
321
- Module executing remote workflow:
322
- `main.py`
381
+ In this example, `task_a` is executed by two different pools, while `task_b` is executed by the pool on port 5050.
382
+
383
+ ### Best practices
384
+
385
+ #### Sizing worker pools
386
+
387
+ When configuring worker pools, it is important to balance the number of processes with the available system resources:
388
+
389
+ - **CPU-bound tasks**: Size the worker pool to match the number of CPU cores. This is the default behavior when spawning a pool.
390
+ - **I/O-bound tasks**: For workloads involving significant I/O, consider oversizing the pool slightly to maximize the system's I/O capacity utilization.
391
+ - **Mixed workloads**: Monitor memory usage and system load to avoid oversubscription, especially for memory-intensive tasks. Use profiling tools to determine the optimal pool size.
392
+
393
+ #### Defining tasks
394
+
395
+ Wool tasks are coroutine functions that execute asynchronously in a remote `asyncio` event loop. To ensure smooth execution and scalability, prioritize:
396
+
397
+ - **Picklability**: Ensure all task arguments and return values are picklable. Avoid passing unpicklable objects such as open file handles, database connections, or lambda functions.
398
+ - **Statelessness and idempotency**: Design tasks to be stateless and idempotent. Avoid relying on global variables or shared mutable state. This ensures predictable behavior and safe retries.
399
+ - **Non-blocking operations**: To achieve higher concurrency, avoid blocking calls within tasks. Use `asyncio`-compatible libraries for I/O operations.
400
+ - **Inter-process synchronization**: Use Wool's synchronization primitives (e.g., `wool.locking`) for inter-worker and inter-pool coordination. Standard `asyncio` primitives will not behave as expected in a multi-process environment.
401
+
402
+ #### Debugging and logging
403
+
404
+ - Enable detailed logging during development to trace task execution and worker pool behavior:
405
+ ```python
406
+ import logging
407
+ logging.basicConfig(level=logging.DEBUG)
408
+ ```
409
+ - Use Wool's built-in logging configuration to capture worker-specific logs.
410
+
411
+ #### Nested pools and sessions
412
+
413
+ Wool supports nesting pools and sessions to achieve complex workflows. Tasks can be dispatched to specific pools by nesting contexts. This is useful for workflows requiring task segregation or resource isolation.
414
+
415
+ Example:
323
416
  ```python
324
417
  import asyncio, wool
325
- from tasks import sample_task
326
418
 
327
- # Execute the decorated function in an external worker pool
419
+ @wool.task
420
+ async def task_a():
421
+ await asyncio.sleep(1)
422
+
423
+ @wool.task
424
+ async def task_b():
425
+ with wool.pool(port=5051):
426
+ await task_a()
427
+
328
428
  async def main():
329
- with wool.PoolSession(port=5050, authkey=b"deadbeef"):
330
- result = await sample_task(1, 2)
331
- print(f"Result: {result}")
429
+ with wool.pool(port=5050):
430
+ await task_a()
431
+ await task_b()
332
432
 
333
- asyncio.new_event_loop().run_until_complete(main())
433
+ asyncio.run(main())
334
434
  ```
335
435
 
336
- To run the demo, first start a worker pool specifying the module defining the tasks to be executed:
337
- ```bash
338
- wool pool up --port 5050 --authkey deadbeef --breadth 1 --module tasks
339
- ```
436
+ #### Performance optimization
340
437
 
341
- Next, in a separate terminal, execute the application defined in `main.py` and, finally, stop the worker pool:
342
- ```bash
343
- python main.py
344
- wool pool down --port 5050 --authkey deadbeef
438
+ - Minimize the size of arguments and return values to reduce serialization overhead.
439
+ - For large datasets, consider using shared memory or passing references (e.g., file paths) instead of transferring the entire data.
440
+ - Profile tasks to identify and optimize performance bottlenecks.
441
+
442
+ #### Task cancellation
443
+
444
+ - Handle task cancellations gracefully by cleaning up resources and rolling back partial changes.
445
+ - Use `asyncio.CancelledError` to detect and respond to cancellations.
446
+
447
+ #### Error propagation
448
+
449
+ - Wool propagates exceptions raised within tasks to the caller. Use this feature to handle errors centrally in your application.
450
+
451
+ Example:
452
+ ```python
453
+ try:
454
+ result = await some_task()
455
+ except Exception as e:
456
+ print(f"Task failed with error: {e}")
345
457
  ```
346
458
 
347
459
  ## License
wool-0.1rc15/README.md ADDED
@@ -0,0 +1,232 @@
1
+ ![](https://raw.githubusercontent.com/wool-labs/wool/refs/heads/main/assets/woolly-transparent-bg-2048.png)
2
+
3
+ **Wool** is a native Python package for transparently executing tasks in a horizontally scalable, distributed network of agnostic worker processes. Any picklable async function or method can be converted into a task with a simple decorator and a client connection.
4
+
5
+ ## Installation
6
+
7
+ ### Using pip
8
+
9
+ To install the package using pip, run the following command:
10
+
11
+ ```sh
12
+ [uv] pip install --pre wool
13
+ ```
14
+
15
+ ### Cloning from GitHub
16
+
17
+ To install the package by cloning from GitHub, run the following commands:
18
+
19
+ ```sh
20
+ git clone https://github.com/wool-labs/wool.git
21
+ cd wool
22
+ [uv] pip install ./wool
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Declaring tasks
28
+
29
+ Wool tasks are coroutine functions that are executed in a remote `asyncio` event loop within a worker process. To declare a task, use the `@wool.task` decorator:
30
+
31
+ ```python
32
+ import wool
33
+
34
+ @wool.task
35
+ async def sample_task(x, y):
36
+ return x + y
37
+ ```
38
+
39
+ Tasks must be picklable, stateless, and idempotent. Avoid passing unpicklable objects as arguments or return values.
40
+
41
+ ### Worker pools
42
+
43
+ Worker pools are responsible for executing tasks. Wool provides two types of pools:
44
+
45
+ #### Ephemeral pools
46
+
47
+ Ephemeral pools are created and destroyed within the scope of a context manager. Use `wool.pool` to declare an ephemeral pool:
48
+
49
+ ```python
50
+ import asyncio, wool
51
+
52
+ @wool.task
53
+ async def sample_task(x, y):
54
+ return x + y
55
+
56
+ async def main():
57
+ with wool.pool():
58
+ result = await sample_task(1, 2)
59
+ print(f"Result: {result}")
60
+
61
+ asyncio.run(main())
62
+ ```
63
+
64
+ #### Durable pools
65
+
66
+ Durable pools are started independently and persist beyond the scope of a single application. Use the `wool` CLI to manage durable pools:
67
+
68
+ ```bash
69
+ wool pool up --port 5050 --authkey deadbeef --module tasks
70
+ ```
71
+
72
+ Connect to a durable pool using `wool.session`:
73
+
74
+ ```python
75
+ import asyncio, wool
76
+
77
+ @wool.task
78
+ async def sample_task(x, y):
79
+ return x + y
80
+
81
+ async def main():
82
+ with wool.session(port=5050, authkey=b"deadbeef"):
83
+ result = await sample_task(1, 2)
84
+ print(f"Result: {result}")
85
+
86
+ asyncio.run(main())
87
+ ```
88
+
89
+ ### CLI commands
90
+
91
+ Wool provides a command-line interface (CLI) for managing worker pools.
92
+
93
+ #### Start the worker pool
94
+
95
+ ```sh
96
+ wool pool up --host <host> --port <port> --authkey <authkey> --breadth <breadth> --module <module>
97
+ ```
98
+
99
+ - `--host`: The host address (default: `localhost`).
100
+ - `--port`: The port number (default: `0`).
101
+ - `--authkey`: The authentication key (default: `b""`).
102
+ - `--breadth`: The number of worker processes (default: number of CPU cores).
103
+ - `--module`: Python module containing Wool task definitions (optional, can be specified multiple times).
104
+
105
+ #### Stop the worker pool
106
+
107
+ ```sh
108
+ wool pool down --host <host> --port <port> --authkey <authkey> --wait
109
+ ```
110
+
111
+ - `--host`: The host address (default: `localhost`).
112
+ - `--port`: The port number (required).
113
+ - `--authkey`: The authentication key (default: `b""`).
114
+ - `--wait`: Wait for in-flight tasks to complete before shutting down.
115
+
116
+ #### Ping the worker pool
117
+
118
+ ```sh
119
+ wool ping --host <host> --port <port> --authkey <authkey>
120
+ ```
121
+
122
+ - `--host`: The host address (default: `localhost`).
123
+ - `--port`: The port number (required).
124
+ - `--authkey`: The authentication key (default: `b""`).
125
+
126
+ ### Advanced usage
127
+
128
+ #### Nested pools and sessions
129
+
130
+ Wool supports nesting pools and sessions to achieve complex workflows. Tasks can be dispatched to specific pools by nesting contexts:
131
+
132
+ ```python
133
+ import asyncio, wool
134
+
135
+ @wool.task
136
+ async def task_a():
137
+ await asyncio.sleep(1)
138
+
139
+ @wool.task
140
+ async def task_b():
141
+ with wool.pool(port=5051):
142
+ await task_a()
143
+
144
+ async def main():
145
+ with wool.pool(port=5050):
146
+ await task_a()
147
+ await task_b()
148
+
149
+ asyncio.run(main())
150
+ ```
151
+
152
+ In this example, `task_a` is executed by two different pools, while `task_b` is executed by the pool on port 5050.
153
+
154
+ ### Best practices
155
+
156
+ #### Sizing worker pools
157
+
158
+ When configuring worker pools, it is important to balance the number of processes with the available system resources:
159
+
160
+ - **CPU-bound tasks**: Size the worker pool to match the number of CPU cores. This is the default behavior when spawning a pool.
161
+ - **I/O-bound tasks**: For workloads involving significant I/O, consider oversizing the pool slightly to maximize the system's I/O capacity utilization.
162
+ - **Mixed workloads**: Monitor memory usage and system load to avoid oversubscription, especially for memory-intensive tasks. Use profiling tools to determine the optimal pool size.
163
+
164
+ #### Defining tasks
165
+
166
+ Wool tasks are coroutine functions that execute asynchronously in a remote `asyncio` event loop. To ensure smooth execution and scalability, prioritize:
167
+
168
+ - **Picklability**: Ensure all task arguments and return values are picklable. Avoid passing unpicklable objects such as open file handles, database connections, or lambda functions.
169
+ - **Statelessness and idempotency**: Design tasks to be stateless and idempotent. Avoid relying on global variables or shared mutable state. This ensures predictable behavior and safe retries.
170
+ - **Non-blocking operations**: To achieve higher concurrency, avoid blocking calls within tasks. Use `asyncio`-compatible libraries for I/O operations.
171
+ - **Inter-process synchronization**: Use Wool's synchronization primitives (e.g., `wool.locking`) for inter-worker and inter-pool coordination. Standard `asyncio` primitives will not behave as expected in a multi-process environment.
172
+
173
+ #### Debugging and logging
174
+
175
+ - Enable detailed logging during development to trace task execution and worker pool behavior:
176
+ ```python
177
+ import logging
178
+ logging.basicConfig(level=logging.DEBUG)
179
+ ```
180
+ - Use Wool's built-in logging configuration to capture worker-specific logs.
181
+
182
+ #### Nested pools and sessions
183
+
184
+ Wool supports nesting pools and sessions to achieve complex workflows. Tasks can be dispatched to specific pools by nesting contexts. This is useful for workflows requiring task segregation or resource isolation.
185
+
186
+ Example:
187
+ ```python
188
+ import asyncio, wool
189
+
190
+ @wool.task
191
+ async def task_a():
192
+ await asyncio.sleep(1)
193
+
194
+ @wool.task
195
+ async def task_b():
196
+ with wool.pool(port=5051):
197
+ await task_a()
198
+
199
+ async def main():
200
+ with wool.pool(port=5050):
201
+ await task_a()
202
+ await task_b()
203
+
204
+ asyncio.run(main())
205
+ ```
206
+
207
+ #### Performance optimization
208
+
209
+ - Minimize the size of arguments and return values to reduce serialization overhead.
210
+ - For large datasets, consider using shared memory or passing references (e.g., file paths) instead of transferring the entire data.
211
+ - Profile tasks to identify and optimize performance bottlenecks.
212
+
213
+ #### Task cancellation
214
+
215
+ - Handle task cancellations gracefully by cleaning up resources and rolling back partial changes.
216
+ - Use `asyncio.CancelledError` to detect and respond to cancellations.
217
+
218
+ #### Error propagation
219
+
220
+ - Wool propagates exceptions raised within tasks to the caller. Use this feature to handle errors centrally in your application.
221
+
222
+ Example:
223
+ ```python
224
+ try:
225
+ result = await some_task()
226
+ except Exception as e:
227
+ print(f"Task failed with error: {e}")
228
+ ```
229
+
230
+ ## License
231
+
232
+ This project is licensed under the Apache License Version 2.0.
@@ -41,7 +41,7 @@ dev = [
41
41
  "pytest",
42
42
  "pytest-asyncio",
43
43
  "pytest-cov",
44
- "pytest-grpc-aio~=0.2.0",
44
+ "pytest-grpc-aio~=0.3.0",
45
45
  "pytest-mock",
46
46
  "ruff",
47
47
  ]
@@ -2,9 +2,6 @@ from contextvars import ContextVar
2
2
  from importlib.metadata import PackageNotFoundError
3
3
  from importlib.metadata import version
4
4
  from typing import Final
5
- from typing import Generic
6
- from typing import TypeVar
7
- from typing import cast
8
5
 
9
6
  from tblib import pickling_support
10
7
 
@@ -29,34 +26,12 @@ from wool._worker_proxy import WorkerProxy
29
26
  pickling_support.install()
30
27
 
31
28
 
32
- SENTINEL = object()
33
-
34
- T = TypeVar("T")
35
-
36
-
37
- class GlobalVar(Generic[T]):
38
- def __init__(self, default: T | None = None) -> None:
39
- self._default = default
40
- self._value = SENTINEL
41
-
42
- def get(self) -> T | None:
43
- if self._value is SENTINEL:
44
- return self._default
45
- else:
46
- return cast(T, self._value)
47
-
48
- def set(self, value: T):
49
- self._value = value
50
-
51
-
52
29
  try:
53
30
  __version__ = version("wool")
54
31
  except PackageNotFoundError:
55
32
  __version__ = "unknown"
56
33
 
57
- __proxy__: Final[ContextVar[WorkerProxy | None]] = ContextVar(
58
- "__proxy__", default=None
59
- )
34
+ __proxy__: Final[ContextVar[WorkerProxy | None]] = ContextVar("__proxy__", default=None)
60
35
 
61
36
  __proxy_pool__: Final[ContextVar[ResourcePool[WorkerProxy] | None]] = ContextVar(
62
37
  "__proxy_pool__", default=None