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