b24api 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. {b24api-2.2.0 → b24api-2.3.0}/PKG-INFO +66 -31
  2. b24api-2.2.0/b24api.egg-info/PKG-INFO → b24api-2.3.0/README.md +64 -41
  3. {b24api-2.2.0 → b24api-2.3.0}/b24api/__init__.py +82 -0
  4. {b24api-2.2.0 → b24api-2.3.0}/b24api/_client_traversal.py +15 -0
  5. {b24api-2.2.0 → b24api-2.3.0}/b24api/_stream.py +28 -77
  6. {b24api-2.2.0 → b24api-2.3.0}/b24api/batch/engine.py +69 -12
  7. {b24api-2.2.0 → b24api-2.3.0}/b24api/batch/logical.py +90 -5
  8. {b24api-2.2.0 → b24api-2.3.0}/b24api/batch/stream.py +2 -1
  9. {b24api-2.2.0 → b24api-2.3.0}/b24api/cli.py +8 -4
  10. {b24api-2.2.0 → b24api-2.3.0}/b24api/cli_contract.py +10 -2
  11. {b24api-2.2.0 → b24api-2.3.0}/b24api/client.py +10 -3
  12. b24api-2.3.0/b24api/completion/__init__.py +5 -0
  13. b24api-2.3.0/b24api/completion/closure.py +31 -0
  14. b24api-2.3.0/b24api/completion/fast_recorder.py +195 -0
  15. b24api-2.3.0/b24api/completion/gate.py +479 -0
  16. b24api-2.3.0/b24api/completion/recorder.py +284 -0
  17. b24api-2.3.0/b24api/completion/reference_recorder.py +180 -0
  18. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/__init__.py +78 -0
  19. b24api-2.3.0/b24api/contracts/bounded_range.py +81 -0
  20. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/command.py +13 -0
  21. b24api-2.3.0/b24api/contracts/completion.py +158 -0
  22. b24api-2.3.0/b24api/contracts/identity_store.py +45 -0
  23. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/keyset_capability.py +1 -1
  24. b24api-2.3.0/b24api/contracts/page_stop.py +68 -0
  25. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/policy.py +2 -0
  26. b24api-2.3.0/b24api/contracts/positional.py +236 -0
  27. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/reference.py +16 -1
  28. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/report.py +16 -41
  29. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/request.py +62 -10
  30. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/traversal.py +142 -1
  31. b24api-2.3.0/b24api/contracts/violation.py +102 -0
  32. {b24api-2.2.0 → b24api-2.3.0}/b24api/errors.py +43 -3
  33. {b24api-2.2.0 → b24api-2.3.0}/b24api/execution/__init__.py +14 -1
  34. {b24api-2.2.0 → b24api-2.3.0}/b24api/execution/context.py +19 -0
  35. {b24api-2.2.0 → b24api-2.3.0}/b24api/execution/executor.py +57 -69
  36. {b24api-2.2.0 → b24api-2.3.0}/b24api/execution/failure.py +26 -9
  37. {b24api-2.2.0 → b24api-2.3.0}/b24api/execution/rate.py +134 -18
  38. {b24api-2.2.0 → b24api-2.3.0}/b24api/execution/snapshot.py +5 -0
  39. b24api-2.3.0/b24api/execution/throttle.py +53 -0
  40. {b24api-2.2.0 → b24api-2.3.0}/b24api/redaction.py +1 -1
  41. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/binding.py +5 -2
  42. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/dispatch.py +8 -2
  43. b24api-2.3.0/b24api/references/dispatch_plan.py +27 -0
  44. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/facade.py +51 -60
  45. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/fanout.py +4 -4
  46. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/scheduler.py +87 -41
  47. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/stream.py +44 -26
  48. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/support.py +67 -0
  49. {b24api-2.2.0 → b24api-2.3.0}/b24api/testing/__init__.py +3 -0
  50. b24api-2.3.0/b24api/testing/scripted.py +147 -0
  51. {b24api-2.2.0 → b24api-2.3.0}/b24api/testing/transport.py +39 -8
  52. {b24api-2.2.0 → b24api-2.3.0}/b24api/transport/base.py +58 -4
  53. {b24api-2.2.0 → b24api-2.3.0}/b24api/transport/httpx.py +102 -13
  54. b24api-2.3.0/b24api/transport/logging_shield.py +244 -0
  55. b24api-2.3.0/b24api/transport/protocol.py +279 -0
  56. b24api-2.3.0/b24api/traversal/control_preflight.py +81 -0
  57. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/counted.py +60 -4
  58. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/counted_batch.py +44 -16
  59. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/cursor.py +13 -3
  60. b24api-2.3.0/b24api/traversal/cursor_domain.py +61 -0
  61. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/driver.py +61 -86
  62. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/facade.py +50 -43
  63. b24api-2.3.0/b24api/traversal/facade_support.py +36 -0
  64. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/identity.py +71 -26
  65. b24api-2.3.0/b24api/traversal/identity_ledger.py +59 -0
  66. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset.py +6 -2
  67. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_eligibility.py +50 -13
  68. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_fast_stream.py +42 -5
  69. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_observation.py +2 -0
  70. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_scheduler.py +43 -0
  71. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_step.py +46 -3
  72. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_transactions.py +36 -3
  73. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_verifier.py +33 -3
  74. b24api-2.3.0/b24api/traversal/offset_rules.py +76 -0
  75. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/ordered_admission.py +2 -3
  76. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/page_validation.py +20 -3
  77. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/plans.py +36 -3
  78. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/sequential.py +65 -12
  79. b24api-2.3.0/b24api/traversal/sparse.py +55 -0
  80. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/stream.py +90 -3
  81. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/values.py +5 -0
  82. b24api-2.2.0/README.md → b24api-2.3.0/b24api.egg-info/PKG-INFO +76 -29
  83. {b24api-2.2.0 → b24api-2.3.0}/b24api.egg-info/SOURCES.txt +22 -0
  84. {b24api-2.2.0 → b24api-2.3.0}/b24api.egg-info/requires.txt +1 -1
  85. {b24api-2.2.0 → b24api-2.3.0}/pyproject.toml +3 -2
  86. b24api-2.2.0/b24api/transport/protocol.py +0 -157
  87. {b24api-2.2.0 → b24api-2.3.0}/LICENSE +0 -0
  88. {b24api-2.2.0 → b24api-2.3.0}/MANIFEST.in +0 -0
  89. {b24api-2.2.0 → b24api-2.3.0}/b24api/_audit.py +0 -0
  90. {b24api-2.2.0 → b24api-2.3.0}/b24api/_error_types.py +0 -0
  91. {b24api-2.2.0 → b24api-2.3.0}/b24api/batch/__init__.py +0 -0
  92. {b24api-2.2.0 → b24api-2.3.0}/b24api/batch/facade.py +0 -0
  93. {b24api-2.2.0 → b24api-2.3.0}/b24api/batch/outcome.py +0 -0
  94. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/dispatch.py +0 -0
  95. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/json.py +0 -0
  96. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/keyset_execution.py +0 -0
  97. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/page.py +0 -0
  98. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/response.py +0 -0
  99. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/stream.py +0 -0
  100. {b24api-2.2.0 → b24api-2.3.0}/b24api/contracts/wire.py +0 -0
  101. {b24api-2.2.0 → b24api-2.3.0}/b24api/encoding.py +0 -0
  102. {b24api-2.2.0 → b24api-2.3.0}/b24api/execution/cleanup.py +0 -0
  103. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/__init__.py +0 -0
  104. {b24api-2.2.0 → b24api-2.3.0}/b24api/references/outcome.py +0 -0
  105. {b24api-2.2.0 → b24api-2.3.0}/b24api/settings.py +0 -0
  106. {b24api-2.2.0 → b24api-2.3.0}/b24api/testing/_isolation.py +0 -0
  107. {b24api-2.2.0 → b24api-2.3.0}/b24api/transport/__init__.py +0 -0
  108. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/__init__.py +0 -0
  109. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_auto.py +0 -0
  110. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_capability.py +0 -0
  111. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_costs.py +0 -0
  112. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_fast_plan.py +0 -0
  113. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_partition.py +0 -0
  114. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_range.py +0 -0
  115. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_reporting.py +0 -0
  116. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/keyset_transaction_contract.py +0 -0
  117. {b24api-2.2.0 → b24api-2.3.0}/b24api/traversal/page_adaptation.py +0 -0
  118. {b24api-2.2.0 → b24api-2.3.0}/b24api.egg-info/dependency_links.txt +0 -0
  119. {b24api-2.2.0 → b24api-2.3.0}/b24api.egg-info/entry_points.txt +0 -0
  120. {b24api-2.2.0 → b24api-2.3.0}/b24api.egg-info/top_level.txt +0 -0
  121. {b24api-2.2.0 → b24api-2.3.0}/setup.cfg +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b24api
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Bitrix24 API
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
- Requires-Dist: httpx[http2]>=0.28.1
8
+ Requires-Dist: httpx[http2]<0.29,>=0.28.1
9
9
  Requires-Dist: pydantic>=2.11.7
