b24api 2.0.0__tar.gz → 2.1.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 (101) hide show
  1. {b24api-2.0.0/b24api.egg-info → b24api-2.1.0}/PKG-INFO +48 -6
  2. {b24api-2.0.0 → b24api-2.1.0}/README.md +47 -5
  3. {b24api-2.0.0 → b24api-2.1.0}/b24api/__init__.py +88 -1
  4. b24api-2.1.0/b24api/_audit.py +78 -0
  5. b24api-2.1.0/b24api/_client_traversal.py +170 -0
  6. b24api-2.1.0/b24api/_error_types.py +31 -0
  7. {b24api-2.0.0 → b24api-2.1.0}/b24api/_stream.py +30 -7
  8. {b24api-2.0.0 → b24api-2.1.0}/b24api/batch/engine.py +200 -73
  9. {b24api-2.0.0 → b24api-2.1.0}/b24api/batch/facade.py +7 -1
  10. {b24api-2.0.0 → b24api-2.1.0}/b24api/batch/stream.py +1 -5
  11. {b24api-2.0.0 → b24api-2.1.0}/b24api/cli.py +27 -1
  12. {b24api-2.0.0 → b24api-2.1.0}/b24api/cli_contract.py +78 -9
  13. {b24api-2.0.0 → b24api-2.1.0}/b24api/client.py +49 -124
  14. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/__init__.py +83 -2
  15. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/json.py +66 -4
  16. b24api-2.1.0/b24api/contracts/keyset_execution.py +232 -0
  17. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/policy.py +80 -2
  18. b24api-2.1.0/b24api/contracts/report.py +385 -0
  19. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/request.py +147 -6
  20. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/response.py +77 -0
  21. b24api-2.1.0/b24api/contracts/traversal.py +262 -0
  22. b24api-2.1.0/b24api/contracts/wire.py +85 -0
  23. b24api-2.1.0/b24api/encoding.py +40 -0
  24. {b24api-2.0.0 → b24api-2.1.0}/b24api/errors.py +149 -54
  25. {b24api-2.0.0 → b24api-2.1.0}/b24api/execution/context.py +15 -0
  26. b24api-2.1.0/b24api/execution/executor.py +700 -0
  27. b24api-2.1.0/b24api/execution/failure.py +131 -0
  28. {b24api-2.0.0 → b24api-2.1.0}/b24api/execution/snapshot.py +7 -1
  29. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/binding.py +35 -25
  30. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/dispatch.py +23 -7
  31. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/facade.py +58 -13
  32. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/fanout.py +6 -1
  33. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/scheduler.py +100 -10
  34. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/stream.py +20 -6
  35. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/support.py +0 -6
  36. b24api-2.1.0/b24api/testing/__init__.py +17 -0
  37. b24api-2.1.0/b24api/testing/_isolation.py +81 -0
  38. b24api-2.1.0/b24api/testing/transport.py +373 -0
  39. b24api-2.1.0/b24api/transport/__init__.py +6 -0
  40. b24api-2.1.0/b24api/transport/base.py +158 -0
  41. {b24api-2.0.0 → b24api-2.1.0}/b24api/transport/httpx.py +54 -12
  42. {b24api-2.0.0 → b24api-2.1.0}/b24api/transport/protocol.py +1 -1
  43. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/counted.py +11 -8
  44. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/counted_batch.py +111 -35
  45. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/cursor.py +20 -14
  46. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/driver.py +321 -38
  47. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/facade.py +118 -32
  48. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/identity.py +28 -19
  49. b24api-2.1.0/b24api/traversal/keyset.py +63 -0
  50. b24api-2.1.0/b24api/traversal/keyset_auto.py +241 -0
  51. b24api-2.1.0/b24api/traversal/keyset_capability.py +392 -0
  52. b24api-2.1.0/b24api/traversal/keyset_costs.py +185 -0
  53. b24api-2.1.0/b24api/traversal/keyset_eligibility.py +159 -0
  54. b24api-2.1.0/b24api/traversal/keyset_fast_plan.py +288 -0
  55. b24api-2.1.0/b24api/traversal/keyset_fast_stream.py +274 -0
  56. b24api-2.1.0/b24api/traversal/keyset_observation.py +270 -0
  57. b24api-2.1.0/b24api/traversal/keyset_partition.py +27 -0
  58. b24api-2.1.0/b24api/traversal/keyset_range.py +80 -0
  59. b24api-2.1.0/b24api/traversal/keyset_reporting.py +112 -0
  60. b24api-2.1.0/b24api/traversal/keyset_scheduler.py +672 -0
  61. b24api-2.1.0/b24api/traversal/keyset_step.py +138 -0
  62. b24api-2.1.0/b24api/traversal/keyset_transaction_contract.py +108 -0
  63. b24api-2.1.0/b24api/traversal/keyset_transactions.py +393 -0
  64. b24api-2.1.0/b24api/traversal/ordered_admission.py +182 -0
  65. b24api-2.1.0/b24api/traversal/page_validation.py +235 -0
  66. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/plans.py +18 -10
  67. b24api-2.1.0/b24api/traversal/sequential.py +198 -0
  68. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/stream.py +15 -6
  69. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/values.py +56 -9
  70. {b24api-2.0.0 → b24api-2.1.0/b24api.egg-info}/PKG-INFO +48 -6
  71. {b24api-2.0.0 → b24api-2.1.0}/b24api.egg-info/SOURCES.txt +26 -0
  72. b24api-2.0.0/b24api/contracts/report.py +0 -137
  73. b24api-2.0.0/b24api/contracts/traversal.py +0 -140
  74. b24api-2.0.0/b24api/execution/executor.py +0 -339
  75. b24api-2.0.0/b24api/transport/__init__.py +0 -6
  76. b24api-2.0.0/b24api/transport/base.py +0 -56
  77. b24api-2.0.0/b24api/traversal/keyset.py +0 -71
  78. b24api-2.0.0/b24api/traversal/sequential.py +0 -149
  79. {b24api-2.0.0 → b24api-2.1.0}/LICENSE +0 -0
  80. {b24api-2.0.0 → b24api-2.1.0}/MANIFEST.in +0 -0
  81. {b24api-2.0.0 → b24api-2.1.0}/b24api/batch/__init__.py +0 -0
  82. {b24api-2.0.0 → b24api-2.1.0}/b24api/batch/logical.py +0 -0
  83. {b24api-2.0.0 → b24api-2.1.0}/b24api/batch/outcome.py +0 -0
  84. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/command.py +0 -0
  85. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/dispatch.py +0 -0
  86. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/reference.py +0 -0
  87. {b24api-2.0.0 → b24api-2.1.0}/b24api/contracts/stream.py +0 -0
  88. {b24api-2.0.0 → b24api-2.1.0}/b24api/execution/__init__.py +0 -0
  89. {b24api-2.0.0 → b24api-2.1.0}/b24api/execution/cleanup.py +0 -0
  90. {b24api-2.0.0 → b24api-2.1.0}/b24api/execution/rate.py +0 -0
  91. {b24api-2.0.0 → b24api-2.1.0}/b24api/redaction.py +0 -0
  92. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/__init__.py +0 -0
  93. {b24api-2.0.0 → b24api-2.1.0}/b24api/references/outcome.py +0 -0
  94. {b24api-2.0.0 → b24api-2.1.0}/b24api/settings.py +0 -0
  95. {b24api-2.0.0 → b24api-2.1.0}/b24api/traversal/__init__.py +0 -0
  96. {b24api-2.0.0 → b24api-2.1.0}/b24api.egg-info/dependency_links.txt +0 -0
  97. {b24api-2.0.0 → b24api-2.1.0}/b24api.egg-info/entry_points.txt +0 -0
  98. {b24api-2.0.0 → b24api-2.1.0}/b24api.egg-info/requires.txt +0 -0
  99. {b24api-2.0.0 → b24api-2.1.0}/b24api.egg-info/top_level.txt +0 -0
  100. {b24api-2.0.0 → b24api-2.1.0}/pyproject.toml +0 -0
  101. {b24api-2.0.0 → b24api-2.1.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.1.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
@@ -132,7 +141,7 @@ more specialized mechanics have explicit names and explicit preconditions.
132
141
  |---|---|---|---|
133
142
  | `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. |
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. | Sequential by default; explicit range, partitioned, or auto execution may batch bounded work after a planning barrier. | Caller-asserted keyset contract, strict monotonic identity, bounded-plan canaries, 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. |
137
146
  | `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
147
 
@@ -169,6 +178,24 @@ async with stream:
169
178
  Without `identity`, successful exhaustion is reported as `MECHANICS_ONLY`: pagination completed,
170
179
  but the client cannot prove that the portal did not duplicate or substitute rows.
171
180
 
181
+ Mapping-backed collections are explicit as well. `MAPPING_VALUES` yields values from a selected
182
+ mapping in insertion order; `MAPPING_VALUES_OR_EMPTY` additionally accepts only an empty terminal
183
+ sequence and records that degradation in the operation report.
184
+
185
+ <!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
186
+ ```python
187
+ from b24api import ResultCollectionShape
188
+
189
+ stream = client.iter_list(
190
+ Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
191
+ selector=ResultSelector(("items",)),
192
+ collection_shape=ResultCollectionShape.MAPPING_VALUES,
193
+ )
194
+ async with stream:
195
+ async for value in stream:
196
+ consume(value)
197
+ ```
198
+
172
199
  ### Counted, physically batched tail
173
200
 
174
201
  The first direct page must contain an exact filtered `total` and, when more rows exist, `next`.
@@ -191,13 +218,23 @@ range, overlap, duplicate identity or total contradiction raises `IncompleteTrav
191
218
 
192
219
  ### No-count keyset
193
220
 
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.
221
+ Keyset traversal is sequential by default, preserving the compatible request shape and first-pull
222
+ behavior. An explicit execution contract can instead capture both ordered boundaries, validate five
223
+ capability canaries, and batch numeric ranges or occupied-anchor partitions. Planning completes
224
+ before any row is emitted, so partial consumption still pays that barrier cost. Fast execution fails
225
+ synchronously when its declared controls or policy capacity are ineligible.
226
+
227
+ The caller must assert that the endpoint has a stable, unique integer key, honors strict numeric
228
+ bounds and ordering, and satisfies the chosen page-completion rule. Concurrent mutation outside the
229
+ captured middle is handled by the finishing sweep; mutation inside it is outside this assertion.
230
+ An advisory `total` may only raise an automatic cost estimate and never proves completion. Reports
231
+ record the selected strategy and reason: unbounded auto continuation has the same
232
+ `ordered_prefix_only` assurance as sequential traversal, while bounded plans additionally report
233
+ `canary_verified_bounds`.
197
234
 
198
235
  <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
199
236
  ```python
200
- from b24api import KeysetSpec, ParameterPath
237
+ from b24api import AutoKeysetExecution, KeysetSpec, ParameterPath, StableIntegerKeysetContract
201
238
 
202
239
  stream = client.iter_list_keyset(
203
240
  Request("example.item.list", replay_safety=ReplaySafety.SAFE),
@@ -207,6 +244,7 @@ stream = client.iter_list_keyset(
207
244
  filter_path=ParameterPath(("filter",)),
208
245
  order_path=ParameterPath(("order",)),
209
246
  ),
247
+ execution=AutoKeysetExecution(contract=StableIntegerKeysetContract()),
210
248
  )
211
249
  ```
212
250
 
@@ -235,6 +273,10 @@ stream = client.iter_list_cursor(
235
273
  Cursor values must be unique and strictly monotonic. If an endpoint exposes only a non-unique
236
274
  boundary, use an application-owned direct-call workflow or supply a unique tie-breaker.
237
275
 
276
+ See [architecture](docs/architecture.md), [migration](docs/migration.md),
277
+ [performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
278
+ contracts and selection guidance.
279
+
238
280
  ### One list method across many parent entities
239
281
 
240
282
  `Binding` applies exact parameter updates to a base request and carries parent correlation. The
@@ -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
@@ -120,7 +129,7 @@ more specialized mechanics have explicit names and explicit preconditions.
120
129
  |---|---|---|---|
121
130
  | `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. |
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. | Sequential by default; explicit range, partitioned, or auto execution may batch bounded work after a planning barrier. | Caller-asserted keyset contract, strict monotonic identity, bounded-plan canaries, 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. |
125
134
  | `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
135
 
@@ -157,6 +166,24 @@ async with stream:
157
166
  Without `identity`, successful exhaustion is reported as `MECHANICS_ONLY`: pagination completed,
158
167
  but the client cannot prove that the portal did not duplicate or substitute rows.
159
168
 
169
+ Mapping-backed collections are explicit as well. `MAPPING_VALUES` yields values from a selected
170
+ mapping in insertion order; `MAPPING_VALUES_OR_EMPTY` additionally accepts only an empty terminal
171
+ sequence and records that degradation in the operation report.
172
+
173
+ <!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
174
+ ```python
175
+ from b24api import ResultCollectionShape
176
+
177
+ stream = client.iter_list(
178
+ Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
179
+ selector=ResultSelector(("items",)),
180
+ collection_shape=ResultCollectionShape.MAPPING_VALUES,
181
+ )
182
+ async with stream:
183
+ async for value in stream:
184
+ consume(value)
185
+ ```
186
+
160
187
  ### Counted, physically batched tail
161
188
 
162
189
  The first direct page must contain an exact filtered `total` and, when more rows exist, `next`.
@@ -179,13 +206,23 @@ range, overlap, duplicate identity or total contradiction raises `IncompleteTrav
179
206
 
180
207
  ### No-count keyset
181
208
 
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.
209
+ Keyset traversal is sequential by default, preserving the compatible request shape and first-pull
210
+ behavior. An explicit execution contract can instead capture both ordered boundaries, validate five
211
+ capability canaries, and batch numeric ranges or occupied-anchor partitions. Planning completes
212
+ before any row is emitted, so partial consumption still pays that barrier cost. Fast execution fails
213
+ synchronously when its declared controls or policy capacity are ineligible.
214
+
215
+ The caller must assert that the endpoint has a stable, unique integer key, honors strict numeric
216
+ bounds and ordering, and satisfies the chosen page-completion rule. Concurrent mutation outside the
217
+ captured middle is handled by the finishing sweep; mutation inside it is outside this assertion.
218
+ An advisory `total` may only raise an automatic cost estimate and never proves completion. Reports
219
+ record the selected strategy and reason: unbounded auto continuation has the same
220
+ `ordered_prefix_only` assurance as sequential traversal, while bounded plans additionally report
221
+ `canary_verified_bounds`.
185
222
 
186
223
  <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
187
224
  ```python
188
- from b24api import KeysetSpec, ParameterPath
225
+ from b24api import AutoKeysetExecution, KeysetSpec, ParameterPath, StableIntegerKeysetContract
189
226
 
190
227
  stream = client.iter_list_keyset(
191
228
  Request("example.item.list", replay_safety=ReplaySafety.SAFE),
@@ -195,6 +232,7 @@ stream = client.iter_list_keyset(
195
232
  filter_path=ParameterPath(("filter",)),
196
233
  order_path=ParameterPath(("order",)),
197
234
  ),
235
+ execution=AutoKeysetExecution(contract=StableIntegerKeysetContract()),
198
236
  )
199
237
  ```
200
238
 
@@ -223,6 +261,10 @@ stream = client.iter_list_cursor(
223
261
  Cursor values must be unique and strictly monotonic. If an endpoint exposes only a non-unique
224
262
  boundary, use an application-owned direct-call workflow or supply a unique tie-breaker.
225
263
 
264
+ See [architecture](docs/architecture.md), [migration](docs/migration.md),
265
+ [performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
266
+ contracts and selection guidance.
267
+
226
268
  ### One list method across many parent entities
227
269
 
228
270
  `Binding` applies exact parameter updates to a base request and carries parent correlation. The
@@ -2,14 +2,23 @@
2
2
 
3
3
  from b24api.client import Bitrix24
4
4
  from b24api.contracts import (
5
+ AmbiguityPolicy,
6
+ AmbiguityReason,
7
+ AutoKeysetExecution,
5
8
  BatchDispatch,
9
+ BinaryEvidence,
10
+ BinaryResponse,
6
11
  Binding,
12
+ BodyEncoding,
13
+ ClosureWitness,
7
14
  Command,
8
15
  CommandFailure,
9
16
  CommandNotExecuted,
10
17
  CommandOutcome,
11
18
  CommandOutcomeUnknown,
12
19
  CommandSuccess,
20
+ CompositeIdentitySpec,
21
+ ConsistencyPolicy,
13
22
  CountedTraversal,
14
23
  CursorSpec,
15
24
  CursorTraversal,
@@ -17,16 +26,31 @@ from b24api.contracts import (
17
26
  DirectDispatch,
18
27
  ExecutionPolicy,
19
28
  IdentityCoercion,
29
+ IdentityComponent,
20
30
  IdentitySpec,
31
+ KeysetAssuranceSource,
32
+ KeysetExecution,
33
+ KeysetExecutionKind,
34
+ KeysetExecutionReport,
35
+ KeysetPageCompletion,
36
+ KeysetPhase,
37
+ KeysetSelectionReason,
21
38
  KeysetSpec,
22
39
  KeysetTraversal,
23
40
  NotExecutedReason,
41
+ OffsetContinuation,
24
42
  OffsetSpec,
25
43
  OperationReport,
26
44
  OperationStream,
45
+ PageDispatch,
46
+ PageOutcome,
47
+ PageRecord,
48
+ PageRejectionCode,
27
49
  ParameterPath,
28
50
  ParameterUpdate,
29
51
  PartialResult,
52
+ PartitionedKeysetExecution,
53
+ RangeKeysetExecution,
30
54
  ReferenceComplete,
31
55
  ReferenceEvent,
32
56
  ReferenceFailure,
@@ -36,16 +60,31 @@ from b24api.contracts import (
36
60
  ReferenceOutcomeUnknown,
37
61
  ReplaySafety,
38
62
  Request,
63
+ RequestHeaders,
64
+ RequestSummary,
39
65
  Response,
40
66
  ResultCollectionShape,
67
+ ResultErrorShape,
68
+ ResultErrorSpec,
41
69
  ResultSelector,
70
+ RetryPolicy,
71
+ SequentialKeysetExecution,
42
72
  SequentialTraversal,
73
+ SplitOrderSpec,
74
+ StableIntegerKeysetContract,
43
75
  TerminalState,
76
+ TotalHintMode,
77
+ TotalTermination,
78
+ TraceClass,
44
79
  TraversalAssurance,
80
+ TraversalIdentity,
81
+ UnknownRequestAudit,
82
+ UnknownRequestCollector,
45
83
  Violation,
46
84
  ViolationSeverity,
47
85
  partition_command_outcomes,
48
86
  partition_reference_outcomes,
87
+ traversal_control_paths,
49
88
  )
50
89
  from b24api.errors import (
51
90
  AmbiguousExecutionError,
@@ -55,57 +94,86 @@ from b24api.errors import (
55
94
  BatchFailed,
56
95
  BudgetExceededError,
57
96
  CapabilityError,
97
+ EnvelopeContractError,
58
98
  HTTPGatewayError,
99
+ IdentityContractError,
59
100
  IncompleteTraversalError,
60
101
  InputSourceError,
61
102
  PaginationError,
62
103
  ProtocolError,
63
104
  ReferenceFailed,
64
105
  ResponseTooLargeError,
106
+ ResultShapeError,
65
107
  TransportError,
66
108
  )
67
109
  from b24api.settings import Settings
68
- from b24api.transport import Transport, WireResponse
110
+ from b24api.transport import Transport, TransportCapabilities, WireRequest, WireResponse, WireTransport
69
111
 
70
112
  __all__ = [
113
+ "AmbiguityPolicy",
114
+ "AmbiguityReason",
71
115
  "AmbiguousExecutionError",
72
116
  "ApiResponseError",
117
+ "AutoKeysetExecution",
73
118
  "B24ApiError",
74
119
  "BatchCommandError",
75
120
  "BatchDispatch",
76
121
  "BatchFailed",
122
+ "BinaryEvidence",
123
+ "BinaryResponse",
77
124
  "Binding",
78
125
  "Bitrix24",
126
+ "BodyEncoding",
79
127
  "BudgetExceededError",
80
128
  "CapabilityError",
129
+ "ClosureWitness",
81
130
  "Command",
82
131
  "CommandFailure",
83
132
  "CommandNotExecuted",
84
133
  "CommandOutcome",
85
134
  "CommandOutcomeUnknown",
86
135
  "CommandSuccess",
136
+ "CompositeIdentitySpec",
137
+ "ConsistencyPolicy",
87
138
  "CountedTraversal",
88
139
  "CursorSpec",
89
140
  "CursorTraversal",
90
141
  "DeliveryOrder",
91
142
  "DirectDispatch",
143
+ "EnvelopeContractError",
92
144
  "ExecutionPolicy",
93
145
  "HTTPGatewayError",
94
146
  "IdentityCoercion",
147
+ "IdentityComponent",
148
+ "IdentityContractError",
95
149
  "IdentitySpec",
96
150
  "IncompleteTraversalError",
97
151
  "InputSourceError",
152
+ "KeysetAssuranceSource",
153
+ "KeysetExecution",
154
+ "KeysetExecutionKind",
155
+ "KeysetExecutionReport",
156
+ "KeysetPageCompletion",
157
+ "KeysetPhase",
158
+ "KeysetSelectionReason",
98
159
  "KeysetSpec",
99
160
  "KeysetTraversal",
100
161
  "NotExecutedReason",
162
+ "OffsetContinuation",
101
163
  "OffsetSpec",
102
164
  "OperationReport",
103
165
  "OperationStream",
166
+ "PageDispatch",
167
+ "PageOutcome",
168
+ "PageRecord",
169
+ "PageRejectionCode",
104
170
  "PaginationError",
105
171
  "ParameterPath",
106
172
  "ParameterUpdate",
107
173
  "PartialResult",
174
+ "PartitionedKeysetExecution",
108
175
  "ProtocolError",
176
+ "RangeKeysetExecution",
109
177
  "ReferenceComplete",
110
178
  "ReferenceEvent",
111
179
  "ReferenceFailed",
@@ -116,19 +184,38 @@ __all__ = [
116
184
  "ReferenceOutcomeUnknown",
117
185
  "ReplaySafety",
118
186
  "Request",
187
+ "RequestHeaders",
188
+ "RequestSummary",
119
189
  "Response",
120
190
  "ResponseTooLargeError",
121
191
  "ResultCollectionShape",
192
+ "ResultErrorShape",
193
+ "ResultErrorSpec",
122
194
  "ResultSelector",
195
+ "ResultShapeError",
196
+ "RetryPolicy",
197
+ "SequentialKeysetExecution",
123
198
  "SequentialTraversal",
124
199
  "Settings",
200
+ "SplitOrderSpec",
201
+ "StableIntegerKeysetContract",
125
202
  "TerminalState",
203
+ "TotalHintMode",
204
+ "TotalTermination",
205
+ "TraceClass",
126
206
  "Transport",
207
+ "TransportCapabilities",
127
208
  "TransportError",
128
209
  "TraversalAssurance",
210
+ "TraversalIdentity",
211
+ "UnknownRequestAudit",
212
+ "UnknownRequestCollector",
129
213
  "Violation",
130
214
  "ViolationSeverity",
215
+ "WireRequest",
131
216
  "WireResponse",
217
+ "WireTransport",
132
218
  "partition_command_outcomes",
133
219
  "partition_reference_outcomes",
220
+ "traversal_control_paths",
134
221
  ]
@@ -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"]
@@ -0,0 +1,170 @@
1
+ """Stateless traversal operation helpers for the public facade."""
2
+
3
+ from __future__ import annotations
4
+ from typing import TYPE_CHECKING
5
+
6
+ from b24api.contracts.keyset_execution import KeysetExecution, SequentialKeysetExecution
7
+ from b24api.contracts.request import IdentitySpec, RequestLike, ResultSelector, TraversalIdentity, canonical_request
8
+ from b24api.contracts.response import ResultCollectionShape
9
+ from b24api.contracts.traversal import CursorSpec, KeysetSpec, OffsetSpec, TotalTermination
10
+ from b24api.traversal.facade import counted_stream, cursor_stream, keyset_stream, sequential_stream
11
+
12
+ if TYPE_CHECKING:
13
+ from b24api.contracts.json import JsonValue
14
+ from b24api.contracts.policy import ExecutionPolicy
15
+ from b24api.contracts.report import Violation
16
+ from b24api.contracts.request import Request
17
+ from b24api.contracts.stream import OperationStream
18
+ from b24api.execution import Executor
19
+
20
+ _ROOT_SELECTOR = ResultSelector.root()
21
+ _DEFAULT_OFFSET = OffsetSpec()
22
+ _DEFAULT_COUNTED_OFFSET = OffsetSpec(total_termination=TotalTermination.EXACT_QUALIFIED)
23
+ _DEFAULT_KEYSET = KeysetSpec()
24
+ _DEFAULT_SEQUENTIAL_KEYSET_EXECUTION = SequentialKeysetExecution()
25
+
26
+
27
+ class _TraversalFacade:
28
+ """Traversal methods sharing the lifecycle owned by ``Bitrix24``."""
29
+
30
+ _executor: Executor
31
+ _default_policy: ExecutionPolicy
32
+
33
+ def _require_open(self) -> None:
34
+ raise NotImplementedError
35
+
36
+ def _audit_unknown(self, request: Request) -> Violation | None:
37
+ raise NotImplementedError
38
+
39
+ def _register_stream[T](self, stream: OperationStream[T]) -> OperationStream[T]:
40
+ raise NotImplementedError
41
+
42
+ def _discard_stream(self, stream: object) -> None:
43
+ raise NotImplementedError
44
+
45
+ def iter_list( # noqa: PLR0913
46
+ self,
47
+ request: RequestLike,
48
+ *,
49
+ selector: ResultSelector = _ROOT_SELECTOR,
50
+ identity: TraversalIdentity | None = None,
51
+ collection_shape: ResultCollectionShape = ResultCollectionShape.SEQUENCE,
52
+ page_size: int = 50,
53
+ offset: OffsetSpec = _DEFAULT_OFFSET,
54
+ policy: ExecutionPolicy | None = None,
55
+ ) -> OperationStream[JsonValue]:
56
+ """Return conservative sequential offset/server-next traversal."""
57
+ self._require_open()
58
+ canonical = canonical_request(request)
59
+ audit_violation = self._audit_unknown(canonical)
60
+ return self._register_stream(
61
+ sequential_stream(
62
+ self._executor,
63
+ canonical,
64
+ selector=selector,
65
+ identity=identity,
66
+ collection_shape=collection_shape,
67
+ page_size=page_size,
68
+ offset=offset,
69
+ policy=policy or self._default_policy,
70
+ deregister=self._discard_stream,
71
+ audit_violations=(() if audit_violation is None else (audit_violation,)),
72
+ ),
73
+ )
74
+
75
+ def iter_list_counted( # noqa: PLR0913
76
+ self,
77
+ request: RequestLike,
78
+ *,
79
+ identity: TraversalIdentity | None = None,
80
+ selector: ResultSelector = _ROOT_SELECTOR,
81
+ collection_shape: ResultCollectionShape = ResultCollectionShape.SEQUENCE,
82
+ page_size: int = 50,
83
+ batch_size: int | None = None,
84
+ offset: OffsetSpec = _DEFAULT_COUNTED_OFFSET,
85
+ policy: ExecutionPolicy | None = None,
86
+ ) -> OperationStream[JsonValue]:
87
+ """Return exact direct-head plus physically batched counted traversal."""
88
+ self._require_open()
89
+ canonical = canonical_request(request)
90
+ audit_violation = self._audit_unknown(canonical)
91
+ return self._register_stream(
92
+ counted_stream(
93
+ self._executor,
94
+ canonical,
95
+ identity=identity,
96
+ selector=selector,
97
+ collection_shape=collection_shape,
98
+ page_size=page_size,
99
+ batch_size=batch_size,
100
+ offset=offset,
101
+ policy=policy or self._default_policy,
102
+ deregister=self._discard_stream,
103
+ audit_violations=(() if audit_violation is None else (audit_violation,)),
104
+ ),
105
+ )
106
+
107
+ def iter_list_keyset( # noqa: PLR0913
108
+ self,
109
+ request: RequestLike,
110
+ *,
111
+ selector: ResultSelector,
112
+ identity: IdentitySpec,
113
+ collection_shape: ResultCollectionShape = ResultCollectionShape.SEQUENCE,
114
+ page_size: int = 50,
115
+ keyset: KeysetSpec = _DEFAULT_KEYSET,
116
+ execution: KeysetExecution = _DEFAULT_SEQUENTIAL_KEYSET_EXECUTION,
117
+ policy: ExecutionPolicy | None = None,
118
+ ) -> OperationStream[JsonValue]:
119
+ """Return exact sequential no-count keyset traversal."""
120
+ self._require_open()
121
+ canonical = canonical_request(request)
122
+ audit_violation = self._audit_unknown(canonical)
123
+ return self._register_stream(
124
+ keyset_stream(
125
+ self._executor,
126
+ canonical,
127
+ selector=selector,
128
+ identity=identity,
129
+ collection_shape=collection_shape,
130
+ page_size=page_size,
131
+ keyset=keyset,
132
+ execution=execution,
133
+ policy=policy or self._default_policy,
134
+ deregister=self._discard_stream,
135
+ audit_violations=(() if audit_violation is None else (audit_violation,)),
136
+ ),
137
+ )
138
+
139
+ def iter_list_cursor( # noqa: PLR0913
140
+ self,
141
+ request: RequestLike,
142
+ *,
143
+ selector: ResultSelector,
144
+ cursor: CursorSpec,
145
+ identity: IdentitySpec | None = None,
146
+ collection_shape: ResultCollectionShape = ResultCollectionShape.SEQUENCE,
147
+ page_size: int = 50,
148
+ policy: ExecutionPolicy | None = None,
149
+ ) -> OperationStream[JsonValue]:
150
+ """Return strict dependent cursor traversal with empty confirmation."""
151
+ self._require_open()
152
+ canonical = canonical_request(request)
153
+ audit_violation = self._audit_unknown(canonical)
154
+ return self._register_stream(
155
+ cursor_stream(
156
+ self._executor,
157
+ canonical,
158
+ selector=selector,
159
+ cursor=cursor,
160
+ identity=identity,
161
+ collection_shape=collection_shape,
162
+ page_size=page_size,
163
+ policy=policy or self._default_policy,
164
+ deregister=self._discard_stream,
165
+ audit_violations=(() if audit_violation is None else (audit_violation,)),
166
+ ),
167
+ )
168
+
169
+
170
+ __all__: list[str] = []