├── .coveragerc ├── .flake8 ├── .github ├── FUNDING.yml └── workflows │ ├── main.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── bitrix24 ├── __init__.py ├── bitrix24.py └── exceptions.py ├── docs ├── disabling-certificate-verification.md ├── using-filters-and-additional-parameters.md └── working-with-large-datasets.md ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── test_async_call_method.py ├── test_bitrix24.py ├── test_pagination.py ├── test_params_preparation.py └── test_request_retry.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | tests/* 4 | setup.py 5 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | max-complexity = 10 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [akopdev] 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Verify 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Python 3.10 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: "3.10" 26 | - name: Setup environment 27 | run: | 28 | make init 29 | - name: Install dependencies 30 | run: | 31 | make install 32 | - name: Lint 33 | run: | 34 | make lint 35 | - name: Test 36 | run: | 37 | make test 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: '3.10' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | .vscode/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Akop Kesheshyan 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. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include bitrix24 3 | include README.md 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .NOTPARALLEL: ; # wait for this target to finish 2 | .EXPORT_ALL_VARIABLES: ; # send all vars to shell 3 | .PHONY: all # All targets are accessible for user 4 | .DEFAULT: help # Running Make will run the help target 5 | 6 | PYTHON = @.venv/bin/python -m 7 | APP = bitrix24 8 | 9 | # ------------------------------------------------------------------------------------------------- 10 | # help: @ List available tasks on this project 11 | # ------------------------------------------------------------------------------------------------- 12 | help: 13 | @grep -oE '^#.[a-zA-Z0-9]+:.*?@ .*$$' $(MAKEFILE_LIST) | tr -d '#' |\ 14 | awk 'BEGIN {FS = ":.*?@ "}; {printf " make%-10s%s\n", $$1, $$2}' 15 | 16 | # ------------------------------------------------------------------------------------------------- 17 | # all: @ Apply all checks at once 18 | # ------------------------------------------------------------------------------------------------- 19 | all: format lint test 20 | 21 | # ------------------------------------------------------------------------------------------------- 22 | # init: @ Setup local environment 23 | # ------------------------------------------------------------------------------------------------- 24 | init: activate install 25 | 26 | # ------------------------------------------------------------------------------------------------- 27 | # update: @ Update package dependencies and install them 28 | # ------------------------------------------------------------------------------------------------- 29 | update: compile install 30 | 31 | # ------------------------------------------------------------------------------------------------- 32 | # Activate virtual environment 33 | # ------------------------------------------------------------------------------------------------- 34 | activate: 35 | @python3 -m venv .venv 36 | @. .venv/bin/activate 37 | 38 | # ------------------------------------------------------------------------------------------------- 39 | # Install packages to current environment 40 | # ------------------------------------------------------------------------------------------------- 41 | install: 42 | $(PYTHON) pip install --upgrade pip 43 | $(PYTHON) pip install -e .[dev] 44 | 45 | # ------------------------------------------------------------------------------------------------- 46 | # test: @ Run tests using pytest 47 | # ------------------------------------------------------------------------------------------------- 48 | test: 49 | $(PYTHON) pytest tests --cov=. 50 | 51 | # ------------------------------------------------------------------------------------------------- 52 | # lint: @ Checks the source code against coding standard rules and safety 53 | # ------------------------------------------------------------------------------------------------- 54 | lint: lint.setup lint.flake8 lint.docs 55 | 56 | # ------------------------------------------------------------------------------------------------- 57 | # format: @ Format source code and auto fix minor issues 58 | # ------------------------------------------------------------------------------------------------- 59 | format: 60 | $(PYTHON) black --quiet --line-length=100 $(APP) 61 | $(PYTHON) isort $(APP) 62 | 63 | # ------------------------------------------------------------------------------------------------- 64 | # setup.py 65 | # ------------------------------------------------------------------------------------------------- 66 | lint.setup: 67 | $(PYTHON) setup check -s 68 | 69 | # ------------------------------------------------------------------------------------------------- 70 | # flake8 71 | # ------------------------------------------------------------------------------------------------- 72 | lint.flake8: 73 | $(PYTHON) flake8 --exclude=.venv,.eggs,*.egg,.git,migrations,__init__.py \ 74 | --filename=*.py,*.pyx \ 75 | --max-line-length=100 . 76 | 77 | # ------------------------------------------------------------------------------------------------- 78 | # safety 79 | # ------------------------------------------------------------------------------------------------- 80 | lint.safety: 81 | $(PYTHON) safety check --full-report 82 | 83 | # ------------------------------------------------------------------------------------------------- 84 | # pydocstyle 85 | # ------------------------------------------------------------------------------------------------- 86 | # Ignored error codes: 87 | # D100 Missing docstring in public module 88 | # D101 Missing docstring in public class 89 | # D102 Missing docstring in public method 90 | # D103 Missing docstring in public function 91 | # D104 Missing docstring in public package 92 | # D105 Missing docstring in magic method 93 | # D106 Missing docstring in public nested class 94 | # D107 Missing docstring in __init__ 95 | lint.docs: 96 | $(PYTHON) pydocstyle --convention=numpy --add-ignore=D100,D101,D102,D103,D104,D105,D106,D107 . 97 | 98 | # ------------------------------------------------------------------------------------------------- 99 | # clean: @ Remove artifacts and temp files 100 | # ------------------------------------------------------------------------------------------------- 101 | clean: 102 | @rm -rf .venv/ dist/ build/ *.egg-info/ .pytest_cache/ .coverage coverage.xml 103 | @find . | grep -E "\(__pycache__|\.pyc|\.pyo\$\)" | xargs rm -rf 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitrix24 REST API for Python 2 | 3 | Easy way to communicate with bitrix24 portal over REST without OAuth 2.0 4 | 5 | ## Description 6 | 7 | Bitrix24 REST is an API wrapper for working with Bitrix24 REST API over webhooks. 8 | No OAuth 2.0 required. It's easy to use and super lightweight, with minimal dependencies. 9 | 10 | ## Features 11 | 12 | - Works with both cloud and on-premises versions of Bitrix24. 13 | - Super easy to setup. No OAuth 2.0 infrastructure required. 14 | - Built with data analysis in mind and fully compatible with Jupyter Notebook. 15 | - Fetch paginated data at once without hassle. 16 | - Works with large datasets and handles rate limits. 17 | 18 | ## Installation 19 | 20 | ``` 21 | pip install bitrix24-rest 22 | ``` 23 | 24 | ## Quickstart 25 | 26 | ```python 27 | from bitrix24 import Bitrix24 28 | 29 | bx24 = Bitrix24('https://example.bitrix24.com/rest/1/33olqeits4avuyqu') 30 | 31 | print(bx24.callMethod('crm.product.list')) 32 | ``` 33 | 34 | In async mode: 35 | 36 | ```python 37 | import asyncio 38 | from bitrix24 import bitrix24 39 | 40 | async def main(): 41 | bx24 = Bitrix24('https://example.bitrix24.com/rest/1/33olqeits4avuyqu') 42 | result = await bx24.callMethod('crm.product.list') 43 | print(result) 44 | 45 | asyncio.run(main()) 46 | ``` 47 | 48 | ## Advanced usage 49 | 50 | - [Using filters and additional parameters](docs/using-filters-and-additional-parameters.md) 51 | - [Working with large datasets](docs/working-with-large-datasets.md) 52 | - [Disabling certificate verification](docs/disabling-certificate-verification.md) 53 | 54 | ## Notes 55 | 56 | List methods return all available items at once. For large collections of data use limits. 57 | 58 | ## Development 59 | 60 | New contributors and pull requests are welcome. If you have any questions or suggestions, feel free to open an issue. 61 | 62 | Code comes with makefile for easy code base management. You can check `make help` for more details. 63 | 64 | ```sh 65 | make init install # to create a local virtual environment and install dependencies 66 | 67 | make test # to run tests 68 | 69 | make lint # to run linter 70 | ``` 71 | 72 | I suggest to use `make all` before committing your changes as it will run all the necessary checks. 73 | 74 | ## Support this project 75 | 76 | You can support this project by starring ⭐, sharing 📤, and contributing. 77 | 78 | You can also support the author by buying him a coffee ☕. Click sponsor button on the top of the page. 79 | -------------------------------------------------------------------------------- /bitrix24/__init__.py: -------------------------------------------------------------------------------- 1 | # ____ _ _ _ ____ _ _ ____ _____ ____ _____ 2 | # | __ )(_) |_ _ __(_)_ _|___ \| || | | _ \| ____/ ___|_ _| 3 | # | _ \| | __| '__| \ \/ / __) | || |_ | |_) | _| \___ \ | | 4 | # | |_) | | |_| | | |> < / __/|__ _| | _ <| |___ ___) || | 5 | # |____/|_|\__|_| |_/_/\_\_____| |_| |_| \_\_____|____/ |_| 6 | 7 | from .bitrix24 import Bitrix24 8 | from .exceptions import BitrixError 9 | 10 | __all__ = ["Bitrix24", "BitrixError"] 11 | -------------------------------------------------------------------------------- /bitrix24/bitrix24.py: -------------------------------------------------------------------------------- 1 | # ____ _ _ _ ____ _ _ ____ _____ ____ _____ 2 | # | __ )(_) |_ _ __(_)_ _|___ \| || | | _ \| ____/ ___|_ _| 3 | # | _ \| | __| '__| \ \/ / __) | || |_ | |_) | _| \___ \ | | 4 | # | |_) | | |_| | | |> < / __/|__ _| | _ <| |___ ___) || | 5 | # |____/|_|\__|_| |_/_/\_\_____| |_| |_| \_\_____|____/ |_| 6 | 7 | import asyncio 8 | import itertools 9 | import ssl 10 | import warnings 11 | from typing import Any, Dict 12 | from urllib.parse import urlparse 13 | 14 | from aiohttp import ClientSession, TCPConnector 15 | 16 | from .exceptions import BitrixError 17 | 18 | 19 | class Bitrix24: 20 | """ 21 | Bitrix24 API class. 22 | 23 | Provides an easy way to communicate with Bitrix24 portal over REST without OAuth. 24 | """ 25 | 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 | ): 34 | """ 35 | Create Bitrix24 API object. 36 | 37 | Parameters 38 | ---------- 39 | domain (str): Bitrix24 webhook domain 40 | 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 44 | """ 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) 50 | 51 | @staticmethod 52 | def _prepare_domain(domain: str) -> str: 53 | """Normalize user passed domain to a valid one.""" 54 | o = urlparse(domain) 55 | if not o.scheme or not o.netloc: 56 | raise BitrixError("Not a valid domain. Please provide a valid domain.") 57 | user_id, code = o.path.split("/")[2:4] 58 | return "{0}://{1}/rest/{2}/{3}".format(o.scheme, o.netloc, user_id, code) 59 | 60 | def _prepare_params(self, params: Dict[str, Any], prev: str = "") -> str: 61 | """ 62 | Transform list of parameters to a valid bitrix array. 63 | 64 | Parameters 65 | ---------- 66 | params (dict): Dictionary of parameters 67 | prev (str): Previous key 68 | 69 | Returns 70 | ------- 71 | str: Prepared parameters 72 | """ 73 | ret = "" 74 | if isinstance(params, dict): 75 | for key, value in params.items(): 76 | if isinstance(value, dict): 77 | if prev: 78 | key = "{0}[{1}]".format(prev, key) 79 | ret += self._prepare_params(value, key) 80 | elif (isinstance(value, list) or isinstance(value, tuple)) and len(value) > 0: 81 | for offset, val in enumerate(value): 82 | if isinstance(val, dict): 83 | ret += self._prepare_params( 84 | val, "{0}[{1}][{2}]".format(prev, key, offset) 85 | ) 86 | else: 87 | if prev: 88 | ret += "{0}[{1}][{2}]={3}&".format(prev, key, offset, val) 89 | else: 90 | ret += "{0}[{1}]={2}&".format(key, offset, val) 91 | else: 92 | if prev: 93 | ret += "{0}[{1}]={2}&".format(prev, key, value) 94 | else: 95 | ret += "{0}={1}&".format(key, value) 96 | return ret 97 | 98 | async def request(self, method: str, params: str = None) -> Dict[str, Any]: 99 | ssl_context = ssl.create_default_context() 100 | if not self._verify_ssl: 101 | ssl_context.check_hostname = False 102 | ssl_context.verify_mode = ssl.CERT_NONE 103 | async with ClientSession(connector=TCPConnector(ssl=ssl_context)) as session: 104 | async with session.get( 105 | f"{self._domain}/{method}.json", params=params, timeout=self._timeout 106 | ) as resp: 107 | if resp.status not in [200, 201]: 108 | raise BitrixError(f"HTTP error: {resp.status}") 109 | response = await resp.json() 110 | if "error" in response: 111 | if response["error"] == "QUERY_LIMIT_EXCEEDED": 112 | await asyncio.sleep(self._retry_after) 113 | return await self.request(method, params) 114 | raise BitrixError(response["error_description"], response["error"]) 115 | return response 116 | 117 | async def _call( 118 | self, method: str, params: Dict[str, Any] = None, start: int = 0 119 | ) -> Dict[str, Any]: 120 | """Async call a REST method with specified parameters. 121 | 122 | Parameters 123 | ---------- 124 | method (str): REST method name 125 | params (dict): Optional arguments which will be converted to a POST request string 126 | start (int): Offset for pagination 127 | """ 128 | if params is None: 129 | params = {} 130 | params["start"] = start 131 | 132 | payload = self._prepare_params(params) 133 | res = await self.request(method, payload) 134 | 135 | if "next" in res and not start and self._fetch_all_pages: 136 | if res["total"] % 50 == 0: 137 | count_tasks = res["total"] // 50 - 1 138 | else: 139 | count_tasks = res["total"] // 50 140 | 141 | tasks = [ 142 | self._call(method, params, (s + 1) * 50) for s in range(count_tasks) 143 | ] 144 | items = await asyncio.gather(*tasks) 145 | if type(res["result"]) is not dict: 146 | return res["result"] + list(itertools.chain(*items)) 147 | if items: 148 | key = list(res["result"].keys())[0] 149 | for item in items: 150 | res["result"][key] += item[key] 151 | return res["result"] 152 | 153 | def callMethod(self, method: str, params: Dict[str, Any] = None, **kwargs) -> Dict[str, Any]: 154 | """Call a REST method with specified parameters. 155 | 156 | Parameters 157 | ---------- 158 | method (str): REST method name 159 | params (dict): Optional arguments which will be converted to a POST request string 160 | 161 | Returns 162 | ------- 163 | Returning the REST method response as an array, an object or a scalar 164 | """ 165 | if params is None: 166 | params = {} 167 | 168 | if not method: 169 | raise BitrixError("Wrong method name", 400) 170 | 171 | try: 172 | loop = asyncio.get_running_loop() 173 | except RuntimeError: 174 | warnings.warn( 175 | "You are using `callMethod` method in a synchronous way. " 176 | "Starting from version 3, this method will be completely asynchronous." 177 | "Please consider updating your code", 178 | DeprecationWarning, 179 | ) 180 | loop = asyncio.new_event_loop() 181 | asyncio.set_event_loop(loop) 182 | try: 183 | result = loop.run_until_complete(self._call(method, params or kwargs)) 184 | finally: 185 | loop.close() 186 | else: 187 | result = asyncio.ensure_future(self._call(method, params or kwargs)) 188 | return result 189 | -------------------------------------------------------------------------------- /bitrix24/exceptions.py: -------------------------------------------------------------------------------- 1 | # ____ _ _ _ ____ _ _ ____ _____ ____ _____ 2 | # | __ )(_) |_ _ __(_)_ _|___ \| || | | _ \| ____/ ___|_ _| 3 | # | _ \| | __| '__| \ \/ / __) | || |_ | |_) | _| \___ \ | | 4 | # | |_) | | |_| | | |> < / __/|__ _| | _ <| |___ ___) || | 5 | # |____/|_|\__|_| |_/_/\_\_____| |_| |_| \_\_____|____/ |_| 6 | 7 | 8 | class BitrixError(ValueError): 9 | def __init__(self, message: str, code: int = 500): 10 | self.message = message 11 | self.code = code 12 | -------------------------------------------------------------------------------- /docs/disabling-certificate-verification.md: -------------------------------------------------------------------------------- 1 | # Disabling certificate verification 2 | 3 | By default, the library verifies SSL certificates. If you want to disable this behavior, you can set `safe` parameter to `False` in the `Bitrix24` class. 4 | 5 | This tells Python's underlying SSL handling to accept the server's certificate even if it's expired or invalid. 6 | 7 | ```python 8 | bx24 = Bitrix24('https://example.bitrix24.com/rest/1/33olqeits4avuyqu', safe=False) 9 | 10 | await bx24.callMethod('crm.deal.list') 11 | ``` 12 | 13 | ## Important Consideration 14 | 15 | Disabling SSL certificate verification undermines the security of HTTPS by making your application vulnerable to man-in-the-middle attacks. 16 | 17 | It should only be used in controlled environments, such as development or testing, where security is not a concern. 18 | 19 | -------------------------------------------------------------------------------- /docs/using-filters-and-additional-parameters.md: -------------------------------------------------------------------------------- 1 | # Using filters and additional parameters 2 | 3 | Define filters and additional parameters in any order using keyword arguments. 4 | 5 | ```python 6 | bx24 = Bitrix24('https://example.bitrix24.com/rest/1/33olqeits4avuyqu') 7 | 8 | await bx24.callMethod('crm.deal.list', 9 | order={'STAGE_ID': 'ASC'}, 10 | filter={'>PROBABILITY': 50}, 11 | select=['ID', 'TITLE', 'STAGE_ID', 'PROBABILITY']) 12 | ``` 13 | 14 | You also can pass filters as a dictionary, similar to the original Bitrix24 API: 15 | 16 | ````python 17 | 18 | payload = { 19 | 'order': {'STAGE_ID': 'ASC'}, 20 | 'filter': {'>PROBABILITY': 50}, 21 | 'select': ['ID', 'TITLE', 'STAGE_ID', 'PROBABILITY'] 22 | } 23 | 24 | await bx24.callMethod('crm.deal.list', payload) 25 | 26 | ```` 27 | -------------------------------------------------------------------------------- /docs/working-with-large-datasets.md: -------------------------------------------------------------------------------- 1 | # Working with large datasets 2 | 3 | Fetching large datasets can be a problem. Bitrix24 REST API has a limit of 50 items per request and a rate limiter 4 | that can block your requests if you exceed the limit. 5 | 6 | This library has a built-in feature to handle large datasets without any hassle. 7 | It will automatically detect if the dataset is paginated and fetch all the data at once. 8 | 9 | Also, all requests are made concurrently, which means you can fetch data faster and with low resource usage. 10 | 11 | However, if you for any reason want to disable this feature, you can do so by setting the `fetch_all_pages` parameter to `False`. 12 | 13 | 14 | ```python 15 | 16 | bx24 = Bitrix24('https://example.bitrix24.com/rest/1/33olqeits4avuyqu', fetch_all_pages=False) 17 | 18 | # `page1` will contain only the first page of the dataset 19 | page1 = await bx24.callMethod('crm.deal.list') 20 | 21 | # fetch next page 22 | page2 = await bx24.callMethod('crm.deal.list', start=50) 23 | ``` 24 | 25 | In this mode, you will need to handle pagination manually. 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # ____ _ _ _ ____ _ _ ____ _____ ____ _____ 2 | # | __ )(_) |_ _ __(_)_ _|___ \| || | | _ \| ____/ ___|_ _| 3 | # | _ \| | __| "__| \ \/ / __) | || |_ | |_) | _| \___ \ | | 4 | # | |_) | | |_| | | |> < / __/|__ _| | _ <| |___ ___) || | 5 | # |____/|_|\__|_| |_/_/\_\_____| |_| |_| \_\_____|____/ |_| 6 | 7 | from distutils.core import setup 8 | from os import path 9 | from setuptools import find_packages 10 | 11 | directory = path.abspath(path.dirname(__file__)) 12 | 13 | setup( 14 | name="bitrix24-rest", 15 | version="2.0.3", 16 | packages=find_packages(), 17 | install_requires=[ 18 | "aiohttp", 19 | ], 20 | extras_require={ 21 | "dev": [ 22 | "flake8", 23 | "safety", 24 | "pydocstyle", 25 | "black", 26 | "isort", 27 | "pytest", 28 | "pytest-cov", 29 | "pytest-asyncio", 30 | "aioresponses", 31 | "pytest-aiohttp" 32 | ], 33 | }, 34 | url="https://github.com/akopdev/bitrix24-python-rest", 35 | license="MIT", 36 | author="Akop Kesheshyan", 37 | author_email="hello@akop.dev", 38 | description="Easy way to communicate with bitrix24 portal over REST without OAuth", 39 | long_description=open(path.join(directory, "README.md"), encoding="utf-8").read(), 40 | long_description_content_type="text/markdown", 41 | keywords="bitrix24 api rest", 42 | classifiers=[ 43 | "Development Status :: 5 - Production/Stable", 44 | "Intended Audience :: Developers", 45 | "License :: OSI Approved :: MIT License", 46 | "Operating System :: POSIX", 47 | "Programming Language :: Python", 48 | "Topic :: Software Development :: Libraries", 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akopdev/bitrix24-python-rest/76551fd6a980f2e249ca838ed9cbb0a707b14d7b/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bitrix24 import Bitrix24 4 | 5 | 6 | @pytest.fixture 7 | def b24(): 8 | return Bitrix24("https://example.bitrix24.com/rest/1/123456789") 9 | -------------------------------------------------------------------------------- /tests/test_async_call_method.py: -------------------------------------------------------------------------------- 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&start=0", 12 | payload={"result": [{"ID": 1}]}, 13 | status=200, 14 | ) 15 | res = await b24.callMethod("user.get", {"ID": 1}) 16 | assert res[0]["ID"] == 1 17 | -------------------------------------------------------------------------------- /tests/test_bitrix24.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bitrix24 import Bitrix24, BitrixError 4 | 5 | 6 | def test_init_with_empty_domain(): 7 | with pytest.raises(BitrixError): 8 | Bitrix24("") 9 | 10 | 11 | def test_call_with_empty_method(b24): 12 | with pytest.raises(BitrixError): 13 | b24.callMethod("") 14 | 15 | 16 | def test_call_non_exists_method(b24): 17 | with pytest.raises(BitrixError): 18 | b24.callMethod("hello.world") 19 | 20 | 21 | def test_call_wrong_method(b24): 22 | with pytest.raises(BitrixError): 23 | b24.callMethod("helloworld") 24 | -------------------------------------------------------------------------------- /tests/test_pagination.py: -------------------------------------------------------------------------------- 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": 82}, 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": 82}, 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_concurrent_requests_nesting_level(b24): 27 | with aioresponses() as m: 28 | m.get( 29 | "https://example.bitrix24.com/rest/1/123456789/tasks.task.list.json?start=0", 30 | payload={"result": {"tasks": [{"ID": 1}]}, "next": 50, "total": 100}, 31 | status=200, 32 | repeat=True, 33 | ) 34 | m.get( 35 | "https://example.bitrix24.com/rest/1/123456789/tasks.task.list.json?start=50", 36 | payload={"result": {"tasks": [{"ID": 2}]}, "total": 100}, 37 | status=200, 38 | repeat=True, 39 | ) 40 | res = await b24.callMethod("tasks.task.list") 41 | assert res == {"tasks": [{"ID": 1}, {"ID": 2}]} 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_request_with_disabled_pagination(): 46 | b24 = Bitrix24("https://example.bitrix24.com/rest/1/123456789", fetch_all_pages=False) 47 | with aioresponses() as m: 48 | m.get( 49 | "https://example.bitrix24.com/rest/1/123456789/crm.deal.list.json?start=0", 50 | payload={"result": [{"ID": 1}], "next": 50, "total": 100}, 51 | status=200, 52 | repeat=True, 53 | ) 54 | res = await b24.callMethod("crm.deal.list") 55 | assert res == [{"ID": 1}] 56 | -------------------------------------------------------------------------------- /tests/test_params_preparation.py: -------------------------------------------------------------------------------- 1 | def test_one_level(b24): 2 | params = {"fruit": "apple"} 3 | param_string = b24._prepare_params(params) 4 | assert param_string == "fruit=apple&" 5 | 6 | 7 | def test_one_level_several_items(b24): 8 | params = {"fruit": "apple", "vegetable": "broccoli"} 9 | param_string = b24._prepare_params(params) 10 | assert param_string == "fruit=apple&vegetable=broccoli&" 11 | 12 | 13 | def test_multi_level(b24): 14 | params = {"fruit": {"citrus": "lemon"}} 15 | param_string = b24._prepare_params(params) 16 | assert param_string == "fruit[citrus]=lemon&" 17 | 18 | 19 | def test_multi_level_deep(b24): 20 | params = {"root": {"level 1": {"level 2": {"level 3": "value"}}}} 21 | param_string = b24._prepare_params(params) 22 | assert param_string == "root[level 1][level 2][level 3]=value&" 23 | 24 | 25 | def test_list_dict_mixed(b24): 26 | params = {"root": {"level 1": [{"list_d 1": "value 1"}, {"list_d 2": "value 2"}]}} 27 | param_string = b24._prepare_params(params) 28 | assert param_string == "root[level 1][0][list_d 1]=value 1&root[level 1][1][list_d 2]=value 2&" 29 | 30 | 31 | def test_multi_level_several_items(b24): 32 | params = {"fruit": {"citrus": "lemon", "sweet": "apple"}} 33 | param_string = b24._prepare_params(params) 34 | assert param_string == "fruit[citrus]=lemon&fruit[sweet]=apple&" 35 | 36 | 37 | def test_list(b24): 38 | params = {"fruit": ["lemon", "apple"]} 39 | param_string = b24._prepare_params(params) 40 | assert param_string == "fruit[0]=lemon&fruit[1]=apple&" 41 | 42 | 43 | def test_tuple(b24): 44 | params = {"fruit": ("lemon", "apple")} 45 | param_string = b24._prepare_params(params) 46 | assert param_string == "fruit[0]=lemon&fruit[1]=apple&" 47 | 48 | 49 | def test_string(b24): 50 | param_string = b24._prepare_params("") 51 | assert param_string == "" 52 | -------------------------------------------------------------------------------- /tests/test_request_retry.py: -------------------------------------------------------------------------------- 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}] 20 | --------------------------------------------------------------------------------