diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9f93cb3..fa62cd8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,11 +9,11 @@ jobs: runs-on: ubuntu-18.04 steps: - - uses: actions/checkout@master - - name: Set up Python 3.9 - uses: actions/setup-python@v1 + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 with: - python-version: 3.9 + python-version: "3.11" - name: Install pypa/build run: >- python -m @@ -31,4 +31,4 @@ jobs: - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@master with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 37956f9..5a097aa 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -19,14 +19,14 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v3 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install requests flake8 pytest + pip install requests flake8 pytest pytest-asyncio if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/.gitignore b/.gitignore index 6b70983..f004a8b 100644 --- a/.gitignore +++ b/.gitignore @@ -103,8 +103,12 @@ venv.bak/ .mypy_cache/ .DS_Store +.idea # Pipenv Pipfile Pipfile.lock .vscode/launch.json + +# Unasync +knockapi/sync_client diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2d970d2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include knockapi/sync_client * diff --git a/README.md b/README.md index 45e5955..7391c02 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,22 @@ client.users.set_preferences( client.users.get_preferences(user_id="jhammond") ``` -### Signing JWTs +## AsyncIO Usage +The library utilises the [unasync](https://github.com/python-trio/unasync) library to provide full AsyncIO support, you just need to import the AsyncKnock client. +```python +import asyncio +from knockapi import AsyncKnock + +client = AsyncKnock(api_key="sk_12345") + +async def example_coroutine(): + messages = await client.messages.list() + print(messages) + +asyncio.run(example_coroutine()) +``` + +## Signing JWTs You can use the `pyjwt` package to [sign JWTs easily](https://pyjwt.readthedocs.io/en/stable/usage.html#encoding-decoding-tokens-with-rs256-rsa). You will need to generate an environment specific signing key, which you can find in the Knock dashboard. diff --git a/knockapi/__init__.py b/knockapi/__init__.py index 220cf65..5793456 100644 --- a/knockapi/__init__.py +++ b/knockapi/__init__.py @@ -1,2 +1,14 @@ -from knockapi.client import Knock -from knockapi.resources import * +__version__ = '0.6.0' + +from knockapi.async_client import Knock as AsyncKnock + +try: + from knockapi.sync_client import Knock +except ImportError: + Knock = None + +__all__ = [ + "AsyncKnock", + "Knock", + "__version__" +] diff --git a/knockapi/client.py b/knockapi/async_client/Knock.py similarity index 50% rename from knockapi/client.py rename to knockapi/async_client/Knock.py index 0f432cd..5318fbb 100644 --- a/knockapi/client.py +++ b/knockapi/async_client/Knock.py @@ -1,94 +1,62 @@ -import requests -from json.decoder import JSONDecodeError - -__version__ = '0.5.2' - - -class Connection(object): - def __init__(self, api_key, host='https://api.knock.app'): - self.api_key = api_key - self.host = host - self.client_version = __version__ - self.headers = { - 'Authorization': 'Bearer {}'.format(self.api_key), - 'User-Agent': 'Knock Python - {}'.format(self.client_version) - } - - def request(self, method, endpoint, payload=None): - url = '{}/v1{}'.format(self.host, endpoint) - - r = requests.request( - method, - url, - params=payload if method == 'get' else None, - json=payload if method != 'get' else None, - headers=self.headers, - ) - - # If we got a successful response, then attempt to deserialize as JSON - if r.ok: - try: - return r.json() - except JSONDecodeError: - return None +from knockapi import __version__ +from knockapi.core import AsyncConnection +from knockapi.async_client.services import User, Workflows, Preferences, Objects, Tenants, BulkOperations, Messages - return r.raise_for_status() - -class Knock(Connection): +class Knock(object): """Client to access the Knock features.""" - @property - def _auth(self): - return self.api_key - @property - def _version(self): - return __version__ + def __init__( + self, + api_key: str, + api_host: str = 'https://api.knock.app', + read_timeout: int = 300 + ): + self.connection = AsyncConnection( + api_key, + api_host, + __version__, + read_timeout + ) @property def users(self): - from .resources import User return User(self) @property def workflows(self): - from .resources import Workflows return Workflows(self) @property def preferences(self): - from .resources import Preferences return Preferences(self) @property def objects(self): - from .resources import Objects return Objects(self) @property def tenants(self): - from .resources import Tenants return Tenants(self) @property def bulk_operations(self): - from .resources import BulkOperations return BulkOperations(self) @property def messages(self): - from .resources import Messages return Messages(self) # Defined at the top level here for convenience - def notify( - self, - key, - recipients, - data={}, - actor=None, - cancellation_key=None, - tenant=None): + async def notify( + self, + key, + recipients, + data={}, + actor=None, + cancellation_key=None, + tenant=None + ): """ Triggers a workflow. @@ -112,10 +80,11 @@ def notify( dict: Response from Knock. """ # Note: this is essentially a delegated method - return self.workflows.trigger( + return await self.workflows.trigger( key=key, recipients=recipients, data=data, actor=actor, cancellation_key=cancellation_key, - tenant=tenant) + tenant=tenant + ) diff --git a/knockapi/async_client/__init__.py b/knockapi/async_client/__init__.py new file mode 100644 index 0000000..ca6d96e --- /dev/null +++ b/knockapi/async_client/__init__.py @@ -0,0 +1 @@ +from .Knock import Knock diff --git a/knockapi/resources/__init__.py b/knockapi/async_client/services/__init__.py similarity index 100% rename from knockapi/resources/__init__.py rename to knockapi/async_client/services/__init__.py diff --git a/knockapi/async_client/services/bulk_operations.py b/knockapi/async_client/services/bulk_operations.py new file mode 100644 index 0000000..81a6604 --- /dev/null +++ b/knockapi/async_client/services/bulk_operations.py @@ -0,0 +1,19 @@ +from knockapi.core.Service import Service + +default_set_id = "default" + + +class BulkOperations(Service): + + async def get(self, bulk_operation_id): + """ + Returns a bulk operation. + + Args: + bulk_operation_id (str): The id of the bulk operation + + Returns: + dict: A Knock BulkOperation + """ + endpoint = f'/bulk_operations/{bulk_operation_id}' + return await self.client.connection.request('get', endpoint) diff --git a/knockapi/resources/messages.py b/knockapi/async_client/services/messages.py similarity index 53% rename from knockapi/resources/messages.py rename to knockapi/async_client/services/messages.py index 01fd294..c287d16 100644 --- a/knockapi/resources/messages.py +++ b/knockapi/async_client/services/messages.py @@ -1,8 +1,11 @@ import json -from .service import Service + +from knockapi.core.Service import Service + class Messages(Service): - def list(self, options=None): + + async def list(self, options=None): """ Gets a paginated list of Message records @@ -17,60 +20,60 @@ def list(self, options=None): if options and options['trigger_data']: options['trigger_data'] = json.dumps(options['trigger_data']) - return self.client.request('get', endpoint, payload=options) + return await self.client.connection.request('get', endpoint, payload=options) - def get(self, id): + async def get(self, message_id): """ Get a message by its id Args: - id: The message ID + message_id: The message ID Returns: dict: Message response from Knock. """ - endpoint = '/messages/{}'.format(id) - return self.client.request('get', endpoint) + endpoint = f'/messages/{message_id}' + return await self.client.connection.request('get', endpoint) - def get_content(self, id): + async def get_content(self, message_id): """ Get a message's content by its id Args: - id: The message ID + message_id: The message ID Returns: dict: MessageContent response from Knock. """ - endpoint = '/messages/{}/content'.format(id) - return self.client.request('get', endpoint) + endpoint = f'/messages/{message_id}/content' + return await self.client.connection.request('get', endpoint) - def get_activities(self, id, options=None): + async def get_activities(self, message_id, options=None): """ Get a message's activities by its id Args: - id: The message ID + message_id: The message ID Returns: dict: paginated Activity response from Knock. """ - endpoint = '/messages/{}/activities'.format(id) + endpoint = f'/messages/{message_id}/activities' if options and options['trigger_data']: options['trigger_data'] = json.dumps(options['trigger_data']) - return self.client.request('get', endpoint, options) + return await self.client.connection.request('get', endpoint, options) - def get_events(self, id, options=None): + async def get_events(self, message_id, options=None): """ Get a message's events by its id Args: - id: The message ID + message_id: The message ID Returns: dict: paginated Event response from Knock. """ - endpoint = '/messages/{}/events'.format(id) - return self.client.request('get', endpoint, options) + endpoint = f'/messages/{message_id}/events' + return await self.client.connection.request('get', endpoint, options) diff --git a/knockapi/resources/objects.py b/knockapi/async_client/services/objects.py similarity index 60% rename from knockapi/resources/objects.py rename to knockapi/async_client/services/objects.py index c9bf486..4072d52 100644 --- a/knockapi/resources/objects.py +++ b/knockapi/async_client/services/objects.py @@ -1,41 +1,43 @@ import json -from .service import Service + +from knockapi.core.Service import Service default_set_id = "default" class Objects(Service): - def get(self, collection, id): + + async def get(self, collection, object_id): """ Returns an object in a collection with the id given. Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection Returns: dict: A Knock Object """ - endpoint = '/objects/{}/{}'.format(collection, id) - return self.client.request('get', endpoint) + endpoint = f'/objects/{collection}/{object_id}' + return await self.client.connection.request('get', endpoint) # NOTE: This is `set_object` as `set` is a reserved keyword - def set_object(self, collection, id, data={}): + async def set_object(self, collection, object_id, data={}): """ Returns an object in a collection with the id given. Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection data (dict): The data to set on the object Returns: dict: A Knock Object """ - endpoint = '/objects/{}/{}'.format(collection, id) - return self.client.request('put', endpoint, payload=data) + endpoint = f'/objects/{collection}/{object_id}' + return await self.client.connection.request('put', endpoint, payload=data) - def bulk_set(self, collection, objects): + async def bulk_set(self, collection, objects): """ Bulk sets up to 100 objects in a collection. @@ -47,24 +49,24 @@ def bulk_set(self, collection, objects): dict: BulkOperation from Knock """ data = {'objects': objects} - endpoint = '/objects/{}/bulk/set'.format(collection) - return self.client.request('post', endpoint, payload=data) + endpoint = f'/objects/{collection}/bulk/set' + return await self.client.connection.request('post', endpoint, payload=data) - def delete(self, collection, id): + async def delete(self, collection, object_id): """ Deletes the given object. Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection Returns: None: No response """ - endpoint = '/objects/{}/{}'.format(collection, id) - return self.client.request('delete', endpoint) + endpoint = f'/objects/{collection}/{object_id}' + return await self.client.connection.request('delete', endpoint) - def bulk_delete(self, collection, object_ids): + async def bulk_delete(self, collection, object_ids): """ Bulk deletes up to 100 objects in a collection. @@ -76,112 +78,109 @@ def bulk_delete(self, collection, object_ids): dict: BulkOperation from Knock """ data = {'object_ids': object_ids} - endpoint = '/objects/{}/bulk/delete'.format(collection) - return self.client.request('post', endpoint, payload=data) + endpoint = f'/objects/{collection}/bulk/delete' + return await self.client.connection.request('post', endpoint, payload=data) ## # Channel data ## - def get_channel_data(self, collection, id, channel_id): + async def get_channel_data(self, collection, object_id, channel_id): """ Get object's channel data for the given channel id. Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection channel_id (str): Target channel ID Returns: dict: Channel data from Knock. """ - endpoint = '/objects/{}/{}/channel_data/{}'.format( - collection, id, channel_id) - return self.client.request('get', endpoint) + endpoint = f'/objects/{collection}/{object_id}/channel_data/{channel_id}' + return await self.client.connection.request('get', endpoint) - def set_channel_data(self, collection, id, channel_id, channel_data): + async def set_channel_data(self, collection, object_id, channel_id, channel_data): """ Upserts object's channel data for the given channel id. Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection channel_id (str): Target channel ID channel_data (dict): Channel data Returns: dict: Channel data from Knock. """ - endpoint = '/objects/{}/{}/channel_data/{}'.format( - collection, id, channel_id) - return self.client.request( + endpoint = f'/objects/{collection}/{object_id}/channel_data/{channel_id}' + return await self.client.connection.request( 'put', endpoint, payload={ 'data': channel_data}) - def unset_channel_data(self, collection, id, channel_id): + async def unset_channel_data(self, collection, object_id, channel_id): """ Unsets the object's channel data for the given channel id. Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection channel_id (str): Target channel ID Returns: None: No response """ - endpoint = '/objects/{}/{}/channel_data/{}'.format( - collection, id, channel_id) - return self.client.request('delete', endpoint) + endpoint = f'/objects/{collection}/{object_id}/channel_data/{channel_id}' + return await self.client.connection.request('delete', endpoint) ## # Messages ## - def get_messages(self, collection, id, options=None): + async def get_messages(self, collection, object_id, options=None): """ Get object's messages Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection options (dict): An optional set of filtering options to pass to the query Returns: dict: Paginated Message response. """ - endpoint = '/objects/{}/{}/messages'.format(collection, id) + endpoint = f'/objects/{collection}/{object_id}/messages' if options and options['trigger_data']: options['trigger_data'] = json.dumps(options['trigger_data']) - return self.client.request('get', endpoint, payload=options) + return await self.client.connection.request('get', endpoint, payload=options) ## # Preferences ## - def get_all_preferences(self, collection, id): + async def get_all_preferences(self, collection, object_id): """ Get an objects full set of preferences Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection Returns: dict: PreferenceSet response from Knock. """ - endpoint = '/objects/{}/{}/preferences'.format(collection, id) - return self.client.request('get', endpoint) + endpoint = f'/objects/{collection}/{object_id}/preferences' + return await self.client.connection.request('get', endpoint) - def get_preferences(self, collection, id, options={}): + async def get_preferences(self, collection, object_id, options={}): """ Get a preference set Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection options (dict): preference_set (str): The preference set to retrieve (defaults to "default") @@ -189,15 +188,14 @@ def get_preferences(self, collection, id, options={}): dict: PreferenceSet response from Knock. """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/objects/{}/{}/preferences/{}'.format( - collection, id, preference_set_id) + endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}' - return self.client.request('get', endpoint) + return await self.client.connection.request('get', endpoint) - def set_preferences( + async def set_preferences( self, collection, - id, + object_id, channel_types=None, categories=None, workflows=None, @@ -207,7 +205,7 @@ def set_preferences( Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection channel_types (dict): A dictionary of channel type preferences categories (dict): A dictionary of category preferences workflows (dict): A dictionary of workflow preferences @@ -218,8 +216,7 @@ def set_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/objects/{}/{}/preferences/{}'.format( - collection, id, preference_set_id) + endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}' params = { 'channel_types': channel_types, @@ -227,16 +224,16 @@ def set_preferences( 'workflows': workflows } - return self.client.request('put', endpoint, payload=params) + return await self.client.connection.request('put', endpoint, payload=params) - def set_channel_types_preferences( - self, collection, id, preferences, options={}): + async def set_channel_types_preferences( + self, collection, object_id, preferences, options={}): """ Sets the channel type preferences Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection preferences (dict): A dictionary of channel type preferences options (dict): A dictionary of options @@ -245,15 +242,14 @@ def set_channel_types_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/objects/{}/{}/preferences/{}/channel_types'.format( - collection, id, preference_set_id) + endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/channel_types' - return self.client.request('put', endpoint, payload=preferences) + return await self.client.connection.request('put', endpoint, payload=preferences) - def set_channel_type_preferences( + async def set_channel_type_preferences( self, collection, - id, + object_id, channel_type, setting, options={}): @@ -262,7 +258,7 @@ def set_channel_type_preferences( Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection channel_type (str): The channel_type to set setting (boolean): The preference setting options (dict): A dictionary of options @@ -272,17 +268,16 @@ def set_channel_type_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/objects/{}/{}/preferences/{}/channel_types/{}'.format( - collection, id, preference_set_id, channel_type) + endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/channel_types/{channel_type}' - return self.client.request( + return await self.client.connection.request( 'put', endpoint, payload={ 'subscribed': setting}) - def set_workflows_preferences( + async def set_workflows_preferences( self, collection, - id, + object_id, preferences, options={}): """ @@ -290,7 +285,7 @@ def set_workflows_preferences( Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection preferences (dict): A dictionary of workflow preferences options (dict): A dictionary of options @@ -299,15 +294,14 @@ def set_workflows_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/objects/{}/{}/preferences/{}/workflows'.format( - collection, id, preference_set_id) + endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/workflows' - return self.client.request('put', endpoint, payload=preferences) + return await self.client.connection.request('put', endpoint, payload=preferences) - def set_workflow_preferences( + async def set_workflow_preferences( self, collection, - id, + object_id, key, setting, options={}): @@ -316,7 +310,7 @@ def set_workflow_preferences( Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection key (str): The workflow key setting (boolean or dict): The preference setting options (dict): A dictionary of options @@ -326,18 +320,17 @@ def set_workflow_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/objects/{}/{}/preferences/{}/workflows/{}'.format( - collection, id, preference_set_id, key) + endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/workflows/{key}' params = setting if isinstance(setting, dict) else { 'subscribed': setting} - return self.client.request('put', endpoint, payload=params) + return await self.client.connection.request('put', endpoint, payload=params) - def set_categories_preferences( + async def set_categories_preferences( self, collection, - id, + object_id, preferences, options={}): """ @@ -345,7 +338,7 @@ def set_categories_preferences( Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection preferences (dict): A dictionary of category preferences options (dict): A dictionary of options @@ -354,15 +347,14 @@ def set_categories_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/objects/{}/{}/preferences/{}/categories'.format( - collection, id, preference_set_id) + endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/categories' - return self.client.request('put', endpoint, payload=preferences) + return await self.client.connection.request('put', endpoint, payload=preferences) - def set_category_preferences( + async def set_category_preferences( self, collection, - id, + object_id, key, setting, options={}): @@ -371,7 +363,7 @@ def set_category_preferences( Args: collection (str): The collection the object belongs to - id (str): The id of the object in the collection + object_id (str): The id of the object in the collection key (str): The category key setting (boolean or dict): The preference setting options (dict): A dictionary of options @@ -381,10 +373,9 @@ def set_category_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/objects/{}/{}/preferences/{}/categories/{}'.format( - collection, id, preference_set_id, key) + endpoint = f'/objects/{collection}/{object_id}/preferences/{preference_set_id}/categories/{key}' params = setting if isinstance(setting, dict) else { 'subscribed': setting} - return self.client.request('put', endpoint, payload=params) + return await self.client.connection.request('put', endpoint, payload=params) diff --git a/knockapi/async_client/services/preferences.py b/knockapi/async_client/services/preferences.py new file mode 100644 index 0000000..b987749 --- /dev/null +++ b/knockapi/async_client/services/preferences.py @@ -0,0 +1,71 @@ +from knockapi.core.Service import Service +from warnings import warn + + +class Preferences(Service): + + async def get_all(self, user_id): + warn("This method is deprecated. Use users.get_all_preferences instead.", DeprecationWarning, stacklevel=2) + return await self.client.users.get_all_preferences(user_id) + + async def get(self, user_id, options={}): + warn("This method is deprecated. Use users.get_preferences instead.", DeprecationWarning, stacklevel=2) + return await self.client.users.get_preferences(user_id, options) + + async def update(self, user_id, channel_types=None, categories=None, workflows=None, options={}): + warn("This method is deprecated. Use users.set_preferences instead.", DeprecationWarning, stacklevel=2) + return await self.client.users.set_preferences( + user_id, + channel_types=channel_types, + categories=categories, + workflows=workflows, + options=options + ) + + async def set_channel_types(self, user_id, preferences, options={}): + warn( + "This method is deprecated. Use users.set_channel_types_preferences instead.", + DeprecationWarning, + stacklevel=2 + ) + return await self.client.users.set_channel_types_preferences(user_id, preferences, options=options) + + async def set_channel_type(self, user_id, channel_type, setting, options={}): + warn( + "This method is deprecated. Use users.set_channel_type_preferences instead.", + DeprecationWarning, + stacklevel=2 + ) + return await self.client.users.set_channel_types_preferences(user_id, channel_type, setting, options=options) + + async def set_workflows(self, user_id, preferences, options={}): + warn( + "This method is deprecated. Use users.set_workflows_preferences instead.", + DeprecationWarning, + stacklevel=2 + ) + return await self.client.users.set_workflows_preferences(user_id, preferences, options=options) + + async def set_workflow(self, user_id, key, setting, options={}): + warn( + "This method is deprecated. Use users.set_workflow_preferences instead.", + DeprecationWarning, + stacklevel=2 + ) + return await self.client.users.set_workflow_preferences(user_id, key, setting, setting, options=options) + + async def set_categories(self, user_id, preferences, options={}): + warn( + "This method is deprecated. Use users.set_categories_preferences instead.", + DeprecationWarning, + stacklevel=2 + ) + return await self.client.users.set_categories_preferences(user_id, preferences, options=options) + + async def set_category(self, user_id, key, setting, options={}): + warn( + "This method is deprecated. Use users.set_category_preferences instead.", + DeprecationWarning, + stacklevel=2 + ) + return await self.client.users.set_category_preferences(user_id, key, setting, setting, options=options) diff --git a/knockapi/async_client/services/tenants.py b/knockapi/async_client/services/tenants.py new file mode 100644 index 0000000..10789e6 --- /dev/null +++ b/knockapi/async_client/services/tenants.py @@ -0,0 +1,58 @@ +from knockapi.core.Service import Service + + +class Tenants(Service): + + async def list(self): + """ + Returns all tenants in the environment + + Args: + None + + Returns: + dict: A Knock Tenant + """ + endpoint = '/tenants' + return await self.client.connection.request('get', endpoint) + + async def get(self, tenant_id): + """ + Returns a tenant with the id given. + + Args: + tenant_id (str): The id of the tenant + + Returns: + dict: A Knock Tenant + """ + endpoint = f'/tenants/{tenant_id}' + return await self.client.connection.request('get', endpoint) + + # NOTE: This is `set_tenant` as `set` is a reserved keyword + async def set_tenant(self, tenant_id, tenant_data={}): + """ + Returns a tenant with the id given and updated settings. + + Args: + tenant_id (str): The id of the tenant + tenant_data (dict): The data to set on the tenant + + Returns: + dict: A Knock Tenant + """ + endpoint = f'/tenants/{tenant_id}' + return await self.client.connection.request('put', endpoint, payload=tenant_data) + + async def delete(self, tenant_id): + """ + Deletes the given tenant. + + Args: + tenant_id (str): The id of the tenant + + Returns: + None: No response + """ + endpoint = f'/tenants/{tenant_id}' + return await self.client.connection.request('delete', endpoint) diff --git a/knockapi/resources/users.py b/knockapi/async_client/services/users.py similarity index 60% rename from knockapi/resources/users.py rename to knockapi/async_client/services/users.py index 7d381c3..0c70ab3 100644 --- a/knockapi/resources/users.py +++ b/knockapi/async_client/services/users.py @@ -1,44 +1,44 @@ import json -from .service import Service +from knockapi.core.Service import Service from warnings import warn default_set_id = "default" class User(Service): - def get_user(self, id): - warn("This method is deprecated. Use User.get instead.", - DeprecationWarning, stacklevel=2) - return self.get(id) - def get(self, id): + async def get_user(self, user_id): + warn("This method is deprecated. Use User.get instead.", DeprecationWarning, stacklevel=2) + return await self.get(user_id) + + async def get(self, user_id): """ Get a user by their id Args: - id: The user ID + user_id: The user ID Returns: dict: User response from Knock. """ - endpoint = '/users/{}'.format(id) - return self.client.request('get', endpoint) + endpoint = f'/users/{user_id}' + return await self.client.connection.request('get', endpoint) - def identify(self, id, data={}): + async def identify(self, user_id, data={}): """ Identify a user, upserting them Args: - id (str): The user ID + user_id (str): The user ID data (dict): Other properties to put on the user Returns: dict: User response from Knock. """ - endpoint = '/users/{}'.format(id) - return self.client.request('put', endpoint, payload=data) + endpoint = f'/users/{user_id}' + return await self.client.connection.request('put', endpoint, payload=data) - def bulk_identify(self, users): + async def bulk_identify(self, users): """ Bulk identifies up to 100 users @@ -50,22 +50,22 @@ def bulk_identify(self, users): """ endpoint = '/users/bulk/identify' data = {'users': users} - return self.client.request('post', endpoint, payload=data) + return await self.client.connection.request('post', endpoint, payload=data) - def delete(self, id): + async def delete(self, user_id): """ Deletes the given user. Args: - id (str): The user ID + user_id (str): The user ID Returns: None: No response """ - endpoint = '/users/{}'.format(id) - return self.client.request('delete', endpoint) + endpoint = f'/users/{user_id}' + return await self.client.connection.request('delete', endpoint) - def bulk_delete(self, user_ids): + async def bulk_delete(self, user_ids): """ Bulk deletes up to 100 users. @@ -77,9 +77,9 @@ def bulk_delete(self, user_ids): """ endpoint = '/users/bulk/delete' data = {'user_ids': user_ids} - return self.client.request('post', endpoint, payload=data) + return await self.client.connection.request('post', endpoint, payload=data) - def get_feed(self, user_id, channel_id, options=None): + async def get_feed(self, user_id, channel_id, options=None): """ Gets a feed for the given user @@ -91,14 +91,14 @@ def get_feed(self, user_id, channel_id, options=None): Returns dict: A Knock feed response """ - endpoint = '/users/{}/feeds/{}'.format(user_id, channel_id) + endpoint = f'/users/{user_id}/feeds/{channel_id}' if options and options['trigger_data']: options['trigger_data'] = json.dumps(options['trigger_data']) - return self.client.request('get', endpoint, payload=options) + return await self.client.connection.request('get', endpoint, payload=options) - def merge(self, user_id, from_user_id): + async def merge(self, user_id, from_user_id): """ Merges the user specified with `from_user_id` into the user specified with `user_id`. @@ -109,64 +109,62 @@ def merge(self, user_id, from_user_id): Returns dict: A Knock User response """ - endpoint = '/users/{}/merge'.format(user_id) + endpoint = f'/users/{user_id}/merge' data = {'from_user_id': from_user_id} - return self.client.request('post', endpoint, payload=data) + return await self.client.connection.request('post', endpoint, payload=data) ## # Channel data ## - def get_channel_data(self, id, channel_id): + async def get_channel_data(self, user_id, channel_id): """ Get user's channel data for the given channel id. Args: - id (str): The user ID + user_id (str): The user ID channel_id (str): Target channel ID Returns: dict: Channel data from Knock. """ - endpoint = '/users/{}/channel_data/{}'.format(id, channel_id) - return self.client.request('get', endpoint) + endpoint = f'/users/{user_id}/channel_data/{channel_id}' + return await self.client.connection.request('get', endpoint) - def set_channel_data(self, id, channel_id, channel_data): + async def set_channel_data(self, user_id, channel_id, channel_data): """ Upserts user's channel data for the given channel id. Args: - id (str): The user ID + user_id (str): The user ID channel_id (str): Target channel ID channel_data (dict): Channel data Returns: dict: Channel data from Knock. """ - endpoint = '/users/{}/channel_data/{}'.format(id, channel_id) - return self.client.request( - 'put', endpoint, payload={ - 'data': channel_data}) + endpoint = f'/users/{user_id}/channel_data/{channel_id}' + return await self.client.connection.request('put', endpoint, payload={'data': channel_data}) - def unset_channel_data(self, id, channel_id): + async def unset_channel_data(self, user_id, channel_id): """ Unsets the user's channel data for the given channel id. Args: - id (str): The user ID + user_id (str): The user ID channel_id (str): Target channel ID Returns: None: no response """ - endpoint = '/users/{}/channel_data/{}'.format(id, channel_id) - return self.client.request('delete', endpoint) + endpoint = f'/users/{user_id}/channel_data/{channel_id}' + return await self.client.connection.request('delete', endpoint) ## # Preferences ## - def get_all_preferences(self, user_id): + async def get_all_preferences(self, user_id): """ Get a users full set of preferences @@ -176,10 +174,10 @@ def get_all_preferences(self, user_id): Returns: dict: PreferenceSet response from Knock. """ - endpoint = '/users/{}/preferences'.format(user_id) - return self.client.request('get', endpoint) + endpoint = f'/users/{user_id}/preferences' + return await self.client.connection.request('get', endpoint) - def get_preferences(self, user_id, options={}): + async def get_preferences(self, user_id, options={}): """ Get a users preference set @@ -192,18 +190,11 @@ def get_preferences(self, user_id, options={}): dict: PreferenceSet response from Knock. """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/users/{}/preferences/{}'.format( - user_id, preference_set_id) + endpoint = f'/users/{user_id}/preferences/{preference_set_id}' - return self.client.request('get', endpoint) + return await self.client.connection.request('get', endpoint) - def set_preferences( - self, - user_id, - channel_types=None, - categories=None, - workflows=None, - options={}): + async def set_preferences(self, user_id, channel_types=None, categories=None, workflows=None, options={}): """ Sets the preference set for the user @@ -219,8 +210,7 @@ def set_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/users/{}/preferences/{}'.format( - user_id, preference_set_id) + endpoint = f'/users/{user_id}/preferences/{preference_set_id}' params = { 'channel_types': channel_types, @@ -228,9 +218,9 @@ def set_preferences( 'workflows': workflows } - return self.client.request('put', endpoint, payload=params) + return await self.client.connection.request('put', endpoint, payload=params) - def bulk_set_preferences(self, user_ids=[], preferences={}, options={}): + async def bulk_set_preferences(self, user_ids=[], preferences={}, options={}): """ Bulk sets the preference set for the users given @@ -254,9 +244,9 @@ def bulk_set_preferences(self, user_ids=[], preferences={}, options={}): 'user_ids': user_ids, } - return self.client.request('put', endpoint, payload=params) + return await self.client.connection.request('put', endpoint, payload=params) - def set_channel_types_preferences(self, user_id, preferences, options={}): + async def set_channel_types_preferences(self, user_id, preferences, options={}): """ Sets the channel type preferences for the user @@ -270,17 +260,11 @@ def set_channel_types_preferences(self, user_id, preferences, options={}): """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/users/{}/preferences/{}/channel_types'.format( - user_id, preference_set_id) + endpoint = f'/users/{user_id}/preferences/{preference_set_id}/channel_types' - return self.client.request('put', endpoint, payload=preferences) + return await self.client.connection.request('put', endpoint, payload=preferences) - def set_channel_type_preferences( - self, - user_id, - channel_type, - setting, - options={}): + async def set_channel_type_preferences(self, user_id, channel_type, setting, options={}): """ Sets the channel type preference for the user @@ -295,14 +279,11 @@ def set_channel_type_preferences( """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/users/{}/preferences/{}/channel_types/{}'.format( - user_id, preference_set_id, channel_type) + endpoint = f'/users/{user_id}/preferences/{preference_set_id}/channel_types/{channel_type}' - return self.client.request( - 'put', endpoint, payload={ - 'subscribed': setting}) + return await self.client.connection.request('put', endpoint, payload={'subscribed': setting}) - def set_workflows_preferences(self, user_id, preferences, options={}): + async def set_workflows_preferences(self, user_id, preferences, options={}): """ Sets the workflow preferences for the user @@ -316,12 +297,11 @@ def set_workflows_preferences(self, user_id, preferences, options={}): """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/users/{}/preferences/{}/workflows'.format( - user_id, preference_set_id) + endpoint = f'/users/{user_id}/preferences/{preference_set_id}/workflows' - return self.client.request('put', endpoint, payload=preferences) + return await self.client.connection.request('put', endpoint, payload=preferences) - def set_workflow_preferences(self, user_id, key, setting, options={}): + async def set_workflow_preferences(self, user_id, key, setting, options={}): """ Sets the workflow preferences for the user @@ -336,15 +316,13 @@ def set_workflow_preferences(self, user_id, key, setting, options={}): """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/users/{}/preferences/{}/workflows/{}'.format( - user_id, preference_set_id, key) + endpoint = f'/users/{user_id}/preferences/{preference_set_id}/workflows/{key}' - params = setting if isinstance(setting, dict) else { - 'subscribed': setting} + params = setting if isinstance(setting, dict) else {'subscribed': setting} - return self.client.request('put', endpoint, payload=params) + return await self.client.connection.request('put', endpoint, payload=params) - def set_categories_preferences(self, user_id, preferences, options={}): + async def set_categories_preferences(self, user_id, preferences, options={}): """ Sets the categories preferences for the user @@ -358,12 +336,11 @@ def set_categories_preferences(self, user_id, preferences, options={}): """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/users/{}/preferences/{}/categories'.format( - user_id, preference_set_id) + endpoint = f'/users/{user_id}/preferences/{preference_set_id}/categories' - return self.client.request('put', endpoint, payload=preferences) + return await self.client.connection.request('put', endpoint, payload=preferences) - def set_category_preferences(self, user_id, key, setting, options={}): + async def set_category_preferences(self, user_id, key, setting, options={}): """ Sets the category preferences for the user @@ -378,28 +355,26 @@ def set_category_preferences(self, user_id, key, setting, options={}): """ preference_set_id = options.get('preference_set', default_set_id) - endpoint = '/users/{}/preferences/{}/categories/{}'.format( - user_id, preference_set_id, key) + endpoint = f'/users/{user_id}/preferences/{preference_set_id}/categories/{key}' - params = setting if isinstance(setting, dict) else { - 'subscribed': setting} + params = setting if isinstance(setting, dict) else {'subscribed': setting} - return self.client.request('put', endpoint, payload=params) + return await self.client.connection.request('put', endpoint, payload=params) ## # Messages ## - def get_messages(self, id, options=None): + async def get_messages(self, user_id, options=None): """ Get user's messages Args: - id (str): The user ID + user_id (str): The user ID options (dict): An optional set of filtering options to pass to the query Returns: dict: Paginated Message response. """ - endpoint = '/users/{}/messages'.format(id) - return self.client.request('get', endpoint, payload=options) + endpoint = f'/users/{user_id}/messages' + return await self.client.connection.request('get', endpoint, payload=options) diff --git a/knockapi/resources/workflows.py b/knockapi/async_client/services/workflows.py similarity index 75% rename from knockapi/resources/workflows.py rename to knockapi/async_client/services/workflows.py index c352d56..0a64b28 100644 --- a/knockapi/resources/workflows.py +++ b/knockapi/async_client/services/workflows.py @@ -1,15 +1,9 @@ -from .service import Service +from knockapi.core.Service import Service class Workflows(Service): - def trigger( - self, - key, - recipients, - data={}, - actor=None, - cancellation_key=None, - tenant=None): + + async def trigger(self, key, recipients, data={}, actor=None, cancellation_key=None, tenant=None): """ Triggers a workflow. @@ -32,7 +26,7 @@ def trigger( Returns: dict: Response from Knock. """ - endpoint = '/workflows/{}/trigger'.format(key) + endpoint = f'/workflows/{key}/trigger' params = { 'actor': actor, @@ -42,9 +36,9 @@ def trigger( 'tenant': tenant } - return self.client.request("post", endpoint, payload=params) + return await self.client.connection.request("post", endpoint, payload=params) - def cancel(self, key, cancellation_key, recipients=None): + async def cancel(self, key, cancellation_key, recipients=None): """ Cancels an in-flight workflow. @@ -56,11 +50,11 @@ def cancel(self, key, cancellation_key, recipients=None): Returns: dict: Response from Knock. """ - endpoint = '/workflows/{}/cancel'.format(key) + endpoint = f'/workflows/{key}/cancel' params = { 'recipients': recipients, 'cancellation_key': cancellation_key } - return self.client.request("post", endpoint, payload=params) + return await self.client.connection.request("post", endpoint, payload=params) diff --git a/knockapi/core/AsyncConnection.py b/knockapi/core/AsyncConnection.py new file mode 100644 index 0000000..668ce2d --- /dev/null +++ b/knockapi/core/AsyncConnection.py @@ -0,0 +1,45 @@ +import aiohttp + +from json.decoder import JSONDecodeError + + +class AsyncConnection(object): + + def __init__( + self, + api_key: str, + api_host: str, + api_version: str, + read_timeout: int + ): + self.api_key = api_key + self.host = api_host + self.client_version = api_version + self.headers = { + 'Authorization': 'Bearer {}'.format(self.api_key), + 'User-Agent': 'Knock Python - {}'.format(self.client_version) + } + self.session = aiohttp.ClientSession( + raise_for_status=True, + headers=self.headers, + read_timeout=read_timeout + ) + + async def request(self, method, endpoint, payload=None): + url = '{}/v1{}'.format(self.host, endpoint) + + r = await self.session.request( + method, + url, + params=payload if method == 'get' else None, + json=payload if method != 'get' else None + ) + + async with r: + + # If we got a successful response, then attempt to deserialize as JSON + if r.ok: + try: + return await r.json() + except JSONDecodeError: + return None diff --git a/knockapi/core/Connection.py b/knockapi/core/Connection.py new file mode 100644 index 0000000..c5b5bc6 --- /dev/null +++ b/knockapi/core/Connection.py @@ -0,0 +1,43 @@ +import requests + +from json.decoder import JSONDecodeError + + +class Connection(object): + def __init__( + self, + api_key: str, + api_host: str, + api_version: str, + read_timeout: int + ): + self.api_key = api_key + self.host = api_host + self.client_version = api_version + self.timeout = read_timeout + self.headers = { + 'Authorization': 'Bearer {}'.format(self.api_key), + 'User-Agent': 'Knock Python - {}'.format(self.client_version) + } + self.session = requests.Session() + + def request(self, method, endpoint, payload=None): + url = '{}/v1{}'.format(self.host, endpoint) + + r = self.session.request( + method, + url, + params=payload if method == 'get' else None, + json=payload if method != 'get' else None, + headers=self.headers, + timeout=self.timeout + ) + + # If we got a successful response, then attempt to deserialize as JSON + if r.ok: + try: + return r.json() + except JSONDecodeError: + return None + + return r.raise_for_status() diff --git a/knockapi/resources/service.py b/knockapi/core/Service.py similarity index 100% rename from knockapi/resources/service.py rename to knockapi/core/Service.py diff --git a/knockapi/core/__init__.py b/knockapi/core/__init__.py new file mode 100644 index 0000000..e1aafc1 --- /dev/null +++ b/knockapi/core/__init__.py @@ -0,0 +1,3 @@ +from .Service import Service +from .AsyncConnection import AsyncConnection +from .Connection import Connection diff --git a/knockapi/resources/bulk_operations.py b/knockapi/resources/bulk_operations.py deleted file mode 100644 index a04927f..0000000 --- a/knockapi/resources/bulk_operations.py +++ /dev/null @@ -1,18 +0,0 @@ -from .service import Service - -default_set_id = "default" - - -class BulkOperations(Service): - def get(self, id): - """ - Returns an bulk operation. - - Args: - id (str): The id of the bulk operation - - Returns: - dict: A Knock BulkOperation - """ - endpoint = '/bulk_operations/{}'.format(id) - return self.client.request('get', endpoint) diff --git a/knockapi/resources/preferences.py b/knockapi/resources/preferences.py deleted file mode 100644 index ed91203..0000000 --- a/knockapi/resources/preferences.py +++ /dev/null @@ -1,84 +0,0 @@ -from .service import Service -from warnings import warn - - -class Preferences(Service): - def get_all(self, user_id): - warn( - "This method is deprecated. Use users.get_all_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.get_all_preferences(user_id) - - def get(self, user_id, options={}): - warn( - "This method is deprecated. Use users.get_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.get_preferences(user_id, options) - - def update( - self, - user_id, - channel_types=None, - categories=None, - workflows=None, - options={}): - warn( - "This method is deprecated. Use users.set_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.set_preferences( - user_id, - channel_types=channel_types, - categories=categories, - workflows=workflows, - options=options) - - def set_channel_types(self, user_id, preferences, options={}): - warn( - "This method is deprecated. Use users.set_channel_types_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.set_channel_types_preferences( - user_id, preferences, options=options) - - def set_channel_type(self, user_id, channel_type, setting, options={}): - warn( - "This method is deprecated. Use users.set_channel_type_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.set_channel_types_preferences( - user_id, channel_type, setting, options=options) - - def set_workflows(self, user_id, preferences, options={}): - warn( - "This method is deprecated. Use users.set_workflows_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.set_workflows_preferences( - user_id, preferences, options=options) - - def set_workflow(self, user_id, key, setting, options={}): - warn( - "This method is deprecated. Use users.set_workflow_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.set_workflow_preferences( - user_id, key, setting, setting, options=options) - - def set_categories(self, user_id, preferences, options={}): - warn( - "This method is deprecated. Use users.set_categories_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.set_categories_preferences( - user_id, preferences, options=options) - - def set_category(self, user_id, key, setting, options={}): - warn( - "This method is deprecated. Use users.set_category_preferences instead.", - DeprecationWarning, - stacklevel=2) - return self.client.users.set_category_preferences( - user_id, key, setting, setting, options=options) diff --git a/knockapi/resources/tenants.py b/knockapi/resources/tenants.py deleted file mode 100644 index 24790a3..0000000 --- a/knockapi/resources/tenants.py +++ /dev/null @@ -1,57 +0,0 @@ -from .service import Service - - -class Tenants(Service): - def list(self): - """ - Returns all tenants in the environment - - Args: - None - - Returns: - dict: A Knock Tenant - """ - endpoint = '/tenants' - return self.client.request('get', endpoint) - - def get(self, id): - """ - Returns a tenant with the id given. - - Args: - id (str): The id of the tenant - - Returns: - dict: A Knock Tenant - """ - endpoint = '/tenants/{}'.format(id) - return self.client.request('get', endpoint) - - # NOTE: This is `set_tenant` as `set` is a reserved keyword - def set_tenant(self, id, tenant_data={}): - """ - Returns a tenant with the id given and updated settings. - - Args: - id (str): The id of the tenant - tenant_data (dict): The data to set on the tenant - - Returns: - dict: A Knock Tenant - """ - endpoint = '/tenants/{}'.format(id) - return self.client.request('put', endpoint, payload=tenant_data) - - def delete(self, id): - """ - Deletes the given tenant. - - Args: - id (str): The id of the tenant - - Returns: - None: No response - """ - endpoint = '/tenants/{}'.format(id) - return self.client.request('delete', endpoint) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..50df01b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=62.3.0", "wheel", "unasync"] +build-backend = "setuptools.build_meta" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/setup.py b/setup.py index 8b28bfc..b324293 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ import setuptools +import unasync -version = '0.5.2' +version = '0.6.0' with open("README.md", "r") as f: long_description = f.read() @@ -8,9 +9,11 @@ setuptools.setup( name='knockapi', version=version, - python_requires='>=2.7.16, <4', + python_requires='>=3.6, <4', install_requires=[ - 'requests' + 'requests', + 'aiohttp', + 'unasync' ], extras_require={ 'dev': [ @@ -38,5 +41,13 @@ packages=setuptools.find_packages(), author='Knock Labs, Inc.', author_email='support@knock.app', - license='MIT' + license='MIT', + cmdclass={"build_py": unasync.cmdclass_build_py( + rules=[ + unasync.Rule("knockapi/async_client", "knockapi/sync_client", additional_replacements={ + "AsyncConnection": "Connection", + "async_client": "sync_client", + }) + ] + )}, ) diff --git a/tests/test_tenants_class.py b/tests/test_tenants_class.py index 0c06f63..8a00d6b 100644 --- a/tests/test_tenants_class.py +++ b/tests/test_tenants_class.py @@ -1,34 +1,37 @@ -from unittest.mock import Mock +import pytest -from knockapi.resources.tenants import Tenants +from unittest.mock import AsyncMock +from knockapi.async_client.services.tenants import Tenants -# Tests - -def test_list(): - mocked_client = Mock() +@pytest.mark.asyncio +async def test_list(): + mocked_client = AsyncMock() tenants = Tenants(mocked_client) - tenants.list() - mocked_client.request.assert_called_with("get", "/tenants") + await tenants.list() + mocked_client.connection.request.assert_awaited_with("get", "/tenants") -def test_get(): - mocked_client = Mock() +@pytest.mark.asyncio +async def test_get(): + mocked_client = AsyncMock() tenants = Tenants(mocked_client) - tenants.get("tenant-123") - mocked_client.request.assert_called_with("get", "/tenants/tenant-123") + await tenants.get("tenant-123") + mocked_client.connection.request.assert_awaited_with("get", "/tenants/tenant-123") -def test_delete(): - mocked_client = Mock() +@pytest.mark.asyncio +async def test_delete(): + mocked_client = AsyncMock() tenants = Tenants(mocked_client) - tenants.delete("tenant-123") - mocked_client.request.assert_called_with("delete", "/tenants/tenant-123") + await tenants.delete("tenant-123") + mocked_client.connection.request.assert_awaited_with("delete", "/tenants/tenant-123") -def test_set(): - mocked_client = Mock() +@pytest.mark.asyncio +async def test_set(): + mocked_client = AsyncMock() tenants = Tenants(mocked_client) - tenants.set_tenant("tenant-123", {"name": "Tenant 1"}) - mocked_client.request.assert_called_with( + await tenants.set_tenant("tenant-123", {"name": "Tenant 1"}) + mocked_client.connection.request.assert_awaited_with( "put", "/tenants/tenant-123", payload={'name': 'Tenant 1'})