Skip to content

Commit 5d55a1a

Browse files
committed
Add support for async method calls and optimise large list fetching
1 parent 7a8db13 commit 5d55a1a

File tree

4 files changed

+71
-57
lines changed

4 files changed

+71
-57
lines changed

bitrix24/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@
77
from .bitrix24 import Bitrix24
88
from .exceptions import BitrixError
99

10-
__version__ = "2.0.0"
1110
__all__ = ["Bitrix24", "BitrixError"]

bitrix24/bitrix24.py

Lines changed: 53 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
# | |_) | | |_| | | |> < / __/|__ _| | _ <| |___ ___) || |
55
# |____/|_|\__|_| |_/_/\_\_____| |_| |_| \_\_____|____/ |_|
66

7-
import warnings
8-
from time import sleep
9-
from typing import Any, Dict
7+
import asyncio
8+
import itertools
9+
from typing import Any, Dict, Optional
1010
from urllib.parse import urlparse
1111

12-
import requests
12+
from aiohttp import ClientSession
1313

1414
from .exceptions import BitrixError
1515

@@ -21,7 +21,7 @@ class Bitrix24:
2121
Provides an easy way to communicate with Bitrix24 portal over REST without OAuth.
2222
"""
2323

24-
def __init__(self, domain: str, timeout: int = 60, safe: bool = True):
24+
def __init__(self, domain: str, timeout: int = 60):
2525
"""
2626
Create Bitrix24 API object.
2727
@@ -32,9 +32,8 @@ def __init__(self, domain: str, timeout: int = 60, safe: bool = True):
3232
"""
3333
self.domain = self._prepare_domain(domain)
3434
self.timeout = timeout
35-
self.safe = safe
3635

37-
def _prepare_domain(self, domain: str):
36+
def _prepare_domain(self, domain: str) -> str:
3837
"""Normalize user passed domain to a valid one."""
3938
if not domain:
4039
raise BitrixError("Empty domain")
@@ -43,7 +42,7 @@ def _prepare_domain(self, domain: str):
4342
user_id, code = o.path.split("/")[2:4]
4443
return "{0}://{1}/rest/{2}/{3}".format(o.scheme, o.netloc, user_id, code)
4544

46-
def _prepare_params(self, params: Dict[str, Any], prev=""):
45+
def _prepare_params(self, params: Dict[str, Any], prev="") -> str:
4746
"""
4847
Transform list of parameters to a valid bitrix array.
4948
@@ -81,26 +80,42 @@ def _prepare_params(self, params: Dict[str, Any], prev=""):
8180
ret += "{0}={1}&".format(key, value)
8281
return ret
8382

84-
def request(self, method, p):
85-
url = "{0}/{1}.json".format(self.domain, method)
86-
if method.rsplit(".", 1)[0] in ["add", "update", "delete", "set"]:
87-
r = requests.post(url, data=p, timeout=self.timeout, verify=self.safe).json()
88-
else:
89-
r = requests.get(url, params=p, timeout=self.timeout, verify=self.safe)
90-
try:
91-
r = r.json()
92-
except requests.exceptions.JSONDecodeError:
93-
warnings.warn("bitrix24: JSON decode error...")
94-
if r.status_code == 403:
95-
warnings.warn(
96-
f"bitrix24: Forbidden: {method}. "
97-
"Check your bitrix24 webhook settings. Returning None! "
98-
)
99-
return None
100-
elif r.ok:
101-
return r.content
102-
103-
def callMethod(self, method: str, **params):
83+
async def request(self, method: str, params: str = None) -> Dict[str, Any]:
84+
async with ClientSession() as session:
85+
async with session.get(
86+
f"{self.domain}/{method}.json", params=params, timeout=self.timeout
87+
) as resp:
88+
if resp.status not in [200, 201]:
89+
raise BitrixError(f"HTTP error: {resp.status}")
90+
response = await resp.json()
91+
if "error" in response:
92+
raise BitrixError(response["error_description"], response["error"])
93+
return response
94+
95+
async def call(self, method: str, params: Dict[str, Any] = {}, start: Optional[int] = None):
96+
"""Async call a REST method with specified parameters.
97+
98+
This method is a replacement for the callMethod method, which is synchronous.
99+
100+
Parameters
101+
----------
102+
method (str): REST method name
103+
params (dict): Optional arguments which will be converted to a POST request string
104+
"""
105+
if start is not None:
106+
params["start"] = start
107+
108+
payload = self._prepare_params(params)
109+
res = await self.request(method, payload)
110+
111+
if "next" in res and start is None:
112+
tasks = [self.call(method, params, start=start) for start in range(res["total"] // 50)]
113+
items = await asyncio.gather(*tasks)
114+
result = list(itertools.chain(*items))
115+
return res["result"] + result
116+
return res["result"]
117+
118+
def callMethod(self, method: str, params: Dict[str, Any] = {}, **kwargs):
104119
"""Call a REST method with specified parameters.
105120
106121
Parameters
@@ -112,32 +127,16 @@ def callMethod(self, method: str, **params):
112127
-------
113128
Returning the REST method response as an array, an object or a scalar
114129
"""
115-
if not method or len(method.split(".")) < 3:
130+
if not method or len(method.split(".")) < 2:
116131
raise BitrixError("Wrong method name", 400)
117132

118133
try:
119-
p = self._prepare_params(params)
120-
r = self.request(method, p)
121-
if not r:
122-
return None
123-
except ValueError:
124-
if r["error"] not in "QUERY_LIMIT_EXCEEDED":
125-
raise BitrixError(message=r["error_description"], code=r["error"])
126-
# Looks like we need to wait until expires limitation time by Bitrix24 API
127-
sleep(2)
128-
return self.callMethod(method, **params)
129-
130-
if "error" in r:
131-
raise BitrixError(r)
132-
if "start" not in params:
133-
params["start"] = 0
134-
if "next" in r and r["total"] > params["start"]:
135-
params["start"] += 50
136-
data = self.callMethod(method, **params)
137-
if isinstance(r["result"], dict):
138-
result = r["result"].copy()
139-
result.update(data)
140-
else:
141-
result = r["result"] + data
142-
return result
143-
return r["result"]
134+
loop = asyncio.get_running_loop()
135+
except RuntimeError:
136+
loop = asyncio.new_event_loop()
137+
asyncio.set_event_loop(loop)
138+
result = loop.run_until_complete(self.call(method, params or kwargs))
139+
loop.close()
140+
else:
141+
result = asyncio.ensure_future(self.call(method, params or kwargs))
142+
return result

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
from distutils.core import setup
88
from os import path
99
from setuptools import find_packages
10-
from bitrix24 import __version__
1110

1211
dir = path.abspath(path.dirname(__file__))
1312

1413
setup(
1514
name="bitrix24-rest",
16-
version=__version__,
15+
version="2.0.0",
1716
packages=find_packages(),
1817
install_requires=[
1918
"aiohttp",
@@ -28,6 +27,7 @@
2827
"pytest",
2928
"pytest-cov",
3029
"pytest-asyncio",
30+
"aioresponses",
3131
"pytest-aiohttp"
3232
],
3333
},

tests/test_async_call_method.py

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

0 commit comments

Comments
 (0)