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.
- {b24api-2.3.0 → b24api-2.5.0}/PKG-INFO +130 -37
- b24api-2.3.0/b24api.egg-info/PKG-INFO → b24api-2.5.0/README.md +109 -47
- b24api-2.5.0/b24api/__init__.py +134 -0
- b24api-2.5.0/b24api/_diagnostics.py +138 -0
- b24api-2.5.0/b24api/_sources.py +172 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/batch/engine.py +190 -44
- {b24api-2.3.0 → b24api-2.5.0}/b24api/batch/facade.py +11 -12
- b24api-2.5.0/b24api/batch/logical.py +396 -0
- b24api-2.5.0/b24api/batch/stream.py +272 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/cli.py +3 -1
- {b24api-2.3.0 → b24api-2.5.0}/b24api/cli_contract.py +12 -7
- {b24api-2.3.0 → b24api-2.5.0}/b24api/client.py +40 -15
- {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/closure.py +3 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/fast_recorder.py +7 -18
- {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/gate.py +286 -158
- b24api-2.5.0/b24api/completion/operation_stream.py +294 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/recorder.py +28 -17
- {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/reference_recorder.py +12 -12
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/__init__.py +5 -2
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/command.py +3 -3
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/completion.py +18 -1
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/dispatch.py +5 -4
- b24api-2.5.0/b24api/contracts/error_base.py +75 -0
- b24api-2.5.0/b24api/contracts/evidence.py +41 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/keyset_execution.py +4 -2
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/policy.py +29 -11
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/positional.py +24 -6
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/reference.py +9 -2
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/report.py +39 -15
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/request.py +75 -53
- b24api-2.5.0/b24api/contracts/request_summary.py +63 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/response.py +6 -33
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/traversal.py +46 -3
- b24api-2.5.0/b24api/contracts/v3_codes.py +61 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/violation.py +3 -7
- {b24api-2.3.0 → b24api-2.5.0}/b24api/errors.py +57 -90
- b24api-2.5.0/b24api/execution/boundary.py +80 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/cleanup.py +15 -12
- {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/context.py +90 -70
- {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/executor.py +238 -145
- {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/failure.py +62 -3
- b24api-2.5.0/b24api/execution/lifecycle.py +400 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/rate.py +93 -88
- {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/snapshot.py +8 -4
- b24api-2.5.0/b24api/execution/throttle.py +107 -0
- b24api-2.5.0/b24api/migration.py +260 -0
- b24api-2.5.0/b24api/py.typed +0 -0
- b24api-2.5.0/b24api/redaction.py +339 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/binding.py +92 -119
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/dispatch.py +37 -31
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/dispatch_plan.py +2 -3
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/facade.py +31 -11
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/fanout.py +35 -82
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/outcome.py +4 -7
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/scheduler.py +215 -231
- b24api-2.5.0/b24api/references/stream.py +274 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/support.py +15 -98
- {b24api-2.3.0 → b24api-2.5.0}/b24api/testing/scripted.py +4 -4
- {b24api-2.3.0 → b24api-2.5.0}/b24api/testing/transport.py +2 -1
- {b24api-2.3.0 → b24api-2.5.0}/b24api/transport/base.py +2 -1
- b24api-2.5.0/b24api/transport/decoding.py +207 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/transport/httpx.py +48 -40
- b24api-2.5.0/b24api/transport/log_records.py +111 -0
- b24api-2.5.0/b24api/transport/logging_shield.py +366 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/transport/protocol.py +81 -54
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/control_preflight.py +23 -7
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/counted.py +10 -5
- b24api-2.5.0/b24api/traversal/counted_batch.py +369 -0
- b24api-2.5.0/b24api/traversal/counted_rules.py +74 -0
- b24api-2.5.0/b24api/traversal/cursor.py +106 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/driver.py +161 -42
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/facade.py +36 -23
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/identity.py +7 -76
- b24api-2.5.0/b24api/traversal/keyset.py +67 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_auto.py +11 -20
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_capability.py +8 -107
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_eligibility.py +12 -8
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_fast_plan.py +1 -39
- b24api-2.5.0/b24api/traversal/keyset_fast_stream.py +174 -0
- b24api-2.5.0/b24api/traversal/keyset_geometry.py +313 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_observation.py +125 -85
- b24api-2.3.0/b24api/traversal/ordered_admission.py → b24api-2.5.0/b24api/traversal/keyset_ordered_admission.py +2 -10
- b24api-2.3.0/b24api/traversal/page_validation.py → b24api-2.5.0/b24api/traversal/keyset_page_validation.py +3 -9
- b24api-2.5.0/b24api/traversal/keyset_plan.py +453 -0
- b24api-2.5.0/b24api/traversal/keyset_reporting.py +96 -0
- b24api-2.5.0/b24api/traversal/keyset_runtime.py +413 -0
- b24api-2.5.0/b24api/traversal/keyset_transaction_contract.py +195 -0
- b24api-2.5.0/b24api/traversal/keyset_transactions.py +451 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_verifier.py +35 -13
- b24api-2.5.0/b24api/traversal/offset_rules.py +198 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/page_adaptation.py +3 -2
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/plans.py +40 -9
- b24api-2.5.0/b24api/traversal/sequential.py +244 -0
- b24api-2.5.0/b24api/traversal/strategy_context.py +181 -0
- b24api-2.5.0/b24api/traversal/stream.py +306 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/values.py +4 -4
- b24api-2.3.0/README.md → b24api-2.5.0/b24api.egg-info/PKG-INFO +140 -35
- {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/SOURCES.txt +20 -8
- {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/requires.txt +3 -1
- b24api-2.5.0/pyproject.toml +132 -0
- b24api-2.3.0/b24api/__init__.py +0 -335
- b24api-2.3.0/b24api/_audit.py +0 -78
- b24api-2.3.0/b24api/_stream.py +0 -274
- b24api-2.3.0/b24api/batch/logical.py +0 -433
- b24api-2.3.0/b24api/batch/stream.py +0 -436
- b24api-2.3.0/b24api/execution/throttle.py +0 -53
- b24api-2.3.0/b24api/redaction.py +0 -205
- b24api-2.3.0/b24api/references/stream.py +0 -394
- b24api-2.3.0/b24api/transport/logging_shield.py +0 -244
- b24api-2.3.0/b24api/traversal/counted_batch.py +0 -288
- b24api-2.3.0/b24api/traversal/cursor.py +0 -108
- b24api-2.3.0/b24api/traversal/keyset.py +0 -68
- b24api-2.3.0/b24api/traversal/keyset_costs.py +0 -185
- b24api-2.3.0/b24api/traversal/keyset_fast_stream.py +0 -311
- b24api-2.3.0/b24api/traversal/keyset_partition.py +0 -27
- b24api-2.3.0/b24api/traversal/keyset_range.py +0 -80
- b24api-2.3.0/b24api/traversal/keyset_reporting.py +0 -111
- b24api-2.3.0/b24api/traversal/keyset_scheduler.py +0 -669
- b24api-2.3.0/b24api/traversal/keyset_transaction_contract.py +0 -108
- b24api-2.3.0/b24api/traversal/keyset_transactions.py +0 -418
- b24api-2.3.0/b24api/traversal/offset_rules.py +0 -76
- b24api-2.3.0/b24api/traversal/sequential.py +0 -252
- b24api-2.3.0/b24api/traversal/stream.py +0 -412
- b24api-2.3.0/pyproject.toml +0 -94
- {b24api-2.3.0 → b24api-2.5.0}/LICENSE +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/MANIFEST.in +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/_client_traversal.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/_error_types.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/batch/__init__.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/batch/outcome.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/completion/__init__.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/bounded_range.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/identity_store.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/json.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/keyset_capability.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/page.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/page_stop.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/stream.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/contracts/wire.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/encoding.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/execution/__init__.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/references/__init__.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/settings.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/testing/__init__.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/testing/_isolation.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/transport/__init__.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/__init__.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/cursor_domain.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/facade_support.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/identity_ledger.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/keyset_step.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api/traversal/sparse.py +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/dependency_links.txt +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/entry_points.txt +0 -0
- {b24api-2.3.0 → b24api-2.5.0}/b24api.egg-info/top_level.txt +0 -0
- {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
|
+
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:
|
|
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
|
|
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
|
-
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
<!-- tested: tests/readme_test.py::test_quickstart_runs_exactly_against_a_scripted_portal -->
|
|
31
52
|
```python
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
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
|
-

|
|
225
|
+

|
|
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
|
|
259
|
-
|
|
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
|
-

|
|
435
|
+

|
|
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
|
-

|
|
447
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
<!-- tested: tests/readme_test.py::test_quickstart_runs_exactly_against_a_scripted_portal -->
|
|
31
21
|
```python
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
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
|
-

|
|
194
|
+

|
|
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
|
|
259
|
-
|
|
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
|
-

|
|
404
|
+

|
|
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
|
-

|
|
416
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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.
|