b24api 0.3.8__tar.gz → 0.3.10__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b24api
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Summary: Bitrix24 API
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -9,7 +9,7 @@ Requires-Dist: fast-depends>=2.4.12
9
9
  Requires-Dist: httpx[http2]>=0.28.1
10
10
  Requires-Dist: pydantic>=2.10.6
11
11
  Requires-Dist: pydantic-settings>=2.8.1
12
- Requires-Dist: retry>=0.9.2
12
+ Requires-Dist: tenacity>=9.1.2
13
13
  Dynamic: license-file
14
14
 
15
15
  # API client for Bitrix24
@@ -1,49 +1,49 @@
1
1
  import contextlib
2
- import json
3
2
  import logging
4
3
  from collections.abc import Generator, Iterable
5
- from functools import partial
6
4
  from itertools import chain, islice
7
5
  from operator import itemgetter
8
6
 
7
+ import h2.exceptions
9
8
  import httpx
10
9
  from fast_depends import inject
10
+ from httpx import Client
11
11
  from pydantic import ValidationError
12
- from retry import retry
12
+ from tenacity import Retrying, before_sleep_log, retry_if_exception_type, stop_after_attempt, wait_exponential
13
13
 
14
14
  from b24api.entity import ApiTypes, BatchResult, ErrorResponse, ListRequest, Request, Response
15
15
  from b24api.error import RetryApiResponseError, RetryHTTPStatusError
16
16
  from b24api.settings import ApiSettings
17
- from b24api.transport import HttpxClient
18
17
 
19
18
 
20
19
  class Bitrix24:
20
+ http: Client = Client(http2=True)
21
+
21
22
  @inject
22
- def __init__(self, client: HttpxClient, settings: ApiSettings) -> None:
23
- self.client = client
23
+ def __init__(self, settings: ApiSettings) -> None:
24
24
  self.settings = settings
25
25
  self.logger = logging.getLogger("b24api")
26
26
 
