b24api 1.0.1__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. b24api-2.1.0/PKG-INFO +462 -0
  2. b24api-2.1.0/README.md +450 -0
  3. b24api-2.1.0/b24api/__init__.py +221 -0
  4. b24api-2.1.0/b24api/_audit.py +78 -0
  5. b24api-2.1.0/b24api/_client_traversal.py +170 -0
  6. b24api-2.1.0/b24api/_error_types.py +31 -0
  7. b24api-2.1.0/b24api/_stream.py +323 -0
  8. b24api-2.1.0/b24api/batch/__init__.py +3 -0
  9. b24api-2.1.0/b24api/batch/engine.py +475 -0
  10. b24api-2.1.0/b24api/batch/facade.py +135 -0
  11. b24api-2.1.0/b24api/batch/logical.py +348 -0
  12. b24api-2.1.0/b24api/batch/outcome.py +167 -0
  13. b24api-2.1.0/b24api/batch/stream.py +435 -0
  14. b24api-2.1.0/b24api/cli.py +221 -0
  15. b24api-2.1.0/b24api/cli_contract.py +400 -0
  16. b24api-2.1.0/b24api/client.py +325 -0
  17. b24api-2.1.0/b24api/contracts/__init__.py +188 -0
  18. b24api-2.1.0/b24api/contracts/command.py +128 -0
  19. b24api-2.1.0/b24api/contracts/dispatch.py +56 -0
  20. b24api-2.1.0/b24api/contracts/json.py +139 -0
  21. b24api-2.1.0/b24api/contracts/keyset_execution.py +232 -0
  22. b24api-2.1.0/b24api/contracts/policy.py +384 -0
  23. b24api-2.1.0/b24api/contracts/reference.py +172 -0
  24. b24api-2.1.0/b24api/contracts/report.py +385 -0
  25. b24api-2.1.0/b24api/contracts/request.py +344 -0
  26. b24api-2.1.0/b24api/contracts/response.py +286 -0
  27. b24api-2.1.0/b24api/contracts/stream.py +56 -0
  28. b24api-2.1.0/b24api/contracts/traversal.py +262 -0
  29. b24api-2.1.0/b24api/contracts/wire.py +85 -0
  30. b24api-2.1.0/b24api/encoding.py +40 -0
  31. b24api-2.1.0/b24api/errors.py +399 -0
  32. b24api-2.1.0/b24api/execution/__init__.py +30 -0
  33. b24api-2.1.0/b24api/execution/cleanup.py +44 -0
  34. b24api-2.1.0/b24api/execution/context.py +350 -0
  35. b24api-2.1.0/b24api/execution/executor.py +700 -0
  36. b24api-2.1.0/b24api/execution/failure.py +131 -0
  37. b24api-2.1.0/b24api/execution/rate.py +220 -0
  38. b24api-2.1.0/b24api/execution/snapshot.py +76 -0
  39. b24api-2.1.0/b24api/redaction.py +205 -0
  40. b24api-2.1.0/b24api/references/__init__.py +3 -0
  41. b24api-2.1.0/b24api/references/binding.py +227 -0
  42. b24api-2.1.0/b24api/references/dispatch.py +435 -0
  43. b24api-2.1.0/b24api/references/facade.py +399 -0
  44. b24api-2.1.0/b24api/references/fanout.py +280 -0
  45. b24api-2.1.0/b24api/references/outcome.py +115 -0
  46. b24api-2.1.0/b24api/references/scheduler.py +570 -0
  47. b24api-2.1.0/b24api/references/stream.py +371 -0
  48. b24api-2.1.0/b24api/references/support.py +185 -0
  49. b24api-2.1.0/b24api/settings.py +222 -0
  50. b24api-2.1.0/b24api/testing/__init__.py +17 -0
  51. b24api-2.1.0/b24api/testing/_isolation.py +81 -0
  52. b24api-2.1.0/b24api/testing/transport.py +373 -0
  53. b24api-2.1.0/b24api/transport/__init__.py +6 -0
  54. b24api-2.1.0/b24api/transport/base.py +158 -0
  55. b24api-2.1.0/b24api/transport/httpx.py +293 -0
  56. b24api-2.1.0/b24api/transport/protocol.py +157 -0
  57. b24api-2.1.0/b24api/traversal/__init__.py +9 -0
  58. b24api-2.1.0/b24api/traversal/counted.py +166 -0
  59. b24api-2.1.0/b24api/traversal/counted_batch.py +260 -0
  60. b24api-2.1.0/b24api/traversal/cursor.py +89 -0
  61. b24api-2.1.0/b24api/traversal/driver.py +697 -0
  62. b24api-2.1.0/b24api/traversal/facade.py +371 -0
  63. b24api-2.1.0/b24api/traversal/identity.py +353 -0
  64. b24api-2.1.0/b24api/traversal/keyset.py +63 -0
  65. b24api-2.1.0/b24api/traversal/keyset_auto.py +241 -0
  66. b24api-2.1.0/b24api/traversal/keyset_capability.py +392 -0
  67. b24api-2.1.0/b24api/traversal/keyset_costs.py +185 -0
  68. b24api-2.1.0/b24api/traversal/keyset_eligibility.py +159 -0
  69. b24api-2.1.0/b24api/traversal/keyset_fast_plan.py +288 -0
  70. b24api-2.1.0/b24api/traversal/keyset_fast_stream.py +274 -0
  71. b24api-2.1.0/b24api/traversal/keyset_observation.py +270 -0
  72. b24api-2.1.0/b24api/traversal/keyset_partition.py +27 -0
  73. b24api-2.1.0/b24api/traversal/keyset_range.py +80 -0
  74. b24api-2.1.0/b24api/traversal/keyset_reporting.py +112 -0
  75. b24api-2.1.0/b24api/traversal/keyset_scheduler.py +672 -0
  76. b24api-2.1.0/b24api/traversal/keyset_step.py +138 -0
  77. b24api-2.1.0/b24api/traversal/keyset_transaction_contract.py +108 -0
  78. b24api-2.1.0/b24api/traversal/keyset_transactions.py +393 -0
  79. b24api-2.1.0/b24api/traversal/ordered_admission.py +182 -0
  80. b24api-2.1.0/b24api/traversal/page_validation.py +235 -0
  81. b24api-2.1.0/b24api/traversal/plans.py +322 -0
  82. b24api-2.1.0/b24api/traversal/sequential.py +198 -0
  83. b24api-2.1.0/b24api/traversal/stream.py +318 -0
  84. b24api-2.1.0/b24api/traversal/values.py +201 -0
  85. b24api-2.1.0/b24api.egg-info/PKG-INFO +462 -0
  86. b24api-2.1.0/b24api.egg-info/SOURCES.txt +92 -0
  87. b24api-2.1.0/b24api.egg-info/entry_points.txt +2 -0
  88. {b24api-1.0.1 → b24api-2.1.0}/b24api.egg-info/requires.txt +0 -3
  89. {b24api-1.0.1 → b24api-2.1.0}/pyproject.toml +16 -25
  90. b24api-1.0.1/PKG-INFO +0 -166
  91. b24api-1.0.1/README.md +0 -151
  92. b24api-1.0.1/b24api/__init__.py +0 -3
  93. b24api-1.0.1/b24api/api.py +0 -517
  94. b24api-1.0.1/b24api/entity.py +0 -144
  95. b24api-1.0.1/b24api/error.py +0 -22
  96. b24api-1.0.1/b24api/helper.py +0 -322
  97. b24api-1.0.1/b24api/query.py +0 -38
  98. b24api-1.0.1/b24api/settings.py +0 -45
  99. b24api-1.0.1/b24api/type.py +0 -5
  100. b24api-1.0.1/b24api.egg-info/PKG-INFO +0 -166
  101. b24api-1.0.1/b24api.egg-info/SOURCES.txt +0 -17
  102. {b24api-1.0.1 → b24api-2.1.0}/LICENSE +0 -0
  103. {b24api-1.0.1 → b24api-2.1.0}/MANIFEST.in +0 -0
  104. {b24api-1.0.1 → b24api-2.1.0}/b24api.egg-info/dependency_links.txt +0 -0
  105. {b24api-1.0.1 → b24api-2.1.0}/b24api.egg-info/top_level.txt +0 -0
  106. {b24api-1.0.1 → b24api-2.1.0}/setup.cfg +0 -0
