b24api 0.4.1__tar.gz → 0.6.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.
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b24api
3
- Version: 0.4.1
3
+ Version: 0.6.0
4
4
  Summary: Bitrix24 API
5
- Requires-Python: >=3.10
5
+ Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
+ Requires-Dist: aioitertools>=0.12.0
8
9
  Requires-Dist: fast-depends>=2.4.12
9
10
  Requires-Dist: httpx[http2]>=0.28.1
10
- Requires-Dist: pydantic>=2.10.6
11
- Requires-Dist: pydantic-settings>=2.8.1
11
+ Requires-Dist: pydantic>=2.11.7
12
+ Requires-Dist: pydantic-settings>=2.10.1
13
+ Requires-Dist: pytest-asyncio>=1.1.0
12
14
  Requires-Dist: tenacity>=9.1.2
13
15
  Dynamic: license-file
14
16
 
@@ -18,19 +20,31 @@ Low-level API client with multiple strategies for lists gathering.
18
20
  All methods support retries.
19
21
 
20
22
  ## Regular call (any method)
23
+
21
24
  ```python
22
- from b24api import Bitrix24
25
+ from b24api import SyncBitrix24
23
26
 
24
- b24 = Bitrix24()
27
+ b24 = SyncBitrix24()
25
28
  result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
26
29
  print(result)
27
30
  ```
28
31
 
32
+ or the same with async
33
+
34
+ ```python
35
+ from b24api import AsyncBitrix24
36
+
37
+ b24 = AsyncBitrix24()
38
+ result = await b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
39
+ print(result)
40
+ ```
41
+
29
42
  ## Batch call (any method, no limit)
43
+
30
44
  ```python
31
- from b24api import Bitrix24
45
+ from b24api import SyncBitrix24
32
46
 
33
- b24 = Bitrix24()
47
+ b24 = SyncBitrix24()
34
48
 
35
49
  requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
36
50
  result = b24.batch(requests)
@@ -42,9 +56,9 @@ Applicable to list methods with `start=<offset>` support.
42
56
  Fetches list chunks one by one.
43
57
 
44
58
  ```python
45
- from b24api import Bitrix24
59
+ from b24api import SyncBitrix24
46
60
 
47
- b24 = Bitrix24()
61
+ b24 = SyncBitrix24()
48
62
  result = b24.list_sequential({"method": "user.get"})
49
63
  print(list(result))
50
64
  ```
@@ -52,12 +66,12 @@ print(list(result))
52
66
  ## Gathering full list (faster)
53
67
  Applicable to list methods with `start=<offset>` support.
54
68
  Fetches first list chunk with regular call, then fetches other chunks with `batch`.
55
- Approximately 3 times faster then `list_sequential`.
69
+ Approximately 3 times faster then `list_sequential`.
56
70
 
57
71
  ```python
58
- from b24api import Bitrix24
72
+ from b24api import SyncBitrix24
59
73
 
60
- b24 = Bitrix24()
74
+ b24 = SyncBitrix24()
61
75
  result = b24.list_batched({"method": "user.get"})
62
76
  print(list(result))
63
77
  ```
@@ -65,12 +79,12 @@ print(list(result))
65
79
  ## Gathering full list (fastest)
66
80
  Applicable to list methods with `filter={<parameters>}` support.
67
81
  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`.
82
+ Approximately 2 times faster then `list_batched`.
69
83
 
70
84
  ```python
71
- from b24api import Bitrix24
85
+ from b24api import SyncBitrix24
72
86
 
73
- b24 = Bitrix24()
87
+ b24 = SyncBitrix24()
74
88
  result = b24.list_batched_no_count({"method": "user.get"})
75
89
  print(list(result))
76
90
  ```
@@ -80,9 +94,9 @@ Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}`
80
94
  Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
81
95
 
82
96
  ```python
83
- from b24api import Bitrix24
97
+ from b24api import SyncBitrix24
84
98
 
85
- b24 = Bitrix24()
99
+ b24 = SyncBitrix24()
86
100
  deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
87
101
  filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
