fluidattacks_core_aio 12.0.0__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.
- fluidattacks_core/aio/__init__.py +14 -0
- fluidattacks_core/aio/processes.py +64 -0
- fluidattacks_core/aio/py.typed +0 -0
- fluidattacks_core/aio/runners.py +11 -0
- fluidattacks_core/aio/tasks.py +213 -0
- fluidattacks_core/aio/threads.py +17 -0
- fluidattacks_core_aio-12.0.0.dist-info/METADATA +14 -0
- fluidattacks_core_aio-12.0.0.dist-info/RECORD +9 -0
- fluidattacks_core_aio-12.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from fluidattacks_core.aio.processes import to_process
|
|
2
|
+
from fluidattacks_core.aio.runners import run
|
|
3
|
+
from fluidattacks_core.aio.tasks import as_completed, gather, merge_async_generators, to_background
|
|
4
|
+
from fluidattacks_core.aio.threads import to_thread
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"as_completed",
|
|
8
|
+
"gather",
|
|
9
|
+
"merge_async_generators",
|
|
10
|
+
"run",
|
|
11
|
+
"to_background",
|
|
12
|
+
"to_process",
|
|
13
|
+
"to_thread",
|
|
14
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextvars
|
|
3
|
+
import functools
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
7
|
+
from typing import ParamSpec, TypeVar
|
|
8
|
+
|
|
9
|
+
P = ParamSpec("P")
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
# Single-slot lazy holder, mutated in place so no ``global`` is needed. The
|
|
13
|
+
# pool is created on first use (not at import), so importing this module has no
|
|
14
|
+
# side effects and environments without ``/dev/shm`` (e.g. AWS Lambda) don't
|
|
15
|
+
# crash at import.
|
|
16
|
+
_POOL: dict[None, ProcessPoolExecutor] = {}
|
|
17
|
+
_POOL_LOCK = threading.Lock()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_process_pool() -> ProcessPoolExecutor:
|
|
21
|
+
"""Return the shared process pool, creating it on first use.
|
|
22
|
+
|
|
23
|
+
Environments without ``/dev/shm`` (e.g. AWS Lambda) cannot create the
|
|
24
|
+
pool's POSIX semaphores, so we fail explicitly and point the caller to the
|
|
25
|
+
thread-based alternative instead of silently degrading. Creation is guarded
|
|
26
|
+
by a lock so concurrent first callers (e.g. across event loops in separate
|
|
27
|
+
threads) share a single pool.
|
|
28
|
+
"""
|
|
29
|
+
if None not in _POOL:
|
|
30
|
+
with _POOL_LOCK:
|
|
31
|
+
if None not in _POOL:
|
|
32
|
+
try:
|
|
33
|
+
_POOL[None] = ProcessPoolExecutor()
|
|
34
|
+
except OSError as exc:
|
|
35
|
+
msg = (
|
|
36
|
+
"ProcessPoolExecutor is unavailable in this "
|
|
37
|
+
"environment (no /dev/shm, e.g. AWS Lambda). "
|
|
38
|
+
"Use to_thread instead."
|
|
39
|
+
)
|
|
40
|
+
raise RuntimeError(msg) from exc
|
|
41
|
+
return _POOL[None]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Public lazy accessor. ``PROCESS_POOL`` used to be an eagerly-created
|
|
45
|
+
# ProcessPoolExecutor instance; it is now a ``() -> ProcessPoolExecutor``
|
|
46
|
+
# callable built on first call. Callers use ``PROCESS_POOL()``. Changing the
|
|
47
|
+
# type of this public entity is a breaking change, hence the major bump.
|
|
48
|
+
PROCESS_POOL = _get_process_pool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def to_process(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
|
|
52
|
+
"""Asynchronously run a function in a separate process.
|
|
53
|
+
|
|
54
|
+
The underlying ProcessPoolExecutor sets a maximum number of worker processes
|
|
55
|
+
based on the number of cores. It is created lazily on first call; in
|
|
56
|
+
environments without ``/dev/shm`` (e.g. AWS Lambda) this raises a
|
|
57
|
+
``RuntimeError`` pointing to ``to_thread``.
|
|
58
|
+
|
|
59
|
+
See: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor
|
|
60
|
+
"""
|
|
61
|
+
loop = asyncio.get_running_loop()
|
|
62
|
+
ctx = contextvars.copy_context()
|
|
63
|
+
func_call = functools.partial(ctx.run, func, *args, **kwargs)
|
|
64
|
+
return await loop.run_in_executor(_get_process_pool(), func_call)
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from collections.abc import Coroutine
|
|
2
|
+
from typing import Any, TypeVar
|
|
3
|
+
|
|
4
|
+
import uvloop
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(coroutine: Coroutine[Any, Any, T]) -> T:
|
|
10
|
+
"""Run a coroutine and return the result, using uvloop."""
|
|
11
|
+
return uvloop.run(coroutine)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Coroutine, Iterable, Iterator
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from typing import Any, Literal, TypeVar, cast, overload
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
# Sentinel object to signal generator completion
|
|
10
|
+
_GENERATOR_DONE_SENTINEL = object()
|
|
11
|
+
|
|
12
|
+
# Logger for the aio module
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def as_completed(
|
|
17
|
+
coroutines: Iterable[Awaitable[T]],
|
|
18
|
+
*,
|
|
19
|
+
concurrency_limit: int | None = None,
|
|
20
|
+
) -> AsyncIterator[Awaitable[T]]:
|
|
21
|
+
"""Run coroutines concurrently, yielding results in order of completion.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
coroutines: An iterable of coroutines.
|
|
25
|
+
concurrency_limit: Maximum number of concurrent coroutines.
|
|
26
|
+
|
|
27
|
+
Yields:
|
|
28
|
+
Results from the coroutines in the order they complete.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
if concurrency_limit:
|
|
32
|
+
semaphore = asyncio.Semaphore(concurrency_limit)
|
|
33
|
+
|
|
34
|
+
async def _run(coroutine: Awaitable[T]) -> T:
|
|
35
|
+
async with semaphore:
|
|
36
|
+
return await coroutine
|
|
37
|
+
else:
|
|
38
|
+
|
|
39
|
+
async def _run(coroutine: Awaitable[T]) -> T:
|
|
40
|
+
return await coroutine
|
|
41
|
+
|
|
42
|
+
tasks = [_run(coroutine) for coroutine in coroutines]
|
|
43
|
+
for task in asyncio.as_completed(tasks):
|
|
44
|
+
yield task
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@overload
|
|
48
|
+
async def gather(
|
|
49
|
+
coroutines: Iterable[Awaitable[T]],
|
|
50
|
+
*,
|
|
51
|
+
concurrency_limit: int | None = None,
|
|
52
|
+
return_exceptions: Literal[False] = False,
|
|
53
|
+
) -> list[T]: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@overload
|
|
57
|
+
async def gather(
|
|
58
|
+
coroutines: Iterable[Awaitable[T]],
|
|
59
|
+
*,
|
|
60
|
+
concurrency_limit: int | None = None,
|
|
61
|
+
return_exceptions: Literal[True],
|
|
62
|
+
) -> list[T | BaseException]: ...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def gather(
|
|
66
|
+
coroutines: Iterable[Awaitable[T]],
|
|
67
|
+
*,
|
|
68
|
+
concurrency_limit: int | None = None,
|
|
69
|
+
return_exceptions: bool = False,
|
|
70
|
+
) -> list[T] | list[T | BaseException]:
|
|
71
|
+
"""Run coroutines concurrently.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
coroutines: An iterable of coroutines.
|
|
75
|
+
concurrency_limit: Maximum number of concurrent coroutines.
|
|
76
|
+
return_exceptions: Whether to return exceptions instead of raising them.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A list of results or exceptions.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
Exception: If return_exceptions is False and any coroutine raises an exception.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
if concurrency_limit:
|
|
86
|
+
semaphore = asyncio.Semaphore(concurrency_limit)
|
|
87
|
+
|
|
88
|
+
async def _run(coroutine: Awaitable[T]) -> T:
|
|
89
|
+
async with semaphore:
|
|
90
|
+
return await coroutine
|
|
91
|
+
else:
|
|
92
|
+
|
|
93
|
+
async def _run(coroutine: Awaitable[T]) -> T:
|
|
94
|
+
return await coroutine
|
|
95
|
+
|
|
96
|
+
tasks = [_run(coroutine) for coroutine in coroutines]
|
|
97
|
+
return await asyncio.gather(*tasks, return_exceptions=return_exceptions)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
BACKGROUND_TASKS = set[asyncio.Task[Any]]()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def to_background(coroutine: Coroutine[Any, Any, T]) -> None:
|
|
104
|
+
"""Run a coroutine in the background, fire-and-forget style."""
|
|
105
|
+
task = asyncio.create_task(coroutine)
|
|
106
|
+
BACKGROUND_TASKS.add(task)
|
|
107
|
+
task.add_done_callback(BACKGROUND_TASKS.discard)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _consume_generator(
|
|
111
|
+
gen: AsyncGenerator[T, None],
|
|
112
|
+
queue: asyncio.Queue[T | object],
|
|
113
|
+
active_generators: list[int],
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Consume a single generator and put its items in the queue.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
gen: The async generator to consume.
|
|
119
|
+
queue: The queue to put items into.
|
|
120
|
+
active_generators: A list containing the count of active generators.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
active_generators[0] += 1
|
|
124
|
+
try:
|
|
125
|
+
async for item in gen:
|
|
126
|
+
await queue.put(item)
|
|
127
|
+
except Exception:
|
|
128
|
+
_LOGGER.exception(
|
|
129
|
+
"Error consuming generator %s, unhandled exception",
|
|
130
|
+
gen,
|
|
131
|
+
)
|
|
132
|
+
finally:
|
|
133
|
+
active_generators[0] -= 1
|
|
134
|
+
await queue.put(_GENERATOR_DONE_SENTINEL)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _start_initial_generator_tasks(
|
|
138
|
+
gen_iter: Iterator[AsyncGenerator[T, None]],
|
|
139
|
+
limit: int,
|
|
140
|
+
queue: asyncio.Queue[T | object],
|
|
141
|
+
active_generators: list[int],
|
|
142
|
+
) -> list[asyncio.Task[None]]:
|
|
143
|
+
tasks: list[asyncio.Task[None]] = []
|
|
144
|
+
# Start initial tasks up to the limit
|
|
145
|
+
for _ in range(limit):
|
|
146
|
+
try:
|
|
147
|
+
gen = next(gen_iter)
|
|
148
|
+
tasks.append(asyncio.create_task(_consume_generator(gen, queue, active_generators)))
|
|
149
|
+
except StopIteration:
|
|
150
|
+
break
|
|
151
|
+
return tasks
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def merge_async_generators(
|
|
155
|
+
generators: Iterable[AsyncGenerator[T, None]],
|
|
156
|
+
limit: int,
|
|
157
|
+
) -> AsyncGenerator[T, None]:
|
|
158
|
+
"""Merge multiple async generators into a single async generator with concurrency control.
|
|
159
|
+
|
|
160
|
+
This function runs multiple async generators concurrently, yielding items as they become
|
|
161
|
+
available. It maintains a maximum number of active generators specified by the limit.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
generators: An iterable of async generators to merge.
|
|
165
|
+
limit: Maximum number of generators to run concurrently.
|
|
166
|
+
|
|
167
|
+
Yields:
|
|
168
|
+
Items from the generators as they become available.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If limit is less than 1.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
>>> async def gen1():
|
|
175
|
+
... yield 1
|
|
176
|
+
... yield 2
|
|
177
|
+
>>> async def gen2():
|
|
178
|
+
... yield 3
|
|
179
|
+
... yield 4
|
|
180
|
+
>>> async for item in merge_async_generators([gen1(), gen2()], limit=2):
|
|
181
|
+
... print(item) # May print 1, 3, 2, 4 in any order
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
if limit < 1:
|
|
185
|
+
msg = "limit must be at least 1"
|
|
186
|
+
raise ValueError(msg)
|
|
187
|
+
|
|
188
|
+
queue: asyncio.Queue[T | object] = asyncio.Queue()
|
|
189
|
+
active_generators = [0] # Use list to allow modification in nested function
|
|
190
|
+
gen_iter = iter(generators)
|
|
191
|
+
tasks = _start_initial_generator_tasks(gen_iter, limit, queue, active_generators)
|
|
192
|
+
# Keep track of how many generators we expect to finish
|
|
193
|
+
expected_done_signals = len(tasks)
|
|
194
|
+
done_signals_received = 0
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
while done_signals_received < expected_done_signals:
|
|
198
|
+
item = await queue.get()
|
|
199
|
+
if item is _GENERATOR_DONE_SENTINEL:
|
|
200
|
+
done_signals_received += 1
|
|
201
|
+
# Try to start a new task if there are more generators
|
|
202
|
+
with suppress(StopIteration):
|
|
203
|
+
next_gen = next(gen_iter)
|
|
204
|
+
tasks.append(
|
|
205
|
+
asyncio.create_task(_consume_generator(next_gen, queue, active_generators)),
|
|
206
|
+
)
|
|
207
|
+
expected_done_signals += 1
|
|
208
|
+
else:
|
|
209
|
+
yield cast("T", item)
|
|
210
|
+
finally:
|
|
211
|
+
# Ensure all tasks are complete
|
|
212
|
+
if tasks:
|
|
213
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import ParamSpec, TypeVar
|
|
4
|
+
|
|
5
|
+
P = ParamSpec("P")
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def to_thread(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
|
|
10
|
+
"""Asynchronously run a function in a separate thread.
|
|
11
|
+
|
|
12
|
+
The underlying ThreadPoolExecutor sets a maximum number of worker threads
|
|
13
|
+
based on the number of cores.
|
|
14
|
+
|
|
15
|
+
See: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor
|
|
16
|
+
"""
|
|
17
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fluidattacks_core_aio
|
|
3
|
+
Version: 12.0.0
|
|
4
|
+
Summary: Fluid Attacks Core Aio Library
|
|
5
|
+
Author-email: Development <development@fluidattacks.com>
|
|
6
|
+
License: MPL-2.0
|
|
7
|
+
Classifier: Development Status :: 1 - Planning
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: uvloop>=0.21.0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
fluidattacks_core/aio/__init__.py,sha256=Xn4sNbAYtqLPV-em8vWVQD5eRH3IigDpAVzHnky0onY,399
|
|
2
|
+
fluidattacks_core/aio/processes.py,sha256=E-Jw2h_8eOQIWN8AeyVAt_2w7Lv1E-F7isz_Ox6g0HY,2620
|
|
3
|
+
fluidattacks_core/aio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
fluidattacks_core/aio/runners.py,sha256=rJYtrshcWawhWWWagRn6Rsre_BsmAECLWG0FYOheLJQ,250
|
|
5
|
+
fluidattacks_core/aio/tasks.py,sha256=bZ-d0BP4ptCK9seQiXY4nFnFGGK3ZhIDQTM52k-yMDc,6487
|
|
6
|
+
fluidattacks_core/aio/threads.py,sha256=JrnAv7_jAlDDj2AI5MviWvxU8EHQVpW-c7uCtst-2yY,556
|
|
7
|
+
fluidattacks_core_aio-12.0.0.dist-info/METADATA,sha256=ebk5Djl8u5M68jQqJqGObaXayOdnErWJ4wAoMm1Bh-4,555
|
|
8
|
+
fluidattacks_core_aio-12.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
+
fluidattacks_core_aio-12.0.0.dist-info/RECORD,,
|