├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── linters.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.rst ├── kanboard.py ├── pyproject.toml └── tests ├── __init__.py └── test_kanboard.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | tests: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | os: [ubuntu-latest, windows-latest] 13 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Unit tests 21 | run: | 22 | python3 -m unittest -v 23 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | linters: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Install Python 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: "3.13" 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install ruff flake8 19 | - name: Run Ruff 20 | run: ruff check --output-format=github . 21 | - name: Run Flake8 22 | run: flake8 --max-line-length 120 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPi 2 | on: 3 | push: 4 | tags: 5 | - '*.*.*' 6 | workflow_dispatch: 7 | jobs: 8 | build: 9 | name: Build distribution package 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.x" 17 | - name: Install pypa/build 18 | run: >- 19 | python3 -m 20 | pip install 21 | build 22 | --user 23 | - name: Build a binary wheel and a source tarball 24 | run: python3 -m build 25 | - name: Store the distribution packages 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: python-package-distributions 29 | path: dist/ 30 | 31 | publish-to-pypi: 32 | name: Publish to PyPI 33 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 34 | needs: 35 | - build 36 | runs-on: ubuntu-latest 37 | environment: 38 | name: pypi 39 | url: https://pypi.org/p/kanboard 40 | permissions: 41 | id-token: write # IMPORTANT: mandatory for trusted publishing 42 | 43 | steps: 44 | - name: Download all the dists 45 | uses: actions/download-artifact@v4 46 | with: 47 | name: python-package-distributions 48 | path: dist/ 49 | - name: Publish distribution 📦 to PyPI 50 | uses: pypa/gh-action-pypi-publish@release/v1 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .ruff_cache 3 | .venv 4 | *.egg-info 5 | *.eggs 6 | *.pyc 7 | build 8 | dist 9 | MANIFEST 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Frederic Guillot 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Python API Client for Kanboard 3 | ============================== 4 | 5 | Client library for Kanboard API. 6 | 7 | - Author: Frédéric Guillot 8 | - License: MIT 9 | 10 | Installation 11 | ============ 12 | 13 | .. code-block:: bash 14 | 15 | python3 -m pip install kanboard 16 | 17 | 18 | - This library is compatible with Python >= 3.9. 19 | - Python 2.7 is no longer supported since version 1.1.0. 20 | - Python 3.7 and 3.8 are no longer supported since version 1.1.7. 21 | 22 | On Fedora (36 and later), you can install the package using DNF: 23 | 24 | .. code-block:: bash 25 | 26 | dnf install python3-kanboard 27 | 28 | 29 | Examples 30 | ======== 31 | 32 | Methods and arguments are the same as the JSON-RPC procedures described in the 33 | `official documentation `_. 34 | 35 | Python methods are dynamically mapped to the API procedures: **You must use named arguments**. 36 | 37 | By default, calls are made synchronously, meaning that they will block the program until completed. 38 | 39 | Creating a new team project 40 | --------------------------- 41 | 42 | .. code-block:: python 43 | 44 | import kanboard 45 | 46 | kb = kanboard.Client("http://localhost/jsonrpc.php", "jsonrpc", "your_api_token") 47 | project_id = kb.create_project(name="My project") 48 | 49 | 50 | Authenticate as user 51 | -------------------- 52 | 53 | .. code-block:: python 54 | 55 | import kanboard 56 | 57 | kb = kanboard.Client("http://localhost/jsonrpc.php", "admin", "secret") 58 | kb.get_my_projects() 59 | 60 | Use a custom authentication header 61 | ---------------------------------- 62 | 63 | If your Kanboard instance is configured to use a custom authentication header (for example, by setting ``define('API_AUTHENTICATION_HEADER', 'X-My-Custom-Auth-Header');`` in your Kanboard configuration), you can authenticate using the following code: 64 | 65 | .. code-block:: python 66 | 67 | import kanboard 68 | 69 | kb = kanboard.Client(url="http://localhost/jsonrpc.php", 70 | username="demo", 71 | password="secret", 72 | auth_header="X-My-Custom-Auth-Header") 73 | 74 | kb.get_me() 75 | 76 | Create a new task 77 | ----------------- 78 | 79 | .. code-block:: python 80 | 81 | import kanboard 82 | 83 | kb = kanboard.Client("http://localhost/jsonrpc.php", "jsonrpc", "your_api_token") 84 | project_id = kb.create_project(name="My project") 85 | task_id = kb.create_task(project_id=project_id, title="My task title") 86 | 87 | Use a personalized user agent 88 | ----------------------------- 89 | 90 | .. code-block:: python 91 | 92 | import kanboard 93 | 94 | kb = kanboard.Client(url="http://localhost/jsonrpc.php", 95 | username="admin", 96 | password="secret", 97 | user_agent="My Kanboard client") 98 | 99 | SSL connection and self-signed certificates 100 | =========================================== 101 | 102 | Example with a valid certificate: 103 | 104 | .. code-block:: python 105 | 106 | import kanboard 107 | 108 | kb = kanboard.Client("https://example.org/jsonrpc.php", "admin", "secret") 109 | kb.get_my_projects() 110 | 111 | Example with a custom certificate: 112 | 113 | .. code-block:: python 114 | 115 | import kanboard 116 | 117 | kb = kanboard.Client(url="https://example.org/jsonrpc.php", 118 | username="admin", 119 | password="secret", 120 | cafile="/path/to/my/cert.pem") 121 | kb.get_my_projects() 122 | 123 | Example with a custom certificate and hostname mismatch: 124 | 125 | .. code-block:: python 126 | 127 | import kanboard 128 | 129 | kb = kanboard.Client(url="https://example.org/jsonrpc.php", 130 | username="admin", 131 | password="secret", 132 | cafile="/path/to/my/cert.pem", 133 | ignore_hostname_verification=True) 134 | kb.get_my_projects() 135 | 136 | Ignore invalid/expired certificates and hostname mismatches, which will make your application vulnerable to man-in-the-middle (MitM) attacks: 137 | 138 | .. code-block:: python 139 | 140 | import kanboard 141 | 142 | kb = kanboard.Client(url="https://example.org/jsonrpc.php", 143 | username="admin", 144 | password="secret", 145 | insecure=True) 146 | kb.get_my_projects() 147 | 148 | Asynchronous I/O 149 | ================ 150 | 151 | The client also exposes async/await style method calls. Similarly to the synchronous calls (see above), 152 | the method names are mapped to the API methods. 153 | 154 | To invoke an asynchronous call, the method name must be appended with ``_async``. For example, a synchronous call 155 | to ``create_project`` can be made asynchronous by calling ``create_project_async`` instead. 156 | 157 | .. code-block:: python 158 | 159 | import asyncio 160 | import kanboard 161 | 162 | kb = kanboard.Client("http://localhost/jsonrpc.php", "jsonrpc", "your_api_token") 163 | 164 | loop = asyncio.get_event_loop() 165 | project_id = loop.run_until_complete(kb.create_project_async(name="My project")) 166 | 167 | 168 | .. code-block:: python 169 | 170 | import asyncio 171 | import kanboard 172 | 173 | async def call_within_function(): 174 | kb = kanboard.Client("http://localhost/jsonrpc.php", "jsonrpc", "your_api_token") 175 | return await kb.create_project_async(name="My project") 176 | 177 | loop = asyncio.get_event_loop() 178 | project_id = loop.run_until_complete(call_within_function()) 179 | 180 | 181 | See the `official API documentation `_ for the complete list of 182 | methods and arguments. 183 | -------------------------------------------------------------------------------- /kanboard.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) Frederic Guillot 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import json 24 | import base64 25 | import functools 26 | import asyncio 27 | import ssl 28 | from typing import Dict, Optional 29 | from urllib import request as http 30 | 31 | 32 | DEFAULT_AUTH_HEADER = "Authorization" 33 | ASYNC_FUNCNAME_MARKER = "_async" 34 | 35 | 36 | class ClientError(Exception): 37 | pass 38 | 39 | 40 | class Client: 41 | """ 42 | Kanboard API client for interacting with the Kanboard JSON-RPC API. 43 | 44 | This client provides both synchronous and asynchronous access to all Kanboard API methods. 45 | Methods are dynamically resolved based on the Kanboard API method names, using snake_case for Python calls. 46 | 47 | Example usage: 48 | from kanboard import Client 49 | 50 | kb = Client(url="http://localhost/jsonrpc.php", 51 | username="jsonrpc", 52 | password="your_api_token") 53 | 54 | project_id = kb.create_project(name="My project") 55 | """ 56 | 57 | def __init__( 58 | self, 59 | url: str, 60 | username: str, 61 | password: str, 62 | auth_header: str = DEFAULT_AUTH_HEADER, 63 | cafile: Optional[str] = None, 64 | insecure: bool = False, 65 | ignore_hostname_verification: bool = False, 66 | user_agent: str = "Kanboard Python API Client", 67 | loop: Optional[asyncio.AbstractEventLoop] = None, 68 | ) -> None: 69 | """ 70 | Initialize a new Kanboard API client instance. 71 | 72 | Args: 73 | url (str): The Kanboard JSON-RPC API endpoint URL (e.g., 74 | 'http://localhost/jsonrpc.php'). 75 | username (str): Kanboard API username or real username. 76 | password (str): Kanboard API token or user password. 77 | auth_header (str, optional): HTTP header for authentication. Defaults to 'Authorization'. 78 | cafile (Optional[str], optional): Path to a custom CA certificate file. Defaults to None. 79 | insecure (bool, optional): If True, ignore SSL certificate errors and hostname mismatches. 80 | Defaults to False. 81 | ignore_hostname_verification (bool, optional): If True, skip SSL certificate hostname verification. 82 | Defaults to False. 83 | user_agent (str, optional): Custom User-Agent string for HTTP requests. Defaults to 84 | 'Kanboard Python API Client'. 85 | loop (Optional[asyncio.AbstractEventLoop], optional): Asyncio event loop to use. If None, uses the 86 | current event loop or creates a new one. 87 | """ 88 | self._url = url 89 | self._username = username 90 | self._password = password 91 | self._auth_header = auth_header 92 | self._cafile = cafile 93 | self._insecure = insecure 94 | self._user_agent = user_agent 95 | self._ignore_hostname_verification = ignore_hostname_verification 96 | 97 | if not loop: 98 | try: 99 | self._event_loop = asyncio.get_event_loop() 100 | except RuntimeError: 101 | self._event_loop = asyncio.new_event_loop() 102 | 103 | def __getattr__(self, name: str): 104 | if self.is_async_method_name(name): 105 | 106 | async def function(*args, **kwargs): 107 | return await self._event_loop.run_in_executor( 108 | None, 109 | functools.partial( 110 | self.execute, method=self._to_camel_case(self.get_funcname_from_async_name(name)), **kwargs 111 | ), 112 | ) 113 | 114 | return function 115 | else: 116 | 117 | def function(*args, **kwargs): 118 | return self.execute(method=self._to_camel_case(name), **kwargs) 119 | 120 | return function 121 | 122 | @staticmethod 123 | def is_async_method_name(funcname: str) -> bool: 124 | return funcname.endswith(ASYNC_FUNCNAME_MARKER) 125 | 126 | @staticmethod 127 | def get_funcname_from_async_name(funcname: str) -> str: 128 | return funcname[: len(funcname) - len(ASYNC_FUNCNAME_MARKER)] 129 | 130 | @staticmethod 131 | def _to_camel_case(snake_str: str) -> str: 132 | components = snake_str.split("_") 133 | return components[0] + "".join(x.title() for x in components[1:]) 134 | 135 | @staticmethod 136 | def _parse_response(response: bytes): 137 | if not response: 138 | raise ClientError("Empty response from server") 139 | try: 140 | body = json.loads(response.decode(errors="ignore")) 141 | 142 | if "error" in body: 143 | message = body.get("error").get("message") 144 | raise ClientError(message) 145 | 146 | return body.get("result") 147 | except ValueError as e: 148 | raise ClientError(f"Failed to parse JSON response: {e}") 149 | 150 | def _do_request(self, headers: Dict[str, str], body: Dict): 151 | try: 152 | request = http.Request(self._url, headers=headers, data=json.dumps(body).encode()) 153 | 154 | ssl_context = ssl.create_default_context(cafile=self._cafile) 155 | if self._insecure: 156 | ssl_context.check_hostname = False 157 | ssl_context.verify_mode = ssl.CERT_NONE 158 | 159 | if self._ignore_hostname_verification: 160 | ssl_context.check_hostname = False 161 | 162 | response = http.urlopen(request, context=ssl_context).read() 163 | except Exception as e: 164 | raise ClientError(str(e)) 165 | return self._parse_response(response) 166 | 167 | def execute(self, method: str, **kwargs): 168 | """ 169 | Call a remote Kanboard API procedure. 170 | 171 | Args: 172 | method (str): The Kanboard API method name in camelCase (e.g., 'createProject'). 173 | **kwargs: Named arguments to pass to the API method. 174 | 175 | Returns: 176 | The result returned by the Kanboard API for the requested method. 177 | 178 | Raises: 179 | ClientError: If the API returns an error or if a network/HTTP error occurs. 180 | """ 181 | payload = {"id": 1, "jsonrpc": "2.0", "method": method, "params": kwargs} 182 | 183 | credentials = base64.b64encode("{}:{}".format(self._username, self._password).encode()) 184 | auth_header_prefix = "Basic " if self._auth_header == DEFAULT_AUTH_HEADER else "" 185 | headers = { 186 | self._auth_header: auth_header_prefix + credentials.decode(), 187 | "Content-Type": "application/json", 188 | "User-Agent": self._user_agent, 189 | } 190 | 191 | return self._do_request(headers, payload) 192 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "kanboard" 3 | version = "1.1.7" 4 | description = "Python client library for Kanboard" 5 | readme = "README.rst" 6 | requires-python = ">=3.9" 7 | license = "MIT" 8 | license-files = ["LICENSE"] 9 | keywords = ["kanboard", "api", "client"] 10 | authors = [ 11 | { name = "Frédéric Guillot", email = "fred@kanboard.net" } 12 | ] 13 | classifiers = [ 14 | "Intended Audience :: Developers", 15 | "Intended Audience :: Information Technology", 16 | "Programming Language :: Python", 17 | "Programming Language :: Python :: 3 :: Only", 18 | ] 19 | 20 | [project.urls] 21 | "Homepage" = "https://github.com/kanboard/python-api-client" 22 | "Bug Reports" = "https://github.com/kanboard/python-api-client/issues" 23 | "Source" = "https://github.com/kanboard/python-api-client" 24 | 25 | [build-system] 26 | requires = ["setuptools>=61.0"] 27 | build-backend = "setuptools.build_meta" 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kanboard/python-api-client/15f42634743427ec2b2e150cd259cbbd8128cd70/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_kanboard.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) Frederic Guillot 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | import unittest 24 | from unittest import mock 25 | import types 26 | import warnings 27 | 28 | import kanboard 29 | 30 | 31 | class TestClient(unittest.TestCase): 32 | def setUp(self): 33 | self.url = "some api url" 34 | self.client = kanboard.Client(self.url, "username", "password") 35 | self.request, self.urlopen = self._create_mocks() 36 | 37 | def ignore_warnings(test_func): 38 | def do_test(self, *args, **kwargs): 39 | with warnings.catch_warnings(): 40 | warnings.simplefilter("ignore") 41 | test_func(self, *args, **kwargs) 42 | 43 | return do_test 44 | 45 | def test_api_call(self): 46 | body = b'{"jsonrpc": "2.0", "result": true, "id": 123}' 47 | self.urlopen.return_value.read.return_value = body 48 | self.assertEqual(True, self.client.remote_procedure()) 49 | self.request.assert_called_once_with(self.url, data=mock.ANY, headers=mock.ANY) 50 | 51 | def test_custom_auth_header(self): 52 | self.client._auth_header = "X-Auth-Header" 53 | body = b'{"jsonrpc": "2.0", "result": true, "id": 123}' 54 | self.urlopen.return_value.read.return_value = body 55 | self.assertEqual(True, self.client.remote_procedure()) 56 | self.request.assert_called_once_with(self.url, data=mock.ANY, headers=mock.ANY) 57 | _, kwargs = self.request.call_args 58 | assert kwargs["headers"]["X-Auth-Header"] == "dXNlcm5hbWU6cGFzc3dvcmQ=" 59 | 60 | def test_http_error(self): 61 | self.urlopen.side_effect = Exception() 62 | with self.assertRaises(kanboard.ClientError): 63 | self.client.remote_procedure() 64 | 65 | def test_empty_response_raises_client_error(self): 66 | self.urlopen.return_value.read.return_value = b"" 67 | with self.assertRaises(kanboard.ClientError) as cm: 68 | self.client.remote_procedure() 69 | self.assertIn("Empty response", str(cm.exception)) 70 | 71 | def test_json_parsing_failure(self): 72 | body = b"{invalid json}" 73 | self.urlopen.return_value.read.return_value = body 74 | with self.assertRaises(kanboard.ClientError) as cm: 75 | self.client.remote_procedure() 76 | self.assertIn("Failed to parse JSON response", str(cm.exception)) 77 | 78 | def test_application_error(self): 79 | body = b'{"jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal error"}, "id": 123}' 80 | self.urlopen.return_value.read.return_value = body 81 | 82 | with self.assertRaises(kanboard.ClientError, msg="Internal error"): 83 | self.client.remote_procedure() 84 | 85 | def test_async_method_call_recognised(self): 86 | method_name = "some_method_async" 87 | result = self.client.is_async_method_name(method_name) 88 | self.assertTrue(result) 89 | 90 | def test_standard_method_call_recognised(self): 91 | method_name = "some_method" 92 | result = self.client.is_async_method_name(method_name) 93 | self.assertFalse(result) 94 | 95 | def test_method_name_extracted_from_async_name(self): 96 | expected_method_name = "some_method" 97 | async_method_name = expected_method_name + "_async" 98 | result = self.client.get_funcname_from_async_name(async_method_name) 99 | self.assertEqual(expected_method_name, result) 100 | 101 | # suppress a RuntimeWarning because coro is not awaited 102 | # this is done on purpose 103 | @ignore_warnings 104 | def test_async_call_generates_coro(self): 105 | method = self.client.my_method_async() 106 | self.assertIsInstance(method, types.CoroutineType) 107 | 108 | @staticmethod 109 | def _create_mocks(): 110 | request_patcher = mock.patch("urllib.request.Request") 111 | urlopen_patcher = mock.patch("urllib.request.urlopen") 112 | return request_patcher.start(), urlopen_patcher.start() 113 | --------------------------------------------------------------------------------