88
102
  result = b24.reference_batched_no_count(
@@ -4,19 +4,31 @@ Low-level API client with multiple strategies for lists gathering.
4
4
  All methods support retries.
5
5
 
6
6
  ## Regular call (any method)
7
+
7
8
  ```python
8
- from b24api import Bitrix24
9
+ from b24api import SyncBitrix24
9
10
 
10
- b24 = Bitrix24()
11
+ b24 = SyncBitrix24()
11
12
  result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
12
13
  print(result)
13
14
  ```
14
15
 
16
+ or the same with async
17
+
18
+ ```python
19
+ from b24api import AsyncBitrix24
20
+
21
+ b24 = AsyncBitrix24()
22
+ result = await b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
23
+ print(result)
24
+ ```
25
+
15
26
  ## Batch call (any method, no limit)
27
+
16
28
  ```python
17
- from b24api import Bitrix24
29
+ from b24api import SyncBitrix24
18
30
 
19
- b24 = Bitrix24()
31
+ b24 = SyncBitrix24()
20
32
 
21
33
  requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
22
34
  result = b24.batch(requests)
@@ -28,9 +40,9 @@ Applicable to list methods with `start=<offset>` support.
28
40
  Fetches list chunks one by one.
29
41
 
30
42
  ```python
31
- from b24api import Bitrix24
43
+ from b24api import SyncBitrix24
32
44
 
33
- b24 = Bitrix24()
45
+ b24 = SyncBitrix24()
34
46
  result = b24.list_sequential({"method": "user.get"})
35
47
  print(list(result))
36
48
  ```
@@ -38,12 +50,12 @@ print(list(result))
38
50
  ## Gathering full list (faster)
39
51
  Applicable to list methods with `start=<offset>` support.
40
52
  Fetches first list chunk with regular call, then fetches other chunks with `batch`.
41
- Approximately 3 times faster then `list_sequential`.
53
+ Approximately 3 times faster then `list_sequential`.
42
54
 
43
55
  ```python
44
- from b24api import Bitrix24
56
+ from b24api import SyncBitrix24
45
57
 
46
- b24 = Bitrix24()
58
+ b24 = SyncBitrix24()
47
59
  result = b24.list_batched({"method": "user.get"})
48
60
  print(list(result))
49
61
  ```
@@ -51,12 +63,12 @@ print(list(result))
51
63
  ## Gathering full list (fastest)
52
64
  Applicable to list methods with `filter={<parameters>}` support.
53
65
  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`.
66
+ Approximately 2 times faster then `list_batched`.
55
67
 
56
68
  ```python
57
- from b24api import Bitrix24
69
+ from b24api import SyncBitrix24
58
70
 
59
- b24 = Bitrix24()
71
+ b24 = SyncBitrix24()
60
72
  result = b24.list_batched_no_count({"method": "user.get"})
61
73
  print(list(result))
62
74
  ```
@@ -66,9 +78,9 @@ Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}`
66
78
  Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
67
79
 
68
80
  ```python
69
- from b24api import Bitrix24
81
+ from b24api import SyncBitrix24
70
82
 
71
- b24 = Bitrix24()
83
+ b24 = SyncBitrix24()
72
84
  deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
73
85
  filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