b24api-2.1.0/PKG-INFO ADDED
@@ -0,0 +1,462 @@
1
+ Metadata-Version: 2.4
2
+ Name: b24api
3
+ Version: 2.1.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
+ # b24api 2.x
14
+
15
+ `b24api` is a thin asynchronous Bitrix24 REST client for Python 3.12+. It knows how to send
16
+ requests, split logical batches, traverse lists, retry safely, preserve caller correlation and
17
+ close resources. It does not contain a Tasks, CRM or IM method catalog and does not impose
18
+ application storage rules.
19
+
20
+ ## Install and configure
21
+
22
+ ```console
23
+ uv sync --frozen
24
+ export BITRIX24_API_WEBHOOK_URL='https://portal.example/rest/.../'
25
+ ```
26
+
27
+ Keep the webhook out of source, logs and command arguments. Reuse one client for a related unit of
28
+ work so its HTTP/2 connection pool and rate state are reused.
29
+
30
+ <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
31
+ ```python
32
+ from b24api import Bitrix24, Request
33
+
34
+ async with Bitrix24() as client:
35
+ profile = await client.call(Request("profile"))
36
+ ```
37
+
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.
40
+
41
+ ## Direct calls
42
+
43
+ Use `call()` for detached decoded JSON and `call_response()` when you also need the immutable
44
+ response envelope: `result`, `total`, `next`, timing and bounded diagnostic evidence.
45
+
46
+ Use `call_bytes()` when a successful method response is a file rather than a Bitrix JSON envelope.
47
+ The operation is explicit and never hides malformed JSON by falling back to bytes.
48
+
49
+ <!-- tested: tests/client_findings_3_test.py::test_binary_call_returns_every_success_byte_without_json_sniffing -->
50
+ ```python
51
+ archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE))
52
+ payload = archive.body
53
+ ```
54
+
55
+ <!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
56
+ ```python
57
+ from b24api import ReplaySafety
58
+
59
+ request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE)
60
+ decoded = await client.call(request)
61
+ response = await client.call_response(request)
62
+ ```
63
+
64
+ ### Replay safety
65
+
66
+ `Request.replay_safety` describes what the client may do when a connection fails after the request
67
+ may already have reached Bitrix:
68
+
69
+ | Value | Meaning | After possible dispatch |
70
+ |---|---|---|
71
+ | `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. |
73
+ | `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
+
75
+ A failure proved to occur before dispatch may still be retried. Method names never imply safety;
76
+ mark a request `SAFE` only when the operation's semantics justify it.
77
+
78
+ Use `ExecutionPolicy` to narrow attempts or resource budgets for one operation:
79
+
80
+ <!-- tested: tests/execution_test.py::test_ambiguous_dispatch_never_retries_unproven_request -->
81
+ ```python
82
+ from b24api import ExecutionPolicy
83
+
84
+ one_attempt = ExecutionPolicy(max_attempts_per_request=1)
85
+ result = await client.call(request, policy=one_attempt)
86
+ ```
87
+
88
+ ## Logical batch and correlation
89
+
90
+ `batch()` accepts an arbitrary-length synchronous or asynchronous command source. It consumes the
91
+ source incrementally and splits it into physical Bitrix batches of at most 50 commands; the full
92
+ input is never materialized.
93
+
94
+ `Command.correlation` is arbitrary caller-owned state. It is retained by reference, returned with
95
+ the outcome, never serialized to Bitrix and never included in safe diagnostics. This is useful for
96
+ matching a result to the object, file, chat or database row that produced its request.
97
+
98
+ <!-- tested: tests/client_v2_test.py::test_logical_batch_is_unbounded_ordered_and_correlation_is_strictly_off_wire -->
99
+ ```python
100
+ from b24api import Command, CommandSuccess
101
+
102
+ commands = (
103
+ Command(
104
+ Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE),
105
+ correlation=item_id,
106
+ )
107
+ for item_id in source_ids
108
+ )
109
+
110
+ async with client.batch(commands, batch_size=25) as stream:
111
+ async for outcome in stream:
112
+ assert isinstance(outcome, CommandSuccess)
113
+ consume(outcome.correlation, outcome.result)
114
+ ```
115
+
116
+ `batch()` is fail-fast. `batch_outcomes()` continues where safe and yields one of
117
+ `CommandSuccess`, `CommandFailure`, `CommandNotExecuted` or `CommandOutcomeUnknown` in input order.
118
+
119
+ <!-- tested: tests/client_v2_test.py::test_batch_outcomes_retains_typed_failure_without_halting_later_commands -->
120
+ ```python
121
+ from b24api import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
122
+
123
+ async with client.batch_outcomes(commands) as stream:
124
+ async for outcome in stream:
125
+ match outcome:
126
+ case CommandSuccess() as success:
127
+ consume(success.correlation, success.result)
128
+ case CommandFailure() | CommandNotExecuted() | CommandOutcomeUnknown():
129
+ handle(outcome)
130
+ ```
131
+
132
+ For independently dispatchable commands, use `fan_out()` or `fan_out_outcomes()` with
133
+ `DirectDispatch` or `BatchDispatch`. Delivery order is explicitly `READY` or `INPUT`.
134
+
135
+ ## Choosing a list operation
136
+
137
+ The unsuffixed operation is the basic strategy with the fewest endpoint assumptions. Faster or
138
+ more specialized mechanics have explicit names and explicit preconditions.
139
+
140
+ | Operation | Use it when | Network mechanics | Completion proof |
141
+ |---|---|---|---|
142
+ | `iter_list` | The method supports ordinary offset pagination. | Pages are requested sequentially using server `next`; no separate count request is made. | Continuation and empty terminal page; add identity for duplicate detection. |
143
+ | `iter_list_counted` | The first response provides an exact filtered `total` and stable offset pages. | Head page is direct; all known tail offsets are grouped into physical Bitrix batches. | Exact total, ranges and identities. |
144
+ | `iter_list_keyset` | The method may omit `total`, but reliably supports ordering and filtering by a unique integer identity. | Sequential by default; explicit range, partitioned, or auto execution may batch bounded work after a planning barrier. | Caller-asserted keyset contract, strict monotonic identity, bounded-plan canaries, and terminal empty confirmation. |
145
+ | `iter_list_cursor` | Each next request depends on a cursor from the previous response. | Sequential dependent cursor requests. | Strict unique monotonic cursor and empty terminal page. |
146
+ | `iter_references` | The same list method must run for many parent parameter sets, such as comments per owner or messages per chat. | Bindings are scheduled with direct or physical-batch dispatch; each binding has its own traversal state. | Per-binding rows, completion/failure and caller correlation. |
147
+
148
+ `page_size` is a local decoded-page cap. It is sent to Bitrix only when you provide the endpoint's
149
+ exact `limit_path`; the client never guesses method-specific parameter names.
150
+
151
+ ### Sequential offset
152
+
153
+ This is the canonical default. It follows the `next` returned by the server and confirms the end
154
+ with an empty page. A `total` present in the response is observational; this strategy does not add
155
+ a separate count request.
156
+
157
+ <!-- tested: tests/client_v2_test.py::test_iter_list_is_sequential_mechanics_only_and_report_is_post_cleanup -->
158
+ ```python
159
+ from b24api import IdentityCoercion, IdentitySpec, ResultSelector
160
+
161
+ identity = IdentitySpec(
162
+ item_path=("ID",),
163
+ filter_key="ID",
164
+ order_key="ID",
165
+ coercion=IdentityCoercion.DECIMAL_STRING_INTEGER,
166
+ )
167
+
168
+ stream = client.iter_list(
169
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE),
170
+ selector=ResultSelector(("items",)),
171
+ identity=identity,
172
+ )
173
+ async with stream:
174
+ async for item in stream:
175
+ consume(item)
176
+ ```
177
+
178
+ Without `identity`, successful exhaustion is reported as `MECHANICS_ONLY`: pagination completed,
179
+ but the client cannot prove that the portal did not duplicate or substitute rows.
180
+
181
+ Mapping-backed collections are explicit as well. `MAPPING_VALUES` yields values from a selected
182
+ mapping in insertion order; `MAPPING_VALUES_OR_EMPTY` additionally accepts only an empty terminal
183
+ sequence and records that degradation in the operation report.
184
+
185
+ <!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
186
+ ```python
187
+ from b24api import ResultCollectionShape
188
+
189
+ stream = client.iter_list(
190
+ Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
191
+ selector=ResultSelector(("items",)),
192
+ collection_shape=ResultCollectionShape.MAPPING_VALUES,
193
+ )
194
+ async with stream:
195
+ async for value in stream:
196
+ consume(value)
197
+ ```
198
+
199
+ ### Counted, physically batched tail
200
+
201
+ The first direct page must contain an exact filtered `total` and, when more rows exist, `next`.
202
+ The client derives all remaining offsets from the observed head width and sends tail pages through
203
+ bounded physical batches.
204
+
205
+ <!-- tested: tests/client_v2_test.py::test_counted_traversal_preserves_frozen_request_shape_and_exact_identity -->
206
+ ```python
207
+ stream = client.iter_list_counted(
208
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE),
209
+ selector=ResultSelector(("items",)),
210
+ identity=identity,
211
+ page_size=50,
212
+ batch_size=50,
213
+ )
214
+ ```
215
+
216
+ Use it only when `total` is exact for the supplied filter and offset pages are stable. Any missing
217
+ range, overlap, duplicate identity or total contradiction raises `IncompleteTraversalError`.
218
+
219
+ ### No-count keyset
220
+
221
+ Keyset traversal is sequential by default, preserving the compatible request shape and first-pull
222
+ behavior. An explicit execution contract can instead capture both ordered boundaries, validate five
223
+ capability canaries, and batch numeric ranges or occupied-anchor partitions. Planning completes
224
+ before any row is emitted, so partial consumption still pays that barrier cost. Fast execution fails
225
+ synchronously when its declared controls or policy capacity are ineligible.
226
+
227
+ The caller must assert that the endpoint has a stable, unique integer key, honors strict numeric
228
+ bounds and ordering, and satisfies the chosen page-completion rule. Concurrent mutation outside the
229
+ captured middle is handled by the finishing sweep; mutation inside it is outside this assertion.
230
+ An advisory `total` may only raise an automatic cost estimate and never proves completion. Reports
231
+ record the selected strategy and reason: unbounded auto continuation has the same
232
+ `ordered_prefix_only` assurance as sequential traversal, while bounded plans additionally report
233
+ `canary_verified_bounds`.
234
+
235
+ <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
236
+ ```python
237
+ from b24api import AutoKeysetExecution, KeysetSpec, ParameterPath, StableIntegerKeysetContract
238
+
239
+ stream = client.iter_list_keyset(
240
+ Request("example.item.list", replay_safety=ReplaySafety.SAFE),
241
+ selector=ResultSelector(("items",)),
242
+ identity=identity,
243
+ keyset=KeysetSpec(
244
+ filter_path=ParameterPath(("filter",)),
245
+ order_path=ParameterPath(("order",)),
246
+ ),
247
+ execution=AutoKeysetExecution(contract=StableIntegerKeysetContract()),
248
+ )
249
+ ```
250
+
251
+ ### Dependent cursor
252
+
253
+ Use a cursor when the next boundary is returned or derived from the previous page, as with many
254
+ message-list methods.
255
+
256
+ <!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
257
+ ```python
258
+ from b24api import CursorSpec, ParameterPath
259
+
260
+ stream = client.iter_list_cursor(
261
+ Request("example.message.list", replay_safety=ReplaySafety.SAFE),
262
+ selector=ResultSelector(("items",)),
263
+ cursor=CursorSpec(
264
+ parameter_path=ParameterPath(("LAST_ID",)),
265
+ item_path=("ID",),
266
+ coercion=IdentityCoercion.DECIMAL_STRING_INTEGER,
267
+ direction="ascending",
268
+ take="last",
269
+ ),
270
+ )
271
+ ```
272
+
273
+ Cursor values must be unique and strictly monotonic. If an endpoint exposes only a non-unique
274
+ boundary, use an application-owned direct-call workflow or supply a unique tie-breaker.
275
+
276
+ See [architecture](docs/architecture.md), [migration](docs/migration.md),
277
+ [performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
278
+ contracts and selection guidance.
279
+
280
+ ### One list method across many parent entities
281
+
282
+ `Binding` applies exact parameter updates to a base request and carries parent correlation. The
283
+ client remains unaware of entity types: a binding can represent a deal, lead, chat or any other
284
+ caller-defined parent.
285
+
286
+ <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
287
+ ```python
288
+ from b24api import (
289
+ BatchDispatch,
290
+ Binding,
291
+ ParameterPath,
292
+ ParameterUpdate,
293
+ ReferenceComplete,
294
+ ReferenceItem,
295
+ SequentialTraversal,
296
+ )
297
+
298
+ bindings = (
299
+ Binding(
300
+ summary=f"owner {parent_id}",
301
+ updates=(ParameterUpdate(ParameterPath(("filter", "OWNER_ID")), parent_id),),
302
+ correlation=parent_id,
303
+ )
304
+ for parent_id in parent_ids
305
+ )
306
+
307
+ stream = client.iter_references(
308
+ Request("example.comment.list", replay_safety=ReplaySafety.SAFE),
309
+ bindings,
310
+ traversal=SequentialTraversal(selector=ResultSelector(("items",)), identity=identity),
311
+ dispatch=BatchDispatch(batch_size=25, concurrency=2),
312
+ )
313
+ async with stream:
314
+ async for event in stream:
315
+ if isinstance(event, ReferenceItem):
316
+ consume(event.correlation, event.item)
317
+ elif isinstance(event, ReferenceComplete):
318
+ record_completion(event.correlation, event.row_count)
319
+ ```
320
+
321
+ For messages across chats, use the same `iter_references()` shape: each binding updates the chat
322
+ parameter and carries the chat correlation; choose `CursorTraversal` when the message endpoint is
323
+ cursor-based. Identity tracking and completion remain scoped to each binding, so equal child IDs
324
+ under different parents are not conflated.
325
+
326
+ <!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
327
+ ```python
328
+ from b24api import (
329
+ Binding,
330
+ CursorSpec,
331
+ CursorTraversal,
332
+ DirectDispatch,
333
+ IdentityCoercion,
334
+ ParameterPath,
335
+ ParameterUpdate,
336
+ ResultSelector,
337
+ )
338
+
339
+ chat_bindings = (
340
+ Binding(
341
+ summary=f"chat {chat_id}",
342
+ updates=(ParameterUpdate(ParameterPath(("DIALOG_ID",)), chat_id),),
343
+ correlation={"chat_id": chat_id},
344
+ )
345
+ for chat_id in chat_ids
346
+ )
347
+
348
+ messages = client.iter_references(
349
+ Request("example.message.list", replay_safety=ReplaySafety.SAFE),
350
+ chat_bindings,
351
+ traversal=CursorTraversal(
352
+ selector=ResultSelector(("items",)),
353
+ cursor=CursorSpec(
354
+ parameter_path=ParameterPath(("LAST_ID",)),
355
+ item_path=("ID",),
356
+ coercion=IdentityCoercion.DECIMAL_STRING_INTEGER,
357
+ direction="ascending",
358
+ take="last",
359
+ ),
360
+ ),
361
+ dispatch=DirectDispatch(concurrency=4),
362
+ )
363
+ ```
364
+
365
+ `iter_reference_outcomes()` additionally yields correlated `ReferenceFailure`,
366
+ `ReferenceNotExecuted` and `ReferenceOutcomeUnknown`. A malformed source object that is not a
367
+ `Binding` has no valid caller correlation, so it terminates the source with `InputSourceError`
368
+ rather than fabricating a reference outcome. Already accepted bindings retain their real outcomes.
369
+
370
+ ## Streams, partial results and reports
371
+
372
+ Every multi-item operation returns an `OperationStream`. Prefer `async with`: a plain `break` does
373
+ not close an arbitrary async iterator. After cleanup, `stream.report` permanently exposes one
374
+ immutable `OperationReport`; before termination it is `None`.
375
+
376
+ <!-- tested: tests/client_v2_test.py::test_partial_helper_closes_without_claiming_completion -->
377
+ ```python
378
+ first = await client.iter_list(request).first()
379
+ page = await client.iter_list(request).collect(limit=100)
380
+
381
+ assert first.report.partial
382
+ assert page.report.partial
383
+ ```
384
+
385
+ Helpers do not pull an extra row just to prove exhaustion. Reaching a requested limit is therefore
386
+ `EARLY_CLOSED`, never a false `COMPLETED`. Cancellation and cleanup preserve the primary exception
387
+ and publish the same final report where the Python exception type permits it.
388
+
389
+ ## Resource boundaries
390
+
391
+ `ExecutionPolicy` bounds requests, pages, elapsed time, attempts, decompressed response bytes,
392
+ buffered commands and rows, direct concurrency and active references. The default response ceiling
393
+ is 16 MiB and is enforced while streaming, before JSON decoding.
394
+
395
+ Sequential and counted exact traversal retain observed identities in memory. There is no database,
396
+ spill file or identity-count refusal. Crossing 100,000 distinct identities emits one
397
+ `RuntimeWarning`; exact tracking continues. Strict keyset and cursor traversal retain only
398
+ monotonic progression state when sufficient.
399
+
400
+ ## CLI
401
+
402
+ The wheel installs `b24api`. Stdout contains only result data; list rows are JSONL. Reports and safe
403
+ errors go to stderr. Credentials come only from `Settings` and cannot be passed as CLI arguments.
404
+
405
+ <!-- tested-console: tests/cli_test.py::test_call_routes_replay_safety_and_keeps_success_data_on_stdout -->
406
+ ```console
407
+ b24api call profile
408
+ b24api call example.item.get --params '{"id":7}' --raw --replay-safety safe
409
+ b24api list example.item.list --params @params.json
410
+ b24api list example.item.list --strategy counted --contract @counted-contract.json
411
+ ```
412
+
413
+ The `--raw` CLI option selects the response envelope; it does not alter the Python API. Advanced
414
+ list strategies use closed JSON `version: 1` contracts. The entire contract is validated before
415
+ client construction. Run `b24api --help` and `b24api list --help` for the compact option surface.
416
+
417
+ Exit codes are `0` success, `2` usage/contract error, `3` unavailable configuration, `4`
418
+ remote/protocol/correctness/incomplete failure, `5` broken output consumer and `130` cancellation.
419
+
420
+ ## Correctness boundaries
421
+
422
+ The client fails closed on contradictory pagination, missing counted ranges, duplicate identities,
423
+ unsafe ambiguous replay, oversized responses and incomplete cleanup. It can prove only facts visible
424
+ through the transport contract: continuation, totals, identity, order, budgets and lifecycle.
425
+
426
+ It cannot generically prove that Bitrix honored the business meaning of a filter, choose an
427
+ application's composite storage key or reconcile an ambiguous write. Applications must validate
428
+ expected business sets and verify writes where needed.
429
+
430
+ ## Performance and profiling
431
+
432
+ The current deterministic profile covers request counts, wall/CPU time, time to first row,
433
+ high-water counters, retained resources and optional Memray allocations:
434
+
435
+ ```console
436
+ uv run python tools/b24api_evidence/profile_runtime.py --capability-suite
437
+ uv run python tools/b24api_evidence/profile_runtime.py --samples 7 --warmups 2
438
+ uv run --with memray python tools/b24api_evidence/profile_runtime.py \
439
+ --case dense-10k --plan counted_batch --samples 7 --warmups 2 \
440
+ --memray-output /tmp/b24api.bin
441
+ uv run --with memray memray stats /tmp/b24api.bin
442
+ ```
443
+
444
+ These deterministic fixtures characterize local resources and network shape; they are not live
445
+ portal latency admission. See [docs/performance.md](docs/performance.md) for current measurements
446
+ and [docs/architecture.md](docs/architecture.md) for guarantees and ownership boundaries.
447
+
448
+ Projects moving from an earlier API surface can use [docs/migration.md](docs/migration.md).
449
+
450
+ ## Verification
451
+
452
+ ```console
453
+ uv sync --frozen
454
+ .venv/bin/pytest -q -p no:cacheprovider
455
+ .venv/bin/ruff check . --no-fix --no-cache
456
+ .venv/bin/ruff format --check . --no-cache
457
+ .venv/bin/mypy --strict b24api tools/b24api_evidence
458
+ git diff --check
459
+ ```
460
+
461
+ The wheel regression installs into an isolated environment, executes the `b24api` entry point and
462
+ checks that tests, live/evidence tooling and credentials are excluded.