b24api 2.3.0__tar.gz → 2.5.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 (156) hide show
  1. {b24api-2.3.0 → b24api-2.5.0}/PKG-INFO +130 -37
  2. b24api-2.3.0/b24api.egg-info/PKG-INFO → b24api-2.5.0/README.md +109 -47
  3. b24api-2.5.0/b24api/__init__.py +134 -0
  4. b24api-2.5.0/b24api/_diagnostics.py +138 -0
  5. b24api-2.5.0/b24api/_sources.py +172 -0
  6. {b24api-2.3.0 → b24api-2.5.0}/b24api/batch/engine.py +190 -44
  7. {b24api-2.3.0 → b24api-2.5.0}/b24api/batch/facade.py +11 -12
  8. b24api-2.5.0/b24api/batch/logical.py +396 -0
  9. b24api-2.5.0/b24api/batch/stream.py +272 -0
  10. {b24api-2.3.0 → b24api-2.5.0}/b24api/cli.py +3 -1
  11. {b24api-2.3.0 → b24api-2.5.0}/b24api/cli_contract.py +12 -7
  12. {b24api-2.3.0 → b24api-2.5.0}/b24api/client.py +40 -15
  13. {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/closure.py +3 -0
  14. {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/fast_recorder.py +7 -18
  15. {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/gate.py +286 -158
  16. b24api-2.5.0/b24api/completion/operation_stream.py +294 -0
  17. {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/recorder.py +28 -17
  18. {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/reference_recorder.py +12 -12
  19. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/__init__.py +5 -2
  20. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/command.py +3 -3
  21. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/completion.py +18 -1
  22. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/dispatch.py +5 -4
  23. b24api-2.5.0/b24api/contracts/error_base.py +75 -0
  24. b24api-2.5.0/b24api/contracts/evidence.py +41 -0
  25. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/keyset_execution.py +4 -2
  26. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/policy.py +29 -11
  27. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/positional.py +24 -6
  28. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/reference.py +9 -2
  29. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/report.py +39 -15
  30. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/request.py +75 -53
  31. b24api-2.5.0/b24api/contracts/request_summary.py +63 -0
  32. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/response.py +6 -33
  33. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/traversal.py +46 -3
  34. b24api-2.5.0/b24api/contracts/v3_codes.py +61 -0
  35. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/violation.py +3 -7
  36. {b24api-2.3.0 → b24api-2.5.0}/b24api/errors.py +57 -90
  37. b24api-2.5.0/b24api/execution/boundary.py +80 -0
  38. {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/cleanup.py +15 -12
  39. {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/context.py +90 -70
  40. {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/executor.py +238 -145
  41. {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/failure.py +62 -3
  42. b24api-2.5.0/b24api/execution/lifecycle.py +400 -0
  43. {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/rate.py +93 -88
  44. {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/snapshot.py +8 -4
  45. b24api-2.5.0/b24api/execution/throttle.py +107 -0
  46. b24api-2.5.0/b24api/migration.py +260 -0
  47. b24api-2.5.0/b24api/py.typed +0 -0
  48. b24api-2.5.0/b24api/redaction.py +339 -0
  49. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/binding.py +92 -119
  50. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/dispatch.py +37 -31
  51. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/dispatch_plan.py +2 -3
  52. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/facade.py +31 -11
  53. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/fanout.py +35 -82
  54. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/outcome.py +4 -7
  55. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/scheduler.py +215 -231
  56. b24api-2.5.0/b24api/references/stream.py +274 -0
  57. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/support.py +15 -98
  58. {b24api-2.3.0 → b24api-2.5.0}/b24api/testing/scripted.py +4 -4
  59. {b24api-2.3.0 → b24api-2.5.0}/b24api/testing/transport.py +2 -1
  60. {b24api-2.3.0 → b24api-2.5.0}/b24api/transport/base.py +2 -1
  61. b24api-2.5.0/b24api/transport/decoding.py +207 -0
  62. {b24api-2.3.0 → b24api-2.5.0}/b24api/transport/httpx.py +48 -40
  63. b24api-2.5.0/b24api/transport/log_records.py +111 -0
  64. b24api-2.5.0/b24api/transport/logging_shield.py +366 -0
  65. {b24api-2.3.0 → b24api-2.5.0}/b24api/transport/protocol.py +81 -54
  66. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/control_preflight.py +23 -7
  67. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/counted.py +10 -5
  68. b24api-2.5.0/b24api/traversal/counted_batch.py +369 -0
  69. b24api-2.5.0/b24api/traversal/counted_rules.py +74 -0
  70. b24api-2.5.0/b24api/traversal/cursor.py +106 -0
  71. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/driver.py +161 -42
  72. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/facade.py +36 -23
  73. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/identity.py +7 -76
  74. b24api-2.5.0/b24api/traversal/keyset.py +67 -0
  75. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_auto.py +11 -20
  76. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_capability.py +8 -107
  77. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_eligibility.py +12 -8
  78. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_fast_plan.py +1 -39
  79. b24api-2.5.0/b24api/traversal/keyset_fast_stream.py +174 -0
  80. b24api-2.5.0/b24api/traversal/keyset_geometry.py +313 -0
  81. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_observation.py +125 -85
  82. b24api-2.3.0/b24api/traversal/ordered_admission.py → b24api-2.5.0/b24api/traversal/keyset_ordered_admission.py +2 -10
  83. b24api-2.3.0/b24api/traversal/page_validation.py → b24api-2.5.0/b24api/traversal/keyset_page_validation.py +3 -9
  84. b24api-2.5.0/b24api/traversal/keyset_plan.py +453 -0
  85. b24api-2.5.0/b24api/traversal/keyset_reporting.py +96 -0
  86. b24api-2.5.0/b24api/traversal/keyset_runtime.py +413 -0
  87. b24api-2.5.0/b24api/traversal/keyset_transaction_contract.py +195 -0
  88. b24api-2.5.0/b24api/traversal/keyset_transactions.py +451 -0
  89. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_verifier.py +35 -13
  90. b24api-2.5.0/b24api/traversal/offset_rules.py +198 -0
  91. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/page_adaptation.py +3 -2
  92. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/plans.py +40 -9
  93. b24api-2.5.0/b24api/traversal/sequential.py +244 -0
  94. b24api-2.5.0/b24api/traversal/strategy_context.py +181 -0
  95. b24api-2.5.0/b24api/traversal/stream.py +306 -0
  96. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/values.py +4 -4
  97. b24api-2.3.0/README.md → b24api-2.5.0/b24api.egg-info/PKG-INFO +140 -35
  98. {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/SOURCES.txt +20 -8
  99. {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/requires.txt +3 -1
  100. b24api-2.5.0/pyproject.toml +132 -0
  101. b24api-2.3.0/b24api/__init__.py +0 -335
  102. b24api-2.3.0/b24api/_audit.py +0 -78
  103. b24api-2.3.0/b24api/_stream.py +0 -274
  104. b24api-2.3.0/b24api/batch/logical.py +0 -433
  105. b24api-2.3.0/b24api/batch/stream.py +0 -436
  106. b24api-2.3.0/b24api/execution/throttle.py +0 -53
  107. b24api-2.3.0/b24api/redaction.py +0 -205
  108. b24api-2.3.0/b24api/references/stream.py +0 -394
  109. b24api-2.3.0/b24api/transport/logging_shield.py +0 -244
  110. b24api-2.3.0/b24api/traversal/counted_batch.py +0 -288
  111. b24api-2.3.0/b24api/traversal/cursor.py +0 -108
  112. b24api-2.3.0/b24api/traversal/keyset.py +0 -68
  113. b24api-2.3.0/b24api/traversal/keyset_costs.py +0 -185
  114. b24api-2.3.0/b24api/traversal/keyset_fast_stream.py +0 -311
  115. b24api-2.3.0/b24api/traversal/keyset_partition.py +0 -27
  116. b24api-2.3.0/b24api/traversal/keyset_range.py +0 -80
  117. b24api-2.3.0/b24api/traversal/keyset_reporting.py +0 -111
  118. b24api-2.3.0/b24api/traversal/keyset_scheduler.py +0 -669
  119. b24api-2.3.0/b24api/traversal/keyset_transaction_contract.py +0 -108
  120. b24api-2.3.0/b24api/traversal/keyset_transactions.py +0 -418
  121. b24api-2.3.0/b24api/traversal/offset_rules.py +0 -76
  122. b24api-2.3.0/b24api/traversal/sequential.py +0 -252
  123. b24api-2.3.0/b24api/traversal/stream.py +0 -412
  124. b24api-2.3.0/pyproject.toml +0 -94
  125. {b24api-2.3.0 → b24api-2.5.0}/LICENSE +0 -0
  126. {b24api-2.3.0 → b24api-2.5.0}/MANIFEST.in +0 -0
  127. {b24api-2.3.0 → b24api-2.5.0}/b24api/_client_traversal.py +0 -0
  128. {b24api-2.3.0 → b24api-2.5.0}/b24api/_error_types.py +0 -0
  129. {b24api-2.3.0 → b24api-2.5.0}/b24api/batch/__init__.py +0 -0
  130. {b24api-2.3.0 → b24api-2.5.0}/b24api/batch/outcome.py +0 -0
  131. {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/__init__.py +0 -0
  132. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/bounded_range.py +0 -0
  133. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/identity_store.py +0 -0
  134. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/json.py +0 -0
  135. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/keyset_capability.py +0 -0
  136. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/page.py +0 -0
  137. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/page_stop.py +0 -0
  138. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/stream.py +0 -0
  139. {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/wire.py +0 -0
  140. {b24api-2.3.0 → b24api-2.5.0}/b24api/encoding.py +0 -0
  141. {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/__init__.py +0 -0
  142. {b24api-2.3.0 → b24api-2.5.0}/b24api/references/__init__.py +0 -0
  143. {b24api-2.3.0 → b24api-2.5.0}/b24api/settings.py +0 -0
  144. {b24api-2.3.0 → b24api-2.5.0}/b24api/testing/__init__.py +0 -0
  145. {b24api-2.3.0 → b24api-2.5.0}/b24api/testing/_isolation.py +0 -0
  146. {b24api-2.3.0 → b24api-2.5.0}/b24api/transport/__init__.py +0 -0
  147. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/__init__.py +0 -0
  148. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/cursor_domain.py +0 -0
  149. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/facade_support.py +0 -0
  150. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/identity_ledger.py +0 -0
  151. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_step.py +0 -0
  152. {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/sparse.py +0 -0
  153. {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/dependency_links.txt +0 -0
  154. {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/entry_points.txt +0 -0
  155. {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/top_level.txt +0 -0
  156. {b24api-2.3.0 → b24api-2.5.0}/setup.cfg +0 -0
@@ -1,16 +1,35 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b24api
3
- Version: 2.3.0
3
+ Version: 2.5.0
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
25
  Requires-Dist: httpx[http2]<0.29,>=0.28.1
9
- Requires-Dist: pydantic>=2.11.7
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
32
- from b24api import Bitrix24, Request, RouteKind
53
+ import os
54
+
55
+ from b24api import Bitrix24, Request
56
+
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:
33
65
 
34
- async with Bitrix24() as client:
35
- profile = await client.call(Request("profile", route=RouteKind.BARE))
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
 
@@ -71,10 +116,17 @@ may already have reached Bitrix:
71
116
  | Value | Meaning | After possible dispatch |
72
117
  |---|---|---|
73
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. |
74
- | `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. |
75
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. |
76
121
 
77
- 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;
78
130
  mark a request `SAFE` only when the operation's semantics justify it.
79
131
 
80
132
  Use `ExecutionPolicy` to narrow attempts or resource budgets for one operation:
@@ -87,6 +139,22 @@ one_attempt = ExecutionPolicy(max_attempts_per_request=1)
87
139
  result = await client.call(request, policy=one_attempt)
88
140
  ```
89
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
+
90
158
  ## Logical batch and correlation
91
159
 
92
160
  `batch()` accepts an arbitrary-length synchronous or asynchronous command source. It consumes the
@@ -100,7 +168,7 @@ matching a result to the object, file, chat or database row that produced its re
100
168
  <!-- tested: tests/client_v2_test.py::test_logical_batch_is_unbounded_ordered_and_correlation_is_strictly_off_wire -->
101
169
  ```python
102
170
  from b24api import RouteKind
103
- from b24api import Command, CommandSuccess
171
+ from b24api.contracts import Command, CommandSuccess
104
172
 
105
173
  commands = (
106
174
  Command(
@@ -121,7 +189,7 @@ async with client.batch(commands, batch_size=25) as stream:
121
189
 
122
190
  <!-- tested: tests/client_v2_test.py::test_batch_outcomes_retains_typed_failure_without_halting_later_commands -->
123
191
  ```python
124
- from b24api import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
192
+ from b24api.contracts import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
125
193
 
126
194
  async with client.batch_outcomes(commands) as stream:
127
195
  async for outcome in stream:
@@ -154,7 +222,16 @@ exact `limit_path`; the client never guesses method-specific parameter names.
154
222
 
155
223
  ### List traversal comparison
156
224
 
157
- ![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.
158
235
 
159
236
  ### Sequential offset
160
237
 
@@ -192,6 +269,12 @@ async with stream:
192
269
  Without `identity`, successful exhaustion is reported as `MECHANICS_ONLY`: pagination completed,
193
270
  but the client cannot prove that the portal did not duplicate or substitute rows.
194
271
 
272
+ An endpoint that honors `start` only at a fixed server window and signals its end only with a
273
+ shorter page (no usable `total`, no `next`) can declare that stop rule with
274
+ `OffsetSpec(short_page_termination=ShortPageTermination.DECLARED_TERMINAL)`; its successful
275
+ exhaustion is always `MECHANICS_ONLY`. See
276
+ [Declared short-page closure](https://github.com/shkarupa-alex/b24api/blob/master/docs/recipes.md#declared-short-page-closure).
277
+
195
278
  Mapping-backed collections are explicit as well. `MAPPING_VALUES` yields values from a selected
196
279
  mapping in insertion order; `MAPPING_VALUES_OR_EMPTY` additionally accepts only an empty terminal
197
280
  sequence and records that degradation in the operation report.
@@ -199,7 +282,7 @@ sequence and records that degradation in the operation report.
199
282
  <!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
200
283
  ```python
201
284
  from b24api import RouteKind
202
- from b24api import ResultCollectionShape
285
+ from b24api.contracts import ResultCollectionShape
203
286
 
204
287
  stream = client.iter_list(
205
288
  Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
@@ -232,6 +315,16 @@ stream = client.iter_list_counted(
232
315
  Use it only when `total` is exact for the supplied filter and offset pages are stable. Any missing
233
316
  range, overlap, duplicate identity or total contradiction raises `IncompleteTraversalError`.
234
317
 
318
+ A filtered call that matches nothing may omit `total` entirely, as `user.get` does with
319
+ `result: []`. A first page with no rows, no `next` and no usable `total` (missing, `null`, or the
320
+ `-1` unknown sentinel) therefore completes as an observed empty source after that one request: the
321
+ report is `completed` and `exhausted`, but its assurance is `mechanics_only` (`identity_exact` with an
322
+ identity), never a count-matched claim, and no total is invented. A first page that reports
323
+ `total: 0` keeps the count-matched result. Rows without a usable total, a remaining `next`, a
324
+ positive total with no rows, a fixed step, or a `ConsistencyPolicy` whose `confirmation_policy` is
325
+ `QUALIFIED_TOTAL` (from `b24api.contracts.policy.ConfirmationPolicy`) stay strict and raise
326
+ `IncompleteTraversalError`.
327
+
235
328
  Physical batching reduces HTTP exchanges, but does not suppress server-side COUNT in ordinary
236
329
  counted list subrequests. Each command still performs its own offset page retrieval and associated
237
330
  total calculation on the server. Do not confuse batching these commands with a no-count traversal.
@@ -255,8 +348,10 @@ Static incompatibility with the auto contract raises `CapabilityError` from the
255
348
  `iter_list_keyset(...)` call before iteration begins. A portal that accepts but contradicts the
256
349
  declared controls fails before emission with `IncompleteTraversalError`; auto never restarts that
257
350
  operation silently. `KeysetTraversal` inside reference traversal remains sequential-only. The
258
- terminal report now includes `keyset_execution` for omitted-execution keyset calls so consumers can
259
- see the requested and selected plan.
351
+ terminal report carries a compact `keyset_selection` (`requested_kind`, `selected_kind`, `reason`)
352
+ for every keyset traversal. A `page_stop` callback needs the ordered page stream, so auto reports
353
+ `AUTO`, `SEQUENTIAL`, `PAGE_STOP`; an explicit `SequentialKeysetExecution()` reports
354
+ `EXPLICIT_SEQUENTIAL`. The detailed `keyset_execution` report is present only when the fast path ran.
260
355
 
261
356
  Use `await client.verify_keyset_capability(...)` as a development/CI/staging guard on a stable
262
357
  representative fixture. It performs five strict-bound checks and returns only a `VERIFIED` report;
@@ -337,10 +432,10 @@ boundary, use an application-owned direct-call workflow or supply a unique tie-b
337
432
  For multiple parent-bound cursor chains, `iter_cursors()` keeps cursor progress and correlation
338
433
  isolated per binding while ready pages share the physical batch queue.
339
434
 
340
- ![Cursor batching across independent chats](cursor-batching.svg)
435
+ ![Cursor batching across independent chats](https://raw.githubusercontent.com/shkarupa-alex/b24api/master/cursor-batching.svg)
341
436
 
342
- See [architecture](docs/architecture.md), [migration](docs/migration.md),
343
- [performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
437
+ 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),
438
+ [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
344
439
  contracts and selection guidance.
345
440
 
346
441
  ### One list method across many parent entities
@@ -349,20 +444,13 @@ contracts and selection guidance.
349
444
  client remains unaware of entity types: a binding can represent a deal, lead, chat or any other
350
445
  caller-defined parent.
351
446
 
352
- ![Reference batching across leads and deals](references-batching.svg)
447
+ ![Reference batching across leads and deals](https://raw.githubusercontent.com/shkarupa-alex/b24api/master/references-batching.svg)
353
448
 
354
449
  <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
355
450
  ```python
356
451
  from b24api import RouteKind
357
- from b24api import (
358
- BatchDispatch,
359
- Binding,
360
- ParameterPath,
361
- ParameterUpdate,
362
- ReferenceComplete,
363
- ReferenceItem,
364
- SequentialTraversal,
365
- )
452
+ from b24api import BatchDispatch, Binding, ParameterPath, ParameterUpdate, SequentialTraversal
453
+ from b24api.contracts import ReferenceComplete, ReferenceItem
366
454
 
367
455
  bindings = (
368
456
  Binding(
@@ -387,6 +475,10 @@ async with stream:
387
475
  record_completion(event.correlation, event.row_count)
388
476
  ```
389
477
 
478
+ With `KeysetTraversal`, a binding may also set simple constant fields directly inside the keyset
479
+ filter, such as `filter[=ownerId]`, while each binding keeps its own cursor; see
480
+ [Keyset references with per-owner filters](https://github.com/shkarupa-alex/b24api/blob/master/docs/recipes.md#keyset-references-with-per-owner-filters).
481
+
390
482
  For messages across chats, use the same `iter_references()` shape: each binding updates the chat
391
483
  parameter and carries the chat correlation; choose `CursorTraversal` when the message endpoint is
392
484
  cursor-based. Identity tracking and completion remain scoped to each binding, so equal child IDs
@@ -518,21 +610,22 @@ uv run --with memray memray stats /tmp/b24api.bin
518
610
  ```
519
611
 
520
612
  These deterministic fixtures characterize local resources and network shape; they are not live
521
- portal latency admission. See [docs/performance.md](docs/performance.md) for current measurements
522
- and [docs/architecture.md](docs/architecture.md) for guarantees and ownership boundaries.
613
+ portal latency admission. See [docs/performance.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/performance.md) for current measurements
614
+ and [docs/architecture.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/architecture.md) for guarantees and ownership boundaries.
523
615
 
524
- Projects moving from an earlier API surface can use [docs/migration.md](docs/migration.md).
616
+ Projects moving from an earlier API surface can use [docs/migration.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/migration.md).
525
617
 
526
618
  ## Verification
527
619
 
528
620
  ```console
529
621
  uv sync --frozen
530
- .venv/bin/pytest -q -p no:cacheprovider
531
- .venv/bin/ruff check . --no-fix --no-cache
532
- .venv/bin/ruff format --check . --no-cache
533
- .venv/bin/mypy --strict b24api tools/b24api_evidence
622
+ make qc
534
623
  git diff --check
535
624
  ```
536
625
 
626
+ `make qc` runs the lint, type and default test checks that CI blocks on. It leaves out the internal
627
+ benches (the pytest marker `slow`: the evidence harness contracts and the 50k/100k-scale runs, which
628
+ take several minutes). `make bench` runs them, and so does the blocking CI job `slow`.
629
+
537
630
  The wheel regression installs into an isolated environment, executes the `b24api` entry point and
538
631
  checks that tests, live/evidence tooling and credentials are excluded.
@@ -1,16 +1,4 @@
1
- Metadata-Version: 2.4
2
- Name: b24api
3
- Version: 2.3.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.29,>=0.28.1
9
- Requires-Dist: pydantic>=2.11.7
10
- Requires-Dist: pydantic-settings>=2.10.1
11
- Dynamic: license-file
12
-
13
- # b24api 2.x
1
+ # b24api 3.x
14
2
 
15
3
  `b24api` is a thin asynchronous Bitrix24 REST client for Python 3.12+. It knows how to send
16
4
  requests, split logical batches, traverse lists, retry safely, preserve caller correlation and
@@ -27,16 +15,42 @@ export BITRIX24_API_WEBHOOK_URL='https://portal.example/rest/.../'
27
15
  Keep the webhook out of source, logs and command arguments. Reuse one client for a related unit of
28
16
  work so its HTTP/2 connection pool and rate state are reused.
29
17
 
30
- <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
18
+ ## Quickstart
19
+
20
+ <!-- tested: tests/readme_test.py::test_quickstart_runs_exactly_against_a_scripted_portal -->
31
21
  ```python
32
- from b24api import Bitrix24, Request, RouteKind
22
+ import os
23
+
24
+ from b24api import Bitrix24, Request
25
+
26
+ async with Bitrix24.from_webhook(os.environ["BITRIX24_API_WEBHOOK_URL"]) as client:
27
+ deals = client.iter_list(Request.bare("crm.deal.list", {"select": ["ID", "TITLE"]}))
28
+ async for deal in deals:
29
+ print(deal["ID"], deal["TITLE"])
30
+ print(deals.report.state)
31
+ ```
32
+
33
+ Four things are at work:
33
34
 
34
- async with Bitrix24() as client:
35
- profile = await client.call(Request("profile", route=RouteKind.BARE))
35
+ - **The client.** `Bitrix24.from_webhook()` checks the URL and owns the connection pool it opens;
36
+ `async with` closes it. `Bitrix24()` reads the same URL from the environment.
37
+ - **The request.** `Request.bare()` names a REST method and its parameters and sends them to the
38
+ classic `/rest/` endpoint. The route is always explicit: `Request.v3()` targets the V3 API.
39
+ - **`iter_list()`.** It walks the list page by page and reads one more, empty, page to confirm
40
+ the end.
41
+ - **The report.** `deals.report` says how the traversal ended. `completed` means every page was
42
+ read; a traversal that stops early or fails records why.
43
+
44
+ A single call returns the decoded `result`:
45
+
46
+ <!-- tested: tests/readme_test.py::test_quickstart_runs_exactly_against_a_scripted_portal -->
47
+ ```python
48
+ users = await client.call(Request.bare("user.get", {"ID": 1}))
36
49
  ```
37
50
 
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.
51
+ The client owns the transport it creates. An injected transport remains caller-owned. `aclose()` is
52
+ idempotent and closes active streams before the owned transport. To prove that no row is missing or
53
+ repeated, give the traversal an identity (see [Choosing a list operation](#choosing-a-list-operation)).
40
54
 
41
55
  ## Direct calls
42
56
 
@@ -71,10 +85,17 @@ may already have reached Bitrix:
71
85
  | Value | Meaning | After possible dispatch |
72
86
  |---|---|---|
73
87
  | `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. |
74
- | `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. |
88
+ | `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. |
75
89
  | `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. |
76
90
 
77
- A failure proved to occur before dispatch may still be retried. Method names never imply safety;
91
+ A failure that proves the request did not run is retried whatever its safety: a transport failure
92
+ before dispatch, or a refusal listed in `AmbiguityPolicy`, which by default is an unstructured 423, 425
93
+ or 429 (`refusal_http_statuses`) or a `QUERY_LIMIT_EXCEEDED` / `OPERATION_TIME_LIMIT` refusal
94
+ (`refusal_api_codes`). A code or status added only to `RetryPolicy` is retried for `SAFE` work alone.
95
+ A physical batch applies these rules to each command: after a failure, only the commands that
96
+ may run again are sent again, in a smaller batch, and an `UNSAFE` command of a batch that may have run
97
+ arrives as unknown. Replay rounds spend the same attempt and retry-time budget as the sends before
98
+ them. Method names never imply safety;
78
99
  mark a request `SAFE` only when the operation's semantics justify it.
79
100
 
80
101
  Use `ExecutionPolicy` to narrow attempts or resource budgets for one operation:
@@ -87,6 +108,22 @@ one_attempt = ExecutionPolicy(max_attempts_per_request=1)
87
108
  result = await client.call(request, policy=one_attempt)
88
109
  ```
89
110
 
111
+ A `policy=` argument replaces the client's default policy wholesale; fields are never merged. The
112
+ client default is `ExecutionPolicy.from_settings(settings)`: the library defaults with
113
+ `max_retry_elapsed_per_request` taken from `Settings.http_timeout` (30 s by default, while a bare
114
+ `ExecutionPolicy()` allows 120 s). To change one field and keep the configured timeout, derive the
115
+ per-call policy from that default:
116
+
117
+ <!-- tested: tests/settings_test.py::test_a_per_call_policy_replaces_the_client_default_without_merging -->
118
+ ```python
119
+ import dataclasses
120
+
121
+ from b24api import ExecutionPolicy
122
+
123
+ one_attempt = dataclasses.replace(ExecutionPolicy.from_settings(settings), max_attempts_per_request=1)
124
+ result = await client.call(request, policy=one_attempt)
125
+ ```
126
+
90
127
  ## Logical batch and correlation
91
128
 
92
129
  `batch()` accepts an arbitrary-length synchronous or asynchronous command source. It consumes the
@@ -100,7 +137,7 @@ matching a result to the object, file, chat or database row that produced its re
100
137
  <!-- tested: tests/client_v2_test.py::test_logical_batch_is_unbounded_ordered_and_correlation_is_strictly_off_wire -->
101
138
  ```python
102
139
  from b24api import RouteKind
103
- from b24api import Command, CommandSuccess
140
+ from b24api.contracts import Command, CommandSuccess
104
141
 
105
142
  commands = (
106
143
  Command(
@@ -121,7 +158,7 @@ async with client.batch(commands, batch_size=25) as stream:
121
158
 
122
159
  <!-- tested: tests/client_v2_test.py::test_batch_outcomes_retains_typed_failure_without_halting_later_commands -->
123
160
  ```python
124
- from b24api import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
161
+ from b24api.contracts import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
125
162
 
126
163
  async with client.batch_outcomes(commands) as stream:
127
164
  async for outcome in stream:
@@ -154,7 +191,16 @@ exact `limit_path`; the client never guesses method-specific parameter names.
154
191
 
155
192
  ### List traversal comparison
156
193
 
157
- ![List traversal comparison](list-traversal-comparison.svg)
194
+ ![List traversal comparison](https://raw.githubusercontent.com/shkarupa-alex/b24api/master/list-traversal-comparison.svg)
195
+
196
+ The animation replays traces executed against a scripted portal with 1,000 dense IDs at 50 rows
197
+ per page. `iter_list` sends 20 pages and one empty confirmation (21 HTTP). `iter_list_counted` sends
198
+ the head and one batch of 19 pages (2 HTTP). `iter_list_keyset` in auto mode selects range
199
+ execution: it reads both ends of the range in one batch, the 19 ranges between them in a second, and
200
+ confirms the end with one call for `ID > 1000` (3 HTTP). A `BoundedIdentityRange` with a qualified
201
+ upper ID needs no confirmation: sequential execution stops when it receives that ID. On a real portal
202
+ auto chooses from the observed geometry, so the plan and its request count can differ; the report's
203
+ `keyset_selection` says which plan ran.
158
204
 
159
205
  ### Sequential offset
160
206
 
@@ -192,6 +238,12 @@ async with stream:
192
238
  Without `identity`, successful exhaustion is reported as `MECHANICS_ONLY`: pagination completed,
193
239
  but the client cannot prove that the portal did not duplicate or substitute rows.
194
240
 
241
+ An endpoint that honors `start` only at a fixed server window and signals its end only with a
242
+ shorter page (no usable `total`, no `next`) can declare that stop rule with
243
+ `OffsetSpec(short_page_termination=ShortPageTermination.DECLARED_TERMINAL)`; its successful
244
+ exhaustion is always `MECHANICS_ONLY`. See
245
+ [Declared short-page closure](https://github.com/shkarupa-alex/b24api/blob/master/docs/recipes.md#declared-short-page-closure).
246
+
195
247
  Mapping-backed collections are explicit as well. `MAPPING_VALUES` yields values from a selected
196
248
  mapping in insertion order; `MAPPING_VALUES_OR_EMPTY` additionally accepts only an empty terminal
197
249
  sequence and records that degradation in the operation report.
@@ -199,7 +251,7 @@ sequence and records that degradation in the operation report.
199
251
  <!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
200
252
  ```python
201
253
  from b24api import RouteKind
202
- from b24api import ResultCollectionShape
254
+ from b24api.contracts import ResultCollectionShape
203
255
 
204
256
  stream = client.iter_list(
205
257
  Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
@@ -232,6 +284,16 @@ stream = client.iter_list_counted(
232
284
  Use it only when `total` is exact for the supplied filter and offset pages are stable. Any missing
233
285
  range, overlap, duplicate identity or total contradiction raises `IncompleteTraversalError`.
234
286
 
287
+ A filtered call that matches nothing may omit `total` entirely, as `user.get` does with
288
+ `result: []`. A first page with no rows, no `next` and no usable `total` (missing, `null`, or the
289
+ `-1` unknown sentinel) therefore completes as an observed empty source after that one request: the
290
+ report is `completed` and `exhausted`, but its assurance is `mechanics_only` (`identity_exact` with an
291
+ identity), never a count-matched claim, and no total is invented. A first page that reports
292
+ `total: 0` keeps the count-matched result. Rows without a usable total, a remaining `next`, a
293
+ positive total with no rows, a fixed step, or a `ConsistencyPolicy` whose `confirmation_policy` is
294
+ `QUALIFIED_TOTAL` (from `b24api.contracts.policy.ConfirmationPolicy`) stay strict and raise
295
+ `IncompleteTraversalError`.
296
+
235
297
  Physical batching reduces HTTP exchanges, but does not suppress server-side COUNT in ordinary
236
298
  counted list subrequests. Each command still performs its own offset page retrieval and associated
237
299
  total calculation on the server. Do not confuse batching these commands with a no-count traversal.
@@ -255,8 +317,10 @@ Static incompatibility with the auto contract raises `CapabilityError` from the
255
317
  `iter_list_keyset(...)` call before iteration begins. A portal that accepts but contradicts the
256
318
  declared controls fails before emission with `IncompleteTraversalError`; auto never restarts that
257
319
  operation silently. `KeysetTraversal` inside reference traversal remains sequential-only. The
258
- terminal report now includes `keyset_execution` for omitted-execution keyset calls so consumers can
259
- see the requested and selected plan.
320
+ terminal report carries a compact `keyset_selection` (`requested_kind`, `selected_kind`, `reason`)
321
+ for every keyset traversal. A `page_stop` callback needs the ordered page stream, so auto reports
322
+ `AUTO`, `SEQUENTIAL`, `PAGE_STOP`; an explicit `SequentialKeysetExecution()` reports
323
+ `EXPLICIT_SEQUENTIAL`. The detailed `keyset_execution` report is present only when the fast path ran.
260
324
 
261
325
  Use `await client.verify_keyset_capability(...)` as a development/CI/staging guard on a stable
262
326
  representative fixture. It performs five strict-bound checks and returns only a `VERIFIED` report;
@@ -337,10 +401,10 @@ boundary, use an application-owned direct-call workflow or supply a unique tie-b
337
401
  For multiple parent-bound cursor chains, `iter_cursors()` keeps cursor progress and correlation
338
402
  isolated per binding while ready pages share the physical batch queue.
339
403
 
340
- ![Cursor batching across independent chats](cursor-batching.svg)
404
+ ![Cursor batching across independent chats](https://raw.githubusercontent.com/shkarupa-alex/b24api/master/cursor-batching.svg)
341
405
 
342
- See [architecture](docs/architecture.md), [migration](docs/migration.md),
343
- [performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
406
+ 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),
407
+ [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
344
408
  contracts and selection guidance.
345
409
 
346
410
  ### One list method across many parent entities
@@ -349,20 +413,13 @@ contracts and selection guidance.
349
413
  client remains unaware of entity types: a binding can represent a deal, lead, chat or any other
350
414
  caller-defined parent.
351
415
 
352
- ![Reference batching across leads and deals](references-batching.svg)
416
+ ![Reference batching across leads and deals](https://raw.githubusercontent.com/shkarupa-alex/b24api/master/references-batching.svg)
353
417
 
354
418
  <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
355
419
  ```python
356
420
  from b24api import RouteKind
357
- from b24api import (
358
- BatchDispatch,
359
- Binding,
360
- ParameterPath,
361
- ParameterUpdate,
362
- ReferenceComplete,
363
- ReferenceItem,
364
- SequentialTraversal,
365
- )
421
+ from b24api import BatchDispatch, Binding, ParameterPath, ParameterUpdate, SequentialTraversal
422
+ from b24api.contracts import ReferenceComplete, ReferenceItem
366
423
 
367
424
  bindings = (
368
425
  Binding(
@@ -387,6 +444,10 @@ async with stream:
387
444
  record_completion(event.correlation, event.row_count)
388
445
  ```
389
446
 
447
+ With `KeysetTraversal`, a binding may also set simple constant fields directly inside the keyset
448
+ filter, such as `filter[=ownerId]`, while each binding keeps its own cursor; see
449
+ [Keyset references with per-owner filters](https://github.com/shkarupa-alex/b24api/blob/master/docs/recipes.md#keyset-references-with-per-owner-filters).
450
+
390
451
  For messages across chats, use the same `iter_references()` shape: each binding updates the chat
391
452
  parameter and carries the chat correlation; choose `CursorTraversal` when the message endpoint is
392
453
  cursor-based. Identity tracking and completion remain scoped to each binding, so equal child IDs
@@ -518,21 +579,22 @@ uv run --with memray memray stats /tmp/b24api.bin
518
579
  ```
519
580
 
520
581
  These deterministic fixtures characterize local resources and network shape; they are not live
521
- portal latency admission. See [docs/performance.md](docs/performance.md) for current measurements
522
- and [docs/architecture.md](docs/architecture.md) for guarantees and ownership boundaries.
582
+ portal latency admission. See [docs/performance.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/performance.md) for current measurements
583
+ and [docs/architecture.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/architecture.md) for guarantees and ownership boundaries.
523
584
 
524
- Projects moving from an earlier API surface can use [docs/migration.md](docs/migration.md).
585
+ Projects moving from an earlier API surface can use [docs/migration.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/migration.md).
525
586
 
526
587
  ## Verification
527
588
 
528
589
  ```console
529
590
  uv sync --frozen
530
- .venv/bin/pytest -q -p no:cacheprovider
531
- .venv/bin/ruff check . --no-fix --no-cache
532
- .venv/bin/ruff format --check . --no-cache
533
- .venv/bin/mypy --strict b24api tools/b24api_evidence
591
+ make qc
534
592
  git diff --check
535
593
  ```
536
594
 
595
+ `make qc` runs the lint, type and default test checks that CI blocks on. It leaves out the internal
596
+ benches (the pytest marker `slow`: the evidence harness contracts and the 50k/100k-scale runs, which
597
+ take several minutes). `make bench` runs them, and so does the blocking CI job `slow`.
598
+
537
599
  The wheel regression installs into an isolated environment, executes the `b24api` entry point and
538
600
  checks that tests, live/evidence tooling and credentials are excluded.