b24api 2.0.0__tar.gz → 2.2.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.
- {b24api-2.0.0/b24api.egg-info → b24api-2.2.0}/PKG-INFO +93 -10
- {b24api-2.0.0 → b24api-2.2.0}/README.md +92 -9
- b24api-2.2.0/b24api/__init__.py +253 -0
- b24api-2.2.0/b24api/_audit.py +78 -0
- b24api-2.2.0/b24api/_client_traversal.py +180 -0
- b24api-2.2.0/b24api/_error_types.py +31 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/_stream.py +30 -7
- {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/engine.py +200 -73
- {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/facade.py +7 -1
- {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/stream.py +1 -5
- {b24api-2.0.0 → b24api-2.2.0}/b24api/cli.py +62 -3
- {b24api-2.0.0 → b24api-2.2.0}/b24api/cli_contract.py +116 -9
- {b24api-2.0.0 → b24api-2.2.0}/b24api/client.py +113 -121
- b24api-2.2.0/b24api/contracts/__init__.py +211 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/dispatch.py +7 -0
- b24api-2.2.0/b24api/contracts/json.py +139 -0
- b24api-2.2.0/b24api/contracts/keyset_capability.py +246 -0
- b24api-2.2.0/b24api/contracts/keyset_execution.py +233 -0
- b24api-2.2.0/b24api/contracts/page.py +70 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/policy.py +80 -2
- {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/reference.py +3 -0
- b24api-2.2.0/b24api/contracts/report.py +393 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/request.py +147 -6
- {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/response.py +81 -0
- b24api-2.2.0/b24api/contracts/traversal.py +268 -0
- b24api-2.2.0/b24api/contracts/wire.py +85 -0
- b24api-2.2.0/b24api/encoding.py +40 -0
- b24api-2.2.0/b24api/errors.py +507 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/context.py +28 -8
- b24api-2.2.0/b24api/execution/executor.py +700 -0
- b24api-2.2.0/b24api/execution/failure.py +131 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/snapshot.py +7 -1
- {b24api-2.0.0 → b24api-2.2.0}/b24api/references/binding.py +53 -26
- b24api-2.2.0/b24api/references/dispatch.py +689 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/references/facade.py +67 -22
- {b24api-2.0.0 → b24api-2.2.0}/b24api/references/fanout.py +6 -1
- {b24api-2.0.0 → b24api-2.2.0}/b24api/references/outcome.py +27 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/references/scheduler.py +199 -15
- {b24api-2.0.0 → b24api-2.2.0}/b24api/references/stream.py +25 -6
- {b24api-2.0.0 → b24api-2.2.0}/b24api/references/support.py +0 -6
- b24api-2.2.0/b24api/testing/__init__.py +17 -0
- b24api-2.2.0/b24api/testing/_isolation.py +81 -0
- b24api-2.2.0/b24api/testing/transport.py +373 -0
- b24api-2.2.0/b24api/transport/__init__.py +6 -0
- b24api-2.2.0/b24api/transport/base.py +158 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/transport/httpx.py +54 -12
- {b24api-2.0.0 → b24api-2.2.0}/b24api/transport/protocol.py +1 -1
- {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/counted.py +16 -9
- {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/counted_batch.py +117 -41
- {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/cursor.py +33 -18
- b24api-2.2.0/b24api/traversal/driver.py +748 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/facade.py +130 -32
- {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/identity.py +31 -21
- b24api-2.2.0/b24api/traversal/keyset.py +64 -0
- b24api-2.2.0/b24api/traversal/keyset_auto.py +236 -0
- b24api-2.2.0/b24api/traversal/keyset_capability.py +392 -0
- b24api-2.2.0/b24api/traversal/keyset_costs.py +185 -0
- b24api-2.2.0/b24api/traversal/keyset_eligibility.py +159 -0
- b24api-2.2.0/b24api/traversal/keyset_fast_plan.py +288 -0
- b24api-2.2.0/b24api/traversal/keyset_fast_stream.py +274 -0
- b24api-2.2.0/b24api/traversal/keyset_observation.py +250 -0
- b24api-2.2.0/b24api/traversal/keyset_partition.py +27 -0
- b24api-2.2.0/b24api/traversal/keyset_range.py +80 -0
- b24api-2.2.0/b24api/traversal/keyset_reporting.py +111 -0
- b24api-2.2.0/b24api/traversal/keyset_scheduler.py +626 -0
- b24api-2.2.0/b24api/traversal/keyset_step.py +138 -0
- b24api-2.2.0/b24api/traversal/keyset_transaction_contract.py +108 -0
- b24api-2.2.0/b24api/traversal/keyset_transactions.py +385 -0
- b24api-2.2.0/b24api/traversal/keyset_verifier.py +498 -0
- b24api-2.2.0/b24api/traversal/ordered_admission.py +182 -0
- b24api-2.2.0/b24api/traversal/page_adaptation.py +90 -0
- b24api-2.2.0/b24api/traversal/page_validation.py +255 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/plans.py +25 -10
- b24api-2.2.0/b24api/traversal/sequential.py +199 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/stream.py +24 -8
- b24api-2.2.0/b24api/traversal/values.py +219 -0
- {b24api-2.0.0 → b24api-2.2.0/b24api.egg-info}/PKG-INFO +93 -10
- {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/SOURCES.txt +30 -0
- b24api-2.0.0/b24api/__init__.py +0 -134
- b24api-2.0.0/b24api/contracts/__init__.py +0 -107
- b24api-2.0.0/b24api/contracts/json.py +0 -77
- b24api-2.0.0/b24api/contracts/report.py +0 -137
- b24api-2.0.0/b24api/contracts/traversal.py +0 -140
- b24api-2.0.0/b24api/errors.py +0 -304
- b24api-2.0.0/b24api/execution/executor.py +0 -339
- b24api-2.0.0/b24api/references/dispatch.py +0 -419
- b24api-2.0.0/b24api/transport/__init__.py +0 -6
- b24api-2.0.0/b24api/transport/base.py +0 -56
- b24api-2.0.0/b24api/traversal/driver.py +0 -414
- b24api-2.0.0/b24api/traversal/keyset.py +0 -71
- b24api-2.0.0/b24api/traversal/sequential.py +0 -149
- b24api-2.0.0/b24api/traversal/values.py +0 -154
- {b24api-2.0.0 → b24api-2.2.0}/LICENSE +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/MANIFEST.in +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/__init__.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/logical.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/outcome.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/command.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/stream.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/__init__.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/cleanup.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/rate.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/redaction.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/references/__init__.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/settings.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/__init__.py +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/dependency_links.txt +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/entry_points.txt +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/requires.txt +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/top_level.txt +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/pyproject.toml +0 -0
- {b24api-2.0.0 → b24api-2.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: b24api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Bitrix24 API
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -43,6 +43,15 @@ idempotent and closes active streams before the owned transport.
|
|
|
43
43
|
Use `call()` for detached decoded JSON and `call_response()` when you also need the immutable
|
|
44
44
|
response envelope: `result`, `total`, `next`, timing and bounded diagnostic evidence.
|
|
45
45
|
|
|
46
|
+
Use `call_bytes()` when a successful method response is a file rather than a Bitrix JSON envelope.
|
|
47
|
+
The operation is explicit and never hides malformed JSON by falling back to bytes.
|
|
48
|
+
|
|
49
|
+
<!-- tested: tests/client_findings_3_test.py::test_binary_call_returns_every_success_byte_without_json_sniffing -->
|
|
50
|
+
```python
|
|
51
|
+
archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE))
|
|
52
|
+
payload = archive.body
|
|
53
|
+
```
|
|
54
|
+
|
|
46
55
|
<!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
|
|
47
56
|
```python
|
|
48
57
|
from b24api import ReplaySafety
|
|
@@ -130,20 +139,30 @@ more specialized mechanics have explicit names and explicit preconditions.
|
|
|
130
139
|
|
|
131
140
|
| Operation | Use it when | Network mechanics | Completion proof |
|
|
132
141
|
|---|---|---|---|
|
|
133
|
-
| `iter_list` | The method supports ordinary offset pagination. |
|
|
142
|
+
| `iter_list` | The method supports ordinary offset pagination. | Sequential requests follow server `next` (the next offset). Ordinary counted Bitrix list endpoints do server-side COUNT for `total` plus LIMIT/OFFSET page retrieval. | Continuation and empty terminal page; add identity for duplicate detection. |
|
|
134
143
|
| `iter_list_counted` | The first response provides an exact filtered `total` and stable offset pages. | Head page is direct; all known tail offsets are grouped into physical Bitrix batches. | Exact total, ranges and identities. |
|
|
135
|
-
| `iter_list_keyset` | The method may omit `total`, but reliably supports ordering and filtering by a unique identity. |
|
|
144
|
+
| `iter_list_keyset` | The method may omit `total`, but reliably supports ordering and filtering by a unique integer identity. | Auto by default: it plans first, then selects boundary-only, sequential, range, or partitioned execution; runtime sends no diagnostic canaries. | Caller-asserted keyset contract, strict monotonic identity, active bound validation, and terminal empty confirmation. |
|
|
136
145
|
| `iter_list_cursor` | Each next request depends on a cursor from the previous response. | Sequential dependent cursor requests. | Strict unique monotonic cursor and empty terminal page. |
|
|
146
|
+
| `iter_cursors` | One or many parent-bound cursor traversals need correlation and shared batching. | Lazy per-parent drivers share the reference batch queue; each binding may have `start_cursor`. | Isolated strict cursor progress and terminal event per binding. |
|
|
137
147
|
| `iter_references` | The same list method must run for many parent parameter sets, such as comments per owner or messages per chat. | Bindings are scheduled with direct or physical-batch dispatch; each binding has its own traversal state. | Per-binding rows, completion/failure and caller correlation. |
|
|
138
148
|
|
|
139
149
|
`page_size` is a local decoded-page cap. It is sent to Bitrix only when you provide the endpoint's
|
|
140
150
|
exact `limit_path`; the client never guesses method-specific parameter names.
|
|
141
151
|
|
|
152
|
+
### List traversal comparison
|
|
153
|
+
|
|
154
|
+

|
|
155
|
+
|
|
142
156
|
### Sequential offset
|
|
143
157
|
|
|
144
158
|
This is the canonical default. It follows the `next` returned by the server and confirms the end
|
|
145
|
-
with an empty page.
|
|
146
|
-
a separate count
|
|
159
|
+
with an empty page. For ordinary counted Bitrix list endpoints, `next` is an offset for the next
|
|
160
|
+
LIMIT/OFFSET page, not a keyset cursor. Producing `total` involves a separate server-side count
|
|
161
|
+
query in addition to retrieving the page. These database operations are performed inside the same
|
|
162
|
+
REST request: the client does not issue an additional HTTP call just for the count. This distinction
|
|
163
|
+
matters for performance: not making a separate HTTP count call does **not** mean avoiding server-side
|
|
164
|
+
COUNT work. `iter_list` does not suppress that work; a returned `total` is observational and does not
|
|
165
|
+
control this strategy's completion. Exact database implementation is endpoint-specific.
|
|
147
166
|
|
|
148
167
|
<!-- tested: tests/client_v2_test.py::test_iter_list_is_sequential_mechanics_only_and_report_is_post_cleanup -->
|
|
149
168
|
```python
|
|
@@ -169,6 +188,24 @@ async with stream:
|
|
|
169
188
|
Without `identity`, successful exhaustion is reported as `MECHANICS_ONLY`: pagination completed,
|
|
170
189
|
but the client cannot prove that the portal did not duplicate or substitute rows.
|
|
171
190
|
|
|
191
|
+
Mapping-backed collections are explicit as well. `MAPPING_VALUES` yields values from a selected
|
|
192
|
+
mapping in insertion order; `MAPPING_VALUES_OR_EMPTY` additionally accepts only an empty terminal
|
|
193
|
+
sequence and records that degradation in the operation report.
|
|
194
|
+
|
|
195
|
+
<!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
|
|
196
|
+
```python
|
|
197
|
+
from b24api import ResultCollectionShape
|
|
198
|
+
|
|
199
|
+
stream = client.iter_list(
|
|
200
|
+
Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
|
|
201
|
+
selector=ResultSelector(("items",)),
|
|
202
|
+
collection_shape=ResultCollectionShape.MAPPING_VALUES,
|
|
203
|
+
)
|
|
204
|
+
async with stream:
|
|
205
|
+
async for value in stream:
|
|
206
|
+
consume(value)
|
|
207
|
+
```
|
|
208
|
+
|
|
172
209
|
### Counted, physically batched tail
|
|
173
210
|
|
|
174
211
|
The first direct page must contain an exact filtered `total` and, when more rows exist, `next`.
|
|
@@ -189,13 +226,48 @@ stream = client.iter_list_counted(
|
|
|
189
226
|
Use it only when `total` is exact for the supplied filter and offset pages are stable. Any missing
|
|
190
227
|
range, overlap, duplicate identity or total contradiction raises `IncompleteTraversalError`.
|
|
191
228
|
|
|
192
|
-
|
|
229
|
+
Physical batching reduces HTTP exchanges, but does not suppress server-side COUNT in ordinary
|
|
230
|
+
counted list subrequests. Each command still performs its own offset page retrieval and associated
|
|
231
|
+
total calculation on the server. Do not confuse batching these commands with a no-count traversal.
|
|
193
232
|
|
|
194
|
-
|
|
195
|
-
identity boundary such as `filter[>ID]`. It is intentionally sequential because a future boundary
|
|
196
|
-
cannot be known safely before the preceding page arrives.
|
|
233
|
+
### No-count keyset
|
|
197
234
|
|
|
198
|
-
|
|
235
|
+
Keyset traversal uses automatic execution by default. Omitting `execution` is equivalent to
|
|
236
|
+
`AutoKeysetExecution(StableIntegerKeysetContract())`: the client captures both ordered boundaries,
|
|
237
|
+
then selects boundary-only, sequential, range, or partitioned execution from the observed geometry
|
|
238
|
+
and available policy capacity. Planning completes before any row is emitted, so partial consumption
|
|
239
|
+
still pays that barrier cost. A sequential selection made by auto is a cost decision; a failed or
|
|
240
|
+
contradictory fast plan is never silently restarted as sequential.
|
|
241
|
+
|
|
242
|
+
By using the default, the caller asserts that the endpoint has a stable, unique integer key, honors
|
|
243
|
+
strict numeric bounds and ordering, and satisfies empty-confirmation completion. Concurrent mutation
|
|
244
|
+
outside the captured middle is handled by the finishing sweep; mutation inside it is outside this
|
|
245
|
+
assertion. Pass `SequentialKeysetExecution()` explicitly when an endpoint cannot satisfy the fast
|
|
246
|
+
contract or when the previous request-by-request behavior is required.
|
|
247
|
+
|
|
248
|
+
Static incompatibility with the auto contract raises `CapabilityError` from the
|
|
249
|
+
`iter_list_keyset(...)` call before iteration begins. A portal that accepts but contradicts the
|
|
250
|
+
declared controls fails before emission with `IncompleteTraversalError`; auto never restarts that
|
|
251
|
+
operation silently. `KeysetTraversal` inside reference traversal remains sequential-only. The
|
|
252
|
+
terminal report now includes `keyset_execution` for omitted-execution keyset calls so consumers can
|
|
253
|
+
see the requested and selected plan.
|
|
254
|
+
|
|
255
|
+
Use `await client.verify_keyset_capability(...)` as a development/CI/staging guard on a stable
|
|
256
|
+
representative fixture. It performs five strict-bound checks and returns only a `VERIFIED` report;
|
|
257
|
+
unsupported and inconclusive verdicts raise `KeysetCapabilityError`. The ordinary
|
|
258
|
+
`iter_list_keyset()` remains a separate caller-asserted operation with zero verifier canaries and
|
|
259
|
+
may emit a partial prefix before a late endpoint contradiction is detected.
|
|
260
|
+
|
|
261
|
+
Every list operation also accepts an immutable `PageAdapter` strategy. The adapter synchronously
|
|
262
|
+
maps selected frozen items using sibling result metadata while preserving cardinality, order and
|
|
263
|
+
configured identities. The identity adapter is the default and preserves existing JSON output.
|
|
264
|
+
|
|
265
|
+
An advisory `total` may only raise an automatic cost estimate and never proves completion. Reports
|
|
266
|
+
record the selected strategy and reason: unbounded auto continuation has the same
|
|
267
|
+
`ordered_prefix_only` assurance as sequential traversal, while bounded plans additionally report
|
|
268
|
+
`caller_asserted_bounds`. Runtime traversal does not perform verifier canaries.
|
|
269
|
+
|
|
270
|
+
<!-- tested: tests/keyset_fast_test.py::test_omitted_execution_defaults_to_auto -->
|
|
199
271
|
```python
|
|
200
272
|
from b24api import KeysetSpec, ParameterPath
|
|
201
273
|
|
|
@@ -235,12 +307,23 @@ stream = client.iter_list_cursor(
|
|
|
235
307
|
Cursor values must be unique and strictly monotonic. If an endpoint exposes only a non-unique
|
|
236
308
|
boundary, use an application-owned direct-call workflow or supply a unique tie-breaker.
|
|
237
309
|
|
|
310
|
+
For multiple parent-bound cursor chains, `iter_cursors()` keeps cursor progress and correlation
|
|
311
|
+
isolated per binding while ready pages share the physical batch queue.
|
|
312
|
+
|
|
313
|
+

|
|
314
|
+
|
|
315
|
+
See [architecture](docs/architecture.md), [migration](docs/migration.md),
|
|
316
|
+
[performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
|
|
317
|
+
contracts and selection guidance.
|
|
318
|
+
|
|
238
319
|
### One list method across many parent entities
|
|
239
320
|
|
|
240
321
|
`Binding` applies exact parameter updates to a base request and carries parent correlation. The
|
|
241
322
|
client remains unaware of entity types: a binding can represent a deal, lead, chat or any other
|
|
242
323
|
caller-defined parent.
|
|
243
324
|
|
|
325
|
+

|
|
326
|
+
|
|
244
327
|
<!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
|
|
245
328
|
```python
|
|
246
329
|
from b24api import (
|
|
@@ -31,6 +31,15 @@ idempotent and closes active streams before the owned transport.
|
|
|
31
31
|
Use `call()` for detached decoded JSON and `call_response()` when you also need the immutable
|
|
32
32
|
response envelope: `result`, `total`, `next`, timing and bounded diagnostic evidence.
|
|
33
33
|
|
|
34
|
+
Use `call_bytes()` when a successful method response is a file rather than a Bitrix JSON envelope.
|
|
35
|
+
The operation is explicit and never hides malformed JSON by falling back to bytes.
|
|
36
|
+
|
|
37
|
+
<!-- tested: tests/client_findings_3_test.py::test_binary_call_returns_every_success_byte_without_json_sniffing -->
|
|
38
|
+
```python
|
|
39
|
+
archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE))
|
|
40
|
+
payload = archive.body
|
|
41
|
+
```
|
|
42
|
+
|
|
34
43
|
<!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
|
|
35
44
|
```python
|
|
36
45
|
from b24api import ReplaySafety
|
|
@@ -118,20 +127,30 @@ more specialized mechanics have explicit names and explicit preconditions.
|
|
|
118
127
|
|
|
119
128
|
| Operation | Use it when | Network mechanics | Completion proof |
|
|
120
129
|
|---|---|---|---|
|
|
121
|
-
| `iter_list` | The method supports ordinary offset pagination. |
|
|
130
|
+
| `iter_list` | The method supports ordinary offset pagination. | Sequential requests follow server `next` (the next offset). Ordinary counted Bitrix list endpoints do server-side COUNT for `total` plus LIMIT/OFFSET page retrieval. | Continuation and empty terminal page; add identity for duplicate detection. |
|
|
122
131
|
| `iter_list_counted` | The first response provides an exact filtered `total` and stable offset pages. | Head page is direct; all known tail offsets are grouped into physical Bitrix batches. | Exact total, ranges and identities. |
|
|
123
|
-
| `iter_list_keyset` | The method may omit `total`, but reliably supports ordering and filtering by a unique identity. |
|
|
132
|
+
| `iter_list_keyset` | The method may omit `total`, but reliably supports ordering and filtering by a unique integer identity. | Auto by default: it plans first, then selects boundary-only, sequential, range, or partitioned execution; runtime sends no diagnostic canaries. | Caller-asserted keyset contract, strict monotonic identity, active bound validation, and terminal empty confirmation. |
|
|
124
133
|
| `iter_list_cursor` | Each next request depends on a cursor from the previous response. | Sequential dependent cursor requests. | Strict unique monotonic cursor and empty terminal page. |
|
|
134
|
+
| `iter_cursors` | One or many parent-bound cursor traversals need correlation and shared batching. | Lazy per-parent drivers share the reference batch queue; each binding may have `start_cursor`. | Isolated strict cursor progress and terminal event per binding. |
|
|
125
135
|
| `iter_references` | The same list method must run for many parent parameter sets, such as comments per owner or messages per chat. | Bindings are scheduled with direct or physical-batch dispatch; each binding has its own traversal state. | Per-binding rows, completion/failure and caller correlation. |
|
|
126
136
|
|
|
127
137
|
`page_size` is a local decoded-page cap. It is sent to Bitrix only when you provide the endpoint's
|
|
128
138
|
exact `limit_path`; the client never guesses method-specific parameter names.
|
|
129
139
|
|
|
140
|
+
### List traversal comparison
|
|
141
|
+
|
|
142
|
+

|
|
143
|
+
|
|
130
144
|
### Sequential offset
|
|
131
145
|
|
|
132
146
|
This is the canonical default. It follows the `next` returned by the server and confirms the end
|
|
133
|
-
with an empty page.
|
|
134
|
-
a separate count
|
|
147
|
+
with an empty page. For ordinary counted Bitrix list endpoints, `next` is an offset for the next
|
|
148
|
+
LIMIT/OFFSET page, not a keyset cursor. Producing `total` involves a separate server-side count
|
|
149
|
+
query in addition to retrieving the page. These database operations are performed inside the same
|
|
150
|
+
REST request: the client does not issue an additional HTTP call just for the count. This distinction
|
|
151
|
+
matters for performance: not making a separate HTTP count call does **not** mean avoiding server-side
|
|
152
|
+
COUNT work. `iter_list` does not suppress that work; a returned `total` is observational and does not
|
|
153
|
+
control this strategy's completion. Exact database implementation is endpoint-specific.
|
|
135
154
|
|
|
136
155
|
<!-- tested: tests/client_v2_test.py::test_iter_list_is_sequential_mechanics_only_and_report_is_post_cleanup -->
|
|
137
156
|
```python
|
|
@@ -157,6 +176,24 @@ async with stream:
|
|
|
157
176
|
Without `identity`, successful exhaustion is reported as `MECHANICS_ONLY`: pagination completed,
|
|
158
177
|
but the client cannot prove that the portal did not duplicate or substitute rows.
|
|
159
178
|
|
|
179
|
+
Mapping-backed collections are explicit as well. `MAPPING_VALUES` yields values from a selected
|
|
180
|
+
mapping in insertion order; `MAPPING_VALUES_OR_EMPTY` additionally accepts only an empty terminal
|
|
181
|
+
sequence and records that degradation in the operation report.
|
|
182
|
+
|
|
183
|
+
<!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
|
|
184
|
+
```python
|
|
185
|
+
from b24api import ResultCollectionShape
|
|
186
|
+
|
|
187
|
+
stream = client.iter_list(
|
|
188
|
+
Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
|
|
189
|
+
selector=ResultSelector(("items",)),
|
|
190
|
+
collection_shape=ResultCollectionShape.MAPPING_VALUES,
|
|
191
|
+
)
|
|
192
|
+
async with stream:
|
|
193
|
+
async for value in stream:
|
|
194
|
+
consume(value)
|
|
195
|
+
```
|
|
196
|
+
|
|
160
197
|
### Counted, physically batched tail
|
|
161
198
|
|
|
162
199
|
The first direct page must contain an exact filtered `total` and, when more rows exist, `next`.
|
|
@@ -177,13 +214,48 @@ stream = client.iter_list_counted(
|
|
|
177
214
|
Use it only when `total` is exact for the supplied filter and offset pages are stable. Any missing
|
|
178
215
|
range, overlap, duplicate identity or total contradiction raises `IncompleteTraversalError`.
|
|
179
216
|
|
|
180
|
-
|
|
217
|
+
Physical batching reduces HTTP exchanges, but does not suppress server-side COUNT in ordinary
|
|
218
|
+
counted list subrequests. Each command still performs its own offset page retrieval and associated
|
|
219
|
+
total calculation on the server. Do not confuse batching these commands with a no-count traversal.
|
|
181
220
|
|
|
182
|
-
|
|
183
|
-
identity boundary such as `filter[>ID]`. It is intentionally sequential because a future boundary
|
|
184
|
-
cannot be known safely before the preceding page arrives.
|
|
221
|
+
### No-count keyset
|
|
185
222
|
|
|
186
|
-
|
|
223
|
+
Keyset traversal uses automatic execution by default. Omitting `execution` is equivalent to
|
|
224
|
+
`AutoKeysetExecution(StableIntegerKeysetContract())`: the client captures both ordered boundaries,
|
|
225
|
+
then selects boundary-only, sequential, range, or partitioned execution from the observed geometry
|
|
226
|
+
and available policy capacity. Planning completes before any row is emitted, so partial consumption
|
|
227
|
+
still pays that barrier cost. A sequential selection made by auto is a cost decision; a failed or
|
|
228
|
+
contradictory fast plan is never silently restarted as sequential.
|
|
229
|
+
|
|
230
|
+
By using the default, the caller asserts that the endpoint has a stable, unique integer key, honors
|
|
231
|
+
strict numeric bounds and ordering, and satisfies empty-confirmation completion. Concurrent mutation
|
|
232
|
+
outside the captured middle is handled by the finishing sweep; mutation inside it is outside this
|
|
233
|
+
assertion. Pass `SequentialKeysetExecution()` explicitly when an endpoint cannot satisfy the fast
|
|
234
|
+
contract or when the previous request-by-request behavior is required.
|
|
235
|
+
|
|
236
|
+
Static incompatibility with the auto contract raises `CapabilityError` from the
|
|
237
|
+
`iter_list_keyset(...)` call before iteration begins. A portal that accepts but contradicts the
|
|
238
|
+
declared controls fails before emission with `IncompleteTraversalError`; auto never restarts that
|
|
239
|
+
operation silently. `KeysetTraversal` inside reference traversal remains sequential-only. The
|
|
240
|
+
terminal report now includes `keyset_execution` for omitted-execution keyset calls so consumers can
|
|
241
|
+
see the requested and selected plan.
|
|
242
|
+
|
|
243
|
+
Use `await client.verify_keyset_capability(...)` as a development/CI/staging guard on a stable
|
|
244
|
+
representative fixture. It performs five strict-bound checks and returns only a `VERIFIED` report;
|
|
245
|
+
unsupported and inconclusive verdicts raise `KeysetCapabilityError`. The ordinary
|
|
246
|
+
`iter_list_keyset()` remains a separate caller-asserted operation with zero verifier canaries and
|
|
247
|
+
may emit a partial prefix before a late endpoint contradiction is detected.
|
|
248
|
+
|
|
249
|
+
Every list operation also accepts an immutable `PageAdapter` strategy. The adapter synchronously
|
|
250
|
+
maps selected frozen items using sibling result metadata while preserving cardinality, order and
|
|
251
|
+
configured identities. The identity adapter is the default and preserves existing JSON output.
|
|
252
|
+
|
|
253
|
+
An advisory `total` may only raise an automatic cost estimate and never proves completion. Reports
|
|
254
|
+
record the selected strategy and reason: unbounded auto continuation has the same
|
|
255
|
+
`ordered_prefix_only` assurance as sequential traversal, while bounded plans additionally report
|
|
256
|
+
`caller_asserted_bounds`. Runtime traversal does not perform verifier canaries.
|
|
257
|
+
|
|
258
|
+
<!-- tested: tests/keyset_fast_test.py::test_omitted_execution_defaults_to_auto -->
|
|
187
259
|
```python
|
|
188
260
|
from b24api import KeysetSpec, ParameterPath
|
|
189
261
|
|
|
@@ -223,12 +295,23 @@ stream = client.iter_list_cursor(
|
|
|
223
295
|
Cursor values must be unique and strictly monotonic. If an endpoint exposes only a non-unique
|
|
224
296
|
boundary, use an application-owned direct-call workflow or supply a unique tie-breaker.
|
|
225
297
|
|
|
298
|
+
For multiple parent-bound cursor chains, `iter_cursors()` keeps cursor progress and correlation
|
|
299
|
+
isolated per binding while ready pages share the physical batch queue.
|
|
300
|
+
|
|
301
|
+

|
|
302
|
+
|
|
303
|
+
See [architecture](docs/architecture.md), [migration](docs/migration.md),
|
|
304
|
+
[performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
|
|
305
|
+
contracts and selection guidance.
|
|
306
|
+
|
|
226
307
|
### One list method across many parent entities
|
|
227
308
|
|
|
228
309
|
`Binding` applies exact parameter updates to a base request and carries parent correlation. The
|
|
229
310
|
client remains unaware of entity types: a binding can represent a deal, lead, chat or any other
|
|
230
311
|
caller-defined parent.
|
|
231
312
|
|
|
313
|
+

|
|
314
|
+
|
|
232
315
|
<!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
|
|
233
316
|
```python
|
|
234
317
|
from b24api import (
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""One explicit, method-agnostic b24api v2 public surface."""
|
|
2
|
+
|
|
3
|
+
from b24api.client import Bitrix24
|
|
4
|
+
from b24api.contracts import (
|
|
5
|
+
AdaptedPage,
|
|
6
|
+
AmbiguityPolicy,
|
|
7
|
+
AmbiguityReason,
|
|
8
|
+
AutoKeysetExecution,
|
|
9
|
+
BatchDispatch,
|
|
10
|
+
BinaryEvidence,
|
|
11
|
+
BinaryResponse,
|
|
12
|
+
Binding,
|
|
13
|
+
BodyEncoding,
|
|
14
|
+
ClosureWitness,
|
|
15
|
+
Command,
|
|
16
|
+
CommandFailure,
|
|
17
|
+
CommandNotExecuted,
|
|
18
|
+
CommandOutcome,
|
|
19
|
+
CommandOutcomeUnknown,
|
|
20
|
+
CommandSuccess,
|
|
21
|
+
CompositeIdentitySpec,
|
|
22
|
+
ConsistencyPolicy,
|
|
23
|
+
CountedTraversal,
|
|
24
|
+
CursorSpec,
|
|
25
|
+
CursorTraversal,
|
|
26
|
+
DeliveryOrder,
|
|
27
|
+
DirectDispatch,
|
|
28
|
+
ExecutionPolicy,
|
|
29
|
+
FrozenJson,
|
|
30
|
+
FrozenMapping,
|
|
31
|
+
IdentityCoercion,
|
|
32
|
+
IdentityComponent,
|
|
33
|
+
IdentityPageAdapter,
|
|
34
|
+
IdentitySpec,
|
|
35
|
+
KeysetAssuranceSource,
|
|
36
|
+
KeysetCapabilityCheckName,
|
|
37
|
+
KeysetCapabilityCheckOutcome,
|
|
38
|
+
KeysetCapabilityCheckResult,
|
|
39
|
+
KeysetCapabilityReport,
|
|
40
|
+
KeysetCapabilityVerdict,
|
|
41
|
+
KeysetExecution,
|
|
42
|
+
KeysetExecutionKind,
|
|
43
|
+
KeysetExecutionReport,
|
|
44
|
+
KeysetInconclusiveReason,
|
|
45
|
+
KeysetPageCompletion,
|
|
46
|
+
KeysetPhase,
|
|
47
|
+
KeysetSelectionReason,
|
|
48
|
+
KeysetSpec,
|
|
49
|
+
KeysetTraversal,
|
|
50
|
+
MembershipRecheck,
|
|
51
|
+
NotExecutedReason,
|
|
52
|
+
OffsetContinuation,
|
|
53
|
+
OffsetSpec,
|
|
54
|
+
OperationReport,
|
|
55
|
+
OperationStream,
|
|
56
|
+
PageAdapter,
|
|
57
|
+
PageDispatch,
|
|
58
|
+
PageOutcome,
|
|
59
|
+
PageRecord,
|
|
60
|
+
PageRejectionCode,
|
|
61
|
+
PageView,
|
|
62
|
+
ParameterPath,
|
|
63
|
+
ParameterUpdate,
|
|
64
|
+
PartialResult,
|
|
65
|
+
PartitionedKeysetExecution,
|
|
66
|
+
RangeKeysetExecution,
|
|
67
|
+
ReferenceComplete,
|
|
68
|
+
ReferenceEvent,
|
|
69
|
+
ReferenceFailure,
|
|
70
|
+
ReferenceItem,
|
|
71
|
+
ReferenceNotExecuted,
|
|
72
|
+
ReferenceOutcome,
|
|
73
|
+
ReferenceOutcomeUnknown,
|
|
74
|
+
ReplaySafety,
|
|
75
|
+
Request,
|
|
76
|
+
RequestHeaders,
|
|
77
|
+
RequestSummary,
|
|
78
|
+
Response,
|
|
79
|
+
ResultCollectionShape,
|
|
80
|
+
ResultErrorShape,
|
|
81
|
+
ResultErrorSpec,
|
|
82
|
+
ResultSelector,
|
|
83
|
+
RetryPolicy,
|
|
84
|
+
SequentialKeysetExecution,
|
|
85
|
+
SequentialTraversal,
|
|
86
|
+
SplitOrderSpec,
|
|
87
|
+
StableIntegerKeysetContract,
|
|
88
|
+
TerminalState,
|
|
89
|
+
TotalHintMode,
|
|
90
|
+
TotalTermination,
|
|
91
|
+
TraceClass,
|
|
92
|
+
TraversalAssurance,
|
|
93
|
+
TraversalIdentity,
|
|
94
|
+
UnknownRequestAudit,
|
|
95
|
+
UnknownRequestCollector,
|
|
96
|
+
Violation,
|
|
97
|
+
ViolationSeverity,
|
|
98
|
+
partition_command_outcomes,
|
|
99
|
+
partition_reference_outcomes,
|
|
100
|
+
traversal_control_paths,
|
|
101
|
+
)
|
|
102
|
+
from b24api.errors import (
|
|
103
|
+
AmbiguousExecutionError,
|
|
104
|
+
ApiResponseError,
|
|
105
|
+
B24ApiError,
|
|
106
|
+
BatchCommandError,
|
|
107
|
+
BatchFailed,
|
|
108
|
+
BudgetExceededError,
|
|
109
|
+
CapabilityError,
|
|
110
|
+
EnvelopeContractError,
|
|
111
|
+
HTTPGatewayError,
|
|
112
|
+
IdentityContractError,
|
|
113
|
+
IncompleteTraversalError,
|
|
114
|
+
InputSourceError,
|
|
115
|
+
KeysetCapabilityError,
|
|
116
|
+
PageAdaptationError,
|
|
117
|
+
PageAdaptationViolation,
|
|
118
|
+
PaginationError,
|
|
119
|
+
ProtocolError,
|
|
120
|
+
ReferenceFailed,
|
|
121
|
+
ResponseTooLargeError,
|
|
122
|
+
ResultShapeError,
|
|
123
|
+
TransportError,
|
|
124
|
+
)
|
|
125
|
+
from b24api.settings import Settings
|
|
126
|
+
from b24api.transport import Transport, TransportCapabilities, WireRequest, WireResponse, WireTransport
|
|
127
|
+
|
|
128
|
+
__all__ = [
|
|
129
|
+
"AdaptedPage",
|
|
130
|
+
"AmbiguityPolicy",
|
|
131
|
+
"AmbiguityReason",
|
|
132
|
+
"AmbiguousExecutionError",
|
|
133
|
+
"ApiResponseError",
|
|
134
|
+
"AutoKeysetExecution",
|
|
135
|
+
"B24ApiError",
|
|
136
|
+
"BatchCommandError",
|
|
137
|
+
"BatchDispatch",
|
|
138
|
+
"BatchFailed",
|
|
139
|
+
"BinaryEvidence",
|
|
140
|
+
"BinaryResponse",
|
|
141
|
+
"Binding",
|
|
142
|
+
"Bitrix24",
|
|
143
|
+
"BodyEncoding",
|
|
144
|
+
"BudgetExceededError",
|
|
145
|
+
"CapabilityError",
|
|
146
|
+
"ClosureWitness",
|
|
147
|
+
"Command",
|
|
148
|
+
"CommandFailure",
|
|
149
|
+
"CommandNotExecuted",
|
|
150
|
+
"CommandOutcome",
|
|
151
|
+
"CommandOutcomeUnknown",
|
|
152
|
+
"CommandSuccess",
|
|
153
|
+
"CompositeIdentitySpec",
|
|
154
|
+
"ConsistencyPolicy",
|
|
155
|
+
"CountedTraversal",
|
|
156
|
+
"CursorSpec",
|
|
157
|
+
"CursorTraversal",
|
|
158
|
+
"DeliveryOrder",
|
|
159
|
+
"DirectDispatch",
|
|
160
|
+
"EnvelopeContractError",
|
|
161
|
+
"ExecutionPolicy",
|
|
162
|
+
"FrozenJson",
|
|
163
|
+
"FrozenMapping",
|
|
164
|
+
"HTTPGatewayError",
|
|
165
|
+
"IdentityCoercion",
|
|
166
|
+
"IdentityComponent",
|
|
167
|
+
"IdentityContractError",
|
|
168
|
+
"IdentityPageAdapter",
|
|
169
|
+
"IdentitySpec",
|
|
170
|
+
"IncompleteTraversalError",
|
|
171
|
+
"InputSourceError",
|
|
172
|
+
"KeysetAssuranceSource",
|
|
173
|
+
"KeysetCapabilityCheckName",
|
|
174
|
+
"KeysetCapabilityCheckOutcome",
|
|
175
|
+
"KeysetCapabilityCheckResult",
|
|
176
|
+
"KeysetCapabilityError",
|
|
177
|
+
"KeysetCapabilityReport",
|
|
178
|
+
"KeysetCapabilityVerdict",
|
|
179
|
+
"KeysetExecution",
|
|
180
|
+
"KeysetExecutionKind",
|
|
181
|
+
"KeysetExecutionReport",
|
|
182
|
+
"KeysetInconclusiveReason",
|
|
183
|
+
"KeysetPageCompletion",
|
|
184
|
+
"KeysetPhase",
|
|
185
|
+
"KeysetSelectionReason",
|
|
186
|
+
"KeysetSpec",
|
|
187
|
+
"KeysetTraversal",
|
|
188
|
+
"MembershipRecheck",
|
|
189
|
+
"NotExecutedReason",
|
|
190
|
+
"OffsetContinuation",
|
|
191
|
+
"OffsetSpec",
|
|
192
|
+
"OperationReport",
|
|
193
|
+
"OperationStream",
|
|
194
|
+
"PageAdaptationError",
|
|
195
|
+
"PageAdaptationViolation",
|
|
196
|
+
"PageAdapter",
|
|
197
|
+
"PageDispatch",
|
|
198
|
+
"PageOutcome",
|
|
199
|
+
"PageRecord",
|
|
200
|
+
"PageRejectionCode",
|
|
201
|
+
"PageView",
|
|
202
|
+
"PaginationError",
|
|
203
|
+
"ParameterPath",
|
|
204
|
+
"ParameterUpdate",
|
|
205
|
+
"PartialResult",
|
|
206
|
+
"PartitionedKeysetExecution",
|
|
207
|
+
"ProtocolError",
|
|
208
|
+
"RangeKeysetExecution",
|
|
209
|
+
"ReferenceComplete",
|
|
210
|
+
"ReferenceEvent",
|
|
211
|
+
"ReferenceFailed",
|
|
212
|
+
"ReferenceFailure",
|
|
213
|
+
"ReferenceItem",
|
|
214
|
+
"ReferenceNotExecuted",
|
|
215
|
+
"ReferenceOutcome",
|
|
216
|
+
"ReferenceOutcomeUnknown",
|
|
217
|
+
"ReplaySafety",
|
|
218
|
+
"Request",
|
|
219
|
+
"RequestHeaders",
|
|
220
|
+
"RequestSummary",
|
|
221
|
+
"Response",
|
|
222
|
+
"ResponseTooLargeError",
|
|
223
|
+
"ResultCollectionShape",
|
|
224
|
+
"ResultErrorShape",
|
|
225
|
+
"ResultErrorSpec",
|
|
226
|
+
"ResultSelector",
|
|
227
|
+
"ResultShapeError",
|
|
228
|
+
"RetryPolicy",
|
|
229
|
+
"SequentialKeysetExecution",
|
|
230
|
+
"SequentialTraversal",
|
|
231
|
+
"Settings",
|
|
232
|
+
"SplitOrderSpec",
|
|
233
|
+
"StableIntegerKeysetContract",
|
|
234
|
+
"TerminalState",
|
|
235
|
+
"TotalHintMode",
|
|
236
|
+
"TotalTermination",
|
|
237
|
+
"TraceClass",
|
|
238
|
+
"Transport",
|
|
239
|
+
"TransportCapabilities",
|
|
240
|
+
"TransportError",
|
|
241
|
+
"TraversalAssurance",
|
|
242
|
+
"TraversalIdentity",
|
|
243
|
+
"UnknownRequestAudit",
|
|
244
|
+
"UnknownRequestCollector",
|
|
245
|
+
"Violation",
|
|
246
|
+
"ViolationSeverity",
|
|
247
|
+
"WireRequest",
|
|
248
|
+
"WireResponse",
|
|
249
|
+
"WireTransport",
|
|
250
|
+
"partition_command_outcomes",
|
|
251
|
+
"partition_reference_outcomes",
|
|
252
|
+
"traversal_control_paths",
|
|
253
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Lazy admission-time observation for logical command sources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from collections.abc import AsyncIterable, AsyncIterator, Callable, Iterable, Iterator
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from b24api.contracts.command import Command
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from b24api.contracts.report import Violation
|
|
11
|
+
from b24api.contracts.request import Request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class _SyncClosable(Protocol):
|
|
16
|
+
def close(self) -> None: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class _AsyncClosable(Protocol):
|
|
21
|
+
async def aclose(self) -> None: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _SyncAuditSource[C](Iterator[Command[C]]):
|
|
25
|
+
def __init__(self, source: Iterable[Command[C]], audit: Callable[[Request], Violation | None]) -> None:
|
|
26
|
+
self._iterator = iter(source)
|
|
27
|
+
self._audit = audit
|
|
28
|
+
self.violations: list[Violation] = []
|
|
29
|
+
|
|
30
|
+
def __iter__(self) -> Self:
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def __next__(self) -> Command[C]:
|
|
34
|
+
command = next(self._iterator)
|
|
35
|
+
if isinstance(command, Command):
|
|
36
|
+
violation = self._audit(command.request)
|
|
37
|
+
if violation is not None:
|
|
38
|
+
self.violations.append(violation)
|
|
39
|
+
return command
|
|
40
|
+
|
|
41
|
+
def close(self) -> None:
|
|
42
|
+
if isinstance(self._iterator, _SyncClosable):
|
|
43
|
+
self._iterator.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _AsyncAuditSource[C](AsyncIterator[Command[C]]):
|
|
47
|
+
def __init__(self, source: AsyncIterable[Command[C]], audit: Callable[[Request], Violation | None]) -> None:
|
|
48
|
+
self._iterator = aiter(source)
|
|
49
|
+
self._audit = audit
|
|
50
|
+
self.violations: list[Violation] = []
|
|
51
|
+
|
|
52
|
+
def __aiter__(self) -> Self:
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def __anext__(self) -> Command[C]:
|
|
56
|
+
command = await anext(self._iterator)
|
|
57
|
+
if isinstance(command, Command):
|
|
58
|
+
violation = self._audit(command.request)
|
|
59
|
+
if violation is not None:
|
|
60
|
+
self.violations.append(violation)
|
|
61
|
+
return command
|
|
62
|
+
|
|
63
|
+
async def aclose(self) -> None:
|
|
64
|
+
if isinstance(self._iterator, _AsyncClosable):
|
|
65
|
+
await self._iterator.aclose()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def audit_command_source[C](
|
|
69
|
+
source: Iterable[Command[C]] | AsyncIterable[Command[C]],
|
|
70
|
+
audit: Callable[[Request], Violation | None],
|
|
71
|
+
) -> Iterable[Command[C]] | AsyncIterable[Command[C]]:
|
|
72
|
+
"""Observe each command exactly when its lazy source admits it."""
|
|
73
|
+
if isinstance(source, AsyncIterable):
|
|
74
|
+
return _AsyncAuditSource(source, audit)
|
|
75
|
+
return _SyncAuditSource(source, audit)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = ["audit_command_source"]
|