b24api 0.1.2__py3-none-any.whl
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/__init__.py +1 -0
- b24api/api.py +273 -0
- b24api/entity.py +91 -0
- b24api/error.py +22 -0
- b24api/future.py +12 -0
- b24api/http.py +13 -0
- b24api/query.py +31 -0
- b24api/settings.py +43 -0
- b24api/type.py +5 -0
- b24api-0.1.2.dist-info/LICENSE +21 -0
- b24api-0.1.2.dist-info/METADATA +16 -0
- b24api-0.1.2.dist-info/RECORD +14 -0
- b24api-0.1.2.dist-info/WHEEL +5 -0
- b24api-0.1.2.dist-info/top_level.txt +1 -0
b24api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from b24api.api import Bitrix24
|
b24api/api.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
from collections.abc import Generator, Iterable
|
|
3
|
+
from itertools import chain
|
|
4
|
+
from operator import itemgetter
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from fast_depends import inject
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
from retry.api import retry_call
|
|
10
|
+
|
|
11
|
+
from b24api.entity import ApiTypes, BatchResult, ErrorResponse, ListRequest, Request, ResultResponse
|
|
12
|
+
from b24api.error import RetryApiResponseError, RetryHTTPStatusError
|
|
13
|
+
from b24api.future import batched
|
|
14
|
+
from b24api.http import HttpxClient
|
|
15
|
+
from b24api.settings import ApiSettings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Bitrix24:
|
|
19
|
+
@inject
|
|
20
|
+
def __init__(self, client: HttpxClient, settings: ApiSettings) -> None:
|
|
21
|
+
self.client = client
|
|
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],
|
|
29
|
+
exceptions=(RetryHTTPStatusError, RetryApiResponseError),
|
|
30
|
+
tries=self.settings.retry_tries,
|
|
31
|
+
delay=self.settings.retry_delay,
|
|
32
|
+
backoff=self.settings.retry_backoff,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return response.result
|
|
36
|
+
|
|
37
|
+
def _call(self, request: Request) -> ResultResponse:
|
|
38
|
+
"""Call any method and return full response."""
|
|
39
|
+
http_response = self.client.post(
|
|
40
|
+
f"{self.settings.webhook_url}{request.method}",
|
|
41
|
+
headers={"Content-Type": "application/json"},
|
|
42
|
+
json=request.model_dump()["parameters"],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
json_response = http_response.raise_for_status().json()
|
|
47
|
+
except httpx.HTTPStatusError as error:
|
|
48
|
+
if http_response.status_code in self.settings.retry_statuses:
|
|
49
|
+
raise RetryHTTPStatusError(
|
|
50
|
+
str(error),
|
|
51
|
+
request=error.request,
|
|
52
|
+
response=error.response,
|
|
53
|
+
) from error
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
with contextlib.suppress(ValidationError):
|
|
57
|
+
ErrorResponse.model_validate(json_response).raise_error(self.settings.retry_errors)
|
|
58
|
+
|
|
59
|
+
return ResultResponse.model_validate(json_response)
|
|
60
|
+
|
|
61
|
+
def batch(
|
|
62
|
+
self,
|
|
63
|
+
requests: Iterable[Request],
|
|
64
|
+
batch_size: int | None = None,
|
|
65
|
+
) -> Generator[ApiTypes, None, None]:
|
|
66
|
+
"""Call infinite sequence of methods within batches and return just `result`s."""
|
|
67
|
+
batch_size = batch_size or self.settings.batch_size
|
|
68
|
+
|
|
69
|
+
for batched_requests in batched(requests, batch_size):
|
|
70
|
+
for response in self._batch(batched_requests):
|
|
71
|
+
yield response.result
|
|
72
|
+
|
|
73
|
+
def _batch(self, requests: Iterable[Request]) -> Generator[ResultResponse, None, None]:
|
|
74
|
+
"""Call batch of methods and return full responses."""
|
|
75
|
+
commands = {f"_{i}": request for i, request in enumerate(requests)}
|
|
76
|
+
request = Request(
|
|
77
|
+
method="batch",
|
|
78
|
+
parameters={
|
|
79
|
+
"halt": True,
|
|
80
|
+
"cmd": {key: request.query for key, request in commands.items()},
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
result = self.call(request)
|
|
85
|
+
|
|
86
|
+
for fix_key in ["result_error", "result_total", "result_next"]:
|
|
87
|
+
if isinstance(result[fix_key], list) and not result[fix_key]:
|
|
88
|
+
result[fix_key] = dict(result[fix_key])
|
|
89
|
+
|
|
90
|
+
result = BatchResult.model_validate(result)
|
|
91
|
+
|
|
92
|
+
for i in range(len(commands)):
|
|
93
|
+
key = f"_{i}"
|
|
94
|
+
|
|
95
|
+
if key in result.result_error:
|
|
96
|
+
ErrorResponse.model_validate(result.result_error[key]).raise_error(self.settings.retry_errors)
|
|
97
|
+
|
|
98
|
+
command = commands[key]
|
|
99
|
+
if key not in result.result:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Expecting `result` to contain result for command {{`{key}`: {command}}}. Got: {result}",
|
|
102
|
+
)
|
|
103
|
+
if key not in result.result_time:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"Expecting `result_time` to contain result for command {{`{key}`: {command}}}. Got: {result}",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
yield ResultResponse(
|
|
109
|
+
result=result.result[key],
|
|
110
|
+
time=result.result_time[key],
|
|
111
|
+
total=result.result_total.get(key, None),
|
|
112
|
+
next=result.result_next.get(key, None),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def list_sequential(
|
|
116
|
+
self,
|
|
117
|
+
head_request: Request,
|
|
118
|
+
list_size: int | None = None,
|
|
119
|
+
) -> Generator[ApiTypes, None, None]:
|
|
120
|
+
"""Call `list` method and return full `result`.
|
|
121
|
+
|
|
122
|
+
Slow (sequential tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
|
|
123
|
+
"""
|
|
124
|
+
list_size = list_size or self.settings.list_size
|
|
125
|
+
|
|
126
|
+
head_response = self._call(head_request)
|
|
127
|
+
yield from self._normalize_list(head_response.result)
|
|
128
|
+
|
|
129
|
+
if head_response.next and head_response.next != list_size:
|
|
130
|
+
raise ValueError(f"Expecting list chunk size to be {list_size}. Got: {head_response.next}")
|
|
131
|
+
|
|
132
|
+
total = head_response.total or 0
|
|
133
|
+
for start in range(list_size, total, list_size):
|
|
134
|
+
tail_request = head_request.model_copy(deep=True)
|
|
135
|
+
tail_request.parameters |= {"start": start}
|
|
136
|
+
tail_response = self._call(tail_request)
|
|
137
|
+
|
|
138
|
+
if tail_response.next and tail_response.next != start + list_size:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"Expecting next list chunk to start at {start + list_size}. Got: {tail_response.next}",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
yield from self._normalize_list(tail_response.result)
|
|
144
|
+
|
|
145
|
+
def list_batched(
|
|
146
|
+
self,
|
|
147
|
+
head_request: Request,
|
|
148
|
+
list_size: int | None = None,
|
|
149
|
+
batch_size: int | None = None,
|
|
150
|
+
) -> Generator[ApiTypes, None, None]:
|
|
151
|
+
"""Call `list` method and return full `result`.
|
|
152
|
+
|
|
153
|
+
Faster (batched tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
|
|
154
|
+
"""
|
|
155
|
+
list_size = list_size or self.settings.list_size
|
|
156
|
+
batch_size = batch_size or self.settings.batch_size
|
|
157
|
+
|
|
158
|
+
head_response = self._call(head_request)
|
|
159
|
+
yield from self._normalize_list(head_response.result)
|
|
160
|
+
|
|
161
|
+
if head_response.next and head_response.next != list_size:
|
|
162
|
+
raise ValueError(f"Expecting chunk size to be {list_size}. Got: {head_response.next}")
|
|
163
|
+
|
|
164
|
+
def _tail_requests() -> Generator[Request, None, None]:
|
|
165
|
+
total = head_response.total or 0
|
|
166
|
+
for start in range(list_size, total, list_size):
|
|
167
|
+
tail_request = head_request.model_copy(deep=True)
|
|
168
|
+
tail_request.parameters |= {"start": start}
|
|
169
|
+
yield tail_request
|
|
170
|
+
|
|
171
|
+
tail_responses = self.batch(_tail_requests(), batch_size)
|
|
172
|
+
tail_responses = map(self._normalize_list, tail_responses)
|
|
173
|
+
tail_responses = chain.from_iterable(tail_responses)
|
|
174
|
+
|
|
175
|
+
yield from tail_responses
|
|
176
|
+
|
|
177
|
+
def list_batched_no_count(
|
|
178
|
+
self,
|
|
179
|
+
request: ListRequest,
|
|
180
|
+
id_key: str = "ID",
|
|
181
|
+
list_size: int | None = None,
|
|
182
|
+
batch_size: int | None = None,
|
|
183
|
+
) -> Generator[ApiTypes, None, None]:
|
|
184
|
+
"""Call `list` method and return full `result`.
|
|
185
|
+
|
|
186
|
+
Fastest (batched, no count) list gathering for methods with `filter` parameter (e.g. `crm.lead.list`).
|
|
187
|
+
"""
|
|
188
|
+
list_size = list_size or self.settings.list_size
|
|
189
|
+
batch_size = batch_size or self.settings.batch_size
|
|
190
|
+
|
|
191
|
+
select_ = request.parameters.select
|
|
192
|
+
if "*" not in select_ and id_key not in select_:
|
|
193
|
+
request.select.append(id_key)
|
|
194
|
+
|
|
195
|
+
id_from, id_to = f">={id_key}", f"<{id_key}"
|
|
196
|
+
|
|
197
|
+
filter_ = request.parameters.filter
|
|
198
|
+
if filter_ and (id_from in filter_ or id_to in filter_):
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Filter parameters `{id_from}` and `{id_to}` are reserved in `list_batched_no_count`",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if request.parameters.order:
|
|
204
|
+
raise ValueError("Ordering parameters are reserved `order`in `list_batched_no_count`")
|
|
205
|
+
|
|
206
|
+
head_request = request.model_copy(deep=True)
|
|
207
|
+
head_request.parameters.start = -1
|
|
208
|
+
head_request.parameters.order = {"ID": "ASC"}
|
|
209
|
+
|
|
210
|
+
tail_request = request.model_copy(deep=True)
|
|
211
|
+
tail_request.parameters.start = -1
|
|
212
|
+
tail_request.parameters.order = {"ID": "DESC"}
|
|
213
|
+
|
|
214
|
+
head_tail = self.batch([head_request, tail_request])
|
|
215
|
+
head, tail = tuple(map(self._normalize_list, head_tail))
|
|
216
|
+
|
|
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:
|
|
224
|
+
|
|
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
|
|
233
|
+
|
|
234
|
+
body = self.batch(_body_requests(), batch_size)
|
|
235
|
+
body = map(self._normalize_list, body)
|
|
236
|
+
body = chain.from_iterable(body)
|
|
237
|
+
|
|
238
|
+
yield from body
|
|
239
|
+
|
|
240
|
+
for item in reversed(tail):
|
|
241
|
+
if int(get_id(item)) > max_head:
|
|
242
|
+
yield item
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _normalize_list(result: list | dict[str, list]) -> list:
|
|
246
|
+
"""Normalize `list` method result to `list of items` structure.
|
|
247
|
+
|
|
248
|
+
There are two kinds of what `list` method `result` may contain:
|
|
249
|
+
- a list of items (e.g. `department-get` and `disk.folder.getchildren`),
|
|
250
|
+
- a dictionary with single item that contains the desired list of items
|
|
251
|
+
(e.g. `tasks` in `tasks.task.list`).
|
|
252
|
+
"""
|
|
253
|
+
if not isinstance(result, list | dict):
|
|
254
|
+
raise TypeError(f"Expecting `result` to be a `list` or a `dict`. Got: {result}")
|
|
255
|
+
|
|
256
|
+
if not result:
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
if isinstance(result, list):
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
if len(result) != 1:
|
|
263
|
+
raise TypeError(
|
|
264
|
+
f"If `result` is a `dict`, expecting single item. Got: {result}",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
key = next(iter(result))
|
|
268
|
+
value = result[key]
|
|
269
|
+
|
|
270
|
+
if not isinstance(value, list):
|
|
271
|
+
raise TypeError(f"If `result` is a `dict`, expecting single `list` item. Got: {result}")
|
|
272
|
+
|
|
273
|
+
return value
|
b24api/entity.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from b24api.error import ApiResponseError, RetryApiResponseError
|
|
7
|
+
from b24api.query import build_query
|
|
8
|
+
from b24api.type import ApiTypes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Request(BaseModel):
|
|
12
|
+
"""Common request structure."""
|
|
13
|
+
|
|
14
|
+
method: str
|
|
15
|
+
parameters: dict[str, ApiTypes] = {}
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def query(self) -> str:
|
|
19
|
+
if not self.parameters:
|
|
20
|
+
return self.method
|
|
21
|
+
|
|
22
|
+
parameters = self.parameters
|
|
23
|
+
if isinstance(self.parameters, BaseModel):
|
|
24
|
+
parameters = self.parameters.model_dump()
|
|
25
|
+
query = build_query(parameters)
|
|
26
|
+
|
|
27
|
+
return f"{self.method}?{query}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ListRequestParameters(BaseModel):
|
|
31
|
+
"""Parameters of `*.list` requests."""
|
|
32
|
+
|
|
33
|
+
select: list[str]
|
|
34
|
+
filter: dict[str, ApiTypes] | None = None
|
|
35
|
+
order: dict[str, str] | None = None
|
|
36
|
+
limit: int | None = None
|
|
37
|
+
start: int | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ListRequest(Request):
|
|
41
|
+
"""List request structure."""
|
|
42
|
+
|
|
43
|
+
parameters: ListRequestParameters = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ErrorResponse(BaseModel):
|
|
47
|
+
"""API error response."""
|
|
48
|
+
|
|
49
|
+
error: str
|
|
50
|
+
error_description: str | None = None
|
|
51
|
+
|
|
52
|
+
def raise_error(self, retry_errors: list[str]) -> Self:
|
|
53
|
+
if self.error in retry_errors:
|
|
54
|
+
raise RetryApiResponseError(
|
|
55
|
+
code=self.error,
|
|
56
|
+
description=self.error_description,
|
|
57
|
+
)
|
|
58
|
+
raise ApiResponseError(
|
|
59
|
+
code=self.error,
|
|
60
|
+
description=self.error_description,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ResponseTime(BaseModel):
|
|
65
|
+
"""Time structure of response."""
|
|
66
|
+
|
|
67
|
+
start: float
|
|
68
|
+
finish: float
|
|
69
|
+
duration: float
|
|
70
|
+
processing: float
|
|
71
|
+
date_start: datetime
|
|
72
|
+
date_finish: datetime
|
|
73
|
+
operating_reset_at: float
|
|
74
|
+
operating: float
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ResultResponse(BaseModel):
|
|
78
|
+
"""API data response."""
|
|
79
|
+
|
|
80
|
+
result: ApiTypes
|
|
81
|
+
time: ResponseTime
|
|
82
|
+
total: int | None = None
|
|
83
|
+
next: int | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BatchResult(BaseModel):
|
|
87
|
+
result: dict[str, ApiTypes]
|
|
88
|
+
result_time: dict[str, ResponseTime]
|
|
89
|
+
result_error: dict[str, ErrorResponse]
|
|
90
|
+
result_total: dict[str, int]
|
|
91
|
+
result_next: dict[str, int]
|
b24api/error.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RetryHTTPStatusError(httpx.HTTPStatusError):
|
|
5
|
+
"""HTTP error that may be retried."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiResponseError(Exception):
|
|
9
|
+
"""API error with description."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
code: str,
|
|
15
|
+
description: str | None,
|
|
16
|
+
) -> None:
|
|
17
|
+
message = f"API error [{code}]: {description}"
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RetryApiResponseError(ApiResponseError):
|
|
22
|
+
"""API error that may be retried."""
|
b24api/future.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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
|
b24api/http.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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)
|
|
10
|
+
yield client
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
HttpxClient = Annotated[Client, Depends(httpx_client)]
|
b24api/query.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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)
|
b24api/settings.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from fast_depends import Depends
|
|
5
|
+
from httpx import codes
|
|
6
|
+
from pydantic import HttpUrl
|
|
7
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Settings(BaseSettings):
|
|
11
|
+
model_config = SettingsConfigDict(
|
|
12
|
+
env_prefix="bitrix_24_api_",
|
|
13
|
+
env_file=".env",
|
|
14
|
+
extra="ignore",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
webhook_url: HttpUrl
|
|
18
|
+
|
|
19
|
+
retry_statuses: list[int] = (
|
|
20
|
+
codes.LOCKED,
|
|
21
|
+
codes.TOO_EARLY,
|
|
22
|
+
codes.TOO_MANY_REQUESTS,
|
|
23
|
+
codes.INTERNAL_SERVER_ERROR,
|
|
24
|
+
codes.BAD_GATEWAY,
|
|
25
|
+
codes.SERVICE_UNAVAILABLE,
|
|
26
|
+
codes.INSUFFICIENT_STORAGE,
|
|
27
|
+
)
|
|
28
|
+
retry_errors: list[str] = ["query_limit_exceeded", "operation_time_limit"]
|
|
29
|
+
|
|
30
|
+
retry_tries: int = 5
|
|
31
|
+
retry_delay: float = 5
|
|
32
|
+
retry_backoff: float = 2
|
|
33
|
+
|
|
34
|
+
list_size: int = 50
|
|
35
|
+
batch_size: int = 50
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def api_settings() -> Generator[Settings, None, None]:
|
|
39
|
+
settings = Settings()
|
|
40
|
+
yield settings
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
ApiSettings = Annotated[Settings, Depends(api_settings)]
|
b24api/type.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Shkarupa Alex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: b24api
|
|
3
|
+
Version: 0.1.2
|
|
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.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
b24api/__init__.py,sha256=EITmZQavNFXCJPFd53sF3imOBajCJnV9bjDrnL-oQdU,31
|
|
2
|
+
b24api/api.py,sha256=QZ1p0vzGf8AFNnbd0irxo2FoBtvX-pTPd2Sb9hQLGJQ,10483
|
|
3
|
+
b24api/entity.py,sha256=3G1EQgxOsmmlXzwVvlNENBKVX-R8nHWb_-gpfyFva6s,2146
|
|
4
|
+
b24api/error.py,sha256=1-3uLKYxYnJyg140vAl-861_SMWr1fh1jbJYN3-HyKY,473
|
|
5
|
+
b24api/future.py,sha256=BJdIfHXDX1IEJCk9X9mcDXGiLXIs-SK5G2RNDMXTL2I,420
|
|
6
|
+
b24api/http.py,sha256=kAhbbgw3LeQqQ7vYjL8WicHBejPDJJzzmVOhb5aTAY4,287
|
|
7
|
+
b24api/query.py,sha256=0M_zjfapNyCdeEOQsy9iA1kUgX2BxH0egnq8dV9kjoQ,926
|
|
8
|
+
b24api/settings.py,sha256=v7IlEXAYmLkVM1zRAw8mp5raNQ88s5HM3AlzuxhkY9Q,1034
|
|
9
|
+
b24api/type.py,sha256=fRhONZS9DCLjuHF6KkqsEtY7tLvzN-vtgY9TnghCw3c,179
|
|
10
|
+
b24api-0.1.2.dist-info/LICENSE,sha256=ssnbHJpzi6Jw6vLkOfnUnF9fLjKIRWGRD3zWBCWYj9k,1070
|
|
11
|
+
b24api-0.1.2.dist-info/METADATA,sha256=OkNLTgf24-DBqFvFO55QCZXWhpS-n38emxs2Pya_WL8,426
|
|
12
|
+
b24api-0.1.2.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
13
|
+
b24api-0.1.2.dist-info/top_level.txt,sha256=mN7BLg3tL8GQgfBzSoJhjGgXqwKaflyMHCt3XDWIWiI,7
|
|
14
|
+
b24api-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b24api
|