wool 0.1rc6__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 CHANGED
@@ -1,23 +1,30 @@
1
1
  import logging
2
2
  from contextvars import ContextVar
3
3
  from importlib.metadata import entry_points
4
- from typing import Final, Literal
4
+ from typing import Final
5
5
 
6
- from wool._cli import WoolPoolCommand, cli
7
- from wool._client import NullClient, WoolClient
8
- from wool._future import WoolFuture
6
+ from tblib import pickling_support
7
+
8
+ from wool._cli import WorkerPoolCommand
9
+ from wool._cli import cli
10
+ from wool._future import Future
9
11
  from wool._logging import __log_format__
10
- from wool._pool import WoolPool
11
- from wool._task import (
12
- WoolTask,
13
- WoolTaskEvent,
14
- WoolTaskEventCallback,
15
- WoolTaskException,
16
- current_task,
17
- task,
18
- )
12
+ from wool._pool import WorkerPool
13
+ from wool._pool import pool
14
+ from wool._session import LocalSession
15
+ from wool._session import WorkerPoolSession
16
+ from wool._session import session
17
+ from wool._task import Task
18
+ from wool._task import TaskEvent
19
+ from wool._task import TaskEventCallback
20
+ from wool._task import TaskException
21
+ from wool._task import current_task
22
+ from wool._task import task
23
+ from wool._worker import Scheduler
19
24
  from wool._worker import Worker
20
25
 
26
+ pickling_support.install()
27
+
21
28
  # PUBLIC
22
29
  __log_format__: str = __log_format__
23
30
 
@@ -25,26 +32,29 @@ __log_format__: str = __log_format__
25
32
  __log_level__: int = logging.INFO
26
33
 
27
34
  # PUBLIC