10
10
  Requires-Dist: pydantic-settings>=2.10.1
11
11
  Dynamic: license-file
@@ -29,10 +29,10 @@ work so its HTTP/2 connection pool and rate state are reused.
29
29
 
30
30
  <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
31
31
  ```python
32
- from b24api import Bitrix24, Request
32
+ from b24api import Bitrix24, Request, RouteKind
33
33
 
34
34
  async with Bitrix24() as client:
35
- profile = await client.call(Request("profile"))
35
+ profile = await client.call(Request("profile", route=RouteKind.BARE))
36
36
  ```
37
37
 
38
38
  The client owns its default transport. An injected transport remains caller-owned. `aclose()` is
@@ -48,15 +48,17 @@ The operation is explicit and never hides malformed JSON by falling back to byte
48
48
 
49
49
  <!-- tested: tests/client_findings_3_test.py::test_binary_call_returns_every_success_byte_without_json_sniffing -->
50
50
  ```python
51
- archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE))
51
+ from b24api import RouteKind
52
+ archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE))
52
53
  payload = archive.body
53
54
  ```
54
55
 
55
56
  <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
56
57
  ```python
58
+ from b24api import RouteKind
57
59
  from b24api import ReplaySafety
58
60
 
59
- request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE)
61
+ request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE, route=RouteKind.BARE)
60
62
  decoded = await client.call(request)
61
63
  response = await client.call_response(request)
62
64
  ```
