Repository: leshchenko1979/fast_bitrix24
Pull request #221 (2024-03-18 15:04:50)

Fix server response parsing

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/leshchenko1979/fast_bitrix24/pull/221?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alexey+Leshchenko) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.90%. Comparing base [(`5b62295`)](https://app.codecov.io/gh/leshchenko1979/fast_bitrix24/commit/5b62295ea5b9014f587e826d3789acfbe04c4cd4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alexey+Leshchenko) to head [(`38ca64f`)](https://app.codecov.io/gh/leshchenko1979/fast_bitrix24/pull/221?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alexey+Leshchenko).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #221      +/-   ##
==========================================
+ Coverage   88.55%   88.90%   +0.35%     
==========================================
  Files          11       11              
  Lines         559      559              
==========================================
+ Hits          495      497       +2     
+ Misses         64       62       -2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/leshchenko1979/fast_bitrix24/pull/221?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alexey+Leshchenko).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alexey+Leshchenko).


Patch:
diff --git a/fast_bitrix24/mult_request.py b/fast_bitrix24/mult_request.py
index b28b8cd..5fdd8b0 100644
--- a/fast_bitrix24/mult_request.py
+++ b/fast_bitrix24/mult_request.py
@@ -30,31 +30,28 @@ class MultipleServerRequestHandler:
         self.get_by_ID = get_by_ID
 
         self.results = None
-        self.task_iterator = self.generate_a_task()
+        self.task_iterator = self.generate_tasks()
         self.tasks = set()
 
-    def generate_a_task(self):
-        """Объединяем элементы item_list в батчи и по одной создаем и
-        возвращаем задачи asyncio с запросами к серверу для каждого батча."""
+    def generate_tasks(self):
+        """Group items in batches and create asyncio tasks for each batch"""
 
         batches = (
-            {
-                "halt": 0,
-                "cmd": {
-                    self.batch_command_label(
-                        i, item
-                    ): f"{self.method}?{http_build_query(item)}"
-                    for i, item in enumerate(next_batch)
-                },
-            }
-            for next_batch in chunked(self.item_list, BITRIX_MAX_BATCH_SIZE)
+            self.package_batch(chunk)
+            for chunk in chunked(self.item_list, BITRIX_MAX_BATCH_SIZE)
         )
 
         for batch in batches:
             yield ensure_future(self.srh.single_request("batch", batch))
 
-    def batch_command_label(self, i: int, item) -> str:
-        return f"cmd{i:010}"
+    def package_batch(self, chunk):
+        return {
+            "halt": 0,
+            "cmd": {
+                f"cmd{i:010}": f"{self.method}?{http_build_query(item)}"
+                for i, item in enumerate(chunk)
+            },
+        }
 
     async def run(self) -> Union[Dict, List]:
         self.top_up_tasks()
diff --git a/fast_bitrix24/server_response.py b/fast_bitrix24/server_response.py
index 01acf99..1f22bfa 100644
--- a/fast_bitrix24/server_response.py
+++ b/fast_bitrix24/server_response.py
@@ -45,11 +45,13 @@ class ServerResponseParser:
         if not self.is_batch():
             return self.extract_from_single_response(self.result)
 
-        if self.get_by_ID:
-            return self.extract_from_single_response(self.result["result"])
-        else:
+        if not self.get_by_ID:
             return self.extract_from_batch_response(self.result["result"])
 
+        extracted = self.extract_from_single_response(self.result["result"])
+
+        return extracted[0] if isinstance(extracted, list) else extracted
+
     def raise_for_errors(self):
         errors = self.extract_errors()
         if errors:
@@ -67,8 +69,7 @@ class ServerResponseParser:
     def is_batch(self) -> bool:
         return isinstance(self.result, dict) and "result" in self.result
 
-    @staticmethod
-    def extract_from_single_response(result: dict):
+    def extract_from_single_response(self, result: dict):
         # если результат вызова содержит только словарь {ключ: список},
         # то вернуть этот список.
         # См. https://github.com/leshchenko1979/fast_bitrix24/issues/132
