Skip to content

Commit 48bad0b

Browse files
committed
Port rate limit and pagination features from old version
1 parent 185fc20 commit 48bad0b

File tree

4 files changed

+106
-22
lines changed

4 files changed

+106
-22
lines changed

bitrix24/bitrix24.py

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
import asyncio
88
import itertools
9-
from typing import Any, Dict, Optional
9+
import ssl
10+
import warnings
11+
from typing import Any, Dict
1012
from urllib.parse import urlparse
1113

12-
from aiohttp import ClientSession
14+
from aiohttp import ClientSession, TCPConnector
1315

1416
from .exceptions import BitrixError
1517

@@ -21,28 +23,40 @@ class Bitrix24:
2123
Provides an easy way to communicate with Bitrix24 portal over REST without OAuth.
2224
"""
2325

24-
def __init__(self, domain: str, timeout: int = 60):
26+
def __init__(
27+
self,
28+
domain: str,
29+
timeout: int = 60,
30+
safe: bool = True,
31+
fetch_all_pages: bool = True,
32+
retry_after: int = 3,
33+
):
2534
"""
2635
Create Bitrix24 API object.
2736
2837
Parameters
2938
----------
3039
domain (str): Bitrix24 webhook domain
3140
timeout (int): Timeout for API request in seconds
41+
safe (bool): Set to `False` to ignore the certificate verification
42+
fetch_all_pages (bool): Fetch all pages for paginated requests
43+
retry_after (int): Retry after seconds for QUERY_LIMIT_EXCEEDED error
3244
"""
33-
self.domain = self._prepare_domain(domain)
34-
self.timeout = timeout
45+
self._domain = self._prepare_domain(domain)
46+
self._timeout = int(timeout)
47+
self._fetch_all_pages = bool(fetch_all_pages)
48+
self._retry_after = int(retry_after)
49+
self._verify_ssl = bool(safe)
3550

3651
def _prepare_domain(self, domain: str) -> str:
3752
"""Normalize user passed domain to a valid one."""
38-
if not domain:
39-
raise BitrixError("Empty domain")
40-
4153
o = urlparse(domain)
54+
if not o.scheme or not o.netloc:
55+
raise BitrixError("Not a valid domain. Please provide a valid domain.")
4256
user_id, code = o.path.split("/")[2:4]
4357
return "{0}://{1}/rest/{2}/{3}".format(o.scheme, o.netloc, user_id, code)
4458

45-
def _prepare_params(self, params: Dict[str, Any], prev="") -> str:
59+
def _prepare_params(self, params: Dict[str, Any], prev: str = "") -> str:
4660
"""
4761
Transform list of parameters to a valid bitrix array.
4862
@@ -81,41 +95,50 @@ def _prepare_params(self, params: Dict[str, Any], prev="") -> str:
8195
return ret
8296

8397
async def request(self, method: str, params: str = None) -> Dict[str, Any]:
84-
async with ClientSession() as session:
98+
ssl_context = ssl.create_default_context()
99+
if not self._verify_ssl:
100+
ssl_context.check_hostname = False
101+
ssl_context.verify_mode = ssl.CERT_NONE
102+
async with ClientSession(connector=TCPConnector(ssl=ssl_context)) as session:
85103
async with session.get(
86-
f"{self.domain}/{method}.json", params=params, timeout=self.timeout
104+
f"{self._domain}/{method}.json", params=params, timeout=self._timeout
87105
) as resp:
88106
if resp.status not in [200, 201]:
89107
raise BitrixError(f"HTTP error: {resp.status}")
90108
response = await resp.json()
91109
if "error" in response:
110+
if response["error"] == "QUERY_LIMIT_EXCEEDED":
111+
await asyncio.sleep(self._retry_after)
112+
return await self.request(method, params)
92113
raise BitrixError(response["error_description"], response["error"])
93114
return response
94115

95-
async def call(self, method: str, params: Dict[str, Any] = {}, start: Optional[int] = None):
116+
async def _call(
117+
self, method: str, params: Dict[str, Any] = {}, start: int = 0
118+
) -> Dict[str, Any]:
96119
"""Async call a REST method with specified parameters.
97120
98-
This method is a replacement for the callMethod method, which is synchronous.
99-
100121
Parameters
101122
----------
102123
method (str): REST method name
103124
params (dict): Optional arguments which will be converted to a POST request string
125+
start (int): Offset for pagination
104126
"""
105-
if start is not None:
106-
params["start"] = start
127+
params["start"] = start
107128

