├── .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 |
--------------------------------------------------------------------------------