@@ -97,11 +99,12 @@ matching a result to the object, file, chat or database row that produced its re
97
99
 
98
100
  <!-- tested: tests/client_v2_test.py::test_logical_batch_is_unbounded_ordered_and_correlation_is_strictly_off_wire -->
99
101
  ```python
102
+ from b24api import RouteKind
100
103
  from b24api import Command, CommandSuccess
101
104
 
102
105
  commands = (
103
106
  Command(
104
- Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE),
107
+ Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE, route=RouteKind.BARE),
105
108
  correlation=item_id,
106
109
  )
107
110
  for item_id in source_ids
@@ -166,6 +169,7 @@ control this strategy's completion. Exact database implementation is endpoint-sp
166
169
 
167
170
  <!-- tested: tests/client_v2_test.py::test_iter_list_is_sequential_mechanics_only_and_report_is_post_cleanup -->
168
171
  ```python
172
+ from b24api import RouteKind
169
173
  from b24api import IdentityCoercion, IdentitySpec, ResultSelector
170
174
 
171
175
  identity = IdentitySpec(
@@ -176,7 +180,7 @@ identity = IdentitySpec(
176
180
  )
177
181
 
178
182
  stream = client.iter_list(
179
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
183
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
180
184
  selector=ResultSelector(("items",)),
181
185
  identity=identity,
182
186
  )
@@ -194,10 +198,11 @@ sequence and records that degradation in the operation report.
194
198
 
195
199
  <!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
196
200
  ```python
201
+ from b24api import RouteKind
197
202
  from b24api import ResultCollectionShape
198
203
 
199
204
  stream = client.iter_list(
200
- Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
205
+ Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
201
206
  selector=ResultSelector(("items",)),
202
207
  collection_shape=ResultCollectionShape.MAPPING_VALUES,
203
208
  )
@@ -214,8 +219,9 @@ bounded physical batches.
214
219
 
215
220
  <!-- tested: tests/client_v2_test.py::test_counted_traversal_preserves_frozen_request_shape_and_exact_identity -->
216
221
  ```python
222
+ from b24api import RouteKind
217
223
  stream = client.iter_list_counted(
218
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
224
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
219
225
  selector=ResultSelector(("items",)),
220
226
  identity=identity,
221
227
  page_size=50,
@@ -257,6 +263,9 @@ representative fixture. It performs five strict-bound checks and returns only a
257
263
  unsupported and inconclusive verdicts raise `KeysetCapabilityError`. The ordinary
258
264
  `iter_list_keyset()` remains a separate caller-asserted operation with zero verifier canaries and
259
265
  may emit a partial prefix before a late endpoint contradiction is detected.
266
+ Keep the guard beside the traversal, for example under
267
+ `if os.environ.get("ENV") != "PROD":`; set `ENV=PROD` only after qualifying the exact portal,
268
+ credentials, method, request/filter, identity, ordering representation, and page cap.
260
269
 
261
270
  Every list operation also accepts an immutable `PageAdapter` strategy. The adapter synchronously
262
271
  maps selected frozen items using sibling result metadata while preserving cardinality, order and
@@ -269,16 +278,33 @@ record the selected strategy and reason: unbounded auto continuation has the sam
269
278
 
270
279
  <!-- tested: tests/keyset_fast_test.py::test_omitted_execution_defaults_to_auto -->
271
280
  ```python
272
- from b24api import KeysetSpec, ParameterPath
281
+ import os
282
+
283
+ from b24api import KeysetSpec, ParameterPath, ReplaySafety, Request, ResultSelector, RouteKind
284
+
285
+ request = Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE)
286
+ selector = ResultSelector(("items",))
287
+ keyset = KeysetSpec(
288
+ filter_path=ParameterPath(("filter",)),
289
+ order_path=ParameterPath(("order",)),
290
+ )
291
+
292
+ if os.environ.get("ENV") != "PROD":
293
+ # Accepting an ID filter does not prove strict bounds or ordering.
294
+ await client.verify_keyset_capability(
295
+ request,
296
+ selector=selector,
297
+ identity=identity,
298
+ page_size=50,
299
+ keyset=keyset,
300
+ )
273
301
 
274
302
  stream = client.iter_list_keyset(
275
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
276
- selector=ResultSelector(("items",)),
303
+ request,
304
+ selector=selector,
277
305
  identity=identity,
278
- keyset=KeysetSpec(
279
- filter_path=ParameterPath(("filter",)),
280
- order_path=ParameterPath(("order",)),
281
- ),
306
+ page_size=50,
307
+ keyset=keyset,
282
308
  )
283
309
  ```
284
310
 
@@ -289,10 +315,11 @@ message-list methods.
289
315
 
290
316
  <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
291
317
  ```python
318
+ from b24api import RouteKind
292
319
  from b24api import CursorSpec, ParameterPath
293
320
 
294
321
  stream = client.iter_list_cursor(
295
- Request("example.message.list", replay_safety=ReplaySafety.SAFE),
322
+ Request("example.message.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
296
323
  selector=ResultSelector(("items",)),
297
324
  cursor=CursorSpec(
298
325
  parameter_path=ParameterPath(("LAST_ID",)),
@@ -326,6 +353,7 @@ caller-defined parent.
326
353
 
327
354
  <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
328
355
  ```python
356
+ from b24api import RouteKind
329
357
  from b24api import (
330
358
  BatchDispatch,
331
359
  Binding,
@@ -346,7 +374,7 @@ bindings = (
346
374
  )
347
375
 
348
376
  stream = client.iter_references(
349
- Request("example.comment.list", replay_safety=ReplaySafety.SAFE),
377
+ Request("example.comment.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
350
378
  bindings,
351
379
  traversal=SequentialTraversal(selector=ResultSelector(("items",)), identity=identity),
352
380
  dispatch=BatchDispatch(batch_size=25, concurrency=2),
@@ -366,6 +394,7 @@ under different parents are not conflated.
366
394
 
367
395
  <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
368
396
  ```python
397
+ from b24api import RouteKind
369
398
  from b24api import (
370
399
  Binding,
371
400
  CursorSpec,
@@ -387,7 +416,7 @@ chat_bindings = (
387
416
  )
388
417
 
389
418
  messages = client.iter_references(
390
- Request("example.message.list", replay_safety=ReplaySafety.SAFE),
419
+ Request("example.message.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
391
420
  chat_bindings,
392
421
  traversal=CursorTraversal(
393
422
  selector=ResultSelector(("items",)),
@@ -430,13 +459,19 @@ and publish the same final report where the Python exception type permits it.
430
459
  ## Resource boundaries
431
460
 
432
461
  `ExecutionPolicy` bounds requests, pages, elapsed time, attempts, decompressed response bytes,
433
- buffered commands and rows, direct concurrency and active references. The default response ceiling
434
- is 16 MiB and is enforced while streaming, before JSON decoding.
435
-
436
- Sequential and counted exact traversal retain observed identities in memory. There is no database,
437
- spill file or identity-count refusal. Crossing 100,000 distinct identities emits one
438
- `RuntimeWarning`; exact tracking continues. Strict keyset and cursor traversal retain only
439
- monotonic progression state when sufficient.
462
+ buffered commands and rows, retained unordered identity keys, direct concurrency and active
463
+ references. The default response ceiling is 16 MiB and is enforced while streaming, before JSON
464
+ decoding.
465
+
466
+ Sequential, counted, and multi-reference exact traversal retain at most `max_identity_keys`
467
+ observed identities per operation in memory (100,000 by default). All active reference bindings
468
+ share that ceiling. A page that would exceed it is rejected atomically with typed budget evidence.
469
+ Set a larger finite ceiling when the expected aggregate cardinality is known, or pass
470
+ `identity_store=` to `iter_list`/`iter_list_counted` so a caller-owned `IdentityStore` (for example a
471
+ SQLite table keyed by `identity_store_key(...)`) proves uniqueness while in-process identity memory
472
+ stays bounded by one page; the client never closes that store. Repeated-page detection still keeps
473
+ one short fingerprint per page, so raise `max_pages` deliberately for very long traversals.
474
+ Strict keyset and cursor traversal retain only monotonic progression state when sufficient.
440
475
 
441
476
  ## CLI
442
477
 
@@ -445,10 +480,10 @@ errors go to stderr. Credentials come only from `Settings` and cannot be passed
445
480
 
446
481
  <!-- tested-console: tests/cli_test.py::test_call_routes_replay_safety_and_keeps_success_data_on_stdout -->
447
482
  ```console
448
- b24api call profile
449
- b24api call example.item.get --params '{"id":7}' --raw --replay-safety safe
450
- b24api list example.item.list --params @params.json
451
- b24api list example.item.list --strategy counted --contract @counted-contract.json
483
+ b24api call profile --route bare
484
+ b24api call example.item.get --route bare --params '{"id":7}' --raw --replay-safety safe
485
+ b24api list example.item.list --route bare --params @params.json
486
+ b24api list example.item.list --route bare --strategy counted --contract @counted-contract.json
452
487
  ```
453
488
 
454
489
  The `--raw` CLI option selects the response envelope; it does not alter the Python API. Advanced
@@ -1,15 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: b24api
3
- Version: 2.2.0
4
- Summary: Bitrix24 API
5
- Requires-Python: >=3.12
6
- Description-Content-Type: text/markdown
7
- License-File: LICENSE
8
- Requires-Dist: httpx[http2]>=0.28.1
9
- Requires-Dist: pydantic>=2.11.7
10
- Requires-Dist: pydantic-settings>=2.10.1
11
- Dynamic: license-file
12
-
13
1
  # b24api 2.x
14
2
 
15
3
  `b24api` is a thin asynchronous Bitrix24 REST client for Python 3.12+. It knows how to send
@@ -29,10 +17,10 @@ work so its HTTP/2 connection pool and rate state are reused.
29
17
 
30
18
  <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
31
19
  ```python
32
- from b24api import Bitrix24, Request
20
+ from b24api import Bitrix24, Request, RouteKind
33
21
 
34
22
  async with Bitrix24() as client:
35
- profile = await client.call(Request("profile"))
23
+ profile = await client.call(Request("profile", route=RouteKind.BARE))
36
24
  ```
37
25
 
38
26
  The client owns its default transport. An injected transport remains caller-owned. `aclose()` is
@@ -48,15 +36,17 @@ The operation is explicit and never hides malformed JSON by falling back to byte
48
36
 
49
37
  <!-- tested: tests/client_findings_3_test.py::test_binary_call_returns_every_success_byte_without_json_sniffing -->
50
38
  ```python
51
- archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE))
39
+ from b24api import RouteKind
40
+ archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE))
52
41
  payload = archive.body
53
42
  ```
54
43
 
55
44
  <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
56
45
  ```python
46
+ from b24api import RouteKind
57
47
  from b24api import ReplaySafety
58
48
 
59
- request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE)
49
+ request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE, route=RouteKind.BARE)
60
50
  decoded = await client.call(request)
