pyarallel 0.1.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.
- pyarallel/__init__.py +10 -0
- pyarallel/core.py +348 -0
- pyarallel-0.1.0.dist-info/METADATA +283 -0
- pyarallel-0.1.0.dist-info/RECORD +6 -0
- pyarallel-0.1.0.dist-info/WHEEL +4 -0
- pyarallel-0.1.0.dist-info/licenses/LICENSE.md +21 -0
pyarallel/__init__.py
ADDED
pyarallel/core.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyarallel: A Powerful Parallel Execution Library for Python
|
|
3
|
+
|
|
4
|
+
This module provides a decorator-based approach to parallel execution, supporting both
|
|
5
|
+
thread and process-based parallelism with advanced features like rate limiting and batch processing.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Simple decorator-based API
|
|
9
|
+
- Support for both I/O-bound (threading) and CPU-bound (multiprocessing) tasks
|
|
10
|
+
- Configurable rate limiting with support for per-second, per-minute, and per-hour rates
|
|
11
|
+
- Batch processing for memory efficiency
|
|
12
|
+
- Worker prewarming for latency-critical applications
|
|
13
|
+
- Automatic executor reuse and cleanup
|
|
14
|
+
- Thread-safe implementation
|
|
15
|
+
|
|
16
|
+
Example Usage:
|
|
17
|
+
```python
|
|
18
|
+
from pyarallel import parallel
|
|
19
|
+
|
|
20
|
+
# Basic I/O-bound task
|
|
21
|
+
@parallel(max_workers=4)
|
|
22
|
+
def fetch_url(url: str) -> dict:
|
|
23
|
+
return requests.get(url).json()
|
|
24
|
+
|
|
25
|
+
# CPU-bound task with rate limiting
|
|
26
|
+
@parallel(
|
|
27
|
+
max_workers=4,
|
|
28
|
+
executor_type="process",
|
|
29
|
+
rate_limit=(100, "minute")
|
|
30
|
+
)
|
|
31
|
+
def process_image(image: bytes) -> bytes:
|
|
32
|
+
return heavy_processing(image)
|
|
33
|
+
|
|
34
|
+
# Batch processing with prewarming
|
|
35
|
+
@parallel(
|
|
36
|
+
max_workers=4,
|
|
37
|
+
batch_size=10,
|
|
38
|
+
prewarm=True
|
|
39
|
+
)
|
|
40
|
+
def analyze_text(text: str) -> dict:
|
|
41
|
+
return text_analysis(text)
|
|
42
|
+
|
|
43
|
+
# Use with lists for parallel execution
|
|
44
|
+
urls = ["http://example1.com", "http://example2.com"]
|
|
45
|
+
results = fetch_url(urls) # Processes URLs in parallel
|
|
46
|
+
|
|
47
|
+
# Single items work too
|
|
48
|
+
result = fetch_url("http://example.com") # Returns [result]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For detailed documentation and examples, see:
|
|
52
|
+
https://github.com/oneryalcin/pyarallel
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
|
|
56
|
+
from functools import wraps
|
|
57
|
+
from typing import Any, Callable, TypeVar, Literal
|
|
58
|
+
from itertools import islice
|
|
59
|
+
import time
|
|
60
|
+
import threading
|
|
61
|
+
from dataclasses import dataclass
|
|
62
|
+
import multiprocessing
|
|
63
|
+
import weakref
|
|
64
|
+
|
|
65
|
+
T = TypeVar('T')
|
|
66
|
+
|
|
67
|
+
TimeUnit = Literal["second", "minute", "hour"]
|
|
68
|
+
ExecutorType = Literal["thread", "process"]
|
|
69
|
+
|
|
70
|
+
# Global executor cache using weak references
|
|
71
|
+
_EXECUTOR_CACHE = weakref.WeakValueDictionary()
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class RateLimit:
|
|
75
|
+
"""
|
|
76
|
+
Configuration for rate limiting parallel operations.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
count: Number of operations allowed per interval
|
|
80
|
+
interval: Time interval for rate limiting ("second", "minute", "hour")
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
```python
|
|
84
|
+
# 100 operations per minute
|
|
85
|
+
rate = RateLimit(100, "minute")
|
|
86
|
+
|
|
87
|
+
@parallel(rate_limit=rate)
|
|
88
|
+
def my_func(): ...
|
|
89
|
+
```
|
|
90
|
+
"""
|
|
91
|
+
count: float
|
|
92
|
+
interval: TimeUnit = "second"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def per_second(self) -> float:
|
|
96
|
+
"""Convert rate to operations per second"""
|
|
97
|
+
multiplier = {
|
|
98
|
+
"second": 1,
|
|
99
|
+
"minute": 60,
|
|
100
|
+
"hour": 3600
|
|
101
|
+
}
|
|
102
|
+
return self.count / multiplier[self.interval]
|
|
103
|
+
|
|
104
|
+
class TokenBucket:
|
|
105
|
+
"""
|
|
106
|
+
Thread-safe token bucket algorithm implementation for rate limiting.
|
|
107
|
+
|
|
108
|
+
The token bucket algorithm provides smooth rate limiting with the ability
|
|
109
|
+
to handle bursts up to the bucket capacity. Tokens are added to the bucket
|
|
110
|
+
at a fixed rate, and each operation consumes one token.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
rate_limit: RateLimit configuration
|
|
114
|
+
capacity: Maximum number of tokens the bucket can hold. Defaults to
|
|
115
|
+
the number of operations allowed per interval.
|
|
116
|
+
"""
|
|
117
|
+
def __init__(self, rate_limit: RateLimit, capacity: int = None):
|
|
118
|
+
self.rate = rate_limit.per_second
|
|
119
|
+
self.capacity = capacity or rate_limit.count
|
|
120
|
+
self.tokens = self.capacity
|
|
121
|
+
self.last_update = time.time()
|
|
122
|
+
self.lock = threading.Lock()
|
|
123
|
+
self.next_allowed = self.last_update # Track next allowed operation time
|
|
124
|
+
|
|
125
|
+
def get_token(self) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
Try to get a token from the bucket.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
bool: True if a token was acquired, False otherwise
|
|
131
|
+
"""
|
|
132
|
+
with self.lock:
|
|
133
|
+
now = time.time()
|
|
134
|
+
if now < self.next_allowed:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
self.next_allowed = max(self.next_allowed + (1 / self.rate), now + (1 / self.rate))
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
def wait_for_token(self):
|
|
141
|
+
"""Block until a token is available"""
|
|
142
|
+
while True:
|
|
143
|
+
with self.lock:
|
|
144
|
+
now = time.time()
|
|
145
|
+
if now >= self.next_allowed:
|
|
146
|
+
self.next_allowed = max(self.next_allowed + (1 / self.rate), now + (1 / self.rate))
|
|
147
|
+
return
|
|
148
|
+
wait_time = self.next_allowed - now
|
|
149
|
+
|
|
150
|
+
time.sleep(max(0.001, wait_time)) # Min sleep 1ms for CPU
|
|
151
|
+
|
|
152
|
+
def get_executor_class(executor_type: ExecutorType):
|
|
153
|
+
"""Get the appropriate executor class based on type"""
|
|
154
|
+
return {
|
|
155
|
+
"thread": ThreadPoolExecutor,
|
|
156
|
+
"process": ProcessPoolExecutor
|
|
157
|
+
}[executor_type]
|
|
158
|
+
|
|
159
|
+
def get_or_create_executor(executor_type: ExecutorType, max_workers: int, prewarm: bool = False):
|
|
160
|
+
"""
|
|
161
|
+
Get a cached executor or create a new one.
|
|
162
|
+
|
|
163
|
+
This function manages a global cache of executors, allowing them to be
|
|
164
|
+
reused across multiple calls. The cache uses weak references, so executors
|
|
165
|
+
are automatically cleaned up when no longer needed.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
executor_type: Type of executor ("thread" or "process")
|
|
169
|
+
max_workers: Maximum number of workers
|
|
170
|
+
prewarm: If True, starts all workers immediately
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
ThreadPoolExecutor or ProcessPoolExecutor
|
|
174
|
+
"""
|
|
175
|
+
key = (executor_type, max_workers)
|
|
176
|
+
executor = _EXECUTOR_CACHE.get(key)
|
|
177
|
+
|
|
178
|
+
if executor is None or executor._shutdown:
|
|
179
|
+
executor_class = get_executor_class(executor_type)
|
|
180
|
+
executor = executor_class(max_workers=max_workers)
|
|
181
|
+
_EXECUTOR_CACHE[key] = executor
|
|
182
|
+
|
|
183
|
+
# Prewarm workers by submitting no-op tasks
|
|
184
|
+
if prewarm:
|
|
185
|
+
futures = [executor.submit(lambda: None) for _ in range(max_workers)]
|
|
186
|
+
for f in futures:
|
|
187
|
+
f.result() # Wait for workers to start
|
|
188
|
+
|
|
189
|
+
return executor
|
|
190
|
+
|
|
191
|
+
def parallel(
|
|
192
|
+
max_workers: int = None,
|
|
193
|
+
batch_size: int = None,
|
|
194
|
+
rate_limit: float | tuple[float, TimeUnit] | RateLimit = None,
|
|
195
|
+
executor_type: ExecutorType = "thread",
|
|
196
|
+
prewarm: bool = False
|
|
197
|
+
):
|
|
198
|
+
"""
|
|
199
|
+
Decorator for parallel execution of functions over iterables.
|
|
200
|
+
|
|
201
|
+
This decorator transforms a function that processes a single item into one
|
|
202
|
+
that can process multiple items in parallel. It supports both thread and
|
|
203
|
+
process-based parallelism, rate limiting, batch processing, and worker
|
|
204
|
+
prewarming.
|
|
205
|
+
|
|
206
|
+
The decorated function should take a single item as its first argument.
|
|
207
|
+
When called with a list/tuple, it will process all items in parallel.
|
|
208
|
+
When called with a single item, it will process it normally and return
|
|
209
|
+
a single-item list.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
max_workers: Maximum number of parallel workers. Defaults to:
|
|
213
|
+
- Processes: CPU count
|
|
214
|
+
- Threads: CPU count * 5
|
|
215
|
+
|
|
216
|
+
batch_size: Number of items to process in each batch. Useful for
|
|
217
|
+
controlling memory usage with large iterables. None means
|
|
218
|
+
process all items at once.
|
|
219
|
+
|
|
220
|
+
rate_limit: Rate limiting configuration, specified as:
|
|
221
|
+
- float: Operations per second
|
|
222
|
+
- tuple[float, TimeUnit]: (count, interval) e.g. (100, "minute")
|
|
223
|
+
- RateLimit: RateLimit instance
|
|
224
|
+
|
|
225
|
+
executor_type: Type of parallelism to use:
|
|
226
|
+
- "thread": For I/O-bound tasks (default)
|
|
227
|
+
- "process": For CPU-bound tasks
|
|
228
|
+
|
|
229
|
+
prewarm: If True, starts all workers immediately. Useful for
|
|
230
|
+
latency-critical applications where cold start time matters.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Callable: Wrapped function that processes items in parallel
|
|
234
|
+
|
|
235
|
+
Examples:
|
|
236
|
+
```python
|
|
237
|
+
# Basic I/O-bound task
|
|
238
|
+
@parallel(max_workers=4)
|
|
239
|
+
def fetch_url(url: str) -> dict:
|
|
240
|
+
return requests.get(url).json()
|
|
241
|
+
|
|
242
|
+
# CPU-bound task with rate limiting
|
|
243
|
+
@parallel(
|
|
244
|
+
max_workers=4,
|
|
245
|
+
executor_type="process",
|
|
246
|
+
rate_limit=(100, "minute")
|
|
247
|
+
)
|
|
248
|
+
def process_image(image: bytes) -> bytes:
|
|
249
|
+
return heavy_processing(image)
|
|
250
|
+
|
|
251
|
+
# Batch processing
|
|
252
|
+
@parallel(max_workers=4, batch_size=10)
|
|
253
|
+
def analyze_text(text: str) -> dict:
|
|
254
|
+
return text_analysis(text)
|
|
255
|
+
|
|
256
|
+
# Usage
|
|
257
|
+
urls = ["http://example1.com", "http://example2.com"]
|
|
258
|
+
results = fetch_url(urls) # Parallel processing
|
|
259
|
+
|
|
260
|
+
single_result = fetch_url("http://example.com") # Returns [result]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Notes:
|
|
264
|
+
- The function preserves the original function's docstring and signature
|
|
265
|
+
- Rate limiting is thread-safe
|
|
266
|
+
- Executors are reused and automatically cleaned up
|
|
267
|
+
- Process pools handle rate limiting in the main process
|
|
268
|
+
- Batch processing helps with memory usage
|
|
269
|
+
- Prewarming is useful for latency-critical applications
|
|
270
|
+
"""
|
|
271
|
+
# Calculate workers here so it's consistent
|
|
272
|
+
workers = max_workers or (
|
|
273
|
+
multiprocessing.cpu_count()
|
|
274
|
+
if executor_type == "process"
|
|
275
|
+
else (multiprocessing.cpu_count() * 5)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Prewarm if requested
|
|
279
|
+
if prewarm:
|
|
280
|
+
get_or_create_executor(executor_type, workers, prewarm=True)
|
|
281
|
+
|
|
282
|
+
def decorator(func: Callable[..., T]) -> Callable[..., list[T]]:
|
|
283
|
+
@wraps(func)
|
|
284
|
+
def wrapper(*args, **kwargs) -> list[T]:
|
|
285
|
+
if not args or not isinstance(args[0], (list, tuple)):
|
|
286
|
+
return [func(*args, **kwargs)]
|
|
287
|
+
|
|
288
|
+
items = args[0]
|
|
289
|
+
other_args = args[1:]
|
|
290
|
+
results = []
|
|
291
|
+
|
|
292
|
+
# Convert rate_limit to RateLimit object
|
|
293
|
+
rate = None
|
|
294
|
+
if rate_limit is not None:
|
|
295
|
+
if isinstance(rate_limit, (int, float)):
|
|
296
|
+
rate = RateLimit(float(rate_limit))
|
|
297
|
+
elif isinstance(rate_limit, tuple):
|
|
298
|
+
rate = RateLimit(*rate_limit)
|
|
299
|
+
else:
|
|
300
|
+
rate = rate_limit
|
|
301
|
+
|
|
302
|
+
# For process pool, rate limiting happens in the main process
|
|
303
|
+
bucket = TokenBucket(rate) if rate and executor_type == "thread" else None
|
|
304
|
+
|
|
305
|
+
def rate_limited_func(item):
|
|
306
|
+
if bucket:
|
|
307
|
+
bucket.wait_for_token()
|
|
308
|
+
return func(item, *other_args, **kwargs)
|
|
309
|
+
|
|
310
|
+
# Get or create cached executor
|
|
311
|
+
executor = get_or_create_executor(executor_type, workers)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
if batch_size is None:
|
|
315
|
+
# Rate limit in main process for ProcessPoolExecutor
|
|
316
|
+
if rate and executor_type == "process":
|
|
317
|
+
bucket = TokenBucket(rate)
|
|
318
|
+
for item in items:
|
|
319
|
+
bucket.wait_for_token()
|
|
320
|
+
|
|
321
|
+
futures = [
|
|
322
|
+
executor.submit(rate_limited_func if executor_type == "thread" else func, item, *other_args, **kwargs)
|
|
323
|
+
for item in items
|
|
324
|
+
]
|
|
325
|
+
results.extend(f.result() for f in as_completed(futures))
|
|
326
|
+
else:
|
|
327
|
+
# Process in batches
|
|
328
|
+
it = iter(items)
|
|
329
|
+
while batch := list(islice(it, batch_size)):
|
|
330
|
+
# Rate limit in main process for ProcessPoolExecutor
|
|
331
|
+
if rate and executor_type == "process":
|
|
332
|
+
bucket = TokenBucket(rate)
|
|
333
|
+
for _ in batch:
|
|
334
|
+
bucket.wait_for_token()
|
|
335
|
+
|
|
336
|
+
futures = [
|
|
337
|
+
executor.submit(rate_limited_func if executor_type == "thread" else func, item, *other_args, **kwargs)
|
|
338
|
+
for item in batch
|
|
339
|
+
]
|
|
340
|
+
results.extend(f.result() for f in as_completed(futures))
|
|
341
|
+
except:
|
|
342
|
+
# Don't shutdown cached executor on error
|
|
343
|
+
raise
|
|
344
|
+
|
|
345
|
+
return results
|
|
346
|
+
|
|
347
|
+
return wrapper
|
|
348
|
+
return decorator
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyarallel
|
|
3
|
+
Version: 0.1.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://github.com/oneryalcin/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
|
+
A powerful,feature-rich parallel execution library for Python that makes concurrent programming easy and efficient.
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **Simple Decorator-Based API**: Just add `@parallel` to your functions
|
|
47
|
+
- **Flexible Parallelism**: Choose between threads (I/O-bound) and processes (CPU-bound)
|
|
48
|
+
- **Smart Rate Limiting**: Control execution rates with per-second, per-minute, or per-hour limits
|
|
49
|
+
- **Batch Processing**: Handle large datasets efficiently with automatic batching
|
|
50
|
+
- **Performance Optimized**:
|
|
51
|
+
- Automatic worker pool reuse
|
|
52
|
+
- Optional worker prewarming for latency-critical applications
|
|
53
|
+
- Smart defaults based on your system
|
|
54
|
+
- **Production Ready**:
|
|
55
|
+
- Thread-safe implementation
|
|
56
|
+
- Memory-efficient with automatic cleanup
|
|
57
|
+
- Comprehensive error handling
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install pyarallel
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from pyarallel import parallel
|
|
69
|
+
|
|
70
|
+
# Basic parallel processing
|
|
71
|
+
@parallel(max_workers=4)
|
|
72
|
+
def fetch_url(url: str) -> dict:
|
|
73
|
+
return requests.get(url).json()
|
|
74
|
+
|
|
75
|
+
# Process multiple URLs in parallel
|
|
76
|
+
urls = ["http://api1.com", "http://api2.com"]
|
|
77
|
+
results = fetch_url(urls)
|
|
78
|
+
|
|
79
|
+
# Rate-limited CPU-intensive task
|
|
80
|
+
@parallel(
|
|
81
|
+
max_workers=4,
|
|
82
|
+
executor_type="process",
|
|
83
|
+
rate_limit=(100, "minute") # 100 ops/minute
|
|
84
|
+
)
|
|
85
|
+
def process_image(image: bytes) -> bytes:
|
|
86
|
+
return heavy_processing(image)
|
|
87
|
+
|
|
88
|
+
# Memory-efficient batch processing
|
|
89
|
+
@parallel(max_workers=4, batch_size=10)
|
|
90
|
+
def analyze_text(text: str) -> dict:
|
|
91
|
+
return text_analysis(text)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Advanced Usage
|
|
95
|
+
|
|
96
|
+
### Rate Limiting
|
|
97
|
+
|
|
98
|
+
Control execution rates using various formats:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# Operations per second
|
|
102
|
+
@parallel(rate_limit=2.0)
|
|
103
|
+
def func1(): ...
|
|
104
|
+
|
|
105
|
+
# Operations per minute
|
|
106
|
+
@parallel(rate_limit=(100, "minute"))
|
|
107
|
+
def func2(): ...
|
|
108
|
+
|
|
109
|
+
# Custom rate limit object
|
|
110
|
+
from pyarallel import RateLimit
|
|
111
|
+
rate = RateLimit(1000, "hour")
|
|
112
|
+
@parallel(rate_limit=rate)
|
|
113
|
+
def func3(): ...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### CPU-Bound Tasks
|
|
117
|
+
|
|
118
|
+
Use process-based parallelism for CPU-intensive operations:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
@parallel(
|
|
122
|
+
max_workers=4,
|
|
123
|
+
executor_type="process", # Use processes instead of threads
|
|
124
|
+
batch_size=10 # Process in batches of 10
|
|
125
|
+
)
|
|
126
|
+
def cpu_intensive(data: bytes) -> bytes:
|
|
127
|
+
return heavy_computation(data)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Latency-Critical Applications
|
|
131
|
+
|
|
132
|
+
Prewarm workers to minimize cold start latency:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
@parallel(
|
|
136
|
+
max_workers=4,
|
|
137
|
+
prewarm=True # Start workers immediately
|
|
138
|
+
)
|
|
139
|
+
def latency_critical(item): ...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Memory-Efficient Processing
|
|
143
|
+
|
|
144
|
+
Handle large datasets with batch processing:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
@parallel(
|
|
148
|
+
max_workers=4,
|
|
149
|
+
batch_size=100 # Process items in batches of 100
|
|
150
|
+
)
|
|
151
|
+
def process_large_dataset(item): ...
|
|
152
|
+
|
|
153
|
+
# Process millions of items without memory issues
|
|
154
|
+
items = range(1_000_000)
|
|
155
|
+
results = process_large_dataset(items)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Best Practices
|
|
159
|
+
|
|
160
|
+
1. **Choose the Right Executor**:
|
|
161
|
+
- Use `executor_type="thread"` (default) for I/O-bound tasks (network, disk)
|
|
162
|
+
- Use `executor_type="process"` for CPU-bound tasks (computation)
|
|
163
|
+
|
|
164
|
+
2. **Optimize Worker Count**:
|
|
165
|
+
- For I/O-bound: `max_workers = cpu_count * 5` (default)
|
|
166
|
+
- For CPU-bound: `max_workers = cpu_count` (default)
|
|
167
|
+
|
|
168
|
+
3. **Control Resource Usage**:
|
|
169
|
+
- Use `batch_size` for large datasets
|
|
170
|
+
- Use `rate_limit` to prevent overwhelming resources
|
|
171
|
+
- Only use `prewarm=True` when cold start latency is critical
|
|
172
|
+
|
|
173
|
+
4. **Handle Errors Properly**:
|
|
174
|
+
```python
|
|
175
|
+
@parallel()
|
|
176
|
+
def my_func(item):
|
|
177
|
+
try:
|
|
178
|
+
return process(item)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
return {"error": str(e), "item": item}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Roadmap
|
|
184
|
+
|
|
185
|
+
### Observability & Debugging
|
|
186
|
+
- **Advanced Telemetry System**
|
|
187
|
+
- Task execution metrics (duration, wait times, queue times)
|
|
188
|
+
- Worker utilization tracking
|
|
189
|
+
- Error frequency analysis
|
|
190
|
+
- SQLite persistence for historical data
|
|
191
|
+
- Interactive visualizations with Plotly
|
|
192
|
+
- Performance bottleneck identification
|
|
193
|
+
|
|
194
|
+
- **Rich Logging System**
|
|
195
|
+
- Configurable log levels per component
|
|
196
|
+
- Structured logging for machine parsing
|
|
197
|
+
- Contextual information for debugging
|
|
198
|
+
- Log rotation and management
|
|
199
|
+
- Integration with popular logging frameworks
|
|
200
|
+
|
|
201
|
+
### Advanced Features
|
|
202
|
+
- **Callback System**
|
|
203
|
+
- Pre/post execution hooks
|
|
204
|
+
- Error handling callbacks
|
|
205
|
+
- Progress tracking
|
|
206
|
+
- Custom metrics collection
|
|
207
|
+
- State management hooks
|
|
208
|
+
|
|
209
|
+
- **Smart Scheduling**
|
|
210
|
+
- Priority queues for tasks
|
|
211
|
+
- Deadline-aware scheduling
|
|
212
|
+
- Resource-aware task distribution
|
|
213
|
+
- Adaptive batch sizing
|
|
214
|
+
- Dynamic worker scaling
|
|
215
|
+
|
|
216
|
+
- **Fault Tolerance**
|
|
217
|
+
- Automatic retries with backoff
|
|
218
|
+
- Circuit breaker pattern
|
|
219
|
+
- Fallback strategies
|
|
220
|
+
- Dead letter queues
|
|
221
|
+
- Task timeout handling
|
|
222
|
+
|
|
223
|
+
- **Resource Management**
|
|
224
|
+
- Memory usage monitoring
|
|
225
|
+
- CPU utilization tracking
|
|
226
|
+
- Network bandwidth control
|
|
227
|
+
- Disk I/O rate limiting
|
|
228
|
+
- Resource quotas per task
|
|
229
|
+
|
|
230
|
+
### Developer Experience
|
|
231
|
+
- **CLI Tools**
|
|
232
|
+
- Task monitoring dashboard
|
|
233
|
+
- Performance profiling
|
|
234
|
+
- Configuration management
|
|
235
|
+
- Log analysis utilities
|
|
236
|
+
- Telemetry visualization
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
### Enterprise Features
|
|
240
|
+
- **Integration**
|
|
241
|
+
- Distributed tracing (OpenTelemetry)
|
|
242
|
+
- Metrics export (Prometheus)
|
|
243
|
+
- Log aggregation (ELK Stack)
|
|
244
|
+
|
|
245
|
+
Want to contribute? Check out our [CONTRIBUTING.md](CONTRIBUTING.md) guide!
|
|
246
|
+
|
|
247
|
+
## API Reference
|
|
248
|
+
|
|
249
|
+
### @parallel Decorator
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
@parallel(
|
|
253
|
+
max_workers: int = None, # Maximum workers (default: based on CPU)
|
|
254
|
+
batch_size: int = None, # Items per batch (default: all at once)
|
|
255
|
+
rate_limit: Union[ # Rate limiting configuration
|
|
256
|
+
float, # - Operations per second
|
|
257
|
+
Tuple[float, str], # - (count, interval)
|
|
258
|
+
RateLimit # - RateLimit object
|
|
259
|
+
] = None,
|
|
260
|
+
executor_type: str = "thread", # "thread" or "process"
|
|
261
|
+
prewarm: bool = False # Prewarm workers
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### RateLimit Class
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
class RateLimit:
|
|
269
|
+
def __init__(self, count: float, interval: str = "second"):
|
|
270
|
+
"""
|
|
271
|
+
Args:
|
|
272
|
+
count: Operations allowed per interval
|
|
273
|
+
interval: "second", "minute", or "hour"
|
|
274
|
+
"""
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Contributing
|
|
278
|
+
|
|
279
|
+
Contributions are welcome! Please check out our [Contributing Guide](CONTRIBUTING.md).
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pyarallel/__init__.py,sha256=XQA0uDv38wz3OWRRKj0JE1M-Teee9y_1TsWU5ajOvSY,191
|
|
2
|
+
pyarallel/core.py,sha256=5kTmCXyRH4agzUkAlHgAJ2c_Pmbrhe_wVC-JaG_z7i4,12522
|
|
3
|
+
pyarallel-0.1.0.dist-info/METADATA,sha256=TOLwHbW2zohxJOrVEE2xpRBlCXxbuiFNSjqp2jj__ZQ,8252
|
|
4
|
+
pyarallel-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
pyarallel-0.1.0.dist-info/licenses/LICENSE.md,sha256=6jlQa2lR3rODieDf4bPeHXaOFHLOLbw2Q8jD2RAkz68,1079
|
|
6
|
+
pyarallel-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Pyarallel Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|