28
- __wool_client__: Final[ContextVar[WoolClient]] = ContextVar(
29
- "__wool_client__", default=NullClient()
35
+ __wool_session__: Final[ContextVar[WorkerPoolSession]] = ContextVar(
36
+ "__wool_session__", default=LocalSession()
30
37
  )
31
38
 
32
- __wool_worker__: Worker | Literal[True] | None = None
39
+ __wool_worker__: Worker | None = None
33
40
 
34
41
  __all__ = [
35
- "WoolTaskException",
36
- "WoolFuture",
37
- "WoolTask",
38
- "WoolTaskEvent",
39
- "WoolTaskEventCallback",
40
- "WoolPool",
41
- "WoolClient",
42
- "WoolPoolCommand",
42
+ "TaskException",
43
+ "Future",
44
+ "Task",
45
+ "TaskEvent",
46
+ "TaskEventCallback",
47
+ "WorkerPool",
48
+ "WorkerPoolSession",
49
+ "WorkerPoolCommand",
50
+ "Scheduler",
43
51
  "__log_format__",
44
52
  "__log_level__",
45
- "__wool_client__",
53
+ "__wool_session__",
46
54
  "cli",
47
55
  "current_task",
56
+ "pool",
57
+ "session",
48
58
  "task",
49
59
  ]
50
60
 
@@ -57,10 +67,10 @@ for symbol in __all__:
57
67
  except AttributeError:
58
68
  continue
59
69
 
60
- for plugin in entry_points(group="wool.cli.plugins"):
70
+ for plugin in entry_points(group="wool_cli_plugins"):
61
71
  try:
62
72
  plugin.load()
63
73
  logging.info(f"Loaded CLI plugin {plugin.name}")
64
74
  except Exception as e:
65
75
  logging.error(f"Failed to load CLI plugin {plugin.name}: {e}")
66
- continue
76
+ raise
wool/_cli.py CHANGED
@@ -9,15 +9,23 @@ from time import perf_counter
9
9
  import click
10
10
 
11
11
  import wool
12
- from wool._client import WoolClient
13
- from wool._pool import WoolPool
12
+ from wool._pool import WorkerPool
13
+ from wool._session import WorkerPoolSession
14
14
  from wool._task import task
15
15
 
16
16
  DEFAULT_PORT = 48800
17
17
 
18
18
 
19
19
  # PUBLIC
20
- class WoolPoolCommand(click.core.Command):
20
+ class WorkerPoolCommand(click.core.Command):
21
+ """
22
+ Custom Click command class for worker pool commands.
23
+
24
+ :param default_host: Default host address.
25
+ :param default_port: Default port number.
26
+ :param default_authkey: Default authentication key.
27
+ """
28
+
21
29
  def __init__(
22
30
  self,
23
31
  *args,
@@ -43,7 +51,11 @@ class WoolPoolCommand(click.core.Command):
43
51
 
44
52
  @contextmanager
45
53
  def timer():
46
- """Context manager to measure the execution time of a code block."""
54
+ """
55
+ Context manager to measure the execution time of a code block.
56
+
57
+ :return: A function to retrieve the elapsed time.
58
+ """
47
59
  start = end = perf_counter()
48
60
  yield lambda: end - start
49
61
  end = perf_counter()
@@ -53,13 +65,10 @@ def to_bytes(context: click.Context, parameter: click.Parameter, value: str):
53
65
  """
54
66
  Convert the given value to bytes.
55
67
 
56
- Args:
57
- context (click.Context): Click context.
58
- parameter (click.Parameter): Click parameter.
59
- value (str): Value to convert.
60
-
61
- Returns:
62
- bytes: The converted value in bytes.
68
+ :param context: Click context.
69
+ :param parameter: Click parameter.
70
+ :param value: Value to convert.
71
+ :return: The converted value in bytes.
63
72
  """
64
73
  if value is None:
65
74
  return b""
@@ -72,13 +81,10 @@ def assert_nonzero(
72
81
  """
73
82
  Assert that the given value is non-zero.
74
83
 
75
- Args:
76
- context (click.Context): Click context.
77
- parameter (click.Parameter): Click parameter.
78
- value (int): Value to check.
79
-
80
- Returns:
81
- int: The original value if it is non-zero.
84
+ :param context: Click context.
85
+ :param parameter: Click parameter.
86
+ :param value: Value to check.
87
+ :return: The original value if it is non-zero.
82
88
  """
83
89
  if value is None:
84
90
  return value
@@ -88,12 +94,11 @@ def assert_nonzero(
88
94
 
89
95
  def debug(ctx, param, value):
90
96
  """
91
- Enable debugging with debugpy.
97
+ Enable debugging mode with a specified port.
92
98
 
93
- Args:
94
- ctx (click.Context): Click context.
95
- param (click.Parameter): Click parameter.
96
- value (bool): Flag value indicating whether to enable debugging.
99
+ :param ctx: The Click context object.
100
+ :param param: The parameter being handled.
101
+ :param value: The port number for the debugger.
97
102
  """
98
103
  if not value or ctx.resilient_parsing:
99
104
  return
@@ -112,7 +117,10 @@ def debug(ctx, param, value):
112
117
  "-d",
113
118
  callback=debug,
114
119
  expose_value=False,
115
- help="Run with debugger listening on the specified port. Execution will block until the debugger is attached.",
120
+ help=(
121
+ "Run with debugger listening on the specified port. Execution will "
122
+ "block until the debugger is attached."
123
+ ),
116
124
  is_eager=True,
117
125
  type=int,
118
126
  )
@@ -128,10 +136,7 @@ def cli(verbosity: int):
128
136
  """
129
137
  CLI command group with options for verbosity, debugging, and version.
130
138
 
131
- Args:
132
- verbosity (int): Verbosity level for logging.
133
- debug (bool): Flag to enable debugging.
134
- version (bool): Flag to display the version and exit.
139
+ :param verbosity: Verbosity level for logging.
135
140
  """
136
141
  match verbosity:
137
142
  case 4:
@@ -150,10 +155,14 @@ def cli(verbosity: int):
150
155
 
151
156
 
152
157
  @cli.group()
153
- def pool(): ...
158
+ def pool():
159
+ """
160
+ CLI command group for managing worker pools.
161
+ """
162
+ pass
154
163
 
155
164
 
156
- @pool.command(cls=partial(WoolPoolCommand, default_port=DEFAULT_PORT))
165
+ @pool.command(cls=partial(WorkerPoolCommand, default_port=DEFAULT_PORT))
157
166
  @click.option(
158
167
  "--breadth", "-b", type=int, default=cpu_count(), callback=assert_nonzero
159
168
  )
@@ -163,14 +172,26 @@ def pool(): ...
163
172
  "-m",
164
173
  multiple=True,
165
174
  type=str,
166
- help="Python module containing workerpool task definitions to be executed by this pool.",
175
+ help=(
176
+ "Python module containing workerpool task definitions to be executed "
177
+ "by this pool."
178
+ ),
167
179
  )
168
180
  def up(host, port, authkey, breadth, modules):
181
+ """
182
+ Start a worker pool with the specified configuration.
183
+
184
+ :param host: The host address for the worker pool.
185
+ :param port: The port number for the worker pool.
186
+ :param authkey: The authentication key for the worker pool.
187
+ :param breadth: The number of worker processes in the pool.
188
+ :param modules: Python modules containing task definitions.
189
+ """
169
190
  for module in modules:
170
191
  importlib.import_module(module)
171
192
  if not authkey:
172
193
  logging.warning("No authkey specified")
173
- workerpool = WoolPool(
194
+ workerpool = WorkerPool(
174
195
  address=(host, port),
175
196
  breadth=breadth,
176
197
  authkey=authkey,
@@ -180,7 +201,7 @@ def up(host, port, authkey, breadth, modules):
180
201
  workerpool.join()
181
202
 
182
203
 
183
- @pool.command(cls=partial(WoolPoolCommand, default_port=DEFAULT_PORT))
204
+ @pool.command(cls=partial(WorkerPoolCommand, default_port=DEFAULT_PORT))
184
205
  @click.option(
185
206
  "--wait",
186
207
  "-w",
@@ -189,17 +210,32 @@ def up(host, port, authkey, breadth, modules):
189
210
  help="Wait for in-flight tasks to complete before shutting down.",
190
211
  )
191
212
  def down(host, port, authkey, wait):
213
+ """
214
+ Shut down the worker pool.
215
+
216
+ :param host: The host address of the worker pool.
217
+ :param port: The port number of the worker pool.
218
+ :param authkey: The authentication key for the worker pool.
219
+ :param wait: Whether to wait for in-flight tasks to complete.
220
+ """
192
221
  assert port
193
222
  if not host:
194
223
  host = "localhost"
195
224
  if not authkey:
196
225
  authkey = b""
197
- client = WoolClient(address=(host, port), authkey=authkey).connect()
198
- client.stop(wait=wait)
226
+ with WorkerPoolSession(address=(host, port), authkey=authkey) as client:
227
+ client.stop(wait=wait)
199
228
 
200
229
 
201
- @cli.command(cls=partial(WoolPoolCommand, default_port=DEFAULT_PORT))
230
+ @cli.command(cls=partial(WorkerPoolCommand, default_port=DEFAULT_PORT))
202
231
  def ping(host, port, authkey):
232
+ """
233
+ Ping the worker pool to check connectivity.
234
+
235
+ :param host: The host address of the worker pool.
236
+ :param port: The port number of the worker pool.
237
+ :param authkey: The authentication key for the worker pool.
238
+ """
203
239
  assert port
204
240
  if not host:
205
241
  host = "localhost"
@@ -208,14 +244,19 @@ def ping(host, port, authkey):
208
244
 
209
245
  async def _():
210
246
  with timer() as t:
211
- await asyncio.create_task(_ping())
247
+ await _ping()
212
248
 
213
249
  print(f"Ping: {int((t() * 1000) + 0.5)} ms")
214
250
 
215
- with WoolClient(address=(host, port), authkey=authkey):
251
+ with WorkerPoolSession(address=(host, port), authkey=authkey):
216
252
  asyncio.get_event_loop().run_until_complete(_())
217
253
 
218
254
 
219
255
  @task
220
256
  async def _ping():
257
+ """
258
+ Asynchronous task to log a ping message.
259
+
260
+ :return: None
261
+ """
221
262
  logging.debug("Ping!")
wool/_event.py ADDED
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from time import perf_counter_ns
5
+ from typing import TYPE_CHECKING
6
+ from typing import Literal
7
+
8
+ from wool._typing import PassthroughDecorator
9
+
10
+ if TYPE_CHECKING:
11
+ from wool._task import Task
12
+ from wool._task import TaskEventCallback
13
+
14
+
15
+ # PUBLIC
16
+ class TaskEvent:
17
+ """
18
+ Represents an event related to a Wool task, such as creation, queuing,
19
+ scheduling, starting, stopping, or completion.
20
+
21
+ Task events are emitted during the lifecycle of a task. These events can
22
+ be used to track task execution and measure performance, such as CPU
23
+ utilization.
24
+
25
+ :param type: The type of the task event.
26
+ :param task: The task associated with the event.
27
+ """
28
+
29
+ type: TaskEventType
30
+ task: Task
31
+
32
+ _handlers: dict[str, list[TaskEventCallback]] = {}
33
+
34
+ def __init__(self, type: TaskEventType, /, task: Task) -> None:
35
+ """
36
+ Initialize a WoolTaskEvent instance.
37
+
38
+ :param type: The type of the task event.
39
+ :param task: The task associated with the event.
40
+ """
41
+ self.type = type
42
+ self.task = task
43
+
44
+ @classmethod
45
+ def handler(
46
+ cls, *event_types: TaskEventType
47
+ ) -> PassthroughDecorator[TaskEventCallback]:
48
+ """
49
+ Register a handler function for specific task event types.
50
+
51
+ :param event_types: The event types to handle.
52
+ :return: A decorator to register the handler function.
53
+ """
54
+
55
+ def _handler(
56
+ fn: TaskEventCallback,
57
+ ) -> TaskEventCallback:
58
+ for event_type in event_types:
59
+ cls._handlers.setdefault(event_type, []).append(fn)
60
+ return fn
61
+
62
+ return _handler
63
+
64
+ def emit(self):
65
+ """
66
+ Emit the task event, invoking all registered handlers for the event
67
+ type.
68
+
69
+ Handlers are called with the event instance and a timestamp.
70
+
71
+ :raises Exception: If any handler raises an exception.
72
+ """
73
+ logging.debug(
74
+ f"Emitting {self.type} event for "
75
+ f"task {self.task.id} "
76
+ f"({self.task.callable.__qualname__})"
77
+ )
78
+ if handlers := self._handlers.get(self.type):
79
+ timestamp = perf_counter_ns()
80
+ for handler in handlers:
81
+ handler(self, timestamp)
82
+
83
+
84
+ # PUBLIC
85
+ TaskEventType = Literal[
86
+ "task-created",
87
+ "task-queued",
88
+ "task-scheduled",
89
+ "task-started",
90
+ "task-stopped",
91
+ "task-completed",
92
+ ]
93
+ """
94
+ Defines the types of events that can occur during the lifecycle of a Wool
95
+ task.
96
+
97
+ - "task-created":
98
+ Emitted when a task is created.
99
+ - "task-queued":
100
+ Emitted when a task is added to the queue.
101
+ - "task-scheduled":
102
+ Emitted when a task is scheduled for execution in a worker's event loop.
103
+ - "task-started":
104
+ Emitted when a task starts execution.
105
+ - "task-stopped":
106
+ Emitted when a task stops execution.
107
+ - "task-completed":
108
+ Emitted when a task completes execution.
109
+ """
wool/_future.py CHANGED
@@ -3,16 +3,34 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import concurrent.futures
5
5
  import logging
6
- from typing import Any, Generator, Generic, TypeVar, cast
6
+ from typing import Any
7
+ from typing import Generator
8
+ from typing import Generic
9
+ from typing import TypeVar
10
+ from typing import cast
7
11
 
8
- from wool._utils import Undefined, UndefinedType
12
+ from wool._utils import Undefined
13
+ from wool._utils import UndefinedType
9
14
 
10
15
  T = TypeVar("T")
11
16
 
12
17
 
13
18
  # PUBLIC
14
- class WoolFuture(Generic[T]):
19
+ class Future(Generic[T]):
20
+ """
21
+ A future object representing the result of an asynchronous operation.
22
+
23
+ WoolFuture provides methods to retrieve the result or exception of an
24
+ asynchronous operation, set the result or exception, and await its
25
+ completion.
26
+
27
+ :param T: The type of the result.
28
+ """
29
+
15
30
  def __init__(self) -> None:
31
+ """
32
+ Initialize a WoolFuture instance.
33
+ """
16
34
  self._result: T | UndefinedType = Undefined
17
35
  self._exception: (
18
36
  BaseException | type[BaseException] | UndefinedType
@@ -21,6 +39,12 @@ class WoolFuture(Generic[T]):
21
39
  self._cancelled: bool = False
22
40
 
23
41
  def __await__(self) -> Generator[Any, None, T]:
42
+ """
43
+ Await the completion of the future.
44
+
45
+ :return: The result of the future.
46
+ """
47
+
24
48
  async def _():
25
49
  while not self.done():
26
50
  await asyncio.sleep(0)
@@ -30,6 +54,13 @@ class WoolFuture(Generic[T]):
30
54
  return _().__await__()
31
55
 
32
56
  def result(self) -> T:
57
+ """
58
+ Retrieve the result of the future.
59
+
60
+ :return: The result of the future.
61
+ :raises BaseException: If the future completed with an exception.
62
+ :raises asyncio.InvalidStateError: If the future is not yet completed.
63
+ """
33
64
  if self._exception is not Undefined:
34
65
  assert (
35
66
  isinstance(self._exception, BaseException)
@@ -43,6 +74,12 @@ class WoolFuture(Generic[T]):
43
74
  raise asyncio.InvalidStateError
44
75
 
45
76
  def set_result(self, result: T) -> None:
77
+ """
78
+ Set the result of the future.
79
+
80
+ :param result: The result to set.
81
+ :raises asyncio.InvalidStateError: If the future is already completed.
82
+ """
46
83
  if self.done():
47
84
  raise asyncio.InvalidStateError
48
85
  else:
@@ -50,7 +87,14 @@ class WoolFuture(Generic[T]):
50
87
  self._done = True
51
88
 
52
89
  def exception(self) -> BaseException | type[BaseException]:
53
- if self._exception is not Undefined:
90
+ """
91
+ Retrieve the exception of the future, if any.
92
+
93
+ :return: The exception of the future.
94
+ :raises asyncio.InvalidStateError: If the future is not yet completed
95
+ or has no exception.
96
+ """
97
+ if self.done() and self._exception is not Undefined:
54
98
  return cast(BaseException | type[BaseException], self._exception)
55
99
  else:
56
100
  raise asyncio.InvalidStateError
@@ -58,6 +102,12 @@ class WoolFuture(Generic[T]):
58
102
  def set_exception(
59
103
  self, exception: BaseException | type[BaseException]
60
104
  ) -> None:
105
+ """
106
+ Set the exception of the future.
107
+
108
+ :param exception: The exception to set.
109
+ :raises asyncio.InvalidStateError: If the future is already completed.
110
+ """
61
111
  if self.done():
62
112
  raise asyncio.InvalidStateError
63
113
  else:
@@ -65,17 +115,35 @@ class WoolFuture(Generic[T]):
65
115
  self._done = True
66
116
 
67
117
  def done(self) -> bool:
68
- return self._done or self._cancelled
118
+ """
119
+ Check if the future is completed.
120
+
121
+ :return: True if the future is completed, False otherwise.
122
+ """
123
+ return self._done
69
124
 
70
125
  def cancel(self) -> None:
71
- self.set_exception(asyncio.CancelledError)
72
- self._cancelled = True
126
+ """
127
+ Cancel the future.
128
+
129
+ :raises asyncio.InvalidStateError: If the future is already completed.
130
+ """
131
+ if self.done():
132
+ raise asyncio.InvalidStateError
133
+ else:
134
+ self._cancelled = True
135
+ self._done = True
73
136
 
74
137
  def cancelled(self) -> bool:
138
+ """
139
+ Check if the future was cancelled.
140
+
141
+ :return: True if the future was cancelled, False otherwise.
142
+ """
75
143
  return self._cancelled
76
144
 
77
145
 
78
- async def poll(future: WoolFuture, task: concurrent.futures.Future) -> None:
146
+ async def poll(future: Future, task: concurrent.futures.Future) -> None:
79
147
  while True:
80
148
  if future.cancelled():
81
149
  task.cancel()
@@ -86,15 +154,18 @@ async def poll(future: WoolFuture, task: concurrent.futures.Future) -> None:
86
154
  await asyncio.sleep(0)
87
155
 
88
156
 
89
- def fulfill(future: WoolFuture):
157
+ def fulfill(future: Future):
90
158
  def callback(task: concurrent.futures.Future):
91
159
  try:
92
- future.set_result(task.result())
160
+ result = task.result()
93
161
  except concurrent.futures.CancelledError:
94
- if not future.cancelled():
162
+ if not future.done():
95
163
  future.cancel()
96
164
  except BaseException as e:
97
165
  logging.exception(e)
98
166
  future.set_exception(e)
167
+ else:
168
+ if not future.done():
169
+ future.set_result(result)
99
170
 
100
171
  return callback
wool/_logging.py CHANGED
@@ -12,6 +12,13 @@ def italic(text: str) -> str:
12
12
 
13
13
  class WoolLogFilter(logging.Filter):
14
14
  def filter(self, record: logging.LogRecord) -> bool:
15
+ """
16
+ Modify the log record to include a reference to the source file and
17
+ line number.
18
+
19
+ :param record: The log record to modify.
20
+ :return: True to indicate the record should be logged.
21
+ """
15
22
  pathname: str = record.pathname
16
23
  cwd: str = os.getcwd()
17
24
  if pathname.startswith(cwd):