pyarallel 0.3.0__tar.gz → 0.6.0__tar.gz
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.
- pyarallel-0.6.0/.github/workflows/ci.yml +31 -0
- pyarallel-0.6.0/CHANGELOG.md +62 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/Makefile +1 -2
- {pyarallel-0.3.0 → pyarallel-0.6.0}/PKG-INFO +23 -15
- {pyarallel-0.3.0 → pyarallel-0.6.0}/README.md +21 -13
- {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/api-reference/async.md +56 -15
- pyarallel-0.6.0/docs/api-reference/core.md +634 -0
- pyarallel-0.6.0/docs/api-reference/rate-limiting.md +87 -0
- pyarallel-0.6.0/docs/development/plans/v0.5.0.md +652 -0
- pyarallel-0.6.0/docs/development/plans/v0.6-engine-unification.md +528 -0
- pyarallel-0.6.0/docs/development/plans/v0.6-ledger-refactor.md +273 -0
- pyarallel-0.6.0/docs/development/roadmap.md +244 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/getting-started/quickstart.md +46 -6
- {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/index.md +2 -2
- pyarallel-0.6.0/docs/user-guide/advanced-features.md +350 -0
- pyarallel-0.6.0/docs/user-guide/best-practices.md +232 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/user-guide/real-world-patterns.md +37 -4
- pyarallel-0.6.0/examples/06_resilient_api_jobs.py +104 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/README.md +15 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/mkdocs.yml +3 -1
- {pyarallel-0.3.0 → pyarallel-0.6.0}/pyarallel/__init__.py +9 -7
- pyarallel-0.6.0/pyarallel/_procexec.py +54 -0
- pyarallel-0.6.0/pyarallel/_run.py +91 -0
- pyarallel-0.6.0/pyarallel/aio.py +607 -0
- pyarallel-0.6.0/pyarallel/checkpoint.py +355 -0
- pyarallel-0.6.0/pyarallel/core.py +1046 -0
- pyarallel-0.6.0/pyarallel/decorators.py +349 -0
- pyarallel-0.6.0/pyarallel/limiter.py +163 -0
- pyarallel-0.6.0/pyarallel/policies.py +133 -0
- pyarallel-0.6.0/pyarallel/result.py +223 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/pyproject.toml +5 -2
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_batch.py +173 -1
- pyarallel-0.6.0/tests/test_checkpoint.py +618 -0
- pyarallel-0.6.0/tests/test_limiter.py +213 -0
- pyarallel-0.6.0/tests/test_max_errors.py +553 -0
- pyarallel-0.6.0/tests/test_metadata.py +164 -0
- pyarallel-0.6.0/tests/test_parity.py +494 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_result.py +109 -5
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_retry.py +283 -2
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_starmap.py +56 -2
- pyarallel-0.6.0/tests/test_streaming.py +625 -0
- pyarallel-0.6.0/tests/typing_assertions.py +165 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/uv.lock +9 -6
- pyarallel-0.3.0/docs/api-reference/core.md +0 -384
- pyarallel-0.3.0/docs/api-reference/rate-limiting.md +0 -60
- pyarallel-0.3.0/docs/development/roadmap.md +0 -56
- pyarallel-0.3.0/docs/user-guide/advanced-features.md +0 -276
- pyarallel-0.3.0/docs/user-guide/best-practices.md +0 -179
- pyarallel-0.3.0/pyarallel/aio.py +0 -419
- pyarallel-0.3.0/pyarallel/core.py +0 -909
- pyarallel-0.3.0/tests/test_streaming.py +0 -297
- {pyarallel-0.3.0 → pyarallel-0.6.0}/.gitignore +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/.python-version +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/CONTRIBUTING.md +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/LICENSE.md +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/development/CONTRIBUTING.md +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/development/devx-principles.md +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/getting-started/installation.md +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/01_basic_usage.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/02_api_calls_with_rate_limiting.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/03_cpu_bound_processing.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/04_batch_processing.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/05_configuration.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/pyarallel/py.typed +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/conftest.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/smoke.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_async.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_decorator.py +0 -0
- {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_parallel_map.py +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
checks:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install
|
|
21
|
+
run: uv sync --group dev
|
|
22
|
+
- name: Lint
|
|
23
|
+
run: |
|
|
24
|
+
uv run ruff check pyarallel tests
|
|
25
|
+
uv run ruff format --check pyarallel tests
|
|
26
|
+
- name: Type check (strict, package + typing assertions)
|
|
27
|
+
run: uv run mypy pyarallel tests/typing_assertions.py
|
|
28
|
+
- name: Tests
|
|
29
|
+
run: uv run pytest -q
|
|
30
|
+
- name: Docs (strict)
|
|
31
|
+
run: uv run mkdocs build --strict
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.6.0 — 2026-07-06
|
|
4
|
+
|
|
5
|
+
**One engine for every execution path.** All collected maps — sync and
|
|
6
|
+
async — now run through the same windowed engine that streaming and
|
|
7
|
+
`max_errors` already used. Behavior changes (pre-1.0, no deprecation
|
|
8
|
+
cycle):
|
|
9
|
+
|
|
10
|
+
- `batch_size` means one thing everywhere: the in-flight admission
|
|
11
|
+
window (default `2 × workers` sync, `2 × concurrency` async). Chunk
|
|
12
|
+
barriers no longer exist anywhere — a slow item never stalls the
|
|
13
|
+
items behind it.
|
|
14
|
+
- Collected maps consume input lazily, one window ahead — generators
|
|
15
|
+
are never materialized. The plain no-kwargs call moves from eager
|
|
16
|
+
upfront submission to windowed admission (benchmarked: parity on
|
|
17
|
+
ms-scale tasks; see the v0.6 plan for numbers).
|
|
18
|
+
- No-drain on stop, everywhere: after a timeout or abort the source
|
|
19
|
+
iterator is never touched again. Unsized inputs return a *shorter*
|
|
20
|
+
result instead of drained-and-appended failure placeholders.
|
|
21
|
+
- New: `ParallelResult.timed_out` / `.aborted` report how the run ended
|
|
22
|
+
(at most one is set — first stop reason wins; both show up in the
|
|
23
|
+
repr). This is the reliable truncation signal for unsized inputs,
|
|
24
|
+
where a timed-out run can contain only successes.
|
|
25
|
+
- `on_progress` with unsized inputs reports items *admitted* so far as
|
|
26
|
+
`total` (previously the final total, because input was materialized).
|
|
27
|
+
Pass a sized input for a real total.
|
|
28
|
+
- Source-iterator errors surface mid-run instead of at materialization,
|
|
29
|
+
and propagate promptly: queued work is cancelled, but sync tasks
|
|
30
|
+
already running cannot be interrupted and may complete — side effects
|
|
31
|
+
included — in the background. Materialize the input first
|
|
32
|
+
(`list(items)`) if you need no-work-after-error guarantees, or use
|
|
33
|
+
the async API (tasks are cancelled and awaited).
|
|
34
|
+
- Exception shape: errors raised from callbacks on the async plain path
|
|
35
|
+
propagate plain — the `ExceptionGroup` wrapper went away with the
|
|
36
|
+
removed `asyncio.TaskGroup`.
|
|
37
|
+
- `parallel_starmap` no longer materializes its input — generators of
|
|
38
|
+
argument tuples stay lazy.
|
|
39
|
+
- Total `timeout=` now binds during cached checkpoint admission too: a
|
|
40
|
+
checkpoint-heavy run can no longer overrun the deadline unnoticed.
|
|
41
|
+
- Fixed: a `TimeoutError` raised by the source iterable itself is an
|
|
42
|
+
input error and propagates — it is no longer repackaged as deadline
|
|
43
|
+
expiry.
|
|
44
|
+
|
|
45
|
+
Full contract and review history:
|
|
46
|
+
[v0.6 Engine Unification Plan](docs/development/plans/v0.6-engine-unification.md).
|
|
47
|
+
|
|
48
|
+
## 0.5.0 — 2026-07-05
|
|
49
|
+
|
|
50
|
+
Structural quality: sliding-window streaming (`ordered=True`, streaming
|
|
51
|
+
`on_progress`), `max_errors` early abort, `ItemResult.attempts`/
|
|
52
|
+
`.duration`, `ParallelResult.ok_values()`, `checkpoint_key=` (schema v2),
|
|
53
|
+
`sequential=True` debug mode, async total `timeout=`, contextvars
|
|
54
|
+
propagation, `worker_init=`/`max_tasks_per_worker=`, typed decorator
|
|
55
|
+
options via `Unpack[TypedDict]`. Details:
|
|
56
|
+
[v0.5.0 Plan](docs/development/plans/v0.5.0.md). Not published to PyPI.
|
|
57
|
+
|
|
58
|
+
## 0.4.0 — 2026-06
|
|
59
|
+
|
|
60
|
+
Repositioned as the fan-out layer for rate-limited APIs: server-driven
|
|
61
|
+
backoff (`retry_if`/`wait_from`), shareable `Limiter`, real token bucket
|
|
62
|
+
(`burst=`), `checkpoint=` resume, strict typing. Tag only, not published.
|
|
@@ -18,7 +18,6 @@ docs-serve:
|
|
|
18
18
|
uv run mkdocs serve
|
|
19
19
|
|
|
20
20
|
docs-deploy:
|
|
21
|
-
uv run python -c "import pyarallel; from pathlib import Path; yml = Path('mkdocs.yml').read_text(); Path('mkdocs.yml').write_text(yml.replace('version: .*', f'version: {pyarallel.__version__}'))"
|
|
22
21
|
uv run mkdocs gh-deploy
|
|
23
22
|
|
|
24
23
|
format:
|
|
@@ -27,7 +26,7 @@ format:
|
|
|
27
26
|
|
|
28
27
|
lint:
|
|
29
28
|
uv run ruff check .
|
|
30
|
-
uv run mypy pyarallel/
|
|
29
|
+
uv run mypy pyarallel/ tests/typing_assertions.py
|
|
31
30
|
|
|
32
31
|
clean:
|
|
33
32
|
rm -rf build/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyarallel
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: The fan-out layer for rate-limited APIs — parallel map with rate limiting, retry, and structured errors. Sync and async, zero dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/oneryalcin/pyarallel
|
|
6
6
|
Project-URL: Repository, https://github.com/oneryalcin/pyarallel.git
|
|
7
7
|
Project-URL: Documentation, https://oneryalcin.github.io/pyarallel
|
|
@@ -41,9 +41,9 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
|
|
42
42
|
[](https://pypi.org/project/pyarallel/) [](https://pepy.tech/project/pyarallel)
|
|
43
43
|
|
|
44
|
-
Apply one function to many inputs — with rate limiting, retry,
|
|
44
|
+
The fan-out layer for rate-limited APIs. Apply one function to many inputs — with rate limiting, retry, resume, and structured errors. Sync and async.
|
|
45
45
|
|
|
46
|
-
Pyarallel is for "fan out one function over N items" workloads:
|
|
46
|
+
Pyarallel is for "fan out one function over N items" workloads against services that throttle you: LLM calls, embeddings, scraping, SaaS APIs — plus general file processing and data crunching. Everyone hand-rolls the same stack for this: a semaphore, tenacity, a rate limiter, ad-hoc 429 handling. Pyarallel is that stack as one coherent tool. Not DAGs, not queues, not distributed systems. Just `concurrent.futures` and `asyncio` with the policies and result handling already built in.
|
|
47
47
|
|
|
48
48
|
**Zero dependencies. Python 3.12+.**
|
|
49
49
|
|
|
@@ -74,7 +74,7 @@ with ThreadPoolExecutor(max_workers=10) as pool:
|
|
|
74
74
|
except Exception as e:
|
|
75
75
|
errors.append((i, e))
|
|
76
76
|
|
|
77
|
-
# No rate limiting. No retry. No
|
|
77
|
+
# No rate limiting. No retry. No resume. And you still
|
|
78
78
|
# need to wire those yourself every time.
|
|
79
79
|
```
|
|
80
80
|
|
|
@@ -123,15 +123,21 @@ pip install pyarallel
|
|
|
123
123
|
|
|
124
124
|
## What You Get
|
|
125
125
|
|
|
126
|
-
- **Rate limiting** — token bucket, per-second/minute/hour: `rate_limit=RateLimit(100, "minute")`
|
|
126
|
+
- **Rate limiting** — token bucket with burst, per-second/minute/hour: `rate_limit=RateLimit(100, "minute", burst=20)`
|
|
127
|
+
- **Shared quota** — one `Limiter` instance across calls and functions when the budget belongs to an API key: `rate_limit=Limiter(RateLimit(100, "minute"))`
|
|
127
128
|
- **Retry with backoff** — per-item, exponential, jitter, exception filtering: `retry=Retry(attempts=3, on=(ConnectionError,))`
|
|
128
|
-
- **
|
|
129
|
-
- **
|
|
130
|
-
- **
|
|
131
|
-
- **
|
|
132
|
-
- **
|
|
133
|
-
- **
|
|
134
|
-
- **
|
|
129
|
+
- **Server-driven backoff** — honor 429 + `Retry-After`: `retry=Retry(retry_if=..., wait_from=...)`; the wait also pauses the shared limiter so one throttled task slows the whole pool
|
|
130
|
+
- **Checkpoint/resume** — `checkpoint="run.ckpt"`: a crash at item 40,000 resumes instead of restarting from zero; `checkpoint_key=lambda u: u.id` keys rows by identity so evolving inputs keep completed work
|
|
131
|
+
- **Early abort** — `max_errors=10`: a dead API costs tens of calls, not thousands; unrun items are marked `Aborted`, partial results returned
|
|
132
|
+
- **One windowed engine** — every API (collected and streaming, sync and async) admits work through a bounded in-flight window: lazy input, generators never materialized, no batch barriers, a straggler never stalls the items behind it
|
|
133
|
+
- **Streaming** — `parallel_iter` / `async_parallel_iter`: `ordered=True` for input-order yields, per-item `attempts`/`duration`
|
|
134
|
+
- **Structured errors** — `ParallelResult` with `.ok`, `.ok_values()`, `.successes()`, `.failures()`, `.raise_on_failure()`, plus `.timed_out`/`.aborted` for how the run ended
|
|
135
|
+
- **Timeouts** — total wall-clock on sync *and* async (`timeout=30.0`), per-task in async (`task_timeout=5.0`)
|
|
136
|
+
- **Debug mode** — `sequential=True` runs inline: no pool, real stack traces, working breakpoints
|
|
137
|
+
- **Progress callbacks** — `on_progress=lambda done, total: print(f"{done}/{total}")` on collected and streaming APIs
|
|
138
|
+
- **Process executor** — CPU-bound work: `executor="process"`, with `worker_init=` and `max_tasks_per_worker=`
|
|
139
|
+
- **Contextvars propagation** — correlation IDs survive into thread workers
|
|
140
|
+
- **Decorator API** — `@parallel` / `@async_parallel` with `.map()`, `.starmap()`, `.stream()` — fully typed via `Unpack[TypedDict]`
|
|
135
141
|
|
|
136
142
|
## Quick Start
|
|
137
143
|
|
|
@@ -254,8 +260,10 @@ Async mirrors: `async_parallel_map`, `async_parallel_starmap`, `async_parallel_i
|
|
|
254
260
|
|
|
255
261
|
| Config | Example |
|
|
256
262
|
|---|---|
|
|
257
|
-
| `RateLimit(count, per)` | `RateLimit(100, "minute")` |
|
|
258
|
-
| `
|
|
263
|
+
| `RateLimit(count, per, burst)` | `RateLimit(100, "minute", burst=20)` |
|
|
264
|
+
| `Limiter(rate_limit)` | shared budget: `Limiter(RateLimit(100, "minute"))` |
|
|
265
|
+
| `Retry(attempts, backoff, on, retry_if, wait_from)` | `Retry(attempts=3, on=(ConnectionError,))` |
|
|
266
|
+
| `checkpoint=` | resumable runs: `checkpoint="run.ckpt"` |
|
|
259
267
|
|
|
260
268
|
Works with instance methods and static methods via `@parallel` decorator — see [full docs](https://oneryalcin.github.io/pyarallel/).
|
|
261
269
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/pyarallel/) [](https://pepy.tech/project/pyarallel)
|
|
4
4
|
|
|
5
|
-
Apply one function to many inputs — with rate limiting, retry,
|
|
5
|
+
The fan-out layer for rate-limited APIs. Apply one function to many inputs — with rate limiting, retry, resume, and structured errors. Sync and async.
|
|
6
6
|
|
|
7
|
-
Pyarallel is for "fan out one function over N items" workloads:
|
|
7
|
+
Pyarallel is for "fan out one function over N items" workloads against services that throttle you: LLM calls, embeddings, scraping, SaaS APIs — plus general file processing and data crunching. Everyone hand-rolls the same stack for this: a semaphore, tenacity, a rate limiter, ad-hoc 429 handling. Pyarallel is that stack as one coherent tool. Not DAGs, not queues, not distributed systems. Just `concurrent.futures` and `asyncio` with the policies and result handling already built in.
|
|
8
8
|
|
|
9
9
|
**Zero dependencies. Python 3.12+.**
|
|
10
10
|
|
|
@@ -35,7 +35,7 @@ with ThreadPoolExecutor(max_workers=10) as pool:
|
|
|
35
35
|
except Exception as e:
|
|
36
36
|
errors.append((i, e))
|
|
37
37
|
|
|
38
|
-
# No rate limiting. No retry. No
|
|
38
|
+
# No rate limiting. No retry. No resume. And you still
|
|
39
39
|
# need to wire those yourself every time.
|
|
40
40
|
```
|
|
41
41
|
|
|
@@ -84,15 +84,21 @@ pip install pyarallel
|
|
|
84
84
|
|
|
85
85
|
## What You Get
|
|
86
86
|
|
|
87
|
-
- **Rate limiting** — token bucket, per-second/minute/hour: `rate_limit=RateLimit(100, "minute")`
|
|
87
|
+
- **Rate limiting** — token bucket with burst, per-second/minute/hour: `rate_limit=RateLimit(100, "minute", burst=20)`
|
|
88
|
+
- **Shared quota** — one `Limiter` instance across calls and functions when the budget belongs to an API key: `rate_limit=Limiter(RateLimit(100, "minute"))`
|
|
88
89
|
- **Retry with backoff** — per-item, exponential, jitter, exception filtering: `retry=Retry(attempts=3, on=(ConnectionError,))`
|
|
89
|
-
- **
|
|
90
|
-
- **
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
93
|
-
- **
|
|
94
|
-
- **
|
|
95
|
-
- **
|
|
90
|
+
- **Server-driven backoff** — honor 429 + `Retry-After`: `retry=Retry(retry_if=..., wait_from=...)`; the wait also pauses the shared limiter so one throttled task slows the whole pool
|
|
91
|
+
- **Checkpoint/resume** — `checkpoint="run.ckpt"`: a crash at item 40,000 resumes instead of restarting from zero; `checkpoint_key=lambda u: u.id` keys rows by identity so evolving inputs keep completed work
|
|
92
|
+
- **Early abort** — `max_errors=10`: a dead API costs tens of calls, not thousands; unrun items are marked `Aborted`, partial results returned
|
|
93
|
+
- **One windowed engine** — every API (collected and streaming, sync and async) admits work through a bounded in-flight window: lazy input, generators never materialized, no batch barriers, a straggler never stalls the items behind it
|
|
94
|
+
- **Streaming** — `parallel_iter` / `async_parallel_iter`: `ordered=True` for input-order yields, per-item `attempts`/`duration`
|
|
95
|
+
- **Structured errors** — `ParallelResult` with `.ok`, `.ok_values()`, `.successes()`, `.failures()`, `.raise_on_failure()`, plus `.timed_out`/`.aborted` for how the run ended
|
|
96
|
+
- **Timeouts** — total wall-clock on sync *and* async (`timeout=30.0`), per-task in async (`task_timeout=5.0`)
|
|
97
|
+
- **Debug mode** — `sequential=True` runs inline: no pool, real stack traces, working breakpoints
|
|
98
|
+
- **Progress callbacks** — `on_progress=lambda done, total: print(f"{done}/{total}")` on collected and streaming APIs
|
|
99
|
+
- **Process executor** — CPU-bound work: `executor="process"`, with `worker_init=` and `max_tasks_per_worker=`
|
|
100
|
+
- **Contextvars propagation** — correlation IDs survive into thread workers
|
|
101
|
+
- **Decorator API** — `@parallel` / `@async_parallel` with `.map()`, `.starmap()`, `.stream()` — fully typed via `Unpack[TypedDict]`
|
|
96
102
|
|
|
97
103
|
## Quick Start
|
|
98
104
|
|
|
@@ -215,8 +221,10 @@ Async mirrors: `async_parallel_map`, `async_parallel_starmap`, `async_parallel_i
|
|
|
215
221
|
|
|
216
222
|
| Config | Example |
|
|
217
223
|
|---|---|
|
|
218
|
-
| `RateLimit(count, per)` | `RateLimit(100, "minute")` |
|
|
219
|
-
| `
|
|
224
|
+
| `RateLimit(count, per, burst)` | `RateLimit(100, "minute", burst=20)` |
|
|
225
|
+
| `Limiter(rate_limit)` | shared budget: `Limiter(RateLimit(100, "minute"))` |
|
|
226
|
+
| `Retry(attempts, backoff, on, retry_if, wait_from)` | `Retry(attempts=3, on=(ConnectionError,))` |
|
|
227
|
+
| `checkpoint=` | resumable runs: `checkpoint="run.ckpt"` |
|
|
220
228
|
|
|
221
229
|
Works with instance methods and static methods via `@parallel` decorator — see [full docs](https://oneryalcin.github.io/pyarallel/).
|
|
222
230
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# API Reference: Async
|
|
2
2
|
|
|
3
|
-
Pyarallel provides async-native parallel execution
|
|
3
|
+
Pyarallel provides async-native parallel execution: a windowed engine driven by `asyncio.wait` with `asyncio.Semaphore` for concurrency control — in-flight tasks are cancelled and awaited on errors and timeouts, so nothing is left running on the loop.
|
|
4
4
|
|
|
5
5
|
## `async_parallel_map`
|
|
6
6
|
|
|
@@ -14,11 +14,15 @@ results = await async_parallel_map(
|
|
|
14
14
|
items, # Any iterable
|
|
15
15
|
*,
|
|
16
16
|
concurrency=4, # Max concurrent tasks
|
|
17
|
-
rate_limit=None, # RateLimit or ops/second
|
|
18
|
-
|
|
17
|
+
rate_limit=None, # RateLimit spec, shared Limiter, or ops/second
|
|
18
|
+
timeout=None, # Total wall-clock timeout (mirror of sync)
|
|
19
|
+
task_timeout=None, # Per-task timeout in seconds
|
|
19
20
|
on_progress=None, # callback(completed, total)
|
|
20
21
|
batch_size=None, # Lazy batch consumption for unsized iterables
|
|
21
22
|
retry=None, # Retry(attempts=3, backoff=1.0)
|
|
23
|
+
checkpoint=None, # Path to a resume file (SQLite)
|
|
24
|
+
checkpoint_key=None, # Stable per-item identity for resume
|
|
25
|
+
max_errors=None, # Abort after N failures
|
|
22
26
|
)
|
|
23
27
|
```
|
|
24
28
|
|
|
@@ -31,11 +35,15 @@ results = await async_parallel_map(
|
|
|
31
35
|
| `fn` | async `Callable` | required | Async function to apply to each item |
|
|
32
36
|
| `items` | `Iterable` | required | Any iterable |
|
|
33
37
|
| `concurrency` | `int` | `4` | Maximum concurrent tasks |
|
|
34
|
-
| `rate_limit` | `RateLimit \| float \| None` | `None` | Rate limiting |
|
|
38
|
+
| `rate_limit` | `Limiter \| RateLimit \| float \| None` | `None` | Rate limiting. Pass a shared `Limiter` to draw from one budget across calls |
|
|
39
|
+
| `timeout` | `float \| None` | `None` | **Total** wall-clock timeout — mirror of the sync `timeout`. Unfinished tasks are cancelled, `result.timed_out` is set; sized slots are marked `TimeoutError`, unsized inputs return a shorter result (the source is never drained) |
|
|
35
40
|
| `task_timeout` | `float \| None` | `None` | **Per-task** timeout in seconds |
|
|
36
|
-
| `on_progress` | `Callable[[int, int], None] \| None` | `None` | Progress callback. For unsized iterables
|
|
37
|
-
| `batch_size` | `int \| None` | `None` |
|
|
41
|
+
| `on_progress` | `Callable[[int, int], None] \| None` | `None` | Progress callback. For unsized iterables, `total` is items seen so far |
|
|
42
|
+
| `batch_size` | `int \| None` | `None` | Admission window: max tasks created but unresolved (default `2 × concurrency`). A lookahead/memory bound, not a chunk size — no barriers, input consumed lazily |
|
|
38
43
|
| `retry` | `Retry \| None` | `None` | Per-item retry with backoff |
|
|
44
|
+
| `checkpoint` | `str \| Path \| None` | `None` | Checkpoint file for resumable runs — completed items load from disk on rerun |
|
|
45
|
+
| `checkpoint_key` | `Callable[[T], str \| int \| bytes] \| None` | `None` | Stable per-item identity — see the [sync docs](core.md#checkpoint-resume) |
|
|
46
|
+
| `max_errors` | `int \| None` | `None` | Abort after this many failures (counted after retries). Windowed admission makes the abort cheap; unrun items are marked `Aborted`, `result.aborted` is set — see the [sync docs](core.md#early-abort-with-max_errors) |
|
|
39
47
|
|
|
40
48
|
### Why `concurrency` instead of `workers`?
|
|
41
49
|
|
|
@@ -47,8 +55,12 @@ Pre-v1, Pyarallel keeps these names intentionally different. If you're moving fr
|
|
|
47
55
|
|
|
48
56
|
### Other Differences from Sync
|
|
49
57
|
|
|
50
|
-
-
|
|
51
|
-
-
|
|
58
|
+
- Both timeouts exist here: `timeout` is total wall-clock (same contract
|
|
59
|
+
as sync), `task_timeout` is per-task via `asyncio.wait_for` — the sync
|
|
60
|
+
API deliberately has no per-task timeout (threads can't be cancelled)
|
|
61
|
+
- On errors and timeouts, in-flight tasks are cancelled and awaited —
|
|
62
|
+
proper cleanup, nothing left running on the loop
|
|
63
|
+
- No `sequential=` — `concurrency=1` already serializes
|
|
52
64
|
|
|
53
65
|
### Examples
|
|
54
66
|
|
|
@@ -76,10 +88,10 @@ results = await async_parallel_map(
|
|
|
76
88
|
When `items` has a known length, `on_progress(done, total)` reports the final
|
|
77
89
|
total.
|
|
78
90
|
|
|
79
|
-
When `items` is unsized (for example a generator)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
When `items` is unsized (for example a generator), input consumption is
|
|
92
|
+
lazy — one window ahead, never materialized — and `total` is the number
|
|
93
|
+
of items *admitted* so far, growing as the run progresses: a percentage
|
|
94
|
+
over it is meaningless. Pass a sized input for a real total.
|
|
83
95
|
|
|
84
96
|
---
|
|
85
97
|
|
|
@@ -102,7 +114,10 @@ Takes the same options as `async_parallel_map`. Also available as `.starmap()` o
|
|
|
102
114
|
|
|
103
115
|
## `async_parallel_iter`
|
|
104
116
|
|
|
105
|
-
Async streaming — yields `ItemResult` in completion order.
|
|
117
|
+
Async streaming — yields `ItemResult` in completion order. A bounded
|
|
118
|
+
window of tasks is in flight at any moment (default `2 × concurrency`,
|
|
119
|
+
override with `batch_size`): memory stays constant, input is consumed
|
|
120
|
+
lazily, and a slow item delays only itself.
|
|
106
121
|
|
|
107
122
|
```python
|
|
108
123
|
from pyarallel import async_parallel_iter
|
|
@@ -114,13 +129,39 @@ async for item in async_parallel_iter(fetch, urls, concurrency=10):
|
|
|
114
129
|
log_error(item.index, item.error)
|
|
115
130
|
```
|
|
116
131
|
|
|
132
|
+
!!! note "Changed in v0.5 (streaming) and v0.6 (everywhere)"
|
|
133
|
+
`batch_size` is an **in-flight bound**, not a chunk size — one
|
|
134
|
+
meaning across every API since v0.6: there are no barriers, and
|
|
135
|
+
input is never materialized.
|
|
136
|
+
|
|
137
|
+
Takes the same `ordered=` and `on_progress=` options as `parallel_iter`:
|
|
138
|
+
`ordered=True` yields in input order with a reorder buffer counted
|
|
139
|
+
inside the window; `on_progress(done, total)` fires per completed item.
|
|
140
|
+
|
|
141
|
+
To stop early, close the generator — unlike sync generators, a bare
|
|
142
|
+
`break` does **not** finalize an async generator promptly (Python defers
|
|
143
|
+
it to event-loop shutdown, so tasks keep running). Wrap the stream in
|
|
144
|
+
`contextlib.aclosing`:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from contextlib import aclosing
|
|
148
|
+
|
|
149
|
+
async with aclosing(async_parallel_iter(fetch, urls)) as stream:
|
|
150
|
+
async for item in stream:
|
|
151
|
+
if enough(item):
|
|
152
|
+
break # aclosing cancels in-flight tasks right here
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Async tasks, unlike threads, are genuinely cancellable — once the
|
|
156
|
+
generator is closed, in-flight work stops.
|
|
157
|
+
|
|
117
158
|
Also available as `.stream()` on `@async_parallel` decorated functions:
|
|
118
159
|
|
|
119
160
|
```python
|
|
120
161
|
@async_parallel(concurrency=10)
|
|
121
162
|
async def fetch(url): ...
|
|
122
163
|
|
|
123
|
-
async for item in fetch.stream(urls
|
|
164
|
+
async for item in fetch.stream(urls):
|
|
124
165
|
if item.ok:
|
|
125
166
|
await db.save(item.value)
|
|
126
167
|
else:
|
|
@@ -143,7 +184,7 @@ async def fn(item): ...
|
|
|
143
184
|
| Parameter | Type | Default | Description |
|
|
144
185
|
|---|---|---|---|
|
|
145
186
|
| `concurrency` | `int` | `4` | Default concurrency for `.map()` |
|
|
146
|
-
| `rate_limit` | `RateLimit \| float \| None` | `None` | Default rate limiting |
|
|
187
|
+
| `rate_limit` | `Limiter \| RateLimit \| float \| None` | `None` | Default rate limiting |
|
|
147
188
|
|
|
148
189
|
### Usage
|
|
149
190
|
|