61
51
  response = await client.call_response(request)
62
52
  ```
@@ -97,11 +87,12 @@ matching a result to the object, file, chat or database row that produced its re
97
87
 
98
88
  <!-- tested: tests/client_v2_test.py::test_logical_batch_is_unbounded_ordered_and_correlation_is_strictly_off_wire -->
99
89
  ```python
90
+ from b24api import RouteKind
100
91
  from b24api import Command, CommandSuccess
101
92
 
102
93
  commands = (
103
94
  Command(
104
- Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE),
95
+ Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE, route=RouteKind.BARE),
105
96
  correlation=item_id,
106
97
  )
107
98
  for item_id in source_ids
@@ -166,6 +157,7 @@ control this strategy's completion. Exact database implementation is endpoint-sp
166
157
 
167
158
  <!-- tested: tests/client_v2_test.py::test_iter_list_is_sequential_mechanics_only_and_report_is_post_cleanup -->
168
159
  ```python
160
+ from b24api import RouteKind
169
161
  from b24api import IdentityCoercion, IdentitySpec, ResultSelector
170
162
 
171
163
  identity = IdentitySpec(
@@ -176,7 +168,7 @@ identity = IdentitySpec(
176
168
  )
177
169
 
178
170
  stream = client.iter_list(
179
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
171
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
180
172
  selector=ResultSelector(("items",)),
181
173
  identity=identity,
182
174
  )
@@ -194,10 +186,11 @@ sequence and records that degradation in the operation report.
194
186
 
195
187
  <!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
196
188
  ```python