27
- retry_ = partial(
28
- retry,
29
- exceptions=(
30
- httpx.ConnectError,
31
- httpx.ConnectTimeout,
32
- httpx.RemoteProtocolError,
33
- RetryHTTPStatusError,
34
- RetryApiResponseError,
27
+ self.http.timeout = self.settings.http_timeout
28
+
29
+ self.retry = Retrying(
30
+ retry=retry_if_exception_type(
31
+ (
32
+ httpx.TransportError,
33
+ h2.exceptions.ProtocolError,
34
+ RetryHTTPStatusError,
35
+ RetryApiResponseError,
36
+ ),
35
37
  ),
36
- tries=self.settings.retry_tries,
37
- delay=self.settings.retry_delay,
38
- backoff=self.settings.retry_backoff,
39
- logger=self.logger,
38
+ wait=wait_exponential(multiplier=self.settings.retry_delay, exp_base=self.settings.retry_backoff),
39
+ stop=stop_after_attempt(self.settings.retry_attempts),
40
+ before_sleep=before_sleep_log(self.logger, logging.WARNING),
41
+ reraise=True,
40
42
  )
41
- self._call_retry = retry_()(self._call)
42
- self._batch_retry = retry_()(self._batch)
43
43
 
44
44
  def call(self, request: Request | dict) -> ApiTypes:
45
- """Call any method (with retries) and return just `result`."""
46
- return self._call_retry(request).result
45
+ """Call any method (with retries) and return `result` from response."""
46
+ return self.retry(self._call, request).result
47
47
 
48
48
  def _call(self, request: Request | dict) -> Response:
49
49
  """Call any method and return full response."""
@@ -51,17 +51,17 @@ class Bitrix24:
51
51
 
52
52
  self.logger.debug("Sending request: %s", request)
53
53
 
54
- http_response = self.client.post(
54
+ http_response = self.http.post(
55
55
  f"{self.settings.webhook_url}{request.method}",
56
56
  headers={"Content-Type": "application/json"},
57
57
  json=request.model_dump(mode="json")["parameters"],
58
58
  )
59
59
 
60
- with contextlib.suppress(httpx.ResponseNotRead, json.JSONDecodeError, ValidationError):
61
- ErrorResponse.model_validate(http_response.json()).raise_error(self.settings.retry_errors)
60
+ with contextlib.suppress(httpx.ResponseNotRead, ValidationError):
61
+ ErrorResponse.model_validate_json(http_response.content).raise_error(self.settings.retry_errors)
62
62
 
63
63
  try:
64
- json_response = http_response.raise_for_status().json()
64
+ http_response.raise_for_status()
65
65
  except httpx.HTTPStatusError as error:
66
66
  if http_response.status_code in self.settings.retry_statuses:
67
67
  raise RetryHTTPStatusError(
@@ -71,11 +71,7 @@ class Bitrix24:
71
71
  ) from error
72
72
  raise
73
73
 
74
- with contextlib.suppress(ValidationError):
75
- ErrorResponse.model_validate(json_response).raise_error(self.settings.retry_errors)
76
-
77
- response = Response.model_validate(json_response)
78
-
74
+ response = Response.model_validate_json(http_response.content)
79
75
  self.logger.debug("Received response: %s", response)
80
76
 
81
77
  return response
@@ -84,17 +80,17 @@ class Bitrix24:
84
80
  self,
85
81
  requests: Iterable[Request | dict],
86
82
  batch_size: int | None = None,
87
- ) -> Generator[ApiTypes, None, None]:
88
- """Call infinite sequence of methods within batches and return just `result`s."""
83
+ ) -> Generator[ApiTypes]:
84
+ """Call unlimited sequence of methods within batches and return `result` from responses."""
89
85
  batch_size = batch_size or self.settings.batch_size
90
86
 
91
87
  tail_requests = iter(requests)
92
88
  while batched_requests := list(islice(tail_requests, batch_size)):
93
- for response in self._batch_retry(batched_requests):
89
+ for response in self.retry(self._batch, batched_requests):
94
90
  yield response.result
95
91
 
96
92
  def _batch(self, requests: Iterable[Request | dict]) -> list[Response]:
97
- """Call batch of methods and return full responses."""
93
+ """Call limited batch of methods and return full responses."""
98
94
  commands = {f"_{i}": Request.model_validate(request) for i, request in enumerate(requests)}
99
95
  request = Request(
100
96
  method="batch",
@@ -139,7 +135,7 @@ class Bitrix24:
139
135
  self,
140
136
  request: Request | dict,
141
137
  list_size: int | None = None,
142
- ) -> Generator[ApiTypes, None, None]:
138
+ ) -> Generator[ApiTypes]:
143
139
  """Call `list` method and return full `result`.
144
140
 
145
141
  Slow (sequential tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
@@ -150,7 +146,7 @@ class Bitrix24:
150
146
  head_request = request.model_copy(deep=True)
151
147
  head_request.parameters["start"] = 0
152
148
 
153
- head_response = self._call_retry(head_request)
149
+ head_response = self.retry(self._call, head_request)
154
150
  yield from self._fix_list_result(head_response.result)
155
151
 
156
152
  if head_response.next and head_response.next != list_size:
@@ -160,7 +156,7 @@ class Bitrix24:
160
156
  for start in range(list_size, total, list_size):
161
157
  tail_request = head_request.model_copy(deep=True)
162
158
  tail_request.parameters["start"] = start
163
- tail_response = self._call_retry(tail_request)
159
+ tail_response = self.retry(self._call, tail_request)
164
160
 
165
161
  if tail_response.next and tail_response.next != start + list_size:
166
162
  raise ValueError(
@@ -173,7 +169,7 @@ class Bitrix24:
173
169
  request: Request | dict,
174
170
  list_size: int | None = None,
175
171
  batch_size: int | None = None,
176
- ) -> Generator[ApiTypes, None, None]:
172
+ ) -> Generator[ApiTypes]:
177
173
  """Call `list` method and return full `result`.
178
174
 
