conduit-connector-sdk 0.1.0.dev1__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.
- conduit/__init__.py +40 -0
- conduit/_batch.py +357 -0
- conduit/_build.py +394 -0
- conduit/_cli.py +85 -0
- conduit/_dispatch.py +80 -0
- conduit/_grpc/__init__.py +46 -0
- conduit/_grpc/_controller.py +83 -0
- conduit/_grpc/adapters.py +188 -0
- conduit/_grpc/config/v1/parameter_pb2.py +43 -0
- conduit/_grpc/config/v1/parameter_pb2.pyi +59 -0
- conduit/_grpc/config/v1/parameter_pb2_grpc.py +4 -0
- conduit/_grpc/connector/v2/destination_pb2.py +113 -0
- conduit/_grpc/connector/v2/destination_pb2.pyi +145 -0
- conduit/_grpc/connector/v2/destination_pb2_grpc.py +418 -0
- conduit/_grpc/connector/v2/source_pb2.py +111 -0
- conduit/_grpc/connector/v2/source_pb2.pyi +140 -0
- conduit/_grpc/connector/v2/source_pb2_grpc.py +428 -0
- conduit/_grpc/connector/v2/specifier_pb2.py +57 -0
- conduit/_grpc/connector/v2/specifier_pb2.pyi +55 -0
- conduit/_grpc/connector/v2/specifier_pb2_grpc.py +81 -0
- conduit/_grpc/opencdc/v1/opencdc_pb2.py +49 -0
- conduit/_grpc/opencdc/v1/opencdc_pb2.pyi +89 -0
- conduit/_grpc/opencdc/v1/opencdc_pb2_grpc.py +4 -0
- conduit/_handshake.py +222 -0
- conduit/_introspect.py +49 -0
- conduit/config.py +551 -0
- conduit/destination.py +435 -0
- conduit/errors.py +412 -0
- conduit/record.py +274 -0
- conduit/schema.py +176 -0
- conduit/serve.py +623 -0
- conduit/source.py +446 -0
- conduit/testing/README.md +16 -0
- conduit/testing/__init__.py +24 -0
- conduit/testing/acceptance.py +446 -0
- conduit/testing/fixtures.py +142 -0
- conduit_connector_sdk-0.1.0.dev1.dist-info/METADATA +245 -0
- conduit_connector_sdk-0.1.0.dev1.dist-info/RECORD +41 -0
- conduit_connector_sdk-0.1.0.dev1.dist-info/WHEEL +4 -0
- conduit_connector_sdk-0.1.0.dev1.dist-info/entry_points.txt +2 -0
- conduit_connector_sdk-0.1.0.dev1.dist-info/licenses/LICENSE +201 -0
conduit/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Python SDK for building Conduit source and destination connectors.
|
|
2
|
+
|
|
3
|
+
Public author-facing surface, per
|
|
4
|
+
``docs/design/20260707-python-connector-sdk.md`` §2/§3: ``Source``,
|
|
5
|
+
``Destination``, ``Record``, ``Change``, ``Operation``, ``BaseConfig``,
|
|
6
|
+
``Field``, ``Specification``, ``serve``, and the connector-facing
|
|
7
|
+
exceptions (``BackoffRetry``, ``BatchWriteError``, ``ConnectorError``).
|
|
8
|
+
|
|
9
|
+
See ``CONTRIBUTING.md`` for the Tier-1 review bar this package is held to.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from conduit.config import BaseConfig, Field, Specification, to_parameters
|
|
15
|
+
from conduit.destination import Destination
|
|
16
|
+
from conduit.errors import BackoffRetry, BatchWriteError, ConnectorError
|
|
17
|
+
from conduit.record import Change, Data, Metadata, Operation, Record
|
|
18
|
+
from conduit.serve import serve
|
|
19
|
+
from conduit.source import Source
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0.dev0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BackoffRetry",
|
|
25
|
+
"BaseConfig",
|
|
26
|
+
"BatchWriteError",
|
|
27
|
+
"Change",
|
|
28
|
+
"ConnectorError",
|
|
29
|
+
"Data",
|
|
30
|
+
"Destination",
|
|
31
|
+
"Field",
|
|
32
|
+
"Metadata",
|
|
33
|
+
"Operation",
|
|
34
|
+
"Record",
|
|
35
|
+
"Source",
|
|
36
|
+
"Specification",
|
|
37
|
+
"__version__",
|
|
38
|
+
"serve",
|
|
39
|
+
"to_parameters",
|
|
40
|
+
]
|
conduit/_batch.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""SDK-level batching middleware: ``sdk.batch.size`` / ``sdk.batch.delay``.
|
|
2
|
+
|
|
3
|
+
Mirrors the Go SDK's batching contract exactly where the two protocols'
|
|
4
|
+
shapes allow, and documents every deliberate divergence at the exact point
|
|
5
|
+
it happens. The Go-side references this module reproduces:
|
|
6
|
+
|
|
7
|
+
- ``internal/batcher.go`` (``conduit-connector-sdk``): the generic
|
|
8
|
+
size/delay-triggered batch accumulator both ``Destination``/``Source``
|
|
9
|
+
batching build on.
|
|
10
|
+
- ``destination_middleware.go``'s ``DestinationWithBatch``/
|
|
11
|
+
``destinationWithBatch``/``writeStrategyBatch``: destination-side wiring
|
|
12
|
+
-- ``sdk.batch.size``/``sdk.batch.delay`` are injected config keys (not
|
|
13
|
+
authored by the connector), and batching only activates when
|
|
14
|
+
``BatchSize > 1 || BatchDelay > 0`` (``destination.go:182``) -- a
|
|
15
|
+
``size`` of 0 *or* 1 with no delay is passthrough: no accumulation, no
|
|
16
|
+
background task, one wire message in for one wire message out.
|
|
17
|
+
- ``source_middleware.go``'s ``SourceWithBatch``/``sourceWithBatch``: the
|
|
18
|
+
same two keys on the source side, with the identical activation
|
|
19
|
+
threshold (``source_middleware.go:709``) and the same "no read-ahead
|
|
20
|
+
task at all when batching is off" property this module's
|
|
21
|
+
:func:`collect_batches` preserves (callers only invoke it when
|
|
22
|
+
:attr:`BatchConfig.enabled` is true; the disabled case never even
|
|
23
|
+
imports this module's event loop machinery into the request path -- see
|
|
24
|
+
``conduit.source``/``conduit.destination`` for the branch point).
|
|
25
|
+
|
|
26
|
+
**Deliberate divergence from Go, documented here rather than silently:**
|
|
27
|
+
the Go SDK's real advantage on the source side is an optional
|
|
28
|
+
``ReadN(ctx, n) ([]Record, error)`` a connector can implement for a
|
|
29
|
+
genuinely batched read (``collectWithReadN``); absent that override, Go
|
|
30
|
+
itself falls back to the single-record ``collectWithRead`` path this
|
|
31
|
+
module's :func:`collect_batches` is modeled on. This SDK's author-facing
|
|
32
|
+
``Source`` ABC has no ``read_batch``/``ReadN`` override point at all yet
|
|
33
|
+
(explicitly deferred past v0.19 core, see
|
|
34
|
+
``docs/design/20260707-python-connector-sdk.md`` §2.6) -- so every Python
|
|
35
|
+
connector using ``sdk.batch.*`` today gets Go's *fallback* behavior
|
|
36
|
+
(buffer individual ``read()`` calls into batches), never Go's optimized
|
|
37
|
+
path. This is a real, current capability gap versus Go, not an oversight:
|
|
38
|
+
adding ``read_batch`` is tracked as its own follow-up, at which point
|
|
39
|
+
:func:`collect_batches` would gain a second, ``ReadN``-shaped caller the
|
|
40
|
+
same way Go's ``collectWithReadN`` sits alongside ``collectWithRead``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import asyncio
|
|
46
|
+
import datetime
|
|
47
|
+
from collections.abc import AsyncIterator, Mapping
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
from typing import TypeVar
|
|
50
|
+
|
|
51
|
+
import conduit._grpc # noqa: F401 -- sets up sys.path, see conduit._grpc.__init__
|
|
52
|
+
from conduit.config import parse_go_duration
|
|
53
|
+
from conduit.errors import (
|
|
54
|
+
INVALID_BATCH_DELAY_CODE,
|
|
55
|
+
INVALID_BATCH_SIZE_CODE,
|
|
56
|
+
ConfigFieldError,
|
|
57
|
+
ConfigValidationError,
|
|
58
|
+
)
|
|
59
|
+
from config.v1 import parameter_pb2 as _parameter_pb2
|
|
60
|
+
|
|
61
|
+
T = TypeVar("T")
|
|
62
|
+
|
|
63
|
+
BATCH_SIZE_KEY = "sdk.batch.size"
|
|
64
|
+
"""Wire config key -- matches Go's `json:"sdk.batch.size"` struct tag exactly."""
|
|
65
|
+
|
|
66
|
+
BATCH_DELAY_KEY = "sdk.batch.delay"
|
|
67
|
+
"""Wire config key -- matches Go's `json:"sdk.batch.delay"` struct tag exactly."""
|
|
68
|
+
|
|
69
|
+
_ZERO_DELAY = datetime.timedelta(0)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(slots=True, frozen=True)
|
|
73
|
+
class BatchConfig:
|
|
74
|
+
"""Parsed ``sdk.batch.size``/``sdk.batch.delay`` for one connector instance.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
size: maximum number of items per batch before it's flushed early.
|
|
78
|
+
``0`` means "no size limit" (Go's default, ``validate:"gt=-1"``
|
|
79
|
+
i.e. any non-negative value is legal) -- a batch then only
|
|
80
|
+
flushes on the delay timer, which can produce arbitrarily
|
|
81
|
+
large batches for a fast source/high-throughput destination if
|
|
82
|
+
``delay`` is set without ``size`` (Go warns about exactly this
|
|
83
|
+
combination at ``Open`` time on both the source and
|
|
84
|
+
destination side; this SDK does not currently have an
|
|
85
|
+
established logging channel to reproduce that warning through
|
|
86
|
+
-- see the module docstring's divergence note -- so it is
|
|
87
|
+
documented here instead).
|
|
88
|
+
delay: maximum time an incomplete batch waits before flushing.
|
|
89
|
+
``timedelta(0)`` means "no delay limit" -- a batch then only
|
|
90
|
+
flushes once ``size`` items have accumulated, which can wait
|
|
91
|
+
indefinitely for a slow/low-throughput source if ``size`` is
|
|
92
|
+
set without ``delay`` (the same Go warning, mirrored the same
|
|
93
|
+
way).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
size: int = 0
|
|
97
|
+
delay: datetime.timedelta = _ZERO_DELAY
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def enabled(self) -> bool:
|
|
101
|
+
"""Whether batching is actually active for this config.
|
|
102
|
+
|
|
103
|
+
The exact Go activation threshold, reproduced verbatim rather than
|
|
104
|
+
rounded to a more "intuitive" ``size >= 1``:
|
|
105
|
+
``destination.go:182``'s ``batchConfig.BatchSize > 1 ||
|
|
106
|
+
batchConfig.BatchDelay > 0`` and ``source_middleware.go:709``'s
|
|
107
|
+
identical condition. A ``size`` of exactly 1 with no delay is
|
|
108
|
+
passthrough, same as ``size == 0`` -- a "batch" of one record is
|
|
109
|
+
never observably different from no batching, so Go doesn't pay for
|
|
110
|
+
the accumulator machinery in that case, and neither does this SDK.
|
|
111
|
+
"""
|
|
112
|
+
return self.size > 1 or self.delay > _ZERO_DELAY
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def sdk_batch_parameters() -> dict[str, _parameter_pb2.Parameter]:
|
|
116
|
+
"""The two ``sdk.batch.*`` parameters every connector gets for free.
|
|
117
|
+
|
|
118
|
+
Mirrors Go's ``DestinationWithBatch``/``SourceWithBatch`` struct tags
|
|
119
|
+
(``json:"sdk.batch.size" default:"0" validate:"gt=-1"`` /
|
|
120
|
+
``json:"sdk.batch.delay" default:"0"``), which Go's ``paramgen`` merges
|
|
121
|
+
into *every* connector's ``Specify`` parameter map automatically via
|
|
122
|
+
``DefaultDestinationMiddleware``/``DefaultSourceMiddleware`` embedding
|
|
123
|
+
-- a Go connector author never has to declare these fields themselves,
|
|
124
|
+
and Conduit's own tooling (CLI, UI) expects them present in every
|
|
125
|
+
connector's advertised parameters, not just ones that happen to
|
|
126
|
+
declare matching fields on their own config struct.
|
|
127
|
+
|
|
128
|
+
``conduit.serve`` merges this into both ``source_params``/
|
|
129
|
+
``destination_params`` for every registered connector, matching that
|
|
130
|
+
behavior -- see ``conduit.serve._build_plugin_server``.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A mapping with exactly the two keys :data:`BATCH_SIZE_KEY`/
|
|
134
|
+
:data:`BATCH_DELAY_KEY`.
|
|
135
|
+
"""
|
|
136
|
+
return {
|
|
137
|
+
BATCH_SIZE_KEY: _parameter_pb2.Parameter(
|
|
138
|
+
default="0",
|
|
139
|
+
description="Maximum size of batch before it gets written/read.",
|
|
140
|
+
type=_parameter_pb2.Parameter.TYPE_INT,
|
|
141
|
+
validations=[
|
|
142
|
+
_parameter_pb2.Validation(
|
|
143
|
+
type=_parameter_pb2.Validation.TYPE_GREATER_THAN, value="-1"
|
|
144
|
+
)
|
|
145
|
+
],
|
|
146
|
+
),
|
|
147
|
+
BATCH_DELAY_KEY: _parameter_pb2.Parameter(
|
|
148
|
+
default="0s",
|
|
149
|
+
description="Maximum delay before an incomplete batch is written/read.",
|
|
150
|
+
type=_parameter_pb2.Parameter.TYPE_DURATION,
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def extract_batch_config(config: Mapping[str, str]) -> tuple[BatchConfig, dict[str, str]]:
|
|
156
|
+
"""Pull ``sdk.batch.size``/``sdk.batch.delay`` out of a raw ``Configure`` config map.
|
|
157
|
+
|
|
158
|
+
Called by ``conduit.source._SourceServicer.Configure``/
|
|
159
|
+
``conduit.destination._DestinationServicer.Configure`` *before* the
|
|
160
|
+
remaining map is handed to the connector's own
|
|
161
|
+
:class:`~conduit.config.BaseConfig` subclass for
|
|
162
|
+
``model_validate`` -- these two keys are SDK-owned, never fields the
|
|
163
|
+
connector author declares themselves (matching Go: they live on
|
|
164
|
+
``DestinationWithBatch``/``SourceWithBatch``, not the connector's own
|
|
165
|
+
config struct).
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
config: the raw ``map<string, string>`` from the ``Configure`` RPC.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
A ``(BatchConfig, remaining)`` pair: the parsed batch config, and a
|
|
172
|
+
**copy** of ``config`` with the two batch keys removed, ready to
|
|
173
|
+
pass to the connector's own config class.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ConfigValidationError: if either value is present and malformed --
|
|
177
|
+
``sdk.batch.size`` must parse as a non-negative integer;
|
|
178
|
+
``sdk.batch.delay`` must parse as a non-negative
|
|
179
|
+
Go-duration-syntax string (:func:`conduit.config.parse_go_duration`).
|
|
180
|
+
Both fields are checked (not fail-fast on the first bad one),
|
|
181
|
+
matching :class:`~conduit.errors.ConfigValidationError`'s
|
|
182
|
+
"exhaustive, not one-at-a-time" design elsewhere in this SDK.
|
|
183
|
+
"""
|
|
184
|
+
remaining = dict(config)
|
|
185
|
+
size_raw = remaining.pop(BATCH_SIZE_KEY, None)
|
|
186
|
+
delay_raw = remaining.pop(BATCH_DELAY_KEY, None)
|
|
187
|
+
|
|
188
|
+
errors: list[ConfigFieldError] = []
|
|
189
|
+
|
|
190
|
+
size = 0
|
|
191
|
+
if size_raw: # narrows out `None` and `""` -- see the two ValueError branches below
|
|
192
|
+
try:
|
|
193
|
+
size = int(size_raw)
|
|
194
|
+
if size < 0:
|
|
195
|
+
raise ValueError("must be >= 0")
|
|
196
|
+
except ValueError:
|
|
197
|
+
errors.append(
|
|
198
|
+
ConfigFieldError(
|
|
199
|
+
field=BATCH_SIZE_KEY,
|
|
200
|
+
code=INVALID_BATCH_SIZE_CODE,
|
|
201
|
+
message=(
|
|
202
|
+
f"{BATCH_SIZE_KEY!r} must be a non-negative integer, got {size_raw!r}"
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
delay = _ZERO_DELAY
|
|
208
|
+
if delay_raw: # narrows out `None` and `""`, same as the size branch above
|
|
209
|
+
try:
|
|
210
|
+
delay = parse_go_duration(delay_raw)
|
|
211
|
+
if delay < _ZERO_DELAY:
|
|
212
|
+
raise ValueError("must be >= 0")
|
|
213
|
+
except ValueError:
|
|
214
|
+
errors.append(
|
|
215
|
+
ConfigFieldError(
|
|
216
|
+
field=BATCH_DELAY_KEY,
|
|
217
|
+
code=INVALID_BATCH_DELAY_CODE,
|
|
218
|
+
message=(
|
|
219
|
+
f"{BATCH_DELAY_KEY!r} must be a non-negative Go-duration-syntax "
|
|
220
|
+
f"string (e.g. '5s', '1h30m'), got {delay_raw!r}"
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if errors:
|
|
226
|
+
raise ConfigValidationError(errors)
|
|
227
|
+
|
|
228
|
+
return BatchConfig(size=size, delay=delay), remaining
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
_STOP = object()
|
|
232
|
+
"""Sentinel distinguishing "the source iterator ended" from a real item of type ``T``."""
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
async def collect_batches(
|
|
236
|
+
items: AsyncIterator[T],
|
|
237
|
+
*,
|
|
238
|
+
config: BatchConfig,
|
|
239
|
+
) -> AsyncIterator[list[T]]:
|
|
240
|
+
"""Group items from ``items`` into ``config``-bounded batches.
|
|
241
|
+
|
|
242
|
+
Shared by both batching call sites -- the destination write path
|
|
243
|
+
(``conduit.destination._DestinationServicer``, over a per-record stream
|
|
244
|
+
flattened from incoming ``Run`` request batches) and the source
|
|
245
|
+
read-ahead path (``conduit.source._SourceServicer``, directly over the
|
|
246
|
+
existing backoff-aware ``_read_loop()``) -- because both reduce to the
|
|
247
|
+
same shape once the per-item stream exists: accumulate until ``size``
|
|
248
|
+
items are buffered or ``delay`` has elapsed since the first buffered
|
|
249
|
+
item, whichever comes first, exactly Go's ``select``-over-(channel-recv,
|
|
250
|
+
timer) shape in ``sourceWithBatch.collectWithRead``/
|
|
251
|
+
``internal.Batcher.Enqueue``. ``asyncio.wait`` over two tasks --
|
|
252
|
+
"the next item" and "the delay timer" -- is this module's equivalent of
|
|
253
|
+
that ``select``.
|
|
254
|
+
|
|
255
|
+
Only called when ``config.enabled`` is true; callers keep the exact
|
|
256
|
+
passthrough branch (no task, no timer, one item in for one batch of one
|
|
257
|
+
item out... actually one item in for one *record* out, unbatched) in
|
|
258
|
+
their own code, matching the Go SDK's own ``if BatchSize > 1 ||
|
|
259
|
+
BatchDelay > 0`` branch point (``destination.go:182``,
|
|
260
|
+
``source_middleware.go:709``) rather than hiding it inside this
|
|
261
|
+
function.
|
|
262
|
+
|
|
263
|
+
**Flush-on-end, not flush-in-``finally``:** when ``items`` ends
|
|
264
|
+
(``anext`` yields the internal stop sentinel), any non-empty buffered
|
|
265
|
+
remainder is yielded once more before this generator returns --
|
|
266
|
+
matching Go's ``Stop()``-triggered ``Flush()``
|
|
267
|
+
(``destination.go:271``): batching must never silently drop
|
|
268
|
+
buffered-but-unflushed items on a controlled stream end, per invariant
|
|
269
|
+
3 (at-least-once is the floor). This flush happens in the main
|
|
270
|
+
``try`` body's own control flow, deliberately **not** in a ``finally``
|
|
271
|
+
block: an async generator's ``finally`` also runs on ``GeneratorExit``
|
|
272
|
+
(e.g. if the caller's own enclosing ``async for`` is abandoned early
|
|
273
|
+
due to cancellation), and ``yield``ing there would raise
|
|
274
|
+
``RuntimeError: async generator ignored GeneratorExit``. This is a
|
|
275
|
+
real, accepted narrow-window limitation, not an oversight: an
|
|
276
|
+
externally-cancelled ``Run`` (e.g. the whole RPC torn down by the
|
|
277
|
+
framework, not a normal drain) can lose a buffered-but-unflushed
|
|
278
|
+
remainder the same way an in-flight, never-acked write is already an
|
|
279
|
+
accepted loss elsewhere in this SDK on a hard stop (see
|
|
280
|
+
``conduit.destination._DestinationServicer.drain``'s own "unavoidable,
|
|
281
|
+
narrow window" note for the same class of tradeoff). The controlled
|
|
282
|
+
shutdown path -- ``drain()`` setting the stop event, which is what
|
|
283
|
+
normally ends ``items`` -- goes through the flush-then-return path
|
|
284
|
+
above, not this one; that's the path invariant 7 (graceful shutdown by
|
|
285
|
+
default) actually depends on.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
items: the per-item async stream to batch. Ending it (raising
|
|
289
|
+
``StopAsyncIteration``) is this function's only normal
|
|
290
|
+
termination signal -- it does not itself watch any stop/cancel
|
|
291
|
+
event; callers arrange for ``items`` to end when they want
|
|
292
|
+
batching to stop (see ``conduit.source``/``conduit.destination``
|
|
293
|
+
for how each wires that through their own ``_stop_event``).
|
|
294
|
+
config: the batch thresholds. Caller-checked ``.enabled`` (see
|
|
295
|
+
above) -- this function does not re-check it.
|
|
296
|
+
|
|
297
|
+
Yields:
|
|
298
|
+
Non-empty lists of ``T``, each a complete batch (either
|
|
299
|
+
size-triggered, delay-triggered, or the final end-of-stream
|
|
300
|
+
remainder).
|
|
301
|
+
"""
|
|
302
|
+
buffer: list[T] = []
|
|
303
|
+
delay_task: asyncio.Task[None] | None = None
|
|
304
|
+
next_task: asyncio.Task[T | object] | None = None
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
while True:
|
|
308
|
+
if next_task is None:
|
|
309
|
+
next_task = asyncio.ensure_future(anext(items, _STOP))
|
|
310
|
+
waiters: set[asyncio.Task[object]] = {next_task}
|
|
311
|
+
if delay_task is not None:
|
|
312
|
+
waiters.add(delay_task)
|
|
313
|
+
|
|
314
|
+
await asyncio.wait(waiters, return_when=asyncio.FIRST_COMPLETED)
|
|
315
|
+
|
|
316
|
+
if delay_task is not None and delay_task.done():
|
|
317
|
+
delay_task = None
|
|
318
|
+
if buffer:
|
|
319
|
+
yield buffer
|
|
320
|
+
buffer = []
|
|
321
|
+
# `next_task` may or may not be done yet -- loop back and
|
|
322
|
+
# `asyncio.wait` on whatever's still pending; a task that's
|
|
323
|
+
# already done resolves immediately on the next `wait`.
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
if next_task.done():
|
|
327
|
+
item = next_task.result()
|
|
328
|
+
next_task = None
|
|
329
|
+
if item is _STOP:
|
|
330
|
+
if buffer:
|
|
331
|
+
yield buffer
|
|
332
|
+
return
|
|
333
|
+
buffer.append(item) # type: ignore[arg-type] # narrowed: item is not _STOP
|
|
334
|
+
if config.size > 0 and len(buffer) >= config.size:
|
|
335
|
+
if delay_task is not None:
|
|
336
|
+
delay_task.cancel()
|
|
337
|
+
delay_task = None
|
|
338
|
+
yield buffer
|
|
339
|
+
buffer = []
|
|
340
|
+
elif config.delay > _ZERO_DELAY and delay_task is None:
|
|
341
|
+
delay_task = asyncio.ensure_future(asyncio.sleep(config.delay.total_seconds()))
|
|
342
|
+
finally:
|
|
343
|
+
# Cleanup only -- deliberately no `yield` here, see docstring.
|
|
344
|
+
if next_task is not None and not next_task.done():
|
|
345
|
+
next_task.cancel()
|
|
346
|
+
if delay_task is not None and not delay_task.done():
|
|
347
|
+
delay_task.cancel()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
__all__ = [
|
|
351
|
+
"BATCH_DELAY_KEY",
|
|
352
|
+
"BATCH_SIZE_KEY",
|
|
353
|
+
"BatchConfig",
|
|
354
|
+
"collect_batches",
|
|
355
|
+
"extract_batch_config",
|
|
356
|
+
"sdk_batch_parameters",
|
|
357
|
+
]
|