189
+ from b24api import RouteKind
197
190
  from b24api import ResultCollectionShape
198
191
 
199
192
  stream = client.iter_list(
200
- Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
193
+ Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
201
194
  selector=ResultSelector(("items",)),
202
195
  collection_shape=ResultCollectionShape.MAPPING_VALUES,
203
196
  )
@@ -214,8 +207,9 @@ bounded physical batches.
214
207
 
215
208
  <!-- tested: tests/client_v2_test.py::test_counted_traversal_preserves_frozen_request_shape_and_exact_identity -->
216
209
  ```python
210
+ from b24api import RouteKind
217
211
  stream = client.iter_list_counted(
218
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
212
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
219
213
  selector=ResultSelector(("items",)),
220
214
  identity=identity,
221
215
  page_size=50,
@@ -257,6 +251,9 @@ representative fixture. It performs five strict-bound checks and returns only a
257
251
  unsupported and inconclusive verdicts raise `KeysetCapabilityError`. The ordinary
258
252
  `iter_list_keyset()` remains a separate caller-asserted operation with zero verifier canaries and
259
253
  may emit a partial prefix before a late endpoint contradiction is detected.
254
+ Keep the guard beside the traversal, for example under
255
+ `if os.environ.get("ENV") != "PROD":`; set `ENV=PROD` only after qualifying the exact portal,
256
+ credentials, method, request/filter, identity, ordering representation, and page cap.
260
257
 
261
258
  Every list operation also accepts an immutable `PageAdapter` strategy. The adapter synchronously
262
259
  maps selected frozen items using sibling result metadata while preserving cardinality, order and
@@ -269,16 +266,33 @@ record the selected strategy and reason: unbounded auto continuation has the sam
269
266
 
270
267
  <!-- tested: tests/keyset_fast_test.py::test_omitted_execution_defaults_to_auto -->
271
268
  ```python
