leasepool 0.1.1__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.
- leasepool/__init__.py +28 -0
- leasepool/_process_logging.py +141 -0
- leasepool/backends.py +98 -0
- leasepool/exceptions.py +18 -0
- leasepool/grinder.py +479 -0
- leasepool/manager.py +840 -0
- leasepool/py.typed +0 -0
- leasepool/types.py +7 -0
- leasepool-0.1.1.dist-info/METADATA +345 -0
- leasepool-0.1.1.dist-info/RECORD +12 -0
- leasepool-0.1.1.dist-info/WHEEL +4 -0
- leasepool-0.1.1.dist-info/licenses/LICENSE +202 -0
leasepool/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
4
|
+
|
|
5
|
+
from .backends import ExecutorBackend
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
LeaseExpiredError,
|
|
8
|
+
LeasePoolError,
|
|
9
|
+
LeasePoolNotStartedError,
|
|
10
|
+
LeaseUnavailableError,
|
|
11
|
+
UnsupportedBackendError,
|
|
12
|
+
)
|
|
13
|
+
from .grinder import WorkGrinder
|
|
14
|
+
from ._process_logging import ProcessLoggingConfig
|
|
15
|
+
from .manager import ExecutorLease, LeasedExecutorManager
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ExecutorBackend",
|
|
19
|
+
"ExecutorLease",
|
|
20
|
+
"LeasedExecutorManager",
|
|
21
|
+
"LeaseExpiredError",
|
|
22
|
+
"LeasePoolError",
|
|
23
|
+
"LeasePoolNotStartedError",
|
|
24
|
+
"LeaseUnavailableError",
|
|
25
|
+
"UnsupportedBackendError",
|
|
26
|
+
"WorkGrinder",
|
|
27
|
+
"ProcessLoggingConfig",
|
|
28
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from logging.handlers import QueueHandler, QueueListener
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class ProcessLoggingConfig:
|
|
12
|
+
"""Configuration for forwarding ProcessPoolExecutor worker logs.
|
|
13
|
+
|
|
14
|
+
This is intentionally opt-in. Normal leasepool logs are emitted in the parent
|
|
15
|
+
process through normal Python loggers and do not need this bridge.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
enabled: Enable child-process log forwarding.
|
|
19
|
+
level: Minimum level configured on the child-process root logger.
|
|
20
|
+
target_logger: Parent-process logger that receives records from workers.
|
|
21
|
+
If omitted, LeasedExecutorManager's logger is used.
|
|
22
|
+
clear_child_handlers: Remove inherited/preconfigured child handlers before
|
|
23
|
+
installing the queue handler. This avoids duplicate child logs after
|
|
24
|
+
fork and prevents children from writing directly to stderr.
|
|
25
|
+
"""
|
|
26
|
+
enabled: bool = False
|
|
27
|
+
level: int | str = logging.INFO
|
|
28
|
+
target_logger: logging.Logger | None = None
|
|
29
|
+
clear_child_handlers: bool = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def coerce_log_level(level: int | str) -> int:
|
|
33
|
+
"""Return a numeric logging level from an int or standard level name."""
|
|
34
|
+
if isinstance(level, str):
|
|
35
|
+
try:
|
|
36
|
+
resolved = logging.getLevelName(level.upper())
|
|
37
|
+
if isinstance(resolved, int):
|
|
38
|
+
return resolved
|
|
39
|
+
raise ValueError(f"Unknown logging level: {level!r}")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise ValueError(f"Invalid logging level: {level!r}") from e
|
|
42
|
+
|
|
43
|
+
return int(level)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LoggerForwardingHandler(logging.Handler):
|
|
47
|
+
"""QueueListener target that forwards records into a parent logger.
|
|
48
|
+
|
|
49
|
+
QueueListener normally writes records directly to concrete handlers. This
|
|
50
|
+
handler instead re-enters the parent's logger hierarchy, so the application
|
|
51
|
+
keeps control over formatters, handlers, filters, propagation, and levels.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, target_logger: logging.Logger):
|
|
55
|
+
"""Initialize the handler with a target logger.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
target_logger (logging.Logger): The parent logger to which records will be forwarded.
|
|
59
|
+
"""
|
|
60
|
+
super().__init__(level=logging.NOTSET)
|
|
61
|
+
self._target_logger = target_logger
|
|
62
|
+
|
|
63
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
64
|
+
"""Forward a log record to the target logger.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
record (logging.LogRecord): The log record to be forwarded.
|
|
68
|
+
"""
|
|
69
|
+
if not self._target_logger.isEnabledFor(record.levelno):
|
|
70
|
+
return
|
|
71
|
+
self._target_logger.handle(record)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def configure_process_worker_logging(
|
|
75
|
+
log_queue: Any,
|
|
76
|
+
*,
|
|
77
|
+
level: int | str,
|
|
78
|
+
clear_existing_handlers: bool,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Install QueueHandler on the worker process root logger.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
log_queue: The multiprocessing queue to which log records will be sent. If None, logging is not configured.
|
|
84
|
+
level: Minimum logging level for the worker process root logger.
|
|
85
|
+
clear_existing_handlers: Whether to remove existing handlers from the root logger before adding the QueueHandler.
|
|
86
|
+
"""
|
|
87
|
+
root_logger = logging.getLogger()
|
|
88
|
+
|
|
89
|
+
if clear_existing_handlers:
|
|
90
|
+
root_logger.handlers.clear()
|
|
91
|
+
|
|
92
|
+
root_logger.addHandler(QueueHandler(log_queue))
|
|
93
|
+
root_logger.setLevel(coerce_log_level(level))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def process_worker_initializer(
|
|
97
|
+
log_queue: Any | None,
|
|
98
|
+
level: int | str,
|
|
99
|
+
clear_existing_handlers: bool,
|
|
100
|
+
user_initializer: Callable[..., Any] | None,
|
|
101
|
+
user_initargs: tuple[Any, ...],
|
|
102
|
+
) -> None:
|
|
103
|
+
"""ProcessPoolExecutor initializer used by leasepool.
|
|
104
|
+
|
|
105
|
+
It first configures child-process logging, then calls the user's original
|
|
106
|
+
initializer, if one was supplied to ProcessPoolExecutor.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
log_queue: The multiprocessing queue to which log records will be sent. If None, logging is not configured.
|
|
110
|
+
level: Minimum logging level for the worker process root logger.
|
|
111
|
+
clear_existing_handlers: Whether to remove existing handlers from the root logger before adding the QueueHandler.
|
|
112
|
+
user_initializer: The original initializer function supplied by the user to ProcessPoolExecutor, if any.
|
|
113
|
+
user_initargs: The original initializer arguments supplied by the user to ProcessPoolExecutor, if any.
|
|
114
|
+
"""
|
|
115
|
+
if log_queue is not None:
|
|
116
|
+
configure_process_worker_logging(
|
|
117
|
+
log_queue,
|
|
118
|
+
level=level,
|
|
119
|
+
clear_existing_handlers=clear_existing_handlers,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if user_initializer is not None:
|
|
123
|
+
user_initializer(*user_initargs)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_queue_listener(
|
|
127
|
+
*,
|
|
128
|
+
log_queue: Any,
|
|
129
|
+
target_logger: logging.Logger,
|
|
130
|
+
) -> QueueListener:
|
|
131
|
+
"""Create the parent-side listener for child-process log records.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
log_queue: The multiprocessing queue from which log records will be received.
|
|
135
|
+
target_logger: The parent logger to which received log records will be forwarded.
|
|
136
|
+
"""
|
|
137
|
+
return QueueListener(
|
|
138
|
+
log_queue,
|
|
139
|
+
LoggerForwardingHandler(target_logger),
|
|
140
|
+
respect_handler_level=True,
|
|
141
|
+
)
|
leasepool/backends.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExecutorBackend(StrEnum):
|
|
9
|
+
"""Enum representing the available executor backends.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
StrEnum (_type_): String-based enum for executor backends.
|
|
13
|
+
"""
|
|
14
|
+
THREAD = "thread"
|
|
15
|
+
PROCESS = "process"
|
|
16
|
+
INTERPRETER = "interpreter"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_backend(backend: ExecutorBackend | str) -> ExecutorBackend:
|
|
20
|
+
"""Normalize the backend to an ExecutorBackend enum.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
backend (ExecutorBackend | str): The backend to normalize.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If the backend is not supported.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ExecutorBackend: The normalized backend.
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(backend, ExecutorBackend):
|
|
32
|
+
return backend
|
|
33
|
+
try:
|
|
34
|
+
return ExecutorBackend(str(backend).lower())
|
|
35
|
+
except ValueError as exc:
|
|
36
|
+
supported = ", ".join(item.value for item in ExecutorBackend)
|
|
37
|
+
raise ValueError(f"Unsupported backend {backend!r}. Supported: {supported}") from exc
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_executor_cls(backend: ExecutorBackend | str) -> type[Executor]:
|
|
41
|
+
"""Resolve the executor class for the given backend.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
backend (ExecutorBackend | str): The backend to resolve.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
UnsupportedBackendError: If the backend is not supported.
|
|
48
|
+
UnsupportedBackendError: If the InterpreterPoolExecutor is not available.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
type[Executor]: The executor class for the given backend.
|
|
52
|
+
"""
|
|
53
|
+
from .exceptions import UnsupportedBackendError
|
|
54
|
+
|
|
55
|
+
normalized = normalize_backend(backend)
|
|
56
|
+
|
|
57
|
+
if normalized is ExecutorBackend.THREAD:
|
|
58
|
+
return ThreadPoolExecutor
|
|
59
|
+
|
|
60
|
+
if normalized is ExecutorBackend.PROCESS:
|
|
61
|
+
return ProcessPoolExecutor
|
|
62
|
+
|
|
63
|
+
if normalized is ExecutorBackend.INTERPRETER:
|
|
64
|
+
try:
|
|
65
|
+
from concurrent.futures import InterpreterPoolExecutor # type: ignore[attr-defined]
|
|
66
|
+
except ImportError as exc:
|
|
67
|
+
raise UnsupportedBackendError(
|
|
68
|
+
"InterpreterPoolExecutor is available only on Python 3.14+."
|
|
69
|
+
) from exc
|
|
70
|
+
|
|
71
|
+
return InterpreterPoolExecutor
|
|
72
|
+
|
|
73
|
+
# Defensive fallback for static analyzers.
|
|
74
|
+
raise UnsupportedBackendError(f"Unsupported backend: {backend!r}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_executor(
|
|
78
|
+
*,
|
|
79
|
+
backend: ExecutorBackend | str,
|
|
80
|
+
max_workers: int,
|
|
81
|
+
name_prefix: str,
|
|
82
|
+
executor_seq: int,
|
|
83
|
+
executor_kwargs: dict[str, Any],
|
|
84
|
+
) -> Executor:
|
|
85
|
+
"""Build an executor instance for the selected backend.
|
|
86
|
+
|
|
87
|
+
`thread_name_prefix` is valid for ThreadPoolExecutor and InterpreterPoolExecutor,
|
|
88
|
+
but not for ProcessPoolExecutor on Python 3.11.
|
|
89
|
+
"""
|
|
90
|
+
executor_cls = resolve_executor_cls(backend)
|
|
91
|
+
kwargs = dict(executor_kwargs)
|
|
92
|
+
|
|
93
|
+
normalized = normalize_backend(backend)
|
|
94
|
+
|
|
95
|
+
if normalized in {ExecutorBackend.THREAD, ExecutorBackend.INTERPRETER}:
|
|
96
|
+
kwargs.setdefault("thread_name_prefix", f"{name_prefix}-{executor_seq}")
|
|
97
|
+
|
|
98
|
+
return executor_cls(max_workers=max_workers, **kwargs)
|
leasepool/exceptions.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class LeasePoolError(Exception):
|
|
2
|
+
"""Base exception for leasepool."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LeasePoolNotStartedError(LeasePoolError):
|
|
6
|
+
"""Raised when the manager has not been started."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LeaseUnavailableError(LeasePoolError):
|
|
10
|
+
"""Raised when no executor can be acquired."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LeaseExpiredError(LeasePoolError):
|
|
14
|
+
"""Raised when a lease has expired or has been revoked."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UnsupportedBackendError(LeasePoolError):
|
|
18
|
+
"""Raised when the selected executor backend is unavailable."""
|