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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any