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.
Files changed (69) hide show
  1. pyarallel-0.6.0/.github/workflows/ci.yml +31 -0
  2. pyarallel-0.6.0/CHANGELOG.md +62 -0
  3. {pyarallel-0.3.0 → pyarallel-0.6.0}/Makefile +1 -2
  4. {pyarallel-0.3.0 → pyarallel-0.6.0}/PKG-INFO +23 -15
  5. {pyarallel-0.3.0 → pyarallel-0.6.0}/README.md +21 -13
  6. {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/api-reference/async.md +56 -15
  7. pyarallel-0.6.0/docs/api-reference/core.md +634 -0
  8. pyarallel-0.6.0/docs/api-reference/rate-limiting.md +87 -0
  9. pyarallel-0.6.0/docs/development/plans/v0.5.0.md +652 -0
  10. pyarallel-0.6.0/docs/development/plans/v0.6-engine-unification.md +528 -0
  11. pyarallel-0.6.0/docs/development/plans/v0.6-ledger-refactor.md +273 -0
  12. pyarallel-0.6.0/docs/development/roadmap.md +244 -0
  13. {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/getting-started/quickstart.md +46 -6
  14. {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/index.md +2 -2
  15. pyarallel-0.6.0/docs/user-guide/advanced-features.md +350 -0
  16. pyarallel-0.6.0/docs/user-guide/best-practices.md +232 -0
  17. {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/user-guide/real-world-patterns.md +37 -4
  18. pyarallel-0.6.0/examples/06_resilient_api_jobs.py +104 -0
  19. {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/README.md +15 -0
  20. {pyarallel-0.3.0 → pyarallel-0.6.0}/mkdocs.yml +3 -1
  21. {pyarallel-0.3.0 → pyarallel-0.6.0}/pyarallel/__init__.py +9 -7
  22. pyarallel-0.6.0/pyarallel/_procexec.py +54 -0
  23. pyarallel-0.6.0/pyarallel/_run.py +91 -0
  24. pyarallel-0.6.0/pyarallel/aio.py +607 -0
  25. pyarallel-0.6.0/pyarallel/checkpoint.py +355 -0
  26. pyarallel-0.6.0/pyarallel/core.py +1046 -0
  27. pyarallel-0.6.0/pyarallel/decorators.py +349 -0
  28. pyarallel-0.6.0/pyarallel/limiter.py +163 -0
  29. pyarallel-0.6.0/pyarallel/policies.py +133 -0
  30. pyarallel-0.6.0/pyarallel/result.py +223 -0
  31. {pyarallel-0.3.0 → pyarallel-0.6.0}/pyproject.toml +5 -2
  32. {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_batch.py +173 -1
  33. pyarallel-0.6.0/tests/test_checkpoint.py +618 -0
  34. pyarallel-0.6.0/tests/test_limiter.py +213 -0
  35. pyarallel-0.6.0/tests/test_max_errors.py +553 -0
  36. pyarallel-0.6.0/tests/test_metadata.py +164 -0
  37. pyarallel-0.6.0/tests/test_parity.py +494 -0
  38. {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_result.py +109 -5
  39. {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_retry.py +283 -2
  40. {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_starmap.py +56 -2
  41. pyarallel-0.6.0/tests/test_streaming.py +625 -0
  42. pyarallel-0.6.0/tests/typing_assertions.py +165 -0
  43. {pyarallel-0.3.0 → pyarallel-0.6.0}/uv.lock +9 -6
  44. pyarallel-0.3.0/docs/api-reference/core.md +0 -384
  45. pyarallel-0.3.0/docs/api-reference/rate-limiting.md +0 -60
  46. pyarallel-0.3.0/docs/development/roadmap.md +0 -56
  47. pyarallel-0.3.0/docs/user-guide/advanced-features.md +0 -276
  48. pyarallel-0.3.0/docs/user-guide/best-practices.md +0 -179
  49. pyarallel-0.3.0/pyarallel/aio.py +0 -419
  50. pyarallel-0.3.0/pyarallel/core.py +0 -909
  51. pyarallel-0.3.0/tests/test_streaming.py +0 -297
  52. {pyarallel-0.3.0 → pyarallel-0.6.0}/.gitignore +0 -0
  53. {pyarallel-0.3.0 → pyarallel-0.6.0}/.python-version +0 -0
  54. {pyarallel-0.3.0 → pyarallel-0.6.0}/CONTRIBUTING.md +0 -0
  55. {pyarallel-0.3.0 → pyarallel-0.6.0}/LICENSE.md +0 -0
  56. {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/development/CONTRIBUTING.md +0 -0
  57. {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/development/devx-principles.md +0 -0
  58. {pyarallel-0.3.0 → pyarallel-0.6.0}/docs/getting-started/installation.md +0 -0
  59. {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/01_basic_usage.py +0 -0
  60. {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/02_api_calls_with_rate_limiting.py +0 -0
  61. {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/03_cpu_bound_processing.py +0 -0
  62. {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/04_batch_processing.py +0 -0
  63. {pyarallel-0.3.0 → pyarallel-0.6.0}/examples/05_configuration.py +0 -0
  64. {pyarallel-0.3.0 → pyarallel-0.6.0}/pyarallel/py.typed +0 -0
  65. {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/conftest.py +0 -0
  66. {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/smoke.py +0 -0
  67. {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_async.py +0 -0
  68. {pyarallel-0.3.0 → pyarallel-0.6.0}/tests/test_decorator.py +0 -0
  69. {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.3.0
4
- Summary: A powerful parallel execution library for Python
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
  [![PyPI version](https://img.shields.io/pypi/v/pyarallel)](https://pypi.org/project/pyarallel/) [![PyPI Downloads](https://static.pepy.tech/badge/pyarallel/month)](https://pepy.tech/project/pyarallel)
43
43
 
44
- Apply one function to many inputs — with rate limiting, retry, batching, and structured errors. Sync and async.
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: API calls, file processing, data crunching. Not DAGs, not queues, not distributed systems. Just `concurrent.futures` and `asyncio` with the common policies and result handling already built in.
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 batching. And you still
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
- - **Batched execution** — lazy input consumption for generators, memory control: `batch_size=500`
129
- - **Streaming** — constant-memory processing via `parallel_iter` / `async_parallel_iter`
130
- - **Structured errors** — `ParallelResult` with `.ok`, `.successes()`, `.failures()`, `.raise_on_failure()`
131
- - **Timeouts** — wall-clock for the whole operation (`timeout=30.0`) or per-task in async (`task_timeout=5.0`)
132
- - **Progress callbacks** — `on_progress=lambda done, total: print(f"{done}/{total}")`
133
- - **Process executor** — CPU-bound work: `executor="process"`
134
- - **Decorator API** — `@parallel` / `@async_parallel` with `.map()`, `.starmap()`, `.stream()`
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
- | `Retry(attempts, backoff, on)` | `Retry(attempts=3, on=(ConnectionError,))` |
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
  [![PyPI version](https://img.shields.io/pypi/v/pyarallel)](https://pypi.org/project/pyarallel/) [![PyPI Downloads](https://static.pepy.tech/badge/pyarallel/month)](https://pepy.tech/project/pyarallel)
4
4
 
5
- Apply one function to many inputs — with rate limiting, retry, batching, and structured errors. Sync and async.
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: API calls, file processing, data crunching. Not DAGs, not queues, not distributed systems. Just `concurrent.futures` and `asyncio` with the common policies and result handling already built in.
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 batching. And you still
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
- - **Batched execution** — lazy input consumption for generators, memory control: `batch_size=500`
90
- - **Streaming** — constant-memory processing via `parallel_iter` / `async_parallel_iter`
91
- - **Structured errors** — `ParallelResult` with `.ok`, `.successes()`, `.failures()`, `.raise_on_failure()`
92
- - **Timeouts** — wall-clock for the whole operation (`timeout=30.0`) or per-task in async (`task_timeout=5.0`)
93
- - **Progress callbacks** — `on_progress=lambda done, total: print(f"{done}/{total}")`
94
- - **Process executor** — CPU-bound work: `executor="process"`
95
- - **Decorator API** — `@parallel` / `@async_parallel` with `.map()`, `.starmap()`, `.stream()`
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
- | `Retry(attempts, backoff, on)` | `Retry(attempts=3, on=(ConnectionError,))` |
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 using `asyncio.TaskGroup` for structured concurrency and `asyncio.Semaphore` for concurrency control.
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
- task_timeout=None, # Per-task timeout in seconds
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 with batching, `total` is items seen so far |
37
- | `batch_size` | `int \| None` | `None` | Process items in chunks. With unsized iterables, input is consumed lazily one batch at a time |
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
- - `task_timeout` is **per-task** (via `asyncio.wait_for`), not total wall-clock like sync `timeout`
51
- - Uses `asyncio.TaskGroup` for structured concurrency proper cleanup on errors
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) and `batch_size` is set,
80
- Pyarallel keeps input consumption lazy instead of materializing the full input
81
- up front. In that mode, `total` is the number of items discovered so far, not a
82
- guaranteed final total.
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. Constant memory.
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, batch_size=1000):
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