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.
Files changed (51) hide show
  1. async_batcher-0.3.0/PKG-INFO +161 -0
  2. async_batcher-0.3.0/README.md +120 -0
  3. async_batcher-0.3.0/async_batcher/__init__.py +20 -0
  4. async_batcher-0.3.0/async_batcher/_options.py +40 -0
  5. async_batcher-0.3.0/async_batcher/aws/_records.py +100 -0
  6. async_batcher-0.3.0/async_batcher/aws/dynamodb/_retry.py +34 -0
  7. async_batcher-0.3.0/async_batcher/aws/dynamodb/exceptions.py +17 -0
  8. async_batcher-0.3.0/async_batcher/aws/dynamodb/get.py +184 -0
  9. {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/aws/dynamodb/write.py +23 -9
  10. async_batcher-0.3.0/async_batcher/aws/firehose/__init__.py +6 -0
  11. async_batcher-0.3.0/async_batcher/aws/firehose/write.py +63 -0
  12. async_batcher-0.3.0/async_batcher/aws/kinesis/__init__.py +6 -0
  13. async_batcher-0.3.0/async_batcher/aws/kinesis/write.py +100 -0
  14. async_batcher-0.3.0/async_batcher/batcher.py +898 -0
  15. async_batcher-0.3.0/async_batcher/callable.py +88 -0
  16. async_batcher-0.3.0/async_batcher/coalescing.py +176 -0
  17. async_batcher-0.3.0/async_batcher/grouped.py +354 -0
  18. async_batcher-0.3.0/async_batcher/kafka/__init__.py +5 -0
  19. async_batcher-0.3.0/async_batcher/kafka/producer.py +181 -0
  20. async_batcher-0.3.0/async_batcher/map.py +175 -0
  21. async_batcher-0.3.0/async_batcher/ml/keras/model.py +78 -0
  22. async_batcher-0.3.0/async_batcher/ml/sklearn/model.py +83 -0
  23. async_batcher-0.3.0/async_batcher/mongodb/__init__.py +5 -0
  24. async_batcher-0.3.0/async_batcher/mongodb/write.py +81 -0
  25. async_batcher-0.3.0/async_batcher/observability.py +58 -0
  26. async_batcher-0.3.0/async_batcher/pinecone/__init__.py +9 -0
  27. async_batcher-0.3.0/async_batcher/pinecone/vectors.py +117 -0
  28. async_batcher-0.3.0/async_batcher/scylladb/update.py +74 -0
  29. async_batcher-0.3.0/async_batcher/sqlalchemy/__init__.py +0 -0
  30. async_batcher-0.3.0/async_batcher/sqlalchemy/write.py +84 -0
  31. async_batcher-0.3.0/pyproject.toml +147 -0
  32. async_batcher-0.3.0/pyproject.toml.orig +125 -0
  33. async_batcher-0.2.2/PKG-INFO +0 -183
  34. async_batcher-0.2.2/README.md +0 -151
  35. async_batcher-0.2.2/async_batcher/__init__.py +0 -1
  36. async_batcher-0.2.2/async_batcher/aws/dynamodb/get.py +0 -96
  37. async_batcher-0.2.2/async_batcher/batcher.py +0 -243
  38. async_batcher-0.2.2/async_batcher/ml/keras/model.py +0 -50
  39. async_batcher-0.2.2/async_batcher/ml/sklearn/model.py +0 -51
  40. async_batcher-0.2.2/async_batcher/scylladb/update.py +0 -47
  41. async_batcher-0.2.2/async_batcher/sqlalchemy/write.py +0 -60
  42. async_batcher-0.2.2/pyproject.toml +0 -78
  43. {async_batcher-0.2.2 → async_batcher-0.3.0}/LICENSE +0 -0
  44. {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/aws/__init__.py +0 -0
  45. {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/aws/dynamodb/__init__.py +0 -0
  46. {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/exceptions.py +0 -0
  47. {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/ml/__init__.py +0 -0
  48. {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/ml/keras/__init__.py +0 -0
  49. {async_batcher-0.2.2 → async_batcher-0.3.0}/async_batcher/ml/sklearn/__init__.py +0 -0
  50. /async_batcher-0.2.2/async_batcher/scylladb/__init__.py → /async_batcher-0.3.0/async_batcher/py.typed +0 -0
  51. {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")