74
86
  result = b24.reference_batched_no_count(
@@ -0,0 +1,4 @@
1
+ from b24api.async_api import AsyncBitrix24
2
+ from b24api.sync_api import SyncBitrix24
3
+
4
+ __all__ = ["AsyncBitrix24", "SyncBitrix24"]
@@ -0,0 +1,262 @@
1
+ from collections.abc import AsyncGenerator, AsyncIterable, Iterable
2
+ from itertools import batched, islice
3
+ from typing import Any
4
+
5
+ from aioitertools.itertools import chain as achain
6
+ from httpx import AsyncClient
7
+ from tenacity import AsyncRetrying
8
+
9
+ from b24api.base_api import BaseBitrix24, BatchedNoCountHelper, ReferenceNoCountHelper
10
+ from b24api.entity import ApiTypes, ListRequest, Request, Response
11
+
12
+
13
+ class AsyncBitrix24(BaseBitrix24):
14
+ @staticmethod
15
+ def _http() -> type[AsyncClient]:
16
+ return AsyncClient
17
+
18
+ @staticmethod
19
+ def _retry() -> type[AsyncRetrying]:
20
+ return AsyncRetrying
21
+
22
+ async def _call(self, request: Request | dict) -> Response:
23
+ """Call any method and return full response."""
24
+ request = Request.model_validate(request)
25
+ self.logger.debug("Sending request: %s", request)
26
+
27
+ response = await self.http.post(
28
+ f"{self.settings.webhook_url}{request.method}",
29
+ headers={"Content-Type": "application/json"},
30
+ json=request.model_dump(mode="json")["parameters"],
31
+ )
32
+ response = self._validate_call_response(request, response)
33
+ self.logger.debug("Received response: %s", response)
34
+
35
+ return response
36
+
37
+ async def call(self, request: Request | dict) -> ApiTypes:
38
+ """Call any method (with retries) and return `result` from response."""
39
+ return (await self.retry(self._call, request)).result
40
+
41
+ async def _batch(self, requests: tuple[Request | dict, ...]) -> list[Response]:
42
+ """Call limited batch of methods and return full responses."""
43
+ commands, request = self._batch_requests(requests)
44
+
45
+ response = await self._call(request)
46
+
47
+ return self._batch_responses(commands, response)
48
+
49
+ async def batch(
50
+ self,
51
+ requests: Iterable[Request | dict | tuple[Request | dict, Any]],
52
+ *,
53
+ batch_size: int | None = None,
54
+ list_method: bool = False,
55
+ with_payload: bool = False,
56
+ ) -> AsyncGenerator[ApiTypes | tuple[ApiTypes, Any]]:
57
+ """Call unlimited sequence of methods within batches and return `result` from responses."""
58
+ batch_size = batch_size or self.settings.batch_size
59
+
60
+ for batched_requests in batched(requests, batch_size):
61
+ if with_payload:
62
+ batched_requests, batched_payloads = zip(*batched_requests, strict=True)
63
+ else:
64
+ batched_payloads = None
65
+
66
+ batched_responses = await self.retry(self._batch, batched_requests)
67
+
68
+ for i, response in enumerate(batched_responses):
69
+ result = response.list_result if list_method else response.result
70
+ if with_payload:
71
+ yield result, batched_payloads[i]
72
+ else:
73
+ yield result
74
+
75
+ async def list_sequential(
76
+ self,
77
+ request: Request | dict,
78
+ *,
79
+ list_size: int | None = None,
80
+ ) -> AsyncGenerator[ApiTypes]:
81
+ """Call `list` method and return full `result`.
82
+
83
+ Slow (sequential tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
84
+ """
85
+ request = Request.model_validate(request)
86
+ list_size = list_size or self.settings.list_size
87
+
88
+ head_request = request.model_copy(deep=True)
89
+ head_request.parameters["start"] = 0
90
+
91
+ head_response = await self.retry(self._call, head_request)
92
+ for item in head_response.list_result:
93
+ yield item
94
+
95
+ tail_requests = self._list_tail_requests(head_request, head_response, list_size=list_size)
96
+ for tail_request in tail_requests:
97
+ tail_response = await self.retry(self._call, tail_request)
98
+
99
+ start = tail_request.parameters["start"]
100
+ if tail_response.next and tail_response.next != start + list_size:
101
+ raise ValueError(
102
+ f"Expecting next list chunk to start at {start + list_size}. Got: {tail_response.next}",
103
+ )
104
+
105
+ for item in tail_response.list_result:
106
+ yield item
107
+
108
+ async def list_batched(
109
+ self,
110
+ request: Request | dict,
111
+ *,
112
+ list_size: int | None = None,
113
+ batch_size: int | None = None,
114
+ ) -> AsyncGenerator[ApiTypes]:
115
+ """Call `list` method and return full `result`.
116
+
117
+ Faster (batched tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
118
+ """
119
+ request = Request.model_validate(request)
120
+ list_size = list_size or self.settings.list_size
121
+ batch_size = batch_size or self.settings.batch_size
122
+
123
+ head_request = request.model_copy(deep=True)
124
+ head_request.parameters["start"] = 0
125
+
126
+ head_response = await self.retry(self._call, head_request)
127
+ for item in head_response.list_result:
128
+ yield item
129
+
130
+ tail_requests = self._list_tail_requests(head_request, head_response, list_size=list_size)
131
+ tail_responses = self.batch(tail_requests, batch_size=batch_size, list_method=True)
132
+ async for tail_response in tail_responses:
133
+ for item in tail_response:
134
+ yield item
135
+
136
+ async def list_batched_no_count(
137
+ self,
138
+ request: ListRequest | dict,
139
+ *,
140
+ id_key: str = "ID",
141
+ list_size: int | None = None,
142
+ batch_size: int | None = None,
143
+ ) -> AsyncGenerator[ApiTypes]:
144
+ """Call `list` method and return full `result`.
145
+
146
+ Fastest (batched, no count) list gathering for methods with `filter` parameter (e.g. `crm.lead.list`).
147
+ """
148
+ list_size = list_size or self.settings.list_size
149
+ batch_size = batch_size or self.settings.batch_size
150
+
151
+ batched_helper = BatchedNoCountHelper(request, id_key, list_size, batch_size)
152
+
153
+ boundary_requests = batched_helper.head_request(), batched_helper.tail_request()
154
+ head_result, tail_result = await self.retry(self._batch, boundary_requests)
155
+ for item in head_result.list_result:
156
+ yield item
157
+
158
+ body_requests = batched_helper.body_requests(head_result, tail_result)
159
+ body_results = self.batch(body_requests, batch_size=batch_size, list_method=True)
160
+ async for item in achain.from_iterable(body_results):
161
+ yield item
162
+
163
+ for item in batched_helper.tail_results(head_result, tail_result):
164
+ yield item
165
+
166
+ async def reference_batched_no_count( # noqa: PLR0913
167
+ self,
168
+ request: ListRequest | dict,
169
+ updates: Iterable[dict | tuple[dict, Any]] | AsyncIterable[dict | tuple[dict, Any]],
170
+ *,
171
+ id_key: str = "ID",
172
+ list_size: int | None = None,
173
+ batch_size: int | None = None,
174
+ with_payload: bool = False,
175
+ ) -> AsyncGenerator[ApiTypes | tuple[ApiTypes, Any]]:
176
+ """Call `list` method with reference `updates` for `filter` and return full `result`.
177
+
178
+ Fastest (batched, no count) list gathering for methods with `filter` parameter and required `reference`
179
+ (e.g. `crm.timeline.comment.list`).
180
+ """
181
+ list_size = list_size or self.settings.list_size
182
+ batch_size = batch_size or self.settings.batch_size
183
+
184
+ reference_helper = ReferenceNoCountHelper(request, updates, id_key, list_size, batch_size, with_payload)
185
+
186
+ if isinstance(updates, AsyncIterable):
187
+ async for item in self._reference_batched_no_count_async_updates(reference_helper):
188
+ yield item
189
+ else:
190
+ async for item in self._reference_batched_no_count_sync_updates(reference_helper):
191
+ yield item
192
+
193
+ async def _reference_batched_no_count_async_updates(
194
+ self,
195
+ reference_helper: ReferenceNoCountHelper,
196
+ ) -> AsyncGenerator[ApiTypes | tuple[ApiTypes, Any]]:
197
+ head_requests, body_requests = (), []
198
+ async for tail_request in reference_helper.atail_requests():
199
+ body_requests.append(tail_request)
200
+ if len(head_requests) + len(body_requests) < reference_helper.batch_size:
201
+ continue
202
+
203
+ body_results, head_requests = await self._reference_batched_no_count_async_updates_batch(
204
+ reference_helper,
205
+ body_requests,
206
+ head_requests,
207
+ )
208
+ body_requests = []
209
+ for item in reference_helper.body_results(body_results):
210
+ yield item
211
+
212
+ while head_requests or body_requests:
213
+ body_results, head_requests = await self._reference_batched_no_count_async_updates_batch(
214
+ reference_helper,
215
+ body_requests,
216
+ head_requests,
217
+ )
218
+ body_requests = []
219
+ for item in reference_helper.body_results(body_results):
220
+ yield item
221
+
222
+ async def _reference_batched_no_count_async_updates_batch(
223
+ self,
224
+ reference_helper: ReferenceNoCountHelper,
225
+ body_requests: list[ListRequest],
226
+ head_requests: tuple[ListRequest, ...],
227
+ ) -> tuple[list[ApiTypes], tuple[ListRequest, ...]]:
228
+ body_requests = head_requests + tuple(body_requests)
229
+ body_results = [
230
+ r
231
+ async for r in self.batch(
232
+ body_requests,
233
+ batch_size=reference_helper.batch_size,
234
+ list_method=True,
235
+ with_payload=reference_helper.with_payload,
236
+ )
237
+ ]
238
+ head_requests = reference_helper.head_requests(body_requests, body_results)
239
+
240
+ return body_results, head_requests
241
+
242
+ async def _reference_batched_no_count_sync_updates(
243
+ self,
244
+ reference_helper: ReferenceNoCountHelper,
245
+ ) -> AsyncGenerator[ApiTypes | tuple[ApiTypes, Any]]:
246
+ head_requests = ()
247
+ tail_requests = iter(reference_helper.tail_requests())
248
+ while body_requests := head_requests + tuple(
249
+ islice(tail_requests, reference_helper.batch_size - len(head_requests)),
250
+ ):
251
+ body_results = [
252
+ r
253
+ async for r in self.batch(
254
+ body_requests,
255
+ batch_size=reference_helper.batch_size,
256
+ list_method=True,
257
+ with_payload=reference_helper.with_payload,
258
+ )
259
+ ]
260
+ head_requests = reference_helper.head_requests(body_requests, body_results)
261
+ for item in reference_helper.body_results(body_results):
262
+ yield item