272
- from b24api import KeysetSpec, ParameterPath
269
+ import os
270
+
271
+ from b24api import KeysetSpec, ParameterPath, ReplaySafety, Request, ResultSelector, RouteKind
272
+
273
+ request = Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE)
274
+ selector = ResultSelector(("items",))
275
+ keyset = KeysetSpec(
276
+ filter_path=ParameterPath(("filter",)),
277
+ order_path=ParameterPath(("order",)),
278
+ )
279
+
280
+ if os.environ.get("ENV") != "PROD":
281
+ # Accepting an ID filter does not prove strict bounds or ordering.
282
+ await client.verify_keyset_capability(
283
+ request,
284
+ selector=selector,
285
+ identity=identity,
286
+ page_size=50,
287
+ keyset=keyset,
288
+ )
273
289
 
274
290
  stream = client.iter_list_keyset(
275
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
276
- selector=ResultSelector(("items",)),
291
+ request,
292
+ selector=selector,
277
293
  identity=identity,
278
- keyset=KeysetSpec(
279
- filter_path=ParameterPath(("filter",)),
280
- order_path=ParameterPath(("order",)),
281
- ),
294
+ page_size=50,
295
+ keyset=keyset,
282
296
  )
283
297
  ```
284
298
 
@@ -289,10 +303,11 @@ message-list methods.
289
303
 
290
304
  <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
291
305
  ```python
