b24api 0.1.3__tar.gz → 0.3.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-0.3.0/PKG-INFO +93 -0
- b24api-0.3.0/README.md +79 -0
- {b24api-0.1.3 → b24api-0.3.0}/b24api/api.py +143 -61
- {b24api-0.1.3 → b24api-0.3.0}/b24api/entity.py +27 -20
- {b24api-0.1.3 → b24api-0.3.0}/b24api/error.py +6 -1
- b24api-0.1.3/b24api/http.py → b24api-0.3.0/b24api/httpc.py +6 -1
- b24api-0.3.0/b24api/query.py +36 -0
- {b24api-0.1.3 → b24api-0.3.0}/b24api/settings.py +3 -3
- b24api-0.3.0/b24api.egg-info/PKG-INFO +93 -0
- {b24api-0.1.3 → b24api-0.3.0}/b24api.egg-info/SOURCES.txt +1 -2
- {b24api-0.1.3 → b24api-0.3.0}/pyproject.toml +2 -4
- b24api-0.1.3/PKG-INFO +0 -16
- b24api-0.1.3/README.md +0 -3
- b24api-0.1.3/b24api/future.py +0 -12
- b24api-0.1.3/b24api/query.py +0 -31
- b24api-0.1.3/b24api.egg-info/PKG-INFO +0 -16
- {b24api-0.1.3 → b24api-0.3.0}/LICENSE +0 -0
- {b24api-0.1.3 → b24api-0.3.0}/MANIFEST.in +0 -0
- {b24api-0.1.3 → b24api-0.3.0}/b24api/__init__.py +0 -0
- {b24api-0.1.3 → b24api-0.3.0}/b24api/type.py +0 -0
- {b24api-0.1.3 → b24api-0.3.0}/b24api.egg-info/dependency_links.txt +0 -0
- {b24api-0.1.3 → b24api-0.3.0}/b24api.egg-info/requires.txt +0 -0
- {b24api-0.1.3 → b24api-0.3.0}/b24api.egg-info/top_level.txt +0 -0
- {b24api-0.1.3 → b24api-0.3.0}/setup.cfg +0 -0
b24api-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: b24api
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Bitrix24 API
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fast-depends>=2.4.12
|
|
9
|
+
Requires-Dist: httpx[http2]>=0.28.1
|
|
10
|
+
Requires-Dist: pydantic>=2.10.6
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.8.1
|
|
12
|
+
Requires-Dist: retry>=0.9.2
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# API client for Bitrix24
|
|
16
|
+
|
|
17
|
+
Low-level API client with multiple strategies for lists gathering.
|
|
18
|
+
All methods support retries.
|
|
19
|
+
|
|
20
|
+
## Regular call (any method)
|
|
21
|
+
```python
|
|
22
|
+
from b24api import Bitrix24
|
|
23
|
+
|
|
24
|
+
b24 = Bitrix24()
|
|
25
|
+
result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
|
|
26
|
+
print(result)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Batch call (any method, no limit)
|
|
30
|
+
```python
|
|
31
|
+
from b24api import Bitrix24
|
|
32
|
+
|
|
33
|
+
b24 = Bitrix24()
|
|
34
|
+
|
|
35
|
+
requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
|
|
36
|
+
result = b24.batch(requests)
|
|
37
|
+
print(result)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Gathering full list (slow)
|
|
41
|
+
Applicable to list methods with `start=<offset>` support.
|
|
42
|
+
Fetches list chunks one by one.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from b24api import Bitrix24
|
|
46
|
+
|
|
47
|
+
b24 = Bitrix24()
|
|
48
|
+
result = b24.list_sequential({"method": "user.get"})
|
|
49
|
+
print(list(result))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Gathering full list (faster)
|
|
53
|
+
Applicable to list methods with `start=<offset>` support.
|
|
54
|
+
Fetches first list chunk with regular call, then fetches other chunks with `batch`.
|
|
55
|
+
Approximately 3 times faster then `list_sequential`.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from b24api import Bitrix24
|
|
59
|
+
|
|
60
|
+
b24 = Bitrix24()
|
|
61
|
+
result = b24.list_batched({"method": "user.get"})
|
|
62
|
+
print(list(result))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Gathering full list (fastest)
|
|
66
|
+
Applicable to list methods with `filter={<parameters>}` support.
|
|
67
|
+
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
68
|
+
Approximately 2 times faster then `list_batched`.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from b24api import Bitrix24
|
|
72
|
+
|
|
73
|
+
b24 = Bitrix24()
|
|
74
|
+
result = b24.list_batched_no_count({"method": "user.get"})
|
|
75
|
+
print(list(result))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Gathering full list with required reference
|
|
79
|
+
Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}` support and required filter parameters.
|
|
80
|
+
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from b24api import Bitrix24
|
|
84
|
+
|
|
85
|
+
b24 = Bitrix24()
|
|
86
|
+
deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
|
|
87
|
+
filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
|
|
88
|
+
result = b24.reference_batched_no_count(
|
|
89
|
+
{"method": "crm.timeline.comment.list", "parameters": {"ENTITY_TYPE": "deal"}},
|
|
90
|
+
filter_updates,
|
|
91
|
+
)
|
|
92
|
+
print(list(result))
|
|
93
|
+
```
|
b24api-0.3.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# API client for Bitrix24
|
|
2
|
+
|
|
3
|
+
Low-level API client with multiple strategies for lists gathering.
|
|
4
|
+
All methods support retries.
|
|
5
|
+
|
|
6
|
+
## Regular call (any method)
|
|
7
|
+
```python
|
|
8
|
+
from b24api import Bitrix24
|
|
9
|
+
|
|
10
|
+
b24 = Bitrix24()
|
|
11
|
+
result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
|
|
12
|
+
print(result)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Batch call (any method, no limit)
|
|
16
|
+
```python
|
|
17
|
+
from b24api import Bitrix24
|
|
18
|
+
|
|
19
|
+
b24 = Bitrix24()
|
|
20
|
+
|
|
21
|
+
requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
|
|
22
|
+
result = b24.batch(requests)
|
|
23
|
+
print(result)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Gathering full list (slow)
|
|
27
|
+
Applicable to list methods with `start=<offset>` support.
|
|
28
|
+
Fetches list chunks one by one.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from b24api import Bitrix24
|
|
32
|
+
|
|
33
|
+
b24 = Bitrix24()
|
|
34
|
+
result = b24.list_sequential({"method": "user.get"})
|
|
35
|
+
print(list(result))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Gathering full list (faster)
|
|
39
|
+
Applicable to list methods with `start=<offset>` support.
|
|
40
|
+
Fetches first list chunk with regular call, then fetches other chunks with `batch`.
|
|
41
|
+
Approximately 3 times faster then `list_sequential`.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from b24api import Bitrix24
|
|
45
|
+
|
|
46
|
+
b24 = Bitrix24()
|
|
47
|
+
result = b24.list_batched({"method": "user.get"})
|
|
48
|
+
print(list(result))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Gathering full list (fastest)
|
|
52
|
+
Applicable to list methods with `filter={<parameters>}` support.
|
|
53
|
+
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
54
|
+
Approximately 2 times faster then `list_batched`.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from b24api import Bitrix24
|
|
58
|
+
|
|
59
|
+
b24 = Bitrix24()
|
|
60
|
+
result = b24.list_batched_no_count({"method": "user.get"})
|
|
61
|
+
print(list(result))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Gathering full list with required reference
|
|
65
|
+
Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}` support and required filter parameters.
|
|
66
|
+
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from b24api import Bitrix24
|
|
70
|
+
|
|
71
|
+
b24 = Bitrix24()
|
|
72
|
+
deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
|
|
73
|
+
filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
|
|
74
|
+
result = b24.reference_batched_no_count(
|
|
75
|
+
{"method": "crm.timeline.comment.list", "parameters": {"ENTITY_TYPE": "deal"}},
|
|
76
|
+
filter_updates,
|
|
77
|
+
)
|
|
78
|
+
print(list(result))
|
|
79
|
+
```
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
2
4
|
from collections.abc import Generator, Iterable
|
|
3
|
-
from itertools import chain
|
|
5
|
+
from itertools import chain, islice
|
|
4
6
|
from operator import itemgetter
|
|
5
7
|
|
|
6
8
|
import httpx
|
|
7
9
|
from fast_depends import inject
|
|
8
10
|
from pydantic import ValidationError
|
|
9
|
-
from retry
|
|
11
|
+
from retry import retry
|
|
10
12
|
|
|
11
|
-
from b24api.entity import ApiTypes, BatchResult, ErrorResponse, ListRequest, Request,
|
|
13
|
+
from b24api.entity import ApiTypes, BatchResult, ErrorResponse, ListRequest, Request, Response
|
|
12
14
|
from b24api.error import RetryApiResponseError, RetryHTTPStatusError
|
|
13
|
-
from b24api.
|
|
14
|
-
from b24api.http import HttpxClient
|
|
15
|
+
from b24api.httpc import HttpxClient
|
|
15
16
|
from b24api.settings import ApiSettings
|
|
16
17
|
|
|
17
18
|
|
|
@@ -20,28 +21,36 @@ class Bitrix24:
|
|
|
20
21
|
def __init__(self, client: HttpxClient, settings: ApiSettings) -> None:
|
|
21
22
|
self.client = client
|
|
22
23
|
self.settings = settings
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"""Call any method (with retries) and return just `result`."""
|
|
26
|
-
response = retry_call(
|
|
27
|
-
self._call,
|
|
28
|
-
fargs=[request],
|
|
24
|
+
self.logger = logging.getLogger("b24api")
|
|
25
|
+
self._call_retry = retry(
|
|
29
26
|
exceptions=(RetryHTTPStatusError, RetryApiResponseError),
|
|
30
27
|
tries=self.settings.retry_tries,
|
|
31
28
|
delay=self.settings.retry_delay,
|
|
32
29
|
backoff=self.settings.retry_backoff,
|
|
33
|
-
|
|
30
|
+
logger=self.logger,
|
|
31
|
+
)(self._call)
|
|
32
|
+
|
|
33
|
+
def call(self, request: Request | dict) -> ApiTypes:
|
|
34
|
+
"""Call any method (with retries) and return just `result`."""
|
|
35
|
+
response = self._call_retry(request)
|
|
34
36
|
|
|
35
37
|
return response.result
|
|
36
38
|
|
|
37
|
-
def _call(self, request: Request) ->
|
|
39
|
+
def _call(self, request: Request | dict) -> Response:
|
|
38
40
|
"""Call any method and return full response."""
|
|
41
|
+
request = Request.model_validate(request)
|
|
42
|
+
|
|
43
|
+
self.logger.debug("Sending request", extra={"request": request})
|
|
44
|
+
|
|
39
45
|
http_response = self.client.post(
|
|
40
46
|
f"{self.settings.webhook_url}{request.method}",
|
|
41
47
|
headers={"Content-Type": "application/json"},
|
|
42
48
|
json=request.model_dump()["parameters"],
|
|
43
49
|
)
|
|
44
50
|
|
|
51
|
+
with contextlib.suppress(httpx.ResponseNotRead, json.JSONDecodeError, ValidationError):
|
|
52
|
+
ErrorResponse.model_validate(http_response.json()).raise_error(self.settings.retry_errors)
|
|
53
|
+
|
|
45
54
|
try:
|
|
46
55
|
json_response = http_response.raise_for_status().json()
|
|
47
56
|
except httpx.HTTPStatusError as error:
|
|
@@ -56,23 +65,37 @@ class Bitrix24:
|
|
|
56
65
|
with contextlib.suppress(ValidationError):
|
|
57
66
|
ErrorResponse.model_validate(json_response).raise_error(self.settings.retry_errors)
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
response = Response.model_validate(json_response)
|
|
69
|
+
|
|
70
|
+
self.logger.debug("Received response", extra={"request": request, "response": response})
|
|
71
|
+
self.logger.info(
|
|
72
|
+
"Received response",
|
|
73
|
+
extra={
|
|
74
|
+
"method": request.method,
|
|
75
|
+
"duration": response.time.duration,
|
|
76
|
+
"processing": response.time.processing,
|
|
77
|
+
"operating": response.time.operating,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return response
|
|
60
82
|
|
|
61
83
|
def batch(
|
|
62
84
|
self,
|
|
63
|
-
requests: Iterable[Request],
|
|
85
|
+
requests: Iterable[Request | dict],
|
|
64
86
|
batch_size: int | None = None,
|
|
65
87
|
) -> Generator[ApiTypes, None, None]:
|
|
66
88
|
"""Call infinite sequence of methods within batches and return just `result`s."""
|
|
67
89
|
batch_size = batch_size or self.settings.batch_size
|
|
68
90
|
|
|
69
|
-
|
|
91
|
+
tail_requests = iter(requests)
|
|
92
|
+
while batched_requests := list(islice(tail_requests, batch_size)):
|
|
70
93
|
for response in self._batch(batched_requests):
|
|
71
94
|
yield response.result
|
|
72
95
|
|
|
73
|
-
def _batch(self, requests: Iterable[Request]) -> Generator[
|
|
96
|
+
def _batch(self, requests: Iterable[Request | dict]) -> Generator[Response, None, None]:
|
|
74
97
|
"""Call batch of methods and return full responses."""
|
|
75
|
-
commands = {f"_{i}": request for i, request in enumerate(requests)}
|
|
98
|
+
commands = {f"_{i}": Request.model_validate(request) for i, request in enumerate(requests)}
|
|
76
99
|
request = Request(
|
|
77
100
|
method="batch",
|
|
78
101
|
parameters={
|
|
@@ -83,10 +106,6 @@ class Bitrix24:
|
|
|
83
106
|
|
|
84
107
|
result = self.call(request)
|
|
85
108
|
|
|
86
|
-
for fix_key in ["result_error", "result_total", "result_next"]:
|
|
87
|
-
if isinstance(result[fix_key], list) and not result[fix_key]:
|
|
88
|
-
result[fix_key] = dict(result[fix_key])
|
|
89
|
-
|
|
90
109
|
result = BatchResult.model_validate(result)
|
|
91
110
|
|
|
92
111
|
for i in range(len(commands)):
|
|
@@ -105,7 +124,7 @@ class Bitrix24:
|
|
|
105
124
|
f"Expecting `result_time` to contain result for command {{`{key}`: {command}}}. Got: {result}",
|
|
106
125
|
)
|
|
107
126
|
|
|
108
|
-
yield
|
|
127
|
+
yield Response(
|
|
109
128
|
result=result.result[key],
|
|
110
129
|
time=result.result_time[key],
|
|
111
130
|
total=result.result_total.get(key, None),
|
|
@@ -114,17 +133,21 @@ class Bitrix24:
|
|
|
114
133
|
|
|
115
134
|
def list_sequential(
|
|
116
135
|
self,
|
|
117
|
-
|
|
136
|
+
request: Request | dict,
|
|
118
137
|
list_size: int | None = None,
|
|
119
138
|
) -> Generator[ApiTypes, None, None]:
|
|
120
139
|
"""Call `list` method and return full `result`.
|
|
121
140
|
|
|
122
141
|
Slow (sequential tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
|
|
123
142
|
"""
|
|
143
|
+
request = Request.model_validate(request)
|
|
124
144
|
list_size = list_size or self.settings.list_size
|
|
125
145
|
|
|
126
|
-
|
|
127
|
-
|
|
146
|
+
head_request = request.model_copy(deep=True)
|
|
147
|
+
head_request.parameters["start"] = 0
|
|
148
|
+
|
|
149
|
+
head_response = self._call_retry(head_request)
|
|
150
|
+
yield from self._fix_list_result(head_response.result)
|
|
128
151
|
|
|
129
152
|
if head_response.next and head_response.next != list_size:
|
|
130
153
|
raise ValueError(f"Expecting list chunk size to be {list_size}. Got: {head_response.next}")
|
|
@@ -132,19 +155,18 @@ class Bitrix24:
|
|
|
132
155
|
total = head_response.total or 0
|
|
133
156
|
for start in range(list_size, total, list_size):
|
|
134
157
|
tail_request = head_request.model_copy(deep=True)
|
|
135
|
-
tail_request.parameters
|
|
136
|
-
tail_response = self.
|
|
158
|
+
tail_request.parameters["start"] = start
|
|
159
|
+
tail_response = self._call_retry(tail_request)
|
|
137
160
|
|
|
138
161
|
if tail_response.next and tail_response.next != start + list_size:
|
|
139
162
|
raise ValueError(
|
|
140
163
|
f"Expecting next list chunk to start at {start + list_size}. Got: {tail_response.next}",
|
|
141
164
|
)
|
|
142
|
-
|
|
143
|
-
yield from self._normalize_list(tail_response.result)
|
|
165
|
+
yield from self._fix_list_result(tail_response.result)
|
|
144
166
|
|
|
145
167
|
def list_batched(
|
|
146
168
|
self,
|
|
147
|
-
|
|
169
|
+
request: Request | dict,
|
|
148
170
|
list_size: int | None = None,
|
|
149
171
|
batch_size: int | None = None,
|
|
150
172
|
) -> Generator[ApiTypes, None, None]:
|
|
@@ -152,11 +174,15 @@ class Bitrix24:
|
|
|
152
174
|
|
|
153
175
|
Faster (batched tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
|
|
154
176
|
"""
|
|
177
|
+
request = Request.model_validate(request)
|
|
155
178
|
list_size = list_size or self.settings.list_size
|
|
156
179
|
batch_size = batch_size or self.settings.batch_size
|
|
157
180
|
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
head_request = request.model_copy(deep=True)
|
|
182
|
+
head_request.parameters["start"] = 0
|
|
183
|
+
|
|
184
|
+
head_response = self._call_retry(head_request)
|
|
185
|
+
yield from self._fix_list_result(head_response.result)
|
|
160
186
|
|
|
161
187
|
if head_response.next and head_response.next != list_size:
|
|
162
188
|
raise ValueError(f"Expecting chunk size to be {list_size}. Got: {head_response.next}")
|
|
@@ -165,18 +191,17 @@ class Bitrix24:
|
|
|
165
191
|
total = head_response.total or 0
|
|
166
192
|
for start in range(list_size, total, list_size):
|
|
167
193
|
tail_request = head_request.model_copy(deep=True)
|
|
168
|
-
tail_request.parameters
|
|
194
|
+
tail_request.parameters["start"] = start
|
|
169
195
|
yield tail_request
|
|
170
196
|
|
|
171
197
|
tail_responses = self.batch(_tail_requests(), batch_size)
|
|
172
|
-
tail_responses = map(self.
|
|
198
|
+
tail_responses = map(self._fix_list_result, tail_responses)
|
|
173
199
|
tail_responses = chain.from_iterable(tail_responses)
|
|
174
|
-
|
|
175
200
|
yield from tail_responses
|
|
176
201
|
|
|
177
202
|
def list_batched_no_count(
|
|
178
203
|
self,
|
|
179
|
-
request: ListRequest,
|
|
204
|
+
request: ListRequest | dict,
|
|
180
205
|
id_key: str = "ID",
|
|
181
206
|
list_size: int | None = None,
|
|
182
207
|
batch_size: int | None = None,
|
|
@@ -185,6 +210,7 @@ class Bitrix24:
|
|
|
185
210
|
|
|
186
211
|
Fastest (batched, no count) list gathering for methods with `filter` parameter (e.g. `crm.lead.list`).
|
|
187
212
|
"""
|
|
213
|
+
request = ListRequest.model_validate(request)
|
|
188
214
|
list_size = list_size or self.settings.list_size
|
|
189
215
|
batch_size = batch_size or self.settings.batch_size
|
|
190
216
|
|
|
@@ -192,7 +218,8 @@ class Bitrix24:
|
|
|
192
218
|
if "*" not in select_ and id_key not in select_:
|
|
193
219
|
request.select.append(id_key)
|
|
194
220
|
|
|
195
|
-
id_from, id_to = f"
|
|
221
|
+
id_from, id_to = f">{id_key}", f"<{id_key}"
|
|
222
|
+
get_id = itemgetter(id_key)
|
|
196
223
|
|
|
197
224
|
filter_ = request.parameters.filter
|
|
198
225
|
if filter_ and (id_from in filter_ or id_to in filter_):
|
|
@@ -211,39 +238,94 @@ class Bitrix24:
|
|
|
211
238
|
tail_request.parameters.start = -1
|
|
212
239
|
tail_request.parameters.order = {"ID": "DESC"}
|
|
213
240
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
get_id = itemgetter(id_key)
|
|
218
|
-
max_head = max(map(int, map(get_id, head)), default=None)
|
|
219
|
-
min_tail = min(map(int, map(get_id, tail)), default=None)
|
|
220
|
-
|
|
221
|
-
yield from head
|
|
241
|
+
head_tail_result = self.batch([head_request, tail_request])
|
|
242
|
+
head_result, tail_result = tuple(map(self._fix_list_result, head_tail_result))
|
|
243
|
+
yield from head_result
|
|
222
244
|
|
|
223
|
-
|
|
245
|
+
max_head_id = max(map(int, map(get_id, head_result)), default=None)
|
|
246
|
+
min_tail_id = min(map(int, map(get_id, tail_result)), default=None)
|
|
224
247
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
body_request.parameters.order = {"ID": "ASC"}
|
|
232
|
-
yield body_request
|
|
248
|
+
def _body_requests() -> Generator[ListRequest, None, None]:
|
|
249
|
+
for start in range(max_head_id, min_tail_id, list_size):
|
|
250
|
+
body_request = head_request.model_copy(deep=True)
|
|
251
|
+
body_request.parameters.filter[id_from] = start
|
|
252
|
+
body_request.parameters.filter[id_to] = min(start + list_size + 1, min_tail_id)
|
|
253
|
+
yield body_request
|
|
233
254
|
|
|
255
|
+
if max_head_id and min_tail_id and max_head_id < min_tail_id:
|
|
234
256
|
body = self.batch(_body_requests(), batch_size)
|
|
235
|
-
body = map(self.
|
|
257
|
+
body = map(self._fix_list_result, body)
|
|
236
258
|
body = chain.from_iterable(body)
|
|
237
|
-
|
|
238
259
|
yield from body
|
|
239
260
|
|
|
240
|
-
for item in reversed(
|
|
241
|
-
if int(get_id(item)) >
|
|
261
|
+
for item in reversed(tail_result):
|
|
262
|
+
if int(get_id(item)) > max_head_id:
|
|
242
263
|
yield item
|
|
243
264
|
|
|
265
|
+
def reference_batched_no_count(
|
|
266
|
+
self,
|
|
267
|
+
request: ListRequest | dict,
|
|
268
|
+
updates: Iterable[dict],
|
|
269
|
+
id_key: str = "ID",
|
|
270
|
+
list_size: int | None = None,
|
|
271
|
+
batch_size: int | None = None,
|
|
272
|
+
) -> Generator[ApiTypes, None, None]:
|
|
273
|
+
"""Call `list` method with reference `updates` to filter and return full `result`.
|
|
274
|
+
|
|
275
|
+
Fastest (batched, no count) list gathering for methods with `filter` parameter and required `reference`
|
|
276
|
+
(e.g. `crm.timeline.comment.list`).
|
|
277
|
+
"""
|
|
278
|
+
request = ListRequest.model_validate(request)
|
|
279
|
+
list_size = list_size or self.settings.list_size
|
|
280
|
+
batch_size = batch_size or self.settings.batch_size
|
|
281
|
+
|
|
282
|
+
select_ = request.parameters.select
|
|
283
|
+
if "*" not in select_ and id_key not in select_:
|
|
284
|
+
request.select.append(id_key)
|
|
285
|
+
|
|
286
|
+
id_from = f">{id_key}"
|
|
287
|
+
get_id = itemgetter(id_key)
|
|
288
|
+
|
|
289
|
+
filter_ = request.parameters.filter
|
|
290
|
+
if filter_ and id_from in filter_:
|
|
291
|
+
raise ValueError(
|
|
292
|
+
f"Filter parameters `{id_from}` is reserved in `reference_batched_no_count`",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if request.parameters.order:
|
|
296
|
+
raise ValueError("Ordering parameters are reserved `order`in `reference_batched_no_count`")
|
|
297
|
+
|
|
298
|
+
def _tail_requests() -> Generator[ListRequest, None, None]:
|
|
299
|
+
for update in updates:
|
|
300
|
+
if id_from in update:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"Filter parameters `{id_from}` is reserved in `reference_batched_no_count`",
|
|
303
|
+
)
|
|
304
|
+
tail_request = request.model_copy(deep=True)
|
|
305
|
+
tail_request.parameters.filter |= update
|
|
306
|
+
tail_request.parameters.start = -1
|
|
307
|
+
tail_request.parameters.order = {"ID": "ASC"}
|
|
308
|
+
yield tail_request
|
|
309
|
+
|
|
310
|
+
head_requests = []
|
|
311
|
+
tail_requests = iter(_tail_requests())
|
|
312
|
+
while body_requests := head_requests + list(islice(tail_requests, batch_size - len(head_requests))):
|
|
313
|
+
body_results = self.batch(body_requests, batch_size)
|
|
314
|
+
body_results = map(self._fix_list_result, body_results)
|
|
315
|
+
|
|
316
|
+
head_requests = []
|
|
317
|
+
for body_request, body_result in zip(body_requests, body_results, strict=True):
|
|
318
|
+
if len(body_result) == list_size:
|
|
319
|
+
max_id = max(map(int, map(get_id, body_result)), default=None)
|
|
320
|
+
head_request = body_request.model_copy(deep=True)
|
|
321
|
+
head_request.parameters.filter[id_from] = max_id
|
|
322
|
+
head_requests.append(head_request)
|
|
323
|
+
|
|
324
|
+
yield from body_result
|
|
325
|
+
|
|
244
326
|
@staticmethod
|
|
245
|
-
def
|
|
246
|
-
"""
|
|
327
|
+
def _fix_list_result(result: list | dict[str, list]) -> list:
|
|
328
|
+
"""Fix `list` method result to `list of items` structure.
|
|
247
329
|
|
|
248
330
|
There are two kinds of what `list` method `result` may contain:
|
|
249
331
|
- a list of items (e.g. `department-get` and `disk.folder.getchildren`),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from typing import Self
|
|
2
|
+
from typing import Annotated, Any, Self
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel
|
|
4
|
+
from pydantic import BaseModel, BeforeValidator
|
|
5
5
|
|
|
6
6
|
from b24api.error import ApiResponseError, RetryApiResponseError
|
|
7
7
|
from b24api.query import build_query
|
|
@@ -9,7 +9,7 @@ from b24api.type import ApiTypes
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Request(BaseModel):
|
|
12
|
-
"""
|
|
12
|
+
"""API request."""
|
|
13
13
|
|
|
14
14
|
method: str
|
|
15
15
|
parameters: dict[str, ApiTypes] = {}
|
|
@@ -30,24 +30,23 @@ class Request(BaseModel):
|
|
|
30
30
|
class ListRequestParameters(BaseModel):
|
|
31
31
|
"""Parameters of `*.list` requests."""
|
|
32
32
|
|
|
33
|
-
select: list[str]
|
|
34
|
-
filter: dict[str, ApiTypes]
|
|
35
|
-
order: dict[str, str]
|
|
36
|
-
limit: int | None = None
|
|
33
|
+
select: list[str] = []
|
|
34
|
+
filter: dict[str, ApiTypes] = {}
|
|
35
|
+
order: dict[str, str] = {}
|
|
37
36
|
start: int | None = None
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
class ListRequest(Request):
|
|
41
|
-
"""
|
|
40
|
+
"""API `*.list` request."""
|
|
42
41
|
|
|
43
|
-
parameters: ListRequestParameters
|
|
42
|
+
parameters: ListRequestParameters
|
|
44
43
|
|
|
45
44
|
|
|
46
45
|
class ErrorResponse(BaseModel):
|
|
47
46
|
"""API error response."""
|
|
48
47
|
|
|
49
48
|
error: str
|
|
50
|
-
error_description: str
|
|
49
|
+
error_description: str
|
|
51
50
|
|
|
52
51
|
def raise_error(self, retry_errors: list[str]) -> Self:
|
|
53
52
|
if self.error in retry_errors:
|
|
@@ -62,7 +61,7 @@ class ErrorResponse(BaseModel):
|
|
|
62
61
|
|
|
63
62
|
|
|
64
63
|
class ResponseTime(BaseModel):
|
|
65
|
-
"""
|
|
64
|
+
"""API Response `time` structure."""
|
|
66
65
|
|
|
67
66
|
start: float
|
|
68
67
|
finish: float
|
|
@@ -70,12 +69,12 @@ class ResponseTime(BaseModel):
|
|
|
70
69
|
processing: float
|
|
71
70
|
date_start: datetime
|
|
72
71
|
date_finish: datetime
|
|
73
|
-
operating_reset_at: float
|
|
74
|
-
operating: float
|
|
72
|
+
operating_reset_at: float | None = None
|
|
73
|
+
operating: float | None = None
|
|
75
74
|
|
|
76
75
|
|
|
77
|
-
class
|
|
78
|
-
"""API
|
|
76
|
+
class Response(BaseModel):
|
|
77
|
+
"""API response."""
|
|
79
78
|
|
|
80
79
|
result: ApiTypes
|
|
81
80
|
time: ResponseTime
|
|
@@ -83,9 +82,17 @@ class ResultResponse(BaseModel):
|
|
|
83
82
|
next: int | None = None
|
|
84
83
|
|
|
85
84
|
|
|
85
|
+
def _php_dict(value: Any) -> Any: # noqa: ANN401
|
|
86
|
+
if isinstance(value, list) and not value:
|
|
87
|
+
return {}
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
86
91
|
class BatchResult(BaseModel):
|
|
87
|
-
result
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
"""API response `result` structure for `batch` method."""
|
|
93
|
+
|
|
94
|
+
result: Annotated[dict[str, ApiTypes], BeforeValidator(_php_dict)]
|
|
95
|
+
result_time: Annotated[dict[str, ResponseTime], BeforeValidator(_php_dict)]
|
|
96
|
+
result_error: Annotated[dict[str, ErrorResponse], BeforeValidator(_php_dict)]
|
|
97
|
+
result_total: Annotated[dict[str, int], BeforeValidator(_php_dict)]
|
|
98
|
+
result_next: Annotated[dict[str, int], BeforeValidator(_php_dict)]
|
|
@@ -14,7 +14,12 @@ class ApiResponseError(Exception):
|
|
|
14
14
|
code: str,
|
|
15
15
|
description: str | None,
|
|
16
16
|
) -> None:
|
|
17
|
-
|
|
17
|
+
if code and description:
|
|
18
|
+
message = f"API error [{code}]: {description}"
|
|
19
|
+
elif code:
|
|
20
|
+
message = f"API error [{code}]"
|
|
21
|
+
else:
|
|
22
|
+
message = f"API error: {description}"
|
|
18
23
|
super().__init__(message)
|
|
19
24
|
|
|
20
25
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from collections.abc import Generator
|
|
2
3
|
from typing import Annotated
|
|
3
4
|
|
|
@@ -6,7 +7,11 @@ from httpx import Client
|
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def httpx_client() -> Generator[Client, None, None]:
|
|
9
|
-
|
|
10
|
+
httpx_logger = logging.getLogger("httpx")
|
|
11
|
+
httpx_logger.setLevel(logging.WARNING)
|
|
12
|
+
|
|
13
|
+
client = Client(http2=True, timeout=30)
|
|
14
|
+
|
|
10
15
|
yield client
|
|
11
16
|
|
|
12
17
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from urllib.parse import quote_plus
|
|
3
|
+
|
|
4
|
+
from b24api.type import ApiTypes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_query(parameters: dict[int | str, ApiTypes], path: str = "%s") -> str:
|
|
8
|
+
query = []
|
|
9
|
+
|
|
10
|
+
if parameters is None:
|
|
11
|
+
return ""
|
|
12
|
+
|
|
13
|
+
for key, value in parameters.items():
|
|
14
|
+
if value is None:
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
value_ = value
|
|
18
|
+
|
|
19
|
+
if isinstance(value_, list | tuple):
|
|
20
|
+
value_ = dict(enumerate(value))
|
|
21
|
+
|
|
22
|
+
if isinstance(value_, dict):
|
|
23
|
+
subquery = build_query(value_, path % key + "[%s]")
|
|
24
|
+
else:
|
|
25
|
+
key_ = quote_plus(path % key)
|
|
26
|
+
|
|
27
|
+
if isinstance(value_, datetime):
|
|
28
|
+
value_ = value.astimezone().isoformat()
|
|
29
|
+
value_ = quote_plus(str(value_))
|
|
30
|
+
|
|
31
|
+
subquery = f"{key_}={value_}"
|
|
32
|
+
|
|
33
|
+
if subquery:
|
|
34
|
+
query.append(subquery)
|
|
35
|
+
|
|
36
|
+
return "&".join(query)
|
|
@@ -9,7 +9,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
9
9
|
|
|
10
10
|
class Settings(BaseSettings):
|
|
11
11
|
model_config = SettingsConfigDict(
|
|
12
|
-
env_prefix="
|
|
12
|
+
env_prefix="bitrix24_api_",
|
|
13
13
|
env_file=".env",
|
|
14
14
|
extra="ignore",
|
|
15
15
|
)
|
|
@@ -35,8 +35,8 @@ class Settings(BaseSettings):
|
|
|
35
35
|
batch_size: int = 50
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def api_settings() -> Generator[Settings, None, None]:
|
|
39
|
-
settings = Settings()
|
|
38
|
+
def api_settings(**kwargs: dict) -> Generator[Settings, None, None]:
|
|
39
|
+
settings = Settings(**kwargs)
|
|
40
40
|
yield settings
|
|
41
41
|
|
|
42
42
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: b24api
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Bitrix24 API
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fast-depends>=2.4.12
|
|
9
|
+
Requires-Dist: httpx[http2]>=0.28.1
|
|
10
|
+
Requires-Dist: pydantic>=2.10.6
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.8.1
|
|
12
|
+
Requires-Dist: retry>=0.9.2
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# API client for Bitrix24
|
|
16
|
+
|
|
17
|
+
Low-level API client with multiple strategies for lists gathering.
|
|
18
|
+
All methods support retries.
|
|
19
|
+
|
|
20
|
+
## Regular call (any method)
|
|
21
|
+
```python
|
|
22
|
+
from b24api import Bitrix24
|
|
23
|
+
|
|
24
|
+
b24 = Bitrix24()
|
|
25
|
+
result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
|
|
26
|
+
print(result)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Batch call (any method, no limit)
|
|
30
|
+
```python
|
|
31
|
+
from b24api import Bitrix24
|
|
32
|
+
|
|
33
|
+
b24 = Bitrix24()
|
|
34
|
+
|
|
35
|
+
requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
|
|
36
|
+
result = b24.batch(requests)
|
|
37
|
+
print(result)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Gathering full list (slow)
|
|
41
|
+
Applicable to list methods with `start=<offset>` support.
|
|
42
|
+
Fetches list chunks one by one.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from b24api import Bitrix24
|
|
46
|
+
|
|
47
|
+
b24 = Bitrix24()
|
|
48
|
+
result = b24.list_sequential({"method": "user.get"})
|
|
49
|
+
print(list(result))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Gathering full list (faster)
|
|
53
|
+
Applicable to list methods with `start=<offset>` support.
|
|
54
|
+
Fetches first list chunk with regular call, then fetches other chunks with `batch`.
|
|
55
|
+
Approximately 3 times faster then `list_sequential`.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from b24api import Bitrix24
|
|
59
|
+
|
|
60
|
+
b24 = Bitrix24()
|
|
61
|
+
result = b24.list_batched({"method": "user.get"})
|
|
62
|
+
print(list(result))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Gathering full list (fastest)
|
|
66
|
+
Applicable to list methods with `filter={<parameters>}` support.
|
|
67
|
+
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
68
|
+
Approximately 2 times faster then `list_batched`.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from b24api import Bitrix24
|
|
72
|
+
|
|
73
|
+
b24 = Bitrix24()
|
|
74
|
+
result = b24.list_batched_no_count({"method": "user.get"})
|
|
75
|
+
print(list(result))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Gathering full list with required reference
|
|
79
|
+
Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}` support and required filter parameters.
|
|
80
|
+
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from b24api import Bitrix24
|
|
84
|
+
|
|
85
|
+
b24 = Bitrix24()
|
|
86
|
+
deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
|
|
87
|
+
filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
|
|
88
|
+
result = b24.reference_batched_no_count(
|
|
89
|
+
{"method": "crm.timeline.comment.list", "parameters": {"ENTITY_TYPE": "deal"}},
|
|
90
|
+
filter_updates,
|
|
91
|
+
)
|
|
92
|
+
print(list(result))
|
|
93
|
+
```
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "b24api"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.3.0"
|
|
9
9
|
description = "Bitrix24 API"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.11"
|
|
@@ -19,11 +19,9 @@ dependencies = [
|
|
|
19
19
|
|
|
20
20
|
[dependency-groups]
|
|
21
21
|
dev = [
|
|
22
|
-
"mypy>=1.15.0",
|
|
23
22
|
"pytest-env>=1.1.5",
|
|
24
23
|
"pytest-httpx>=0.35.0",
|
|
25
24
|
"pytest-mock>=3.14.0",
|
|
26
|
-
"ruff>=0.9.9",
|
|
27
25
|
]
|
|
28
26
|
|
|
29
27
|
[tool.ruff]
|
|
@@ -59,7 +57,7 @@ addopts = ["--strict-config", "--strict-markers"]
|
|
|
59
57
|
xfail_strict = true
|
|
60
58
|
|
|
61
59
|
[tool.pytest_env]
|
|
62
|
-
|
|
60
|
+
BITRIX24_API_WEBHOOK_URL = "https://bitrix24.com/rest/0/test/"
|
|
63
61
|
|
|
64
62
|
|
|
65
63
|
[tool.coverage.report]
|
b24api-0.1.3/PKG-INFO
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: b24api
|
|
3
|
-
Version: 0.1.3
|
|
4
|
-
Summary: Bitrix24 API
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Dist: fast-depends>=2.4.12
|
|
9
|
-
Requires-Dist: httpx[http2]>=0.28.1
|
|
10
|
-
Requires-Dist: pydantic>=2.10.6
|
|
11
|
-
Requires-Dist: pydantic-settings>=2.8.1
|
|
12
|
-
Requires-Dist: retry>=0.9.2
|
|
13
|
-
|
|
14
|
-
# API client for Bitrix24
|
|
15
|
-
|
|
16
|
-
Low-level API client with multiple strategies for lists gathering.
|
b24api-0.1.3/README.md
DELETED
b24api-0.1.3/b24api/future.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
try:
|
|
2
|
-
from itertools import batched # python 3.12+
|
|
3
|
-
except ImportError:
|
|
4
|
-
from collections.abc import Generator, Iterable
|
|
5
|
-
from itertools import islice
|
|
6
|
-
|
|
7
|
-
def batched(iterable: Iterable, n: int) -> Generator[tuple, None, None]:
|
|
8
|
-
if n < 1:
|
|
9
|
-
raise ValueError("n must be at least one")
|
|
10
|
-
iterator = iter(iterable)
|
|
11
|
-
while batch := tuple(islice(iterator, n)):
|
|
12
|
-
yield batch
|
b24api-0.1.3/b24api/query.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from urllib.parse import quote_plus
|
|
3
|
-
|
|
4
|
-
from b24api.type import ApiTypes
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def build_query(parameters: dict[int | str, ApiTypes], convention: str = "%s") -> str:
|
|
8
|
-
query = []
|
|
9
|
-
|
|
10
|
-
if parameters is None:
|
|
11
|
-
return ""
|
|
12
|
-
|
|
13
|
-
for key, value in parameters.items():
|
|
14
|
-
if value is None:
|
|
15
|
-
continue
|
|
16
|
-
|
|
17
|
-
if isinstance(value, list | tuple):
|
|
18
|
-
value = dict(enumerate(value)) # noqa: PLW2901
|
|
19
|
-
|
|
20
|
-
if isinstance(value, dict):
|
|
21
|
-
subquery = build_query(value, convention % key + "[%s]")
|
|
22
|
-
else:
|
|
23
|
-
key_ = quote_plus(convention % key)
|
|
24
|
-
value_ = value.isoformat() if isinstance(value, datetime) else value
|
|
25
|
-
# TODO: check date filtering [with .replace(microsecond=0).astimezone()]
|
|
26
|
-
value_ = quote_plus(str(value_))
|
|
27
|
-
subquery = f"{key_}={value_}"
|
|
28
|
-
|
|
29
|
-
query.append(subquery)
|
|
30
|
-
|
|
31
|
-
return "&".join(query)
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.2
|
|
2
|
-
Name: b24api
|
|
3
|
-
Version: 0.1.3
|
|
4
|
-
Summary: Bitrix24 API
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Dist: fast-depends>=2.4.12
|
|
9
|
-
Requires-Dist: httpx[http2]>=0.28.1
|
|
10
|
-
Requires-Dist: pydantic>=2.10.6
|
|
11
|
-
Requires-Dist: pydantic-settings>=2.8.1
|
|
12
|
-
Requires-Dist: retry>=0.9.2
|
|
13
|
-
|
|
14
|
-
# API client for Bitrix24
|
|
15
|
-
|
|
16
|
-
Low-level API client with multiple strategies for lists gathering.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|