Skip to content

Commit

Permalink
Fix 1.5.5+ Batch сломался (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
leshchenko1979 authored Jun 7, 2022
1 parent 8749028 commit 24dca59
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 39 deletions.
2 changes: 1 addition & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ all_lead_info = b.list_and_get('crm.lead')

* `items: dict | Iterable[dict]` - параметры вызываемого метода. Может быть списком, и тогда метод будет вызван для каждого элемента списка, а может быть одним словарем параметров для единичного вызова.

* `raw: bool = False` - если `True`, то `items` воспринимается как один элемент (даже если в `items` был передан список) и не заворачивается в батч. Требуется для работы со старыми методами, принимающими на вход параметры списком (`tasks.elapseditem.*`), а также для передачи значений `None` (которые плохо обрабатываются в батче). Подробней см. [PR #158](https://github.com/leshchenko1979/fast_bitrix24/pull/158). По умолчанию - `False`.
* `raw: bool = False` - если `True`, то `items` воспринимается как один элемент (даже если в `items` был передан список) и не заворачивается в батч. Требуется для работы со старыми методами, принимающими на вход параметры списком (`tasks.elapseditem.*`), а также для передачи значений `None` (которые плохо обрабатываются в батче). Подробней см. [PR #158](https://github.com/leshchenko1979/fast_bitrix24/pull/158). При этом возвращается необработанный ответ сервера в виде словаря. По умолчанию - `False`.

Если `raw=False`, то `call()` вызывает `method`, последовательно подставляя в параметры запроса все элементы `items`, и возвращает список ответов сервера для каждого из отправленных запросов. При этом запросы к Битриксу группируются в батчи. Либо, если `items` - не список, а словарь с параметрами, то происходит единичный вызов и возвращается его результат.

Expand Down
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,11 @@ b.call('crm.deal.update', tasks)
Метод [`call()`](API.md#метод-callself-method-str-items-dict--iterabledict--any--none--raw-bool--false---dict--listdict--any) возвращает список ответов сервера по каждому элементу переданного списка.

### call(raw=True)
Рекомендуется использовать метод `call(raw=True)` в следующих случаях:
- для вызова методов типа `crm.lead.fields` или `crm.deal.fields`, которые не требуют дополнительных параметров и возвращают словарь (`dict`), а не список (`list`),
- для отправки запросов, в параметрах которых есть `None` (для стирания значения полей).
Вызов `call` с парамтером `raw=True` отправляет на сервер переданные ему параметры в оригинальном, необработанном виде (пропуская упаковку в батчи), и возвращает ответ сервера без какой-либо обработки.

```python
# вернуть dict с полями лида
b.call('crm.lead.fields', raw=True)
Подобный вызов можно использовать в отладочных целях, но кроме того, придется его использовать для отправки запросов, в параметрах которых есть `None` (None применяется для стирания значения полей, а упаковка в батчи мешает передавать `None`).

```python
# стереть DESCRIPTION в лиде 123
params = {"ID": 123, "fields": {"DESCRIPTION": None}}
b.call('crm.lead.update', params, raw=True)
Expand Down
16 changes: 12 additions & 4 deletions fast_bitrix24/bitrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@
from typing import Iterable, Union

import aiohttp
from beartype import beartype
import icontract
from beartype import beartype

from . import correct_asyncio
from .logger import log, logger
from .server_response import ServerResponseParser
from .srh import ServerRequestHandler
from .user_request import (
BatchUserRequest,
CallUserRequest,
GetAllUserRequest,
GetByIDUserRequest,
ListAndGetUserRequest,
RawCallUserRequest,
)
from .logger import log, logger


class BitrixAsync:
Expand Down Expand Up @@ -179,7 +179,15 @@ async def call_batch(self, params: dict) -> dict:
команды, а значение - ответ сервера по этой команде.
"""

return await self.srh.run_async(BatchUserRequest(self, params).run())
response = ServerResponseParser(
await self.srh.run_async(RawCallUserRequest(self, "batch", params).run())
)

errors = response.extract_errors()
if errors:
raise RuntimeError(errors)

return response.result["result"]

@contextmanager
@beartype
Expand Down
18 changes: 14 additions & 4 deletions fast_bitrix24/server_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from beartype.typing import Dict, List, Union


class ServerResponseParser:
def __init__(self, response: dict):
self.response = response
Expand All @@ -26,6 +27,15 @@ def error_description(self):
def result_error(self):
return self.response.get("result_error")

def extract_errors(self):
if self.is_batch():
if self.result.get("result_error"):
return self.result["result_error"]
elif self.result_error:
return self.result_error

return None

def extract_results(self) -> Union[Dict, List[Dict]]:
"""Вернуть результаты запроса.
Expand All @@ -35,13 +45,13 @@ def extract_results(self) -> Union[Dict, List[Dict]]:
Returns:
Any: Результаты запроса, по возможности превращенные в плоский список.
"""
errors = self.extract_errors()
if errors:
raise RuntimeError(errors)

if self.is_batch():
if self.result.get("result_error"):
raise RuntimeError(self.result["result_error"])
return self.extract_from_batch_response(self.result["result"])
else:
if self.result_error:
raise RuntimeError(self.result_error)
return self.extract_from_single_response(self.result)

def is_batch(self) -> bool:
Expand Down
30 changes: 8 additions & 22 deletions fast_bitrix24/user_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ def __init__(self, bitrix, method: str, item_list: Union[Dict, Iterable[Dict]]):
@icontract.require(lambda self: self.item_list, "call(): item_list can't be empty")
@icontract.require(
lambda self: not self.bitrix.verbose
or not self.ID_list or "__len__" in dir(self.ID_list),
or not self.ID_list
or "__len__" in dir(self.ID_list),
"call(): 'ID_list' should be a Sequence "
"if a progress bar is to be displayed",
)
Expand Down Expand Up @@ -277,31 +278,16 @@ def standardized_params(self, p):
"""Пропускаем все проверки и изменения параметров."""
return p

def check_special_limitations(self):
return True


class BatchUserRequest(UserRequestAbstract):
@beartype
@icontract.require(lambda params: params, "call_batch(): params can't be empty")
def __init__(self, bitrix, params: Dict):
super().__init__(bitrix, "batch", params)

def standardized_method(self, method):
return "batch"
def standardized_method(self, method: str):
"""Пропускаем все проверки и изменения методов."""
return method

@icontract.require(
lambda self: self.st_params.keys() == {"HALT", "CMD"},
"call_batch(): params should contain only 'halt' and 'cmd' "
"clauses at the highest level",
)
@icontract.require(
lambda self: isinstance(self.st_params["CMD"], dict),
"call_batch(): 'cmd' clause should contain a dict",
)
def check_special_limitations(self):
return True

async def run(self):
return await self.srh.single_request(self.method, self.params)


class ListAndGetUserRequest:
@beartype
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setuptools.setup(
name="fast_bitrix24",
version="1.5.6",
version="1.5.7",
author="Alexey Leshchenko",
author_email="[email protected]",
description='API wrapper для быстрого получения данных от Битрикс24 через '
Expand Down
64 changes: 64 additions & 0 deletions tests/real_responses/call_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
response = {
"result": {
"result": {
"statuses": [
{"NAME": "Новый лид", "SORT": 10, "STATUS_ID": "NEW"},
{"NAME": "Рубрикация лида", "SORT": 20, "STATUS_ID": "IN_PROCESS"},
{
"NAME": "Назначение ответственного",
"SORT": 30,
"STATUS_ID": "PROCESSED",
},
{"NAME": "Повторный контакт", "SORT": 40, "STATUS_ID": "1"},
{"NAME": "Качественный лид", "SORT": 50, "STATUS_ID": "CONVERTED"},
{"NAME": "Некачественный лид", "SORT": 60, "STATUS_ID": "JUNK"},
{"NAME": "Принадлежит МП вне BX24", "SORT": 70, "STATUS_ID": "2"},
{"NAME": "Задвоенные лиды", "SORT": 80, "STATUS_ID": "3"},
{"NAME": "Внутренняя почта", "SORT": 90, "STATUS_ID": "4"},
{
"NAME": "Сперва позвонили, а потом завели контакт и компанию",
"SORT": 100,
"STATUS_ID": "5",
},
],
"sources": [
{"NAME": "Портальная СРМ", "SORT": 10, "STATUS_ID": "CALL"},
{"NAME": "Электронная почта", "SORT": 20, "STATUS_ID": "WEBFORM"},
{"NAME": "Входящий звонок", "SORT": 30, "STATUS_ID": "CALLBACK"},
{"NAME": "Письмо (бумажное)", "SORT": 40, "STATUS_ID": "RC_GENERATOR"},
{"NAME": "Задача от маркетинга", "SORT": 50, "STATUS_ID": "STORE"},
{"NAME": "Иное", "SORT": 60, "STATUS_ID": "1"},
{"NAME": "Запрос с сайта", "SORT": 70, "STATUS_ID": "2"},
],
},
"result_error": [],
"result_total": [],
"result_next": [],
"result_time": {
"statuses": {
"start": 1654518286.547828,
"finish": 1654518286.55755,
"duration": 0.009721994400024414,
"processing": 0.009358882904052734,
"date_start": "2022-06-06T15:24:46+03:00",
"date_finish": "2022-06-06T15:24:46+03:00",
},
"sources": {
"start": 1654518286.55769,
"finish": 1654518286.558538,
"duration": 0.0008480548858642578,
"processing": 0.0006170272827148438,
"date_start": "2022-06-06T15:24:46+03:00",
"date_finish": "2022-06-06T15:24:46+03:00",
},
},
},
"time": {
"start": 1654518286.426799,
"finish": 1654518286.558576,
"duration": 0.13177704811096191,
"processing": 0.010885000228881836,
"date_start": "2022-06-06T15:24:46+03:00",
"date_finish": "2022-06-06T15:24:46+03:00",
},
}
31 changes: 31 additions & 0 deletions tests/test_server_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,34 @@ def test_get_all_non_list_method(bx_dummy):
bx_dummy.srh = MockSRH(response)
results = bx_dummy.get_all("user.fields")
assert isinstance(results, dict)


def test_batch_and_call_raw(bx_dummy):
from tests.real_responses.call_batch import response

bx_dummy.srh = MockSRH(response)
results = bx_dummy.call_batch(
{
"halt": 0,
"cmd": {
"statuses": "crm.status.entity.items?entityId=STATUS",
"sources": "crm.status.entity.items?entityId=SOURCE",
},
}
)
assert isinstance(results, dict)
assert results.keys() == {"statuses", "sources"}

bx_dummy.srh = MockSRH(response)
results = bx_dummy.call("batch",
{
"halt": 0,
"cmd": {
"statuses": "crm.status.entity.items?entityId=STATUS",
"sources": "crm.status.entity.items?entityId=SOURCE",
},
},
raw=True
)
assert isinstance(results, dict)
assert results.keys() == {"result", "time"}
2 changes: 1 addition & 1 deletion tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def test_call(self, bx_dummy, monkeypatch):
b = bx_dummy

async def stub(*args, **kwargs):
return {"result": "ok"}
return {"result": {"result": {"ok"}}}

monkeypatch.setattr(b.srh, "request_attempt", stub)
assert b.srh.request_attempt is stub
Expand Down

0 comments on commit 24dca59

Please sign in to comment.