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 ADDED
@@ -0,0 +1,10 @@
1
+ from .core import parallel, RateLimit
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["parallel", "RateLimit"]
5
+
6
+ __doc__ = f"""
7
+ pyarallel v{__version__}
8
+
9
+ Simple parallel processing framework for Python.
10
+ """
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.