b24api 0.1.3__tar.gz → 0.2.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.2.0/PKG-INFO ADDED
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.2
2
+ Name: b24api
3
+ Version: 0.2.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
+
14
+ # API client for Bitrix24
15
+
16
+ Low-level API client with multiple strategies for lists gathering.
17
+ All methods support retries.
18
+
19
+ ## Regular call (any method)
20
+ ```python
21
+ from b24api import Bitrix24
22
+
23
+ b24 = Bitrix24()
24
+ result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
25
+ print(result)
26
+ ```
27
+
28
+ ## Batch call (any method, no limit)
29
+ ```python
30
+ from b24api import Bitrix24
31
+
32
+ b24 = Bitrix24()
33
+
34
+ requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
35
+ result = b24.batch(requests)
36
+ print(result)
37
+ ```
38
+
39
+ ## Gathering full list (slow)
40
+ Applicable to list methods with `start=<offset>` support.
41
+ Fetches list chunks one by one.
42
+
43
+ ```python
44
+ from b24api import Bitrix24
45
+
46
+ b24 = Bitrix24()
47
+ result = b24.list_sequential({"method": "user.get"})
48
+ print(list(result))
49
+ ```
50
+
51
+ ## Gathering full list (faster)
52
+ Applicable to list methods with `start=<offset>` support.
53
+ Fetches first list chunk with regular call, then fetches other chunks with `batch`.
54
+
55
+ ```python
56
+ from b24api import Bitrix24
57
+
58
+ b24 = Bitrix24()
59
+ result = b24.list_batched({"method": "user.get"})
60
+ print(list(result))
61
+ ```
62
+
63
+ ## Gathering full list (fastest)
64
+ Applicable to list methods with `filter={<parameters>}` support.
65
+ Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
66
+
67
+ ```python
68
+ from b24api import Bitrix24
69
+
70
+ b24 = Bitrix24()
71
+ result = b24.list_batched_no_count({"method": "user.get"})
72
+ print(list(result))
73
+ ```
74
+
75
+ ## Gathering full list with required reference
76
+ Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}` support and required filter parameters.
77
+ Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
78
+
79
+ ```python
80
+ from b24api import Bitrix24
81
+
82
+ b24 = Bitrix24()
83
+ deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
84
+ filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
85
+ result = b24.reference_batched_no_count(
86
+ {"method": "crm.timeline.comment.list", "parameters": {"ENTITY_TYPE": "deal"}},
87
+ filter_updates,
88
+ )
89
+ print(list(result))
90
+ ```
b24api-0.2.0/README.md ADDED
@@ -0,0 +1,77 @@
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
+
42
+ ```python
43
+ from b24api import Bitrix24
44
+
45
+ b24 = Bitrix24()
46
+ result = b24.list_batched({"method": "user.get"})
47
+ print(list(result))
48
+ ```
49
+
50
+ ## Gathering full list (fastest)
51
+ Applicable to list methods with `filter={<parameters>}` support.
52
+ Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
53
+
54
+ ```python
55
+ from b24api import Bitrix24
56
+
57
+ b24 = Bitrix24()
58
+ result = b24.list_batched_no_count({"method": "user.get"})
59
+ print(list(result))
60
+ ```
61
+
62
+ ## Gathering full list with required reference
63
+ Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}` support and required filter parameters.
64
+ Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
65
+
66
+ ```python
67
+ from b24api import Bitrix24
68
+
69
+ b24 = Bitrix24()
70
+ deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
71
+ filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
72
+ result = b24.reference_batched_no_count(
73
+ {"method": "crm.timeline.comment.list", "parameters": {"ENTITY_TYPE": "deal"}},
74
+ filter_updates,
75
+ )
76
+ print(list(result))
77
+ ```
@@ -1,17 +1,17 @@
1
1
  import contextlib
2
+ import logging
2
3
  from collections.abc import Generator, Iterable
3
- from itertools import chain
4
+ from itertools import chain, islice
4
5
  from operator import itemgetter
5
6
 
6
7
  import httpx
7
8
  from fast_depends import inject
8
9
  from pydantic import ValidationError
9
- from retry.api import retry_call
10
+ from retry import retry
10
11
 
11
- from b24api.entity import ApiTypes, BatchResult, ErrorResponse, ListRequest, Request, ResultResponse
12
+ from b24api.entity import ApiTypes, BatchResult, ErrorResponse, ListRequest, Request, Response
12
13
  from b24api.error import RetryApiResponseError, RetryHTTPStatusError
13
- from b24api.future import batched
14
- from b24api.http import HttpxClient
14
+ from b24api.httpc import HttpxClient
15
15
  from b24api.settings import ApiSettings
16
16
 
17
17
 
@@ -20,22 +20,27 @@ class Bitrix24:
20
20
  def __init__(self, client: HttpxClient, settings: ApiSettings) -> None:
21
21
  self.client = client
22
22
  self.settings = settings
23
-
24
- def call(self, request: Request) -> ApiTypes:
25
- """Call any method (with retries) and return just `result`."""
26
- response = retry_call(
27
- self._call,
28
- fargs=[request],
23
+ self.logger = logging.getLogger(__name__)
24
+ self._call_retry = retry(
29
25
  exceptions=(RetryHTTPStatusError, RetryApiResponseError),
30
26
  tries=self.settings.retry_tries,
31
27
  delay=self.settings.retry_delay,
32
28
  backoff=self.settings.retry_backoff,
33
- )
29
+ logger=self.logger,
30
+ )(self._call)
31
+
32
+ def call(self, request: Request | dict) -> ApiTypes:
33
+ """Call any method (with retries) and return just `result`."""
34
+ response = self._call_retry(request)
34
35
 
35
36
  return response.result
36
37
 
37
- def _call(self, request: Request) -> ResultResponse:
38
+ def _call(self, request: Request | dict) -> Response:
38
39
  """Call any method and return full response."""
40
+ request = Request.model_validate(request)
41
+
42
+ self.logger.debug("Sending request", extra={"request": request})
43
+
39
44
  http_response = self.client.post(
40
45
  f"{self.settings.webhook_url}{request.method}",
41
46
  headers={"Content-Type": "application/json"},
@@ -56,23 +61,37 @@ class Bitrix24:
56
61
  with contextlib.suppress(ValidationError):
57
62
  ErrorResponse.model_validate(json_response).raise_error(self.settings.retry_errors)
58
63
 
59
- return ResultResponse.model_validate(json_response)
64
+ response = Response.model_validate(json_response)
65
+
66
+ self.logger.debug("Received response", extra={"request": request, "response": response})
67
+ self.logger.info(
68
+ "Received response",
69
+ extra={
70
+ "method": request.method,
71
+ "duration": response.time.duration,
72
+ "processing": response.time.processing,
73
+ "operating": response.time.operating,
74
+ },
75
+ )
76
+
77
+ return response
60
78
 
61
79
  def batch(
62
80
  self,
63
- requests: Iterable[Request],
81
+ requests: Iterable[Request | dict],
64
82
  batch_size: int | None = None,
65
83
  ) -> Generator[ApiTypes, None, None]:
66
84
  """Call infinite sequence of methods within batches and return just `result`s."""
67
85
  batch_size = batch_size or self.settings.batch_size
68
86
 
69
- for batched_requests in batched(requests, batch_size):
87
+ tail_requests = iter(requests)
88
+ while batched_requests := list(islice(tail_requests, batch_size)):
70
89
  for response in self._batch(batched_requests):
71
90
  yield response.result
72
91
 
73
- def _batch(self, requests: Iterable[Request]) -> Generator[ResultResponse, None, None]:
92
+ def _batch(self, requests: Iterable[Request | dict]) -> Generator[Response, None, None]:
74
93
  """Call batch of methods and return full responses."""
75
- commands = {f"_{i}": request for i, request in enumerate(requests)}
94
+ commands = {f"_{i}": Request.model_validate(request) for i, request in enumerate(requests)}
76
95
  request = Request(
77
96
  method="batch",
78
97
  parameters={
@@ -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 ResultResponse(
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
- head_request: Request,
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
- head_response = self._call(head_request)
127
- yield from self._normalize_list(head_response.result)
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 |= {"start": start}
136
- tail_response = self._call(tail_request)
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
- head_request: Request,
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
- head_response = self._call(head_request)
159
- yield from self._normalize_list(head_response.result)
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 |= {"start": start}
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._normalize_list, tail_responses)
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">={id_key}", f"<{id_key}"
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
- head_tail = self.batch([head_request, tail_request])
215
- head, tail = tuple(map(self._normalize_list, head_tail))
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
216
244
 
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
222
-
223
- if max_head < min_tail:
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
- def _body_requests() -> Generator[Request, None, None]:
226
- for start in range(max_head + 1, min_tail, list_size):
227
- body_request = request.model_copy(deep=True)
228
- body_request.parameters.start = -1
229
- body_request.parameters.filter[id_from] = start
230
- body_request.parameters.filter[id_to] = min(start + list_size, min_tail)
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 < min_tail_id:
234
256
  body = self.batch(_body_requests(), batch_size)
235
- body = map(self._normalize_list, body)
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(tail):
241
- if int(get_id(item)) > max_head:
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 _normalize_list(result: list | dict[str, list]) -> list:
246
- """Normalize `list` method result to `list of items` structure.
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`),
@@ -9,7 +9,7 @@ from b24api.type import ApiTypes
9
9
 
10
10
 
11
11
  class Request(BaseModel):
12
- """Common request structure."""
12
+ """API request."""
13
13
 
14
14
  method: str
15
15
  parameters: dict[str, ApiTypes] = {}
@@ -30,17 +30,16 @@ 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] | None = None
35
- order: dict[str, str] | None = None
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
- """List request structure."""
40
+ """API `*.list` request."""
42
41
 
43
- parameters: ListRequestParameters = None
42
+ parameters: ListRequestParameters
44
43
 
45
44
 
46
45
  class ErrorResponse(BaseModel):
@@ -62,7 +61,7 @@ class ErrorResponse(BaseModel):
62
61
 
63
62
 
64
63
  class ResponseTime(BaseModel):
65
- """Time structure of response."""
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 ResultResponse(BaseModel):
78
- """API data response."""
76
+ class Response(BaseModel):
77
+ """API response."""
79
78
 
80
79
  result: ApiTypes
81
80
  time: ResponseTime
@@ -84,6 +83,8 @@ class ResultResponse(BaseModel):
84
83
 
85
84
 
86
85
  class BatchResult(BaseModel):
86
+ """API response `result` structure for `batch` method."""
87
+
87
88
  result: dict[str, ApiTypes]
88
89
  result_time: dict[str, ResponseTime]
89
90
  result_error: dict[str, ErrorResponse]
@@ -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
- client = Client(http2=True)
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="bitrix_24_api_",
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,90 @@
1
+ Metadata-Version: 2.2
2
+ Name: b24api
3
+ Version: 0.2.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
+
14
+ # API client for Bitrix24
15
+
16
+ Low-level API client with multiple strategies for lists gathering.
17
+ All methods support retries.
18
+
19
+ ## Regular call (any method)
20
+ ```python
21
+ from b24api import Bitrix24
22
+
23
+ b24 = Bitrix24()
24
+ result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
25
+ print(result)
26
+ ```
27
+
28
+ ## Batch call (any method, no limit)
29
+ ```python
30
+ from b24api import Bitrix24
31
+
32
+ b24 = Bitrix24()
33
+
34
+ requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
35
+ result = b24.batch(requests)
36
+ print(result)
37
+ ```
38
+
39
+ ## Gathering full list (slow)
40
+ Applicable to list methods with `start=<offset>` support.
41
+ Fetches list chunks one by one.
42
+
43
+ ```python
44
+ from b24api import Bitrix24
45
+
46
+ b24 = Bitrix24()
47
+ result = b24.list_sequential({"method": "user.get"})
48
+ print(list(result))
49
+ ```
50
+
51
+ ## Gathering full list (faster)
52
+ Applicable to list methods with `start=<offset>` support.
53
+ Fetches first list chunk with regular call, then fetches other chunks with `batch`.
54
+
55
+ ```python
56
+ from b24api import Bitrix24
57
+
58
+ b24 = Bitrix24()
59
+ result = b24.list_batched({"method": "user.get"})
60
+ print(list(result))
61
+ ```
62
+
63
+ ## Gathering full list (fastest)
64
+ Applicable to list methods with `filter={<parameters>}` support.
65
+ Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
66
+
67
+ ```python
68
+ from b24api import Bitrix24
69
+
70
+ b24 = Bitrix24()
71
+ result = b24.list_batched_no_count({"method": "user.get"})
72
+ print(list(result))
73
+ ```
74
+
75
+ ## Gathering full list with required reference
76
+ Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}` support and required filter parameters.
77
+ Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
78
+
79
+ ```python
80
+ from b24api import Bitrix24
81
+
82
+ b24 = Bitrix24()
83
+ deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
84
+ filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
85
+ result = b24.reference_batched_no_count(
86
+ {"method": "crm.timeline.comment.list", "parameters": {"ENTITY_TYPE": "deal"}},
87
+ filter_updates,
88
+ )
89
+ print(list(result))
90
+ ```
@@ -6,8 +6,7 @@ b24api/__init__.py
6
6
  b24api/api.py
7
7
  b24api/entity.py
8
8
  b24api/error.py
9
- b24api/future.py
10
- b24api/http.py
9
+ b24api/httpc.py
11
10
  b24api/query.py
12
11
  b24api/settings.py
13
12
  b24api/type.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "b24api"
8
- version = "0.1.3"
8
+ version = "0.2.0"
9
9
  description = "Bitrix24 API"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -59,7 +59,7 @@ addopts = ["--strict-config", "--strict-markers"]
59
59
  xfail_strict = true
60
60
 
61
61
  [tool.pytest_env]
62
- BITRIX_24_API_WEBHOOK_URL = "https://bitrix24.com/rest/0/test/"
62
+ BITRIX24_API_WEBHOOK_URL = "https://bitrix24.com/rest/0/test/"
63
63
 
64
64
 
65
65
  [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
@@ -1,3 +0,0 @@
1
- # API client for Bitrix24
2
-
3
- Low-level API client with multiple strategies for lists gathering.
@@ -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
@@ -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