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.
Files changed (112) hide show
  1. {b24api-2.0.0/b24api.egg-info → b24api-2.2.0}/PKG-INFO +93 -10
  2. {b24api-2.0.0 → b24api-2.2.0}/README.md +92 -9
  3. b24api-2.2.0/b24api/__init__.py +253 -0
  4. b24api-2.2.0/b24api/_audit.py +78 -0
  5. b24api-2.2.0/b24api/_client_traversal.py +180 -0
  6. b24api-2.2.0/b24api/_error_types.py +31 -0
  7. {b24api-2.0.0 → b24api-2.2.0}/b24api/_stream.py +30 -7
  8. {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/engine.py +200 -73
  9. {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/facade.py +7 -1
  10. {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/stream.py +1 -5
  11. {b24api-2.0.0 → b24api-2.2.0}/b24api/cli.py +62 -3
  12. {b24api-2.0.0 → b24api-2.2.0}/b24api/cli_contract.py +116 -9
  13. {b24api-2.0.0 → b24api-2.2.0}/b24api/client.py +113 -121
  14. b24api-2.2.0/b24api/contracts/__init__.py +211 -0
  15. {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/dispatch.py +7 -0
  16. b24api-2.2.0/b24api/contracts/json.py +139 -0
  17. b24api-2.2.0/b24api/contracts/keyset_capability.py +246 -0
  18. b24api-2.2.0/b24api/contracts/keyset_execution.py +233 -0
  19. b24api-2.2.0/b24api/contracts/page.py +70 -0
  20. {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/policy.py +80 -2
  21. {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/reference.py +3 -0
  22. b24api-2.2.0/b24api/contracts/report.py +393 -0
  23. {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/request.py +147 -6
  24. {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/response.py +81 -0
  25. b24api-2.2.0/b24api/contracts/traversal.py +268 -0
  26. b24api-2.2.0/b24api/contracts/wire.py +85 -0
  27. b24api-2.2.0/b24api/encoding.py +40 -0
  28. b24api-2.2.0/b24api/errors.py +507 -0
  29. {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/context.py +28 -8
  30. b24api-2.2.0/b24api/execution/executor.py +700 -0
  31. b24api-2.2.0/b24api/execution/failure.py +131 -0
  32. {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/snapshot.py +7 -1
  33. {b24api-2.0.0 → b24api-2.2.0}/b24api/references/binding.py +53 -26
  34. b24api-2.2.0/b24api/references/dispatch.py +689 -0
  35. {b24api-2.0.0 → b24api-2.2.0}/b24api/references/facade.py +67 -22
  36. {b24api-2.0.0 → b24api-2.2.0}/b24api/references/fanout.py +6 -1
  37. {b24api-2.0.0 → b24api-2.2.0}/b24api/references/outcome.py +27 -0
  38. {b24api-2.0.0 → b24api-2.2.0}/b24api/references/scheduler.py +199 -15
  39. {b24api-2.0.0 → b24api-2.2.0}/b24api/references/stream.py +25 -6
  40. {b24api-2.0.0 → b24api-2.2.0}/b24api/references/support.py +0 -6
  41. b24api-2.2.0/b24api/testing/__init__.py +17 -0
  42. b24api-2.2.0/b24api/testing/_isolation.py +81 -0
  43. b24api-2.2.0/b24api/testing/transport.py +373 -0
  44. b24api-2.2.0/b24api/transport/__init__.py +6 -0
  45. b24api-2.2.0/b24api/transport/base.py +158 -0
  46. {b24api-2.0.0 → b24api-2.2.0}/b24api/transport/httpx.py +54 -12
  47. {b24api-2.0.0 → b24api-2.2.0}/b24api/transport/protocol.py +1 -1
  48. {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/counted.py +16 -9
  49. {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/counted_batch.py +117 -41
  50. {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/cursor.py +33 -18
  51. b24api-2.2.0/b24api/traversal/driver.py +748 -0
  52. {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/facade.py +130 -32
  53. {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/identity.py +31 -21
  54. b24api-2.2.0/b24api/traversal/keyset.py +64 -0
  55. b24api-2.2.0/b24api/traversal/keyset_auto.py +236 -0
  56. b24api-2.2.0/b24api/traversal/keyset_capability.py +392 -0
  57. b24api-2.2.0/b24api/traversal/keyset_costs.py +185 -0
  58. b24api-2.2.0/b24api/traversal/keyset_eligibility.py +159 -0
  59. b24api-2.2.0/b24api/traversal/keyset_fast_plan.py +288 -0
  60. b24api-2.2.0/b24api/traversal/keyset_fast_stream.py +274 -0
  61. b24api-2.2.0/b24api/traversal/keyset_observation.py +250 -0
  62. b24api-2.2.0/b24api/traversal/keyset_partition.py +27 -0
  63. b24api-2.2.0/b24api/traversal/keyset_range.py +80 -0
  64. b24api-2.2.0/b24api/traversal/keyset_reporting.py +111 -0
  65. b24api-2.2.0/b24api/traversal/keyset_scheduler.py +626 -0
  66. b24api-2.2.0/b24api/traversal/keyset_step.py +138 -0
  67. b24api-2.2.0/b24api/traversal/keyset_transaction_contract.py +108 -0
  68. b24api-2.2.0/b24api/traversal/keyset_transactions.py +385 -0
  69. b24api-2.2.0/b24api/traversal/keyset_verifier.py +498 -0
  70. b24api-2.2.0/b24api/traversal/ordered_admission.py +182 -0
  71. b24api-2.2.0/b24api/traversal/page_adaptation.py +90 -0
  72. b24api-2.2.0/b24api/traversal/page_validation.py +255 -0
  73. {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/plans.py +25 -10
  74. b24api-2.2.0/b24api/traversal/sequential.py +199 -0
  75. {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/stream.py +24 -8
  76. b24api-2.2.0/b24api/traversal/values.py +219 -0
  77. {b24api-2.0.0 → b24api-2.2.0/b24api.egg-info}/PKG-INFO +93 -10
  78. {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/SOURCES.txt +30 -0
  79. b24api-2.0.0/b24api/__init__.py +0 -134
  80. b24api-2.0.0/b24api/contracts/__init__.py +0 -107
  81. b24api-2.0.0/b24api/contracts/json.py +0 -77
  82. b24api-2.0.0/b24api/contracts/report.py +0 -137
  83. b24api-2.0.0/b24api/contracts/traversal.py +0 -140
  84. b24api-2.0.0/b24api/errors.py +0 -304
  85. b24api-2.0.0/b24api/execution/executor.py +0 -339
  86. b24api-2.0.0/b24api/references/dispatch.py +0 -419
  87. b24api-2.0.0/b24api/transport/__init__.py +0 -6
  88. b24api-2.0.0/b24api/transport/base.py +0 -56
  89. b24api-2.0.0/b24api/traversal/driver.py +0 -414
  90. b24api-2.0.0/b24api/traversal/keyset.py +0 -71
  91. b24api-2.0.0/b24api/traversal/sequential.py +0 -149
  92. b24api-2.0.0/b24api/traversal/values.py +0 -154
  93. {b24api-2.0.0 → b24api-2.2.0}/LICENSE +0 -0
  94. {b24api-2.0.0 → b24api-2.2.0}/MANIFEST.in +0 -0
  95. {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/__init__.py +0 -0
  96. {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/logical.py +0 -0
  97. {b24api-2.0.0 → b24api-2.2.0}/b24api/batch/outcome.py +0 -0
  98. {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/command.py +0 -0
  99. {b24api-2.0.0 → b24api-2.2.0}/b24api/contracts/stream.py +0 -0
  100. {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/__init__.py +0 -0
  101. {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/cleanup.py +0 -0
  102. {b24api-2.0.0 → b24api-2.2.0}/b24api/execution/rate.py +0 -0
  103. {b24api-2.0.0 → b24api-2.2.0}/b24api/redaction.py +0 -0
  104. {b24api-2.0.0 → b24api-2.2.0}/b24api/references/__init__.py +0 -0
  105. {b24api-2.0.0 → b24api-2.2.0}/b24api/settings.py +0 -0
  106. {b24api-2.0.0 → b24api-2.2.0}/b24api/traversal/__init__.py +0 -0
  107. {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/dependency_links.txt +0 -0
  108. {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/entry_points.txt +0 -0
  109. {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/requires.txt +0 -0
  110. {b24api-2.0.0 → b24api-2.2.0}/b24api.egg-info/top_level.txt +0 -0
  111. {b24api-2.0.0 → b24api-2.2.0}/pyproject.toml +0 -0
  112. {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.0.0
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. | Pages are requested sequentially using server `next`; no separate count request is made. | Continuation and empty terminal page; add identity for duplicate detection. |
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. | Sequential pages advance an identity boundary; no count request. | Strict monotonic identity and empty terminal page. |
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
+ ![List traversal comparison](list-traversal-comparison.svg)
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. A `total` present in the response is observational; this strategy does not add
146
- a separate count request.
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
- ### No-count keyset
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
- Keyset traversal does not ask the server for a count. The method must honor ordering and a strict
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
- <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
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
+ ![Cursor batching across independent chats](cursor-batching.svg)
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
+ ![Reference batching across leads and deals](references-batching.svg)
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. | Pages are requested sequentially using server `next`; no separate count request is made. | Continuation and empty terminal page; add identity for duplicate detection. |
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. | Sequential pages advance an identity boundary; no count request. | Strict monotonic identity and empty terminal page. |
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
+ ![List traversal comparison](list-traversal-comparison.svg)
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. A `total` present in the response is observational; this strategy does not add
134
- a separate count request.
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
- ### No-count keyset
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
- Keyset traversal does not ask the server for a count. The method must honor ordering and a strict
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
- <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
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
+ ![Cursor batching across independent chats](cursor-batching.svg)
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
+ ![Reference batching across leads and deals](references-batching.svg)
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"]