306
+ from b24api import RouteKind
292
307
  from b24api import CursorSpec, ParameterPath
293
308
 
294
309
  stream = client.iter_list_cursor(
295
- Request("example.message.list", replay_safety=ReplaySafety.SAFE),
310
+ Request("example.message.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
296
311
  selector=ResultSelector(("items",)),
297
312
  cursor=CursorSpec(
298
313
  parameter_path=ParameterPath(("LAST_ID",)),
@@ -326,6 +341,7 @@ caller-defined parent.
326
341
 
327
342
  <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
328
343
  ```python
344
+ from b24api import RouteKind
329
345
  from b24api import (
330
346
  BatchDispatch,
331
347
  Binding,
@@ -346,7 +362,7 @@ bindings = (
346
362
  )
347
363
 
348
364
  stream = client.iter_references(
349
- Request("example.comment.list", replay_safety=ReplaySafety.SAFE),
365
+ Request("example.comment.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
350
366
  bindings,
351
367
  traversal=SequentialTraversal(selector=ResultSelector(("items",)), identity=identity),
352
368
  dispatch=BatchDispatch(batch_size=25, concurrency=2),
@@ -366,6 +382,7 @@ under different parents are not conflated.
366
382
 
367
383
  <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
368
384
  ```python
385
+ from b24api import RouteKind
369
386
  from b24api import (
370
387
  Binding,
371
388
  CursorSpec,
@@ -387,7 +404,7 @@ chat_bindings = (
387
404
  )
388
405
 
389
406
  messages = client.iter_references(
390
- Request("example.message.list", replay_safety=ReplaySafety.SAFE),
407
+ Request("example.message.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
391
408
  chat_bindings,
392
409
  traversal=CursorTraversal(
393
410
  selector=ResultSelector(("items",)),
@@ -430,13 +447,19 @@ and publish the same final report where the Python exception type permits it.
430
447
  ## Resource boundaries
431
448
 
432
449
  `ExecutionPolicy` bounds requests, pages, elapsed time, attempts, decompressed response bytes,
433
- buffered commands and rows, direct concurrency and active references. The default response ceiling
434
- is 16 MiB and is enforced while streaming, before JSON decoding.
435
-
436
- Sequential and counted exact traversal retain observed identities in memory. There is no database,
437
- spill file or identity-count refusal. Crossing 100,000 distinct identities emits one
438
- `RuntimeWarning`; exact tracking continues. Strict keyset and cursor traversal retain only
439
- monotonic progression state when sufficient.
450
+ buffered commands and rows, retained unordered identity keys, direct concurrency and active
451
+ references. The default response ceiling is 16 MiB and is enforced while streaming, before JSON
452
+ decoding.
453
+
454
+ Sequential, counted, and multi-reference exact traversal retain at most `max_identity_keys`
455
+ observed identities per operation in memory (100,000 by default). All active reference bindings
456
+ share that ceiling. A page that would exceed it is rejected atomically with typed budget evidence.
457
+ Set a larger finite ceiling when the expected aggregate cardinality is known, or pass
458
+ `identity_store=` to `iter_list`/`iter_list_counted` so a caller-owned `IdentityStore` (for example a
459
+ SQLite table keyed by `identity_store_key(...)`) proves uniqueness while in-process identity memory
460
+ stays bounded by one page; the client never closes that store. Repeated-page detection still keeps
461
+ one short fingerprint per page, so raise `max_pages` deliberately for very long traversals.
462
+ Strict keyset and cursor traversal retain only monotonic progression state when sufficient.
440
463
 
441
464
  ## CLI
442
465
 
@@ -445,10 +468,10 @@ errors go to stderr. Credentials come only from `Settings` and cannot be passed
445
468
 
446
469
  <!-- tested-console: tests/cli_test.py::test_call_routes_replay_safety_and_keeps_success_data_on_stdout -->
447
470
  ```console
448
- b24api call profile
449
- b24api call example.item.get --params '{"id":7}' --raw --replay-safety safe
450
- b24api list example.item.list --params @params.json
451
- b24api list example.item.list --strategy counted --contract @counted-contract.json
471
+ b24api call profile --route bare
472
+ b24api call example.item.get --route bare --params '{"id":7}' --raw --replay-safety safe
473
+ b24api list example.item.list --route bare --params @params.json
474
+ b24api list example.item.list --route bare --strategy counted --contract @counted-contract.json
452
475
  ```
453
476
 
454
477
  The `--raw` CLI option selects the response envelope; it does not alter the Python API. Advanced