108129
payload = self._prepare_params(params)
109130
res = await self.request(method, payload)
110131

111-
if "next" in res and start is None:
112-
tasks = [self.call(method, params, start=start) for start in range(res["total"] // 50)]
132+
if "next" in res and not start and self._fetch_all_pages:
133+
tasks = [
134+
self._call(method, params, (s + 1) * 50) for s in range(res["total"] // 50 - 1)
135+
]
113136
items = await asyncio.gather(*tasks)
114137
result = list(itertools.chain(*items))
115138
return res["result"] + result
116139
return res["result"]
117140

118-
def callMethod(self, method: str, params: Dict[str, Any] = {}, **kwargs):
141+
def callMethod(self, method: str, params: Dict[str, Any] = {}, **kwargs) -> Dict[str, Any]:
119142
"""Call a REST method with specified parameters.
120143
121144
Parameters
@@ -133,10 +156,16 @@ def callMethod(self, method: str, params: Dict[str, Any] = {}, **kwargs):
133156
try:
134157
loop = asyncio.get_running_loop()
135158
except RuntimeError:
159+
warnings.warn(
160+
"You are using `callMethod` method in a synchronous way. "
161+
"Starting from version 3, this method will be completly asynchronous."
162+
"Please consider updating your code",
163+
DeprecationWarning,
164+
)
136165
loop = asyncio.new_event_loop()
137166
asyncio.set_event_loop(loop)
138-
result = loop.run_until_complete(self.call(method, params or kwargs))
167+
result = loop.run_until_complete(self._call(method, params or kwargs))
139168
loop.close()
140169
else:
141-
result = asyncio.ensure_future(self.call(method, params or kwargs))
170+
result = asyncio.ensure_future(self._call(method, params or kwargs))
142171
return result

tests/test_async_call_method.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
async def test_async_call_method(b24: Bitrix24):
99
with aioresponses() as m:
1010
m.get(
11-
"https://example.bitrix24.com/rest/1/123456789/user.get.json?ID=1",
11+
"https://example.bitrix24.com/rest/1/123456789/user.get.json?ID=1&start=0",
1212
payload={"result": [{"ID": 1}]},
1313
status=200,
1414
)

tests/test_pagination.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
from aioresponses import aioresponses
3+
from bitrix24 import Bitrix24
4+
5+
6+
@pytest.mark.asyncio
7+
async def test_concurrent_requests(b24):
8+
with aioresponses() as m:
9+
m.get(
10+
"https://example.bitrix24.com/rest/1/123456789/crm.deal.list.json?start=0",
11+
payload={"result": [{"ID": 1}], "next": 50, "total": 100},
12+
status=200,
13+
repeat=True,
14+
)
15+
m.get(
16+
"https://example.bitrix24.com/rest/1/123456789/crm.deal.list.json?start=50",
17+
payload={"result": [{"ID": 2}], "total": 100},
18+
status=200,
19+
repeat=True,
20+
)
21+
res = await b24.callMethod("crm.deal.list")
22+
assert res == [{"ID": 1}, {"ID": 2}]
23+
24+
25+
@pytest.mark.asyncio
26+
async def test_request_with_disabled_pagination():
27+
b24 = Bitrix24("https://example.bitrix24.com/rest/1/123456789", fetch_all_pages=False)
28+
with aioresponses() as m:
29+
m.get(
30+
"https://example.bitrix24.com/rest/1/123456789/crm.deal.list.json?start=0",
31+
payload={"result": [{"ID": 1}], "next": 50, "total": 100},
32+
status=200,
33+
repeat=True,
34+
)
35+
res = await b24.callMethod("crm.deal.list")
36+
assert res == [{"ID": 1}]

tests/test_request_retry.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
from aioresponses import aioresponses
3+
4+
5+
@pytest.mark.asyncio
6+
async def test_rate_limit_exceeded(b24):
7+
with aioresponses() as m:
8+
m.get(
9+
"https://example.bitrix24.com/rest/1/123456789/crm.deal.list.json?start=0",
10+
payload={"error": "QUERY_LIMIT_EXCEEDED"},
11+
status=200,
12+
)
13+
m.get(
14+
"https://example.bitrix24.com/rest/1/123456789/crm.deal.list.json?start=0",
15+
payload={"result": [{"ID": 1}], "total": 100},
16+
status=200
17+
)
18+
res = await b24.callMethod("crm.deal.list")
19+
assert res == [{"ID": 1}]

0 commit comments

Comments
 (0)