@@ -76,8 +77,8 @@ class ServerResponseParser:
         # метод `crm.stagehistory.list` возвращает dict["items", list] --
         # разворачиваем его в список
 
-        if ServerResponseParser.is_nested(result):
-            contents = ServerResponseParser.extract_from_nested(result)
+        if self.is_nested(result):
+            contents = self.extract_from_nested(result)
             if isinstance(contents, list):
                 return contents
 
@@ -88,7 +89,7 @@ class ServerResponseParser:
         return isinstance(result, dict) and len(result) == 1
 
     @staticmethod
-    def extract_from_nested(result):
+    def extract_from_nested(result: dict):
         return next(iter(result.values()))
 
 


Test patch:
diff --git a/tests/real_responses/crm_dealcategory_stage_list_batch_of_one.py b/tests/real_responses/crm_dealcategory_stage_list_batch_of_one.py
new file mode 100644
index 0000000..c896c88
--- /dev/null
+++ b/tests/real_responses/crm_dealcategory_stage_list_batch_of_one.py
@@ -0,0 +1,50 @@
+request = {"halt": 0, "cmd": {0: "crm.dealcategory.stage.list?ID=0&"}}
+response = {
+    "result": {
+        "result": [
+            [
+                {"NAME": "Новая заявка", "SORT": 10, "STATUS_ID": "NEW"},
+                {"NAME": "потенциальные", "SORT": 20, "STATUS_ID": "PREPARATION"},
+                {
+                    "NAME": "Запись на прием",
+                    "SORT": 30,
+                    "STATUS_ID": "PREPAYMENT_INVOICE",
+                },
+                {"NAME": "Прием состоялся", "SORT": 40, "STATUS_ID": "EXECUTING"},
+                {
+                    "NAME": "Запись на последующий прием",
+                    "SORT": 50,
+                    "STATUS_ID": "FINAL_INVOICE",
+                },
+                {"NAME": "неотвеченные", "SORT": 60, "STATUS_ID": "WON"},
+                {"NAME": "удалить столбик-лишний", "SORT": 70, "STATUS_ID": "LOSE"},
+                {"NAME": "спам", "SORT": 80, "STATUS_ID": "APOLOGY"},
+            ]
+        ],
+        "result_error": [],
+        "result_total": [],
+        "result_next": [],
+        "result_time": [
+            {
+                "start": 1670596304.721726,
+                "finish": 1670596304.729467,
+                "duration": 0.007740974426269531,
+                "processing": 0.006625175476074219,
+                "date_start": "2022-12-09T17:31:44+03:00",
+                "date_finish": "2022-12-09T17:31:44+03:00",
+                "operating_reset_at": 1670596904,
+                "operating": 0,
+            }
+        ],
+    },
+    "time": {
+        "start": 1670596304.683407,
+        "finish": 1670596304.731127,
+        "duration": 0.04771995544433594,
+        "processing": 0.009470939636230469,
+        "date_start": "2022-12-09T17:31:44+03:00",
+        "date_finish": "2022-12-09T17:31:44+03:00",
+        "operating_reset_at": 1670596904,
+        "operating": 0,
+    },
+}
diff --git a/tests/test_server_responses.py b/tests/test_server_responses.py
index 331613a..4fc0f06 100644
--- a/tests/test_server_responses.py
+++ b/tests/test_server_responses.py
@@ -183,6 +183,17 @@ def test_crm_dealcategory_stage_list(bx_dummy):
     results = bx_dummy.get_by_ID("crm.dealcategory.stage.list", ["2", "4", "8", 0])
     assert isinstance(results, dict)
 
+
+def test_crm_dealcategory_stage_list_batch_of_one(bx_dummy):
+    from tests.real_responses.crm_dealcategory_stage_list_batch_of_one import response
+
+    bx_dummy.srh = MockSRH(response)
+    results = bx_dummy.get_by_ID("crm.dealcategory.stage.list", [0])
+
+    # should return a list of dicts
+    assert isinstance(results, list) and isinstance(results[0], dict)
+
+
 def test_catalog_document_element_list(bx_dummy):
     from tests.real_responses.catalog_document_element_list import response