179
175
  Faster (batched tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
@@ -185,13 +181,13 @@ class Bitrix24:
185
181
  head_request = request.model_copy(deep=True)
186
182
  head_request.parameters["start"] = 0
187
183
 
188
- head_response = self._call_retry(head_request)
184
+ head_response = self.retry(self._call, head_request)
189
185
  yield from self._fix_list_result(head_response.result)
190
186
 
191
187
  if head_response.next and head_response.next != list_size:
192
188
  raise ValueError(f"Expecting chunk size to be {list_size}. Got: {head_response.next}")
193
189
 
194
- def _tail_requests() -> Generator[Request, None, None]:
190
+ def _tail_requests() -> Generator[Request]:
195
191
  total = head_response.total or 0
196
192
  for start in range(list_size, total, list_size):
197
193
  tail_request = head_request.model_copy(deep=True)
@@ -209,7 +205,7 @@ class Bitrix24:
209
205
  id_key: str = "ID",
210
206
  list_size: int | None = None,
211
207
  batch_size: int | None = None,
212
- ) -> Generator[ApiTypes, None, None]:
208
+ ) -> Generator[ApiTypes]:
213
209
  """Call `list` method and return full `result`.
214
210
 
215
211
  Fastest (batched, no count) list gathering for methods with `filter` parameter (e.g. `crm.lead.list`).
@@ -232,7 +228,7 @@ class Bitrix24:
232
228
  )
233
229
 
234
230
  if request.parameters.order:
235
- raise ValueError("Ordering parameters are reserved `order`in `list_batched_no_count`")
231
+ raise ValueError("Ordering parameters are reserved in `list_batched_no_count`")
236
232
 
237
233
  head_request = request.model_copy(deep=True)
238
234
  head_request.parameters.start = -1
@@ -249,7 +245,7 @@ class Bitrix24:
249
245
  max_head_id = max(map(int, map(get_id, head_result)), default=None)
250
246
  min_tail_id = min(map(int, map(get_id, tail_result)), default=None)
251
247
 
252
- def _body_requests() -> Generator[ListRequest, None, None]:
248
+ def _body_requests() -> Generator[ListRequest]:
253
249
  for start in range(max_head_id, min_tail_id, list_size):
254
250
  body_request = head_request.model_copy(deep=True)
255
251
  body_request.parameters.filter[id_from] = start
@@ -273,8 +269,8 @@ class Bitrix24:
273
269
  id_key: str = "ID",
274
270
  list_size: int | None = None,
275
271
  batch_size: int | None = None,
276
- ) -> Generator[ApiTypes, None, None]:
277
- """Call `list` method with reference `updates` to filter and return full `result`.
272
+ ) -> Generator[ApiTypes]:
273
+ """Call `list` method with reference `updates` for `filter` and return full `result`.
278
274
 
279
275
  Fastest (batched, no count) list gathering for methods with `filter` parameter and required `reference`
280
276
  (e.g. `crm.timeline.comment.list`).
@@ -299,7 +295,7 @@ class Bitrix24:
299
295
  if request.parameters.order:
300
296
  raise ValueError("Ordering parameters are reserved `order`in `reference_batched_no_count`")
301
297
 
302
- def _tail_requests() -> Generator[ListRequest, None, None]:
298
+ def _tail_requests() -> Generator[ListRequest]:
303
299
  for update in updates:
304
300
  if id_from in update:
305
301
  raise ValueError(
@@ -354,6 +350,6 @@ class Bitrix24:
354
350
  value = result[key]
355
351
 
356
352
  if not isinstance(value, list):
357
- raise TypeError(f"If `result` is a `dict`, expecting single `list` item. Got: {result}")
353
+ raise TypeError(f"If `result` is a `dict`, expecting single item to be a `list`. Got: {result}")
358
354
 
359
355
  return value
@@ -5,10 +5,7 @@ from b24api.type import ApiTypes
5
5
 
6
6
 
7
7
  def build_query(parameters: dict[int | str, ApiTypes], path: str = "%s") -> str:
8
- query = []
9
-
10
- if parameters is None:
11
- return ""
8
+ query: list[str] = []
12
9
 
13
10
  for key, value in parameters.items():
14
11
  if value is None:
@@ -8,14 +8,10 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
8
8
 
9
9
 
10
10
  class Settings(BaseSettings):
11
- model_config = SettingsConfigDict(
12
- env_prefix="bitrix24_api_",
13
- env_file=".env",
14
- extra="ignore",
15
- )
16
-
17
11
  webhook_url: HttpUrl
18
12
 
13
+ http_timeout: int = 30
14
+
19
15
  retry_statuses: list[int] = (
20
16
  codes.LOCKED,
21
17
  codes.TOO_EARLY,
@@ -27,15 +23,21 @@ class Settings(BaseSettings):
27
23
  )
28
24
  retry_errors: list[str] = ["query_limit_exceeded", "operation_time_limit"]
29
25
 
30
- retry_tries: int = 5
26
+ retry_attempts: int = 5
31
27
  retry_delay: float = 5
32
28
  retry_backoff: float = 2
33
29
 
34
30
  list_size: int = 50
35
31
  batch_size: int = 50
36
32
 
33
+ model_config = SettingsConfigDict(
34
+ env_prefix="bitrix24_api_",
35
+ env_file=".env",
36
+ extra="ignore",
37
+ )
38
+
37
39
 
38
- def api_settings(**kwargs: dict) -> Generator[Settings, None, None]:
40
+ def api_settings(**kwargs: dict) -> Generator[Settings]:
39
41
  settings = Settings(**kwargs)
40
42
  yield settings
41
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b24api
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Summary: Bitrix24 API
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -9,7 +9,7 @@ Requires-Dist: fast-depends>=2.4.12
9
9
  Requires-Dist: httpx[http2]>=0.28.1
10
10
  Requires-Dist: pydantic>=2.10.6
11
11
  Requires-Dist: pydantic-settings>=2.8.1
12
- Requires-Dist: retry>=0.9.2
12
+ Requires-Dist: tenacity>=9.1.2
13
13
  Dynamic: license-file
14
14
 
15
15
  # API client for Bitrix24
@@ -8,7 +8,6 @@ b24api/entity.py
8
8
  b24api/error.py
9
9
  b24api/query.py
10
10
  b24api/settings.py
11
- b24api/transport.py
12
11
  b24api/type.py
13
12
  b24api.egg-info/PKG-INFO
14
13
  b24api.egg-info/SOURCES.txt
@@ -2,4 +2,4 @@ fast-depends>=2.4.12
2
2
  httpx[http2]>=0.28.1
3
3
  pydantic>=2.10.6
4
4
  pydantic-settings>=2.8.1
5
- retry>=0.9.2
5
+ tenacity>=9.1.2
@@ -1,11 +1,17 @@
1
1
  [build-system]
2
- requires = ["setuptools >= 75"]
2
+ requires = [
3
+ "setuptools>=41",
4
+ "wheel",
5
+ "setuptools-git-versioning>=2.0,<3"
6
+ ]
3
7
  build-backend = "setuptools.build_meta"
4
8
 
9
+ [tool.setuptools-git-versioning]
10
+ enabled = true
5
11
 
6
12
  [project]
7
13
  name = "b24api"
8
- version = "0.3.8"
14
+ dynamic = ["version"]
9
15
  description = "Bitrix24 API"
10
16
  readme = "README.md"
11
17
  requires-python = ">=3.10"
@@ -14,7 +20,7 @@ dependencies = [
14
20
  "httpx[http2]>=0.28.1",
15
21
  "pydantic>=2.10.6",
16
22
  "pydantic-settings>=2.8.1",
17
- "retry>=0.9.2",
23
+ "tenacity>=9.1.2",
18
24
  ]
19
25
 
20
26
  [dependency-groups]
@@ -22,6 +28,9 @@ dev = [
22
28
  "pytest-env>=1.1.5",
23
29
  "pytest-httpx>=0.35.0",
24
30
  "pytest-mock>=3.14.0",
31
+ "setuptools>=80.9.0",
32
+ "setuptools-git-versioning>=2.1.0",
33
+ "wheel>=0.45.1",
25
34
  ]
26
35
 
27
36
  [tool.ruff]
@@ -1,14 +0,0 @@
1
- from collections.abc import Generator
2
- from typing import Annotated
3
-
4
- from fast_depends import Depends
5
- from httpx import Client
6
-
7
-
8
- def httpx_client() -> Generator[Client, None, None]:
9
- client = Client(http2=True, timeout=30)
10
-
11
- yield client
12
-
13
-
14
- HttpxClient = Annotated[Client, Depends(httpx_client)]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes