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 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,21 +9,41 @@ 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):
21
- def __init__(self, *args, default_host="localhost", default_port=0, default_authkey=b"", **kwargs):
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
+
29
+ def __init__(
30
+ self,
31
+ *args,
32
+ default_host="localhost",
33
+ default_port=0,
34
+ default_authkey=b"",
35
+ **kwargs,
36
+ ):
22
37
  params = kwargs.pop("params", [])
23
38
  params = [
24
39
  click.Option(["--host", "-h"], type=str, default=default_host),
25
40
  click.Option(["--port", "-p"], type=int, default=default_port),
26
- click.Option(["--authkey", "-a"], type=str, default=default_authkey, callback=to_bytes),
41
+ click.Option(
42
+ ["--authkey", "-a"],
43
+ type=str,
44
+ default=default_authkey,
45
+ callback=to_bytes,
46
+ ),
27
47
  *params,
28
48
  ]
29
49
  super().__init__(*args, params=params, **kwargs)
@@ -31,7 +51,11 @@ class WoolPoolCommand(click.core.Command):
31
51
 
32
52
  @contextmanager
33
53
  def timer():
34
- """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
+ """
35
59
  start = end = perf_counter()
36
60
  yield lambda: end - start
37
61
  end = perf_counter()
@@ -41,13 +65,10 @@ def to_bytes(context: click.Context, parameter: click.Parameter, value: str):
41
65
  """
42
66
  Convert the given value to bytes.
43
67
 
44
- Args:
45
- context (click.Context): Click context.
46
- parameter (click.Parameter): Click parameter.
47
- value (str): Value to convert.
48
-
49
- Returns:
50
- 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.
51
72
  """
52
73
  if value is None:
53
74
  return b""
@@ -60,13 +81,10 @@ def assert_nonzero(
60
81
  """
61
82
  Assert that the given value is non-zero.
62
83
 
63
- Args:
64
- context (click.Context): Click context.
65
- parameter (click.Parameter): Click parameter.
66
- value (int): Value to check.
67
-
68
- Returns:
69
- 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.
70
88
  """
71
89
  if value is None:
72
90
  return value
@@ -76,12 +94,11 @@ def assert_nonzero(
76
94
 
77
95
  def debug(ctx, param, value):
78
96
  """
79
- Enable debugging with debugpy.
97
+ Enable debugging mode with a specified port.
80
98
 
81
- Args:
82
- ctx (click.Context): Click context.
83
- param (click.Parameter): Click parameter.
84
- 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.
85
102
  """
86
103
  if not value or ctx.resilient_parsing:
87
104
  return
@@ -100,7 +117,10 @@ def debug(ctx, param, value):
100
117
  "-d",
101
118
  callback=debug,
102
119
  expose_value=False,
103
- 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
+ ),
104
124
  is_eager=True,
105
125
  type=int,
106
126
  )
@@ -116,10 +136,7 @@ def cli(verbosity: int):
116
136
  """
117
137
  CLI command group with options for verbosity, debugging, and version.
118
138
 
119
- Args:
120
- verbosity (int): Verbosity level for logging.
121
- debug (bool): Flag to enable debugging.
122
- version (bool): Flag to display the version and exit.
139
+ :param verbosity: Verbosity level for logging.
123
140
  """
124
141
  match verbosity:
125
142
  case 4:
@@ -138,10 +155,14 @@ def cli(verbosity: int):
138
155
 
139
156
 
140
157
  @cli.group()
141
- def pool(): ...
158
+ def pool():
159
+ """
160
+ CLI command group for managing worker pools.
161
+ """
162
+ pass
142
163
 
143
164
 
144
- @pool.command(cls=partial(WoolPoolCommand, default_port=DEFAULT_PORT))
165
+ @pool.command(cls=partial(WorkerPoolCommand, default_port=DEFAULT_PORT))
145
166
  @click.option(
146
167
  "--breadth", "-b", type=int, default=cpu_count(), callback=assert_nonzero
147
168
  )
@@ -151,14 +172,26 @@ def pool(): ...
151
172
  "-m",
152
173
  multiple=True,
153
174
  type=str,
154
- 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
+ ),
155
179
  )
156
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
+ """
157
190
  for module in modules:
158
191
  importlib.import_module(module)
159
192
  if not authkey:
160
193
  logging.warning("No authkey specified")
161
- workerpool = WoolPool(
194
+ workerpool = WorkerPool(
162
195
  address=(host, port),
163
196
  breadth=breadth,
164
197
  authkey=authkey,
@@ -168,7 +201,7 @@ def up(host, port, authkey, breadth, modules):
168
201
  workerpool.join()
169
202
 
170
203
 
171
- @pool.command(cls=partial(WoolPoolCommand, default_port=DEFAULT_PORT))
204
+ @pool.command(cls=partial(WorkerPoolCommand, default_port=DEFAULT_PORT))
172
205
  @click.option(
173
206
  "--wait",
174
207
  "-w",
@@ -177,17 +210,32 @@ def up(host, port, authkey, breadth, modules):
177
210
  help="Wait for in-flight tasks to complete before shutting down.",
178
211
  )
179
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
+ """
180
221
  assert port
181
222
  if not host:
182
223
  host = "localhost"
183
224
  if not authkey:
184
225
  authkey = b""
185
- client = WoolClient(address=(host, port), authkey=authkey).connect()
186
- client.stop(wait=wait)
226
+ with WorkerPoolSession(address=(host, port), authkey=authkey) as client:
227
+ client.stop(wait=wait)
187
228
 
188
229
 
189
- @cli.command(cls=partial(WoolPoolCommand, default_port=DEFAULT_PORT))
230
+ @cli.command(cls=partial(WorkerPoolCommand, default_port=DEFAULT_PORT))
190
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
+ """
191
239
  assert port
192
240
  if not host:
193
241
  host = "localhost"
@@ -196,14 +244,19 @@ def ping(host, port, authkey):
196
244
 
197
245
  async def _():
198
246
  with timer() as t:
199
- await asyncio.create_task(_ping())
247
+ await _ping()
200
248
 
201
249
  print(f"Ping: {int((t() * 1000) + 0.5)} ms")
202
250
 
203
- with WoolClient(address=(host, port), authkey=authkey):
251
+ with WorkerPoolSession(address=(host, port), authkey=authkey):
204
252
  asyncio.get_event_loop().run_until_complete(_())
205
253
 
206
254
 
207
255
  @task
208
256
  async def _ping():
257
+ """
258
+ Asynchronous task to log a ping message.
259
+
260
+ :return: None
261
+ """
209
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):
@@ -21,7 +28,13 @@ class WoolLogFilter(logging.Filter):
21
28
  return True
22
29
 
23
30
 
24
- __log_format__: str = f"{grey(italic('pid:'))}%(process)-8d {grey(italic('process:'))}%(processName)-12s {grey(italic('thread:'))}%(threadName)-20s %(levelname)12s %(message)-60s {grey(italic('%(ref)s'))}"
31
+ __log_format__: str = (
32
+ f"{grey(italic('pid:'))}%(process)-8d "
33
+ f"{grey(italic('process:'))}%(processName)-12s "
34
+ f"{grey(italic('thread:'))}%(threadName)-20s "
35
+ "%(levelname)12s %(message)-60s "
36
+ f"{grey(italic('%(ref)s'))}"
37
+ )
25
38
 
26
39
  formatter: logging.Formatter = logging.Formatter(__log_format__)
27
40