b24api 2.2.0__tar.gz → 2.4.2__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 (156) hide show
  1. {b24api-2.2.0 → b24api-2.4.2}/PKG-INFO +182 -64
  2. b24api-2.2.0/b24api.egg-info/PKG-INFO → b24api-2.4.2/README.md +160 -73
  3. b24api-2.4.2/b24api/__init__.py +134 -0
  4. {b24api-2.2.0 → b24api-2.4.2}/b24api/_client_traversal.py +15 -0
  5. b24api-2.4.2/b24api/_diagnostics.py +138 -0
  6. b24api-2.4.2/b24api/_sources.py +172 -0
  7. {b24api-2.2.0 → b24api-2.4.2}/b24api/batch/engine.py +255 -52
  8. {b24api-2.2.0 → b24api-2.4.2}/b24api/batch/facade.py +11 -12
  9. b24api-2.4.2/b24api/batch/logical.py +396 -0
  10. b24api-2.4.2/b24api/batch/stream.py +272 -0
  11. {b24api-2.2.0 → b24api-2.4.2}/b24api/cli.py +11 -5
  12. {b24api-2.2.0 → b24api-2.4.2}/b24api/cli_contract.py +21 -8
  13. {b24api-2.2.0 → b24api-2.4.2}/b24api/client.py +50 -18
  14. b24api-2.4.2/b24api/completion/__init__.py +5 -0
  15. b24api-2.4.2/b24api/completion/closure.py +31 -0
  16. b24api-2.4.2/b24api/completion/fast_recorder.py +184 -0
  17. b24api-2.4.2/b24api/completion/gate.py +578 -0
  18. b24api-2.4.2/b24api/completion/operation_stream.py +294 -0
  19. b24api-2.4.2/b24api/completion/recorder.py +275 -0
  20. b24api-2.4.2/b24api/completion/reference_recorder.py +172 -0
  21. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/__init__.py +80 -1
  22. b24api-2.4.2/b24api/contracts/bounded_range.py +81 -0
  23. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/command.py +16 -3
  24. b24api-2.4.2/b24api/contracts/completion.py +173 -0
  25. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/dispatch.py +5 -4
  26. b24api-2.4.2/b24api/contracts/error_base.py +75 -0
  27. b24api-2.4.2/b24api/contracts/evidence.py +41 -0
  28. b24api-2.4.2/b24api/contracts/identity_store.py +45 -0
  29. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/keyset_capability.py +1 -1
  30. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/keyset_execution.py +4 -2
  31. b24api-2.4.2/b24api/contracts/page_stop.py +68 -0
  32. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/policy.py +31 -11
  33. b24api-2.4.2/b24api/contracts/positional.py +254 -0
  34. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/reference.py +17 -2
  35. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/report.py +55 -56
  36. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/request.py +125 -51
  37. b24api-2.4.2/b24api/contracts/request_summary.py +63 -0
  38. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/response.py +6 -33
  39. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/traversal.py +142 -1
  40. b24api-2.4.2/b24api/contracts/v3_codes.py +61 -0
  41. b24api-2.4.2/b24api/contracts/violation.py +98 -0
  42. {b24api-2.2.0 → b24api-2.4.2}/b24api/errors.py +99 -92
  43. {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/__init__.py +14 -1
  44. b24api-2.4.2/b24api/execution/boundary.py +80 -0
  45. {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/cleanup.py +15 -12
  46. {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/context.py +109 -70
  47. {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/executor.py +270 -189
  48. {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/failure.py +88 -12
  49. b24api-2.4.2/b24api/execution/lifecycle.py +400 -0
  50. b24api-2.4.2/b24api/execution/rate.py +341 -0
  51. {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/snapshot.py +13 -4
  52. b24api-2.4.2/b24api/execution/throttle.py +107 -0
  53. b24api-2.4.2/b24api/migration.py +260 -0
  54. b24api-2.4.2/b24api/py.typed +0 -0
  55. b24api-2.4.2/b24api/redaction.py +339 -0
  56. {b24api-2.2.0 → b24api-2.4.2}/b24api/references/binding.py +29 -113
  57. {b24api-2.2.0 → b24api-2.4.2}/b24api/references/dispatch.py +42 -33
  58. b24api-2.4.2/b24api/references/dispatch_plan.py +26 -0
  59. {b24api-2.2.0 → b24api-2.4.2}/b24api/references/facade.py +68 -71
  60. {b24api-2.2.0 → b24api-2.4.2}/b24api/references/fanout.py +39 -86
  61. {b24api-2.2.0 → b24api-2.4.2}/b24api/references/outcome.py +4 -7
  62. {b24api-2.2.0 → b24api-2.4.2}/b24api/references/scheduler.py +251 -225
  63. b24api-2.4.2/b24api/references/stream.py +274 -0
  64. {b24api-2.2.0 → b24api-2.4.2}/b24api/references/support.py +41 -67
  65. {b24api-2.2.0 → b24api-2.4.2}/b24api/testing/__init__.py +3 -0
  66. b24api-2.4.2/b24api/testing/scripted.py +147 -0
  67. {b24api-2.2.0 → b24api-2.4.2}/b24api/testing/transport.py +39 -7
  68. {b24api-2.2.0 → b24api-2.4.2}/b24api/transport/base.py +59 -4
  69. b24api-2.4.2/b24api/transport/decoding.py +207 -0
  70. {b24api-2.2.0 → b24api-2.4.2}/b24api/transport/httpx.py +144 -47
  71. b24api-2.4.2/b24api/transport/log_records.py +111 -0
  72. b24api-2.4.2/b24api/transport/logging_shield.py +366 -0
  73. b24api-2.4.2/b24api/transport/protocol.py +306 -0
  74. b24api-2.4.2/b24api/traversal/control_preflight.py +94 -0
  75. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/counted.py +68 -7
  76. b24api-2.4.2/b24api/traversal/counted_batch.py +369 -0
  77. b24api-2.4.2/b24api/traversal/counted_rules.py +74 -0
  78. b24api-2.4.2/b24api/traversal/cursor.py +106 -0
  79. b24api-2.4.2/b24api/traversal/cursor_domain.py +61 -0
  80. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/driver.py +205 -124
  81. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/facade.py +76 -59
  82. b24api-2.4.2/b24api/traversal/facade_support.py +36 -0
  83. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/identity.py +59 -83
  84. b24api-2.4.2/b24api/traversal/identity_ledger.py +59 -0
  85. b24api-2.4.2/b24api/traversal/keyset.py +67 -0
  86. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_auto.py +11 -20
  87. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_capability.py +8 -107
  88. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_eligibility.py +61 -20
  89. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_fast_plan.py +1 -39
  90. b24api-2.4.2/b24api/traversal/keyset_fast_stream.py +174 -0
  91. b24api-2.4.2/b24api/traversal/keyset_geometry.py +313 -0
  92. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_observation.py +125 -83
  93. b24api-2.2.0/b24api/traversal/ordered_admission.py → b24api-2.4.2/b24api/traversal/keyset_ordered_admission.py +4 -13
  94. b24api-2.2.0/b24api/traversal/page_validation.py → b24api-2.4.2/b24api/traversal/keyset_page_validation.py +23 -12
  95. b24api-2.4.2/b24api/traversal/keyset_plan.py +453 -0
  96. b24api-2.4.2/b24api/traversal/keyset_reporting.py +96 -0
  97. b24api-2.4.2/b24api/traversal/keyset_runtime.py +413 -0
  98. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_step.py +46 -3
  99. b24api-2.4.2/b24api/traversal/keyset_transaction_contract.py +195 -0
  100. b24api-2.4.2/b24api/traversal/keyset_transactions.py +451 -0
  101. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_verifier.py +68 -16
  102. b24api-2.4.2/b24api/traversal/offset_rules.py +159 -0
  103. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/page_adaptation.py +3 -2
  104. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/plans.py +44 -11
  105. b24api-2.4.2/b24api/traversal/sequential.py +232 -0
  106. b24api-2.4.2/b24api/traversal/sparse.py +55 -0
  107. b24api-2.4.2/b24api/traversal/strategy_context.py +181 -0
  108. b24api-2.4.2/b24api/traversal/stream.py +299 -0
  109. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/values.py +9 -4
  110. b24api-2.2.0/README.md → b24api-2.4.2/b24api.egg-info/PKG-INFO +191 -61
  111. {b24api-2.2.0 → b24api-2.4.2}/b24api.egg-info/SOURCES.txt +42 -8
  112. b24api-2.4.2/b24api.egg-info/requires.txt +5 -0
  113. b24api-2.4.2/pyproject.toml +132 -0
  114. b24api-2.2.0/b24api/__init__.py +0 -253
  115. b24api-2.2.0/b24api/_audit.py +0 -78
  116. b24api-2.2.0/b24api/_stream.py +0 -323
  117. b24api-2.2.0/b24api/batch/logical.py +0 -348
  118. b24api-2.2.0/b24api/batch/stream.py +0 -435
  119. b24api-2.2.0/b24api/execution/rate.py +0 -220
  120. b24api-2.2.0/b24api/redaction.py +0 -205
  121. b24api-2.2.0/b24api/references/stream.py +0 -376
  122. b24api-2.2.0/b24api/transport/protocol.py +0 -157
  123. b24api-2.2.0/b24api/traversal/counted_batch.py +0 -260
  124. b24api-2.2.0/b24api/traversal/cursor.py +0 -98
  125. b24api-2.2.0/b24api/traversal/keyset.py +0 -64
  126. b24api-2.2.0/b24api/traversal/keyset_costs.py +0 -185
  127. b24api-2.2.0/b24api/traversal/keyset_fast_stream.py +0 -274
  128. b24api-2.2.0/b24api/traversal/keyset_partition.py +0 -27
  129. b24api-2.2.0/b24api/traversal/keyset_range.py +0 -80
  130. b24api-2.2.0/b24api/traversal/keyset_reporting.py +0 -111
  131. b24api-2.2.0/b24api/traversal/keyset_scheduler.py +0 -626
  132. b24api-2.2.0/b24api/traversal/keyset_transaction_contract.py +0 -108
  133. b24api-2.2.0/b24api/traversal/keyset_transactions.py +0 -385
  134. b24api-2.2.0/b24api/traversal/sequential.py +0 -199
  135. b24api-2.2.0/b24api/traversal/stream.py +0 -325
  136. b24api-2.2.0/b24api.egg-info/requires.txt +0 -3
  137. b24api-2.2.0/pyproject.toml +0 -93
  138. {b24api-2.2.0 → b24api-2.4.2}/LICENSE +0 -0
  139. {b24api-2.2.0 → b24api-2.4.2}/MANIFEST.in +0 -0
  140. {b24api-2.2.0 → b24api-2.4.2}/b24api/_error_types.py +0 -0
  141. {b24api-2.2.0 → b24api-2.4.2}/b24api/batch/__init__.py +0 -0
  142. {b24api-2.2.0 → b24api-2.4.2}/b24api/batch/outcome.py +0 -0
  143. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/json.py +0 -0
  144. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/page.py +0 -0
  145. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/stream.py +0 -0
  146. {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/wire.py +0 -0
  147. {b24api-2.2.0 → b24api-2.4.2}/b24api/encoding.py +0 -0
  148. {b24api-2.2.0 → b24api-2.4.2}/b24api/references/__init__.py +0 -0
  149. {b24api-2.2.0 → b24api-2.4.2}/b24api/settings.py +0 -0
  150. {b24api-2.2.0 → b24api-2.4.2}/b24api/testing/_isolation.py +0 -0
  151. {b24api-2.2.0 → b24api-2.4.2}/b24api/transport/__init__.py +0 -0
  152. {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/__init__.py +0 -0
  153. {b24api-2.2.0 → b24api-2.4.2}/b24api.egg-info/dependency_links.txt +0 -0
  154. {b24api-2.2.0 → b24api-2.4.2}/b24api.egg-info/entry_points.txt +0 -0
  155. {b24api-2.2.0 → b24api-2.4.2}/b24api.egg-info/top_level.txt +0 -0
  156. {b24api-2.2.0 → b24api-2.4.2}/setup.cfg +0 -0
@@ -1,16 +1,35 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b24api
3
- Version: 2.2.0
3
+ Version: 2.4.2
4
4
  Summary: Bitrix24 API
5
+ Author-email: Shkarupa Alex <shkarupa.alex@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/shkarupa-alex/b24api
8
+ Project-URL: Repository, https://github.com/shkarupa-alex/b24api
9
+ Project-URL: Issues, https://github.com/shkarupa-alex/b24api/issues
10
+ Keywords: bitrix24,rest,api,client,async,httpx,batch,pagination
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
5
22
  Requires-Python: >=3.12
6
23
  Description-Content-Type: text/markdown
7
24
  License-File: LICENSE
8
- Requires-Dist: httpx[http2]>=0.28.1
9
- Requires-Dist: pydantic>=2.11.7
25
+ Requires-Dist: httpx[http2]<0.29,>=0.28.1
26
+ Requires-Dist: h2<4.5,>=4.3.0
27
+ Requires-Dist: hpack<4.3,>=4.1.0
28
+ Requires-Dist: pydantic>=2.12.0
10
29
  Requires-Dist: pydantic-settings>=2.10.1
11
30
  Dynamic: license-file
12
31
 
13
- # b24api 2.x
32
+ # b24api 3.x
14
33
 
15
34
  `b24api` is a thin asynchronous Bitrix24 REST client for Python 3.12+. It knows how to send
16
35
  requests, split logical batches, traverse lists, retry safely, preserve caller correlation and
@@ -27,16 +46,42 @@ export BITRIX24_API_WEBHOOK_URL='https://portal.example/rest/.../'
27
46
  Keep the webhook out of source, logs and command arguments. Reuse one client for a related unit of
28
47
  work so its HTTP/2 connection pool and rate state are reused.
29
48
 
30
- <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
49
+ ## Quickstart
50
+
51
+ <!-- tested: tests/readme_test.py::test_quickstart_runs_exactly_against_a_scripted_portal -->
31
52
  ```python
53
+ import os
54
+
32
55
  from b24api import Bitrix24, Request
33
56
 
34
- async with Bitrix24() as client:
35
- profile = await client.call(Request("profile"))
57
+ async with Bitrix24.from_webhook(os.environ["BITRIX24_API_WEBHOOK_URL"]) as client:
58
+ deals = client.iter_list(Request.bare("crm.deal.list", {"select": ["ID", "TITLE"]}))
59
+ async for deal in deals:
60
+ print(deal["ID"], deal["TITLE"])
61
+ print(deals.report.state)
62
+ ```
63
+
64
+ Four things are at work:
65
+
66
+ - **The client.** `Bitrix24.from_webhook()` checks the URL and owns the connection pool it opens;
67
+ `async with` closes it. `Bitrix24()` reads the same URL from the environment.
68
+ - **The request.** `Request.bare()` names a REST method and its parameters and sends them to the
69
+ classic `/rest/` endpoint. The route is always explicit: `Request.v3()` targets the V3 API.
70
+ - **`iter_list()`.** It walks the list page by page and reads one more, empty, page to confirm
71
+ the end.
72
+ - **The report.** `deals.report` says how the traversal ended. `completed` means every page was
73
+ read; a traversal that stops early or fails records why.
74
+
75
+ A single call returns the decoded `result`:
76
+
77
+ <!-- tested: tests/readme_test.py::test_quickstart_runs_exactly_against_a_scripted_portal -->
78
+ ```python
79
+ users = await client.call(Request.bare("user.get", {"ID": 1}))
36
80
  ```
37
81
 
38
- The client owns its default transport. An injected transport remains caller-owned. `aclose()` is
39
- idempotent and closes active streams before the owned transport.
82
+ The client owns the transport it creates. An injected transport remains caller-owned. `aclose()` is
83
+ idempotent and closes active streams before the owned transport. To prove that no row is missing or
84
+ repeated, give the traversal an identity (see [Choosing a list operation](#choosing-a-list-operation)).
40
85
 
41
86
  ## Direct calls
42
87
 
@@ -48,15 +93,17 @@ The operation is explicit and never hides malformed JSON by falling back to byte
48
93
 
49
94
  <!-- tested: tests/client_findings_3_test.py::test_binary_call_returns_every_success_byte_without_json_sniffing -->
50
95
  ```python
51
- archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE))
96
+ from b24api import RouteKind
97
+ archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE))
52
98
  payload = archive.body
53
99
  ```
54
100
 
55
101
  <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
56
102
  ```python
103
+ from b24api import RouteKind
57
104
  from b24api import ReplaySafety
58
105
 
59
- request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE)
106
+ request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE, route=RouteKind.BARE)
60
107
  decoded = await client.call(request)
61
108
  response = await client.call_response(request)
62
109
  ```
@@ -69,10 +116,17 @@ may already have reached Bitrix:
69
116
  | Value | Meaning | After possible dispatch |
70
117
  |---|---|---|
71
118
  | `SAFE` | Repeating the request cannot create a second business effect. Typical reads and explicitly idempotent operations belong here. | Automatic retry is allowed within policy budgets. |
72
- | `UNSAFE` | Repeating the request is known to risk a duplicate effect, for example creating an entity without an idempotency key. | No automatic replay; the caller receives an ambiguous-execution error and reconciles state. |
119
+ | `UNSAFE` | Repeating the request is known to risk a duplicate effect, for example creating an entity without an idempotency key. | No automatic replay; the caller receives an ambiguous-execution error and reconciles state. It is retried only when the failure proves it did not run. |
73
120
  | `UNKNOWN` | The caller has not established whether replay is safe. This is the default. | Same conservative behavior as `UNSAFE`, while diagnostics preserve that safety was unknown rather than known unsafe. |
74
121
 
75
- A failure proved to occur before dispatch may still be retried. Method names never imply safety;
122
+ A failure that proves the request did not run is retried whatever its safety: a transport failure
123
+ before dispatch, or a refusal listed in `AmbiguityPolicy`, which by default is an unstructured 423, 425
124
+ or 429 (`refusal_http_statuses`) or a `QUERY_LIMIT_EXCEEDED` / `OPERATION_TIME_LIMIT` refusal
125
+ (`refusal_api_codes`). A code or status added only to `RetryPolicy` is retried for `SAFE` work alone.
126
+ A physical batch applies these rules to each command: after a failure, only the commands that
127
+ may run again are sent again, in a smaller batch, and an `UNSAFE` command of a batch that may have run
128
+ arrives as unknown. Replay rounds spend the same attempt and retry-time budget as the sends before
129
+ them. Method names never imply safety;
76
130
  mark a request `SAFE` only when the operation's semantics justify it.
77
131
 
78
132
  Use `ExecutionPolicy` to narrow attempts or resource budgets for one operation:
@@ -85,6 +139,22 @@ one_attempt = ExecutionPolicy(max_attempts_per_request=1)
85
139
  result = await client.call(request, policy=one_attempt)
86
140
  ```
87
141
 
142
+ A `policy=` argument replaces the client's default policy wholesale; fields are never merged. The
143
+ client default is `ExecutionPolicy.from_settings(settings)`: the library defaults with
144
+ `max_retry_elapsed_per_request` taken from `Settings.http_timeout` (30 s by default, while a bare
145
+ `ExecutionPolicy()` allows 120 s). To change one field and keep the configured timeout, derive the
146
+ per-call policy from that default:
147
+
148
+ <!-- tested: tests/settings_test.py::test_a_per_call_policy_replaces_the_client_default_without_merging -->
149
+ ```python
150
+ import dataclasses
151
+
152
+ from b24api import ExecutionPolicy
153
+
154
+ one_attempt = dataclasses.replace(ExecutionPolicy.from_settings(settings), max_attempts_per_request=1)
155
+ result = await client.call(request, policy=one_attempt)
156
+ ```
157
+
88
158
  ## Logical batch and correlation
89
159
 
90
160
  `batch()` accepts an arbitrary-length synchronous or asynchronous command source. It consumes the
@@ -97,11 +167,12 @@ matching a result to the object, file, chat or database row that produced its re
97
167
 
98
168
  <!-- tested: tests/client_v2_test.py::test_logical_batch_is_unbounded_ordered_and_correlation_is_strictly_off_wire -->
99
169
  ```python
100
- from b24api import Command, CommandSuccess
170
+ from b24api import RouteKind
171
+ from b24api.contracts import Command, CommandSuccess
101
172
 
102
173
  commands = (
103
174
  Command(
104
- Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE),
175
+ Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE, route=RouteKind.BARE),
105
176
  correlation=item_id,
106
177
  )
107
178
  for item_id in source_ids
@@ -118,7 +189,7 @@ async with client.batch(commands, batch_size=25) as stream:
118
189
 
119
190
  <!-- tested: tests/client_v2_test.py::test_batch_outcomes_retains_typed_failure_without_halting_later_commands -->
120
191
  ```python
121
- from b24api import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
192
+ from b24api.contracts import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
122
193
 
123
194
  async with client.batch_outcomes(commands) as stream:
124
195
  async for outcome in stream:
@@ -151,7 +222,16 @@ exact `limit_path`; the client never guesses method-specific parameter names.
151
222
 
152
223
  ### List traversal comparison
153
224
 
154
- ![List traversal comparison](list-traversal-comparison.svg)
225
+ ![List traversal comparison](https://raw.githubusercontent.com/shkarupa-alex/b24api/master/list-traversal-comparison.svg)
226
+
227
+ The animation replays traces executed against a scripted portal with 1,000 dense IDs at 50 rows
228
+ per page. `iter_list` sends 20 pages and one empty confirmation (21 HTTP). `iter_list_counted` sends
229
+ the head and one batch of 19 pages (2 HTTP). `iter_list_keyset` in auto mode selects range
230
+ execution: it reads both ends of the range in one batch, the 19 ranges between them in a second, and
231
+ confirms the end with one call for `ID > 1000` (3 HTTP). A `BoundedIdentityRange` with a qualified
232
+ upper ID needs no confirmation: sequential execution stops when it receives that ID. On a real portal
233
+ auto chooses from the observed geometry, so the plan and its request count can differ; the report's
234
+ `keyset_selection` says which plan ran.
155
235
 
156
236
  ### Sequential offset
157
237
 
@@ -166,6 +246,7 @@ control this strategy's completion. Exact database implementation is endpoint-sp
166
246
 
167
247
  <!-- tested: tests/client_v2_test.py::test_iter_list_is_sequential_mechanics_only_and_report_is_post_cleanup -->
168
248
  ```python
249
+ from b24api import RouteKind
169
250
  from b24api import IdentityCoercion, IdentitySpec, ResultSelector
170
251
 
171
252
  identity = IdentitySpec(
@@ -176,7 +257,7 @@ identity = IdentitySpec(
176
257
  )
177
258
 
178
259
  stream = client.iter_list(
179
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
260
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
180
261
  selector=ResultSelector(("items",)),
181
262
  identity=identity,
182
263
  )
@@ -194,10 +275,11 @@ sequence and records that degradation in the operation report.
194
275
 
195
276
  <!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
196
277
  ```python
197
- from b24api import ResultCollectionShape
278
+ from b24api import RouteKind
279
+ from b24api.contracts import ResultCollectionShape
198
280
 
199
281
  stream = client.iter_list(
200
- Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
282
+ Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
201
283
  selector=ResultSelector(("items",)),
202
284
  collection_shape=ResultCollectionShape.MAPPING_VALUES,
203
285
  )
@@ -214,8 +296,9 @@ bounded physical batches.
214
296
 
215
297
  <!-- tested: tests/client_v2_test.py::test_counted_traversal_preserves_frozen_request_shape_and_exact_identity -->
216
298
  ```python
299
+ from b24api import RouteKind
217
300
  stream = client.iter_list_counted(
218
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
301
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
219
302
  selector=ResultSelector(("items",)),
220
303
  identity=identity,
221
304
  page_size=50,
@@ -226,6 +309,16 @@ stream = client.iter_list_counted(
226
309
  Use it only when `total` is exact for the supplied filter and offset pages are stable. Any missing
227
310
  range, overlap, duplicate identity or total contradiction raises `IncompleteTraversalError`.
228
311
 
312
+ A filtered call that matches nothing may omit `total` entirely, as `user.get` does with
313
+ `result: []`. A first page with no rows, no `next` and no usable `total` (missing, `null`, or the
314
+ `-1` unknown sentinel) therefore completes as an observed empty source after that one request: the
315
+ report is `completed` and `exhausted`, but its assurance is `mechanics_only` (`identity_exact` with an
316
+ identity), never a count-matched claim, and no total is invented. A first page that reports
317
+ `total: 0` keeps the count-matched result. Rows without a usable total, a remaining `next`, a
318
+ positive total with no rows, a fixed step, or a `ConsistencyPolicy` whose `confirmation_policy` is
319
+ `QUALIFIED_TOTAL` (from `b24api.contracts.policy.ConfirmationPolicy`) stay strict and raise
320
+ `IncompleteTraversalError`.
321
+
229
322
  Physical batching reduces HTTP exchanges, but does not suppress server-side COUNT in ordinary
230
323
  counted list subrequests. Each command still performs its own offset page retrieval and associated
231
324
  total calculation on the server. Do not confuse batching these commands with a no-count traversal.
@@ -249,14 +342,19 @@ Static incompatibility with the auto contract raises `CapabilityError` from the
249
342
  `iter_list_keyset(...)` call before iteration begins. A portal that accepts but contradicts the
250
343
  declared controls fails before emission with `IncompleteTraversalError`; auto never restarts that
251
344
  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.
345
+ terminal report carries a compact `keyset_selection` (`requested_kind`, `selected_kind`, `reason`)
346
+ for every keyset traversal. A `page_stop` callback needs the ordered page stream, so auto reports
347
+ `AUTO`, `SEQUENTIAL`, `PAGE_STOP`; an explicit `SequentialKeysetExecution()` reports
348
+ `EXPLICIT_SEQUENTIAL`. The detailed `keyset_execution` report is present only when the fast path ran.
254
349
 
255
350
  Use `await client.verify_keyset_capability(...)` as a development/CI/staging guard on a stable
256
351
  representative fixture. It performs five strict-bound checks and returns only a `VERIFIED` report;
257
352
  unsupported and inconclusive verdicts raise `KeysetCapabilityError`. The ordinary
258
353
  `iter_list_keyset()` remains a separate caller-asserted operation with zero verifier canaries and
259
354
  may emit a partial prefix before a late endpoint contradiction is detected.
355
+ Keep the guard beside the traversal, for example under
356
+ `if os.environ.get("ENV") != "PROD":`; set `ENV=PROD` only after qualifying the exact portal,
357
+ credentials, method, request/filter, identity, ordering representation, and page cap.
260
358
 
261
359
  Every list operation also accepts an immutable `PageAdapter` strategy. The adapter synchronously
262
360
  maps selected frozen items using sibling result metadata while preserving cardinality, order and
@@ -269,16 +367,33 @@ record the selected strategy and reason: unbounded auto continuation has the sam
269
367
 
270
368
  <!-- tested: tests/keyset_fast_test.py::test_omitted_execution_defaults_to_auto -->
271
369
  ```python
272
- from b24api import KeysetSpec, ParameterPath
370
+ import os
371
+
372
+ from b24api import KeysetSpec, ParameterPath, ReplaySafety, Request, ResultSelector, RouteKind
373
+
374
+ request = Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE)
375
+ selector = ResultSelector(("items",))
376
+ keyset = KeysetSpec(
377
+ filter_path=ParameterPath(("filter",)),
378
+ order_path=ParameterPath(("order",)),
379
+ )
380
+
381
+ if os.environ.get("ENV") != "PROD":
382
+ # Accepting an ID filter does not prove strict bounds or ordering.
383
+ await client.verify_keyset_capability(
384
+ request,
385
+ selector=selector,
386
+ identity=identity,
387
+ page_size=50,
388
+ keyset=keyset,
389
+ )
273
390
 
274
391
  stream = client.iter_list_keyset(
275
- Request("example.item.list", replay_safety=ReplaySafety.SAFE),
276
- selector=ResultSelector(("items",)),
392
+ request,
393
+ selector=selector,
277
394
  identity=identity,
278
- keyset=KeysetSpec(
279
- filter_path=ParameterPath(("filter",)),
280
- order_path=ParameterPath(("order",)),
281
- ),
395
+ page_size=50,
396
+ keyset=keyset,
282
397
  )
283
398
  ```
284
399
 
@@ -289,10 +404,11 @@ message-list methods.
289
404
 
290
405
  <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
291
406
  ```python
407
+ from b24api import RouteKind
292
408
  from b24api import CursorSpec, ParameterPath
293
409
 
294
410
  stream = client.iter_list_cursor(
295
- Request("example.message.list", replay_safety=ReplaySafety.SAFE),
411
+ Request("example.message.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
296
412
  selector=ResultSelector(("items",)),
297
413
  cursor=CursorSpec(
298
414
  parameter_path=ParameterPath(("LAST_ID",)),
@@ -310,10 +426,10 @@ boundary, use an application-owned direct-call workflow or supply a unique tie-b
310
426
  For multiple parent-bound cursor chains, `iter_cursors()` keeps cursor progress and correlation
311
427
  isolated per binding while ready pages share the physical batch queue.
312
428
 
313
- ![Cursor batching across independent chats](cursor-batching.svg)
429
+ ![Cursor batching across independent chats](https://raw.githubusercontent.com/shkarupa-alex/b24api/master/cursor-batching.svg)
314
430
 
315
- See [architecture](docs/architecture.md), [migration](docs/migration.md),
316
- [performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
431
+ See [architecture](https://github.com/shkarupa-alex/b24api/blob/master/docs/architecture.md), [migration](https://github.com/shkarupa-alex/b24api/blob/master/docs/migration.md),
432
+ [performance](https://github.com/shkarupa-alex/b24api/blob/master/docs/performance.md), and [endpoint recipes](https://github.com/shkarupa-alex/b24api/blob/master/docs/recipes.md) for the complete
317
433
  contracts and selection guidance.
318
434
 
319
435
  ### One list method across many parent entities
@@ -322,19 +438,13 @@ contracts and selection guidance.
322
438
  client remains unaware of entity types: a binding can represent a deal, lead, chat or any other
323
439
  caller-defined parent.
324
440
 
325
- ![Reference batching across leads and deals](references-batching.svg)
441
+ ![Reference batching across leads and deals](https://raw.githubusercontent.com/shkarupa-alex/b24api/master/references-batching.svg)
326
442
 
327
443
  <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
328
444
  ```python
329
- from b24api import (
330
- BatchDispatch,
331
- Binding,
332
- ParameterPath,
333
- ParameterUpdate,
334
- ReferenceComplete,
335
- ReferenceItem,
336
- SequentialTraversal,
337
- )
445
+ from b24api import RouteKind
446
+ from b24api import BatchDispatch, Binding, ParameterPath, ParameterUpdate, SequentialTraversal
447
+ from b24api.contracts import ReferenceComplete, ReferenceItem
338
448
 
339
449
  bindings = (
340
450
  Binding(
@@ -346,7 +456,7 @@ bindings = (
346
456
  )
347
457
 
348
458
  stream = client.iter_references(
349
- Request("example.comment.list", replay_safety=ReplaySafety.SAFE),
459
+ Request("example.comment.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
350
460
  bindings,
351
461
  traversal=SequentialTraversal(selector=ResultSelector(("items",)), identity=identity),
352
462
  dispatch=BatchDispatch(batch_size=25, concurrency=2),
@@ -366,6 +476,7 @@ under different parents are not conflated.
366
476
 
367
477
  <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
368
478
  ```python
479
+ from b24api import RouteKind
369
480
  from b24api import (
370
481
  Binding,
371
482
  CursorSpec,
@@ -387,7 +498,7 @@ chat_bindings = (
387
498
  )
388
499
 
389
500
  messages = client.iter_references(
390
- Request("example.message.list", replay_safety=ReplaySafety.SAFE),
501
+ Request("example.message.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
391
502
  chat_bindings,
392
503
  traversal=CursorTraversal(
393
504
  selector=ResultSelector(("items",)),
@@ -430,13 +541,19 @@ and publish the same final report where the Python exception type permits it.
430
541
  ## Resource boundaries
431
542
 
432
543
  `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.
544
+ buffered commands and rows, retained unordered identity keys, direct concurrency and active
545
+ references. The default response ceiling is 16 MiB and is enforced while streaming, before JSON
546
+ decoding.
547
+
548
+ Sequential, counted, and multi-reference exact traversal retain at most `max_identity_keys`
549
+ observed identities per operation in memory (100,000 by default). All active reference bindings
550
+ share that ceiling. A page that would exceed it is rejected atomically with typed budget evidence.
551
+ Set a larger finite ceiling when the expected aggregate cardinality is known, or pass
552
+ `identity_store=` to `iter_list`/`iter_list_counted` so a caller-owned `IdentityStore` (for example a
553
+ SQLite table keyed by `identity_store_key(...)`) proves uniqueness while in-process identity memory
554
+ stays bounded by one page; the client never closes that store. Repeated-page detection still keeps
555
+ one short fingerprint per page, so raise `max_pages` deliberately for very long traversals.
556
+ Strict keyset and cursor traversal retain only monotonic progression state when sufficient.
440
557
 
441
558
  ## CLI
442
559
 
@@ -445,10 +562,10 @@ errors go to stderr. Credentials come only from `Settings` and cannot be passed
445
562
 
446
563
  <!-- tested-console: tests/cli_test.py::test_call_routes_replay_safety_and_keeps_success_data_on_stdout -->
447
564
  ```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
565
+ b24api call profile --route bare
566
+ b24api call example.item.get --route bare --params '{"id":7}' --raw --replay-safety safe
567
+ b24api list example.item.list --route bare --params @params.json
568
+ b24api list example.item.list --route bare --strategy counted --contract @counted-contract.json
452
569
  ```
453
570
 
454
571
  The `--raw` CLI option selects the response envelope; it does not alter the Python API. Advanced
@@ -483,21 +600,22 @@ uv run --with memray memray stats /tmp/b24api.bin
483
600
  ```
484
601
 
485
602
  These deterministic fixtures characterize local resources and network shape; they are not live
486
- portal latency admission. See [docs/performance.md](docs/performance.md) for current measurements
487
- and [docs/architecture.md](docs/architecture.md) for guarantees and ownership boundaries.
603
+ portal latency admission. See [docs/performance.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/performance.md) for current measurements
604
+ and [docs/architecture.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/architecture.md) for guarantees and ownership boundaries.
488
605
 
489
- Projects moving from an earlier API surface can use [docs/migration.md](docs/migration.md).
606
+ Projects moving from an earlier API surface can use [docs/migration.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/migration.md).
490
607
 
491
608
  ## Verification
492
609
 
493
610
  ```console
494
611
  uv sync --frozen
495
- .venv/bin/pytest -q -p no:cacheprovider
496
- .venv/bin/ruff check . --no-fix --no-cache
497
- .venv/bin/ruff format --check . --no-cache
498
- .venv/bin/mypy --strict b24api tools/b24api_evidence
612
+ make qc
499
613
  git diff --check
500
614
  ```
501
615
 
616
+ `make qc` runs the lint, type and default test checks that CI blocks on. It leaves out the internal
617
+ benches (the pytest marker `slow`: the evidence harness contracts and the 50k/100k-scale runs, which
618
+ take several minutes). `make bench` runs them, and so does the blocking CI job `slow`.
619
+
502
620
  The wheel regression installs into an isolated environment, executes the `b24api` entry point and
503
621
  checks that tests, live/evidence tooling and credentials are excluded.