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.
- {b24api-0.3.8/b24api.egg-info → b24api-0.3.10}/PKG-INFO +2 -2
- {b24api-0.3.8 → b24api-0.3.10}/b24api/api.py +44 -48
- {b24api-0.3.8 → b24api-0.3.10}/b24api/query.py +1 -4
- {b24api-0.3.8 → b24api-0.3.10}/b24api/settings.py +10 -8
- {b24api-0.3.8 → b24api-0.3.10/b24api.egg-info}/PKG-INFO +2 -2
- {b24api-0.3.8 → b24api-0.3.10}/b24api.egg-info/SOURCES.txt +0 -1
- {b24api-0.3.8 → b24api-0.3.10}/b24api.egg-info/requires.txt +1 -1
- {b24api-0.3.8 → b24api-0.3.10}/pyproject.toml +12 -3
- b24api-0.3.8/b24api/transport.py +0 -14
- {b24api-0.3.8 → b24api-0.3.10}/LICENSE +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/MANIFEST.in +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/README.md +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/b24api/__init__.py +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/b24api/entity.py +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/b24api/error.py +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/b24api/type.py +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/b24api.egg-info/dependency_links.txt +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/b24api.egg-info/top_level.txt +0 -0
- {b24api-0.3.8 → b24api-0.3.10}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: b24api
|
|
3
|
-
Version: 0.3.
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
46
|
-
return self.
|
|
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.
|
|
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,
|
|
61
|
-
ErrorResponse.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
"""Call
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
277
|
-
"""Call `list` method with reference `updates`
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
12
|
+
Requires-Dist: tenacity>=9.1.2
|
|
13
13
|
Dynamic: license-file
|
|
14
14
|
|
|
15
15
|
# API client for Bitrix24
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = [
|
|
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
|
-
|
|
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
|
-
"
|
|
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]
|
b24api-0.3.8/b24api/transport.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|