pyarallel 0.1.3__tar.gz → 0.3.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 (70) hide show
  1. {pyarallel-0.1.3 → pyarallel-0.3.0}/.gitignore +9 -2
  2. {pyarallel-0.1.3 → pyarallel-0.3.0}/CONTRIBUTING.md +4 -3
  3. pyarallel-0.3.0/Makefile +46 -0
  4. pyarallel-0.3.0/PKG-INFO +268 -0
  5. pyarallel-0.3.0/README.md +229 -0
  6. pyarallel-0.3.0/docs/api-reference/async.md +183 -0
  7. pyarallel-0.3.0/docs/api-reference/core.md +384 -0
  8. pyarallel-0.3.0/docs/api-reference/rate-limiting.md +60 -0
  9. {pyarallel-0.1.3 → pyarallel-0.3.0}/docs/development/CONTRIBUTING.md +4 -3
  10. pyarallel-0.3.0/docs/development/devx-principles.md +49 -0
  11. pyarallel-0.3.0/docs/development/roadmap.md +56 -0
  12. pyarallel-0.3.0/docs/getting-started/installation.md +28 -0
  13. pyarallel-0.3.0/docs/getting-started/quickstart.md +146 -0
  14. pyarallel-0.3.0/docs/index.md +57 -0
  15. pyarallel-0.3.0/docs/user-guide/advanced-features.md +276 -0
  16. pyarallel-0.3.0/docs/user-guide/best-practices.md +179 -0
  17. pyarallel-0.3.0/docs/user-guide/real-world-patterns.md +255 -0
  18. pyarallel-0.3.0/examples/01_basic_usage.py +98 -0
  19. pyarallel-0.3.0/examples/02_api_calls_with_rate_limiting.py +128 -0
  20. pyarallel-0.3.0/examples/03_cpu_bound_processing.py +150 -0
  21. pyarallel-0.3.0/examples/04_batch_processing.py +134 -0
  22. pyarallel-0.3.0/examples/05_configuration.py +104 -0
  23. pyarallel-0.3.0/examples/README.md +100 -0
  24. {pyarallel-0.1.3 → pyarallel-0.3.0}/mkdocs.yml +6 -5
  25. pyarallel-0.3.0/pyarallel/__init__.py +34 -0
  26. pyarallel-0.3.0/pyarallel/aio.py +419 -0
  27. pyarallel-0.3.0/pyarallel/core.py +909 -0
  28. pyarallel-0.3.0/pyarallel/py.typed +0 -0
  29. {pyarallel-0.1.3 → pyarallel-0.3.0}/pyproject.toml +16 -20
  30. pyarallel-0.3.0/tests/conftest.py +1 -0
  31. pyarallel-0.3.0/tests/smoke.py +261 -0
  32. pyarallel-0.3.0/tests/test_async.py +195 -0
  33. pyarallel-0.3.0/tests/test_batch.py +238 -0
  34. pyarallel-0.3.0/tests/test_decorator.py +168 -0
  35. pyarallel-0.3.0/tests/test_parallel_map.py +183 -0
  36. pyarallel-0.3.0/tests/test_result.py +196 -0
  37. pyarallel-0.3.0/tests/test_retry.py +376 -0
  38. pyarallel-0.3.0/tests/test_starmap.py +178 -0
  39. pyarallel-0.3.0/tests/test_streaming.py +297 -0
  40. pyarallel-0.3.0/uv.lock +1212 -0
  41. pyarallel-0.1.3/Makefile +0 -36
  42. pyarallel-0.1.3/PKG-INFO +0 -490
  43. pyarallel-0.1.3/README.md +0 -451
  44. pyarallel-0.1.3/TASKS.md +0 -153
  45. pyarallel-0.1.3/docs/api-reference/configuration-api.md +0 -49
  46. pyarallel-0.1.3/docs/api-reference/decorators.md +0 -64
  47. pyarallel-0.1.3/docs/api-reference/rate-limiting.md +0 -60
  48. pyarallel-0.1.3/docs/development/roadmap.md +0 -78
  49. pyarallel-0.1.3/docs/getting-started/installation.md +0 -61
  50. pyarallel-0.1.3/docs/getting-started/quickstart.md +0 -94
  51. pyarallel-0.1.3/docs/index.md +0 -101
  52. pyarallel-0.1.3/docs/user-guide/advanced-features.md +0 -174
  53. pyarallel-0.1.3/docs/user-guide/best-practices.md +0 -207
  54. pyarallel-0.1.3/docs/user-guide/configuration.md +0 -150
  55. pyarallel-0.1.3/pyarallel/__init__.py +0 -10
  56. pyarallel-0.1.3/pyarallel/config.py +0 -154
  57. pyarallel-0.1.3/pyarallel/config_manager.py +0 -368
  58. pyarallel-0.1.3/pyarallel/core.py +0 -414
  59. pyarallel-0.1.3/pyarallel/env_config.py +0 -58
  60. pyarallel-0.1.3/sandbox/sandbox_test.py +0 -91
  61. pyarallel-0.1.3/setup.py +0 -46
  62. pyarallel-0.1.3/tests/conftest.py +0 -120
  63. pyarallel-0.1.3/tests/test_config_manager.py +0 -68
  64. pyarallel-0.1.3/tests/test_decorator_config.py +0 -127
  65. pyarallel-0.1.3/tests/test_env_config.py +0 -68
  66. pyarallel-0.1.3/tests/test_pyarallel.py +0 -184
  67. pyarallel-0.1.3/tests/test_runtime_config.py +0 -119
  68. pyarallel-0.1.3/uv.lock +0 -871
  69. {pyarallel-0.1.3 → pyarallel-0.3.0}/.python-version +0 -0
  70. {pyarallel-0.1.3 → pyarallel-0.3.0}/LICENSE.md +0 -0
@@ -44,5 +44,12 @@ htmlcov/
44
44
  *.log
45
45
 
46
46
 
47
- # docs
48
- site
47
+ # docs
48
+ site
49
+
50
+ # Claude Code / local dev
51
+ CLAUDE.md
52
+ AGENTS.md
53
+ .omx/
54
+ interim-feedback.md
55
+ pain_points.md
@@ -40,15 +40,16 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue]
40
40
 
41
41
  ## Use a Consistent Coding Style
42
42
 
43
- * Use [Black](https://github.com/psf/black) for Python code formatting
44
- * Keep line length to 88 characters (Black default)
43
+ * Use `uv run ruff format .` for formatting
44
+ * Use `uv run ruff check .` for linting and import ordering
45
+ * Keep line length to 88 characters
45
46
  * Use type hints for function arguments and return values
46
47
  * Write docstrings for all public functions and classes
47
48
 
48
49
  ## Running Tests
49
50
 
50
51
  ```bash
51
- pytest tests/
52
+ uv run python -m pytest tests/ -q
52
53
  ```
53
54
 
54
55
  ## License
@@ -0,0 +1,46 @@
1
+ .PHONY: help test docs-serve docs-deploy format lint clean build publish
2
+
3
+ help:
4
+ @echo "Available commands:"
5
+ @echo " make test Run pytest suite"
6
+ @echo " make docs-serve Start mkdocs development server"
7
+ @echo " make docs-deploy Deploy documentation to GitHub Pages"
8
+ @echo " make format Format code with ruff"
9
+ @echo " make lint Run ruff check + mypy"
10
+ @echo " make clean Remove build artifacts"
11
+ @echo " make build Build package"
12
+ @echo " make publish Publish package to PyPI"
13
+
14
+ test:
15
+ uv run pytest tests/ -v
16
+
17
+ docs-serve:
18
+ uv run mkdocs serve
19
+
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
+ uv run mkdocs gh-deploy
23
+
24
+ format:
25
+ uv run ruff format .
26
+ uv run ruff check --fix .
27
+
28
+ lint:
29
+ uv run ruff check .
30
+ uv run mypy pyarallel/
31
+
32
+ clean:
33
+ rm -rf build/
34
+ rm -rf dist/
35
+ rm -rf *.egg-info/
36
+ find . -type d -name __pycache__ -exec rm -rf {} +
37
+ find . -type f -name '*.pyc' -delete
38
+ find . -type f -name '*.pyo' -delete
39
+ find . -type f -name '*.pyd' -delete
40
+
41
+ build:
42
+ $(MAKE) clean
43
+ uv run python -m build
44
+
45
+ publish: build
46
+ uv run twine upload dist/*
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyarallel
3
+ Version: 0.3.0
4
+ Summary: A powerful parallel execution library for Python
5
+ Project-URL: Homepage, https://github.com/oneryalcin/pyarallel
6
+ Project-URL: Repository, https://github.com/oneryalcin/pyarallel.git
7
+ Project-URL: Documentation, https://oneryalcin.github.io/pyarallel
8
+ Author-email: Mehmet Oner Yalcin <oneryalcin@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2025 Pyarallel Contributors
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE.md
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
+ Requires-Python: >=3.12
38
+ Description-Content-Type: text/markdown
39
+
40
+ # Pyarallel
41
+
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
+
44
+ Apply one function to many inputs — with rate limiting, retry, batching, and structured errors. Sync and async.
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.
47
+
48
+ **Zero dependencies. Python 3.12+.**
49
+
50
+ ## Before / After
51
+
52
+ Fetch 10,000 URLs with rate limiting and error handling.
53
+
54
+ **concurrent.futures:**
55
+
56
+ ```python
57
+ import requests
58
+ from concurrent.futures import ThreadPoolExecutor, as_completed
59
+
60
+ def fetch(url):
61
+ return requests.get(url, timeout=10).json()
62
+
63
+ urls = ["https://api.example.com/users/1", "https://api.example.com/users/2", ...]
64
+
65
+ results = [None] * len(urls)
66
+ errors = []
67
+
68
+ with ThreadPoolExecutor(max_workers=10) as pool:
69
+ futures = {pool.submit(fetch, url): i for i, url in enumerate(urls)}
70
+ for f in as_completed(futures):
71
+ i = futures[f]
72
+ try:
73
+ results[i] = f.result()
74
+ except Exception as e:
75
+ errors.append((i, e))
76
+
77
+ # No rate limiting. No retry. No batching. And you still
78
+ # need to wire those yourself every time.
79
+ ```
80
+
81
+ **pyarallel:**
82
+
83
+ ```python
84
+ from pyarallel import parallel_map, RateLimit, Retry
85
+
86
+ result = parallel_map(
87
+ fetch, urls,
88
+ workers=10,
89
+ rate_limit=RateLimit(100, "minute"),
90
+ retry=Retry(attempts=3, on=(ConnectionError, TimeoutError)),
91
+ )
92
+
93
+ for idx, val in result.successes():
94
+ save(val)
95
+ for idx, exc in result.failures():
96
+ log_error(idx, exc)
97
+ ```
98
+
99
+ Same thing, async:
100
+
101
+ ```python
102
+ import httpx
103
+ from pyarallel import async_parallel_map, RateLimit, Retry
104
+
105
+ async def fetch_async(url):
106
+ async with httpx.AsyncClient() as client:
107
+ return (await client.get(url, timeout=10)).json()
108
+
109
+ result = await async_parallel_map(
110
+ fetch_async, urls,
111
+ concurrency=10,
112
+ rate_limit=RateLimit(100, "minute"),
113
+ retry=Retry(attempts=3, on=(ConnectionError, TimeoutError)),
114
+ )
115
+ # Same result model — result.ok, result.successes(), result.failures()
116
+ ```
117
+
118
+ ## Install
119
+
120
+ ```bash
121
+ pip install pyarallel
122
+ ```
123
+
124
+ ## What You Get
125
+
126
+ - **Rate limiting** — token bucket, per-second/minute/hour: `rate_limit=RateLimit(100, "minute")`
127
+ - **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()`
135
+
136
+ ## Quick Start
137
+
138
+ ### Sync
139
+
140
+ ```python
141
+ import requests
142
+ from pyarallel import parallel_map, RateLimit, Retry
143
+
144
+ def fetch(url):
145
+ return requests.get(url, timeout=10).json()
146
+
147
+ # Fan out over a list, get ordered results
148
+ result = parallel_map(fetch, urls, workers=10)
149
+
150
+ # Rate-limited API calls with retry
151
+ def call_api(user_id):
152
+ return requests.get(f"https://api.example.com/users/{user_id}").json()
153
+
154
+ result = parallel_map(
155
+ call_api, user_ids,
156
+ workers=10,
157
+ rate_limit=RateLimit(100, "minute"),
158
+ retry=Retry(attempts=3, backoff=1.0, on=(ConnectionError, TimeoutError)),
159
+ )
160
+
161
+ # CPU-bound with processes
162
+ from PIL import Image
163
+
164
+ def resize_image(path):
165
+ img = Image.open(path)
166
+ img.thumbnail((800, 600))
167
+ img.save(path.replace(".png", "_thumb.png"))
168
+
169
+ result = parallel_map(resize_image, paths, executor="process")
170
+ ```
171
+
172
+ ### Async
173
+
174
+ ```python
175
+ import httpx
176
+ from pyarallel import async_parallel_map
177
+
178
+ async def fetch_async(url):
179
+ async with httpx.AsyncClient() as client:
180
+ return (await client.get(url, timeout=10)).json()
181
+
182
+ result = await async_parallel_map(
183
+ fetch_async, urls, concurrency=20, task_timeout=5.0,
184
+ )
185
+ ```
186
+
187
+ ### Decorator
188
+
189
+ Adds `.map()`, `.starmap()`, `.stream()` without changing the function:
190
+
191
+ ```python
192
+ from pyarallel import parallel, async_parallel, RateLimit
193
+
194
+ @parallel(workers=8, rate_limit=RateLimit(100, "minute"))
195
+ def fetch(url):
196
+ return requests.get(url).json()
197
+
198
+ fetch("http://example.com") # normal call — returns dict
199
+ fetch.map(urls) # parallel — returns ParallelResult
200
+ fetch.stream(urls, batch_size=500) # streaming — yields ItemResult
201
+
202
+ @async_parallel(concurrency=10)
203
+ async def fetch_async(url):
204
+ async with httpx.AsyncClient() as c:
205
+ return (await c.get(url)).json()
206
+
207
+ await fetch_async.map(urls) # async parallel
208
+ ```
209
+
210
+ ### Streaming — Constant Memory
211
+
212
+ For ETL, pipelines, or datasets too large to hold in memory:
213
+
214
+ ```python
215
+ from pyarallel import parallel_iter
216
+
217
+ def transform(row):
218
+ return {"id": row["id"], "name": row["name"].strip().title()}
219
+
220
+ for item in parallel_iter(transform, ten_million_rows, batch_size=1000):
221
+ if item.ok:
222
+ db.save(item.value)
223
+ else:
224
+ log_error(item.index, item.error)
225
+ ```
226
+
227
+ ### Error Handling
228
+
229
+ All errors collected, never silently swallowed:
230
+
231
+ ```python
232
+ def send_email(msg):
233
+ return smtp.send(msg["to"], msg["subject"], msg["body"])
234
+
235
+ result = parallel_map(send_email, messages)
236
+
237
+ if result.ok:
238
+ values = result.values() # list of all results, in order
239
+ else:
240
+ for idx, exc in result.failures():
241
+ log_error(idx, exc)
242
+ result.raise_on_failure() # or raise ExceptionGroup with all errors
243
+ ```
244
+
245
+ ## API Summary
246
+
247
+ | Function | Decorator | Returns | Use case |
248
+ |---|---|---|---|
249
+ | `parallel_map(fn, items)` | `.map(items)` | `ParallelResult` | Results fit in memory |
250
+ | `parallel_starmap(fn, items)` | `.starmap(items)` | `ParallelResult` | Multi-arg, fits in memory |
251
+ | `parallel_iter(fn, items)` | `.stream(items)` | `Iterator[ItemResult]` | Streaming, constant memory |
252
+
253
+ Async mirrors: `async_parallel_map`, `async_parallel_starmap`, `async_parallel_iter`
254
+
255
+ | Config | Example |
256
+ |---|---|
257
+ | `RateLimit(count, per)` | `RateLimit(100, "minute")` |
258
+ | `Retry(attempts, backoff, on)` | `Retry(attempts=3, on=(ConnectionError,))` |
259
+
260
+ Works with instance methods and static methods via `@parallel` decorator — see [full docs](https://oneryalcin.github.io/pyarallel/).
261
+
262
+ ## Documentation
263
+
264
+ [Full docs](https://oneryalcin.github.io/pyarallel/) — API reference, advanced features, best practices.
265
+
266
+ ## License
267
+
268
+ MIT — see [LICENSE.md](LICENSE.md).
@@ -0,0 +1,229 @@
1
+ # Pyarallel
2
+
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
+
5
+ Apply one function to many inputs — with rate limiting, retry, batching, and structured errors. Sync and async.
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.
8
+
9
+ **Zero dependencies. Python 3.12+.**
10
+
11
+ ## Before / After
12
+
13
+ Fetch 10,000 URLs with rate limiting and error handling.
14
+
15
+ **concurrent.futures:**
16
+
17
+ ```python
18
+ import requests
19
+ from concurrent.futures import ThreadPoolExecutor, as_completed
20
+
21
+ def fetch(url):
22
+ return requests.get(url, timeout=10).json()
23
+
24
+ urls = ["https://api.example.com/users/1", "https://api.example.com/users/2", ...]
25
+
26
+ results = [None] * len(urls)
27
+ errors = []
28
+
29
+ with ThreadPoolExecutor(max_workers=10) as pool:
30
+ futures = {pool.submit(fetch, url): i for i, url in enumerate(urls)}
31
+ for f in as_completed(futures):
32
+ i = futures[f]
33
+ try:
34
+ results[i] = f.result()
35
+ except Exception as e:
36
+ errors.append((i, e))
37
+
38
+ # No rate limiting. No retry. No batching. And you still
39
+ # need to wire those yourself every time.
40
+ ```
41
+
42
+ **pyarallel:**
43
+
44
+ ```python
45
+ from pyarallel import parallel_map, RateLimit, Retry
46
+
47
+ result = parallel_map(
48
+ fetch, urls,
49
+ workers=10,
50
+ rate_limit=RateLimit(100, "minute"),
51
+ retry=Retry(attempts=3, on=(ConnectionError, TimeoutError)),
52
+ )
53
+
54
+ for idx, val in result.successes():
55
+ save(val)
56
+ for idx, exc in result.failures():
57
+ log_error(idx, exc)
58
+ ```
59
+
60
+ Same thing, async:
61
+
62
+ ```python
63
+ import httpx
64
+ from pyarallel import async_parallel_map, RateLimit, Retry
65
+
66
+ async def fetch_async(url):
67
+ async with httpx.AsyncClient() as client:
68
+ return (await client.get(url, timeout=10)).json()
69
+
70
+ result = await async_parallel_map(
71
+ fetch_async, urls,
72
+ concurrency=10,
73
+ rate_limit=RateLimit(100, "minute"),
74
+ retry=Retry(attempts=3, on=(ConnectionError, TimeoutError)),
75
+ )
76
+ # Same result model — result.ok, result.successes(), result.failures()
77
+ ```
78
+
79
+ ## Install
80
+
81
+ ```bash
82
+ pip install pyarallel
83
+ ```
84
+
85
+ ## What You Get
86
+
87
+ - **Rate limiting** — token bucket, per-second/minute/hour: `rate_limit=RateLimit(100, "minute")`
88
+ - **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()`
96
+
97
+ ## Quick Start
98
+
99
+ ### Sync
100
+
101
+ ```python
102
+ import requests
103
+ from pyarallel import parallel_map, RateLimit, Retry
104
+
105
+ def fetch(url):
106
+ return requests.get(url, timeout=10).json()
107
+
108
+ # Fan out over a list, get ordered results
109
+ result = parallel_map(fetch, urls, workers=10)
110
+
111
+ # Rate-limited API calls with retry
112
+ def call_api(user_id):
113
+ return requests.get(f"https://api.example.com/users/{user_id}").json()
114
+
115
+ result = parallel_map(
116
+ call_api, user_ids,
117
+ workers=10,
118
+ rate_limit=RateLimit(100, "minute"),
119
+ retry=Retry(attempts=3, backoff=1.0, on=(ConnectionError, TimeoutError)),
120
+ )
121
+
122
+ # CPU-bound with processes
123
+ from PIL import Image
124
+
125
+ def resize_image(path):
126
+ img = Image.open(path)
127
+ img.thumbnail((800, 600))
128
+ img.save(path.replace(".png", "_thumb.png"))
129
+
130
+ result = parallel_map(resize_image, paths, executor="process")
131
+ ```
132
+
133
+ ### Async
134
+
135
+ ```python
136
+ import httpx
137
+ from pyarallel import async_parallel_map
138
+
139
+ async def fetch_async(url):
140
+ async with httpx.AsyncClient() as client:
141
+ return (await client.get(url, timeout=10)).json()
142
+
143
+ result = await async_parallel_map(
144
+ fetch_async, urls, concurrency=20, task_timeout=5.0,
145
+ )
146
+ ```
147
+
148
+ ### Decorator
149
+
150
+ Adds `.map()`, `.starmap()`, `.stream()` without changing the function:
151
+
152
+ ```python
153
+ from pyarallel import parallel, async_parallel, RateLimit
154
+
155
+ @parallel(workers=8, rate_limit=RateLimit(100, "minute"))
156
+ def fetch(url):
157
+ return requests.get(url).json()
158
+
159
+ fetch("http://example.com") # normal call — returns dict
160
+ fetch.map(urls) # parallel — returns ParallelResult
161
+ fetch.stream(urls, batch_size=500) # streaming — yields ItemResult
162
+
163
+ @async_parallel(concurrency=10)
164
+ async def fetch_async(url):
165
+ async with httpx.AsyncClient() as c:
166
+ return (await c.get(url)).json()
167
+
168
+ await fetch_async.map(urls) # async parallel
169
+ ```
170
+
171
+ ### Streaming — Constant Memory
172
+
173
+ For ETL, pipelines, or datasets too large to hold in memory:
174
+
175
+ ```python
176
+ from pyarallel import parallel_iter
177
+
178
+ def transform(row):
179
+ return {"id": row["id"], "name": row["name"].strip().title()}
180
+
181
+ for item in parallel_iter(transform, ten_million_rows, batch_size=1000):
182
+ if item.ok:
183
+ db.save(item.value)
184
+ else:
185
+ log_error(item.index, item.error)
186
+ ```
187
+
188
+ ### Error Handling
189
+
190
+ All errors collected, never silently swallowed:
191
+
192
+ ```python
193
+ def send_email(msg):
194
+ return smtp.send(msg["to"], msg["subject"], msg["body"])
195
+
196
+ result = parallel_map(send_email, messages)
197
+
198
+ if result.ok:
199
+ values = result.values() # list of all results, in order
200
+ else:
201
+ for idx, exc in result.failures():
202
+ log_error(idx, exc)
203
+ result.raise_on_failure() # or raise ExceptionGroup with all errors
204
+ ```
205
+
206
+ ## API Summary
207
+
208
+ | Function | Decorator | Returns | Use case |
209
+ |---|---|---|---|
210
+ | `parallel_map(fn, items)` | `.map(items)` | `ParallelResult` | Results fit in memory |
211
+ | `parallel_starmap(fn, items)` | `.starmap(items)` | `ParallelResult` | Multi-arg, fits in memory |
212
+ | `parallel_iter(fn, items)` | `.stream(items)` | `Iterator[ItemResult]` | Streaming, constant memory |
213
+
214
+ Async mirrors: `async_parallel_map`, `async_parallel_starmap`, `async_parallel_iter`
215
+
216
+ | Config | Example |
217
+ |---|---|
218
+ | `RateLimit(count, per)` | `RateLimit(100, "minute")` |
219
+ | `Retry(attempts, backoff, on)` | `Retry(attempts=3, on=(ConnectionError,))` |
220
+
221
+ Works with instance methods and static methods via `@parallel` decorator — see [full docs](https://oneryalcin.github.io/pyarallel/).
222
+
223
+ ## Documentation
224
+
225
+ [Full docs](https://oneryalcin.github.io/pyarallel/) — API reference, advanced features, best practices.
226
+
227
+ ## License
228
+
229
+ MIT — see [LICENSE.md](LICENSE.md).