b24api 2.2.0__tar.gz → 2.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {b24api-2.2.0 → b24api-2.4.2}/PKG-INFO +182 -64
- b24api-2.2.0/b24api.egg-info/PKG-INFO → b24api-2.4.2/README.md +160 -73
- b24api-2.4.2/b24api/__init__.py +134 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/_client_traversal.py +15 -0
- b24api-2.4.2/b24api/_diagnostics.py +138 -0
- b24api-2.4.2/b24api/_sources.py +172 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/batch/engine.py +255 -52
- {b24api-2.2.0 → b24api-2.4.2}/b24api/batch/facade.py +11 -12
- b24api-2.4.2/b24api/batch/logical.py +396 -0
- b24api-2.4.2/b24api/batch/stream.py +272 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/cli.py +11 -5
- {b24api-2.2.0 → b24api-2.4.2}/b24api/cli_contract.py +21 -8
- {b24api-2.2.0 → b24api-2.4.2}/b24api/client.py +50 -18
- b24api-2.4.2/b24api/completion/__init__.py +5 -0
- b24api-2.4.2/b24api/completion/closure.py +31 -0
- b24api-2.4.2/b24api/completion/fast_recorder.py +184 -0
- b24api-2.4.2/b24api/completion/gate.py +578 -0
- b24api-2.4.2/b24api/completion/operation_stream.py +294 -0
- b24api-2.4.2/b24api/completion/recorder.py +275 -0
- b24api-2.4.2/b24api/completion/reference_recorder.py +172 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/__init__.py +80 -1
- b24api-2.4.2/b24api/contracts/bounded_range.py +81 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/command.py +16 -3
- b24api-2.4.2/b24api/contracts/completion.py +173 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/dispatch.py +5 -4
- b24api-2.4.2/b24api/contracts/error_base.py +75 -0
- b24api-2.4.2/b24api/contracts/evidence.py +41 -0
- b24api-2.4.2/b24api/contracts/identity_store.py +45 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/keyset_capability.py +1 -1
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/keyset_execution.py +4 -2
- b24api-2.4.2/b24api/contracts/page_stop.py +68 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/policy.py +31 -11
- b24api-2.4.2/b24api/contracts/positional.py +254 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/reference.py +17 -2
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/report.py +55 -56
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/request.py +125 -51
- b24api-2.4.2/b24api/contracts/request_summary.py +63 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/response.py +6 -33
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/traversal.py +142 -1
- b24api-2.4.2/b24api/contracts/v3_codes.py +61 -0
- b24api-2.4.2/b24api/contracts/violation.py +98 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/errors.py +99 -92
- {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/__init__.py +14 -1
- b24api-2.4.2/b24api/execution/boundary.py +80 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/cleanup.py +15 -12
- {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/context.py +109 -70
- {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/executor.py +270 -189
- {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/failure.py +88 -12
- b24api-2.4.2/b24api/execution/lifecycle.py +400 -0
- b24api-2.4.2/b24api/execution/rate.py +341 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/execution/snapshot.py +13 -4
- b24api-2.4.2/b24api/execution/throttle.py +107 -0
- b24api-2.4.2/b24api/migration.py +260 -0
- b24api-2.4.2/b24api/py.typed +0 -0
- b24api-2.4.2/b24api/redaction.py +339 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/references/binding.py +29 -113
- {b24api-2.2.0 → b24api-2.4.2}/b24api/references/dispatch.py +42 -33
- b24api-2.4.2/b24api/references/dispatch_plan.py +26 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/references/facade.py +68 -71
- {b24api-2.2.0 → b24api-2.4.2}/b24api/references/fanout.py +39 -86
- {b24api-2.2.0 → b24api-2.4.2}/b24api/references/outcome.py +4 -7
- {b24api-2.2.0 → b24api-2.4.2}/b24api/references/scheduler.py +251 -225
- b24api-2.4.2/b24api/references/stream.py +274 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/references/support.py +41 -67
- {b24api-2.2.0 → b24api-2.4.2}/b24api/testing/__init__.py +3 -0
- b24api-2.4.2/b24api/testing/scripted.py +147 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/testing/transport.py +39 -7
- {b24api-2.2.0 → b24api-2.4.2}/b24api/transport/base.py +59 -4
- b24api-2.4.2/b24api/transport/decoding.py +207 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/transport/httpx.py +144 -47
- b24api-2.4.2/b24api/transport/log_records.py +111 -0
- b24api-2.4.2/b24api/transport/logging_shield.py +366 -0
- b24api-2.4.2/b24api/transport/protocol.py +306 -0
- b24api-2.4.2/b24api/traversal/control_preflight.py +94 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/counted.py +68 -7
- b24api-2.4.2/b24api/traversal/counted_batch.py +369 -0
- b24api-2.4.2/b24api/traversal/counted_rules.py +74 -0
- b24api-2.4.2/b24api/traversal/cursor.py +106 -0
- b24api-2.4.2/b24api/traversal/cursor_domain.py +61 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/driver.py +205 -124
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/facade.py +76 -59
- b24api-2.4.2/b24api/traversal/facade_support.py +36 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/identity.py +59 -83
- b24api-2.4.2/b24api/traversal/identity_ledger.py +59 -0
- b24api-2.4.2/b24api/traversal/keyset.py +67 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_auto.py +11 -20
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_capability.py +8 -107
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_eligibility.py +61 -20
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_fast_plan.py +1 -39
- b24api-2.4.2/b24api/traversal/keyset_fast_stream.py +174 -0
- b24api-2.4.2/b24api/traversal/keyset_geometry.py +313 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_observation.py +125 -83
- b24api-2.2.0/b24api/traversal/ordered_admission.py → b24api-2.4.2/b24api/traversal/keyset_ordered_admission.py +4 -13
- b24api-2.2.0/b24api/traversal/page_validation.py → b24api-2.4.2/b24api/traversal/keyset_page_validation.py +23 -12
- b24api-2.4.2/b24api/traversal/keyset_plan.py +453 -0
- b24api-2.4.2/b24api/traversal/keyset_reporting.py +96 -0
- b24api-2.4.2/b24api/traversal/keyset_runtime.py +413 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_step.py +46 -3
- b24api-2.4.2/b24api/traversal/keyset_transaction_contract.py +195 -0
- b24api-2.4.2/b24api/traversal/keyset_transactions.py +451 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/keyset_verifier.py +68 -16
- b24api-2.4.2/b24api/traversal/offset_rules.py +159 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/page_adaptation.py +3 -2
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/plans.py +44 -11
- b24api-2.4.2/b24api/traversal/sequential.py +232 -0
- b24api-2.4.2/b24api/traversal/sparse.py +55 -0
- b24api-2.4.2/b24api/traversal/strategy_context.py +181 -0
- b24api-2.4.2/b24api/traversal/stream.py +299 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/values.py +9 -4
- b24api-2.2.0/README.md → b24api-2.4.2/b24api.egg-info/PKG-INFO +191 -61
- {b24api-2.2.0 → b24api-2.4.2}/b24api.egg-info/SOURCES.txt +42 -8
- b24api-2.4.2/b24api.egg-info/requires.txt +5 -0
- b24api-2.4.2/pyproject.toml +132 -0
- b24api-2.2.0/b24api/__init__.py +0 -253
- b24api-2.2.0/b24api/_audit.py +0 -78
- b24api-2.2.0/b24api/_stream.py +0 -323
- b24api-2.2.0/b24api/batch/logical.py +0 -348
- b24api-2.2.0/b24api/batch/stream.py +0 -435
- b24api-2.2.0/b24api/execution/rate.py +0 -220
- b24api-2.2.0/b24api/redaction.py +0 -205
- b24api-2.2.0/b24api/references/stream.py +0 -376
- b24api-2.2.0/b24api/transport/protocol.py +0 -157
- b24api-2.2.0/b24api/traversal/counted_batch.py +0 -260
- b24api-2.2.0/b24api/traversal/cursor.py +0 -98
- b24api-2.2.0/b24api/traversal/keyset.py +0 -64
- b24api-2.2.0/b24api/traversal/keyset_costs.py +0 -185
- b24api-2.2.0/b24api/traversal/keyset_fast_stream.py +0 -274
- b24api-2.2.0/b24api/traversal/keyset_partition.py +0 -27
- b24api-2.2.0/b24api/traversal/keyset_range.py +0 -80
- b24api-2.2.0/b24api/traversal/keyset_reporting.py +0 -111
- b24api-2.2.0/b24api/traversal/keyset_scheduler.py +0 -626
- b24api-2.2.0/b24api/traversal/keyset_transaction_contract.py +0 -108
- b24api-2.2.0/b24api/traversal/keyset_transactions.py +0 -385
- b24api-2.2.0/b24api/traversal/sequential.py +0 -199
- b24api-2.2.0/b24api/traversal/stream.py +0 -325
- b24api-2.2.0/b24api.egg-info/requires.txt +0 -3
- b24api-2.2.0/pyproject.toml +0 -93
- {b24api-2.2.0 → b24api-2.4.2}/LICENSE +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/MANIFEST.in +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/_error_types.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/batch/__init__.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/batch/outcome.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/json.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/page.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/stream.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/contracts/wire.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/encoding.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/references/__init__.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/settings.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/testing/_isolation.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/transport/__init__.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api/traversal/__init__.py +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api.egg-info/dependency_links.txt +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api.egg-info/entry_points.txt +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/b24api.egg-info/top_level.txt +0 -0
- {b24api-2.2.0 → b24api-2.4.2}/setup.cfg +0 -0
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: b24api
|
|
3
|
-
Version: 2.2
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: Bitrix24 API
|
|
5
|
+
Author-email: Shkarupa Alex <shkarupa.alex@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shkarupa-alex/b24api
|
|
8
|
+
Project-URL: Repository, https://github.com/shkarupa-alex/b24api
|
|
9
|
+
Project-URL: Issues, https://github.com/shkarupa-alex/b24api/issues
|
|
10
|
+
Keywords: bitrix24,rest,api,client,async,httpx,batch,pagination
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
5
22
|
Requires-Python: >=3.12
|
|
6
23
|
Description-Content-Type: text/markdown
|
|
7
24
|
License-File: LICENSE
|
|
8
|
-
Requires-Dist: httpx[http2]
|
|
9
|
-
Requires-Dist:
|
|
25
|
+
Requires-Dist: httpx[http2]<0.29,>=0.28.1
|
|
26
|
+
Requires-Dist: h2<4.5,>=4.3.0
|
|
27
|
+
Requires-Dist: hpack<4.3,>=4.1.0
|
|
28
|
+
Requires-Dist: pydantic>=2.12.0
|
|
10
29
|
Requires-Dist: pydantic-settings>=2.10.1
|
|
11
30
|
Dynamic: license-file
|
|
12
31
|
|
|
13
|
-
# b24api
|
|
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
|
|
53
|
+
import os
|
|
54
|
+
|
|
32
55
|
from b24api import Bitrix24, Request
|
|
33
56
|
|
|
34
|
-
async with Bitrix24() as client:
|
|
35
|
-
|
|
57
|
+
async with Bitrix24.from_webhook(os.environ["BITRIX24_API_WEBHOOK_URL"]) as client:
|
|
58
|
+
deals = client.iter_list(Request.bare("crm.deal.list", {"select": ["ID", "TITLE"]}))
|
|
59
|
+
async for deal in deals:
|
|
60
|
+
print(deal["ID"], deal["TITLE"])
|
|
61
|
+
print(deals.report.state)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Four things are at work:
|
|
65
|
+
|
|
66
|
+
- **The client.** `Bitrix24.from_webhook()` checks the URL and owns the connection pool it opens;
|
|
67
|
+
`async with` closes it. `Bitrix24()` reads the same URL from the environment.
|
|
68
|
+
- **The request.** `Request.bare()` names a REST method and its parameters and sends them to the
|
|
69
|
+
classic `/rest/` endpoint. The route is always explicit: `Request.v3()` targets the V3 API.
|
|
70
|
+
- **`iter_list()`.** It walks the list page by page and reads one more, empty, page to confirm
|
|
71
|
+
the end.
|
|
72
|
+
- **The report.** `deals.report` says how the traversal ended. `completed` means every page was
|
|
73
|
+
read; a traversal that stops early or fails records why.
|
|
74
|
+
|
|
75
|
+
A single call returns the decoded `result`:
|
|
76
|
+
|
|
77
|
+
<!-- tested: tests/readme_test.py::test_quickstart_runs_exactly_against_a_scripted_portal -->
|
|
78
|
+
```python
|
|
79
|
+
users = await client.call(Request.bare("user.get", {"ID": 1}))
|
|
36
80
|
```
|
|
37
81
|
|
|
38
|
-
The client owns
|
|
39
|
-
idempotent and closes active streams before the owned transport.
|
|
82
|
+
The client owns the transport it creates. An injected transport remains caller-owned. `aclose()` is
|
|
83
|
+
idempotent and closes active streams before the owned transport. To prove that no row is missing or
|
|
84
|
+
repeated, give the traversal an identity (see [Choosing a list operation](#choosing-a-list-operation)).
|
|
40
85
|
|
|
41
86
|
## Direct calls
|
|
42
87
|
|
|
@@ -48,15 +93,17 @@ The operation is explicit and never hides malformed JSON by falling back to byte
|
|
|
48
93
|
|
|
49
94
|
<!-- tested: tests/client_findings_3_test.py::test_binary_call_returns_every_success_byte_without_json_sniffing -->
|
|
50
95
|
```python
|
|
51
|
-
|
|
96
|
+
from b24api import RouteKind
|
|
97
|
+
archive = await client.call_bytes(Request("example.export.download", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE))
|
|
52
98
|
payload = archive.body
|
|
53
99
|
```
|
|
54
100
|
|
|
55
101
|
<!-- tested: tests/client_v2_test.py::test_call_and_call_response_have_stable_detached_types -->
|
|
56
102
|
```python
|
|
103
|
+
from b24api import RouteKind
|
|
57
104
|
from b24api import ReplaySafety
|
|
58
105
|
|
|
59
|
-
request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE)
|
|
106
|
+
request = Request("example.item.get", {"id": 7}, ReplaySafety.SAFE, route=RouteKind.BARE)
|
|
60
107
|
decoded = await client.call(request)
|
|
61
108
|
response = await client.call_response(request)
|
|
62
109
|
```
|
|
@@ -69,10 +116,17 @@ may already have reached Bitrix:
|
|
|
69
116
|
| Value | Meaning | After possible dispatch |
|
|
70
117
|
|---|---|---|
|
|
71
118
|
| `SAFE` | Repeating the request cannot create a second business effect. Typical reads and explicitly idempotent operations belong here. | Automatic retry is allowed within policy budgets. |
|
|
72
|
-
| `UNSAFE` | Repeating the request is known to risk a duplicate effect, for example creating an entity without an idempotency key. | No automatic replay; the caller receives an ambiguous-execution error and reconciles state. |
|
|
119
|
+
| `UNSAFE` | Repeating the request is known to risk a duplicate effect, for example creating an entity without an idempotency key. | No automatic replay; the caller receives an ambiguous-execution error and reconciles state. It is retried only when the failure proves it did not run. |
|
|
73
120
|
| `UNKNOWN` | The caller has not established whether replay is safe. This is the default. | Same conservative behavior as `UNSAFE`, while diagnostics preserve that safety was unknown rather than known unsafe. |
|
|
74
121
|
|
|
75
|
-
A failure
|
|
122
|
+
A failure that proves the request did not run is retried whatever its safety: a transport failure
|
|
123
|
+
before dispatch, or a refusal listed in `AmbiguityPolicy`, which by default is an unstructured 423, 425
|
|
124
|
+
or 429 (`refusal_http_statuses`) or a `QUERY_LIMIT_EXCEEDED` / `OPERATION_TIME_LIMIT` refusal
|
|
125
|
+
(`refusal_api_codes`). A code or status added only to `RetryPolicy` is retried for `SAFE` work alone.
|
|
126
|
+
A physical batch applies these rules to each command: after a failure, only the commands that
|
|
127
|
+
may run again are sent again, in a smaller batch, and an `UNSAFE` command of a batch that may have run
|
|
128
|
+
arrives as unknown. Replay rounds spend the same attempt and retry-time budget as the sends before
|
|
129
|
+
them. Method names never imply safety;
|
|
76
130
|
mark a request `SAFE` only when the operation's semantics justify it.
|
|
77
131
|
|
|
78
132
|
Use `ExecutionPolicy` to narrow attempts or resource budgets for one operation:
|
|
@@ -85,6 +139,22 @@ one_attempt = ExecutionPolicy(max_attempts_per_request=1)
|
|
|
85
139
|
result = await client.call(request, policy=one_attempt)
|
|
86
140
|
```
|
|
87
141
|
|
|
142
|
+
A `policy=` argument replaces the client's default policy wholesale; fields are never merged. The
|
|
143
|
+
client default is `ExecutionPolicy.from_settings(settings)`: the library defaults with
|
|
144
|
+
`max_retry_elapsed_per_request` taken from `Settings.http_timeout` (30 s by default, while a bare
|
|
145
|
+
`ExecutionPolicy()` allows 120 s). To change one field and keep the configured timeout, derive the
|
|
146
|
+
per-call policy from that default:
|
|
147
|
+
|
|
148
|
+
<!-- tested: tests/settings_test.py::test_a_per_call_policy_replaces_the_client_default_without_merging -->
|
|
149
|
+
```python
|
|
150
|
+
import dataclasses
|
|
151
|
+
|
|
152
|
+
from b24api import ExecutionPolicy
|
|
153
|
+
|
|
154
|
+
one_attempt = dataclasses.replace(ExecutionPolicy.from_settings(settings), max_attempts_per_request=1)
|
|
155
|
+
result = await client.call(request, policy=one_attempt)
|
|
156
|
+
```
|
|
157
|
+
|
|
88
158
|
## Logical batch and correlation
|
|
89
159
|
|
|
90
160
|
`batch()` accepts an arbitrary-length synchronous or asynchronous command source. It consumes the
|
|
@@ -97,11 +167,12 @@ matching a result to the object, file, chat or database row that produced its re
|
|
|
97
167
|
|
|
98
168
|
<!-- tested: tests/client_v2_test.py::test_logical_batch_is_unbounded_ordered_and_correlation_is_strictly_off_wire -->
|
|
99
169
|
```python
|
|
100
|
-
from b24api import
|
|
170
|
+
from b24api import RouteKind
|
|
171
|
+
from b24api.contracts import Command, CommandSuccess
|
|
101
172
|
|
|
102
173
|
commands = (
|
|
103
174
|
Command(
|
|
104
|
-
Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE),
|
|
175
|
+
Request("example.item.get", {"id": item_id}, ReplaySafety.SAFE, route=RouteKind.BARE),
|
|
105
176
|
correlation=item_id,
|
|
106
177
|
)
|
|
107
178
|
for item_id in source_ids
|
|
@@ -118,7 +189,7 @@ async with client.batch(commands, batch_size=25) as stream:
|
|
|
118
189
|
|
|
119
190
|
<!-- tested: tests/client_v2_test.py::test_batch_outcomes_retains_typed_failure_without_halting_later_commands -->
|
|
120
191
|
```python
|
|
121
|
-
from b24api import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
|
|
192
|
+
from b24api.contracts import CommandFailure, CommandNotExecuted, CommandOutcomeUnknown
|
|
122
193
|
|
|
123
194
|
async with client.batch_outcomes(commands) as stream:
|
|
124
195
|
async for outcome in stream:
|
|
@@ -151,7 +222,16 @@ exact `limit_path`; the client never guesses method-specific parameter names.
|
|
|
151
222
|
|
|
152
223
|
### List traversal comparison
|
|
153
224
|
|
|
154
|
-

|
|
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.
|
|
155
235
|
|
|
156
236
|
### Sequential offset
|
|
157
237
|
|
|
@@ -166,6 +246,7 @@ control this strategy's completion. Exact database implementation is endpoint-sp
|
|
|
166
246
|
|
|
167
247
|
<!-- tested: tests/client_v2_test.py::test_iter_list_is_sequential_mechanics_only_and_report_is_post_cleanup -->
|
|
168
248
|
```python
|
|
249
|
+
from b24api import RouteKind
|
|
169
250
|
from b24api import IdentityCoercion, IdentitySpec, ResultSelector
|
|
170
251
|
|
|
171
252
|
identity = IdentitySpec(
|
|
@@ -176,7 +257,7 @@ identity = IdentitySpec(
|
|
|
176
257
|
)
|
|
177
258
|
|
|
178
259
|
stream = client.iter_list(
|
|
179
|
-
Request("example.item.list", replay_safety=ReplaySafety.SAFE),
|
|
260
|
+
Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
|
|
180
261
|
selector=ResultSelector(("items",)),
|
|
181
262
|
identity=identity,
|
|
182
263
|
)
|
|
@@ -194,10 +275,11 @@ sequence and records that degradation in the operation report.
|
|
|
194
275
|
|
|
195
276
|
<!-- tested: tests/client_findings_3_test.py::test_shape_rejection_is_retained_as_zero_admission_page_evidence -->
|
|
196
277
|
```python
|
|
197
|
-
from b24api import
|
|
278
|
+
from b24api import RouteKind
|
|
279
|
+
from b24api.contracts import ResultCollectionShape
|
|
198
280
|
|
|
199
281
|
stream = client.iter_list(
|
|
200
|
-
Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE),
|
|
282
|
+
Request("example.dictionary.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
|
|
201
283
|
selector=ResultSelector(("items",)),
|
|
202
284
|
collection_shape=ResultCollectionShape.MAPPING_VALUES,
|
|
203
285
|
)
|
|
@@ -214,8 +296,9 @@ bounded physical batches.
|
|
|
214
296
|
|
|
215
297
|
<!-- tested: tests/client_v2_test.py::test_counted_traversal_preserves_frozen_request_shape_and_exact_identity -->
|
|
216
298
|
```python
|
|
299
|
+
from b24api import RouteKind
|
|
217
300
|
stream = client.iter_list_counted(
|
|
218
|
-
Request("example.item.list", replay_safety=ReplaySafety.SAFE),
|
|
301
|
+
Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
|
|
219
302
|
selector=ResultSelector(("items",)),
|
|
220
303
|
identity=identity,
|
|
221
304
|
page_size=50,
|
|
@@ -226,6 +309,16 @@ stream = client.iter_list_counted(
|
|
|
226
309
|
Use it only when `total` is exact for the supplied filter and offset pages are stable. Any missing
|
|
227
310
|
range, overlap, duplicate identity or total contradiction raises `IncompleteTraversalError`.
|
|
228
311
|
|
|
312
|
+
A filtered call that matches nothing may omit `total` entirely, as `user.get` does with
|
|
313
|
+
`result: []`. A first page with no rows, no `next` and no usable `total` (missing, `null`, or the
|
|
314
|
+
`-1` unknown sentinel) therefore completes as an observed empty source after that one request: the
|
|
315
|
+
report is `completed` and `exhausted`, but its assurance is `mechanics_only` (`identity_exact` with an
|
|
316
|
+
identity), never a count-matched claim, and no total is invented. A first page that reports
|
|
317
|
+
`total: 0` keeps the count-matched result. Rows without a usable total, a remaining `next`, a
|
|
318
|
+
positive total with no rows, a fixed step, or a `ConsistencyPolicy` whose `confirmation_policy` is
|
|
319
|
+
`QUALIFIED_TOTAL` (from `b24api.contracts.policy.ConfirmationPolicy`) stay strict and raise
|
|
320
|
+
`IncompleteTraversalError`.
|
|
321
|
+
|
|
229
322
|
Physical batching reduces HTTP exchanges, but does not suppress server-side COUNT in ordinary
|
|
230
323
|
counted list subrequests. Each command still performs its own offset page retrieval and associated
|
|
231
324
|
total calculation on the server. Do not confuse batching these commands with a no-count traversal.
|
|
@@ -249,14 +342,19 @@ Static incompatibility with the auto contract raises `CapabilityError` from the
|
|
|
249
342
|
`iter_list_keyset(...)` call before iteration begins. A portal that accepts but contradicts the
|
|
250
343
|
declared controls fails before emission with `IncompleteTraversalError`; auto never restarts that
|
|
251
344
|
operation silently. `KeysetTraversal` inside reference traversal remains sequential-only. The
|
|
252
|
-
terminal report
|
|
253
|
-
|
|
345
|
+
terminal report carries a compact `keyset_selection` (`requested_kind`, `selected_kind`, `reason`)
|
|
346
|
+
for every keyset traversal. A `page_stop` callback needs the ordered page stream, so auto reports
|
|
347
|
+
`AUTO`, `SEQUENTIAL`, `PAGE_STOP`; an explicit `SequentialKeysetExecution()` reports
|
|
348
|
+
`EXPLICIT_SEQUENTIAL`. The detailed `keyset_execution` report is present only when the fast path ran.
|
|
254
349
|
|
|
255
350
|
Use `await client.verify_keyset_capability(...)` as a development/CI/staging guard on a stable
|
|
256
351
|
representative fixture. It performs five strict-bound checks and returns only a `VERIFIED` report;
|
|
257
352
|
unsupported and inconclusive verdicts raise `KeysetCapabilityError`. The ordinary
|
|
258
353
|
`iter_list_keyset()` remains a separate caller-asserted operation with zero verifier canaries and
|
|
259
354
|
may emit a partial prefix before a late endpoint contradiction is detected.
|
|
355
|
+
Keep the guard beside the traversal, for example under
|
|
356
|
+
`if os.environ.get("ENV") != "PROD":`; set `ENV=PROD` only after qualifying the exact portal,
|
|
357
|
+
credentials, method, request/filter, identity, ordering representation, and page cap.
|
|
260
358
|
|
|
261
359
|
Every list operation also accepts an immutable `PageAdapter` strategy. The adapter synchronously
|
|
262
360
|
maps selected frozen items using sibling result metadata while preserving cardinality, order and
|
|
@@ -269,16 +367,33 @@ record the selected strategy and reason: unbounded auto continuation has the sam
|
|
|
269
367
|
|
|
270
368
|
<!-- tested: tests/keyset_fast_test.py::test_omitted_execution_defaults_to_auto -->
|
|
271
369
|
```python
|
|
272
|
-
|
|
370
|
+
import os
|
|
371
|
+
|
|
372
|
+
from b24api import KeysetSpec, ParameterPath, ReplaySafety, Request, ResultSelector, RouteKind
|
|
373
|
+
|
|
374
|
+
request = Request("example.item.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE)
|
|
375
|
+
selector = ResultSelector(("items",))
|
|
376
|
+
keyset = KeysetSpec(
|
|
377
|
+
filter_path=ParameterPath(("filter",)),
|
|
378
|
+
order_path=ParameterPath(("order",)),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if os.environ.get("ENV") != "PROD":
|
|
382
|
+
# Accepting an ID filter does not prove strict bounds or ordering.
|
|
383
|
+
await client.verify_keyset_capability(
|
|
384
|
+
request,
|
|
385
|
+
selector=selector,
|
|
386
|
+
identity=identity,
|
|
387
|
+
page_size=50,
|
|
388
|
+
keyset=keyset,
|
|
389
|
+
)
|
|
273
390
|
|
|
274
391
|
stream = client.iter_list_keyset(
|
|
275
|
-
|
|
276
|
-
selector=
|
|
392
|
+
request,
|
|
393
|
+
selector=selector,
|
|
277
394
|
identity=identity,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
order_path=ParameterPath(("order",)),
|
|
281
|
-
),
|
|
395
|
+
page_size=50,
|
|
396
|
+
keyset=keyset,
|
|
282
397
|
)
|
|
283
398
|
```
|
|
284
399
|
|
|
@@ -289,10 +404,11 @@ message-list methods.
|
|
|
289
404
|
|
|
290
405
|
<!-- tested: tests/client_v2_test.py::test_keyset_and_cursor_are_explicit_strict_alternatives -->
|
|
291
406
|
```python
|
|
407
|
+
from b24api import RouteKind
|
|
292
408
|
from b24api import CursorSpec, ParameterPath
|
|
293
409
|
|
|
294
410
|
stream = client.iter_list_cursor(
|
|
295
|
-
Request("example.message.list", replay_safety=ReplaySafety.SAFE),
|
|
411
|
+
Request("example.message.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
|
|
296
412
|
selector=ResultSelector(("items",)),
|
|
297
413
|
cursor=CursorSpec(
|
|
298
414
|
parameter_path=ParameterPath(("LAST_ID",)),
|
|
@@ -310,10 +426,10 @@ boundary, use an application-owned direct-call workflow or supply a unique tie-b
|
|
|
310
426
|
For multiple parent-bound cursor chains, `iter_cursors()` keeps cursor progress and correlation
|
|
311
427
|
isolated per binding while ready pages share the physical batch queue.
|
|
312
428
|
|
|
313
|
-

|
|
429
|
+

|
|
314
430
|
|
|
315
|
-
See [architecture](docs/architecture.md), [migration](docs/migration.md),
|
|
316
|
-
[performance](docs/performance.md), and [endpoint recipes](docs/recipes.md) for the complete
|
|
431
|
+
See [architecture](https://github.com/shkarupa-alex/b24api/blob/master/docs/architecture.md), [migration](https://github.com/shkarupa-alex/b24api/blob/master/docs/migration.md),
|
|
432
|
+
[performance](https://github.com/shkarupa-alex/b24api/blob/master/docs/performance.md), and [endpoint recipes](https://github.com/shkarupa-alex/b24api/blob/master/docs/recipes.md) for the complete
|
|
317
433
|
contracts and selection guidance.
|
|
318
434
|
|
|
319
435
|
### One list method across many parent entities
|
|
@@ -322,19 +438,13 @@ contracts and selection guidance.
|
|
|
322
438
|
client remains unaware of entity types: a binding can represent a deal, lead, chat or any other
|
|
323
439
|
caller-defined parent.
|
|
324
440
|
|
|
325
|
-

|
|
441
|
+

|
|
326
442
|
|
|
327
443
|
<!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
|
|
328
444
|
```python
|
|
329
|
-
from b24api import
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
ParameterPath,
|
|
333
|
-
ParameterUpdate,
|
|
334
|
-
ReferenceComplete,
|
|
335
|
-
ReferenceItem,
|
|
336
|
-
SequentialTraversal,
|
|
337
|
-
)
|
|
445
|
+
from b24api import RouteKind
|
|
446
|
+
from b24api import BatchDispatch, Binding, ParameterPath, ParameterUpdate, SequentialTraversal
|
|
447
|
+
from b24api.contracts import ReferenceComplete, ReferenceItem
|
|
338
448
|
|
|
339
449
|
bindings = (
|
|
340
450
|
Binding(
|
|
@@ -346,7 +456,7 @@ bindings = (
|
|
|
346
456
|
)
|
|
347
457
|
|
|
348
458
|
stream = client.iter_references(
|
|
349
|
-
Request("example.comment.list", replay_safety=ReplaySafety.SAFE),
|
|
459
|
+
Request("example.comment.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
|
|
350
460
|
bindings,
|
|
351
461
|
traversal=SequentialTraversal(selector=ResultSelector(("items",)), identity=identity),
|
|
352
462
|
dispatch=BatchDispatch(batch_size=25, concurrency=2),
|
|
@@ -366,6 +476,7 @@ under different parents are not conflated.
|
|
|
366
476
|
|
|
367
477
|
<!-- tested: tests/client_v2_test.py::test_bound_references_apply_nested_updates_off_wire_and_emit_exact_completion -->
|
|
368
478
|
```python
|
|
479
|
+
from b24api import RouteKind
|
|
369
480
|
from b24api import (
|
|
370
481
|
Binding,
|
|
371
482
|
CursorSpec,
|
|
@@ -387,7 +498,7 @@ chat_bindings = (
|
|
|
387
498
|
)
|
|
388
499
|
|
|
389
500
|
messages = client.iter_references(
|
|
390
|
-
Request("example.message.list", replay_safety=ReplaySafety.SAFE),
|
|
501
|
+
Request("example.message.list", replay_safety=ReplaySafety.SAFE, route=RouteKind.BARE),
|
|
391
502
|
chat_bindings,
|
|
392
503
|
traversal=CursorTraversal(
|
|
393
504
|
selector=ResultSelector(("items",)),
|
|
@@ -430,13 +541,19 @@ and publish the same final report where the Python exception type permits it.
|
|
|
430
541
|
## Resource boundaries
|
|
431
542
|
|
|
432
543
|
`ExecutionPolicy` bounds requests, pages, elapsed time, attempts, decompressed response bytes,
|
|
433
|
-
buffered commands and rows, direct concurrency and active
|
|
434
|
-
is 16 MiB and is enforced while streaming, before JSON
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
544
|
+
buffered commands and rows, retained unordered identity keys, direct concurrency and active
|
|
545
|
+
references. The default response ceiling is 16 MiB and is enforced while streaming, before JSON
|
|
546
|
+
decoding.
|
|
547
|
+
|
|
548
|
+
Sequential, counted, and multi-reference exact traversal retain at most `max_identity_keys`
|
|
549
|
+
observed identities per operation in memory (100,000 by default). All active reference bindings
|
|
550
|
+
share that ceiling. A page that would exceed it is rejected atomically with typed budget evidence.
|
|
551
|
+
Set a larger finite ceiling when the expected aggregate cardinality is known, or pass
|
|
552
|
+
`identity_store=` to `iter_list`/`iter_list_counted` so a caller-owned `IdentityStore` (for example a
|
|
553
|
+
SQLite table keyed by `identity_store_key(...)`) proves uniqueness while in-process identity memory
|
|
554
|
+
stays bounded by one page; the client never closes that store. Repeated-page detection still keeps
|
|
555
|
+
one short fingerprint per page, so raise `max_pages` deliberately for very long traversals.
|
|
556
|
+
Strict keyset and cursor traversal retain only monotonic progression state when sufficient.
|
|
440
557
|
|
|
441
558
|
## CLI
|
|
442
559
|
|
|
@@ -445,10 +562,10 @@ errors go to stderr. Credentials come only from `Settings` and cannot be passed
|
|
|
445
562
|
|
|
446
563
|
<!-- tested-console: tests/cli_test.py::test_call_routes_replay_safety_and_keeps_success_data_on_stdout -->
|
|
447
564
|
```console
|
|
448
|
-
b24api call profile
|
|
449
|
-
b24api call example.item.get --params '{"id":7}' --raw --replay-safety safe
|
|
450
|
-
b24api list example.item.list --params @params.json
|
|
451
|
-
b24api list example.item.list --strategy counted --contract @counted-contract.json
|
|
565
|
+
b24api call profile --route bare
|
|
566
|
+
b24api call example.item.get --route bare --params '{"id":7}' --raw --replay-safety safe
|
|
567
|
+
b24api list example.item.list --route bare --params @params.json
|
|
568
|
+
b24api list example.item.list --route bare --strategy counted --contract @counted-contract.json
|
|
452
569
|
```
|
|
453
570
|
|
|
454
571
|
The `--raw` CLI option selects the response envelope; it does not alter the Python API. Advanced
|
|
@@ -483,21 +600,22 @@ uv run --with memray memray stats /tmp/b24api.bin
|
|
|
483
600
|
```
|
|
484
601
|
|
|
485
602
|
These deterministic fixtures characterize local resources and network shape; they are not live
|
|
486
|
-
portal latency admission. See [docs/performance.md](docs/performance.md) for current measurements
|
|
487
|
-
and [docs/architecture.md](docs/architecture.md) for guarantees and ownership boundaries.
|
|
603
|
+
portal latency admission. See [docs/performance.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/performance.md) for current measurements
|
|
604
|
+
and [docs/architecture.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/architecture.md) for guarantees and ownership boundaries.
|
|
488
605
|
|
|
489
|
-
Projects moving from an earlier API surface can use [docs/migration.md](docs/migration.md).
|
|
606
|
+
Projects moving from an earlier API surface can use [docs/migration.md](https://github.com/shkarupa-alex/b24api/blob/master/docs/migration.md).
|
|
490
607
|
|
|
491
608
|
## Verification
|
|
492
609
|
|
|
493
610
|
```console
|
|
494
611
|
uv sync --frozen
|
|
495
|
-
|
|
496
|
-
.venv/bin/ruff check . --no-fix --no-cache
|
|
497
|
-
.venv/bin/ruff format --check . --no-cache
|
|
498
|
-
.venv/bin/mypy --strict b24api tools/b24api_evidence
|
|
612
|
+
make qc
|
|
499
613
|
git diff --check
|
|
500
614
|
```
|
|
501
615
|
|
|
616
|
+
`make qc` runs the lint, type and default test checks that CI blocks on. It leaves out the internal
|
|
617
|
+
benches (the pytest marker `slow`: the evidence harness contracts and the 50k/100k-scale runs, which
|
|
618
|
+
take several minutes). `make bench` runs them, and so does the blocking CI job `slow`.
|
|
619
|
+
|
|
502
620
|
The wheel regression installs into an isolated environment, executes the `b24api` entry point and
|
|
503
621
|
checks that tests, live/evidence tooling and credentials are excluded.
|