async-batcher 0.2.2__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.
- async_batcher-0.3.0/PKG-INFO +161 -0
- async_batcher-0.3.0/README.md +120 -0
- async_batcher-0.3.0/async_batcher/__init__.py +20 -0
- async_batcher-0.3.0/async_batcher/_options.py +40 -0
- async_batcher-0.3.0/async_batcher/aws/_records.py +100 -0
- async_batcher-0.3.0/async_batcher/aws/dynamodb/_retry.py +34 -0
- async_batcher-0.3.0/async_batcher/aws/dynamodb/exceptions.py +17 -0
- async_batcher-0.3.0/async_batcher/aws/dynamodb/get.py +184 -0
- {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/aws/dynamodb/write.py +23 -9
- async_batcher-0.3.0/async_batcher/aws/firehose/__init__.py +6 -0
- async_batcher-0.3.0/async_batcher/aws/firehose/write.py +63 -0
- async_batcher-0.3.0/async_batcher/aws/kinesis/__init__.py +6 -0
- async_batcher-0.3.0/async_batcher/aws/kinesis/write.py +100 -0
- async_batcher-0.3.0/async_batcher/batcher.py +898 -0
- async_batcher-0.3.0/async_batcher/callable.py +88 -0
- async_batcher-0.3.0/async_batcher/coalescing.py +176 -0
- async_batcher-0.3.0/async_batcher/grouped.py +354 -0
- async_batcher-0.3.0/async_batcher/kafka/__init__.py +5 -0
- async_batcher-0.3.0/async_batcher/kafka/producer.py +181 -0
- async_batcher-0.3.0/async_batcher/map.py +175 -0
- async_batcher-0.3.0/async_batcher/ml/keras/model.py +78 -0
- async_batcher-0.3.0/async_batcher/ml/sklearn/model.py +83 -0
- async_batcher-0.3.0/async_batcher/mongodb/__init__.py +5 -0
- async_batcher-0.3.0/async_batcher/mongodb/write.py +81 -0
- async_batcher-0.3.0/async_batcher/observability.py +58 -0
- async_batcher-0.3.0/async_batcher/pinecone/__init__.py +9 -0
- async_batcher-0.3.0/async_batcher/pinecone/vectors.py +117 -0
- async_batcher-0.3.0/async_batcher/scylladb/update.py +74 -0
- async_batcher-0.3.0/async_batcher/sqlalchemy/__init__.py +0 -0
- async_batcher-0.3.0/async_batcher/sqlalchemy/write.py +84 -0
- async_batcher-0.3.0/pyproject.toml +147 -0
- async_batcher-0.3.0/pyproject.toml.orig +125 -0
- async_batcher-0.2.2/PKG-INFO +0 -183
- async_batcher-0.2.2/README.md +0 -151
- async_batcher-0.2.2/async_batcher/__init__.py +0 -1
- async_batcher-0.2.2/async_batcher/aws/dynamodb/get.py +0 -96
- async_batcher-0.2.2/async_batcher/batcher.py +0 -243
- async_batcher-0.2.2/async_batcher/ml/keras/model.py +0 -50
- async_batcher-0.2.2/async_batcher/ml/sklearn/model.py +0 -51
- async_batcher-0.2.2/async_batcher/scylladb/update.py +0 -47
- async_batcher-0.2.2/async_batcher/sqlalchemy/write.py +0 -60
- async_batcher-0.2.2/pyproject.toml +0 -78
- {async_batcher-0.2.2 → async_batcher-0.3.0}/LICENSE +0 -0
- {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/aws/__init__.py +0 -0
- {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/aws/dynamodb/__init__.py +0 -0
- {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/exceptions.py +0 -0
- {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/ml/__init__.py +0 -0
- {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/ml/keras/__init__.py +0 -0
- {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/ml/sklearn/__init__.py +0 -0
- /async_batcher-0.2.2/async_batcher/scylladb/__init__.py → /async_batcher-0.3.0/async_batcher/py.typed +0 -0
- {async_batcher-0.2.2/async_batcher/sqlalchemy → async_batcher-0.3.0/async_batcher/scylladb}/__init__.py +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: async-batcher
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Async Python batching for bulk functions, database writes, messaging, and model inference.
|
|
5
|
+
Keywords: asyncio,batching,bulk-processing,inference,backpressure
|
|
6
|
+
Author: Hussein Awala
|
|
7
|
+
Author-email: Hussein Awala <hussein@awala.fr>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Requires-Dist: aioboto3>=15.5,<16 ; extra == 'aws'
|
|
17
|
+
Requires-Dist: aiohttp>=3.14.4,<4 ; extra == 'aws'
|
|
18
|
+
Requires-Dist: urllib3>=2.8.0,<3 ; extra == 'aws'
|
|
19
|
+
Requires-Dist: idna>=3.15,<4 ; extra == 'aws'
|
|
20
|
+
Requires-Dist: aiokafka>=0.15.0,<1 ; extra == 'kafka'
|
|
21
|
+
Requires-Dist: keras>=3.15.1,<4 ; extra == 'keras'
|
|
22
|
+
Requires-Dist: pygments>=2.20,<3 ; extra == 'keras'
|
|
23
|
+
Requires-Dist: pymongo>=4.19,<5 ; extra == 'mongodb'
|
|
24
|
+
Requires-Dist: pinecone>=10.3,<11 ; extra == 'pinecone'
|
|
25
|
+
Requires-Dist: scylla-driver>=3.29.13,<4 ; extra == 'scylla'
|
|
26
|
+
Requires-Dist: scikit-learn>=1.9.1,<2 ; extra == 'sklearn'
|
|
27
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.10,<3 ; extra == 'sqlalchemy'
|
|
28
|
+
Requires-Python: >=3.11, <4.0
|
|
29
|
+
Project-URL: Repository, https://github.com/hussein-awala/async-batcher
|
|
30
|
+
Project-URL: Bug Tracker, https://github.com/hussein-awala/async-batcher/issues
|
|
31
|
+
Project-URL: Documentation, https://hussein.awala.fr/async-batcher/
|
|
32
|
+
Provides-Extra: aws
|
|
33
|
+
Provides-Extra: kafka
|
|
34
|
+
Provides-Extra: keras
|
|
35
|
+
Provides-Extra: mongodb
|
|
36
|
+
Provides-Extra: pinecone
|
|
37
|
+
Provides-Extra: scylla
|
|
38
|
+
Provides-Extra: sklearn
|
|
39
|
+
Provides-Extra: sqlalchemy
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# AsyncBatcher
|
|
43
|
+
|
|
44
|
+
[Documentation](https://hussein.awala.fr/async-batcher/0.3.0/) ·
|
|
45
|
+
[Release notes](https://hussein.awala.fr/async-batcher/release-notes/)
|
|
46
|
+
|
|
47
|
+
AsyncBatcher combines concurrent Python requests into batches and routes each result back to its caller.
|
|
48
|
+
Wrap an existing bulk function, subclass `AsyncBatcher`, or use an adapter for a supported database,
|
|
49
|
+
messaging client, or prediction model. The core package has no runtime dependencies.
|
|
50
|
+
|
|
51
|
+
## Start with your workload
|
|
52
|
+
|
|
53
|
+
| You need to… | Start here |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| Combine concurrent web requests into model predictions or writes | [Function quickstart](#quickstart), then the [FastAPI lifespan recipe](https://hussein.awala.fr/async-batcher/0.3.0/fastapi-recipe/) |
|
|
56
|
+
| Import a large file or consume an async stream without keeping all results | [Bounded maps](https://hussein.awala.fr/async-batcher/0.3.0/bulk-processing/), [SQLite imports](https://hussein.awala.fr/async-batcher/0.3.0/cookbook/#import-a-file-or-async-stream-into-sqlite), and [recovery choices](https://hussein.awala.fr/async-batcher/0.3.0/cookbook/#choose-an-import-or-recovery-policy) |
|
|
57
|
+
| Use an existing database, messaging, or prediction client | Choose its [adapter](#optional-adapters) and read its failure/ownership rules |
|
|
58
|
+
| Send embeddings to a vector store | [Two-stage pipeline](https://hussein.awala.fr/async-batcher/0.3.0/cookbook/#connect-embedding-and-vector-storage-stages) and [Pinecone](https://hussein.awala.fr/async-batcher/0.3.0/pinecone/) |
|
|
59
|
+
| Keep tenants, model revisions, or backend destinations separate | [Compatible request groups](https://hussein.awala.fr/async-batcher/0.3.0/grouped-batching/) and the [workload guide](https://hussein.awala.fr/async-batcher/0.3.0/workloads/) |
|
|
60
|
+
| Limit bytes, tokens, or estimated work as well as request count | [Cost budgets](https://hussein.awala.fr/async-batcher/0.3.0/cost-aware-batching/) |
|
|
61
|
+
|
|
62
|
+
Start with a [bounded importer or interactive service](https://hussein.awala.fr/async-batcher/0.3.0/starting-configurations/).
|
|
63
|
+
The [workload guide](https://hussein.awala.fr/async-batcher/0.3.0/workloads/) helps choose limits, preserve routing metadata, and decide
|
|
64
|
+
whether batching fits your backend. Batching requires a useful bulk operation; it is not a rate
|
|
65
|
+
limiter, durable queue, or exactly-once delivery system.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
Python 3.11 or newer is required. This README targets **Async Batcher 0.3.0**;
|
|
70
|
+
check the [release history](https://hussein.awala.fr/async-batcher/release-notes/)
|
|
71
|
+
for publication status. Install the package into your application's environment:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python -m pip install 'async-batcher==0.3.0'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The core has no runtime dependencies. Add only the extras you need, for example:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python -m pip install 'async-batcher[aws]==0.3.0'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See [installation](https://hussein.awala.fr/async-batcher/0.3.0/installation/) for uv, other extras, and repository recipes,
|
|
84
|
+
and [migration from 0.2.2](https://hussein.awala.fr/async-batcher/0.3.0/migration/) before upgrading. Core and adapter usage
|
|
85
|
+
does not need a source checkout. Runnable files under `examples/` are repository
|
|
86
|
+
recipes, with separate [setup instructions](https://hussein.awala.fr/async-batcher/0.3.0/installation/#run-the-repository-recipes).
|
|
87
|
+
|
|
88
|
+
## Quickstart
|
|
89
|
+
|
|
90
|
+
Save this as `demo.py` and run `python demo.py` in the environment where you installed the package:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import asyncio
|
|
94
|
+
|
|
95
|
+
from async_batcher import AsyncBatcher
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def double_many(values: list[int]) -> list[int]:
|
|
99
|
+
return [value * 2 for value in values]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def main():
|
|
103
|
+
async with AsyncBatcher.from_callable(
|
|
104
|
+
double_many, max_batch_size=5, max_queue_size=20, max_queue_time=0.01, concurrency=2
|
|
105
|
+
) as batcher:
|
|
106
|
+
results = await asyncio.gather(*(batcher.process(i) for i in range(10)))
|
|
107
|
+
print(results) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
asyncio.run(main())
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`process()` is a coroutine: submit calls concurrently to form batches. A loop that awaits each
|
|
114
|
+
call before submitting the next normally produces single-item batches. Processing starts
|
|
115
|
+
automatically. Stop the batcher before closing its clients, model resources, or executor.
|
|
116
|
+
Existing [subclasses](https://hussein.awala.fr/async-batcher/0.3.0/api/) and adapters support the same managed lifecycle.
|
|
117
|
+
|
|
118
|
+
## Guides
|
|
119
|
+
|
|
120
|
+
- [Compatibility and migration](https://hussein.awala.fr/async-batcher/0.3.0/migration/): published features, Python requirements, and upgrade checks.
|
|
121
|
+
- [Bounded starting configurations](https://hussein.awala.fr/async-batcher/0.3.0/starting-configurations/): copyable importer and interactive service examples.
|
|
122
|
+
- [Choose by workload](https://hussein.awala.fr/async-batcher/0.3.0/workloads/): request compatibility, safe metadata, and configuration ownership.
|
|
123
|
+
- [Batch an existing function](https://hussein.awala.fr/async-batcher/0.3.0/functions/): callable processors and managed cleanup.
|
|
124
|
+
- [Bulk and streaming producers](https://hussein.awala.fr/async-batcher/0.3.0/bulk-processing/): bounded maps and waiting for queue space.
|
|
125
|
+
- [Cost budgets](https://hussein.awala.fr/async-batcher/0.3.0/cost-aware-batching/): byte, token, or work limits alongside batch counts.
|
|
126
|
+
- [Variable-length inference](https://hussein.awala.fr/async-batcher/0.3.0/variable-length-inference/): padding, conservative token budgets, and per-request output shapes.
|
|
127
|
+
- [Operational visibility](https://hussein.awala.fr/async-batcher/0.3.0/observability/): snapshots and payload-free completion events.
|
|
128
|
+
- [Grouped batching](https://hussein.awala.fr/async-batcher/0.3.0/grouped-batching/): isolate compatible requests with bounded child ownership.
|
|
129
|
+
- [In-flight coalescing](https://hussein.awala.fr/async-batcher/0.3.0/coalescing/): share equivalent concurrent requests with bounded ownership.
|
|
130
|
+
- [Cookbook](https://hussein.awala.fr/async-batcher/0.3.0/cookbook/): runnable SQLite imports, embedding pipelines, and measured batch sizes.
|
|
131
|
+
- [Resumable imports](https://hussein.awala.fr/async-batcher/0.3.0/resumable-import/): immutable JSONL input with atomic SQLite checkpoints.
|
|
132
|
+
- [Rejected-record imports](https://hussein.awala.fr/async-batcher/0.3.0/rejected-import/): continue past known input errors and retain a metadata-only audit.
|
|
133
|
+
- [Durable embedding handoff](https://hussein.awala.fr/async-batcher/0.3.0/durable-embeddings/): save vectors before delivery and replay unfinished upserts.
|
|
134
|
+
- [Provider quota and retry policy](https://hussein.awala.fr/async-batcher/0.3.0/provider-policy/): shared pacing, finite throttling retries, and cancellation.
|
|
135
|
+
- [Cohere to Pinecone](https://hussein.awala.fr/async-batcher/0.3.0/cohere-pinecone/): real embeddings into an existing compatible vector index.
|
|
136
|
+
- [FastAPI recipe](https://hussein.awala.fr/async-batcher/0.3.0/fastapi-recipe/): lifespan-managed predictions, SQLite writes, and HTTP overload handling.
|
|
137
|
+
- [Operational recipes](https://hussein.awala.fr/async-batcher/0.3.0/operations/): separate deadlines, shared backend capacity, thread lifetime, and bounded telemetry.
|
|
138
|
+
- [Core API and lifecycle](https://hussein.awala.fr/async-batcher/0.3.0/api/): limits, result mapping, errors, shutdown, restart, and executors.
|
|
139
|
+
- [Contributing](https://github.com/hussein-awala/async-batcher/blob/0.3.0/CONTRIBUTING.md): locked development environments, test selection, integration services, and CI.
|
|
140
|
+
- [Benchmarks](https://github.com/hussein-awala/async-batcher/blob/0.3.0/BENCHMARK.md): reproducible core measurements and archived application results.
|
|
141
|
+
- [Public type contracts](https://hussein.awala.fr/async-batcher/0.3.0/typing/): inferred results, typed options, and installed-package checks.
|
|
142
|
+
- [Security reporting](https://github.com/hussein-awala/async-batcher/blob/0.3.0/SECURITY.md) and [Code of Conduct](https://github.com/hussein-awala/async-batcher/blob/0.3.0/CODE_OF_CONDUCT.md).
|
|
143
|
+
|
|
144
|
+
## Optional adapters
|
|
145
|
+
|
|
146
|
+
The eight extras are independent; importing `async_batcher` does not import their clients.
|
|
147
|
+
All adapters use the core lifecycle, with the limits and resource ownership described in each guide.
|
|
148
|
+
|
|
149
|
+
| Extra | Operations | Guide |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| `aws` | DynamoDB reads/writes; Kinesis and Firehose records | [DynamoDB](https://hussein.awala.fr/async-batcher/0.3.0/dynamodb/), [AWS streams](https://hussein.awala.fr/async-batcher/0.3.0/aws-streams/) |
|
|
152
|
+
| `sqlalchemy` | SQLAlchemy inserts and ORM updates | [SQLAlchemy](https://hussein.awala.fr/async-batcher/0.3.0/databases/#sqlalchemy) |
|
|
153
|
+
| `scylla` | cqlengine insert/update/delete batches | [ScyllaDB](https://hussein.awala.fr/async-batcher/0.3.0/databases/#scylladb) |
|
|
154
|
+
| `mongodb` | Ordered native PyMongo writes | [MongoDB](https://hussein.awala.fr/async-batcher/0.3.0/databases/#mongodb) |
|
|
155
|
+
| `kafka` | Native aiokafka producer batches | [Kafka](https://hussein.awala.fr/async-batcher/0.3.0/kafka/) |
|
|
156
|
+
| `pinecone` | Vector upsert/fetch/delete | [Pinecone](https://hussein.awala.fr/async-batcher/0.3.0/pinecone/) |
|
|
157
|
+
| `keras` | Dense-input Keras inference | [Keras](https://hussein.awala.fr/async-batcher/0.3.0/machine-learning/#keras) |
|
|
158
|
+
| `sklearn` | Dense or sparse scikit-learn inference | [scikit-learn](https://hussein.awala.fr/async-batcher/0.3.0/machine-learning/#scikit-learn) |
|
|
159
|
+
|
|
160
|
+
Services that do not offer a suitable bulk operation need a custom `process_batch` implementation.
|
|
161
|
+
Batching alone does not enforce a service's request-rate limit or guarantee exactly-once side effects.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# AsyncBatcher
|
|
2
|
+
|
|
3
|
+
[Documentation](https://hussein.awala.fr/async-batcher/0.3.0/) ·
|
|
4
|
+
[Release notes](https://hussein.awala.fr/async-batcher/release-notes/)
|
|
5
|
+
|
|
6
|
+
AsyncBatcher combines concurrent Python requests into batches and routes each result back to its caller.
|
|
7
|
+
Wrap an existing bulk function, subclass `AsyncBatcher`, or use an adapter for a supported database,
|
|
8
|
+
messaging client, or prediction model. The core package has no runtime dependencies.
|
|
9
|
+
|
|
10
|
+
## Start with your workload
|
|
11
|
+
|
|
12
|
+
| You need to… | Start here |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| Combine concurrent web requests into model predictions or writes | [Function quickstart](#quickstart), then the [FastAPI lifespan recipe](https://hussein.awala.fr/async-batcher/0.3.0/fastapi-recipe/) |
|
|
15
|
+
| Import a large file or consume an async stream without keeping all results | [Bounded maps](https://hussein.awala.fr/async-batcher/0.3.0/bulk-processing/), [SQLite imports](https://hussein.awala.fr/async-batcher/0.3.0/cookbook/#import-a-file-or-async-stream-into-sqlite), and [recovery choices](https://hussein.awala.fr/async-batcher/0.3.0/cookbook/#choose-an-import-or-recovery-policy) |
|
|
16
|
+
| Use an existing database, messaging, or prediction client | Choose its [adapter](#optional-adapters) and read its failure/ownership rules |
|
|
17
|
+
| Send embeddings to a vector store | [Two-stage pipeline](https://hussein.awala.fr/async-batcher/0.3.0/cookbook/#connect-embedding-and-vector-storage-stages) and [Pinecone](https://hussein.awala.fr/async-batcher/0.3.0/pinecone/) |
|
|
18
|
+
| Keep tenants, model revisions, or backend destinations separate | [Compatible request groups](https://hussein.awala.fr/async-batcher/0.3.0/grouped-batching/) and the [workload guide](https://hussein.awala.fr/async-batcher/0.3.0/workloads/) |
|
|
19
|
+
| Limit bytes, tokens, or estimated work as well as request count | [Cost budgets](https://hussein.awala.fr/async-batcher/0.3.0/cost-aware-batching/) |
|
|
20
|
+
|
|
21
|
+
Start with a [bounded importer or interactive service](https://hussein.awala.fr/async-batcher/0.3.0/starting-configurations/).
|
|
22
|
+
The [workload guide](https://hussein.awala.fr/async-batcher/0.3.0/workloads/) helps choose limits, preserve routing metadata, and decide
|
|
23
|
+
whether batching fits your backend. Batching requires a useful bulk operation; it is not a rate
|
|
24
|
+
limiter, durable queue, or exactly-once delivery system.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Python 3.11 or newer is required. This README targets **Async Batcher 0.3.0**;
|
|
29
|
+
check the [release history](https://hussein.awala.fr/async-batcher/release-notes/)
|
|
30
|
+
for publication status. Install the package into your application's environment:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python -m pip install 'async-batcher==0.3.0'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The core has no runtime dependencies. Add only the extras you need, for example:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install 'async-batcher[aws]==0.3.0'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
See [installation](https://hussein.awala.fr/async-batcher/0.3.0/installation/) for uv, other extras, and repository recipes,
|
|
43
|
+
and [migration from 0.2.2](https://hussein.awala.fr/async-batcher/0.3.0/migration/) before upgrading. Core and adapter usage
|
|
44
|
+
does not need a source checkout. Runnable files under `examples/` are repository
|
|
45
|
+
recipes, with separate [setup instructions](https://hussein.awala.fr/async-batcher/0.3.0/installation/#run-the-repository-recipes).
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
Save this as `demo.py` and run `python demo.py` in the environment where you installed the package:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import asyncio
|
|
53
|
+
|
|
54
|
+
from async_batcher import AsyncBatcher
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def double_many(values: list[int]) -> list[int]:
|
|
58
|
+
return [value * 2 for value in values]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def main():
|
|
62
|
+
async with AsyncBatcher.from_callable(
|
|
63
|
+
double_many, max_batch_size=5, max_queue_size=20, max_queue_time=0.01, concurrency=2
|
|
64
|
+
) as batcher:
|
|
65
|
+
results = await asyncio.gather(*(batcher.process(i) for i in range(10)))
|
|
66
|
+
print(results) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`process()` is a coroutine: submit calls concurrently to form batches. A loop that awaits each
|
|
73
|
+
call before submitting the next normally produces single-item batches. Processing starts
|
|
74
|
+
automatically. Stop the batcher before closing its clients, model resources, or executor.
|
|
75
|
+
Existing [subclasses](https://hussein.awala.fr/async-batcher/0.3.0/api/) and adapters support the same managed lifecycle.
|
|
76
|
+
|
|
77
|
+
## Guides
|
|
78
|
+
|
|
79
|
+
- [Compatibility and migration](https://hussein.awala.fr/async-batcher/0.3.0/migration/): published features, Python requirements, and upgrade checks.
|
|
80
|
+
- [Bounded starting configurations](https://hussein.awala.fr/async-batcher/0.3.0/starting-configurations/): copyable importer and interactive service examples.
|
|
81
|
+
- [Choose by workload](https://hussein.awala.fr/async-batcher/0.3.0/workloads/): request compatibility, safe metadata, and configuration ownership.
|
|
82
|
+
- [Batch an existing function](https://hussein.awala.fr/async-batcher/0.3.0/functions/): callable processors and managed cleanup.
|
|
83
|
+
- [Bulk and streaming producers](https://hussein.awala.fr/async-batcher/0.3.0/bulk-processing/): bounded maps and waiting for queue space.
|
|
84
|
+
- [Cost budgets](https://hussein.awala.fr/async-batcher/0.3.0/cost-aware-batching/): byte, token, or work limits alongside batch counts.
|
|
85
|
+
- [Variable-length inference](https://hussein.awala.fr/async-batcher/0.3.0/variable-length-inference/): padding, conservative token budgets, and per-request output shapes.
|
|
86
|
+
- [Operational visibility](https://hussein.awala.fr/async-batcher/0.3.0/observability/): snapshots and payload-free completion events.
|
|
87
|
+
- [Grouped batching](https://hussein.awala.fr/async-batcher/0.3.0/grouped-batching/): isolate compatible requests with bounded child ownership.
|
|
88
|
+
- [In-flight coalescing](https://hussein.awala.fr/async-batcher/0.3.0/coalescing/): share equivalent concurrent requests with bounded ownership.
|
|
89
|
+
- [Cookbook](https://hussein.awala.fr/async-batcher/0.3.0/cookbook/): runnable SQLite imports, embedding pipelines, and measured batch sizes.
|
|
90
|
+
- [Resumable imports](https://hussein.awala.fr/async-batcher/0.3.0/resumable-import/): immutable JSONL input with atomic SQLite checkpoints.
|
|
91
|
+
- [Rejected-record imports](https://hussein.awala.fr/async-batcher/0.3.0/rejected-import/): continue past known input errors and retain a metadata-only audit.
|
|
92
|
+
- [Durable embedding handoff](https://hussein.awala.fr/async-batcher/0.3.0/durable-embeddings/): save vectors before delivery and replay unfinished upserts.
|
|
93
|
+
- [Provider quota and retry policy](https://hussein.awala.fr/async-batcher/0.3.0/provider-policy/): shared pacing, finite throttling retries, and cancellation.
|
|
94
|
+
- [Cohere to Pinecone](https://hussein.awala.fr/async-batcher/0.3.0/cohere-pinecone/): real embeddings into an existing compatible vector index.
|
|
95
|
+
- [FastAPI recipe](https://hussein.awala.fr/async-batcher/0.3.0/fastapi-recipe/): lifespan-managed predictions, SQLite writes, and HTTP overload handling.
|
|
96
|
+
- [Operational recipes](https://hussein.awala.fr/async-batcher/0.3.0/operations/): separate deadlines, shared backend capacity, thread lifetime, and bounded telemetry.
|
|
97
|
+
- [Core API and lifecycle](https://hussein.awala.fr/async-batcher/0.3.0/api/): limits, result mapping, errors, shutdown, restart, and executors.
|
|
98
|
+
- [Contributing](https://github.com/hussein-awala/async-batcher/blob/0.3.0/CONTRIBUTING.md): locked development environments, test selection, integration services, and CI.
|
|
99
|
+
- [Benchmarks](https://github.com/hussein-awala/async-batcher/blob/0.3.0/BENCHMARK.md): reproducible core measurements and archived application results.
|
|
100
|
+
- [Public type contracts](https://hussein.awala.fr/async-batcher/0.3.0/typing/): inferred results, typed options, and installed-package checks.
|
|
101
|
+
- [Security reporting](https://github.com/hussein-awala/async-batcher/blob/0.3.0/SECURITY.md) and [Code of Conduct](https://github.com/hussein-awala/async-batcher/blob/0.3.0/CODE_OF_CONDUCT.md).
|
|
102
|
+
|
|
103
|
+
## Optional adapters
|
|
104
|
+
|
|
105
|
+
The eight extras are independent; importing `async_batcher` does not import their clients.
|
|
106
|
+
All adapters use the core lifecycle, with the limits and resource ownership described in each guide.
|
|
107
|
+
|
|
108
|
+
| Extra | Operations | Guide |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `aws` | DynamoDB reads/writes; Kinesis and Firehose records | [DynamoDB](https://hussein.awala.fr/async-batcher/0.3.0/dynamodb/), [AWS streams](https://hussein.awala.fr/async-batcher/0.3.0/aws-streams/) |
|
|
111
|
+
| `sqlalchemy` | SQLAlchemy inserts and ORM updates | [SQLAlchemy](https://hussein.awala.fr/async-batcher/0.3.0/databases/#sqlalchemy) |
|
|
112
|
+
| `scylla` | cqlengine insert/update/delete batches | [ScyllaDB](https://hussein.awala.fr/async-batcher/0.3.0/databases/#scylladb) |
|
|
113
|
+
| `mongodb` | Ordered native PyMongo writes | [MongoDB](https://hussein.awala.fr/async-batcher/0.3.0/databases/#mongodb) |
|
|
114
|
+
| `kafka` | Native aiokafka producer batches | [Kafka](https://hussein.awala.fr/async-batcher/0.3.0/kafka/) |
|
|
115
|
+
| `pinecone` | Vector upsert/fetch/delete | [Pinecone](https://hussein.awala.fr/async-batcher/0.3.0/pinecone/) |
|
|
116
|
+
| `keras` | Dense-input Keras inference | [Keras](https://hussein.awala.fr/async-batcher/0.3.0/machine-learning/#keras) |
|
|
117
|
+
| `sklearn` | Dense or sparse scikit-learn inference | [scikit-learn](https://hussein.awala.fr/async-batcher/0.3.0/machine-learning/#scikit-learn) |
|
|
118
|
+
|
|
119
|
+
Services that do not offer a suitable bulk operation need a custom `process_batch` implementation.
|
|
120
|
+
Batching alone does not enforce a service's request-rate limit or guarantee exactly-once side effects.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from async_batcher.batcher import AsyncBatcher
|
|
4
|
+
from async_batcher.coalescing import CoalescingBatcher
|
|
5
|
+
from async_batcher.exceptions import AsyncBatchException, QueueFullException
|
|
6
|
+
from async_batcher.grouped import GroupedBatcher, GroupLimitError
|
|
7
|
+
from async_batcher.map import MapOutcome
|
|
8
|
+
from async_batcher.observability import BatcherStats, BatchEvent
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AsyncBatcher",
|
|
12
|
+
"MapOutcome",
|
|
13
|
+
"CoalescingBatcher",
|
|
14
|
+
"AsyncBatchException",
|
|
15
|
+
"QueueFullException",
|
|
16
|
+
"BatchEvent",
|
|
17
|
+
"BatcherStats",
|
|
18
|
+
"GroupedBatcher",
|
|
19
|
+
"GroupLimitError",
|
|
20
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Keyword types shared by the public constructor and callable factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Generic, TypedDict, TypeVar
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from concurrent.futures import Executor
|
|
10
|
+
from contextvars import Context
|
|
11
|
+
from numbers import Real
|
|
12
|
+
|
|
13
|
+
from async_batcher.observability import BatchEvent
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LegacyOptions(TypedDict, total=False):
|
|
19
|
+
batch_size: int
|
|
20
|
+
sleep_time: object
|
|
21
|
+
buffering_time: object
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CoreOptions(LegacyOptions, Generic[T], total=False):
|
|
25
|
+
"""Shared options for adapters that declare their own count limit."""
|
|
26
|
+
|
|
27
|
+
max_queue_time: float
|
|
28
|
+
concurrency: int
|
|
29
|
+
max_queue_size: int
|
|
30
|
+
max_batch_cost: float | Real | None
|
|
31
|
+
item_cost: Callable[[T], float | Real] | None
|
|
32
|
+
executor: Executor | None
|
|
33
|
+
on_batch: Callable[[BatchEvent], None] | None
|
|
34
|
+
processor_context: Context | None
|
|
35
|
+
log_exceptions: bool
|
|
36
|
+
discard_cancelled: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BatcherOptions(CoreOptions[T], total=False):
|
|
40
|
+
max_batch_size: int
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import random
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RecordBatchError(Exception):
|
|
12
|
+
"""A record rejected by Kinesis or Firehose after bounded retries.
|
|
13
|
+
|
|
14
|
+
``code`` and ``message`` are the service's error fields. ``attempts`` counts
|
|
15
|
+
calls containing this record, including the initial call. No payload is kept.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, code: str, message: str, attempts: int):
|
|
19
|
+
self.code = code
|
|
20
|
+
self.message = message
|
|
21
|
+
self.attempts = attempts
|
|
22
|
+
super().__init__(f"{code}: {message} (after {attempts} attempt(s))")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_options(max_batch_size: int, max_retries: int, retry_jitter: bool = False) -> None:
|
|
26
|
+
if not 1 <= max_batch_size <= 500:
|
|
27
|
+
raise ValueError("max_batch_size must be between 1 and 500")
|
|
28
|
+
if isinstance(max_retries, bool) or not isinstance(max_retries, int) or max_retries < 0:
|
|
29
|
+
raise ValueError("max_retries must be a non-negative integer")
|
|
30
|
+
if not isinstance(retry_jitter, bool):
|
|
31
|
+
raise TypeError("retry_jitter must be a boolean")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def send_records(
|
|
35
|
+
operation: Callable[..., Awaitable[dict[str, Any]]],
|
|
36
|
+
records: list[dict[str, Any]],
|
|
37
|
+
sizes: list[int],
|
|
38
|
+
*,
|
|
39
|
+
max_bytes: int,
|
|
40
|
+
max_retries: int,
|
|
41
|
+
response_key: str,
|
|
42
|
+
success_keys: tuple[str, ...],
|
|
43
|
+
retry_codes: set[str],
|
|
44
|
+
retry_jitter: bool = False,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> list[dict[str, Any] | Exception]:
|
|
47
|
+
"""Split by service limits and retry only explicitly rejected records."""
|
|
48
|
+
results: list[dict[str, Any] | Exception] = []
|
|
49
|
+
start = 0
|
|
50
|
+
while start < len(records):
|
|
51
|
+
end = start
|
|
52
|
+
size = 0
|
|
53
|
+
while end < len(records) and end - start < 500 and size + sizes[end] <= max_bytes:
|
|
54
|
+
size += sizes[end]
|
|
55
|
+
end += 1
|
|
56
|
+
if end == start:
|
|
57
|
+
raise ValueError("record exceeds the request byte limit")
|
|
58
|
+
pending = list(range(start, end))
|
|
59
|
+
# Every slot is replaced by an acknowledgement or an error below.
|
|
60
|
+
results.extend(RuntimeError("record was not processed") for _ in pending)
|
|
61
|
+
for attempt in range(max_retries + 1):
|
|
62
|
+
try:
|
|
63
|
+
response = await operation(Records=[records[index] for index in pending], **kwargs)
|
|
64
|
+
entries = response.get(response_key)
|
|
65
|
+
if not isinstance(entries, list) or len(entries) != len(pending):
|
|
66
|
+
raise ValueError(f"{response_key} must contain one result per submitted record")
|
|
67
|
+
for entry in entries:
|
|
68
|
+
if not isinstance(entry, dict):
|
|
69
|
+
raise ValueError("record response must be a mapping")
|
|
70
|
+
if "ErrorCode" in entry:
|
|
71
|
+
if (
|
|
72
|
+
not isinstance(entry["ErrorCode"], str)
|
|
73
|
+
or not entry["ErrorCode"]
|
|
74
|
+
or not isinstance(entry.get("ErrorMessage", ""), str)
|
|
75
|
+
):
|
|
76
|
+
raise ValueError("record response contains an invalid error code or message")
|
|
77
|
+
elif not all(isinstance(entry.get(key), str) and entry[key] for key in success_keys):
|
|
78
|
+
raise ValueError("record response lacks an acknowledgement or error code")
|
|
79
|
+
except Exception as error:
|
|
80
|
+
# An earlier response may already have acknowledged other records.
|
|
81
|
+
# Do not replace those outcomes or resend them after a request error.
|
|
82
|
+
for index in pending:
|
|
83
|
+
results[index] = error
|
|
84
|
+
break
|
|
85
|
+
retry = []
|
|
86
|
+
for index, entry in zip(pending, entries, strict=True):
|
|
87
|
+
code = entry.get("ErrorCode")
|
|
88
|
+
if code is None:
|
|
89
|
+
results[index] = entry
|
|
90
|
+
elif code in retry_codes and attempt < max_retries:
|
|
91
|
+
retry.append(index)
|
|
92
|
+
else:
|
|
93
|
+
results[index] = RecordBatchError(code, entry.get("ErrorMessage", ""), attempt + 1)
|
|
94
|
+
if not retry:
|
|
95
|
+
break
|
|
96
|
+
pending = retry
|
|
97
|
+
delay = min(0.1 * 2 ** min(attempt, 6), 5.0)
|
|
98
|
+
await asyncio.sleep(random.uniform(0, delay) if retry_jitter else delay)
|
|
99
|
+
start = end
|
|
100
|
+
return results
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from async_batcher.aws.dynamodb.exceptions import DynamoDBRetryError
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_max_retries(max_retries: int) -> None:
|
|
13
|
+
if isinstance(max_retries, bool) or not isinstance(max_retries, int) or max_retries < 0:
|
|
14
|
+
raise ValueError("max_retries must be a non-negative integer")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def batch_responses(
|
|
18
|
+
operation: Callable[..., Awaitable[dict[str, Any]]],
|
|
19
|
+
request_items: dict[str, Any],
|
|
20
|
+
unprocessed_key: str,
|
|
21
|
+
max_retries: int,
|
|
22
|
+
**kwargs: Any,
|
|
23
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
24
|
+
delay = 0.1
|
|
25
|
+
for attempt in range(max_retries + 1):
|
|
26
|
+
response = await operation(RequestItems=request_items, **kwargs)
|
|
27
|
+
yield response
|
|
28
|
+
request_items = response.get(unprocessed_key, {})
|
|
29
|
+
if not request_items:
|
|
30
|
+
return
|
|
31
|
+
if attempt == max_retries:
|
|
32
|
+
raise DynamoDBRetryError(unprocessed_key, request_items, max_retries)
|
|
33
|
+
await asyncio.sleep(delay)
|
|
34
|
+
delay = min(delay * 2, 5.0)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from async_batcher.exceptions import AsyncBatchException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DynamoDBRetryError(AsyncBatchException):
|
|
9
|
+
"""A batch still contains unprocessed requests after its retry budget is exhausted.
|
|
10
|
+
|
|
11
|
+
Requests omitted from ``unprocessed_items`` may already have succeeded. A failed
|
|
12
|
+
batch write is not rolled back.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, response_key: str, unprocessed_items: dict[str, Any], max_retries: int):
|
|
16
|
+
self.unprocessed_items = unprocessed_items
|
|
17
|
+
super().__init__(f"DynamoDB returned {response_key} after {max_retries} retries")
|