diff --git a/README.rst b/README.rst index 3564b284..ecb0cb05 100644 --- a/README.rst +++ b/README.rst @@ -59,6 +59,8 @@ Contents * `Return all resources across all pages as a list`_ + * `Asynchronous Usage`_ + * `Requests without a Workspace in Scope`_ * `Personal Access Token without a Workspace`_ @@ -428,6 +430,62 @@ Return all resources across all pages as a list all_devices = paginator.flatten_to_list() +Asynchronous Usage +~~~~~~~~~~~~~~~~~~ + +Use ``AsyncSeam`` inside an event loop, e.g., with asyncio-based +frameworks such as FastAPI. +It accepts the same options and exposes the same API methods as ``Seam``, +except every API method is a coroutine that must be awaited. + +Use the client as an async context manager, +or call ``await seam.close()`` when done, +to release the underlying connection pool. + +.. code-block:: python + + import asyncio + + from seam import AsyncSeam + + + async def main(): + async with AsyncSeam() as seam: + devices = await seam.devices.list() + + lock = await seam.locks.get(name="Front Door") + await seam.locks.unlock_door(device_id=lock.device_id) + + + asyncio.run(main()) + +Requests run concurrently with the standard asyncio tools. + +.. code-block:: python + + async def list_resources(seam): + return await asyncio.gather( + seam.devices.list(), + seam.connected_accounts.list(), + ) + +Paginate with the same ``create_paginator`` helper. +The paginator methods are coroutines, +and ``flatten`` returns an async generator. + +.. code-block:: python + + async def list_connected_accounts(seam): + paginator = seam.create_paginator(seam.connected_accounts.list, {"limit": 20}) + + connected_accounts, pagination = await paginator.first_page() + + async for account in paginator.flatten(): + print(account.account_type_display_name) + +The ``AsyncSeamWithoutWorkspace`` client is the equivalent async variant of +``SeamWithoutWorkspace``. + Requests without a Workspace in Scope ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/codegen/layouts/partials/abstract-route-class.hbs b/codegen/layouts/partials/abstract-route-class.hbs index ec969fdb..9c47caea 100644 --- a/codegen/layouts/partials/abstract-route-class.hbs +++ b/codegen/layouts/partials/abstract-route-class.hbs @@ -16,7 +16,7 @@ class {{className}}(abc.ABC): {{#each methods}} @abc.abstractmethod - def {{> method-signature}}: + {{#if ../isAsync}}async {{/if}}def {{> method-signature}}: """{{> method-docstring}}""" raise NotImplementedError() {{/each}} diff --git a/codegen/layouts/partials/abstract-routes.hbs b/codegen/layouts/partials/abstract-routes.hbs index 18f70ffd..3a629aa8 100644 --- a/codegen/layouts/partials/abstract-routes.hbs +++ b/codegen/layouts/partials/abstract-routes.hbs @@ -1,5 +1,5 @@ @dataclass -class AbstractRoutes(abc.ABC): -{{#each routesNamespaces}} +class {{className}}(abc.ABC): +{{#each namespaces}} {{namespace}}: {{abstractClassName}} {{/each}} diff --git a/codegen/layouts/partials/route-method.hbs b/codegen/layouts/partials/route-method.hbs index b615e7f8..d86bd6d5 100644 --- a/codegen/layouts/partials/route-method.hbs +++ b/codegen/layouts/partials/route-method.hbs @@ -1,5 +1,5 @@ @route_metadata(path="{{path}}", has_required_parameters={{#if hasRequiredParameters}}True{{else}}False{{/if}}, has_pagination={{#if hasPagination}}True{{else}}False{{/if}}) - def {{> method-signature}}: + {{#if isAsync}}async {{/if}}def {{> method-signature}}: """{{> method-docstring}}""" {{payloadVar}}: Dict[str, Any] = {} @@ -13,7 +13,7 @@ raise ValueError("At least one parameter is required for {{path}}") {{/if}} - {{#unless (eq returnType "None")}}res = {{/unless}}self.client.{{httpVerb}}("{{path}}", {{payloadArg}}={{payloadVar}}) + {{#unless (eq returnType "None")}}res = {{/unless}}{{#if isAsync}}await {{/if}}self.client.{{httpVerb}}("{{path}}", {{payloadArg}}={{payloadVar}}) {{#if (eq returnType "ActionAttempt")}} wait_for_action_attempt = ( @@ -22,7 +22,7 @@ else wait_for_action_attempt ) - return resolve_action_attempt( + return {{#if isAsync}}await resolve_action_attempt_async{{else}}resolve_action_attempt{{/if}}( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt diff --git a/codegen/layouts/route.hbs b/codegen/layouts/route.hbs index 075e746c..edd132af 100644 --- a/codegen/layouts/route.hbs +++ b/codegen/layouts/route.hbs @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata {{#if importNull}} from ..null import Null @@ -9,16 +9,19 @@ from ..null import Null from ..resources import ({{#each resourceClasses}}{{this}}{{#unless @last}},{{/unless}}{{/each}}) {{/if}} {{#each childClasses}} -from .{{module}} import {{abstractClassName}}, {{className}} +from .{{module}} import {{abstractClassName}}, {{className}}, {{asyncAbstractClassName}}, {{asyncClassName}} {{/each}} {{#if importResolveActionAttempt}} -from ..modules.action_attempts import resolve_action_attempt +from ..modules.action_attempts import resolve_action_attempt, resolve_action_attempt_async {{/if}} {{> abstract-route-class abstractClass}} +{{> abstract-route-class asyncAbstractClass}} + + class {{className}}({{abstractClassName}}): {{#if isDeprecated}} """.. deprecated:: @@ -40,3 +43,26 @@ class {{className}}({{abstractClassName}}): {{> route-method}} {{/each}} + + +class {{asyncClassName}}({{asyncAbstractClassName}}): +{{#if isDeprecated}} + """.. deprecated:: + This route is deprecated.""" +{{/if}} + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults +{{#each childClasses}} + self._{{namespace}} = {{asyncClassName}}(client=client, defaults=defaults) +{{/each}} +{{#each childClasses}} + + @property + def {{namespace}}(self) -> {{asyncClassName}}: + return self._{{namespace}} +{{/each}} +{{#each methods}} + +{{> route-method isAsync=true}} +{{/each}} diff --git a/codegen/layouts/routes-index.hbs b/codegen/layouts/routes-index.hbs index e4c951fd..074ebf82 100644 --- a/codegen/layouts/routes-index.hbs +++ b/codegen/layouts/routes-index.hbs @@ -1,13 +1,16 @@ from typing import Any, Dict import abc from dataclasses import dataclass -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient {{#each namespaces}} -from .{{namespace}} import {{abstractClassName}}, {{className}} +from .{{namespace}} import {{abstractClassName}}, {{className}}, {{asyncAbstractClassName}}, {{asyncClassName}} {{/each}} -{{> abstract-routes}} +{{> abstract-routes abstractRoutes}} + + +{{> abstract-routes asyncAbstractRoutes}} class Routes(AbstractRoutes): @@ -15,3 +18,10 @@ class Routes(AbstractRoutes): {{#each namespaces}} self.{{namespace}} = {{className}}(client=client, defaults=defaults) {{/each}} + + +class AsyncRoutes(AbstractAsyncRoutes): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): +{{#each namespaces}} + self.{{namespace}} = {{asyncClassName}}(client=client, defaults=defaults) +{{/each}} diff --git a/codegen/lib/layouts/route.ts b/codegen/lib/layouts/route.ts index 00c57863..1ff722e1 100644 --- a/codegen/lib/layouts/route.ts +++ b/codegen/lib/layouts/route.ts @@ -35,6 +35,7 @@ export interface MethodLayoutContext { export interface AbstractClassLayoutContext { className: string + isAsync: boolean isDeprecated: boolean showPass: boolean childProperties: Array<{ namespace: string; abstractClassName: string }> @@ -44,13 +45,18 @@ export interface AbstractClassLayoutContext { export interface RouteLayoutContext { className: string abstractClassName: string + asyncClassName: string + asyncAbstractClassName: string isDeprecated: boolean abstractClass: AbstractClassLayoutContext + asyncAbstractClass: AbstractClassLayoutContext resourceClasses: string[] childClasses: Array<{ namespace: string className: string abstractClassName: string + asyncClassName: string + asyncAbstractClassName: string module: string }> importResolveActionAttempt: boolean @@ -109,32 +115,52 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => { ) const abstractClassName = `Abstract${cls.name}` + const asyncClassName = `Async${cls.name}` + const asyncAbstractClassName = `AbstractAsync${cls.name}` const methods = cls.methods.map(getMethodLayoutContext) const importNull = methods.some(({ params }) => params.some(({ isNullable }) => isNullable), ) + const showPass = + cls.methods.length === 0 && cls.childClassIdentifiers.length === 0 + return { className: cls.name, abstractClassName, + asyncClassName, + asyncAbstractClassName, isDeprecated: cls.isDeprecated, abstractClass: { className: abstractClassName, + isAsync: false, isDeprecated: cls.isDeprecated, - showPass: - cls.methods.length === 0 && cls.childClassIdentifiers.length === 0, + showPass, childProperties: cls.childClassIdentifiers.map((identifier) => ({ namespace: identifier.namespace, abstractClassName: `Abstract${identifier.className}`, })), methods, }, + asyncAbstractClass: { + className: asyncAbstractClassName, + isAsync: true, + isDeprecated: cls.isDeprecated, + showPass, + childProperties: cls.childClassIdentifiers.map((identifier) => ({ + namespace: identifier.namespace, + abstractClassName: `AbstractAsync${identifier.className}`, + })), + methods, + }, resourceClasses, childClasses: cls.childClassIdentifiers.map((identifier) => ({ namespace: identifier.namespace, className: identifier.className, abstractClassName: `Abstract${identifier.className}`, + asyncClassName: `Async${identifier.className}`, + asyncAbstractClassName: `AbstractAsync${identifier.className}`, module: `${cls.namespace}_${identifier.namespace}`, })), importResolveActionAttempt, diff --git a/codegen/lib/layouts/routes-index.ts b/codegen/lib/layouts/routes-index.ts index c7aaf0a0..af61a7fe 100644 --- a/codegen/lib/layouts/routes-index.ts +++ b/codegen/lib/layouts/routes-index.ts @@ -5,13 +5,21 @@ import { pascalCase } from 'change-case' +interface AbstractRoutesLayoutContext { + className: string + namespaces: Array<{ namespace: string; abstractClassName: string }> +} + export interface RoutesIndexLayoutContext { namespaces: Array<{ namespace: string className: string abstractClassName: string + asyncClassName: string + asyncAbstractClassName: string }> - routesNamespaces: Array<{ namespace: string; abstractClassName: string }> + abstractRoutes: AbstractRoutesLayoutContext + asyncAbstractRoutes: AbstractRoutesLayoutContext } export const setRoutesIndexLayoutContext = ( @@ -21,9 +29,21 @@ export const setRoutesIndexLayoutContext = ( namespace: ns, className: pascalCase(ns), abstractClassName: `Abstract${pascalCase(ns)}`, + asyncClassName: `Async${pascalCase(ns)}`, + asyncAbstractClassName: `AbstractAsync${pascalCase(ns)}`, })), - routesNamespaces: topLevelNamespaces.map((ns) => ({ - namespace: ns, - abstractClassName: `Abstract${pascalCase(ns)}`, - })), + abstractRoutes: { + className: 'AbstractRoutes', + namespaces: topLevelNamespaces.map((ns) => ({ + namespace: ns, + abstractClassName: `Abstract${pascalCase(ns)}`, + })), + }, + asyncAbstractRoutes: { + className: 'AbstractAsyncRoutes', + namespaces: topLevelNamespaces.map((ns) => ({ + namespace: ns, + abstractClassName: `AbstractAsync${pascalCase(ns)}`, + })), + }, }) diff --git a/pyproject.toml b/pyproject.toml index a0e754df..db646dc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dev = [ "pytest-watch>=4.2.0,<5", "rstcheck>=6.3.0,<7", "mypy>=2.3.0,<3", + "pytest-asyncio>=1.0.0,<2", ] [build-system] @@ -48,3 +49,5 @@ target-version = ["py311"] norecursedirs = [ "node_modules" ] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" diff --git a/seam/__init__.py b/seam/__init__.py index ff7378fd..73fa3bf8 100644 --- a/seam/__init__.py +++ b/seam/__init__.py @@ -1,7 +1,7 @@ # flake8: noqa -from .seam import Seam -from .seam_without_workspace import SeamWithoutWorkspace +from .seam import AsyncSeam, Seam +from .seam_without_workspace import AsyncSeamWithoutWorkspace, SeamWithoutWorkspace from httpx_retries import Retry from .options import SeamInvalidOptionsError from .auth import SeamInvalidTokenError diff --git a/seam/client.py b/seam/client.py index 18f6ace7..0f9e7fe0 100644 --- a/seam/client.py +++ b/seam/client.py @@ -48,7 +48,59 @@ def _handle_error_response(self, response: Response): raise NotImplementedError -class SeamHttpClient(httpx.Client, AbstractSeamHttpClient): +def _build_client_options( + base_url: str, + timeout: Optional[float], + httpx_options: Optional[Dict[str, Any]], + kwargs: Dict[str, Any], +) -> Dict[str, Any]: + return { + "base_url": base_url, + "timeout": timeout, + **kwargs, + **(httpx_options or {}), + } + + +class SeamHttpResponseHandler: + def _handle_response(self, response: Response): + if not 200 <= response.status_code < 300: + self._handle_error_response(response) + + if "application/json" in response.headers.get("content-type", ""): + return response.json() + + return response.text + + def _handle_error_response(self, response: Response): + status_code = response.status_code + request_id = response.headers.get("seam-request-id") + + if status_code == 401: + raise SeamHttpUnauthorizedError(request_id) + + if not is_api_error_response(response): + response.raise_for_status() + + error = response.json().get("error", {}) + error_type = error.get("type", "unknown_error") + error_message = error.get("message", "Unknown error") + error_data = error.get("data", None) + + error_details = { + "type": error_type, + "message": error_message, + "data": error_data, + } + + if error_type == "invalid_input": + error_details["validation_errors"] = error.get("validation_errors") + raise SeamHttpInvalidInputError(error_details, status_code, request_id) + + raise SeamHttpApiError(error_details, status_code, request_id) + + +class SeamHttpClient(httpx.Client, SeamHttpResponseHandler, AbstractSeamHttpClient): def __init__( self, base_url: str, @@ -58,12 +110,7 @@ def __init__( httpx_options: Optional[Dict[str, Any]] = None, **kwargs, ): - options = { - "base_url": base_url, - "timeout": timeout, - **kwargs, - **(httpx_options or {}), - } + options = _build_client_options(base_url, timeout, httpx_options, kwargs) custom_headers = options.pop("headers", {}) self._retry_policy = DEFAULT_RETRIES if retries is None else retries @@ -115,41 +162,66 @@ def request(self, method, url, *args, **kwargs) -> Any: return self._handle_response(response) - def _handle_response(self, response: Response): - if not 200 <= response.status_code < 300: - self._handle_error_response(response) - if "application/json" in response.headers.get("content-type", ""): - return response.json() +class AsyncSeamHttpClient( + httpx.AsyncClient, SeamHttpResponseHandler, AbstractSeamHttpClient +): + def __init__( + self, + base_url: str, + auth_headers: Dict[str, str], + retries: Optional[Retry] = DEFAULT_RETRIES, + timeout: Optional[float] = DEFAULT_TIMEOUT, + httpx_options: Optional[Dict[str, Any]] = None, + **kwargs, + ): + options = _build_client_options(base_url, timeout, httpx_options, kwargs) - return response.text + custom_headers = options.pop("headers", {}) + self._retry_policy = DEFAULT_RETRIES if retries is None else retries - def _handle_error_response(self, response: Response): - status_code = response.status_code - request_id = response.headers.get("seam-request-id") + super().__init__(**options) - if status_code == 401: - raise SeamHttpUnauthorizedError(request_id) + headers = {**auth_headers, **custom_headers, **SDK_HEADERS} + self.headers.update(headers) - if not is_api_error_response(response): - response.raise_for_status() + def _init_transport(self, *args, **kwargs) -> httpx.AsyncBaseTransport: + transport = super()._init_transport(*args, **kwargs) - error = response.json().get("error", {}) - error_type = error.get("type", "unknown_error") - error_message = error.get("message", "Unknown error") - error_data = error.get("data", None) + if kwargs.get("transport") is not None: + return transport - error_details = { - "type": error_type, - "message": error_message, - "data": error_data, - } + return RetryTransport(transport=transport, retry=self._retry_policy) - if error_type == "invalid_input": - error_details["validation_errors"] = error.get("validation_errors") - raise SeamHttpInvalidInputError(error_details, status_code, request_id) + def _init_proxy_transport(self, *args, **kwargs) -> httpx.AsyncBaseTransport: + transport = super()._init_proxy_transport(*args, **kwargs) + return RetryTransport(transport=transport, retry=self._retry_policy) - raise SeamHttpApiError(error_details, status_code, request_id) + async def get(self, url, **kwargs) -> Any: + return await self.request("GET", url, **kwargs) + + async def post(self, url, data=None, json=None, **kwargs) -> Any: + return await self.request("POST", url, data=data, json=json, **kwargs) + + async def put(self, url, data=None, json=None, **kwargs) -> Any: + return await self.request("PUT", url, data=data, json=json, **kwargs) + + async def patch(self, url, data=None, json=None, **kwargs) -> Any: + return await self.request("PATCH", url, data=data, json=json, **kwargs) + + async def delete(self, url, json=None, **kwargs) -> Any: + return await self.request("DELETE", url, json=json, **kwargs) + + async def request(self, method, url, *args, **kwargs) -> Any: + if isinstance(kwargs.get("params"), Mapping): + url = with_search_params(url, kwargs.pop("params")) + + if "json" in kwargs: + kwargs["json"] = replace_null(kwargs["json"]) + + response = await super().request(method, url, *args, **kwargs) + + return self._handle_response(response) def with_search_params(url: Any, params: Mapping[str, Any]) -> Any: diff --git a/seam/models.py b/seam/models.py index 288e2c0f..8339979a 100644 --- a/seam/models.py +++ b/seam/models.py @@ -2,7 +2,7 @@ from typing_extensions import Self import abc -from .routes import AbstractRoutes +from .routes import AbstractAsyncRoutes, AbstractRoutes from .resources import Workspace @@ -43,6 +43,43 @@ def from_personal_access_token( raise NotImplementedError +class AbstractAsyncSeam(AbstractAsyncRoutes): + @abc.abstractmethod + def __init__( + self, + api_key: Optional[str] = None, + *, + personal_access_token: Optional[str] = None, + workspace_id: Optional[str] = None, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + ): + raise NotImplementedError + + @classmethod + @abc.abstractmethod + def from_api_key( + cls, + api_key: str, + *, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + ) -> Self: + raise NotImplementedError + + @classmethod + @abc.abstractmethod + def from_personal_access_token( + cls, + personal_access_token: str, + workspace_id: str, + *, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + ) -> Self: + raise NotImplementedError + + class AbstractSeamWithoutWorkspaceWorkspaces(abc.ABC): @abc.abstractmethod def create( @@ -63,6 +100,26 @@ def list( raise NotImplementedError() +class AbstractAsyncSeamWithoutWorkspaceWorkspaces(abc.ABC): + @abc.abstractmethod + async def create( + self, + *, + connect_partner_name: str, + name: str, + is_sandbox: Optional[bool] = None, + webview_logo_shape: Optional[str] = None, + webview_primary_button_color: Optional[str] = None, + ) -> Workspace: + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + ) -> List[Workspace]: + raise NotImplementedError() + + class AbstractSeamWithoutWorkspace: wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] @@ -86,3 +143,28 @@ def from_personal_access_token( wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, ) -> Self: raise NotImplementedError + + +class AbstractAsyncSeamWithoutWorkspace: + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] + + @abc.abstractmethod + def __init__( + self, + personal_access_token: str, + *, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + ): + raise NotImplementedError + + @classmethod + @abc.abstractmethod + def from_personal_access_token( + cls, + personal_access_token: str, + *, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + ) -> Self: + raise NotImplementedError diff --git a/seam/modules/action_attempts.py b/seam/modules/action_attempts.py index d764d3cb..52ba420d 100644 --- a/seam/modules/action_attempts.py +++ b/seam/modules/action_attempts.py @@ -1,7 +1,8 @@ from typing import Dict, Optional, Union +import asyncio import time -from ..client import SeamHttpClient +from ..client import AsyncSeamHttpClient, SeamHttpClient from ..exceptions import SeamActionAttemptFailedError, SeamActionAttemptTimeoutError from ..resources import ActionAttempt @@ -65,3 +66,63 @@ def resolve_action_attempt( ) return action_attempt + + +async def get_action_attempt_async( + client: AsyncSeamHttpClient, action_attempt_id: str +) -> ActionAttempt: + res = await client.post( + "/action_attempts/get", json={"action_attempt_id": action_attempt_id} + ) + + return ActionAttempt.from_dict(res["action_attempt"]) + + +async def poll_until_ready_async( + client: AsyncSeamHttpClient, + *, + action_attempt_id: str, + timeout: float = TIMEOUT, + polling_interval: float = POLLING_INTERVAL, +) -> ActionAttempt: + time_waiting = 0.0 + + action_attempt = await get_action_attempt_async(client, action_attempt_id) + + while action_attempt.status == "pending": + await asyncio.sleep(polling_interval) + time_waiting += polling_interval + + if time_waiting > timeout: + raise SeamActionAttemptTimeoutError(action_attempt, timeout) + + action_attempt = await get_action_attempt_async(client, action_attempt_id) + + if action_attempt.status == "error": + raise SeamActionAttemptFailedError(action_attempt) + + return action_attempt + + +async def resolve_action_attempt_async( + client: AsyncSeamHttpClient, + *, + action_attempt: ActionAttempt, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]], +) -> ActionAttempt: + if wait_for_action_attempt is True: + return await poll_until_ready_async( + client=client, action_attempt_id=action_attempt.action_attempt_id + ) + + if isinstance(wait_for_action_attempt, dict): + return await poll_until_ready_async( + client=client, + action_attempt_id=action_attempt.action_attempt_id, + timeout=wait_for_action_attempt.get("timeout", TIMEOUT), + polling_interval=wait_for_action_attempt.get( + "polling_interval", POLLING_INTERVAL + ), + ) + + return action_attempt diff --git a/seam/paginator.py b/seam/paginator.py index 62aa3a9b..3f0829ac 100644 --- a/seam/paginator.py +++ b/seam/paginator.py @@ -1,10 +1,27 @@ -from typing import Callable, Dict, Any, Optional, Tuple, Generator, List +from typing import ( + Any, + AsyncGenerator, + Callable, + Dict, + Generator, + List, + Optional, + Tuple, +) from json import JSONDecodeError from httpx import Response -from .client import SeamHttpClient +from .client import AsyncSeamHttpClient, SeamHttpClient from .pagination import Pagination +def parse_pagination(pagination: Dict[str, Any]) -> Pagination: + return Pagination( + has_next_page=pagination.get("has_next_page", False), + next_page_cursor=pagination.get("next_page_cursor"), + next_page_url=pagination.get("next_page_url"), + ) + + class SeamPaginator: """ Handles pagination for API list endpoints. @@ -98,14 +115,117 @@ def _cache_pagination(self, response: Response, page_key: str) -> None: try: # httpx response hooks fire before the response body is read. response.read() - response_json = response.json() - pagination = response_json.get("pagination", {}) + pagination = response.json().get("pagination", {}) except JSONDecodeError: pagination = {} if isinstance(pagination, dict): - self._pagination_cache[page_key] = Pagination( - has_next_page=pagination.get("has_next_page", False), - next_page_cursor=pagination.get("next_page_cursor"), - next_page_url=pagination.get("next_page_url"), + self._pagination_cache[page_key] = parse_pagination(pagination) + + +class AsyncSeamPaginator: + """ + Handles pagination for API list endpoints using an async client. + + Iterates through pages of results returned by an awaitable function. + """ + + _FIRST_PAGE = "FIRST_PAGE" + + def __init__( + self, + client: AsyncSeamHttpClient, + request: Callable, + params: Optional[Dict[str, Any]] = None, + ): + """ + Initializes the Paginator. + + Args: + request: The coroutine function to call to fetch a page of data. + http_client: The async Seam HTTP client used in the request. + params: Initial parameters to pass to the callable function. + """ + self._request = request + self.client = client + self._params = params or {} + self._pagination_cache: Dict[str, Pagination] = {} + + async def first_page(self) -> Tuple[List[Any], Pagination | None]: + """Fetches the first page of results.""" + + async def cache_pagination(response: Response) -> None: + await self._cache_pagination(response, self._FIRST_PAGE) + + self.client.event_hooks["response"].append(cache_pagination) + data = await self._request(**self._params) + self.client.event_hooks["response"].pop() + + pagination = self._pagination_cache.get(self._FIRST_PAGE) + + return data, pagination + + async def next_page( + self, next_page_cursor: str, / + ) -> Tuple[List[Any], Pagination | None]: + """Fetches the next page of results using a cursor.""" + if not next_page_cursor: + raise ValueError("Cannot get the next page with a null next_page_cursor.") + + params = { + **self._params, + "page_cursor": next_page_cursor, + } + + async def cache_pagination(response: Response) -> None: + await self._cache_pagination(response, next_page_cursor) + + self.client.event_hooks["response"].append(cache_pagination) + data = await self._request(**params) + self.client.event_hooks["response"].pop() + + pagination = self._pagination_cache.get(next_page_cursor) + + return data, pagination + + async def flatten_to_list(self) -> List[Any]: + """Fetches all pages and returns all items as a single list.""" + all_items = [] + current_items, pagination = await self.first_page() + + if current_items: + all_items.extend(current_items) + + while pagination and pagination.has_next_page and pagination.next_page_cursor: + current_items, pagination = await self.next_page( + pagination.next_page_cursor ) + if current_items: + all_items.extend(current_items) + + return all_items + + async def flatten(self) -> AsyncGenerator[Any, None]: + """Fetches all pages and yields items one by one using an async generator.""" + current_items, pagination = await self.first_page() + for item in current_items or []: + yield item + + while pagination and pagination.has_next_page and pagination.next_page_cursor: + current_items, pagination = await self.next_page( + pagination.next_page_cursor + ) + for item in current_items or []: + yield item + + async def _cache_pagination(self, response: Response, page_key: str) -> None: + """Extracts pagination dict from response, creates Pagination object, and caches it.""" + try: + # httpx response hooks fire before the response body is read. + await response.aread() + pagination = response.json().get("pagination", {}) + except JSONDecodeError: + pagination = {} + + if isinstance(pagination, dict): + self._pagination_cache[page_key] = parse_pagination(pagination) diff --git a/seam/routes/__init__.py b/seam/routes/__init__.py index 49e7deff..2ad703bb 100644 --- a/seam/routes/__init__.py +++ b/seam/routes/__init__.py @@ -1,27 +1,92 @@ from typing import Any, Dict import abc from dataclasses import dataclass -from ..client import SeamHttpClient -from .access_codes import AbstractAccessCodes, AccessCodes -from .access_grants import AbstractAccessGrants, AccessGrants -from .access_methods import AbstractAccessMethods, AccessMethods -from .acs import AbstractAcs, Acs -from .action_attempts import AbstractActionAttempts, ActionAttempts -from .client_sessions import AbstractClientSessions, ClientSessions -from .connect_webviews import AbstractConnectWebviews, ConnectWebviews -from .connected_accounts import AbstractConnectedAccounts, ConnectedAccounts -from .customers import AbstractCustomers, Customers -from .devices import AbstractDevices, Devices -from .events import AbstractEvents, Events -from .instant_keys import AbstractInstantKeys, InstantKeys -from .locks import AbstractLocks, Locks -from .noise_sensors import AbstractNoiseSensors, NoiseSensors -from .phones import AbstractPhones, Phones -from .spaces import AbstractSpaces, Spaces -from .thermostats import AbstractThermostats, Thermostats -from .user_identities import AbstractUserIdentities, UserIdentities -from .webhooks import AbstractWebhooks, Webhooks -from .workspaces import AbstractWorkspaces, Workspaces +from ..client import SeamHttpClient, AsyncSeamHttpClient +from .access_codes import ( + AbstractAccessCodes, + AccessCodes, + AbstractAsyncAccessCodes, + AsyncAccessCodes, +) +from .access_grants import ( + AbstractAccessGrants, + AccessGrants, + AbstractAsyncAccessGrants, + AsyncAccessGrants, +) +from .access_methods import ( + AbstractAccessMethods, + AccessMethods, + AbstractAsyncAccessMethods, + AsyncAccessMethods, +) +from .acs import AbstractAcs, Acs, AbstractAsyncAcs, AsyncAcs +from .action_attempts import ( + AbstractActionAttempts, + ActionAttempts, + AbstractAsyncActionAttempts, + AsyncActionAttempts, +) +from .client_sessions import ( + AbstractClientSessions, + ClientSessions, + AbstractAsyncClientSessions, + AsyncClientSessions, +) +from .connect_webviews import ( + AbstractConnectWebviews, + ConnectWebviews, + AbstractAsyncConnectWebviews, + AsyncConnectWebviews, +) +from .connected_accounts import ( + AbstractConnectedAccounts, + ConnectedAccounts, + AbstractAsyncConnectedAccounts, + AsyncConnectedAccounts, +) +from .customers import ( + AbstractCustomers, + Customers, + AbstractAsyncCustomers, + AsyncCustomers, +) +from .devices import AbstractDevices, Devices, AbstractAsyncDevices, AsyncDevices +from .events import AbstractEvents, Events, AbstractAsyncEvents, AsyncEvents +from .instant_keys import ( + AbstractInstantKeys, + InstantKeys, + AbstractAsyncInstantKeys, + AsyncInstantKeys, +) +from .locks import AbstractLocks, Locks, AbstractAsyncLocks, AsyncLocks +from .noise_sensors import ( + AbstractNoiseSensors, + NoiseSensors, + AbstractAsyncNoiseSensors, + AsyncNoiseSensors, +) +from .phones import AbstractPhones, Phones, AbstractAsyncPhones, AsyncPhones +from .spaces import AbstractSpaces, Spaces, AbstractAsyncSpaces, AsyncSpaces +from .thermostats import ( + AbstractThermostats, + Thermostats, + AbstractAsyncThermostats, + AsyncThermostats, +) +from .user_identities import ( + AbstractUserIdentities, + UserIdentities, + AbstractAsyncUserIdentities, + AsyncUserIdentities, +) +from .webhooks import AbstractWebhooks, Webhooks, AbstractAsyncWebhooks, AsyncWebhooks +from .workspaces import ( + AbstractWorkspaces, + Workspaces, + AbstractAsyncWorkspaces, + AsyncWorkspaces, +) @dataclass @@ -48,6 +113,30 @@ class AbstractRoutes(abc.ABC): workspaces: AbstractWorkspaces +@dataclass +class AbstractAsyncRoutes(abc.ABC): + access_codes: AbstractAsyncAccessCodes + access_grants: AbstractAsyncAccessGrants + access_methods: AbstractAsyncAccessMethods + acs: AbstractAsyncAcs + action_attempts: AbstractAsyncActionAttempts + client_sessions: AbstractAsyncClientSessions + connect_webviews: AbstractAsyncConnectWebviews + connected_accounts: AbstractAsyncConnectedAccounts + customers: AbstractAsyncCustomers + devices: AbstractAsyncDevices + events: AbstractAsyncEvents + instant_keys: AbstractAsyncInstantKeys + locks: AbstractAsyncLocks + noise_sensors: AbstractAsyncNoiseSensors + phones: AbstractAsyncPhones + spaces: AbstractAsyncSpaces + thermostats: AbstractAsyncThermostats + user_identities: AbstractAsyncUserIdentities + webhooks: AbstractAsyncWebhooks + workspaces: AbstractAsyncWorkspaces + + class Routes(AbstractRoutes): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.access_codes = AccessCodes(client=client, defaults=defaults) @@ -70,3 +159,29 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.user_identities = UserIdentities(client=client, defaults=defaults) self.webhooks = Webhooks(client=client, defaults=defaults) self.workspaces = Workspaces(client=client, defaults=defaults) + + +class AsyncRoutes(AbstractAsyncRoutes): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.access_codes = AsyncAccessCodes(client=client, defaults=defaults) + self.access_grants = AsyncAccessGrants(client=client, defaults=defaults) + self.access_methods = AsyncAccessMethods(client=client, defaults=defaults) + self.acs = AsyncAcs(client=client, defaults=defaults) + self.action_attempts = AsyncActionAttempts(client=client, defaults=defaults) + self.client_sessions = AsyncClientSessions(client=client, defaults=defaults) + self.connect_webviews = AsyncConnectWebviews(client=client, defaults=defaults) + self.connected_accounts = AsyncConnectedAccounts( + client=client, defaults=defaults + ) + self.customers = AsyncCustomers(client=client, defaults=defaults) + self.devices = AsyncDevices(client=client, defaults=defaults) + self.events = AsyncEvents(client=client, defaults=defaults) + self.instant_keys = AsyncInstantKeys(client=client, defaults=defaults) + self.locks = AsyncLocks(client=client, defaults=defaults) + self.noise_sensors = AsyncNoiseSensors(client=client, defaults=defaults) + self.phones = AsyncPhones(client=client, defaults=defaults) + self.spaces = AsyncSpaces(client=client, defaults=defaults) + self.thermostats = AsyncThermostats(client=client, defaults=defaults) + self.user_identities = AsyncUserIdentities(client=client, defaults=defaults) + self.webhooks = AsyncWebhooks(client=client, defaults=defaults) + self.workspaces = AsyncWorkspaces(client=client, defaults=defaults) diff --git a/seam/routes/access_codes.py b/seam/routes/access_codes.py index be1c44a3..c34fe2ca 100644 --- a/seam/routes/access_codes.py +++ b/seam/routes/access_codes.py @@ -1,11 +1,21 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import AccessCode -from .access_codes_simulate import AbstractAccessCodesSimulate, AccessCodesSimulate -from .access_codes_unmanaged import AbstractAccessCodesUnmanaged, AccessCodesUnmanaged +from .access_codes_simulate import ( + AbstractAccessCodesSimulate, + AccessCodesSimulate, + AbstractAsyncAccessCodesSimulate, + AsyncAccessCodesSimulate, +) +from .access_codes_unmanaged import ( + AbstractAccessCodesUnmanaged, + AccessCodesUnmanaged, + AbstractAsyncAccessCodesUnmanaged, + AsyncAccessCodesUnmanaged, +) class AbstractAccessCodes(abc.ABC): @@ -366,25 +376,1012 @@ def update_multiple( raise NotImplementedError() +class AbstractAsyncAccessCodes(abc.ABC): + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAsyncAccessCodesSimulate: + raise NotImplementedError() + + @property + @abc.abstractmethod + def unmanaged(self) -> AbstractAsyncAccessCodesUnmanaged: + raise NotImplementedError() + + @abc.abstractmethod + async def create( + self, + *, + device_id: str, + allow_external_modification: Optional[bool] = None, + attempt_for_offline_device: Optional[bool] = None, + code: Optional[str] = None, + common_code_key: Optional[str] = None, + ends_at: Optional[str] = None, + is_external_modification_allowed: Optional[bool] = None, + is_offline_access_code: Optional[bool] = None, + is_one_time_use: Optional[bool] = None, + max_time_rounding: Optional[str] = None, + name: Optional[str] = None, + prefer_native_scheduling: Optional[bool] = None, + preferred_code_length: Optional[float] = None, + starts_at: Optional[str] = None, + use_backup_access_code_pool: Optional[bool] = None, + use_offline_access_code: Optional[bool] = None, + ) -> AccessCode: + """Creates a new `access code `_. For granting access, we recommend `Access Grants `_ instead: they work across both standalone smart locks and access control systems and manage the underlying codes for you. Use this low-level endpoint only when you need direct control over a code on a single device, such as setting a custom PIN value. + + :param device_id: ID of the device for which you want to create the new access code. + + :param allow_external_modification: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param attempt_for_offline_device: + + :param code: Code to be used for access. + + :param common_code_key: Key to identify access codes that should have the same code. Any two access codes with the same ``common_code_key`` are guaranteed to have the same ``code``. See also `Creating and Updating Multiple Linked Access Codes `_. + + :param ends_at: Date and time at which the validity of the new access code ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param is_offline_access_code: Indicates whether the access code is an `offline access code `_. + + :param is_one_time_use: Indicates whether the `offline access code `_ is a single-use access code. + + :param max_time_rounding: Maximum rounding adjustment. To create a daily-bound `offline access code `_ for devices that support this feature, set this parameter to ``1d``. + + :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. + + Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. + + To provide a consistent experience, Seam identifies the code on Seam by its name but may modify the name that appears on the lock provider's app or on the device. For example, Seam may add additional characters or truncate the name to meet provider constraints. + + To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). + + :param prefer_native_scheduling: Indicates whether `native scheduling `_ should be used for time-bound codes when supported by the provider. Default: ``true``. + + :param preferred_code_length: Preferred code length. Only applicable if you do not specify a ``code``. If the affected device does not support the preferred code length, Seam reverts to using the shortest supported code length. + + :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. + + :param use_backup_access_code_pool: Indicates whether to use a `backup access code pool `_ provided by Seam. If ``true``, you can use ```/access_codes/pull_backup_access_code`` `_. + + :param use_offline_access_code: Deprecated: Use ``is_offline_access_code`` instead. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def create_multiple( + self, + *, + device_ids: List[str], + allow_external_modification: Optional[bool] = None, + attempt_for_offline_device: Optional[bool] = None, + behavior_when_code_cannot_be_shared: Optional[str] = None, + code: Optional[str] = None, + ends_at: Optional[str] = None, + is_external_modification_allowed: Optional[bool] = None, + name: Optional[str] = None, + prefer_native_scheduling: Optional[bool] = None, + preferred_code_length: Optional[float] = None, + starts_at: Optional[str] = None, + use_backup_access_code_pool: Optional[bool] = None, + ) -> List[AccessCode]: + """Creates new `access codes `_ that share a common code across multiple devices. + + Users with more than one door lock in a property may want to create groups of linked access codes, all of which have the same code (PIN). For example, a short-term rental host may want to provide guests the same PIN for both a front door lock and a back door lock. + + If you specify a custom code, Seam assigns this custom code to each of the resulting access codes. However, in this case, Seam does not link these access codes together with a ``common_code_key``. That is, ``common_code_key`` remains null for these access codes. + + If you want to change these access codes that are not linked by a ``common_code_key``, you cannot use ``/access_codes/update_multiple``. However, you can update each of these access codes individually, using ``/access_codes/update``. + + See also `Creating and Updating Multiple Linked Access Codes `_. + + For granting a person access to a space, `Access Grants `_ are the default and recommended approach and work across both standalone smart locks and access systems. Use the lower-level Access Codes API directly only when you specifically need to manage individual PIN codes. + + :param device_ids: IDs of the devices for which you want to create the new access codes. + + :param allow_external_modification: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param attempt_for_offline_device: + + :param behavior_when_code_cannot_be_shared: Desired behavior if any device cannot share a code. If ``throw`` (default), no access codes will be created if any device cannot share a code. If ``create_random_code``, a random code will be created on devices that cannot share a code. + + :param code: Code to be used for access. + + :param ends_at: Date and time at which the validity of the new access code ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. + + Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. + + To provide a consistent experience, Seam identifies the code on Seam by its name but may modify the name that appears on the lock provider's app or on the device. For example, Seam may add additional characters or truncate the name to meet provider constraints. + + To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). + + :param prefer_native_scheduling: Indicates whether `native scheduling `_ should be used for time-bound codes when supported by the provider. Default: ``true``. + + :param preferred_code_length: Preferred code length. If the affected devices do not support the preferred code length, Seam reverts to using the shortest supported code length. + + :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. + + :param use_backup_access_code_pool: Indicates whether to use a `backup access code pool `_ provided by Seam. If ``true``, you can use ```/access_codes/pull_backup_access_code`` `_. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete( + self, *, access_code_id: str, device_id: Optional[str] = None + ) -> None: + """Deletes an `access code `_. + + :param access_code_id: ID of the access code that you want to delete. + + :param device_id: ID of the device for which you want to delete the access code. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def generate_code(self, *, device_id: str) -> AccessCode: + """Generates a code for an `access code `_, given a device ID. + + :param device_id: ID of the device for which you want to generate a code. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, + *, + access_code_id: Optional[str] = None, + code: Optional[str] = None, + device_id: Optional[str] = None, + ) -> AccessCode: + """Returns a specified `access code `_. + + You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param access_code_id: ID of the access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param code: Code of the access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param device_id: ID of the device containing the access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + access_code_ids: Optional[List[str]] = None, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, + access_method_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_id: Optional[str] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> List[AccessCode]: + """Returns a list of all `access codes `_. + + Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param access_code_ids: IDs of the access codes that you want to retrieve. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param access_grant_id: ID of the access grant for which you want to list access codes. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param access_grant_key: Key of the access grant for which you want to list access codes. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param access_method_id: ID of the access method for which you want to list access codes. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param customer_key: Customer key for which you want to list access codes. + + :param device_id: ID of the device for which you want to list access codes. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param limit: Numerical limit on the number of access codes to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned access codes to include all records that satisfy a partial match using ``name``, ``code`` or ``access_code_id``. + + :param user_identifier_key: Your user ID for the user by which to filter access codes. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: + """Retrieves a backup access code for an `access code `_. See also `Managing Backup Access Codes `_. + + A backup access code pool is a collection of pre-programmed access codes stored on a device, ready for use. These codes are programmed in addition to the regular access codes on Seam, serving as a safety net for any issues with the primary codes. If there's ever a complication with a primary access code—be it due to intermittent connectivity, manual removal from a device, or provider outages—a backup code can be retrieved. Its end time can then be adjusted to align with the original code, facilitating seamless and uninterrupted access. + + You can pull a backup access code from the pool at any time. These backup codes are guaranteed to work immediately and automatically programmed to be removed from the device after the access code ends. + + You can only pull backup access codes for time-bound access codes. + + Before pulling a backup access code, make sure that the device's ``properties.supports_backup_access_code_pool`` is ``true``. Then, to activate the backup pool, set ``use_backup_access_code_pool`` to ``true`` when creating an access code. + + :param access_code_id: ID of the access code for which you want to pull a backup access code. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def report_device_constraints( + self, + *, + device_id: str, + max_code_length: Optional[int] = None, + min_code_length: Optional[int] = None, + supported_code_lengths: Optional[List[float]] = None, + ) -> None: + """Enables you to report access code-related constraints for a device. Currently, supports reporting supported code length constraints for SmartThings devices. + + Specify either ``supported_code_lengths`` or ``min_code_length``/``max_code_length``. + + :param device_id: ID of the device for which you want to report constraints. + + :param max_code_length: Maximum supported code length as an integer between 4 and 20, inclusive. You can specify either ``min_code_length``/``max_code_length`` or ``supported_code_lengths``. + + :param min_code_length: Minimum supported code length as an integer between 4 and 20, inclusive. You can specify either ``min_code_length``/``max_code_length`` or ``supported_code_lengths``. + + :param supported_code_lengths: Array of supported code lengths as integers between 4 and 20, inclusive. You can specify either ``supported_code_lengths`` or ``min_code_length``/``max_code_length``. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + access_code_id: str, + allow_external_modification: Optional[bool] = None, + attempt_for_offline_device: Optional[bool] = None, + code: Optional[str] = None, + device_id: Optional[str] = None, + ends_at: Optional[str] = None, + is_external_modification_allowed: Optional[bool] = None, + is_managed: Optional[bool] = None, + name: Optional[str] = None, + starts_at: Optional[str] = None, + type: Optional[str] = None, + ) -> None: + """Updates a specified active or upcoming `access code `_. + + See also `Modifying Access Codes `_. + + :param access_code_id: ID of the access code that you want to update. + + :param allow_external_modification: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param attempt_for_offline_device: + + :param code: Code to be used for access. + + :param device_id: ID of the device containing the access code that you want to update. + + :param ends_at: Date and time at which the validity of the new access code ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param is_managed: Indicates whether the access code is managed through Seam. Note that to convert an unmanaged access code into a managed access code, use ``/access_codes/unmanaged/convert_to_managed``. + + :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. + + Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. + + To provide a consistent experience, Seam identifies the code on Seam by its name but may modify the name that appears on the lock provider's app or on the device. For example, Seam may add additional characters or truncate the name to meet provider constraints. + + To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). + + :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. + + :param type: Type to which you want to convert the access code. To convert a time-bound access code to an ongoing access code, set ``type`` to ``ongoing``. See also `Changing a time-bound access code to permanent access `_. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update_multiple( + self, + *, + common_code_key: str, + ends_at: Optional[str] = None, + name: Optional[str] = None, + starts_at: Optional[str] = None, + ) -> None: + """Updates `access codes `_ that share a common code across multiple devices. + + Specify the ``common_code_key`` to identify the set of access codes that you want to update. + + See also `Update Linked Access Codes `_. + + :param common_code_key: Key that links the group of access codes, assigned on creation by ``/access_codes/create_multiple``. + + :param ends_at: Date and time at which the validity of the new access code ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. + + Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. + + To provide a consistent experience, Seam identifies the code on Seam by its name but may modify the name that appears on the lock provider's app or on the device. For example, Seam may add additional characters or truncate the name to meet provider constraints. + + To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). + + :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AccessCodes(AbstractAccessCodes): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults - self._simulate = AccessCodesSimulate(client=client, defaults=defaults) - self._unmanaged = AccessCodesUnmanaged(client=client, defaults=defaults) + self._simulate = AccessCodesSimulate(client=client, defaults=defaults) + self._unmanaged = AccessCodesUnmanaged(client=client, defaults=defaults) + + @property + def simulate(self) -> AccessCodesSimulate: + return self._simulate + + @property + def unmanaged(self) -> AccessCodesUnmanaged: + return self._unmanaged + + @route_metadata( + path="/access_codes/create", has_required_parameters=True, has_pagination=False + ) + def create( + self, + *, + device_id: str, + allow_external_modification: Optional[bool] = None, + attempt_for_offline_device: Optional[bool] = None, + code: Optional[str] = None, + common_code_key: Optional[str] = None, + ends_at: Optional[str] = None, + is_external_modification_allowed: Optional[bool] = None, + is_offline_access_code: Optional[bool] = None, + is_one_time_use: Optional[bool] = None, + max_time_rounding: Optional[str] = None, + name: Optional[str] = None, + prefer_native_scheduling: Optional[bool] = None, + preferred_code_length: Optional[float] = None, + starts_at: Optional[str] = None, + use_backup_access_code_pool: Optional[bool] = None, + use_offline_access_code: Optional[bool] = None, + ) -> AccessCode: + """Creates a new `access code `_. For granting access, we recommend `Access Grants `_ instead: they work across both standalone smart locks and access control systems and manage the underlying codes for you. Use this low-level endpoint only when you need direct control over a code on a single device, such as setting a custom PIN value. + + :param device_id: ID of the device for which you want to create the new access code. + + :param allow_external_modification: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param attempt_for_offline_device: + + :param code: Code to be used for access. + + :param common_code_key: Key to identify access codes that should have the same code. Any two access codes with the same ``common_code_key`` are guaranteed to have the same ``code``. See also `Creating and Updating Multiple Linked Access Codes `_. + + :param ends_at: Date and time at which the validity of the new access code ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param is_offline_access_code: Indicates whether the access code is an `offline access code `_. + + :param is_one_time_use: Indicates whether the `offline access code `_ is a single-use access code. + + :param max_time_rounding: Maximum rounding adjustment. To create a daily-bound `offline access code `_ for devices that support this feature, set this parameter to ``1d``. + + :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. + + Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. + + To provide a consistent experience, Seam identifies the code on Seam by its name but may modify the name that appears on the lock provider's app or on the device. For example, Seam may add additional characters or truncate the name to meet provider constraints. + + To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). + + :param prefer_native_scheduling: Indicates whether `native scheduling `_ should be used for time-bound codes when supported by the provider. Default: ``true``. + + :param preferred_code_length: Preferred code length. Only applicable if you do not specify a ``code``. If the affected device does not support the preferred code length, Seam reverts to using the shortest supported code length. + + :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. + + :param use_backup_access_code_pool: Indicates whether to use a `backup access code pool `_ provided by Seam. If ``true``, you can use ```/access_codes/pull_backup_access_code`` `_. + + :param use_offline_access_code: Deprecated: Use ``is_offline_access_code`` instead. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if allow_external_modification is not None: + json_payload["allow_external_modification"] = allow_external_modification + if attempt_for_offline_device is not None: + json_payload["attempt_for_offline_device"] = attempt_for_offline_device + if code is not None: + json_payload["code"] = code + if common_code_key is not None: + json_payload["common_code_key"] = common_code_key + if ends_at is not None: + json_payload["ends_at"] = ends_at + if is_external_modification_allowed is not None: + json_payload["is_external_modification_allowed"] = ( + is_external_modification_allowed + ) + if is_offline_access_code is not None: + json_payload["is_offline_access_code"] = is_offline_access_code + if is_one_time_use is not None: + json_payload["is_one_time_use"] = is_one_time_use + if max_time_rounding is not None: + json_payload["max_time_rounding"] = max_time_rounding + if name is not None: + json_payload["name"] = name + if prefer_native_scheduling is not None: + json_payload["prefer_native_scheduling"] = prefer_native_scheduling + if preferred_code_length is not None: + json_payload["preferred_code_length"] = preferred_code_length + if starts_at is not None: + json_payload["starts_at"] = starts_at + if use_backup_access_code_pool is not None: + json_payload["use_backup_access_code_pool"] = use_backup_access_code_pool + if use_offline_access_code is not None: + json_payload["use_offline_access_code"] = use_offline_access_code + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/create" + ) + + res = self.client.post("/access_codes/create", json=json_payload) + + return AccessCode.from_dict(res["access_code"]) + + @route_metadata( + path="/access_codes/create_multiple", + has_required_parameters=True, + has_pagination=False, + ) + def create_multiple( + self, + *, + device_ids: List[str], + allow_external_modification: Optional[bool] = None, + attempt_for_offline_device: Optional[bool] = None, + behavior_when_code_cannot_be_shared: Optional[str] = None, + code: Optional[str] = None, + ends_at: Optional[str] = None, + is_external_modification_allowed: Optional[bool] = None, + name: Optional[str] = None, + prefer_native_scheduling: Optional[bool] = None, + preferred_code_length: Optional[float] = None, + starts_at: Optional[str] = None, + use_backup_access_code_pool: Optional[bool] = None, + ) -> List[AccessCode]: + """Creates new `access codes `_ that share a common code across multiple devices. + + Users with more than one door lock in a property may want to create groups of linked access codes, all of which have the same code (PIN). For example, a short-term rental host may want to provide guests the same PIN for both a front door lock and a back door lock. + + If you specify a custom code, Seam assigns this custom code to each of the resulting access codes. However, in this case, Seam does not link these access codes together with a ``common_code_key``. That is, ``common_code_key`` remains null for these access codes. + + If you want to change these access codes that are not linked by a ``common_code_key``, you cannot use ``/access_codes/update_multiple``. However, you can update each of these access codes individually, using ``/access_codes/update``. + + See also `Creating and Updating Multiple Linked Access Codes `_. + + For granting a person access to a space, `Access Grants `_ are the default and recommended approach and work across both standalone smart locks and access systems. Use the lower-level Access Codes API directly only when you specifically need to manage individual PIN codes. + + :param device_ids: IDs of the devices for which you want to create the new access codes. + + :param allow_external_modification: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param attempt_for_offline_device: + + :param behavior_when_code_cannot_be_shared: Desired behavior if any device cannot share a code. If ``throw`` (default), no access codes will be created if any device cannot share a code. If ``create_random_code``, a random code will be created on devices that cannot share a code. + + :param code: Code to be used for access. + + :param ends_at: Date and time at which the validity of the new access code ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. + + Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. + + To provide a consistent experience, Seam identifies the code on Seam by its name but may modify the name that appears on the lock provider's app or on the device. For example, Seam may add additional characters or truncate the name to meet provider constraints. + + To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). + + :param prefer_native_scheduling: Indicates whether `native scheduling `_ should be used for time-bound codes when supported by the provider. Default: ``true``. + + :param preferred_code_length: Preferred code length. If the affected devices do not support the preferred code length, Seam reverts to using the shortest supported code length. + + :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. + + :param use_backup_access_code_pool: Indicates whether to use a `backup access code pool `_ provided by Seam. If ``true``, you can use ```/access_codes/pull_backup_access_code`` `_. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_ids is not None: + json_payload["device_ids"] = device_ids + if allow_external_modification is not None: + json_payload["allow_external_modification"] = allow_external_modification + if attempt_for_offline_device is not None: + json_payload["attempt_for_offline_device"] = attempt_for_offline_device + if behavior_when_code_cannot_be_shared is not None: + json_payload["behavior_when_code_cannot_be_shared"] = ( + behavior_when_code_cannot_be_shared + ) + if code is not None: + json_payload["code"] = code + if ends_at is not None: + json_payload["ends_at"] = ends_at + if is_external_modification_allowed is not None: + json_payload["is_external_modification_allowed"] = ( + is_external_modification_allowed + ) + if name is not None: + json_payload["name"] = name + if prefer_native_scheduling is not None: + json_payload["prefer_native_scheduling"] = prefer_native_scheduling + if preferred_code_length is not None: + json_payload["preferred_code_length"] = preferred_code_length + if starts_at is not None: + json_payload["starts_at"] = starts_at + if use_backup_access_code_pool is not None: + json_payload["use_backup_access_code_pool"] = use_backup_access_code_pool + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/create_multiple" + ) + + res = self.client.put("/access_codes/create_multiple", json=json_payload) + + return [AccessCode.from_dict(item) for item in res["access_codes"]] + + @route_metadata( + path="/access_codes/delete", has_required_parameters=True, has_pagination=False + ) + def delete(self, *, access_code_id: str, device_id: Optional[str] = None) -> None: + """Deletes an `access code `_. + + :param access_code_id: ID of the access code that you want to delete. + + :param device_id: ID of the device for which you want to delete the access code. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_code_id is not None: + params["access_code_id"] = access_code_id + if device_id is not None: + params["device_id"] = device_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/delete" + ) + + self.client.delete("/access_codes/delete", params=params) + + return None + + @route_metadata( + path="/access_codes/generate_code", + has_required_parameters=True, + has_pagination=False, + ) + def generate_code(self, *, device_id: str) -> AccessCode: + """Generates a code for an `access code `_, given a device ID. + + :param device_id: ID of the device for which you want to generate a code. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/generate_code" + ) + + res = self.client.get("/access_codes/generate_code", params=params) + + return AccessCode.from_dict(res["generated_code"]) + + @route_metadata( + path="/access_codes/get", has_required_parameters=True, has_pagination=False + ) + def get( + self, + *, + access_code_id: Optional[str] = None, + code: Optional[str] = None, + device_id: Optional[str] = None, + ) -> AccessCode: + """Returns a specified `access code `_. + + You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param access_code_id: ID of the access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param code: Code of the access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param device_id: ID of the device containing the access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_code_id is not None: + params["access_code_id"] = access_code_id + if code is not None: + params["code"] = code + if device_id is not None: + params["device_id"] = device_id + + if not params: + raise ValueError("At least one parameter is required for /access_codes/get") + + res = self.client.get("/access_codes/get", params=params) + + return AccessCode.from_dict(res["access_code"]) + + @route_metadata( + path="/access_codes/list", has_required_parameters=True, has_pagination=True + ) + def list( + self, + *, + access_code_ids: Optional[List[str]] = None, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, + access_method_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_id: Optional[str] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> List[AccessCode]: + """Returns a list of all `access codes `_. + + Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param access_code_ids: IDs of the access codes that you want to retrieve. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param access_grant_id: ID of the access grant for which you want to list access codes. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param access_grant_key: Key of the access grant for which you want to list access codes. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param access_method_id: ID of the access method for which you want to list access codes. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param customer_key: Customer key for which you want to list access codes. + + :param device_id: ID of the device for which you want to list access codes. Specify ``device_id``, ``access_code_ids``, ``access_method_id``, ``access_grant_id``, or ``access_grant_key``. + + :param limit: Numerical limit on the number of access codes to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned access codes to include all records that satisfy a partial match using ``name``, ``code`` or ``access_code_id``. + + :param user_identifier_key: Your user ID for the user by which to filter access codes. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_code_ids is not None: + params["access_code_ids"] = access_code_ids + if access_grant_id is not None: + params["access_grant_id"] = access_grant_id + if access_grant_key is not None: + params["access_grant_key"] = access_grant_key + if access_method_id is not None: + params["access_method_id"] = access_method_id + if customer_key is not None: + params["customer_key"] = customer_key + if device_id is not None: + params["device_id"] = device_id + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if user_identifier_key is not None: + params["user_identifier_key"] = user_identifier_key + + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/list" + ) + + res = self.client.get("/access_codes/list", params=params) + + return [AccessCode.from_dict(item) for item in res["access_codes"]] + + @route_metadata( + path="/access_codes/pull_backup_access_code", + has_required_parameters=True, + has_pagination=False, + ) + def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: + """Retrieves a backup access code for an `access code `_. See also `Managing Backup Access Codes `_. + + A backup access code pool is a collection of pre-programmed access codes stored on a device, ready for use. These codes are programmed in addition to the regular access codes on Seam, serving as a safety net for any issues with the primary codes. If there's ever a complication with a primary access code—be it due to intermittent connectivity, manual removal from a device, or provider outages—a backup code can be retrieved. Its end time can then be adjusted to align with the original code, facilitating seamless and uninterrupted access. + + You can pull a backup access code from the pool at any time. These backup codes are guaranteed to work immediately and automatically programmed to be removed from the device after the access code ends. + + You can only pull backup access codes for time-bound access codes. + + Before pulling a backup access code, make sure that the device's ``properties.supports_backup_access_code_pool`` is ``true``. Then, to activate the backup pool, set ``use_backup_access_code_pool`` to ``true`` when creating an access code. + + :param access_code_id: ID of the access code for which you want to pull a backup access code. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_code_id is not None: + json_payload["access_code_id"] = access_code_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/pull_backup_access_code" + ) + + res = self.client.post( + "/access_codes/pull_backup_access_code", json=json_payload + ) + + return AccessCode.from_dict(res["access_code"]) + + @route_metadata( + path="/access_codes/report_device_constraints", + has_required_parameters=True, + has_pagination=False, + ) + def report_device_constraints( + self, + *, + device_id: str, + max_code_length: Optional[int] = None, + min_code_length: Optional[int] = None, + supported_code_lengths: Optional[List[float]] = None, + ) -> None: + """Enables you to report access code-related constraints for a device. Currently, supports reporting supported code length constraints for SmartThings devices. + + Specify either ``supported_code_lengths`` or ``min_code_length``/``max_code_length``. + + :param device_id: ID of the device for which you want to report constraints. + + :param max_code_length: Maximum supported code length as an integer between 4 and 20, inclusive. You can specify either ``min_code_length``/``max_code_length`` or ``supported_code_lengths``. + + :param min_code_length: Minimum supported code length as an integer between 4 and 20, inclusive. You can specify either ``min_code_length``/``max_code_length`` or ``supported_code_lengths``. + + :param supported_code_lengths: Array of supported code lengths as integers between 4 and 20, inclusive. You can specify either ``supported_code_lengths`` or ``min_code_length``/``max_code_length``. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if max_code_length is not None: + json_payload["max_code_length"] = max_code_length + if min_code_length is not None: + json_payload["min_code_length"] = min_code_length + if supported_code_lengths is not None: + json_payload["supported_code_lengths"] = supported_code_lengths + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/report_device_constraints" + ) + + self.client.post("/access_codes/report_device_constraints", json=json_payload) + + return None + + @route_metadata( + path="/access_codes/update", has_required_parameters=True, has_pagination=False + ) + def update( + self, + *, + access_code_id: str, + allow_external_modification: Optional[bool] = None, + attempt_for_offline_device: Optional[bool] = None, + code: Optional[str] = None, + device_id: Optional[str] = None, + ends_at: Optional[str] = None, + is_external_modification_allowed: Optional[bool] = None, + is_managed: Optional[bool] = None, + name: Optional[str] = None, + starts_at: Optional[str] = None, + type: Optional[str] = None, + ) -> None: + """Updates a specified active or upcoming `access code `_. + + See also `Modifying Access Codes `_. + + :param access_code_id: ID of the access code that you want to update. + + :param allow_external_modification: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param attempt_for_offline_device: + + :param code: Code to be used for access. + + :param device_id: ID of the device containing the access code that you want to update. + + :param ends_at: Date and time at which the validity of the new access code ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. Default: ``false``. + + :param is_managed: Indicates whether the access code is managed through Seam. Note that to convert an unmanaged access code into a managed access code, use ``/access_codes/unmanaged/convert_to_managed``. + + :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. + + Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. + + To provide a consistent experience, Seam identifies the code on Seam by its name but may modify the name that appears on the lock provider's app or on the device. For example, Seam may add additional characters or truncate the name to meet provider constraints. + + To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). + + :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. + + :param type: Type to which you want to convert the access code. To convert a time-bound access code to an ongoing access code, set ``type`` to ``ongoing``. See also `Changing a time-bound access code to permanent access `_. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_code_id is not None: + json_payload["access_code_id"] = access_code_id + if allow_external_modification is not None: + json_payload["allow_external_modification"] = allow_external_modification + if attempt_for_offline_device is not None: + json_payload["attempt_for_offline_device"] = attempt_for_offline_device + if code is not None: + json_payload["code"] = code + if device_id is not None: + json_payload["device_id"] = device_id + if ends_at is not None: + json_payload["ends_at"] = ends_at + if is_external_modification_allowed is not None: + json_payload["is_external_modification_allowed"] = ( + is_external_modification_allowed + ) + if is_managed is not None: + json_payload["is_managed"] = is_managed + if name is not None: + json_payload["name"] = name + if starts_at is not None: + json_payload["starts_at"] = starts_at + if type is not None: + json_payload["type"] = type + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/update" + ) + + self.client.put("/access_codes/update", json=json_payload) + + return None + + @route_metadata( + path="/access_codes/update_multiple", + has_required_parameters=True, + has_pagination=False, + ) + def update_multiple( + self, + *, + common_code_key: str, + ends_at: Optional[str] = None, + name: Optional[str] = None, + starts_at: Optional[str] = None, + ) -> None: + """Updates `access codes `_ that share a common code across multiple devices. + + Specify the ``common_code_key`` to identify the set of access codes that you want to update. + + See also `Update Linked Access Codes `_. + + :param common_code_key: Key that links the group of access codes, assigned on creation by ``/access_codes/create_multiple``. + + :param ends_at: Date and time at which the validity of the new access code ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. + + Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. + + To provide a consistent experience, Seam identifies the code on Seam by its name but may modify the name that appears on the lock provider's app or on the device. For example, Seam may add additional characters or truncate the name to meet provider constraints. + + To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). + + :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if common_code_key is not None: + json_payload["common_code_key"] = common_code_key + if ends_at is not None: + json_payload["ends_at"] = ends_at + if name is not None: + json_payload["name"] = name + if starts_at is not None: + json_payload["starts_at"] = starts_at + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/update_multiple" + ) + + self.client.patch("/access_codes/update_multiple", json=json_payload) + + return None + + +class AsyncAccessCodes(AbstractAsyncAccessCodes): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._simulate = AsyncAccessCodesSimulate(client=client, defaults=defaults) + self._unmanaged = AsyncAccessCodesUnmanaged(client=client, defaults=defaults) @property - def simulate(self) -> AccessCodesSimulate: + def simulate(self) -> AsyncAccessCodesSimulate: return self._simulate @property - def unmanaged(self) -> AccessCodesUnmanaged: + def unmanaged(self) -> AsyncAccessCodesUnmanaged: return self._unmanaged @route_metadata( path="/access_codes/create", has_required_parameters=True, has_pagination=False ) - def create( + async def create( self, *, device_id: str, @@ -489,7 +1486,7 @@ def create( "At least one parameter is required for /access_codes/create" ) - res = self.client.post("/access_codes/create", json=json_payload) + res = await self.client.post("/access_codes/create", json=json_payload) return AccessCode.from_dict(res["access_code"]) @@ -498,7 +1495,7 @@ def create( has_required_parameters=True, has_pagination=False, ) - def create_multiple( + async def create_multiple( self, *, device_ids: List[str], @@ -595,14 +1592,16 @@ def create_multiple( "At least one parameter is required for /access_codes/create_multiple" ) - res = self.client.put("/access_codes/create_multiple", json=json_payload) + res = await self.client.put("/access_codes/create_multiple", json=json_payload) return [AccessCode.from_dict(item) for item in res["access_codes"]] @route_metadata( path="/access_codes/delete", has_required_parameters=True, has_pagination=False ) - def delete(self, *, access_code_id: str, device_id: Optional[str] = None) -> None: + async def delete( + self, *, access_code_id: str, device_id: Optional[str] = None + ) -> None: """Deletes an `access code `_. :param access_code_id: ID of the access code that you want to delete. @@ -622,7 +1621,7 @@ def delete(self, *, access_code_id: str, device_id: Optional[str] = None) -> Non "At least one parameter is required for /access_codes/delete" ) - self.client.delete("/access_codes/delete", params=params) + await self.client.delete("/access_codes/delete", params=params) return None @@ -631,7 +1630,7 @@ def delete(self, *, access_code_id: str, device_id: Optional[str] = None) -> Non has_required_parameters=True, has_pagination=False, ) - def generate_code(self, *, device_id: str) -> AccessCode: + async def generate_code(self, *, device_id: str) -> AccessCode: """Generates a code for an `access code `_, given a device ID. :param device_id: ID of the device for which you want to generate a code. @@ -649,14 +1648,14 @@ def generate_code(self, *, device_id: str) -> AccessCode: "At least one parameter is required for /access_codes/generate_code" ) - res = self.client.get("/access_codes/generate_code", params=params) + res = await self.client.get("/access_codes/generate_code", params=params) return AccessCode.from_dict(res["generated_code"]) @route_metadata( path="/access_codes/get", has_required_parameters=True, has_pagination=False ) - def get( + async def get( self, *, access_code_id: Optional[str] = None, @@ -688,14 +1687,14 @@ def get( if not params: raise ValueError("At least one parameter is required for /access_codes/get") - res = self.client.get("/access_codes/get", params=params) + res = await self.client.get("/access_codes/get", params=params) return AccessCode.from_dict(res["access_code"]) @route_metadata( path="/access_codes/list", has_required_parameters=True, has_pagination=True ) - def list( + async def list( self, *, access_code_ids: Optional[List[str]] = None, @@ -764,7 +1763,7 @@ def list( "At least one parameter is required for /access_codes/list" ) - res = self.client.get("/access_codes/list", params=params) + res = await self.client.get("/access_codes/list", params=params) return [AccessCode.from_dict(item) for item in res["access_codes"]] @@ -773,7 +1772,7 @@ def list( has_required_parameters=True, has_pagination=False, ) - def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: + async def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: """Retrieves a backup access code for an `access code `_. See also `Managing Backup Access Codes `_. A backup access code pool is a collection of pre-programmed access codes stored on a device, ready for use. These codes are programmed in addition to the regular access codes on Seam, serving as a safety net for any issues with the primary codes. If there's ever a complication with a primary access code—be it due to intermittent connectivity, manual removal from a device, or provider outages—a backup code can be retrieved. Its end time can then be adjusted to align with the original code, facilitating seamless and uninterrupted access. @@ -799,7 +1798,7 @@ def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: "At least one parameter is required for /access_codes/pull_backup_access_code" ) - res = self.client.post( + res = await self.client.post( "/access_codes/pull_backup_access_code", json=json_payload ) @@ -810,7 +1809,7 @@ def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: has_required_parameters=True, has_pagination=False, ) - def report_device_constraints( + async def report_device_constraints( self, *, device_id: str, @@ -847,14 +1846,16 @@ def report_device_constraints( "At least one parameter is required for /access_codes/report_device_constraints" ) - self.client.post("/access_codes/report_device_constraints", json=json_payload) + await self.client.post( + "/access_codes/report_device_constraints", json=json_payload + ) return None @route_metadata( path="/access_codes/update", has_required_parameters=True, has_pagination=False ) - def update( + async def update( self, *, access_code_id: str, @@ -934,7 +1935,7 @@ def update( "At least one parameter is required for /access_codes/update" ) - self.client.put("/access_codes/update", json=json_payload) + await self.client.put("/access_codes/update", json=json_payload) return None @@ -943,7 +1944,7 @@ def update( has_required_parameters=True, has_pagination=False, ) - def update_multiple( + async def update_multiple( self, *, common_code_key: str, @@ -988,6 +1989,6 @@ def update_multiple( "At least one parameter is required for /access_codes/update_multiple" ) - self.client.patch("/access_codes/update_multiple", json=json_payload) + await self.client.patch("/access_codes/update_multiple", json=json_payload) return None diff --git a/seam/routes/access_codes_simulate.py b/seam/routes/access_codes_simulate.py index 1faed843..3c1040e2 100644 --- a/seam/routes/access_codes_simulate.py +++ b/seam/routes/access_codes_simulate.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import UnmanagedAccessCode @@ -25,6 +25,26 @@ def create_unmanaged_access_code( raise NotImplementedError() +class AbstractAsyncAccessCodesSimulate(abc.ABC): + + @abc.abstractmethod + async def create_unmanaged_access_code( + self, *, code: str, device_id: str, name: str + ) -> UnmanagedAccessCode: + """Simulates the creation of an `unmanaged access code `_ in a `sandbox workspace `_. + + :param code: Code of the simulated unmanaged access code. + + :param device_id: ID of the device for which you want to simulate the creation of an unmanaged access code. + + :param name: Name of the simulated unmanaged access code. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AccessCodesSimulate(AbstractAccessCodesSimulate): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -68,3 +88,48 @@ def create_unmanaged_access_code( ) return UnmanagedAccessCode.from_dict(res["access_code"]) + + +class AsyncAccessCodesSimulate(AbstractAsyncAccessCodesSimulate): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/access_codes/simulate/create_unmanaged_access_code", + has_required_parameters=True, + has_pagination=False, + ) + async def create_unmanaged_access_code( + self, *, code: str, device_id: str, name: str + ) -> UnmanagedAccessCode: + """Simulates the creation of an `unmanaged access code `_ in a `sandbox workspace `_. + + :param code: Code of the simulated unmanaged access code. + + :param device_id: ID of the device for which you want to simulate the creation of an unmanaged access code. + + :param name: Name of the simulated unmanaged access code. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if code is not None: + json_payload["code"] = code + if device_id is not None: + json_payload["device_id"] = device_id + if name is not None: + json_payload["name"] = name + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/simulate/create_unmanaged_access_code" + ) + + res = await self.client.post( + "/access_codes/simulate/create_unmanaged_access_code", json=json_payload + ) + + return UnmanagedAccessCode.from_dict(res["access_code"]) diff --git a/seam/routes/access_codes_unmanaged.py b/seam/routes/access_codes_unmanaged.py index 0f53f032..8a8cd367 100644 --- a/seam/routes/access_codes_unmanaged.py +++ b/seam/routes/access_codes_unmanaged.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import UnmanagedAccessCode @@ -119,6 +119,119 @@ def update( raise NotImplementedError() +class AbstractAsyncAccessCodesUnmanaged(abc.ABC): + + @abc.abstractmethod + async def convert_to_managed( + self, + *, + access_code_id: str, + allow_external_modification: Optional[bool] = None, + force: Optional[bool] = None, + is_external_modification_allowed: Optional[bool] = None, + ) -> None: + """Converts an `unmanaged access code `_ to an `access code managed through Seam `_. + + An unmanaged access code has a limited set of operations that you can perform on it. Once you convert an unmanaged access code to a managed access code, the full set of access code operations and lifecycle events becomes available for it. + + Note that not all device providers support converting an unmanaged access code to a managed access code. + + :param access_code_id: ID of the unmanaged access code that you want to convert to a managed access code. + + :param allow_external_modification: Indicates whether `external modification `_ of the access code is allowed. + + :param force: Indicates whether to force the access code conversion. To switch management of an access code from one Seam workspace to another, set ``force`` to ``true``. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the access code is allowed. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, access_code_id: str) -> None: + """Deletes an `unmanaged access code `_. + + :param access_code_id: ID of the unmanaged access code that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, + *, + access_code_id: Optional[str] = None, + code: Optional[str] = None, + device_id: Optional[str] = None, + ) -> UnmanagedAccessCode: + """Returns a specified `unmanaged access code `_. + + You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param access_code_id: ID of the unmanaged access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param code: Code of the unmanaged access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param device_id: ID of the device containing the unmanaged access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + device_id: str, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> List[UnmanagedAccessCode]: + """Returns a list of all `unmanaged access codes `_. + + :param device_id: ID of the device for which you want to list unmanaged access codes. + + :param limit: Numerical limit on the number of unmanaged access codes to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned access codes to include all records that satisfy a partial match using ``name``, ``code`` or ``access_code_id``. + + :param user_identifier_key: Your user ID for the user by which to filter unmanaged access codes. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + access_code_id: str, + is_managed: bool, + allow_external_modification: Optional[bool] = None, + force: Optional[bool] = None, + is_external_modification_allowed: Optional[bool] = None, + ) -> None: + """Updates a specified `unmanaged access code `_. + + :param access_code_id: ID of the unmanaged access code that you want to update. + + :param is_managed: + + :param allow_external_modification: Indicates whether `external modification `_ of the code is allowed. + + :param force: Indicates whether to force the unmanaged access code update. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AccessCodesUnmanaged(AbstractAccessCodesUnmanaged): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -345,3 +458,231 @@ def update( self.client.patch("/access_codes/unmanaged/update", json=json_payload) return None + + +class AsyncAccessCodesUnmanaged(AbstractAsyncAccessCodesUnmanaged): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/access_codes/unmanaged/convert_to_managed", + has_required_parameters=True, + has_pagination=False, + ) + async def convert_to_managed( + self, + *, + access_code_id: str, + allow_external_modification: Optional[bool] = None, + force: Optional[bool] = None, + is_external_modification_allowed: Optional[bool] = None, + ) -> None: + """Converts an `unmanaged access code `_ to an `access code managed through Seam `_. + + An unmanaged access code has a limited set of operations that you can perform on it. Once you convert an unmanaged access code to a managed access code, the full set of access code operations and lifecycle events becomes available for it. + + Note that not all device providers support converting an unmanaged access code to a managed access code. + + :param access_code_id: ID of the unmanaged access code that you want to convert to a managed access code. + + :param allow_external_modification: Indicates whether `external modification `_ of the access code is allowed. + + :param force: Indicates whether to force the access code conversion. To switch management of an access code from one Seam workspace to another, set ``force`` to ``true``. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the access code is allowed. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_code_id is not None: + json_payload["access_code_id"] = access_code_id + if allow_external_modification is not None: + json_payload["allow_external_modification"] = allow_external_modification + if force is not None: + json_payload["force"] = force + if is_external_modification_allowed is not None: + json_payload["is_external_modification_allowed"] = ( + is_external_modification_allowed + ) + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/convert_to_managed" + ) + + await self.client.patch( + "/access_codes/unmanaged/convert_to_managed", json=json_payload + ) + + return None + + @route_metadata( + path="/access_codes/unmanaged/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, access_code_id: str) -> None: + """Deletes an `unmanaged access code `_. + + :param access_code_id: ID of the unmanaged access code that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_code_id is not None: + params["access_code_id"] = access_code_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/delete" + ) + + await self.client.delete("/access_codes/unmanaged/delete", params=params) + + return None + + @route_metadata( + path="/access_codes/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get( + self, + *, + access_code_id: Optional[str] = None, + code: Optional[str] = None, + device_id: Optional[str] = None, + ) -> UnmanagedAccessCode: + """Returns a specified `unmanaged access code `_. + + You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param access_code_id: ID of the unmanaged access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param code: Code of the unmanaged access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :param device_id: ID of the device containing the unmanaged access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_code_id is not None: + params["access_code_id"] = access_code_id + if code is not None: + params["code"] = code + if device_id is not None: + params["device_id"] = device_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/get" + ) + + res = await self.client.get("/access_codes/unmanaged/get", params=params) + + return UnmanagedAccessCode.from_dict(res["access_code"]) + + @route_metadata( + path="/access_codes/unmanaged/list", + has_required_parameters=True, + has_pagination=True, + ) + async def list( + self, + *, + device_id: str, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> List[UnmanagedAccessCode]: + """Returns a list of all `unmanaged access codes `_. + + :param device_id: ID of the device for which you want to list unmanaged access codes. + + :param limit: Numerical limit on the number of unmanaged access codes to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned access codes to include all records that satisfy a partial match using ``name``, ``code`` or ``access_code_id``. + + :param user_identifier_key: Your user ID for the user by which to filter unmanaged access codes. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if user_identifier_key is not None: + params["user_identifier_key"] = user_identifier_key + + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/list" + ) + + res = await self.client.get("/access_codes/unmanaged/list", params=params) + + return [UnmanagedAccessCode.from_dict(item) for item in res["access_codes"]] + + @route_metadata( + path="/access_codes/unmanaged/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + access_code_id: str, + is_managed: bool, + allow_external_modification: Optional[bool] = None, + force: Optional[bool] = None, + is_external_modification_allowed: Optional[bool] = None, + ) -> None: + """Updates a specified `unmanaged access code `_. + + :param access_code_id: ID of the unmanaged access code that you want to update. + + :param is_managed: + + :param allow_external_modification: Indicates whether `external modification `_ of the code is allowed. + + :param force: Indicates whether to force the unmanaged access code update. + + :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_code_id is not None: + json_payload["access_code_id"] = access_code_id + if is_managed is not None: + json_payload["is_managed"] = is_managed + if allow_external_modification is not None: + json_payload["allow_external_modification"] = allow_external_modification + if force is not None: + json_payload["force"] = force + if is_external_modification_allowed is not None: + json_payload["is_external_modification_allowed"] = ( + is_external_modification_allowed + ) + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/update" + ) + + await self.client.patch("/access_codes/unmanaged/update", json=json_payload) + + return None diff --git a/seam/routes/access_grants.py b/seam/routes/access_grants.py index a114698f..9f78d9fd 100644 --- a/seam/routes/access_grants.py +++ b/seam/routes/access_grants.py @@ -1,12 +1,14 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import AccessGrant, Batch from .access_grants_unmanaged import ( AbstractAccessGrantsUnmanaged, AccessGrantsUnmanaged, + AbstractAsyncAccessGrantsUnmanaged, + AsyncAccessGrantsUnmanaged, ) @@ -215,6 +217,211 @@ def update( raise NotImplementedError() +class AbstractAsyncAccessGrants(abc.ABC): + + @property + @abc.abstractmethod + def unmanaged(self) -> AbstractAsyncAccessGrantsUnmanaged: + raise NotImplementedError() + + @abc.abstractmethod + async def create( + self, + *, + requested_access_methods: List[Dict[str, Any]], + user_identity_id: Optional[str] = None, + user_identity: Optional[Dict[str, Any]] = None, + access_grant_key: Optional[str] = None, + acs_entrance_ids: Optional[List[str]] = None, + customization_profile_id: Optional[str] = None, + device_ids: Optional[List[str]] = None, + ends_at: Optional[Union[str, Null]] = None, + location: Optional[Dict[str, Any]] = None, + location_ids: Optional[List[str]] = None, + name: Optional[Union[str, Null]] = None, + reservation_key: Optional[str] = None, + space_ids: Optional[List[str]] = None, + space_keys: Optional[List[str]] = None, + starts_at: Optional[str] = None, + ) -> AccessGrant: + """Creates a new `Access Grant `_. Access Grants are the default and recommended way to grant a user access to any physical space, irrespective of the locking hardware. They work with both standalone smart locks (using ``device_ids``) and access control systems (using ``acs_entrance_ids`` or ``space_ids``), and can issue PIN codes, key cards, and mobile keys through a single request. + + :param requested_access_methods: + + :param user_identity_id: ID of user identity for whom access is being granted. + + :param user_identity: When used, creates a new user identity with the given details, and grants them access. + + :param access_grant_key: Unique key for the access grant within the workspace. + + :param acs_entrance_ids: Set of IDs of the `entrances `_ to which access is being granted. + + :param customization_profile_id: ID of the customization profile to apply to the Access Grant and its access methods. + + :param device_ids: Set of IDs of the `devices `_ to which access is being granted. + + :param ends_at: Date and time at which the validity of the new grant ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param location: Deprecated: Create a space first, then reference it using ``space_ids``. + + :param location_ids: Deprecated: Use ``space_ids``. + + :param name: Name for the access grant. + + :param reservation_key: Reservation key for the access grant. + + :param space_ids: Set of IDs of existing spaces to which access is being granted. + + :param space_keys: Set of keys of existing spaces to which access is being granted. + + :param starts_at: Date and time at which the validity of the new grant starts, in `ISO 8601 `_ format. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, access_grant_id: str) -> None: + """Delete an Access Grant. + + :param access_grant_id: ID of Access Grant to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, + *, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, + ) -> AccessGrant: + """Get an Access Grant. + + :param access_grant_id: ID of Access Grant to get. + + :param access_grant_key: Unique key of Access Grant to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get_related( + self, + *, + access_grant_ids: Optional[List[str]] = None, + access_grant_keys: Optional[List[str]] = None, + exclude: Optional[List[str]] = None, + include: Optional[List[str]] = None, + ) -> Batch: + """Gets all related resources for one or more Access Grants. + + :param access_grant_ids: IDs of the access grants that you want to get along with their related resources. + + :param access_grant_keys: Keys of the access grants that you want to get along with their related resources. + + :param exclude: + + :param include: + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + access_code_id: Optional[str] = None, + access_grant_ids: Optional[List[str]] = None, + access_grant_key: Optional[Union[str, Null]] = None, + acs_entrance_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_id: Optional[str] = None, + limit: Optional[float] = None, + location_id: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, + reservation_key: Optional[str] = None, + space_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> List[AccessGrant]: + """Gets an Access Grant. + + :param access_code_id: ID of the access code by which you want to filter the list of Access Grants. + + :param access_grant_ids: IDs of the access grants to retrieve. + + :param access_grant_key: Filter Access Grants by access_grant_key. Use null to filter for Access Grants without an access_grant_key. + + :param acs_entrance_id: ID of the entrance by which you want to filter the list of Access Grants. + + :param acs_system_id: ID of the access system by which you want to filter the list of Access Grants. + + :param customer_key: Customer key for which you want to list access grants. + + :param device_id: ID of the device by which you want to filter the list of Access Grants. + + :param limit: Numerical limit on the number of access grants to return. + + :param location_id: Deprecated: Use ``space_id``. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param reservation_key: Filter Access Grants by reservation_key. + + :param space_id: ID of the space by which you want to filter the list of Access Grants. + + :param user_identity_id: ID of user identity by which you want to filter the list of Access Grants. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def request_access_methods( + self, *, access_grant_id: str, requested_access_methods: List[Dict[str, Any]] + ) -> AccessGrant: + """Adds additional requested access methods to an existing Access Grant. + + :param access_grant_id: ID of the Access Grant to add access methods to. + + :param requested_access_methods: Array of requested access methods to add to the access grant. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, + ends_at: Optional[Union[str, Null]] = None, + name: Optional[Union[str, Null]] = None, + starts_at: Optional[str] = None, + ) -> None: + """Updates an existing Access Grant's time window. + + :param access_grant_id: ID of the Access Grant to update. Provide either ``access_grant_id`` or ``access_grant_key``. + + :param access_grant_key: Key of the Access Grant to update. Provide either ``access_grant_id`` or ``access_grant_key``. + + :param ends_at: Date and time at which the validity of the grant ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param name: Display name for the access grant. + + :param starts_at: Date and time at which the validity of the grant starts, in `ISO 8601 `_ format. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AccessGrants(AbstractAccessGrants): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -590,3 +797,380 @@ def update( self.client.patch("/access_grants/update", json=json_payload) return None + + +class AsyncAccessGrants(AbstractAsyncAccessGrants): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._unmanaged = AsyncAccessGrantsUnmanaged(client=client, defaults=defaults) + + @property + def unmanaged(self) -> AsyncAccessGrantsUnmanaged: + return self._unmanaged + + @route_metadata( + path="/access_grants/create", has_required_parameters=True, has_pagination=False + ) + async def create( + self, + *, + requested_access_methods: List[Dict[str, Any]], + user_identity_id: Optional[str] = None, + user_identity: Optional[Dict[str, Any]] = None, + access_grant_key: Optional[str] = None, + acs_entrance_ids: Optional[List[str]] = None, + customization_profile_id: Optional[str] = None, + device_ids: Optional[List[str]] = None, + ends_at: Optional[Union[str, Null]] = None, + location: Optional[Dict[str, Any]] = None, + location_ids: Optional[List[str]] = None, + name: Optional[Union[str, Null]] = None, + reservation_key: Optional[str] = None, + space_ids: Optional[List[str]] = None, + space_keys: Optional[List[str]] = None, + starts_at: Optional[str] = None, + ) -> AccessGrant: + """Creates a new `Access Grant `_. Access Grants are the default and recommended way to grant a user access to any physical space, irrespective of the locking hardware. They work with both standalone smart locks (using ``device_ids``) and access control systems (using ``acs_entrance_ids`` or ``space_ids``), and can issue PIN codes, key cards, and mobile keys through a single request. + + :param requested_access_methods: + + :param user_identity_id: ID of user identity for whom access is being granted. + + :param user_identity: When used, creates a new user identity with the given details, and grants them access. + + :param access_grant_key: Unique key for the access grant within the workspace. + + :param acs_entrance_ids: Set of IDs of the `entrances `_ to which access is being granted. + + :param customization_profile_id: ID of the customization profile to apply to the Access Grant and its access methods. + + :param device_ids: Set of IDs of the `devices `_ to which access is being granted. + + :param ends_at: Date and time at which the validity of the new grant ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param location: Deprecated: Create a space first, then reference it using ``space_ids``. + + :param location_ids: Deprecated: Use ``space_ids``. + + :param name: Name for the access grant. + + :param reservation_key: Reservation key for the access grant. + + :param space_ids: Set of IDs of existing spaces to which access is being granted. + + :param space_keys: Set of keys of existing spaces to which access is being granted. + + :param starts_at: Date and time at which the validity of the new grant starts, in `ISO 8601 `_ format. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if requested_access_methods is not None: + json_payload["requested_access_methods"] = requested_access_methods + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if user_identity is not None: + json_payload["user_identity"] = user_identity + if access_grant_key is not None: + json_payload["access_grant_key"] = access_grant_key + if acs_entrance_ids is not None: + json_payload["acs_entrance_ids"] = acs_entrance_ids + if customization_profile_id is not None: + json_payload["customization_profile_id"] = customization_profile_id + if device_ids is not None: + json_payload["device_ids"] = device_ids + if ends_at is not None: + json_payload["ends_at"] = ends_at + if location is not None: + json_payload["location"] = location + if location_ids is not None: + json_payload["location_ids"] = location_ids + if name is not None: + json_payload["name"] = name + if reservation_key is not None: + json_payload["reservation_key"] = reservation_key + if space_ids is not None: + json_payload["space_ids"] = space_ids + if space_keys is not None: + json_payload["space_keys"] = space_keys + if starts_at is not None: + json_payload["starts_at"] = starts_at + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/create" + ) + + res = await self.client.post("/access_grants/create", json=json_payload) + + return AccessGrant.from_dict(res["access_grant"]) + + @route_metadata( + path="/access_grants/delete", has_required_parameters=True, has_pagination=False + ) + async def delete(self, *, access_grant_id: str) -> None: + """Delete an Access Grant. + + :param access_grant_id: ID of Access Grant to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_grant_id is not None: + params["access_grant_id"] = access_grant_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_grants/delete" + ) + + await self.client.delete("/access_grants/delete", params=params) + + return None + + @route_metadata( + path="/access_grants/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, + *, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, + ) -> AccessGrant: + """Get an Access Grant. + + :param access_grant_id: ID of Access Grant to get. + + :param access_grant_key: Unique key of Access Grant to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_grant_id is not None: + params["access_grant_id"] = access_grant_id + if access_grant_key is not None: + params["access_grant_key"] = access_grant_key + + if not params: + raise ValueError( + "At least one parameter is required for /access_grants/get" + ) + + res = await self.client.get("/access_grants/get", params=params) + + return AccessGrant.from_dict(res["access_grant"]) + + @route_metadata( + path="/access_grants/get_related", + has_required_parameters=True, + has_pagination=False, + ) + async def get_related( + self, + *, + access_grant_ids: Optional[List[str]] = None, + access_grant_keys: Optional[List[str]] = None, + exclude: Optional[List[str]] = None, + include: Optional[List[str]] = None, + ) -> Batch: + """Gets all related resources for one or more Access Grants. + + :param access_grant_ids: IDs of the access grants that you want to get along with their related resources. + + :param access_grant_keys: Keys of the access grants that you want to get along with their related resources. + + :param exclude: + + :param include: + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_grant_ids is not None: + params["access_grant_ids"] = access_grant_ids + if access_grant_keys is not None: + params["access_grant_keys"] = access_grant_keys + if exclude is not None: + params["exclude"] = exclude + if include is not None: + params["include"] = include + + if not params: + raise ValueError( + "At least one parameter is required for /access_grants/get_related" + ) + + res = await self.client.get("/access_grants/get_related", params=params) + + return Batch.from_dict(res["batch"]) + + @route_metadata( + path="/access_grants/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + access_code_id: Optional[str] = None, + access_grant_ids: Optional[List[str]] = None, + access_grant_key: Optional[Union[str, Null]] = None, + acs_entrance_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_id: Optional[str] = None, + limit: Optional[float] = None, + location_id: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, + reservation_key: Optional[str] = None, + space_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> List[AccessGrant]: + """Gets an Access Grant. + + :param access_code_id: ID of the access code by which you want to filter the list of Access Grants. + + :param access_grant_ids: IDs of the access grants to retrieve. + + :param access_grant_key: Filter Access Grants by access_grant_key. Use null to filter for Access Grants without an access_grant_key. + + :param acs_entrance_id: ID of the entrance by which you want to filter the list of Access Grants. + + :param acs_system_id: ID of the access system by which you want to filter the list of Access Grants. + + :param customer_key: Customer key for which you want to list access grants. + + :param device_id: ID of the device by which you want to filter the list of Access Grants. + + :param limit: Numerical limit on the number of access grants to return. + + :param location_id: Deprecated: Use ``space_id``. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param reservation_key: Filter Access Grants by reservation_key. + + :param space_id: ID of the space by which you want to filter the list of Access Grants. + + :param user_identity_id: ID of user identity by which you want to filter the list of Access Grants. + + :returns: OK""" + params: Dict[str, Any] = {} + + if access_code_id is not None: + params["access_code_id"] = access_code_id + if access_grant_ids is not None: + params["access_grant_ids"] = access_grant_ids + if access_grant_key is not None: + params["access_grant_key"] = access_grant_key + if acs_entrance_id is not None: + params["acs_entrance_id"] = acs_entrance_id + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if customer_key is not None: + params["customer_key"] = customer_key + if device_id is not None: + params["device_id"] = device_id + if limit is not None: + params["limit"] = limit + if location_id is not None: + params["location_id"] = location_id + if page_cursor is not None: + params["page_cursor"] = page_cursor + if reservation_key is not None: + params["reservation_key"] = reservation_key + if space_id is not None: + params["space_id"] = space_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + res = await self.client.get("/access_grants/list", params=params) + + return [AccessGrant.from_dict(item) for item in res["access_grants"]] + + @route_metadata( + path="/access_grants/request_access_methods", + has_required_parameters=True, + has_pagination=False, + ) + async def request_access_methods( + self, *, access_grant_id: str, requested_access_methods: List[Dict[str, Any]] + ) -> AccessGrant: + """Adds additional requested access methods to an existing Access Grant. + + :param access_grant_id: ID of the Access Grant to add access methods to. + + :param requested_access_methods: Array of requested access methods to add to the access grant. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_grant_id is not None: + json_payload["access_grant_id"] = access_grant_id + if requested_access_methods is not None: + json_payload["requested_access_methods"] = requested_access_methods + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/request_access_methods" + ) + + res = await self.client.post( + "/access_grants/request_access_methods", json=json_payload + ) + + return AccessGrant.from_dict(res["access_grant"]) + + @route_metadata( + path="/access_grants/update", has_required_parameters=True, has_pagination=False + ) + async def update( + self, + *, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, + ends_at: Optional[Union[str, Null]] = None, + name: Optional[Union[str, Null]] = None, + starts_at: Optional[str] = None, + ) -> None: + """Updates an existing Access Grant's time window. + + :param access_grant_id: ID of the Access Grant to update. Provide either ``access_grant_id`` or ``access_grant_key``. + + :param access_grant_key: Key of the Access Grant to update. Provide either ``access_grant_id`` or ``access_grant_key``. + + :param ends_at: Date and time at which the validity of the grant ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param name: Display name for the access grant. + + :param starts_at: Date and time at which the validity of the grant starts, in `ISO 8601 `_ format. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_grant_id is not None: + json_payload["access_grant_id"] = access_grant_id + if access_grant_key is not None: + json_payload["access_grant_key"] = access_grant_key + if ends_at is not None: + json_payload["ends_at"] = ends_at + if name is not None: + json_payload["name"] = name + if starts_at is not None: + json_payload["starts_at"] = starts_at + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/update" + ) + + await self.client.patch("/access_grants/update", json=json_payload) + + return None diff --git a/seam/routes/access_grants_unmanaged.py b/seam/routes/access_grants_unmanaged.py index 95f338d2..0164bef5 100644 --- a/seam/routes/access_grants_unmanaged.py +++ b/seam/routes/access_grants_unmanaged.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import UnmanagedAccessGrant @@ -71,6 +71,71 @@ def update( raise NotImplementedError() +class AbstractAsyncAccessGrantsUnmanaged(abc.ABC): + + @abc.abstractmethod + async def get(self, *, access_grant_id: str) -> UnmanagedAccessGrant: + """Get an unmanaged Access Grant (where is_managed = false). + + :param access_grant_id: ID of unmanaged Access Grant to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + acs_entrance_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + reservation_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> List[UnmanagedAccessGrant]: + """Gets unmanaged Access Grants (where is_managed = false). + + :param acs_entrance_id: ID of the entrance by which you want to filter the list of unmanaged Access Grants. + + :param acs_system_id: ID of the access system by which you want to filter the list of unmanaged Access Grants. + + :param limit: Numerical limit on the number of unmanaged access grants to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param reservation_key: Filter unmanaged Access Grants by reservation_key. + + :param user_identity_id: ID of user identity by which you want to filter the list of unmanaged Access Grants. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + access_grant_id: str, + is_managed: Literal[True], + access_grant_key: Optional[str] = None, + ) -> None: + """Updates an unmanaged Access Grant to make it managed. + + This endpoint can only be used to convert unmanaged access grants to managed ones by setting ``is_managed`` to ``true``. It cannot be used to convert managed access grants back to unmanaged. + + When converting an unmanaged access grant to managed, all associated access methods will also be converted to managed. + + :param access_grant_id: ID of the unmanaged Access Grant to update. + + :param is_managed: Must be set to true to convert the unmanaged access grant to managed. + + :param access_grant_key: Unique key for the access grant. If not provided, the existing key will be preserved. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AccessGrantsUnmanaged(AbstractAccessGrantsUnmanaged): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -194,3 +259,128 @@ def update( self.client.patch("/access_grants/unmanaged/update", json=json_payload) return None + + +class AsyncAccessGrantsUnmanaged(AbstractAsyncAccessGrantsUnmanaged): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/access_grants/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get(self, *, access_grant_id: str) -> UnmanagedAccessGrant: + """Get an unmanaged Access Grant (where is_managed = false). + + :param access_grant_id: ID of unmanaged Access Grant to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_grant_id is not None: + params["access_grant_id"] = access_grant_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_grants/unmanaged/get" + ) + + res = await self.client.get("/access_grants/unmanaged/get", params=params) + + return UnmanagedAccessGrant.from_dict(res["access_grant"]) + + @route_metadata( + path="/access_grants/unmanaged/list", + has_required_parameters=False, + has_pagination=True, + ) + async def list( + self, + *, + acs_entrance_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + reservation_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> List[UnmanagedAccessGrant]: + """Gets unmanaged Access Grants (where is_managed = false). + + :param acs_entrance_id: ID of the entrance by which you want to filter the list of unmanaged Access Grants. + + :param acs_system_id: ID of the access system by which you want to filter the list of unmanaged Access Grants. + + :param limit: Numerical limit on the number of unmanaged access grants to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param reservation_key: Filter unmanaged Access Grants by reservation_key. + + :param user_identity_id: ID of user identity by which you want to filter the list of unmanaged Access Grants. + + :returns: OK""" + params: Dict[str, Any] = {} + + if acs_entrance_id is not None: + params["acs_entrance_id"] = acs_entrance_id + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if reservation_key is not None: + params["reservation_key"] = reservation_key + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + res = await self.client.get("/access_grants/unmanaged/list", params=params) + + return [UnmanagedAccessGrant.from_dict(item) for item in res["access_grants"]] + + @route_metadata( + path="/access_grants/unmanaged/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + access_grant_id: str, + is_managed: Literal[True], + access_grant_key: Optional[str] = None, + ) -> None: + """Updates an unmanaged Access Grant to make it managed. + + This endpoint can only be used to convert unmanaged access grants to managed ones by setting ``is_managed`` to ``true``. It cannot be used to convert managed access grants back to unmanaged. + + When converting an unmanaged access grant to managed, all associated access methods will also be converted to managed. + + :param access_grant_id: ID of the unmanaged Access Grant to update. + + :param is_managed: Must be set to true to convert the unmanaged access grant to managed. + + :param access_grant_key: Unique key for the access grant. If not provided, the existing key will be preserved. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_grant_id is not None: + json_payload["access_grant_id"] = access_grant_id + if is_managed is not None: + json_payload["is_managed"] = is_managed + if access_grant_key is not None: + json_payload["access_grant_key"] = access_grant_key + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/unmanaged/update" + ) + + await self.client.patch("/access_grants/unmanaged/update", json=json_payload) + + return None diff --git a/seam/routes/access_methods.py b/seam/routes/access_methods.py index 5de07671..3ed49f0c 100644 --- a/seam/routes/access_methods.py +++ b/seam/routes/access_methods.py @@ -1,14 +1,19 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import ActionAttempt, AccessMethod, Batch from .access_methods_unmanaged import ( AbstractAccessMethodsUnmanaged, AccessMethodsUnmanaged, + AbstractAsyncAccessMethodsUnmanaged, + AsyncAccessMethodsUnmanaged, +) +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, ) -from ..modules.action_attempts import resolve_action_attempt class AbstractAccessMethods(abc.ABC): @@ -169,6 +174,164 @@ def unlock_door( raise NotImplementedError() +class AbstractAsyncAccessMethods(abc.ABC): + + @property + @abc.abstractmethod + def unmanaged(self) -> AbstractAsyncAccessMethodsUnmanaged: + raise NotImplementedError() + + @abc.abstractmethod + async def assign_card( + self, + *, + access_method_id: str, + card_number: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Assigns a pre-registered card credential, identified by ``card_number``, to a card-mode access method. Use this endpoint for access systems that use pre-registered cards, where a physical card must be associated with an access method before it can be used for access. Assigning a card credential also triggers issuance of the access method. + + :param access_method_id: ID of the ``access_method`` to assign the credential to. + + :param card_number: Card number of the credential to assign. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete( + self, + *, + access_method_id: Optional[str] = None, + access_grant_id: Optional[str] = None, + reservation_key: Optional[str] = None, + ) -> None: + """Deletes an access method. + + :param access_method_id: ID of access method to delete. + + :param access_grant_id: ID of access grant whose access methods should be deleted. + + :param reservation_key: Reservation key of the access grant whose access methods should be deleted. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def encode( + self, + *, + access_method_id: str, + acs_encoder_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Encodes an existing access method onto a plastic card placed on the specified `encoder `_. + + :param access_method_id: ID of the ``access_method`` to encode onto a card. + + :param acs_encoder_id: ID of the ``acs_encoder`` to use to encode the ``access_method``. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, access_method_id: str) -> AccessMethod: + """Gets an access method. + + :param access_method_id: ID of access method to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get_related( + self, + *, + access_method_ids: List[str], + exclude: Optional[List[str]] = None, + include: Optional[List[str]] = None, + ) -> Batch: + """Gets all related resources for one or more Access Methods. + + :param access_method_ids: IDs of the access methods that you want to get along with their related resources. + + :param exclude: + + :param include: + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + access_code_id: Optional[str] = None, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, + acs_entrance_id: Optional[str] = None, + device_id: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + space_id: Optional[str] = None, + ) -> List[AccessMethod]: + """Lists all access methods, usually filtered by Access Grant. + + :param access_code_id: ID of the access code by which to filter the returned access methods. Must be combined with ``access_grant_id``, ``access_grant_key``, or ``acs_entrance_id``. + + :param access_grant_id: ID of Access Grant to list access methods for. + + :param access_grant_key: Key of Access Grant to list access methods for. + + :param acs_entrance_id: ID of the entrance for which you want to retrieve all access methods that grant access to it. + + :param device_id: ID of the device by which to filter the returned access methods. Must be combined with ``access_grant_id``, ``access_grant_key``, or ``acs_entrance_id``. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param space_id: ID of the space by which to filter the returned access methods. Must be combined with ``access_grant_id``, ``access_grant_key``, or ``acs_entrance_id``. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def unlock_door( + self, + *, + access_method_id: str, + acs_entrance_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Remotely unlocks a specified `entrance `_ using the cloud key credential associated with an access method. Returns an action attempt that tracks the progress of the unlock operation. + + :param access_method_id: ID of the cloud_key ``access_method`` to use for the unlock operation. + + :param acs_entrance_id: ID of the entrance to unlock. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AccessMethods(AbstractAccessMethods): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -494,3 +657,330 @@ def unlock_door( action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, ) + + +class AsyncAccessMethods(AbstractAsyncAccessMethods): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._unmanaged = AsyncAccessMethodsUnmanaged(client=client, defaults=defaults) + + @property + def unmanaged(self) -> AsyncAccessMethodsUnmanaged: + return self._unmanaged + + @route_metadata( + path="/access_methods/assign_card", + has_required_parameters=True, + has_pagination=False, + ) + async def assign_card( + self, + *, + access_method_id: str, + card_number: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Assigns a pre-registered card credential, identified by ``card_number``, to a card-mode access method. Use this endpoint for access systems that use pre-registered cards, where a physical card must be associated with an access method before it can be used for access. Assigning a card credential also triggers issuance of the access method. + + :param access_method_id: ID of the ``access_method`` to assign the credential to. + + :param card_number: Card number of the credential to assign. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_method_id is not None: + json_payload["access_method_id"] = access_method_id + if card_number is not None: + json_payload["card_number"] = card_number + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_methods/assign_card" + ) + + res = await self.client.post("/access_methods/assign_card", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/access_methods/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete( + self, + *, + access_method_id: Optional[str] = None, + access_grant_id: Optional[str] = None, + reservation_key: Optional[str] = None, + ) -> None: + """Deletes an access method. + + :param access_method_id: ID of access method to delete. + + :param access_grant_id: ID of access grant whose access methods should be deleted. + + :param reservation_key: Reservation key of the access grant whose access methods should be deleted. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_method_id is not None: + params["access_method_id"] = access_method_id + if access_grant_id is not None: + params["access_grant_id"] = access_grant_id + if reservation_key is not None: + params["reservation_key"] = reservation_key + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/delete" + ) + + await self.client.delete("/access_methods/delete", params=params) + + return None + + @route_metadata( + path="/access_methods/encode", + has_required_parameters=True, + has_pagination=False, + ) + async def encode( + self, + *, + access_method_id: str, + acs_encoder_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Encodes an existing access method onto a plastic card placed on the specified `encoder `_. + + :param access_method_id: ID of the ``access_method`` to encode onto a card. + + :param acs_encoder_id: ID of the ``acs_encoder`` to use to encode the ``access_method``. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_method_id is not None: + json_payload["access_method_id"] = access_method_id + if acs_encoder_id is not None: + json_payload["acs_encoder_id"] = acs_encoder_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_methods/encode" + ) + + res = await self.client.post("/access_methods/encode", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/access_methods/get", has_required_parameters=True, has_pagination=False + ) + async def get(self, *, access_method_id: str) -> AccessMethod: + """Gets an access method. + + :param access_method_id: ID of access method to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_method_id is not None: + params["access_method_id"] = access_method_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/get" + ) + + res = await self.client.get("/access_methods/get", params=params) + + return AccessMethod.from_dict(res["access_method"]) + + @route_metadata( + path="/access_methods/get_related", + has_required_parameters=True, + has_pagination=False, + ) + async def get_related( + self, + *, + access_method_ids: List[str], + exclude: Optional[List[str]] = None, + include: Optional[List[str]] = None, + ) -> Batch: + """Gets all related resources for one or more Access Methods. + + :param access_method_ids: IDs of the access methods that you want to get along with their related resources. + + :param exclude: + + :param include: + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_method_ids is not None: + params["access_method_ids"] = access_method_ids + if exclude is not None: + params["exclude"] = exclude + if include is not None: + params["include"] = include + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/get_related" + ) + + res = await self.client.get("/access_methods/get_related", params=params) + + return Batch.from_dict(res["batch"]) + + @route_metadata( + path="/access_methods/list", has_required_parameters=True, has_pagination=True + ) + async def list( + self, + *, + access_code_id: Optional[str] = None, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, + acs_entrance_id: Optional[str] = None, + device_id: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + space_id: Optional[str] = None, + ) -> List[AccessMethod]: + """Lists all access methods, usually filtered by Access Grant. + + :param access_code_id: ID of the access code by which to filter the returned access methods. Must be combined with ``access_grant_id``, ``access_grant_key``, or ``acs_entrance_id``. + + :param access_grant_id: ID of Access Grant to list access methods for. + + :param access_grant_key: Key of Access Grant to list access methods for. + + :param acs_entrance_id: ID of the entrance for which you want to retrieve all access methods that grant access to it. + + :param device_id: ID of the device by which to filter the returned access methods. Must be combined with ``access_grant_id``, ``access_grant_key``, or ``acs_entrance_id``. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param space_id: ID of the space by which to filter the returned access methods. Must be combined with ``access_grant_id``, ``access_grant_key``, or ``acs_entrance_id``. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_code_id is not None: + params["access_code_id"] = access_code_id + if access_grant_id is not None: + params["access_grant_id"] = access_grant_id + if access_grant_key is not None: + params["access_grant_key"] = access_grant_key + if acs_entrance_id is not None: + params["acs_entrance_id"] = acs_entrance_id + if device_id is not None: + params["device_id"] = device_id + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if space_id is not None: + params["space_id"] = space_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/list" + ) + + res = await self.client.get("/access_methods/list", params=params) + + return [AccessMethod.from_dict(item) for item in res["access_methods"]] + + @route_metadata( + path="/access_methods/unlock_door", + has_required_parameters=True, + has_pagination=False, + ) + async def unlock_door( + self, + *, + access_method_id: str, + acs_entrance_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Remotely unlocks a specified `entrance `_ using the cloud key credential associated with an access method. Returns an action attempt that tracks the progress of the unlock operation. + + :param access_method_id: ID of the cloud_key ``access_method`` to use for the unlock operation. + + :param acs_entrance_id: ID of the entrance to unlock. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_method_id is not None: + json_payload["access_method_id"] = access_method_id + if acs_entrance_id is not None: + json_payload["acs_entrance_id"] = acs_entrance_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_methods/unlock_door" + ) + + res = await self.client.post("/access_methods/unlock_door", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) diff --git a/seam/routes/access_methods_unmanaged.py b/seam/routes/access_methods_unmanaged.py index e0c4b591..9fd5315c 100644 --- a/seam/routes/access_methods_unmanaged.py +++ b/seam/routes/access_methods_unmanaged.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import UnmanagedAccessMethod @@ -43,6 +43,44 @@ def list( raise NotImplementedError() +class AbstractAsyncAccessMethodsUnmanaged(abc.ABC): + + @abc.abstractmethod + async def get(self, *, access_method_id: str) -> UnmanagedAccessMethod: + """Gets an unmanaged access method (where is_managed = false). + + :param access_method_id: ID of unmanaged access method to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + access_grant_id: str, + acs_entrance_id: Optional[str] = None, + device_id: Optional[str] = None, + space_id: Optional[str] = None, + ) -> List[UnmanagedAccessMethod]: + """Lists all unmanaged access methods (where is_managed = false), usually filtered by Access Grant. + + :param access_grant_id: ID of Access Grant to list unmanaged access methods for. + + :param acs_entrance_id: ID of the entrance for which you want to retrieve all unmanaged access methods. + + :param device_id: ID of the device for which you want to retrieve all unmanaged access methods. + + :param space_id: ID of the space for which you want to retrieve all unmanaged access methods. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AccessMethodsUnmanaged(AbstractAccessMethodsUnmanaged): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -120,3 +158,82 @@ def list( res = self.client.get("/access_methods/unmanaged/list", params=params) return [UnmanagedAccessMethod.from_dict(item) for item in res["access_methods"]] + + +class AsyncAccessMethodsUnmanaged(AbstractAsyncAccessMethodsUnmanaged): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/access_methods/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get(self, *, access_method_id: str) -> UnmanagedAccessMethod: + """Gets an unmanaged access method (where is_managed = false). + + :param access_method_id: ID of unmanaged access method to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_method_id is not None: + params["access_method_id"] = access_method_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/unmanaged/get" + ) + + res = await self.client.get("/access_methods/unmanaged/get", params=params) + + return UnmanagedAccessMethod.from_dict(res["access_method"]) + + @route_metadata( + path="/access_methods/unmanaged/list", + has_required_parameters=True, + has_pagination=False, + ) + async def list( + self, + *, + access_grant_id: str, + acs_entrance_id: Optional[str] = None, + device_id: Optional[str] = None, + space_id: Optional[str] = None, + ) -> List[UnmanagedAccessMethod]: + """Lists all unmanaged access methods (where is_managed = false), usually filtered by Access Grant. + + :param access_grant_id: ID of Access Grant to list unmanaged access methods for. + + :param acs_entrance_id: ID of the entrance for which you want to retrieve all unmanaged access methods. + + :param device_id: ID of the device for which you want to retrieve all unmanaged access methods. + + :param space_id: ID of the space for which you want to retrieve all unmanaged access methods. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_grant_id is not None: + params["access_grant_id"] = access_grant_id + if acs_entrance_id is not None: + params["acs_entrance_id"] = acs_entrance_id + if device_id is not None: + params["device_id"] = device_id + if space_id is not None: + params["space_id"] = space_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/unmanaged/list" + ) + + res = await self.client.get("/access_methods/unmanaged/list", params=params) + + return [UnmanagedAccessMethod.from_dict(item) for item in res["access_methods"]] diff --git a/seam/routes/acs.py b/seam/routes/acs.py index 32e8a724..00ba0072 100644 --- a/seam/routes/acs.py +++ b/seam/routes/acs.py @@ -1,13 +1,38 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata -from .acs_access_groups import AbstractAcsAccessGroups, AcsAccessGroups -from .acs_credentials import AbstractAcsCredentials, AcsCredentials -from .acs_encoders import AbstractAcsEncoders, AcsEncoders -from .acs_entrances import AbstractAcsEntrances, AcsEntrances -from .acs_systems import AbstractAcsSystems, AcsSystems -from .acs_users import AbstractAcsUsers, AcsUsers +from .acs_access_groups import ( + AbstractAcsAccessGroups, + AcsAccessGroups, + AbstractAsyncAcsAccessGroups, + AsyncAcsAccessGroups, +) +from .acs_credentials import ( + AbstractAcsCredentials, + AcsCredentials, + AbstractAsyncAcsCredentials, + AsyncAcsCredentials, +) +from .acs_encoders import ( + AbstractAcsEncoders, + AcsEncoders, + AbstractAsyncAcsEncoders, + AsyncAcsEncoders, +) +from .acs_entrances import ( + AbstractAcsEntrances, + AcsEntrances, + AbstractAsyncAcsEntrances, + AsyncAcsEntrances, +) +from .acs_systems import ( + AbstractAcsSystems, + AcsSystems, + AbstractAsyncAcsSystems, + AsyncAcsSystems, +) +from .acs_users import AbstractAcsUsers, AcsUsers, AbstractAsyncAcsUsers, AsyncAcsUsers class AbstractAcs(abc.ABC): @@ -43,6 +68,39 @@ def users(self) -> AbstractAcsUsers: raise NotImplementedError() +class AbstractAsyncAcs(abc.ABC): + + @property + @abc.abstractmethod + def access_groups(self) -> AbstractAsyncAcsAccessGroups: + raise NotImplementedError() + + @property + @abc.abstractmethod + def credentials(self) -> AbstractAsyncAcsCredentials: + raise NotImplementedError() + + @property + @abc.abstractmethod + def encoders(self) -> AbstractAsyncAcsEncoders: + raise NotImplementedError() + + @property + @abc.abstractmethod + def entrances(self) -> AbstractAsyncAcsEntrances: + raise NotImplementedError() + + @property + @abc.abstractmethod + def systems(self) -> AbstractAsyncAcsSystems: + raise NotImplementedError() + + @property + @abc.abstractmethod + def users(self) -> AbstractAsyncAcsUsers: + raise NotImplementedError() + + class Acs(AbstractAcs): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -77,3 +135,39 @@ def systems(self) -> AcsSystems: @property def users(self) -> AcsUsers: return self._users + + +class AsyncAcs(AbstractAsyncAcs): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._access_groups = AsyncAcsAccessGroups(client=client, defaults=defaults) + self._credentials = AsyncAcsCredentials(client=client, defaults=defaults) + self._encoders = AsyncAcsEncoders(client=client, defaults=defaults) + self._entrances = AsyncAcsEntrances(client=client, defaults=defaults) + self._systems = AsyncAcsSystems(client=client, defaults=defaults) + self._users = AsyncAcsUsers(client=client, defaults=defaults) + + @property + def access_groups(self) -> AsyncAcsAccessGroups: + return self._access_groups + + @property + def credentials(self) -> AsyncAcsCredentials: + return self._credentials + + @property + def encoders(self) -> AsyncAcsEncoders: + return self._encoders + + @property + def entrances(self) -> AsyncAcsEntrances: + return self._entrances + + @property + def systems(self) -> AsyncAcsSystems: + return self._systems + + @property + def users(self) -> AsyncAcsUsers: + return self._users diff --git a/seam/routes/acs_access_groups.py b/seam/routes/acs_access_groups.py index c4557563..507e7196 100644 --- a/seam/routes/acs_access_groups.py +++ b/seam/routes/acs_access_groups.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import AcsAccessGroup, AcsEntrance, AcsUser @@ -112,6 +112,113 @@ def remove_user( raise NotImplementedError() +class AbstractAsyncAcsAccessGroups(abc.ABC): + + @abc.abstractmethod + async def add_user( + self, + *, + acs_access_group_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Adds a specified `access system user `_ to a specified `access group `_. + + :param acs_access_group_id: ID of the access group to which you want to add an access system user. + + :param acs_user_id: ID of the access system user that you want to add to an access group. You can only provide one of acs_user_id or user_identity_id. + + :param user_identity_id: ID of the desired user identity that you want to add to an access group. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the access group membership belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, acs_access_group_id: str) -> None: + """Deletes a specified `access group `_. + + :param acs_access_group_id: ID of the access group that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, acs_access_group_id: str) -> AcsAccessGroup: + """Returns a specified `access group `_. + + :param acs_access_group_id: ID of the access group that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + search: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> List[AcsAccessGroup]: + """Returns a list of all `access groups `_. + + :param acs_system_id: ID of the access system for which you want to retrieve all access groups. + + :param acs_user_id: ID of the access system user for which you want to retrieve all access groups. + + :param search: String for which to search. Filters returned access groups to include all records that satisfy a partial match using ``name`` or ``acs_access_group_id``. + + :param user_identity_id: ID of the user identity for which you want to retrieve all access groups. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_accessible_entrances( + self, *, acs_access_group_id: str + ) -> List[AcsEntrance]: + """Returns a list of all accessible entrances for a specified `access group `_. + + :param acs_access_group_id: ID of the access group for which you want to retrieve all accessible entrances. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_users(self, *, acs_access_group_id: str) -> List[AcsUser]: + """Returns a list of all `access system users `_ in an `access group `_. + + :param acs_access_group_id: ID of the access group for which you want to retrieve all access system users. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def remove_user( + self, + *, + acs_access_group_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Removes a specified `access system user `_ from a specified `access group `_. + + :param acs_access_group_id: ID of the access group from which you want to remove an access system user. + + :param acs_user_id: ID of the access system user that you want to remove from an access group. + + :param user_identity_id: ID of the user identity associated with the user that you want to remove from an access group. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AcsAccessGroups(AbstractAcsAccessGroups): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -343,3 +450,236 @@ def remove_user( self.client.delete("/acs/access_groups/remove_user", params=params) return None + + +class AsyncAcsAccessGroups(AbstractAsyncAcsAccessGroups): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/acs/access_groups/add_user", + has_required_parameters=True, + has_pagination=False, + ) + async def add_user( + self, + *, + acs_access_group_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Adds a specified `access system user `_ to a specified `access group `_. + + :param acs_access_group_id: ID of the access group to which you want to add an access system user. + + :param acs_user_id: ID of the access system user that you want to add to an access group. You can only provide one of acs_user_id or user_identity_id. + + :param user_identity_id: ID of the desired user identity that you want to add to an access group. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the access group membership belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_access_group_id is not None: + json_payload["acs_access_group_id"] = acs_access_group_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/access_groups/add_user" + ) + + await self.client.put("/acs/access_groups/add_user", json=json_payload) + + return None + + @route_metadata( + path="/acs/access_groups/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, acs_access_group_id: str) -> None: + """Deletes a specified `access group `_. + + :param acs_access_group_id: ID of the access group that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_access_group_id is not None: + params["acs_access_group_id"] = acs_access_group_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/delete" + ) + + await self.client.delete("/acs/access_groups/delete", params=params) + + return None + + @route_metadata( + path="/acs/access_groups/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get(self, *, acs_access_group_id: str) -> AcsAccessGroup: + """Returns a specified `access group `_. + + :param acs_access_group_id: ID of the access group that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_access_group_id is not None: + params["acs_access_group_id"] = acs_access_group_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/get" + ) + + res = await self.client.get("/acs/access_groups/get", params=params) + + return AcsAccessGroup.from_dict(res["acs_access_group"]) + + @route_metadata( + path="/acs/access_groups/list", + has_required_parameters=False, + has_pagination=False, + ) + async def list( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + search: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> List[AcsAccessGroup]: + """Returns a list of all `access groups `_. + + :param acs_system_id: ID of the access system for which you want to retrieve all access groups. + + :param acs_user_id: ID of the access system user for which you want to retrieve all access groups. + + :param search: String for which to search. Filters returned access groups to include all records that satisfy a partial match using ``name`` or ``acs_access_group_id``. + + :param user_identity_id: ID of the user identity for which you want to retrieve all access groups. + + :returns: OK""" + params: Dict[str, Any] = {} + + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if search is not None: + params["search"] = search + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + res = await self.client.get("/acs/access_groups/list", params=params) + + return [AcsAccessGroup.from_dict(item) for item in res["acs_access_groups"]] + + @route_metadata( + path="/acs/access_groups/list_accessible_entrances", + has_required_parameters=True, + has_pagination=False, + ) + async def list_accessible_entrances( + self, *, acs_access_group_id: str + ) -> List[AcsEntrance]: + """Returns a list of all accessible entrances for a specified `access group `_. + + :param acs_access_group_id: ID of the access group for which you want to retrieve all accessible entrances. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_access_group_id is not None: + params["acs_access_group_id"] = acs_access_group_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/list_accessible_entrances" + ) + + res = await self.client.get( + "/acs/access_groups/list_accessible_entrances", params=params + ) + + return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + + @route_metadata( + path="/acs/access_groups/list_users", + has_required_parameters=True, + has_pagination=False, + ) + async def list_users(self, *, acs_access_group_id: str) -> List[AcsUser]: + """Returns a list of all `access system users `_ in an `access group `_. + + :param acs_access_group_id: ID of the access group for which you want to retrieve all access system users. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_access_group_id is not None: + params["acs_access_group_id"] = acs_access_group_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/list_users" + ) + + res = await self.client.get("/acs/access_groups/list_users", params=params) + + return [AcsUser.from_dict(item) for item in res["acs_users"]] + + @route_metadata( + path="/acs/access_groups/remove_user", + has_required_parameters=True, + has_pagination=False, + ) + async def remove_user( + self, + *, + acs_access_group_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Removes a specified `access system user `_ from a specified `access group `_. + + :param acs_access_group_id: ID of the access group from which you want to remove an access system user. + + :param acs_user_id: ID of the access system user that you want to remove from an access group. + + :param user_identity_id: ID of the user identity associated with the user that you want to remove from an access group. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_access_group_id is not None: + params["acs_access_group_id"] = acs_access_group_id + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/remove_user" + ) + + await self.client.delete("/acs/access_groups/remove_user", params=params) + + return None diff --git a/seam/routes/acs_credentials.py b/seam/routes/acs_credentials.py index c3be5f9a..7c02a486 100644 --- a/seam/routes/acs_credentials.py +++ b/seam/routes/acs_credentials.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import AcsCredential, AcsEntrance @@ -182,6 +182,184 @@ def update( raise NotImplementedError() +class AbstractAsyncAcsCredentials(abc.ABC): + + @abc.abstractmethod + async def assign( + self, + *, + acs_credential_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Assigns a specified `credential `_ to a specified `access system user `_. + + :param acs_credential_id: ID of the credential that you want to assign to an access system user. + + :param acs_user_id: ID of the access system user to whom you want to assign a credential. You can only provide one of acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity to whom you want to assign a credential. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the credential belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def create( + self, + *, + access_method: str, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + allowed_acs_entrance_ids: Optional[List[str]] = None, + assa_abloy_vostio_metadata: Optional[Dict[str, Any]] = None, + code: Optional[str] = None, + credential_manager_acs_system_id: Optional[str] = None, + ends_at: Optional[str] = None, + is_multi_phone_sync_credential: Optional[bool] = None, + salto_space_metadata: Optional[Dict[str, Any]] = None, + starts_at: Optional[str] = None, + user_identity_id: Optional[str] = None, + visionline_metadata: Optional[Dict[str, Any]] = None, + ) -> AcsCredential: + """Creates a new `credential `_ for a specified `ACS user `_. For granting access, we recommend `Access Grants `_ instead: they create and manage the underlying credentials for you, across access systems and standalone smart locks alike. Use this low-level endpoint only when you need direct control over an individual ACS credential. + + :param access_method: Access method for the new credential. Supported values: ``code``, ``card``, ``mobile_key``, ``cloud_key``. + + :param acs_system_id: ID of the access system to which the new credential belongs. You must provide either ``acs_user_id`` or the combination of ``user_identity_id`` and ``acs_system_id``. + + :param acs_user_id: ID of the access system user to whom the new credential belongs. You must provide either ``acs_user_id`` or the combination of ``user_identity_id`` and ``acs_system_id``. + + :param allowed_acs_entrance_ids: Set of IDs of the `entrances `_ for which the new credential grants access. + + :param assa_abloy_vostio_metadata: Vostio-specific metadata for the new credential. + + :param code: Access (PIN) code for the new credential. There may be manufacturer-specific code restrictions. For details, see the applicable `device or system integration guide `_. + + :param credential_manager_acs_system_id: ACS system ID of the credential manager for the new credential. + + :param ends_at: Date and time at which the validity of the new credential ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param is_multi_phone_sync_credential: Indicates whether the new credential is a `multi-phone sync credential `_. + + :param salto_space_metadata: Salto Space-specific metadata for the new credential. + + :param starts_at: Date and time at which the validity of the new credential starts, in `ISO 8601 `_ format. + + :param user_identity_id: ID of the user identity to whom the new credential belongs. You must provide either ``acs_user_id`` or the combination of ``user_identity_id`` and ``acs_system_id``. If the access system contains a user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the credential belongs to the access system user. If the access system does not have a corresponding user, one is created. + + :param visionline_metadata: Visionline-specific metadata for the new credential. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, acs_credential_id: str) -> None: + """Deletes a specified `credential `_. + + :param acs_credential_id: ID of the credential that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, acs_credential_id: str) -> AcsCredential: + """Returns a specified `credential `_. + + :param acs_credential_id: ID of the credential that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + acs_user_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + created_before: Optional[str] = None, + is_multi_phone_sync_credential: Optional[bool] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + ) -> List[AcsCredential]: + """Returns a list of all `credentials `_. + + :param acs_user_id: ID of the access system user for which you want to retrieve all credentials. + + :param acs_system_id: ID of the access system for which you want to retrieve all credentials. + + :param user_identity_id: ID of the user identity for which you want to retrieve all credentials. + + :param created_before: Date and time, in `ISO 8601 `_ format, before which events to return were created. + + :param is_multi_phone_sync_credential: Indicates whether you want to retrieve only multi-phone sync credentials or non-multi-phone sync credentials. + + :param limit: Number of credentials to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned credentials to include all records that satisfy a partial match using ``display_name``, ``code``, ``card_number``, ``acs_user_id`` or ``acs_credential_id``. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_accessible_entrances( + self, *, acs_credential_id: str + ) -> List[AcsEntrance]: + """Returns a list of all `entrances `_ to which a `credential `_ grants access. + + :param acs_credential_id: ID of the credential for which you want to retrieve all entrances to which the credential grants access. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def unassign( + self, + *, + acs_credential_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Unassigns a specified `credential `_ from a specified `access system user `_. + + :param acs_credential_id: ID of the credential that you want to unassign from an access system user. + + :param acs_user_id: ID of the access system user from which you want to unassign a credential. You can only provide one of acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity from which you want to unassign a credential. You can only provide one of acs_user_id or user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + acs_credential_id: str, + code: Optional[str] = None, + ends_at: Optional[str] = None, + ) -> None: + """Updates the code and ends at date and time for a specified `credential `_. + + :param acs_credential_id: ID of the credential that you want to update. + + :param code: Replacement access (PIN) code for the credential that you want to update. + + :param ends_at: Replacement date and time at which the validity of the credential ends, in `ISO 8601 `_ format. Must be a time in the future and after the ``starts_at`` value that you set when creating the credential. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AcsCredentials(AbstractAcsCredentials): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -534,3 +712,359 @@ def update( self.client.patch("/acs/credentials/update", json=json_payload) return None + + +class AsyncAcsCredentials(AbstractAsyncAcsCredentials): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/acs/credentials/assign", + has_required_parameters=True, + has_pagination=False, + ) + async def assign( + self, + *, + acs_credential_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Assigns a specified `credential `_ to a specified `access system user `_. + + :param acs_credential_id: ID of the credential that you want to assign to an access system user. + + :param acs_user_id: ID of the access system user to whom you want to assign a credential. You can only provide one of acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity to whom you want to assign a credential. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the credential belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_credential_id is not None: + json_payload["acs_credential_id"] = acs_credential_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/credentials/assign" + ) + + await self.client.patch("/acs/credentials/assign", json=json_payload) + + return None + + @route_metadata( + path="/acs/credentials/create", + has_required_parameters=True, + has_pagination=False, + ) + async def create( + self, + *, + access_method: str, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + allowed_acs_entrance_ids: Optional[List[str]] = None, + assa_abloy_vostio_metadata: Optional[Dict[str, Any]] = None, + code: Optional[str] = None, + credential_manager_acs_system_id: Optional[str] = None, + ends_at: Optional[str] = None, + is_multi_phone_sync_credential: Optional[bool] = None, + salto_space_metadata: Optional[Dict[str, Any]] = None, + starts_at: Optional[str] = None, + user_identity_id: Optional[str] = None, + visionline_metadata: Optional[Dict[str, Any]] = None, + ) -> AcsCredential: + """Creates a new `credential `_ for a specified `ACS user `_. For granting access, we recommend `Access Grants `_ instead: they create and manage the underlying credentials for you, across access systems and standalone smart locks alike. Use this low-level endpoint only when you need direct control over an individual ACS credential. + + :param access_method: Access method for the new credential. Supported values: ``code``, ``card``, ``mobile_key``, ``cloud_key``. + + :param acs_system_id: ID of the access system to which the new credential belongs. You must provide either ``acs_user_id`` or the combination of ``user_identity_id`` and ``acs_system_id``. + + :param acs_user_id: ID of the access system user to whom the new credential belongs. You must provide either ``acs_user_id`` or the combination of ``user_identity_id`` and ``acs_system_id``. + + :param allowed_acs_entrance_ids: Set of IDs of the `entrances `_ for which the new credential grants access. + + :param assa_abloy_vostio_metadata: Vostio-specific metadata for the new credential. + + :param code: Access (PIN) code for the new credential. There may be manufacturer-specific code restrictions. For details, see the applicable `device or system integration guide `_. + + :param credential_manager_acs_system_id: ACS system ID of the credential manager for the new credential. + + :param ends_at: Date and time at which the validity of the new credential ends, in `ISO 8601 `_ format. Must be a time in the future and after ``starts_at``. + + :param is_multi_phone_sync_credential: Indicates whether the new credential is a `multi-phone sync credential `_. + + :param salto_space_metadata: Salto Space-specific metadata for the new credential. + + :param starts_at: Date and time at which the validity of the new credential starts, in `ISO 8601 `_ format. + + :param user_identity_id: ID of the user identity to whom the new credential belongs. You must provide either ``acs_user_id`` or the combination of ``user_identity_id`` and ``acs_system_id``. If the access system contains a user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the credential belongs to the access system user. If the access system does not have a corresponding user, one is created. + + :param visionline_metadata: Visionline-specific metadata for the new credential. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_method is not None: + json_payload["access_method"] = access_method + if acs_system_id is not None: + json_payload["acs_system_id"] = acs_system_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if allowed_acs_entrance_ids is not None: + json_payload["allowed_acs_entrance_ids"] = allowed_acs_entrance_ids + if assa_abloy_vostio_metadata is not None: + json_payload["assa_abloy_vostio_metadata"] = assa_abloy_vostio_metadata + if code is not None: + json_payload["code"] = code + if credential_manager_acs_system_id is not None: + json_payload["credential_manager_acs_system_id"] = ( + credential_manager_acs_system_id + ) + if ends_at is not None: + json_payload["ends_at"] = ends_at + if is_multi_phone_sync_credential is not None: + json_payload["is_multi_phone_sync_credential"] = ( + is_multi_phone_sync_credential + ) + if salto_space_metadata is not None: + json_payload["salto_space_metadata"] = salto_space_metadata + if starts_at is not None: + json_payload["starts_at"] = starts_at + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if visionline_metadata is not None: + json_payload["visionline_metadata"] = visionline_metadata + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/credentials/create" + ) + + res = await self.client.post("/acs/credentials/create", json=json_payload) + + return AcsCredential.from_dict(res["acs_credential"]) + + @route_metadata( + path="/acs/credentials/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, acs_credential_id: str) -> None: + """Deletes a specified `credential `_. + + :param acs_credential_id: ID of the credential that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_credential_id is not None: + params["acs_credential_id"] = acs_credential_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/credentials/delete" + ) + + await self.client.delete("/acs/credentials/delete", params=params) + + return None + + @route_metadata( + path="/acs/credentials/get", has_required_parameters=True, has_pagination=False + ) + async def get(self, *, acs_credential_id: str) -> AcsCredential: + """Returns a specified `credential `_. + + :param acs_credential_id: ID of the credential that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_credential_id is not None: + params["acs_credential_id"] = acs_credential_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/credentials/get" + ) + + res = await self.client.get("/acs/credentials/get", params=params) + + return AcsCredential.from_dict(res["acs_credential"]) + + @route_metadata( + path="/acs/credentials/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + acs_user_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + created_before: Optional[str] = None, + is_multi_phone_sync_credential: Optional[bool] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + ) -> List[AcsCredential]: + """Returns a list of all `credentials `_. + + :param acs_user_id: ID of the access system user for which you want to retrieve all credentials. + + :param acs_system_id: ID of the access system for which you want to retrieve all credentials. + + :param user_identity_id: ID of the user identity for which you want to retrieve all credentials. + + :param created_before: Date and time, in `ISO 8601 `_ format, before which events to return were created. + + :param is_multi_phone_sync_credential: Indicates whether you want to retrieve only multi-phone sync credentials or non-multi-phone sync credentials. + + :param limit: Number of credentials to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned credentials to include all records that satisfy a partial match using ``display_name``, ``code``, ``card_number``, ``acs_user_id`` or ``acs_credential_id``. + + :returns: OK""" + params: Dict[str, Any] = {} + + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + if created_before is not None: + params["created_before"] = created_before + if is_multi_phone_sync_credential is not None: + params["is_multi_phone_sync_credential"] = is_multi_phone_sync_credential + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + + res = await self.client.get("/acs/credentials/list", params=params) + + return [AcsCredential.from_dict(item) for item in res["acs_credentials"]] + + @route_metadata( + path="/acs/credentials/list_accessible_entrances", + has_required_parameters=True, + has_pagination=False, + ) + async def list_accessible_entrances( + self, *, acs_credential_id: str + ) -> List[AcsEntrance]: + """Returns a list of all `entrances `_ to which a `credential `_ grants access. + + :param acs_credential_id: ID of the credential for which you want to retrieve all entrances to which the credential grants access. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_credential_id is not None: + params["acs_credential_id"] = acs_credential_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/credentials/list_accessible_entrances" + ) + + res = await self.client.get( + "/acs/credentials/list_accessible_entrances", params=params + ) + + return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + + @route_metadata( + path="/acs/credentials/unassign", + has_required_parameters=True, + has_pagination=False, + ) + async def unassign( + self, + *, + acs_credential_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Unassigns a specified `credential `_ from a specified `access system user `_. + + :param acs_credential_id: ID of the credential that you want to unassign from an access system user. + + :param acs_user_id: ID of the access system user from which you want to unassign a credential. You can only provide one of acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity from which you want to unassign a credential. You can only provide one of acs_user_id or user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_credential_id is not None: + json_payload["acs_credential_id"] = acs_credential_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/credentials/unassign" + ) + + await self.client.patch("/acs/credentials/unassign", json=json_payload) + + return None + + @route_metadata( + path="/acs/credentials/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + acs_credential_id: str, + code: Optional[str] = None, + ends_at: Optional[str] = None, + ) -> None: + """Updates the code and ends at date and time for a specified `credential `_. + + :param acs_credential_id: ID of the credential that you want to update. + + :param code: Replacement access (PIN) code for the credential that you want to update. + + :param ends_at: Replacement date and time at which the validity of the credential ends, in `ISO 8601 `_ format. Must be a time in the future and after the ``starts_at`` value that you set when creating the credential. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_credential_id is not None: + json_payload["acs_credential_id"] = acs_credential_id + if code is not None: + json_payload["code"] = code + if ends_at is not None: + json_payload["ends_at"] = ends_at + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/credentials/update" + ) + + await self.client.patch("/acs/credentials/update", json=json_payload) + + return None diff --git a/seam/routes/acs_encoders.py b/seam/routes/acs_encoders.py index 7bfc114d..de351d14 100644 --- a/seam/routes/acs_encoders.py +++ b/seam/routes/acs_encoders.py @@ -1,11 +1,19 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import ActionAttempt, AcsEncoder -from .acs_encoders_simulate import AbstractAcsEncodersSimulate, AcsEncodersSimulate -from ..modules.action_attempts import resolve_action_attempt +from .acs_encoders_simulate import ( + AbstractAcsEncodersSimulate, + AcsEncodersSimulate, + AbstractAsyncAcsEncodersSimulate, + AsyncAcsEncodersSimulate, +) +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, +) class AbstractAcsEncoders(abc.ABC): @@ -124,6 +132,122 @@ def scan_to_assign_credential( raise NotImplementedError() +class AbstractAsyncAcsEncoders(abc.ABC): + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAsyncAcsEncodersSimulate: + raise NotImplementedError() + + @abc.abstractmethod + async def encode_credential( + self, + *, + acs_encoder_id: str, + access_method_id: Optional[str] = None, + acs_credential_id: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Encodes an existing `credential `_ onto a plastic card placed on the specified `encoder `_. Either provide an ``acs_credential_id`` or an ``access_method_id`` + + :param acs_encoder_id: ID of the ``acs_encoder`` to use to encode the ``acs_credential``. + + :param access_method_id: ID of the ``access_method`` to encode onto a card. + + :param acs_credential_id: ID of the ``acs_credential`` to encode onto a card. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, acs_encoder_id: str) -> AcsEncoder: + """Returns a specified `encoder `_. + + :param acs_encoder_id: ID of the encoder that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + acs_system_id: Optional[str] = None, + acs_system_ids: Optional[List[str]] = None, + acs_encoder_ids: Optional[List[str]] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + ) -> List[AcsEncoder]: + """Returns a list of all `encoders `_. + + :param acs_system_id: ID of the access system for which you want to retrieve all encoders. + + :param acs_system_ids: IDs of the access systems for which you want to retrieve all encoders. + + :param acs_encoder_ids: IDs of the encoders that you want to retrieve. + + :param limit: Number of encoders to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def scan_credential( + self, + *, + acs_encoder_id: str, + salto_ks_metadata: Optional[Dict[str, Any]] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Scans an encoded `acs_credential `_ from a plastic card placed on the specified `encoder `_. + + :param acs_encoder_id: ID of the encoder to use for the scan. + + :param salto_ks_metadata: Salto KS-specific metadata for the scan action. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def scan_to_assign_credential( + self, + *, + acs_encoder_id: str, + acs_user_id: Optional[str] = None, + salto_ks_metadata: Optional[Dict[str, Any]] = None, + user_identity_id: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Scans a physical card placed on the specified `encoder `_ and assigns the scanned credential to an ACS user. Provide either an ``acs_user_id`` or a ``user_identity_id``. + + :param acs_encoder_id: ID of the ``acs_encoder`` to use to scan the credential. + + :param acs_user_id: ID of the ``acs_user`` to assign the scanned credential to. + + :param salto_ks_metadata: Salto KS-specific metadata for the scan action. + + :param user_identity_id: ID of the ``user_identity`` to assign the scanned credential to. If the ACS system contains an ACS user linked to this user identity, it is used. Otherwise, one is created. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AcsEncoders(AbstractAcsEncoders): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -362,3 +486,245 @@ def scan_to_assign_credential( action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, ) + + +class AsyncAcsEncoders(AbstractAsyncAcsEncoders): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._simulate = AsyncAcsEncodersSimulate(client=client, defaults=defaults) + + @property + def simulate(self) -> AsyncAcsEncodersSimulate: + return self._simulate + + @route_metadata( + path="/acs/encoders/encode_credential", + has_required_parameters=True, + has_pagination=False, + ) + async def encode_credential( + self, + *, + acs_encoder_id: str, + access_method_id: Optional[str] = None, + acs_credential_id: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Encodes an existing `credential `_ onto a plastic card placed on the specified `encoder `_. Either provide an ``acs_credential_id`` or an ``access_method_id`` + + :param acs_encoder_id: ID of the ``acs_encoder`` to use to encode the ``acs_credential``. + + :param access_method_id: ID of the ``access_method`` to encode onto a card. + + :param acs_credential_id: ID of the ``acs_credential`` to encode onto a card. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_encoder_id is not None: + json_payload["acs_encoder_id"] = acs_encoder_id + if access_method_id is not None: + json_payload["access_method_id"] = access_method_id + if acs_credential_id is not None: + json_payload["acs_credential_id"] = acs_credential_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/encode_credential" + ) + + res = await self.client.post( + "/acs/encoders/encode_credential", json=json_payload + ) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/acs/encoders/get", has_required_parameters=True, has_pagination=False + ) + async def get(self, *, acs_encoder_id: str) -> AcsEncoder: + """Returns a specified `encoder `_. + + :param acs_encoder_id: ID of the encoder that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_encoder_id is not None: + params["acs_encoder_id"] = acs_encoder_id + + if not params: + raise ValueError("At least one parameter is required for /acs/encoders/get") + + res = await self.client.get("/acs/encoders/get", params=params) + + return AcsEncoder.from_dict(res["acs_encoder"]) + + @route_metadata( + path="/acs/encoders/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + acs_system_id: Optional[str] = None, + acs_system_ids: Optional[List[str]] = None, + acs_encoder_ids: Optional[List[str]] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + ) -> List[AcsEncoder]: + """Returns a list of all `encoders `_. + + :param acs_system_id: ID of the access system for which you want to retrieve all encoders. + + :param acs_system_ids: IDs of the access systems for which you want to retrieve all encoders. + + :param acs_encoder_ids: IDs of the encoders that you want to retrieve. + + :param limit: Number of encoders to return. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :returns: OK""" + params: Dict[str, Any] = {} + + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if acs_system_ids is not None: + params["acs_system_ids"] = acs_system_ids + if acs_encoder_ids is not None: + params["acs_encoder_ids"] = acs_encoder_ids + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + + res = await self.client.get("/acs/encoders/list", params=params) + + return [AcsEncoder.from_dict(item) for item in res["acs_encoders"]] + + @route_metadata( + path="/acs/encoders/scan_credential", + has_required_parameters=True, + has_pagination=False, + ) + async def scan_credential( + self, + *, + acs_encoder_id: str, + salto_ks_metadata: Optional[Dict[str, Any]] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Scans an encoded `acs_credential `_ from a plastic card placed on the specified `encoder `_. + + :param acs_encoder_id: ID of the encoder to use for the scan. + + :param salto_ks_metadata: Salto KS-specific metadata for the scan action. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_encoder_id is not None: + json_payload["acs_encoder_id"] = acs_encoder_id + if salto_ks_metadata is not None: + json_payload["salto_ks_metadata"] = salto_ks_metadata + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/scan_credential" + ) + + res = await self.client.post("/acs/encoders/scan_credential", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/acs/encoders/scan_to_assign_credential", + has_required_parameters=True, + has_pagination=False, + ) + async def scan_to_assign_credential( + self, + *, + acs_encoder_id: str, + acs_user_id: Optional[str] = None, + salto_ks_metadata: Optional[Dict[str, Any]] = None, + user_identity_id: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Scans a physical card placed on the specified `encoder `_ and assigns the scanned credential to an ACS user. Provide either an ``acs_user_id`` or a ``user_identity_id``. + + :param acs_encoder_id: ID of the ``acs_encoder`` to use to scan the credential. + + :param acs_user_id: ID of the ``acs_user`` to assign the scanned credential to. + + :param salto_ks_metadata: Salto KS-specific metadata for the scan action. + + :param user_identity_id: ID of the ``user_identity`` to assign the scanned credential to. If the ACS system contains an ACS user linked to this user identity, it is used. Otherwise, one is created. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_encoder_id is not None: + json_payload["acs_encoder_id"] = acs_encoder_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if salto_ks_metadata is not None: + json_payload["salto_ks_metadata"] = salto_ks_metadata + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/scan_to_assign_credential" + ) + + res = await self.client.post( + "/acs/encoders/scan_to_assign_credential", json=json_payload + ) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) diff --git a/seam/routes/acs_encoders_simulate.py b/seam/routes/acs_encoders_simulate.py index d5819adc..7f45007c 100644 --- a/seam/routes/acs_encoders_simulate.py +++ b/seam/routes/acs_encoders_simulate.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata @@ -77,6 +77,79 @@ def next_credential_scan_will_succeed( raise NotImplementedError() +class AbstractAsyncAcsEncodersSimulate(abc.ABC): + + @abc.abstractmethod + async def next_credential_encode_will_fail( + self, + *, + acs_encoder_id: str, + error_code: Optional[str] = None, + acs_credential_id: Optional[str] = None, + ) -> None: + """Simulates that the next attempt to encode a `credential `_ using the specified `encoder `_ will fail. You can only perform this action within a `sandbox workspace `_. + + :param acs_encoder_id: ID of the ``acs_encoder`` that will be used in the next request to encode the ``acs_credential``. + + :param error_code: Code of the error to simulate. + + :param acs_credential_id: ID of the ``acs_credential`` that will fail to be encoded onto a card in the next request. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def next_credential_encode_will_succeed( + self, *, acs_encoder_id: str, scenario: Optional[str] = None + ) -> None: + """Simulates that the next attempt to encode a `credential `_ using the specified `encoder `_ will succeed. You can only perform this action within a `sandbox workspace `_. + + :param acs_encoder_id: ID of the ``acs_encoder`` that will be used in the next request to encode the ``acs_credential``. + + :param scenario: Scenario to simulate. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def next_credential_scan_will_fail( + self, + *, + acs_encoder_id: str, + error_code: Optional[str] = None, + acs_credential_id_on_seam: Optional[str] = None, + ) -> None: + """Simulates that the next attempt to scan a `credential `_ using the specified `encoder `_ will fail. You can only perform this action within a `sandbox workspace `_. + + :param acs_encoder_id: ID of the ``acs_encoder`` that will fail to scan the ``acs_credential`` in the next request. + + :param error_code: + + :param acs_credential_id_on_seam: + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def next_credential_scan_will_succeed( + self, + *, + acs_encoder_id: str, + acs_credential_id_on_seam: Optional[str] = None, + scenario: Optional[str] = None, + ) -> None: + """Simulates that the next attempt to scan a `credential `_ using the specified `encoder `_ will succeed. You can only perform this action within a `sandbox workspace `_. + + :param acs_encoder_id: ID of the ``acs_encoder`` that will be used in the next request to scan the ``acs_credential``. + + :param acs_credential_id_on_seam: ID of the Seam ``acs_credential`` that matches the ``acs_credential`` on the encoder in this simulation. + + :param scenario: Scenario to simulate. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AcsEncodersSimulate(AbstractAcsEncodersSimulate): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -239,3 +312,167 @@ def next_credential_scan_will_succeed( ) return None + + +class AsyncAcsEncodersSimulate(AbstractAsyncAcsEncodersSimulate): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/acs/encoders/simulate/next_credential_encode_will_fail", + has_required_parameters=True, + has_pagination=False, + ) + async def next_credential_encode_will_fail( + self, + *, + acs_encoder_id: str, + error_code: Optional[str] = None, + acs_credential_id: Optional[str] = None, + ) -> None: + """Simulates that the next attempt to encode a `credential `_ using the specified `encoder `_ will fail. You can only perform this action within a `sandbox workspace `_. + + :param acs_encoder_id: ID of the ``acs_encoder`` that will be used in the next request to encode the ``acs_credential``. + + :param error_code: Code of the error to simulate. + + :param acs_credential_id: ID of the ``acs_credential`` that will fail to be encoded onto a card in the next request. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_encoder_id is not None: + json_payload["acs_encoder_id"] = acs_encoder_id + if error_code is not None: + json_payload["error_code"] = error_code + if acs_credential_id is not None: + json_payload["acs_credential_id"] = acs_credential_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/simulate/next_credential_encode_will_fail" + ) + + await self.client.post( + "/acs/encoders/simulate/next_credential_encode_will_fail", json=json_payload + ) + + return None + + @route_metadata( + path="/acs/encoders/simulate/next_credential_encode_will_succeed", + has_required_parameters=True, + has_pagination=False, + ) + async def next_credential_encode_will_succeed( + self, *, acs_encoder_id: str, scenario: Optional[str] = None + ) -> None: + """Simulates that the next attempt to encode a `credential `_ using the specified `encoder `_ will succeed. You can only perform this action within a `sandbox workspace `_. + + :param acs_encoder_id: ID of the ``acs_encoder`` that will be used in the next request to encode the ``acs_credential``. + + :param scenario: Scenario to simulate. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_encoder_id is not None: + json_payload["acs_encoder_id"] = acs_encoder_id + if scenario is not None: + json_payload["scenario"] = scenario + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/simulate/next_credential_encode_will_succeed" + ) + + await self.client.post( + "/acs/encoders/simulate/next_credential_encode_will_succeed", + json=json_payload, + ) + + return None + + @route_metadata( + path="/acs/encoders/simulate/next_credential_scan_will_fail", + has_required_parameters=True, + has_pagination=False, + ) + async def next_credential_scan_will_fail( + self, + *, + acs_encoder_id: str, + error_code: Optional[str] = None, + acs_credential_id_on_seam: Optional[str] = None, + ) -> None: + """Simulates that the next attempt to scan a `credential `_ using the specified `encoder `_ will fail. You can only perform this action within a `sandbox workspace `_. + + :param acs_encoder_id: ID of the ``acs_encoder`` that will fail to scan the ``acs_credential`` in the next request. + + :param error_code: + + :param acs_credential_id_on_seam: + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_encoder_id is not None: + json_payload["acs_encoder_id"] = acs_encoder_id + if error_code is not None: + json_payload["error_code"] = error_code + if acs_credential_id_on_seam is not None: + json_payload["acs_credential_id_on_seam"] = acs_credential_id_on_seam + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/simulate/next_credential_scan_will_fail" + ) + + await self.client.post( + "/acs/encoders/simulate/next_credential_scan_will_fail", json=json_payload + ) + + return None + + @route_metadata( + path="/acs/encoders/simulate/next_credential_scan_will_succeed", + has_required_parameters=True, + has_pagination=False, + ) + async def next_credential_scan_will_succeed( + self, + *, + acs_encoder_id: str, + acs_credential_id_on_seam: Optional[str] = None, + scenario: Optional[str] = None, + ) -> None: + """Simulates that the next attempt to scan a `credential `_ using the specified `encoder `_ will succeed. You can only perform this action within a `sandbox workspace `_. + + :param acs_encoder_id: ID of the ``acs_encoder`` that will be used in the next request to scan the ``acs_credential``. + + :param acs_credential_id_on_seam: ID of the Seam ``acs_credential`` that matches the ``acs_credential`` on the encoder in this simulation. + + :param scenario: Scenario to simulate. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_encoder_id is not None: + json_payload["acs_encoder_id"] = acs_encoder_id + if acs_credential_id_on_seam is not None: + json_payload["acs_credential_id_on_seam"] = acs_credential_id_on_seam + if scenario is not None: + json_payload["scenario"] = scenario + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/simulate/next_credential_scan_will_succeed" + ) + + await self.client.post( + "/acs/encoders/simulate/next_credential_scan_will_succeed", + json=json_payload, + ) + + return None diff --git a/seam/routes/acs_entrances.py b/seam/routes/acs_entrances.py index b4a02333..3198eeb8 100644 --- a/seam/routes/acs_entrances.py +++ b/seam/routes/acs_entrances.py @@ -1,10 +1,13 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import AcsEntrance, AcsCredential, ActionAttempt -from ..modules.action_attempts import resolve_action_attempt +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, +) class AbstractAcsEntrances(abc.ABC): @@ -119,6 +122,118 @@ def unlock( raise NotImplementedError() +class AbstractAsyncAcsEntrances(abc.ABC): + + @abc.abstractmethod + async def get(self, *, acs_entrance_id: str) -> AcsEntrance: + """Returns a specified `access system entrance `_. + + :param acs_entrance_id: ID of the entrance that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def grant_access( + self, + *, + acs_entrance_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Grants a specified `access system user `_ access to a specified `access system entrance `_. + + :param acs_entrance_id: ID of the entrance to which you want to grant an access system user access. + + :param acs_user_id: ID of the access system user to whom you want to grant access to an entrance. You can only provide one of acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity to whom you want to grant access to an entrance. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the access group membership belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + access_method_id: Optional[str] = None, + acs_credential_id: Optional[str] = None, + acs_entrance_ids: Optional[List[str]] = None, + acs_system_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + limit: Optional[int] = None, + location_id: Optional[Union[str, Null]] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + space_id: Optional[str] = None, + ) -> List[AcsEntrance]: + """Returns a list of all `access system entrances `_. + + :param access_method_id: ID of the access method for which you want to retrieve all entrances to which it grants access. + + :param acs_credential_id: ID of the credential for which you want to retrieve all entrances. + + :param acs_entrance_ids: IDs of the entrances for which you want to retrieve all entrances. + + :param acs_system_id: ID of the access system for which you want to retrieve all entrances. + + :param connected_account_id: ID of the connected account for which you want to retrieve all entrances. + + :param customer_key: Customer key for which you want to list entrances. + + :param limit: Maximum number of records to return per page. + + :param location_id: Deprecated: Use ``space_id``. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned entrances to include all records that satisfy a partial match using ``display_name``. + + :param space_id: ID of the space for which you want to list entrances. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_credentials_with_access( + self, *, acs_entrance_id: str, include_if: Optional[List[str]] = None + ) -> List[AcsCredential]: + """Returns a list of all `credentials `_ with access to a specified `entrance `_. + + :param acs_entrance_id: ID of the entrance for which you want to list all credentials that grant access. + + :param include_if: Conditions that credentials must meet to be included in the returned list. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def unlock( + self, + *, + acs_credential_id: str, + acs_entrance_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Remotely unlocks a specified `entrance `_ using a cloud_key credential. Returns an action attempt that tracks the progress of the unlock operation. + + :param acs_credential_id: ID of the cloud_key credential to use for the unlock operation. + + :param acs_entrance_id: ID of the entrance to unlock. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AcsEntrances(AbstractAcsEntrances): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -341,3 +456,227 @@ def unlock( action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, ) + + +class AsyncAcsEntrances(AbstractAsyncAcsEntrances): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/acs/entrances/get", has_required_parameters=True, has_pagination=False + ) + async def get(self, *, acs_entrance_id: str) -> AcsEntrance: + """Returns a specified `access system entrance `_. + + :param acs_entrance_id: ID of the entrance that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_entrance_id is not None: + params["acs_entrance_id"] = acs_entrance_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/entrances/get" + ) + + res = await self.client.get("/acs/entrances/get", params=params) + + return AcsEntrance.from_dict(res["acs_entrance"]) + + @route_metadata( + path="/acs/entrances/grant_access", + has_required_parameters=True, + has_pagination=False, + ) + async def grant_access( + self, + *, + acs_entrance_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Grants a specified `access system user `_ access to a specified `access system entrance `_. + + :param acs_entrance_id: ID of the entrance to which you want to grant an access system user access. + + :param acs_user_id: ID of the access system user to whom you want to grant access to an entrance. You can only provide one of acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity to whom you want to grant access to an entrance. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the access group membership belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_entrance_id is not None: + json_payload["acs_entrance_id"] = acs_entrance_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/entrances/grant_access" + ) + + await self.client.post("/acs/entrances/grant_access", json=json_payload) + + return None + + @route_metadata( + path="/acs/entrances/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + access_method_id: Optional[str] = None, + acs_credential_id: Optional[str] = None, + acs_entrance_ids: Optional[List[str]] = None, + acs_system_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + limit: Optional[int] = None, + location_id: Optional[Union[str, Null]] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + space_id: Optional[str] = None, + ) -> List[AcsEntrance]: + """Returns a list of all `access system entrances `_. + + :param access_method_id: ID of the access method for which you want to retrieve all entrances to which it grants access. + + :param acs_credential_id: ID of the credential for which you want to retrieve all entrances. + + :param acs_entrance_ids: IDs of the entrances for which you want to retrieve all entrances. + + :param acs_system_id: ID of the access system for which you want to retrieve all entrances. + + :param connected_account_id: ID of the connected account for which you want to retrieve all entrances. + + :param customer_key: Customer key for which you want to list entrances. + + :param limit: Maximum number of records to return per page. + + :param location_id: Deprecated: Use ``space_id``. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned entrances to include all records that satisfy a partial match using ``display_name``. + + :param space_id: ID of the space for which you want to list entrances. + + :returns: OK""" + params: Dict[str, Any] = {} + + if access_method_id is not None: + params["access_method_id"] = access_method_id + if acs_credential_id is not None: + params["acs_credential_id"] = acs_credential_id + if acs_entrance_ids is not None: + params["acs_entrance_ids"] = acs_entrance_ids + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if customer_key is not None: + params["customer_key"] = customer_key + if limit is not None: + params["limit"] = limit + if location_id is not None: + params["location_id"] = location_id + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if space_id is not None: + params["space_id"] = space_id + + res = await self.client.get("/acs/entrances/list", params=params) + + return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + + @route_metadata( + path="/acs/entrances/list_credentials_with_access", + has_required_parameters=True, + has_pagination=False, + ) + async def list_credentials_with_access( + self, *, acs_entrance_id: str, include_if: Optional[List[str]] = None + ) -> List[AcsCredential]: + """Returns a list of all `credentials `_ with access to a specified `entrance `_. + + :param acs_entrance_id: ID of the entrance for which you want to list all credentials that grant access. + + :param include_if: Conditions that credentials must meet to be included in the returned list. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_entrance_id is not None: + params["acs_entrance_id"] = acs_entrance_id + if include_if is not None: + params["include_if"] = include_if + + if not params: + raise ValueError( + "At least one parameter is required for /acs/entrances/list_credentials_with_access" + ) + + res = await self.client.get( + "/acs/entrances/list_credentials_with_access", params=params + ) + + return [AcsCredential.from_dict(item) for item in res["acs_credentials"]] + + @route_metadata( + path="/acs/entrances/unlock", has_required_parameters=True, has_pagination=False + ) + async def unlock( + self, + *, + acs_credential_id: str, + acs_entrance_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Remotely unlocks a specified `entrance `_ using a cloud_key credential. Returns an action attempt that tracks the progress of the unlock operation. + + :param acs_credential_id: ID of the cloud_key credential to use for the unlock operation. + + :param acs_entrance_id: ID of the entrance to unlock. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_credential_id is not None: + json_payload["acs_credential_id"] = acs_credential_id + if acs_entrance_id is not None: + json_payload["acs_entrance_id"] = acs_entrance_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/entrances/unlock" + ) + + res = await self.client.post("/acs/entrances/unlock", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) diff --git a/seam/routes/acs_systems.py b/seam/routes/acs_systems.py index 0766d017..58474d02 100644 --- a/seam/routes/acs_systems.py +++ b/seam/routes/acs_systems.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import AcsSystem @@ -74,6 +74,75 @@ def report_devices( raise NotImplementedError() +class AbstractAsyncAcsSystems(abc.ABC): + + @abc.abstractmethod + async def get(self, *, acs_system_id: str) -> AcsSystem: + """Returns a specified `access system `_. + + :param acs_system_id: ID of the access system that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + search: Optional[str] = None, + ) -> List[AcsSystem]: + """Returns a list of all `access systems `_. + + To filter the list of returned access systems by a specific connected account ID, include the ``connected_account_id`` in the request body. If you omit the ``connected_account_id`` parameter, the response includes all access systems connected to your workspace. + + :param connected_account_id: ID of the connected account by which you want to filter the list of access systems. + + :param customer_key: Customer key for which you want to list access systems. + + :param search: String for which to search. Filters returned access systems to include all records that satisfy a partial match using ``name`` or ``acs_system_id``. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_compatible_credential_manager_acs_systems( + self, *, acs_system_id: str + ) -> List[AcsSystem]: + """Returns a list of all credential manager systems that are compatible with a specified `access system `_. + + Specify the access system for which you want to retrieve all compatible credential manager systems by including the corresponding ``acs_system_id`` in the request body. + + :param acs_system_id: ID of the access system for which you want to retrieve all compatible credential manager systems. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def report_devices( + self, + *, + acs_system_id: str, + acs_encoders: Optional[List[Dict[str, Any]]] = None, + acs_entrances: Optional[List[Dict[str, Any]]] = None, + ) -> None: + """Reports ACS system device status including encoders and entrances. + + :param acs_system_id: ID of the ACS system to report resources for + + :param acs_encoders: Array of ACS encoders to report + + :param acs_entrances: Array of ACS entrances to report + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AcsSystems(AbstractAcsSystems): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -207,3 +276,138 @@ def report_devices( self.client.post("/acs/systems/report_devices", json=json_payload) return None + + +class AsyncAcsSystems(AbstractAsyncAcsSystems): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/acs/systems/get", has_required_parameters=True, has_pagination=False + ) + async def get(self, *, acs_system_id: str) -> AcsSystem: + """Returns a specified `access system `_. + + :param acs_system_id: ID of the access system that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + + if not params: + raise ValueError("At least one parameter is required for /acs/systems/get") + + res = await self.client.get("/acs/systems/get", params=params) + + return AcsSystem.from_dict(res["acs_system"]) + + @route_metadata( + path="/acs/systems/list", has_required_parameters=False, has_pagination=False + ) + async def list( + self, + *, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + search: Optional[str] = None, + ) -> List[AcsSystem]: + """Returns a list of all `access systems `_. + + To filter the list of returned access systems by a specific connected account ID, include the ``connected_account_id`` in the request body. If you omit the ``connected_account_id`` parameter, the response includes all access systems connected to your workspace. + + :param connected_account_id: ID of the connected account by which you want to filter the list of access systems. + + :param customer_key: Customer key for which you want to list access systems. + + :param search: String for which to search. Filters returned access systems to include all records that satisfy a partial match using ``name`` or ``acs_system_id``. + + :returns: OK""" + params: Dict[str, Any] = {} + + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if customer_key is not None: + params["customer_key"] = customer_key + if search is not None: + params["search"] = search + + res = await self.client.get("/acs/systems/list", params=params) + + return [AcsSystem.from_dict(item) for item in res["acs_systems"]] + + @route_metadata( + path="/acs/systems/list_compatible_credential_manager_acs_systems", + has_required_parameters=True, + has_pagination=False, + ) + async def list_compatible_credential_manager_acs_systems( + self, *, acs_system_id: str + ) -> List[AcsSystem]: + """Returns a list of all credential manager systems that are compatible with a specified `access system `_. + + Specify the access system for which you want to retrieve all compatible credential manager systems by including the corresponding ``acs_system_id`` in the request body. + + :param acs_system_id: ID of the access system for which you want to retrieve all compatible credential manager systems. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/systems/list_compatible_credential_manager_acs_systems" + ) + + res = await self.client.get( + "/acs/systems/list_compatible_credential_manager_acs_systems", params=params + ) + + return [AcsSystem.from_dict(item) for item in res["acs_systems"]] + + @route_metadata( + path="/acs/systems/report_devices", + has_required_parameters=True, + has_pagination=False, + ) + async def report_devices( + self, + *, + acs_system_id: str, + acs_encoders: Optional[List[Dict[str, Any]]] = None, + acs_entrances: Optional[List[Dict[str, Any]]] = None, + ) -> None: + """Reports ACS system device status including encoders and entrances. + + :param acs_system_id: ID of the ACS system to report resources for + + :param acs_encoders: Array of ACS encoders to report + + :param acs_entrances: Array of ACS entrances to report + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_system_id is not None: + json_payload["acs_system_id"] = acs_system_id + if acs_encoders is not None: + json_payload["acs_encoders"] = acs_encoders + if acs_entrances is not None: + json_payload["acs_entrances"] = acs_entrances + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/systems/report_devices" + ) + + await self.client.post("/acs/systems/report_devices", json=json_payload) + + return None diff --git a/seam/routes/acs_users.py b/seam/routes/acs_users.py index 72d854e8..ffbcdd3c 100644 --- a/seam/routes/acs_users.py +++ b/seam/routes/acs_users.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import AcsUser, AcsEntrance @@ -266,6 +266,266 @@ def update( raise NotImplementedError() +class AbstractAsyncAcsUsers(abc.ABC): + + @abc.abstractmethod + async def add_to_access_group( + self, *, acs_access_group_id: str, acs_user_id: str + ) -> None: + """Adds a specified `access system user `_ to a specified `access group `_. + + :param acs_access_group_id: ID of the access group to which you want to add an access system user. + + :param acs_user_id: ID of the access system user that you want to add to an access group. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def create( + self, + *, + acs_system_id: str, + full_name: str, + access_schedule: Optional[Dict[str, Any]] = None, + acs_access_group_ids: Optional[List[str]] = None, + email: Optional[str] = None, + email_address: Optional[str] = None, + phone_number: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> AcsUser: + """Creates a new `access system user `_. + + :param acs_system_id: ID of the access system to which you want to add the new access system user. + + :param full_name: Full name of the new access system user. + + :param access_schedule: ``starts_at`` and ``ends_at`` timestamps for the new access system user's access. If you specify an ``access_schedule``, you may include both ``starts_at`` and ``ends_at``. If you omit ``starts_at``, it defaults to the current time. ``ends_at`` is optional and must be a time in the future and after ``starts_at``. + + :param acs_access_group_ids: Array of access group IDs to indicate the access groups to which you want to add the new access system user. + + :param email: Deprecated: use email_address. + + :param email_address: Email address of the `access system user `_. + + :param phone_number: Phone number of the `access system user `_ in E.164 format (for example, ``+15555550100``). + + :param user_identity_id: ID of the user identity with which you want to associate the new access system user. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Deletes a specified `access system user `_ and invalidates the access system user's `credentials `_. + + :param acs_system_id: ID of the access system that you want to delete. You must provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user that you want to delete. You must provide either acs_user_id or user_identity_id + + :param user_identity_id: ID of the user identity that you want to delete. You must provide either acs_user_id or user_identity_id. If you provide user_identity_id, you must also provide acs_system_id. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, + *, + acs_user_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> AcsUser: + """Returns a specified `access system user `_. + + :param acs_user_id: ID of the access system user that you want to get. You can only provide acs_user_id or user_identity_id. + + :param acs_system_id: ID of the access system that you want to get. You can only provide acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity that you want to get. You can only provide acs_user_id or user_identity_id. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + acs_system_id: Optional[str] = None, + created_before: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identity_email_address: Optional[str] = None, + user_identity_id: Optional[str] = None, + user_identity_phone_number: Optional[str] = None, + ) -> List[AcsUser]: + """Returns a list of all `access system users `_. + + :param acs_system_id: ID of the ``acs_system`` for which you want to retrieve all access system users. + + :param created_before: Timestamp by which to limit returned access system users. Returns users created before this timestamp. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned access system users to include all records that satisfy a partial match using ``full_name``, ``phone_number``, ``email_address``, ``acs_user_id``, ``user_identity_id``, ``user_identity_full_name`` or ``user_identity_phone_number``. + + :param user_identity_email_address: Email address of the user identity for which you want to retrieve all access system users. + + :param user_identity_id: ID of the user identity for which you want to retrieve all access system users. + + :param user_identity_phone_number: Phone number of the user identity for which you want to retrieve all access system users, in `E.164 format `_ (for example, ``+15555550100``). + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_accessible_entrances( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> List[AcsEntrance]: + """Lists the `entrances `_ to which a specified `access system user `_ has access. + + :param acs_system_id: ID of the access system for which you want to list accessible entrances. You can only provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user for whom you want to list accessible entrances. You can only provide acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity for whom you want to list accessible entrances. You can only provide acs_user_id or user_identity_id. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def remove_from_access_group( + self, + *, + acs_access_group_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Removes a specified `access system user `_ from a specified `access group `_. + + :param acs_access_group_id: ID of the access group from which you want to remove an access system user. + + :param acs_user_id: ID of the access system user that you want to remove from an access group. You can only provide acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity that you want to remove from an access group. You can only provide acs_user_id or user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def revoke_access_to_all_entrances( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Revokes access to all `entrances `_ for a specified `access system user `_. + + :param acs_system_id: ID of the access system for which you want to revoke access. You can only provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user for whom you want to revoke access. You can only provide acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity for whom you want to revoke access. You can only provide acs_user_id or user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def suspend( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """`Suspends `_ a specified `access system user `_. Suspending an access system user revokes their access temporarily. To restore an access system user's access, you can `unsuspend `_ them. + + :param acs_system_id: ID of the access system that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :param acs_user_id: ID of the access system user that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :param user_identity_id: ID of the user identity that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def unsuspend( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """`Unsuspends `_ a specified suspended `access system user `_. While `suspending an access system user `_ revokes their access temporarily, unsuspending the access system user restores their access. + + :param acs_system_id: ID of the access system of the user that you want to unsuspend. You can only provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user that you want to unsuspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :param user_identity_id: ID of the user identity that you want to unsuspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + access_schedule: Optional[Union[Dict[str, Any], Null]] = None, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + email: Optional[str] = None, + email_address: Optional[str] = None, + full_name: Optional[str] = None, + hid_acs_system_id: Optional[str] = None, + phone_number: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Updates the properties of a specified `access system user `_. + + :param access_schedule: ``starts_at`` and ``ends_at`` timestamps for the access system user's access. If you specify an ``access_schedule``, you may include both ``starts_at`` and ``ends_at``. If you omit ``starts_at``, it defaults to the current time. ``ends_at`` is optional and must be a time in the future and after ``starts_at``. + + :param acs_system_id: ID of the access system that you want to update. You can only provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user that you want to update. You can only provide acs_user_id or user_identity_id. + + :param email: Deprecated: use email_address. + + :param email_address: Email address of the `access system user `_. + + :param full_name: Full name of the `access system user `_. + + :param hid_acs_system_id: ID of the HID access control system associated with the user. + + :param phone_number: Phone number of the `access system user `_ in E.164 format (for example, ``+15555550100``). + + :param user_identity_id: ID of the user identity that you want to update. You can only provide acs_user_id or user_identity_id. If you provide user_identity_id, you must also provide acs_system_id. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class AcsUsers(AbstractAcsUsers): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -750,3 +1010,493 @@ def update( self.client.patch("/acs/users/update", json=json_payload) return None + + +class AsyncAcsUsers(AbstractAsyncAcsUsers): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/acs/users/add_to_access_group", + has_required_parameters=True, + has_pagination=False, + ) + async def add_to_access_group( + self, *, acs_access_group_id: str, acs_user_id: str + ) -> None: + """Adds a specified `access system user `_ to a specified `access group `_. + + :param acs_access_group_id: ID of the access group to which you want to add an access system user. + + :param acs_user_id: ID of the access system user that you want to add to an access group. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_access_group_id is not None: + json_payload["acs_access_group_id"] = acs_access_group_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/users/add_to_access_group" + ) + + await self.client.put("/acs/users/add_to_access_group", json=json_payload) + + return None + + @route_metadata( + path="/acs/users/create", has_required_parameters=True, has_pagination=False + ) + async def create( + self, + *, + acs_system_id: str, + full_name: str, + access_schedule: Optional[Dict[str, Any]] = None, + acs_access_group_ids: Optional[List[str]] = None, + email: Optional[str] = None, + email_address: Optional[str] = None, + phone_number: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> AcsUser: + """Creates a new `access system user `_. + + :param acs_system_id: ID of the access system to which you want to add the new access system user. + + :param full_name: Full name of the new access system user. + + :param access_schedule: ``starts_at`` and ``ends_at`` timestamps for the new access system user's access. If you specify an ``access_schedule``, you may include both ``starts_at`` and ``ends_at``. If you omit ``starts_at``, it defaults to the current time. ``ends_at`` is optional and must be a time in the future and after ``starts_at``. + + :param acs_access_group_ids: Array of access group IDs to indicate the access groups to which you want to add the new access system user. + + :param email: Deprecated: use email_address. + + :param email_address: Email address of the `access system user `_. + + :param phone_number: Phone number of the `access system user `_ in E.164 format (for example, ``+15555550100``). + + :param user_identity_id: ID of the user identity with which you want to associate the new access system user. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_system_id is not None: + json_payload["acs_system_id"] = acs_system_id + if full_name is not None: + json_payload["full_name"] = full_name + if access_schedule is not None: + json_payload["access_schedule"] = access_schedule + if acs_access_group_ids is not None: + json_payload["acs_access_group_ids"] = acs_access_group_ids + if email is not None: + json_payload["email"] = email + if email_address is not None: + json_payload["email_address"] = email_address + if phone_number is not None: + json_payload["phone_number"] = phone_number + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError("At least one parameter is required for /acs/users/create") + + res = await self.client.post("/acs/users/create", json=json_payload) + + return AcsUser.from_dict(res["acs_user"]) + + @route_metadata( + path="/acs/users/delete", has_required_parameters=True, has_pagination=False + ) + async def delete( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Deletes a specified `access system user `_ and invalidates the access system user's `credentials `_. + + :param acs_system_id: ID of the access system that you want to delete. You must provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user that you want to delete. You must provide either acs_user_id or user_identity_id + + :param user_identity_id: ID of the user identity that you want to delete. You must provide either acs_user_id or user_identity_id. If you provide user_identity_id, you must also provide acs_system_id. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError("At least one parameter is required for /acs/users/delete") + + await self.client.delete("/acs/users/delete", params=params) + + return None + + @route_metadata( + path="/acs/users/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, + *, + acs_user_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> AcsUser: + """Returns a specified `access system user `_. + + :param acs_user_id: ID of the access system user that you want to get. You can only provide acs_user_id or user_identity_id. + + :param acs_system_id: ID of the access system that you want to get. You can only provide acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity that you want to get. You can only provide acs_user_id or user_identity_id. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError("At least one parameter is required for /acs/users/get") + + res = await self.client.get("/acs/users/get", params=params) + + return AcsUser.from_dict(res["acs_user"]) + + @route_metadata( + path="/acs/users/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + acs_system_id: Optional[str] = None, + created_before: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identity_email_address: Optional[str] = None, + user_identity_id: Optional[str] = None, + user_identity_phone_number: Optional[str] = None, + ) -> List[AcsUser]: + """Returns a list of all `access system users `_. + + :param acs_system_id: ID of the ``acs_system`` for which you want to retrieve all access system users. + + :param created_before: Timestamp by which to limit returned access system users. Returns users created before this timestamp. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned access system users to include all records that satisfy a partial match using ``full_name``, ``phone_number``, ``email_address``, ``acs_user_id``, ``user_identity_id``, ``user_identity_full_name`` or ``user_identity_phone_number``. + + :param user_identity_email_address: Email address of the user identity for which you want to retrieve all access system users. + + :param user_identity_id: ID of the user identity for which you want to retrieve all access system users. + + :param user_identity_phone_number: Phone number of the user identity for which you want to retrieve all access system users, in `E.164 format `_ (for example, ``+15555550100``). + + :returns: OK""" + params: Dict[str, Any] = {} + + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if created_before is not None: + params["created_before"] = created_before + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if user_identity_email_address is not None: + params["user_identity_email_address"] = user_identity_email_address + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + if user_identity_phone_number is not None: + params["user_identity_phone_number"] = user_identity_phone_number + + res = await self.client.get("/acs/users/list", params=params) + + return [AcsUser.from_dict(item) for item in res["acs_users"]] + + @route_metadata( + path="/acs/users/list_accessible_entrances", + has_required_parameters=True, + has_pagination=False, + ) + async def list_accessible_entrances( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> List[AcsEntrance]: + """Lists the `entrances `_ to which a specified `access system user `_ has access. + + :param acs_system_id: ID of the access system for which you want to list accessible entrances. You can only provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user for whom you want to list accessible entrances. You can only provide acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity for whom you want to list accessible entrances. You can only provide acs_user_id or user_identity_id. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/users/list_accessible_entrances" + ) + + res = await self.client.get( + "/acs/users/list_accessible_entrances", params=params + ) + + return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + + @route_metadata( + path="/acs/users/remove_from_access_group", + has_required_parameters=True, + has_pagination=False, + ) + async def remove_from_access_group( + self, + *, + acs_access_group_id: str, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Removes a specified `access system user `_ from a specified `access group `_. + + :param acs_access_group_id: ID of the access group from which you want to remove an access system user. + + :param acs_user_id: ID of the access system user that you want to remove from an access group. You can only provide acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity that you want to remove from an access group. You can only provide acs_user_id or user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_access_group_id is not None: + params["acs_access_group_id"] = acs_access_group_id + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/users/remove_from_access_group" + ) + + await self.client.delete("/acs/users/remove_from_access_group", params=params) + + return None + + @route_metadata( + path="/acs/users/revoke_access_to_all_entrances", + has_required_parameters=True, + has_pagination=False, + ) + async def revoke_access_to_all_entrances( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Revokes access to all `entrances `_ for a specified `access system user `_. + + :param acs_system_id: ID of the access system for which you want to revoke access. You can only provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user for whom you want to revoke access. You can only provide acs_user_id or user_identity_id. + + :param user_identity_id: ID of the user identity for whom you want to revoke access. You can only provide acs_user_id or user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_system_id is not None: + json_payload["acs_system_id"] = acs_system_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/users/revoke_access_to_all_entrances" + ) + + await self.client.post( + "/acs/users/revoke_access_to_all_entrances", json=json_payload + ) + + return None + + @route_metadata( + path="/acs/users/suspend", has_required_parameters=True, has_pagination=False + ) + async def suspend( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """`Suspends `_ a specified `access system user `_. Suspending an access system user revokes their access temporarily. To restore an access system user's access, you can `unsuspend `_ them. + + :param acs_system_id: ID of the access system that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :param acs_user_id: ID of the access system user that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :param user_identity_id: ID of the user identity that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_system_id is not None: + json_payload["acs_system_id"] = acs_system_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/users/suspend" + ) + + await self.client.post("/acs/users/suspend", json=json_payload) + + return None + + @route_metadata( + path="/acs/users/unsuspend", has_required_parameters=True, has_pagination=False + ) + async def unsuspend( + self, + *, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """`Unsuspends `_ a specified suspended `access system user `_. While `suspending an access system user `_ revokes their access temporarily, unsuspending the access system user restores their access. + + :param acs_system_id: ID of the access system of the user that you want to unsuspend. You can only provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user that you want to unsuspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :param user_identity_id: ID of the user identity that you want to unsuspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_system_id is not None: + json_payload["acs_system_id"] = acs_system_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/users/unsuspend" + ) + + await self.client.post("/acs/users/unsuspend", json=json_payload) + + return None + + @route_metadata( + path="/acs/users/update", has_required_parameters=True, has_pagination=False + ) + async def update( + self, + *, + access_schedule: Optional[Union[Dict[str, Any], Null]] = None, + acs_system_id: Optional[str] = None, + acs_user_id: Optional[str] = None, + email: Optional[str] = None, + email_address: Optional[str] = None, + full_name: Optional[str] = None, + hid_acs_system_id: Optional[str] = None, + phone_number: Optional[str] = None, + user_identity_id: Optional[str] = None, + ) -> None: + """Updates the properties of a specified `access system user `_. + + :param access_schedule: ``starts_at`` and ``ends_at`` timestamps for the access system user's access. If you specify an ``access_schedule``, you may include both ``starts_at`` and ``ends_at``. If you omit ``starts_at``, it defaults to the current time. ``ends_at`` is optional and must be a time in the future and after ``starts_at``. + + :param acs_system_id: ID of the access system that you want to update. You can only provide acs_system_id with user_identity_id. + + :param acs_user_id: ID of the access system user that you want to update. You can only provide acs_user_id or user_identity_id. + + :param email: Deprecated: use email_address. + + :param email_address: Email address of the `access system user `_. + + :param full_name: Full name of the `access system user `_. + + :param hid_acs_system_id: ID of the HID access control system associated with the user. + + :param phone_number: Phone number of the `access system user `_ in E.164 format (for example, ``+15555550100``). + + :param user_identity_id: ID of the user identity that you want to update. You can only provide acs_user_id or user_identity_id. If you provide user_identity_id, you must also provide acs_system_id. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if access_schedule is not None: + json_payload["access_schedule"] = access_schedule + if acs_system_id is not None: + json_payload["acs_system_id"] = acs_system_id + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if email is not None: + json_payload["email"] = email + if email_address is not None: + json_payload["email_address"] = email_address + if full_name is not None: + json_payload["full_name"] = full_name + if hid_acs_system_id is not None: + json_payload["hid_acs_system_id"] = hid_acs_system_id + if phone_number is not None: + json_payload["phone_number"] = phone_number + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError("At least one parameter is required for /acs/users/update") + + await self.client.patch("/acs/users/update", json=json_payload) + + return None diff --git a/seam/routes/action_attempts.py b/seam/routes/action_attempts.py index 95f8ea36..12a65a44 100644 --- a/seam/routes/action_attempts.py +++ b/seam/routes/action_attempts.py @@ -1,10 +1,13 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import ActionAttempt -from ..modules.action_attempts import resolve_action_attempt +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, +) class AbstractActionAttempts(abc.ABC): @@ -50,6 +53,49 @@ def list( raise NotImplementedError() +class AbstractAsyncActionAttempts(abc.ABC): + + @abc.abstractmethod + async def get( + self, + *, + action_attempt_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Returns a specified `action attempt `_. + + :param action_attempt_id: ID of the action attempt that you want to get. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + action_attempt_ids: Optional[List[str]] = None, + device_id: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + ) -> List[ActionAttempt]: + """Returns a list of the `action attempts `_ that you specify as an array of ``action_attempt_id``s. + + :param action_attempt_ids: IDs of the action attempts that you want to retrieve. + + :param device_id: ID of the device to filter action attempts by. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :returns: OK""" + raise NotImplementedError() + + class ActionAttempts(AbstractActionAttempts): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -133,3 +179,88 @@ def list( res = self.client.get("/action_attempts/list", params=params) return [ActionAttempt.from_dict(item) for item in res["action_attempts"]] + + +class AsyncActionAttempts(AbstractAsyncActionAttempts): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/action_attempts/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, + *, + action_attempt_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Returns a specified `action attempt `_. + + :param action_attempt_id: ID of the action attempt that you want to get. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if action_attempt_id is not None: + params["action_attempt_id"] = action_attempt_id + + if not params: + raise ValueError( + "At least one parameter is required for /action_attempts/get" + ) + + res = await self.client.get("/action_attempts/get", params=params) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/action_attempts/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + action_attempt_ids: Optional[List[str]] = None, + device_id: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + ) -> List[ActionAttempt]: + """Returns a list of the `action attempts `_ that you specify as an array of ``action_attempt_id``s. + + :param action_attempt_ids: IDs of the action attempts that you want to retrieve. + + :param device_id: ID of the device to filter action attempts by. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :returns: OK""" + params: Dict[str, Any] = {} + + if action_attempt_ids is not None: + params["action_attempt_ids"] = action_attempt_ids + if device_id is not None: + params["device_id"] = device_id + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + + res = await self.client.get("/action_attempts/list", params=params) + + return [ActionAttempt.from_dict(item) for item in res["action_attempts"]] diff --git a/seam/routes/client_sessions.py b/seam/routes/client_sessions.py index 50414c6a..c48416d4 100644 --- a/seam/routes/client_sessions.py +++ b/seam/routes/client_sessions.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import ClientSession @@ -159,6 +159,160 @@ def revoke(self, *, client_session_id: str) -> None: raise NotImplementedError() +class AbstractAsyncClientSessions(abc.ABC): + + @abc.abstractmethod + async def create( + self, + *, + connect_webview_ids: Optional[List[str]] = None, + connected_account_ids: Optional[List[str]] = None, + customer_id: Optional[str] = None, + customer_key: Optional[str] = None, + expires_at: Optional[str] = None, + user_identifier_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + user_identity_ids: Optional[List[str]] = None, + ) -> ClientSession: + """Creates a new `client session `_. + + :param connect_webview_ids: IDs of the `Connect Webviews `_ for which you want to create a client session. + + :param connected_account_ids: IDs of the `connected accounts `_ for which you want to create a client session. + + :param customer_id: Customer ID that you want to associate with the new client session. + + :param customer_key: Customer key that you want to associate with the new client session. + + :param expires_at: Date and time at which the client session should expire, in `ISO 8601 `_ format. + + :param user_identifier_key: Your user ID for the user for whom you want to create a client session. + + :param user_identity_id: ID of the `user identity `_ for which you want to create a client session. + + :param user_identity_ids: Deprecated: Use ``user_identity_id`` instead. IDs of the `user identities `_ that you want to associate with the client session. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, client_session_id: str) -> None: + """Deletes a `client session `_. + + :param client_session_id: ID of the client session that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, + *, + client_session_id: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> ClientSession: + """Returns a specified `client session `_. + + :param client_session_id: ID of the client session that you want to get. + + :param user_identifier_key: User identifier key associated with the client session that you want to get. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def get_or_create( + self, + *, + connect_webview_ids: Optional[List[str]] = None, + connected_account_ids: Optional[List[str]] = None, + expires_at: Optional[str] = None, + user_identifier_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + user_identity_ids: Optional[List[str]] = None, + ) -> ClientSession: + """Returns a `client session `_ with specific characteristics or creates a new client session with these characteristics if it does not yet exist. + + :param connect_webview_ids: IDs of the `Connect Webviews `_ that you want to associate with the client session (or that are already associated with the existing client session). + + :param connected_account_ids: IDs of the `connected accounts `_ that you want to associate with the client session (or that are already associated with the existing client session). + + :param expires_at: Date and time at which the client session should expire in `ISO 8601 `_ format. If the client session already exists, this will update the expiration before returning it. + + :param user_identifier_key: Your user ID for the user that you want to associate with the client session (or that is already associated with the existing client session). + + :param user_identity_id: ID of the `user identity `_ that you want to associate with the client session (or that are already associated with the existing client session). + + :param user_identity_ids: Deprecated: Use ``user_identity_id``. IDs of the `user identities `_ that you want to associate with the client session. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def grant_access( + self, + *, + client_session_id: Optional[str] = None, + connect_webview_ids: Optional[List[str]] = None, + connected_account_ids: Optional[List[str]] = None, + user_identifier_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + user_identity_ids: Optional[List[str]] = None, + ) -> None: + """Grants a `client session `_ access to one or more resources, such as `Connect Webviews `_, `user identities `_, and so on. + + :param client_session_id: ID of the client session to which you want to grant access to resources. + + :param connect_webview_ids: IDs of the `Connect Webviews `_ that you want to associate with the client session. + + :param connected_account_ids: IDs of the `connected accounts `_ that you want to associate with the client session. + + :param user_identifier_key: Your user ID for the user that you want to associate with the client session. + + :param user_identity_id: ID of the `user identity `_ that you want to associate with the client session. + + :param user_identity_ids: Deprecated: Use ``user_identity_id``. IDs of the `user identities `_ that you want to associate with the client session. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + client_session_id: Optional[str] = None, + connect_webview_id: Optional[str] = None, + user_identifier_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + without_user_identifier_key: Optional[bool] = None, + ) -> List[ClientSession]: + """Returns a list of all `client sessions `_. + + :param client_session_id: ID of the client session that you want to retrieve. + + :param connect_webview_id: ID of the `Connect Webview `_ for which you want to retrieve client sessions. + + :param user_identifier_key: Your user ID for the user by which you want to filter client sessions. + + :param user_identity_id: ID of the `user identity `_ for which you want to retrieve client sessions. + + :param without_user_identifier_key: Indicates whether to retrieve only client sessions without associated user identifier keys. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def revoke(self, *, client_session_id: str) -> None: + """Revokes a `client session `_. + + Note that `deleting a client session `_ is a separate action. + + :param client_session_id: ID of the client session that you want to revoke. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class ClientSessions(AbstractClientSessions): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -448,3 +602,296 @@ def revoke(self, *, client_session_id: str) -> None: self.client.post("/client_sessions/revoke", json=json_payload) return None + + +class AsyncClientSessions(AbstractAsyncClientSessions): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/client_sessions/create", + has_required_parameters=False, + has_pagination=False, + ) + async def create( + self, + *, + connect_webview_ids: Optional[List[str]] = None, + connected_account_ids: Optional[List[str]] = None, + customer_id: Optional[str] = None, + customer_key: Optional[str] = None, + expires_at: Optional[str] = None, + user_identifier_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + user_identity_ids: Optional[List[str]] = None, + ) -> ClientSession: + """Creates a new `client session `_. + + :param connect_webview_ids: IDs of the `Connect Webviews `_ for which you want to create a client session. + + :param connected_account_ids: IDs of the `connected accounts `_ for which you want to create a client session. + + :param customer_id: Customer ID that you want to associate with the new client session. + + :param customer_key: Customer key that you want to associate with the new client session. + + :param expires_at: Date and time at which the client session should expire, in `ISO 8601 `_ format. + + :param user_identifier_key: Your user ID for the user for whom you want to create a client session. + + :param user_identity_id: ID of the `user identity `_ for which you want to create a client session. + + :param user_identity_ids: Deprecated: Use ``user_identity_id`` instead. IDs of the `user identities `_ that you want to associate with the client session. + + :returns: OK""" + json_payload: Dict[str, Any] = {} + + if connect_webview_ids is not None: + json_payload["connect_webview_ids"] = connect_webview_ids + if connected_account_ids is not None: + json_payload["connected_account_ids"] = connected_account_ids + if customer_id is not None: + json_payload["customer_id"] = customer_id + if customer_key is not None: + json_payload["customer_key"] = customer_key + if expires_at is not None: + json_payload["expires_at"] = expires_at + if user_identifier_key is not None: + json_payload["user_identifier_key"] = user_identifier_key + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if user_identity_ids is not None: + json_payload["user_identity_ids"] = user_identity_ids + + res = await self.client.put("/client_sessions/create", json=json_payload) + + return ClientSession.from_dict(res["client_session"]) + + @route_metadata( + path="/client_sessions/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, client_session_id: str) -> None: + """Deletes a `client session `_. + + :param client_session_id: ID of the client session that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if client_session_id is not None: + params["client_session_id"] = client_session_id + + if not params: + raise ValueError( + "At least one parameter is required for /client_sessions/delete" + ) + + await self.client.delete("/client_sessions/delete", params=params) + + return None + + @route_metadata( + path="/client_sessions/get", has_required_parameters=False, has_pagination=False + ) + async def get( + self, + *, + client_session_id: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> ClientSession: + """Returns a specified `client session `_. + + :param client_session_id: ID of the client session that you want to get. + + :param user_identifier_key: User identifier key associated with the client session that you want to get. + + :returns: OK""" + params: Dict[str, Any] = {} + + if client_session_id is not None: + params["client_session_id"] = client_session_id + if user_identifier_key is not None: + params["user_identifier_key"] = user_identifier_key + + res = await self.client.get("/client_sessions/get", params=params) + + return ClientSession.from_dict(res["client_session"]) + + @route_metadata( + path="/client_sessions/get_or_create", + has_required_parameters=False, + has_pagination=False, + ) + async def get_or_create( + self, + *, + connect_webview_ids: Optional[List[str]] = None, + connected_account_ids: Optional[List[str]] = None, + expires_at: Optional[str] = None, + user_identifier_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + user_identity_ids: Optional[List[str]] = None, + ) -> ClientSession: + """Returns a `client session `_ with specific characteristics or creates a new client session with these characteristics if it does not yet exist. + + :param connect_webview_ids: IDs of the `Connect Webviews `_ that you want to associate with the client session (or that are already associated with the existing client session). + + :param connected_account_ids: IDs of the `connected accounts `_ that you want to associate with the client session (or that are already associated with the existing client session). + + :param expires_at: Date and time at which the client session should expire in `ISO 8601 `_ format. If the client session already exists, this will update the expiration before returning it. + + :param user_identifier_key: Your user ID for the user that you want to associate with the client session (or that is already associated with the existing client session). + + :param user_identity_id: ID of the `user identity `_ that you want to associate with the client session (or that are already associated with the existing client session). + + :param user_identity_ids: Deprecated: Use ``user_identity_id``. IDs of the `user identities `_ that you want to associate with the client session. + + :returns: OK""" + json_payload: Dict[str, Any] = {} + + if connect_webview_ids is not None: + json_payload["connect_webview_ids"] = connect_webview_ids + if connected_account_ids is not None: + json_payload["connected_account_ids"] = connected_account_ids + if expires_at is not None: + json_payload["expires_at"] = expires_at + if user_identifier_key is not None: + json_payload["user_identifier_key"] = user_identifier_key + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if user_identity_ids is not None: + json_payload["user_identity_ids"] = user_identity_ids + + res = await self.client.post( + "/client_sessions/get_or_create", json=json_payload + ) + + return ClientSession.from_dict(res["client_session"]) + + @route_metadata( + path="/client_sessions/grant_access", + has_required_parameters=True, + has_pagination=False, + ) + async def grant_access( + self, + *, + client_session_id: Optional[str] = None, + connect_webview_ids: Optional[List[str]] = None, + connected_account_ids: Optional[List[str]] = None, + user_identifier_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + user_identity_ids: Optional[List[str]] = None, + ) -> None: + """Grants a `client session `_ access to one or more resources, such as `Connect Webviews `_, `user identities `_, and so on. + + :param client_session_id: ID of the client session to which you want to grant access to resources. + + :param connect_webview_ids: IDs of the `Connect Webviews `_ that you want to associate with the client session. + + :param connected_account_ids: IDs of the `connected accounts `_ that you want to associate with the client session. + + :param user_identifier_key: Your user ID for the user that you want to associate with the client session. + + :param user_identity_id: ID of the `user identity `_ that you want to associate with the client session. + + :param user_identity_ids: Deprecated: Use ``user_identity_id``. IDs of the `user identities `_ that you want to associate with the client session. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if client_session_id is not None: + json_payload["client_session_id"] = client_session_id + if connect_webview_ids is not None: + json_payload["connect_webview_ids"] = connect_webview_ids + if connected_account_ids is not None: + json_payload["connected_account_ids"] = connected_account_ids + if user_identifier_key is not None: + json_payload["user_identifier_key"] = user_identifier_key + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if user_identity_ids is not None: + json_payload["user_identity_ids"] = user_identity_ids + + if not json_payload: + raise ValueError( + "At least one parameter is required for /client_sessions/grant_access" + ) + + await self.client.patch("/client_sessions/grant_access", json=json_payload) + + return None + + @route_metadata( + path="/client_sessions/list", + has_required_parameters=False, + has_pagination=False, + ) + async def list( + self, + *, + client_session_id: Optional[str] = None, + connect_webview_id: Optional[str] = None, + user_identifier_key: Optional[str] = None, + user_identity_id: Optional[str] = None, + without_user_identifier_key: Optional[bool] = None, + ) -> List[ClientSession]: + """Returns a list of all `client sessions `_. + + :param client_session_id: ID of the client session that you want to retrieve. + + :param connect_webview_id: ID of the `Connect Webview `_ for which you want to retrieve client sessions. + + :param user_identifier_key: Your user ID for the user by which you want to filter client sessions. + + :param user_identity_id: ID of the `user identity `_ for which you want to retrieve client sessions. + + :param without_user_identifier_key: Indicates whether to retrieve only client sessions without associated user identifier keys. + + :returns: OK""" + params: Dict[str, Any] = {} + + if client_session_id is not None: + params["client_session_id"] = client_session_id + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + if user_identifier_key is not None: + params["user_identifier_key"] = user_identifier_key + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + if without_user_identifier_key is not None: + params["without_user_identifier_key"] = without_user_identifier_key + + res = await self.client.get("/client_sessions/list", params=params) + + return [ClientSession.from_dict(item) for item in res["client_sessions"]] + + @route_metadata( + path="/client_sessions/revoke", + has_required_parameters=True, + has_pagination=False, + ) + async def revoke(self, *, client_session_id: str) -> None: + """Revokes a `client session `_. + + Note that `deleting a client session `_ is a separate action. + + :param client_session_id: ID of the client session that you want to revoke. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if client_session_id is not None: + json_payload["client_session_id"] = client_session_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /client_sessions/revoke" + ) + + await self.client.post("/client_sessions/revoke", json=json_payload) + + return None diff --git a/seam/routes/connect_webviews.py b/seam/routes/connect_webviews.py index a9f68083..c9a95b70 100644 --- a/seam/routes/connect_webviews.py +++ b/seam/routes/connect_webviews.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import ConnectWebview @@ -107,6 +107,107 @@ def list( raise NotImplementedError() +class AbstractAsyncConnectWebviews(abc.ABC): + + @abc.abstractmethod + async def create( + self, + *, + accepted_capabilities: Optional[List[str]] = None, + accepted_providers: Optional[List[str]] = None, + automatically_manage_new_devices: Optional[bool] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, + custom_redirect_failure_url: Optional[str] = None, + custom_redirect_url: Optional[str] = None, + customer_key: Optional[str] = None, + excluded_providers: Optional[List[str]] = None, + provider_category: Optional[str] = None, + wait_for_device_creation: Optional[bool] = None, + ) -> ConnectWebview: + """Creates a new `Connect Webview `_. + + To enable a user to connect their devices or systems to Seam, they must sign in to their device or system account. To enable a user to sign in, you create a ``connect_webview``. After creating the Connect Webview, you receive a URL that you can use to display the visual component of this Connect Webview for your user. You can open an iframe or new window to display the Connect Webview. + + You should make a new ``connect_webview`` for each unique login request. Each ``connect_webview`` tracks the user that signed in with it. You receive an error if you reuse a Connect Webview for the same user twice or if you use the same Connect Webview for multiple users. + + See also: `Connect Webview Process `_. + + :param accepted_capabilities: List of accepted device capabilities that restrict the types of devices that can be connected through the Connect Webview. If not provided, defaults will be determined based on the accepted providers. + + :param accepted_providers: Accepted device provider keys as an alternative to ``provider_category``. Use this parameter to specify accepted providers explicitly. See `Customize the Brands to Display in Your Connect Webviews `_. To list all provider keys, use ```/devices/list_device_providers`` `_ with no filters. + + :param automatically_manage_new_devices: Indicates whether newly-added devices should appear as `managed devices `_. See also: `Customize the Behavior Settings of Your Connect Webviews `_. + + :param custom_metadata: Custom metadata that you want to associate with the Connect Webview. Supports up to 50 JSON key:value pairs. `Adding custom metadata to a Connect Webview `_ enables you to store custom information, like customer details or internal IDs from your application. The custom metadata is then transferred to any `connected accounts `_ that were connected using the Connect Webview, making it easy to find and filter these resources in your `workspace `_. You can also `filter Connect Webviews by custom metadata `_. + + :param custom_redirect_failure_url: Alternative URL that you want to redirect the user to on an error. If you do not set this parameter, the Connect Webview falls back to the ``custom_redirect_url``. + + :param custom_redirect_url: URL that you want to redirect the user to after the provider login is complete. + + :param customer_key: Associate the Connect Webview, the connected account, and all resources under the connected account with a customer. If the connected account already exists, it will be associated with the customer. If the connected account already exists, but is already associated with a customer, the Connect Webview will show an error. + + :param excluded_providers: List of provider keys to exclude from the Connect Webview. These providers will not be shown when the user tries to connect an account. + + :param provider_category: Specifies the category of providers that you want to include. To list all providers within a category, use ```/devices/list_device_providers`` `_ with the desired ``provider_category`` filter. + + :param wait_for_device_creation: Indicates whether Seam should finish syncing all devices in a newly-connected account before completing the associated Connect Webview. See also: `Customize the Behavior Settings of Your Connect Webviews `_. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, connect_webview_id: str) -> None: + """Deletes a `Connect Webview `_. + + You do not need to delete a Connect Webview once a user completes it. Instead, you can simply ignore completed Connect Webviews. + + :param connect_webview_id: ID of the Connect Webview that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, connect_webview_id: str) -> ConnectWebview: + """Returns a specified `Connect Webview `_. + + Unless you're using a ``custom_redirect_url``, you should poll a newly-created ``connect_webview`` to find out if the user has signed in or to get details about what devices they've connected. + + :param connect_webview_id: ID of the Connect Webview that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, + customer_key: Optional[str] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> List[ConnectWebview]: + """Returns a list of all `Connect Webviews `_. + + :param custom_metadata_has: Custom metadata pairs by which you want to `filter Connect Webviews `_. Returns Connect Webviews with ``custom_metadata`` that contains all of the provided key:value pairs. + + :param customer_key: Customer key for which you want to list connect webviews. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned Connect Webviews to include all records that satisfy a partial match using ``connect_webview_id``, ``accepted_providers``, ``custom_metadata``, or ``customer_key``. + + :param user_identifier_key: Your user ID for the user by which you want to filter Connect Webviews. + + :returns: OK""" + raise NotImplementedError() + + class ConnectWebviews(AbstractConnectWebviews): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -291,3 +392,189 @@ def list( res = self.client.get("/connect_webviews/list", params=params) return [ConnectWebview.from_dict(item) for item in res["connect_webviews"]] + + +class AsyncConnectWebviews(AbstractAsyncConnectWebviews): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/connect_webviews/create", + has_required_parameters=False, + has_pagination=False, + ) + async def create( + self, + *, + accepted_capabilities: Optional[List[str]] = None, + accepted_providers: Optional[List[str]] = None, + automatically_manage_new_devices: Optional[bool] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, + custom_redirect_failure_url: Optional[str] = None, + custom_redirect_url: Optional[str] = None, + customer_key: Optional[str] = None, + excluded_providers: Optional[List[str]] = None, + provider_category: Optional[str] = None, + wait_for_device_creation: Optional[bool] = None, + ) -> ConnectWebview: + """Creates a new `Connect Webview `_. + + To enable a user to connect their devices or systems to Seam, they must sign in to their device or system account. To enable a user to sign in, you create a ``connect_webview``. After creating the Connect Webview, you receive a URL that you can use to display the visual component of this Connect Webview for your user. You can open an iframe or new window to display the Connect Webview. + + You should make a new ``connect_webview`` for each unique login request. Each ``connect_webview`` tracks the user that signed in with it. You receive an error if you reuse a Connect Webview for the same user twice or if you use the same Connect Webview for multiple users. + + See also: `Connect Webview Process `_. + + :param accepted_capabilities: List of accepted device capabilities that restrict the types of devices that can be connected through the Connect Webview. If not provided, defaults will be determined based on the accepted providers. + + :param accepted_providers: Accepted device provider keys as an alternative to ``provider_category``. Use this parameter to specify accepted providers explicitly. See `Customize the Brands to Display in Your Connect Webviews `_. To list all provider keys, use ```/devices/list_device_providers`` `_ with no filters. + + :param automatically_manage_new_devices: Indicates whether newly-added devices should appear as `managed devices `_. See also: `Customize the Behavior Settings of Your Connect Webviews `_. + + :param custom_metadata: Custom metadata that you want to associate with the Connect Webview. Supports up to 50 JSON key:value pairs. `Adding custom metadata to a Connect Webview `_ enables you to store custom information, like customer details or internal IDs from your application. The custom metadata is then transferred to any `connected accounts `_ that were connected using the Connect Webview, making it easy to find and filter these resources in your `workspace `_. You can also `filter Connect Webviews by custom metadata `_. + + :param custom_redirect_failure_url: Alternative URL that you want to redirect the user to on an error. If you do not set this parameter, the Connect Webview falls back to the ``custom_redirect_url``. + + :param custom_redirect_url: URL that you want to redirect the user to after the provider login is complete. + + :param customer_key: Associate the Connect Webview, the connected account, and all resources under the connected account with a customer. If the connected account already exists, it will be associated with the customer. If the connected account already exists, but is already associated with a customer, the Connect Webview will show an error. + + :param excluded_providers: List of provider keys to exclude from the Connect Webview. These providers will not be shown when the user tries to connect an account. + + :param provider_category: Specifies the category of providers that you want to include. To list all providers within a category, use ```/devices/list_device_providers`` `_ with the desired ``provider_category`` filter. + + :param wait_for_device_creation: Indicates whether Seam should finish syncing all devices in a newly-connected account before completing the associated Connect Webview. See also: `Customize the Behavior Settings of Your Connect Webviews `_. + + :returns: OK""" + json_payload: Dict[str, Any] = {} + + if accepted_capabilities is not None: + json_payload["accepted_capabilities"] = accepted_capabilities + if accepted_providers is not None: + json_payload["accepted_providers"] = accepted_providers + if automatically_manage_new_devices is not None: + json_payload["automatically_manage_new_devices"] = ( + automatically_manage_new_devices + ) + if custom_metadata is not None: + json_payload["custom_metadata"] = custom_metadata + if custom_redirect_failure_url is not None: + json_payload["custom_redirect_failure_url"] = custom_redirect_failure_url + if custom_redirect_url is not None: + json_payload["custom_redirect_url"] = custom_redirect_url + if customer_key is not None: + json_payload["customer_key"] = customer_key + if excluded_providers is not None: + json_payload["excluded_providers"] = excluded_providers + if provider_category is not None: + json_payload["provider_category"] = provider_category + if wait_for_device_creation is not None: + json_payload["wait_for_device_creation"] = wait_for_device_creation + + res = await self.client.post("/connect_webviews/create", json=json_payload) + + return ConnectWebview.from_dict(res["connect_webview"]) + + @route_metadata( + path="/connect_webviews/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, connect_webview_id: str) -> None: + """Deletes a `Connect Webview `_. + + You do not need to delete a Connect Webview once a user completes it. Instead, you can simply ignore completed Connect Webviews. + + :param connect_webview_id: ID of the Connect Webview that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + + if not params: + raise ValueError( + "At least one parameter is required for /connect_webviews/delete" + ) + + await self.client.delete("/connect_webviews/delete", params=params) + + return None + + @route_metadata( + path="/connect_webviews/get", has_required_parameters=True, has_pagination=False + ) + async def get(self, *, connect_webview_id: str) -> ConnectWebview: + """Returns a specified `Connect Webview `_. + + Unless you're using a ``custom_redirect_url``, you should poll a newly-created ``connect_webview`` to find out if the user has signed in or to get details about what devices they've connected. + + :param connect_webview_id: ID of the Connect Webview that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + + if not params: + raise ValueError( + "At least one parameter is required for /connect_webviews/get" + ) + + res = await self.client.get("/connect_webviews/get", params=params) + + return ConnectWebview.from_dict(res["connect_webview"]) + + @route_metadata( + path="/connect_webviews/list", + has_required_parameters=False, + has_pagination=True, + ) + async def list( + self, + *, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, + customer_key: Optional[str] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> List[ConnectWebview]: + """Returns a list of all `Connect Webviews `_. + + :param custom_metadata_has: Custom metadata pairs by which you want to `filter Connect Webviews `_. Returns Connect Webviews with ``custom_metadata`` that contains all of the provided key:value pairs. + + :param customer_key: Customer key for which you want to list connect webviews. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned Connect Webviews to include all records that satisfy a partial match using ``connect_webview_id``, ``accepted_providers``, ``custom_metadata``, or ``customer_key``. + + :param user_identifier_key: Your user ID for the user by which you want to filter Connect Webviews. + + :returns: OK""" + params: Dict[str, Any] = {} + + if custom_metadata_has is not None: + params["custom_metadata_has"] = custom_metadata_has + if customer_key is not None: + params["customer_key"] = customer_key + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if user_identifier_key is not None: + params["user_identifier_key"] = user_identifier_key + + res = await self.client.get("/connect_webviews/list", params=params) + + return [ConnectWebview.from_dict(item) for item in res["connect_webviews"]] diff --git a/seam/routes/connected_accounts.py b/seam/routes/connected_accounts.py index 268a6cef..bfd15fe8 100644 --- a/seam/routes/connected_accounts.py +++ b/seam/routes/connected_accounts.py @@ -1,12 +1,14 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import ConnectedAccount from .connected_accounts_simulate import ( AbstractConnectedAccountsSimulate, ConnectedAccountsSimulate, + AbstractAsyncConnectedAccountsSimulate, + AsyncConnectedAccountsSimulate, ) @@ -114,6 +116,110 @@ def update( raise NotImplementedError() +class AbstractAsyncConnectedAccounts(abc.ABC): + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAsyncConnectedAccountsSimulate: + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, connected_account_id: str) -> None: + """Deletes a specified `connected account `_. + + Deleting a connected account triggers a ``connected_account.deleted`` event and removes the connected account and all data associated with the connected account from Seam, including devices, events, access codes, and so on. For every deleted resource, Seam sends a corresponding deleted event, but the resource is not deleted from the provider. + + For example, if you delete a connected account with a device that has an access code, Seam sends a ``connected_account.deleted`` event, a ``device.deleted`` event, and an ``access_code.deleted`` event, but Seam does not remove the access code from the device. + + :param connected_account_id: ID of the connected account that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, *, connected_account_id: Optional[str] = None, email: Optional[str] = None + ) -> ConnectedAccount: + """Returns a specified `connected account `_. + + :param connected_account_id: ID of the connected account that you want to get. + + :param email: Email address associated with the connected account that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, + customer_key: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + space_id: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> List[ConnectedAccount]: + """Returns a list of all `connected accounts `_. + + :param custom_metadata_has: Custom metadata pairs by which you want to filter connected accounts. Returns connected accounts with ``custom_metadata`` that contains all of the provided key:value pairs. + + :param customer_key: Customer key by which you want to filter connected accounts. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned connected accounts to include all records that satisfy a partial match using ``connected_account_id``, ``account_type``, ``customer_key``, ``custom_metadata``, ``user_identifier.username``, ``user_identifier.email`` or ``user_identifier.phone``. + + :param space_id: ID of the space by which you want to filter connected accounts. + + :param user_identifier_key: Your user ID for the user by which you want to filter connected accounts. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def sync(self, *, connected_account_id: str) -> None: + """Request a `connected account `_ sync attempt for the specified ``connected_account_id``. + + :param connected_account_id: ID of the connected account that you want to sync. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + connected_account_id: str, + accepted_capabilities: Optional[List[str]] = None, + automatically_manage_new_devices: Optional[bool] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, + customer_key: Optional[str] = None, + display_name: Optional[str] = None, + ) -> None: + """Updates a `connected account `_. + + :param connected_account_id: ID of the connected account that you want to update. + + :param accepted_capabilities: List of accepted device capabilities that restrict the types of devices that can be connected through this connected account. Valid values are ``lock``, ``thermostat``, ``noise_sensor``, and ``access_control``. + + :param automatically_manage_new_devices: Indicates whether newly-added devices should appear as `managed devices `_. + + :param custom_metadata: Custom metadata that you want to associate with the connected account. Entirely replaces the existing custom metadata object. If a new Connect Webview contains custom metadata and is used to reconnect a connected account, the custom metadata from the Connect Webview will entirely replace the entire custom metadata object on the connected account. Supports up to 50 JSON key:value pairs. `Adding custom metadata to a connected account `_ enables you to store custom information, like customer details or internal IDs from your application. Then, you can `filter connected accounts by the desired metadata `_. + + :param customer_key: The customer key to associate with this connected account. If provided, the connected account and all resources under the connected account will be moved to this customer. May only be provided if the connected account is not already associated with a customer. + + :param display_name: Human-readable name for the connected account, shown in the dashboard. For example, ``Booking from Airbnb House 1``. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class ConnectedAccounts(AbstractConnectedAccounts): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -320,3 +426,213 @@ def update( self.client.patch("/connected_accounts/update", json=json_payload) return None + + +class AsyncConnectedAccounts(AbstractAsyncConnectedAccounts): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._simulate = AsyncConnectedAccountsSimulate( + client=client, defaults=defaults + ) + + @property + def simulate(self) -> AsyncConnectedAccountsSimulate: + return self._simulate + + @route_metadata( + path="/connected_accounts/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, connected_account_id: str) -> None: + """Deletes a specified `connected account `_. + + Deleting a connected account triggers a ``connected_account.deleted`` event and removes the connected account and all data associated with the connected account from Seam, including devices, events, access codes, and so on. For every deleted resource, Seam sends a corresponding deleted event, but the resource is not deleted from the provider. + + For example, if you delete a connected account with a device that has an access code, Seam sends a ``connected_account.deleted`` event, a ``device.deleted`` event, and an ``access_code.deleted`` event, but Seam does not remove the access code from the device. + + :param connected_account_id: ID of the connected account that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + + if not params: + raise ValueError( + "At least one parameter is required for /connected_accounts/delete" + ) + + await self.client.delete("/connected_accounts/delete", params=params) + + return None + + @route_metadata( + path="/connected_accounts/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get( + self, *, connected_account_id: Optional[str] = None, email: Optional[str] = None + ) -> ConnectedAccount: + """Returns a specified `connected account `_. + + :param connected_account_id: ID of the connected account that you want to get. + + :param email: Email address associated with the connected account that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if email is not None: + params["email"] = email + + if not params: + raise ValueError( + "At least one parameter is required for /connected_accounts/get" + ) + + res = await self.client.get("/connected_accounts/get", params=params) + + return ConnectedAccount.from_dict(res["connected_account"]) + + @route_metadata( + path="/connected_accounts/list", + has_required_parameters=False, + has_pagination=True, + ) + async def list( + self, + *, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, + customer_key: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + space_id: Optional[str] = None, + user_identifier_key: Optional[str] = None, + ) -> List[ConnectedAccount]: + """Returns a list of all `connected accounts `_. + + :param custom_metadata_has: Custom metadata pairs by which you want to filter connected accounts. Returns connected accounts with ``custom_metadata`` that contains all of the provided key:value pairs. + + :param customer_key: Customer key by which you want to filter connected accounts. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned connected accounts to include all records that satisfy a partial match using ``connected_account_id``, ``account_type``, ``customer_key``, ``custom_metadata``, ``user_identifier.username``, ``user_identifier.email`` or ``user_identifier.phone``. + + :param space_id: ID of the space by which you want to filter connected accounts. + + :param user_identifier_key: Your user ID for the user by which you want to filter connected accounts. + + :returns: OK""" + params: Dict[str, Any] = {} + + if custom_metadata_has is not None: + params["custom_metadata_has"] = custom_metadata_has + if customer_key is not None: + params["customer_key"] = customer_key + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if space_id is not None: + params["space_id"] = space_id + if user_identifier_key is not None: + params["user_identifier_key"] = user_identifier_key + + res = await self.client.get("/connected_accounts/list", params=params) + + return [ConnectedAccount.from_dict(item) for item in res["connected_accounts"]] + + @route_metadata( + path="/connected_accounts/sync", + has_required_parameters=True, + has_pagination=False, + ) + async def sync(self, *, connected_account_id: str) -> None: + """Request a `connected account `_ sync attempt for the specified ``connected_account_id``. + + :param connected_account_id: ID of the connected account that you want to sync. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if connected_account_id is not None: + json_payload["connected_account_id"] = connected_account_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /connected_accounts/sync" + ) + + await self.client.post("/connected_accounts/sync", json=json_payload) + + return None + + @route_metadata( + path="/connected_accounts/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + connected_account_id: str, + accepted_capabilities: Optional[List[str]] = None, + automatically_manage_new_devices: Optional[bool] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, + customer_key: Optional[str] = None, + display_name: Optional[str] = None, + ) -> None: + """Updates a `connected account `_. + + :param connected_account_id: ID of the connected account that you want to update. + + :param accepted_capabilities: List of accepted device capabilities that restrict the types of devices that can be connected through this connected account. Valid values are ``lock``, ``thermostat``, ``noise_sensor``, and ``access_control``. + + :param automatically_manage_new_devices: Indicates whether newly-added devices should appear as `managed devices `_. + + :param custom_metadata: Custom metadata that you want to associate with the connected account. Entirely replaces the existing custom metadata object. If a new Connect Webview contains custom metadata and is used to reconnect a connected account, the custom metadata from the Connect Webview will entirely replace the entire custom metadata object on the connected account. Supports up to 50 JSON key:value pairs. `Adding custom metadata to a connected account `_ enables you to store custom information, like customer details or internal IDs from your application. Then, you can `filter connected accounts by the desired metadata `_. + + :param customer_key: The customer key to associate with this connected account. If provided, the connected account and all resources under the connected account will be moved to this customer. May only be provided if the connected account is not already associated with a customer. + + :param display_name: Human-readable name for the connected account, shown in the dashboard. For example, ``Booking from Airbnb House 1``. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if connected_account_id is not None: + json_payload["connected_account_id"] = connected_account_id + if accepted_capabilities is not None: + json_payload["accepted_capabilities"] = accepted_capabilities + if automatically_manage_new_devices is not None: + json_payload["automatically_manage_new_devices"] = ( + automatically_manage_new_devices + ) + if custom_metadata is not None: + json_payload["custom_metadata"] = custom_metadata + if customer_key is not None: + json_payload["customer_key"] = customer_key + if display_name is not None: + json_payload["display_name"] = display_name + + if not json_payload: + raise ValueError( + "At least one parameter is required for /connected_accounts/update" + ) + + await self.client.patch("/connected_accounts/update", json=json_payload) + + return None diff --git a/seam/routes/connected_accounts_simulate.py b/seam/routes/connected_accounts_simulate.py index 3837200f..e846d971 100644 --- a/seam/routes/connected_accounts_simulate.py +++ b/seam/routes/connected_accounts_simulate.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata @@ -16,6 +16,18 @@ def disconnect(self, *, connected_account_id: str) -> None: raise NotImplementedError() +class AbstractAsyncConnectedAccountsSimulate(abc.ABC): + + @abc.abstractmethod + async def disconnect(self, *, connected_account_id: str) -> None: + """Simulates a connected account becoming disconnected from Seam. Only applicable for `sandbox workspaces `_. + + :param connected_account_id: ID of the connected account you want to simulate as disconnected. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class ConnectedAccountsSimulate(AbstractConnectedAccountsSimulate): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -45,3 +57,36 @@ def disconnect(self, *, connected_account_id: str) -> None: self.client.post("/connected_accounts/simulate/disconnect", json=json_payload) return None + + +class AsyncConnectedAccountsSimulate(AbstractAsyncConnectedAccountsSimulate): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/connected_accounts/simulate/disconnect", + has_required_parameters=True, + has_pagination=False, + ) + async def disconnect(self, *, connected_account_id: str) -> None: + """Simulates a connected account becoming disconnected from Seam. Only applicable for `sandbox workspaces `_. + + :param connected_account_id: ID of the connected account you want to simulate as disconnected. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if connected_account_id is not None: + json_payload["connected_account_id"] = connected_account_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /connected_accounts/simulate/disconnect" + ) + + await self.client.post( + "/connected_accounts/simulate/disconnect", json=json_payload + ) + + return None diff --git a/seam/routes/customers.py b/seam/routes/customers.py index 79bf9210..b848ebcb 100644 --- a/seam/routes/customers.py +++ b/seam/routes/customers.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import CustomerPortal @@ -187,6 +187,188 @@ def push_data( raise NotImplementedError() +class AbstractAsyncCustomers(abc.ABC): + + @abc.abstractmethod + async def create_portal( + self, + *, + customer_resources_filters: Optional[List[Dict[str, Any]]] = None, + customization_profile_id: Optional[str] = None, + deep_link: Optional[Dict[str, Any]] = None, + exclude_locale_picker: Optional[bool] = None, + features: Optional[Dict[str, Any]] = None, + is_embedded: Optional[bool] = None, + landing_page: Optional[Dict[str, Any]] = None, + locale: Optional[str] = None, + navigation_mode: Optional[str] = None, + read_only: Optional[bool] = None, + customer_data: Optional[Dict[str, Any]] = None, + ) -> CustomerPortal: + """Creates a new customer portal magic link with configurable features. + + :param customer_resources_filters: Filter configuration for resources based on their custom_metadata. Each filter specifies a field, operation, and value to match against resource custom_metadata. + + :param customization_profile_id: The ID of the customization profile to use for the portal. + + :param deep_link: Deep link target resource for initial redirect. When set, the portal will navigate directly to the specified resource. + + :param exclude_locale_picker: Whether to exclude the option to select a locale within the portal UI. + + :param features: + + :param is_embedded: Whether the portal is embedded in another application. + + :param landing_page: Configuration for the landing page when the portal loads. + + :param locale: The locale to use for the portal. + + :param navigation_mode: Navigation mode for the portal. 'restricted' tells frontend to hide navigation UI, typically used for embedded deep links. + + :param read_only: Whether the portal is read-only. When true, the customer can browse the portal but cannot perform any mutating action; write requests made with the portal's client session are rejected. + + :param customer_data: + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete_data( + self, + *, + access_grant_keys: Optional[List[str]] = None, + booking_keys: Optional[List[str]] = None, + building_keys: Optional[List[str]] = None, + common_area_keys: Optional[List[str]] = None, + customer_keys: Optional[List[str]] = None, + facility_keys: Optional[List[str]] = None, + guest_keys: Optional[List[str]] = None, + listing_keys: Optional[List[str]] = None, + property_keys: Optional[List[str]] = None, + property_listing_keys: Optional[List[str]] = None, + reservation_keys: Optional[List[str]] = None, + resident_keys: Optional[List[str]] = None, + room_keys: Optional[List[str]] = None, + space_keys: Optional[List[str]] = None, + staff_member_keys: Optional[List[str]] = None, + tenant_keys: Optional[List[str]] = None, + unit_keys: Optional[List[str]] = None, + user_identity_keys: Optional[List[str]] = None, + user_keys: Optional[List[str]] = None, + ) -> None: + """Deletes customer data including resources like spaces, properties, rooms, users, etc. + This will delete the partner resources and any related Seam resources (user identities, access grants, spaces). + + :param access_grant_keys: List of access grant keys to delete. + + :param booking_keys: List of booking keys to delete. + + :param building_keys: List of building keys to delete. + + :param common_area_keys: List of common area keys to delete. + + :param customer_keys: List of customer keys to delete all data for. + + :param facility_keys: List of facility keys to delete. + + :param guest_keys: List of guest keys to delete. + + :param listing_keys: List of listing keys to delete. + + :param property_keys: List of property keys to delete. + + :param property_listing_keys: List of property listing keys to delete. + + :param reservation_keys: List of reservation keys to delete. + + :param resident_keys: List of resident keys to delete. + + :param room_keys: List of room keys to delete. + + :param space_keys: List of space keys to delete. + + :param staff_member_keys: List of staff member keys to delete. + + :param tenant_keys: List of tenant keys to delete. + + :param unit_keys: List of unit keys to delete. + + :param user_identity_keys: List of user identity keys to delete. + + :param user_keys: List of user keys to delete.""" + raise NotImplementedError() + + @abc.abstractmethod + async def push_data( + self, + *, + customer_key: str, + access_grants: Optional[List[Dict[str, Any]]] = None, + bookings: Optional[List[Dict[str, Any]]] = None, + buildings: Optional[List[Dict[str, Any]]] = None, + common_areas: Optional[List[Dict[str, Any]]] = None, + facilities: Optional[List[Dict[str, Any]]] = None, + guests: Optional[List[Dict[str, Any]]] = None, + listings: Optional[List[Dict[str, Any]]] = None, + properties: Optional[List[Dict[str, Any]]] = None, + property_listings: Optional[List[Dict[str, Any]]] = None, + reservations: Optional[List[Dict[str, Any]]] = None, + residents: Optional[List[Dict[str, Any]]] = None, + rooms: Optional[List[Dict[str, Any]]] = None, + sites: Optional[List[Dict[str, Any]]] = None, + spaces: Optional[List[Dict[str, Any]]] = None, + staff_members: Optional[List[Dict[str, Any]]] = None, + tenants: Optional[List[Dict[str, Any]]] = None, + units: Optional[List[Dict[str, Any]]] = None, + user_identities: Optional[List[Dict[str, Any]]] = None, + users: Optional[List[Dict[str, Any]]] = None, + ) -> None: + """Pushes customer data including resources like spaces, properties, rooms, users, etc. + + :param customer_key: Your unique identifier for the customer. + + :param access_grants: List of access grants. + + :param bookings: List of bookings. + + :param buildings: List of buildings. + + :param common_areas: List of shared common areas. + + :param facilities: List of gym or fitness facilities. + + :param guests: List of guests. + + :param listings: List of property listings. + + :param properties: List of short-term rental properties. + + :param property_listings: List of property listings. + + :param reservations: List of reservations. + + :param residents: List of residents. + + :param rooms: List of hotel or hospitality rooms. + + :param sites: List of general sites or areas. + + :param spaces: List of general spaces or areas. + + :param staff_members: List of staff members. + + :param tenants: List of tenants. + + :param units: List of multi-family residential units. + + :param user_identities: List of user identities. + + :param users: List of users. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class Customers(AbstractCustomers): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -500,3 +682,318 @@ def push_data( self.client.post("/customers/push_data", json=json_payload) return None + + +class AsyncCustomers(AbstractAsyncCustomers): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/customers/create_portal", + has_required_parameters=False, + has_pagination=False, + ) + async def create_portal( + self, + *, + customer_resources_filters: Optional[List[Dict[str, Any]]] = None, + customization_profile_id: Optional[str] = None, + deep_link: Optional[Dict[str, Any]] = None, + exclude_locale_picker: Optional[bool] = None, + features: Optional[Dict[str, Any]] = None, + is_embedded: Optional[bool] = None, + landing_page: Optional[Dict[str, Any]] = None, + locale: Optional[str] = None, + navigation_mode: Optional[str] = None, + read_only: Optional[bool] = None, + customer_data: Optional[Dict[str, Any]] = None, + ) -> CustomerPortal: + """Creates a new customer portal magic link with configurable features. + + :param customer_resources_filters: Filter configuration for resources based on their custom_metadata. Each filter specifies a field, operation, and value to match against resource custom_metadata. + + :param customization_profile_id: The ID of the customization profile to use for the portal. + + :param deep_link: Deep link target resource for initial redirect. When set, the portal will navigate directly to the specified resource. + + :param exclude_locale_picker: Whether to exclude the option to select a locale within the portal UI. + + :param features: + + :param is_embedded: Whether the portal is embedded in another application. + + :param landing_page: Configuration for the landing page when the portal loads. + + :param locale: The locale to use for the portal. + + :param navigation_mode: Navigation mode for the portal. 'restricted' tells frontend to hide navigation UI, typically used for embedded deep links. + + :param read_only: Whether the portal is read-only. When true, the customer can browse the portal but cannot perform any mutating action; write requests made with the portal's client session are rejected. + + :param customer_data: + + :returns: OK""" + json_payload: Dict[str, Any] = {} + + if customer_resources_filters is not None: + json_payload["customer_resources_filters"] = customer_resources_filters + if customization_profile_id is not None: + json_payload["customization_profile_id"] = customization_profile_id + if deep_link is not None: + json_payload["deep_link"] = deep_link + if exclude_locale_picker is not None: + json_payload["exclude_locale_picker"] = exclude_locale_picker + if features is not None: + json_payload["features"] = features + if is_embedded is not None: + json_payload["is_embedded"] = is_embedded + if landing_page is not None: + json_payload["landing_page"] = landing_page + if locale is not None: + json_payload["locale"] = locale + if navigation_mode is not None: + json_payload["navigation_mode"] = navigation_mode + if read_only is not None: + json_payload["read_only"] = read_only + if customer_data is not None: + json_payload["customer_data"] = customer_data + + res = await self.client.post("/customers/create_portal", json=json_payload) + + return CustomerPortal.from_dict(res["customer_portal"]) + + @route_metadata( + path="/customers/delete_data", + has_required_parameters=False, + has_pagination=False, + ) + async def delete_data( + self, + *, + access_grant_keys: Optional[List[str]] = None, + booking_keys: Optional[List[str]] = None, + building_keys: Optional[List[str]] = None, + common_area_keys: Optional[List[str]] = None, + customer_keys: Optional[List[str]] = None, + facility_keys: Optional[List[str]] = None, + guest_keys: Optional[List[str]] = None, + listing_keys: Optional[List[str]] = None, + property_keys: Optional[List[str]] = None, + property_listing_keys: Optional[List[str]] = None, + reservation_keys: Optional[List[str]] = None, + resident_keys: Optional[List[str]] = None, + room_keys: Optional[List[str]] = None, + space_keys: Optional[List[str]] = None, + staff_member_keys: Optional[List[str]] = None, + tenant_keys: Optional[List[str]] = None, + unit_keys: Optional[List[str]] = None, + user_identity_keys: Optional[List[str]] = None, + user_keys: Optional[List[str]] = None, + ) -> None: + """Deletes customer data including resources like spaces, properties, rooms, users, etc. + This will delete the partner resources and any related Seam resources (user identities, access grants, spaces). + + :param access_grant_keys: List of access grant keys to delete. + + :param booking_keys: List of booking keys to delete. + + :param building_keys: List of building keys to delete. + + :param common_area_keys: List of common area keys to delete. + + :param customer_keys: List of customer keys to delete all data for. + + :param facility_keys: List of facility keys to delete. + + :param guest_keys: List of guest keys to delete. + + :param listing_keys: List of listing keys to delete. + + :param property_keys: List of property keys to delete. + + :param property_listing_keys: List of property listing keys to delete. + + :param reservation_keys: List of reservation keys to delete. + + :param resident_keys: List of resident keys to delete. + + :param room_keys: List of room keys to delete. + + :param space_keys: List of space keys to delete. + + :param staff_member_keys: List of staff member keys to delete. + + :param tenant_keys: List of tenant keys to delete. + + :param unit_keys: List of unit keys to delete. + + :param user_identity_keys: List of user identity keys to delete. + + :param user_keys: List of user keys to delete.""" + params: Dict[str, Any] = {} + + if access_grant_keys is not None: + params["access_grant_keys"] = access_grant_keys + if booking_keys is not None: + params["booking_keys"] = booking_keys + if building_keys is not None: + params["building_keys"] = building_keys + if common_area_keys is not None: + params["common_area_keys"] = common_area_keys + if customer_keys is not None: + params["customer_keys"] = customer_keys + if facility_keys is not None: + params["facility_keys"] = facility_keys + if guest_keys is not None: + params["guest_keys"] = guest_keys + if listing_keys is not None: + params["listing_keys"] = listing_keys + if property_keys is not None: + params["property_keys"] = property_keys + if property_listing_keys is not None: + params["property_listing_keys"] = property_listing_keys + if reservation_keys is not None: + params["reservation_keys"] = reservation_keys + if resident_keys is not None: + params["resident_keys"] = resident_keys + if room_keys is not None: + params["room_keys"] = room_keys + if space_keys is not None: + params["space_keys"] = space_keys + if staff_member_keys is not None: + params["staff_member_keys"] = staff_member_keys + if tenant_keys is not None: + params["tenant_keys"] = tenant_keys + if unit_keys is not None: + params["unit_keys"] = unit_keys + if user_identity_keys is not None: + params["user_identity_keys"] = user_identity_keys + if user_keys is not None: + params["user_keys"] = user_keys + + await self.client.delete("/customers/delete_data", params=params) + + return None + + @route_metadata( + path="/customers/push_data", has_required_parameters=True, has_pagination=False + ) + async def push_data( + self, + *, + customer_key: str, + access_grants: Optional[List[Dict[str, Any]]] = None, + bookings: Optional[List[Dict[str, Any]]] = None, + buildings: Optional[List[Dict[str, Any]]] = None, + common_areas: Optional[List[Dict[str, Any]]] = None, + facilities: Optional[List[Dict[str, Any]]] = None, + guests: Optional[List[Dict[str, Any]]] = None, + listings: Optional[List[Dict[str, Any]]] = None, + properties: Optional[List[Dict[str, Any]]] = None, + property_listings: Optional[List[Dict[str, Any]]] = None, + reservations: Optional[List[Dict[str, Any]]] = None, + residents: Optional[List[Dict[str, Any]]] = None, + rooms: Optional[List[Dict[str, Any]]] = None, + sites: Optional[List[Dict[str, Any]]] = None, + spaces: Optional[List[Dict[str, Any]]] = None, + staff_members: Optional[List[Dict[str, Any]]] = None, + tenants: Optional[List[Dict[str, Any]]] = None, + units: Optional[List[Dict[str, Any]]] = None, + user_identities: Optional[List[Dict[str, Any]]] = None, + users: Optional[List[Dict[str, Any]]] = None, + ) -> None: + """Pushes customer data including resources like spaces, properties, rooms, users, etc. + + :param customer_key: Your unique identifier for the customer. + + :param access_grants: List of access grants. + + :param bookings: List of bookings. + + :param buildings: List of buildings. + + :param common_areas: List of shared common areas. + + :param facilities: List of gym or fitness facilities. + + :param guests: List of guests. + + :param listings: List of property listings. + + :param properties: List of short-term rental properties. + + :param property_listings: List of property listings. + + :param reservations: List of reservations. + + :param residents: List of residents. + + :param rooms: List of hotel or hospitality rooms. + + :param sites: List of general sites or areas. + + :param spaces: List of general spaces or areas. + + :param staff_members: List of staff members. + + :param tenants: List of tenants. + + :param units: List of multi-family residential units. + + :param user_identities: List of user identities. + + :param users: List of users. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if customer_key is not None: + json_payload["customer_key"] = customer_key + if access_grants is not None: + json_payload["access_grants"] = access_grants + if bookings is not None: + json_payload["bookings"] = bookings + if buildings is not None: + json_payload["buildings"] = buildings + if common_areas is not None: + json_payload["common_areas"] = common_areas + if facilities is not None: + json_payload["facilities"] = facilities + if guests is not None: + json_payload["guests"] = guests + if listings is not None: + json_payload["listings"] = listings + if properties is not None: + json_payload["properties"] = properties + if property_listings is not None: + json_payload["property_listings"] = property_listings + if reservations is not None: + json_payload["reservations"] = reservations + if residents is not None: + json_payload["residents"] = residents + if rooms is not None: + json_payload["rooms"] = rooms + if sites is not None: + json_payload["sites"] = sites + if spaces is not None: + json_payload["spaces"] = spaces + if staff_members is not None: + json_payload["staff_members"] = staff_members + if tenants is not None: + json_payload["tenants"] = tenants + if units is not None: + json_payload["units"] = units + if user_identities is not None: + json_payload["user_identities"] = user_identities + if users is not None: + json_payload["users"] = users + + if not json_payload: + raise ValueError( + "At least one parameter is required for /customers/push_data" + ) + + await self.client.post("/customers/push_data", json=json_payload) + + return None diff --git a/seam/routes/devices.py b/seam/routes/devices.py index 8a0761a7..cee08bf5 100644 --- a/seam/routes/devices.py +++ b/seam/routes/devices.py @@ -1,11 +1,21 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import Device, DeviceProvider -from .devices_simulate import AbstractDevicesSimulate, DevicesSimulate -from .devices_unmanaged import AbstractDevicesUnmanaged, DevicesUnmanaged +from .devices_simulate import ( + AbstractDevicesSimulate, + DevicesSimulate, + AbstractAsyncDevicesSimulate, + AsyncDevicesSimulate, +) +from .devices_unmanaged import ( + AbstractDevicesUnmanaged, + DevicesUnmanaged, + AbstractAsyncDevicesUnmanaged, + AsyncDevicesUnmanaged, +) class AbstractDevices(abc.ABC): @@ -150,6 +160,148 @@ def update( raise NotImplementedError() +class AbstractAsyncDevices(abc.ABC): + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAsyncDevicesSimulate: + raise NotImplementedError() + + @property + @abc.abstractmethod + def unmanaged(self) -> AbstractAsyncDevicesUnmanaged: + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, *, device_id: Optional[str] = None, name: Optional[str] = None + ) -> Device: + """Returns a specified `device `_. + + You must specify either ``device_id`` or ``name``. + + :param device_id: ID of the device that you want to get. + + :param name: Name of the device that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + connected_account_ids: Optional[List[str]] = None, + created_before: Optional[str] = None, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, + customer_key: Optional[str] = None, + device_ids: Optional[List[str]] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + limit: Optional[float] = None, + manufacturer: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + space_id: Optional[str] = None, + unstable_location_id: Optional[Union[str, Null]] = None, + user_identifier_key: Optional[str] = None, + ) -> List[Device]: + """Returns a list of all `devices `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. + + :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. + + :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. + + :param customer_key: Customer key for which you want to list devices. + + :param device_ids: Array of device IDs for which you want to list devices. + + :param device_type: Device type for which you want to list devices. + + :param device_types: Array of device types for which you want to list devices. + + :param limit: Numerical limit on the number of devices to return. + + :param manufacturer: Manufacturer for which you want to list devices. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. + + :param space_id: ID of the space for which you want to list devices. + + :param unstable_location_id: Deprecated: Use ``space_id``. + + :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_device_providers( + self, *, provider_category: Optional[str] = None + ) -> List[DeviceProvider]: + """Returns a list of all device providers. + + The information that this endpoint returns for each provider includes a set of `capability flags `_, such as ``device_provider.can_remotely_unlock``. If at least one supported device from a provider has a specific capability, the corresponding capability flag is ``true``. + + When you create a `Connect Webview `_, you can customize the providers—that is, the brands—that it displays. In the ``/connect_webviews/create`` request, include the desired set of device provider keys in the ``accepted_providers`` parameter. See also `Customize the Brands to Display in Your Connect Webviews `_. + + :param provider_category: Category for which you want to list providers. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def report_provider_metadata(self, *, devices: List[Dict[str, Any]]) -> None: + """Updates provider-specific metadata for devices. + + :param devices: Array of devices with provider metadata to update + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + device_id: str, + backup_access_code_pool_enabled: Optional[bool] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, + is_managed: Optional[bool] = None, + name: Optional[Union[str, Null]] = None, + properties: Optional[Dict[str, Any]] = None, + ) -> None: + """Updates a specified `device `_. + + You can add or change `custom metadata `_ for a device, change the device's name, or `convert a managed device to unmanaged `_. + + :param device_id: ID of the device that you want to update. + + :param backup_access_code_pool_enabled: Indicates whether the device's `backup access code pool `_ is enabled. Set to ``false`` to disable the pool: Seam stops refilling it and removes any backup codes that have not yet been pulled into active use. + + :param custom_metadata: Custom metadata that you want to associate with the device. Supports up to 50 JSON key:value pairs. `Adding custom metadata to a device `_ enables you to store custom information, like customer details or internal IDs from your application. Then, you can `filter devices by the desired metadata `_. + + :param is_managed: Indicates whether the device is managed. To unmanage a device, set ``is_managed`` to ``false``. + + :param name: Name for the device. + + :param properties: + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class Devices(AbstractDevices): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -397,3 +549,252 @@ def update( self.client.patch("/devices/update", json=json_payload) return None + + +class AsyncDevices(AbstractAsyncDevices): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._simulate = AsyncDevicesSimulate(client=client, defaults=defaults) + self._unmanaged = AsyncDevicesUnmanaged(client=client, defaults=defaults) + + @property + def simulate(self) -> AsyncDevicesSimulate: + return self._simulate + + @property + def unmanaged(self) -> AsyncDevicesUnmanaged: + return self._unmanaged + + @route_metadata( + path="/devices/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, *, device_id: Optional[str] = None, name: Optional[str] = None + ) -> Device: + """Returns a specified `device `_. + + You must specify either ``device_id`` or ``name``. + + :param device_id: ID of the device that you want to get. + + :param name: Name of the device that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + if name is not None: + params["name"] = name + + if not params: + raise ValueError("At least one parameter is required for /devices/get") + + res = await self.client.get("/devices/get", params=params) + + return Device.from_dict(res["device"]) + + @route_metadata( + path="/devices/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + connected_account_ids: Optional[List[str]] = None, + created_before: Optional[str] = None, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, + customer_key: Optional[str] = None, + device_ids: Optional[List[str]] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + limit: Optional[float] = None, + manufacturer: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + space_id: Optional[str] = None, + unstable_location_id: Optional[Union[str, Null]] = None, + user_identifier_key: Optional[str] = None, + ) -> List[Device]: + """Returns a list of all `devices `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. + + :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. + + :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. + + :param customer_key: Customer key for which you want to list devices. + + :param device_ids: Array of device IDs for which you want to list devices. + + :param device_type: Device type for which you want to list devices. + + :param device_types: Array of device types for which you want to list devices. + + :param limit: Numerical limit on the number of devices to return. + + :param manufacturer: Manufacturer for which you want to list devices. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. + + :param space_id: ID of the space for which you want to list devices. + + :param unstable_location_id: Deprecated: Use ``space_id``. + + :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. + + :returns: OK""" + params: Dict[str, Any] = {} + + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if connected_account_ids is not None: + params["connected_account_ids"] = connected_account_ids + if created_before is not None: + params["created_before"] = created_before + if custom_metadata_has is not None: + params["custom_metadata_has"] = custom_metadata_has + if customer_key is not None: + params["customer_key"] = customer_key + if device_ids is not None: + params["device_ids"] = device_ids + if device_type is not None: + params["device_type"] = device_type + if device_types is not None: + params["device_types"] = device_types + if limit is not None: + params["limit"] = limit + if manufacturer is not None: + params["manufacturer"] = manufacturer + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if space_id is not None: + params["space_id"] = space_id + if unstable_location_id is not None: + params["unstable_location_id"] = unstable_location_id + if user_identifier_key is not None: + params["user_identifier_key"] = user_identifier_key + + res = await self.client.get("/devices/list", params=params) + + return [Device.from_dict(item) for item in res["devices"]] + + @route_metadata( + path="/devices/list_device_providers", + has_required_parameters=False, + has_pagination=False, + ) + async def list_device_providers( + self, *, provider_category: Optional[str] = None + ) -> List[DeviceProvider]: + """Returns a list of all device providers. + + The information that this endpoint returns for each provider includes a set of `capability flags `_, such as ``device_provider.can_remotely_unlock``. If at least one supported device from a provider has a specific capability, the corresponding capability flag is ``true``. + + When you create a `Connect Webview `_, you can customize the providers—that is, the brands—that it displays. In the ``/connect_webviews/create`` request, include the desired set of device provider keys in the ``accepted_providers`` parameter. See also `Customize the Brands to Display in Your Connect Webviews `_. + + :param provider_category: Category for which you want to list providers. + + :returns: OK""" + params: Dict[str, Any] = {} + + if provider_category is not None: + params["provider_category"] = provider_category + + res = await self.client.get("/devices/list_device_providers", params=params) + + return [DeviceProvider.from_dict(item) for item in res["device_providers"]] + + @route_metadata( + path="/devices/report_provider_metadata", + has_required_parameters=True, + has_pagination=False, + ) + async def report_provider_metadata(self, *, devices: List[Dict[str, Any]]) -> None: + """Updates provider-specific metadata for devices. + + :param devices: Array of devices with provider metadata to update + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if devices is not None: + json_payload["devices"] = devices + + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/report_provider_metadata" + ) + + await self.client.post("/devices/report_provider_metadata", json=json_payload) + + return None + + @route_metadata( + path="/devices/update", has_required_parameters=True, has_pagination=False + ) + async def update( + self, + *, + device_id: str, + backup_access_code_pool_enabled: Optional[bool] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, + is_managed: Optional[bool] = None, + name: Optional[Union[str, Null]] = None, + properties: Optional[Dict[str, Any]] = None, + ) -> None: + """Updates a specified `device `_. + + You can add or change `custom metadata `_ for a device, change the device's name, or `convert a managed device to unmanaged `_. + + :param device_id: ID of the device that you want to update. + + :param backup_access_code_pool_enabled: Indicates whether the device's `backup access code pool `_ is enabled. Set to ``false`` to disable the pool: Seam stops refilling it and removes any backup codes that have not yet been pulled into active use. + + :param custom_metadata: Custom metadata that you want to associate with the device. Supports up to 50 JSON key:value pairs. `Adding custom metadata to a device `_ enables you to store custom information, like customer details or internal IDs from your application. Then, you can `filter devices by the desired metadata `_. + + :param is_managed: Indicates whether the device is managed. To unmanage a device, set ``is_managed`` to ``false``. + + :param name: Name for the device. + + :param properties: + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if backup_access_code_pool_enabled is not None: + json_payload["backup_access_code_pool_enabled"] = ( + backup_access_code_pool_enabled + ) + if custom_metadata is not None: + json_payload["custom_metadata"] = custom_metadata + if is_managed is not None: + json_payload["is_managed"] = is_managed + if name is not None: + json_payload["name"] = name + if properties is not None: + json_payload["properties"] = properties + + if not json_payload: + raise ValueError("At least one parameter is required for /devices/update") + + await self.client.patch("/devices/update", json=json_payload) + + return None diff --git a/seam/routes/devices_simulate.py b/seam/routes/devices_simulate.py index f76b5d83..406ea98b 100644 --- a/seam/routes/devices_simulate.py +++ b/seam/routes/devices_simulate.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata @@ -72,6 +72,74 @@ def remove(self, *, device_id: str) -> None: raise NotImplementedError() +class AbstractAsyncDevicesSimulate(abc.ABC): + + @abc.abstractmethod + async def connect(self, *, device_id: str) -> None: + """Simulates connecting a device to Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. + + :param device_id: ID of the device that you want to simulate connecting to Seam. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def connect_to_hub(self, *, device_id: str) -> None: + """Simulates bringing the Wi‑Fi hub (bridge) back online for a device. + Only applicable for sandbox workspaces and currently + implemented for August and TTLock locks. + This will clear the ``hub_disconnected`` error on the device. + + :param device_id: ID of the device whose hub you want to reconnect. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def disconnect(self, *, device_id: str) -> None: + """Simulates disconnecting a device from Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. + + :param device_id: ID of the device that you want to simulate disconnecting from Seam. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def disconnect_from_hub(self, *, device_id: str) -> None: + """Simulates taking the Wi‑Fi hub (bridge) offline for a device. + Only applicable for sandbox workspaces and currently + implemented for August, TTLock, and IglooHome devices. + This will set the ``hub_disconnected`` error on the device, or mark the + IglooHome bridge offline in sandbox. + + :param device_id: ID of the device whose hub you want to disconnect. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def paid_subscription(self, *, device_id: str, is_expired: bool) -> None: + """Toggle the simulated Nuki Smart Hosting subscription for a device (sandbox only). + Send ``is_expired: true`` to simulate an expired subscription, or ``false`` to simulate an active subscription. + The actual device error is created/cleared by the poller after this state change. + + :param device_id: + + :param is_expired: + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def remove(self, *, device_id: str) -> None: + """Simulates removing a device from Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. + + :param device_id: ID of the device that you want to simulate removing from Seam. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class DevicesSimulate(AbstractDevicesSimulate): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -239,3 +307,174 @@ def remove(self, *, device_id: str) -> None: self.client.post("/devices/simulate/remove", json=json_payload) return None + + +class AsyncDevicesSimulate(AbstractAsyncDevicesSimulate): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/devices/simulate/connect", + has_required_parameters=True, + has_pagination=False, + ) + async def connect(self, *, device_id: str) -> None: + """Simulates connecting a device to Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. + + :param device_id: ID of the device that you want to simulate connecting to Seam. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/connect" + ) + + await self.client.post("/devices/simulate/connect", json=json_payload) + + return None + + @route_metadata( + path="/devices/simulate/connect_to_hub", + has_required_parameters=True, + has_pagination=False, + ) + async def connect_to_hub(self, *, device_id: str) -> None: + """Simulates bringing the Wi‑Fi hub (bridge) back online for a device. + Only applicable for sandbox workspaces and currently + implemented for August and TTLock locks. + This will clear the ``hub_disconnected`` error on the device. + + :param device_id: ID of the device whose hub you want to reconnect. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/connect_to_hub" + ) + + await self.client.post("/devices/simulate/connect_to_hub", json=json_payload) + + return None + + @route_metadata( + path="/devices/simulate/disconnect", + has_required_parameters=True, + has_pagination=False, + ) + async def disconnect(self, *, device_id: str) -> None: + """Simulates disconnecting a device from Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. + + :param device_id: ID of the device that you want to simulate disconnecting from Seam. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/disconnect" + ) + + await self.client.post("/devices/simulate/disconnect", json=json_payload) + + return None + + @route_metadata( + path="/devices/simulate/disconnect_from_hub", + has_required_parameters=True, + has_pagination=False, + ) + async def disconnect_from_hub(self, *, device_id: str) -> None: + """Simulates taking the Wi‑Fi hub (bridge) offline for a device. + Only applicable for sandbox workspaces and currently + implemented for August, TTLock, and IglooHome devices. + This will set the ``hub_disconnected`` error on the device, or mark the + IglooHome bridge offline in sandbox. + + :param device_id: ID of the device whose hub you want to disconnect. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/disconnect_from_hub" + ) + + await self.client.post( + "/devices/simulate/disconnect_from_hub", json=json_payload + ) + + return None + + @route_metadata( + path="/devices/simulate/paid_subscription", + has_required_parameters=True, + has_pagination=False, + ) + async def paid_subscription(self, *, device_id: str, is_expired: bool) -> None: + """Toggle the simulated Nuki Smart Hosting subscription for a device (sandbox only). + Send ``is_expired: true`` to simulate an expired subscription, or ``false`` to simulate an active subscription. + The actual device error is created/cleared by the poller after this state change. + + :param device_id: + + :param is_expired: + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if is_expired is not None: + json_payload["is_expired"] = is_expired + + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/paid_subscription" + ) + + await self.client.post("/devices/simulate/paid_subscription", json=json_payload) + + return None + + @route_metadata( + path="/devices/simulate/remove", + has_required_parameters=True, + has_pagination=False, + ) + async def remove(self, *, device_id: str) -> None: + """Simulates removing a device from Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. + + :param device_id: ID of the device that you want to simulate removing from Seam. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/remove" + ) + + await self.client.post("/devices/simulate/remove", json=json_payload) + + return None diff --git a/seam/routes/devices_unmanaged.py b/seam/routes/devices_unmanaged.py index 79439d60..156f0d04 100644 --- a/seam/routes/devices_unmanaged.py +++ b/seam/routes/devices_unmanaged.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import UnmanagedDevice @@ -97,6 +97,97 @@ def update( raise NotImplementedError() +class AbstractAsyncDevicesUnmanaged(abc.ABC): + + @abc.abstractmethod + async def get( + self, *, device_id: Optional[str] = None, name: Optional[str] = None + ) -> UnmanagedDevice: + """Returns a specified `unmanaged device `_. + + An unmanaged device has a limited set of visible properties and a subset of supported events. You cannot control an unmanaged device. Any `access codes `_ on an unmanaged device are unmanaged. To control an unmanaged device with Seam, `convert it to a managed device `_. + + You must specify either ``device_id`` or ``name``. + + :param device_id: ID of the unmanaged device that you want to get. + + :param name: Name of the unmanaged device that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + connected_account_ids: Optional[List[str]] = None, + created_before: Optional[str] = None, + customer_key: Optional[str] = None, + device_ids: Optional[List[str]] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + limit: Optional[float] = None, + manufacturer: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + ) -> List[UnmanagedDevice]: + """Returns a list of all `unmanaged devices `_. + + An unmanaged device has a limited set of visible properties and a subset of supported events. You cannot control an unmanaged device. Any `access codes `_ on an unmanaged device are unmanaged. To control an unmanaged device with Seam, `convert it to a managed device `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. + + :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. + + :param customer_key: Customer key for which you want to list devices. + + :param device_ids: Array of device IDs for which you want to list devices. + + :param device_type: Device type for which you want to list devices. + + :param device_types: Array of device types for which you want to list devices. + + :param limit: Numerical limit on the number of devices to return. + + :param manufacturer: Manufacturer for which you want to list devices. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + device_id: str, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, + is_managed: Optional[Literal[True]] = None, + ) -> None: + """Updates a specified `unmanaged device `_. To convert an unmanaged device to managed, set ``is_managed`` to ``true``. + + An unmanaged device has a limited set of visible properties and a subset of supported events. You cannot control an unmanaged device. Any `access codes `_ on an unmanaged device are unmanaged. To control an unmanaged device with Seam, `convert it to a managed device `_. + + :param device_id: ID of the unmanaged device that you want to update. + + :param custom_metadata: Custom metadata that you want to associate with the device. Supports up to 50 JSON key:value pairs. + + :param is_managed: Indicates whether the device is managed. Set this parameter to ``true`` to convert an unmanaged device to managed. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class DevicesUnmanaged(AbstractDevicesUnmanaged): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -260,3 +351,168 @@ def update( self.client.patch("/devices/unmanaged/update", json=json_payload) return None + + +class AsyncDevicesUnmanaged(AbstractAsyncDevicesUnmanaged): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/devices/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get( + self, *, device_id: Optional[str] = None, name: Optional[str] = None + ) -> UnmanagedDevice: + """Returns a specified `unmanaged device `_. + + An unmanaged device has a limited set of visible properties and a subset of supported events. You cannot control an unmanaged device. Any `access codes `_ on an unmanaged device are unmanaged. To control an unmanaged device with Seam, `convert it to a managed device `_. + + You must specify either ``device_id`` or ``name``. + + :param device_id: ID of the unmanaged device that you want to get. + + :param name: Name of the unmanaged device that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + if name is not None: + params["name"] = name + + if not params: + raise ValueError( + "At least one parameter is required for /devices/unmanaged/get" + ) + + res = await self.client.get("/devices/unmanaged/get", params=params) + + return UnmanagedDevice.from_dict(res["device"]) + + @route_metadata( + path="/devices/unmanaged/list", + has_required_parameters=False, + has_pagination=True, + ) + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + connected_account_ids: Optional[List[str]] = None, + created_before: Optional[str] = None, + customer_key: Optional[str] = None, + device_ids: Optional[List[str]] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + limit: Optional[float] = None, + manufacturer: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + ) -> List[UnmanagedDevice]: + """Returns a list of all `unmanaged devices `_. + + An unmanaged device has a limited set of visible properties and a subset of supported events. You cannot control an unmanaged device. Any `access codes `_ on an unmanaged device are unmanaged. To control an unmanaged device with Seam, `convert it to a managed device `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. + + :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. + + :param customer_key: Customer key for which you want to list devices. + + :param device_ids: Array of device IDs for which you want to list devices. + + :param device_type: Device type for which you want to list devices. + + :param device_types: Array of device types for which you want to list devices. + + :param limit: Numerical limit on the number of devices to return. + + :param manufacturer: Manufacturer for which you want to list devices. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. + + :returns: OK""" + params: Dict[str, Any] = {} + + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if connected_account_ids is not None: + params["connected_account_ids"] = connected_account_ids + if created_before is not None: + params["created_before"] = created_before + if customer_key is not None: + params["customer_key"] = customer_key + if device_ids is not None: + params["device_ids"] = device_ids + if device_type is not None: + params["device_type"] = device_type + if device_types is not None: + params["device_types"] = device_types + if limit is not None: + params["limit"] = limit + if manufacturer is not None: + params["manufacturer"] = manufacturer + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + + res = await self.client.get("/devices/unmanaged/list", params=params) + + return [UnmanagedDevice.from_dict(item) for item in res["devices"]] + + @route_metadata( + path="/devices/unmanaged/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + device_id: str, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, + is_managed: Optional[Literal[True]] = None, + ) -> None: + """Updates a specified `unmanaged device `_. To convert an unmanaged device to managed, set ``is_managed`` to ``true``. + + An unmanaged device has a limited set of visible properties and a subset of supported events. You cannot control an unmanaged device. Any `access codes `_ on an unmanaged device are unmanaged. To control an unmanaged device with Seam, `convert it to a managed device `_. + + :param device_id: ID of the unmanaged device that you want to update. + + :param custom_metadata: Custom metadata that you want to associate with the device. Supports up to 50 JSON key:value pairs. + + :param is_managed: Indicates whether the device is managed. Set this parameter to ``true`` to convert an unmanaged device to managed. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if custom_metadata is not None: + json_payload["custom_metadata"] = custom_metadata + if is_managed is not None: + json_payload["is_managed"] = is_managed + + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/unmanaged/update" + ) + + await self.client.patch("/devices/unmanaged/update", json=json_payload) + + return None diff --git a/seam/routes/events.py b/seam/routes/events.py index 9d1e0c50..349aa7d5 100644 --- a/seam/routes/events.py +++ b/seam/routes/events.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import SeamEvent @@ -125,6 +125,126 @@ def list( raise NotImplementedError() +class AbstractAsyncEvents(abc.ABC): + + @abc.abstractmethod + async def get( + self, + *, + event_id: Optional[str] = None, + device_id: Optional[str] = None, + event_type: Optional[str] = None, + ) -> SeamEvent: + """Returns a specified event. This endpoint returns the same event that would be sent to a `webhook `_, but it enables you to retrieve an event that already took place. + + :param event_id: Unique identifier for the event that you want to get. + + :param device_id: Unique identifier for the device that triggered the event that you want to get. + + :param event_type: Type of the event that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + access_code_id: Optional[str] = None, + access_code_ids: Optional[List[str]] = None, + access_grant_id: Optional[str] = None, + access_grant_ids: Optional[List[str]] = None, + access_method_id: Optional[str] = None, + access_method_ids: Optional[List[str]] = None, + acs_access_group_id: Optional[str] = None, + acs_credential_id: Optional[str] = None, + acs_encoder_id: Optional[str] = None, + acs_entrance_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + acs_system_ids: Optional[List[str]] = None, + acs_user_id: Optional[str] = None, + between: Optional[List[str]] = None, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_id: Optional[str] = None, + device_ids: Optional[List[str]] = None, + event_ids: Optional[List[str]] = None, + event_type: Optional[str] = None, + event_types: Optional[List[str]] = None, + limit: Optional[float] = None, + since: Optional[str] = None, + space_id: Optional[str] = None, + space_ids: Optional[List[str]] = None, + unstable_offset: Optional[float] = None, + user_identity_id: Optional[str] = None, + ) -> List[SeamEvent]: + """Returns a list of all events. This endpoint returns the same events that would be sent to a `webhook `_, but it enables you to filter or see events that already took place. + + :param access_code_id: ID of the access code for which you want to list events. + + :param access_code_ids: IDs of the access codes for which you want to list events. + + :param access_grant_id: ID of the access grant for which you want to list events. + + :param access_grant_ids: IDs of the access grants for which you want to list events. + + :param access_method_id: ID of the access method for which you want to list events. + + :param access_method_ids: IDs of the access methods for which you want to list events. + + :param acs_access_group_id: ID of the ACS access group for which you want to list events. + + :param acs_credential_id: ID of the ACS credential for which you want to list events. + + :param acs_encoder_id: ID of the ACS encoder for which you want to list events. + + :param acs_entrance_id: ID of the ACS entrance for which you want to list events. + + :param acs_system_id: ID of the access system for which you want to list events. + + :param acs_system_ids: IDs of the access systems for which you want to list events. + + :param acs_user_id: ID of the ACS user for which you want to list events. + + :param between: Lower and upper timestamps to define an exclusive interval containing the events that you want to list. You must include ``since`` or ``between``. + + :param connect_webview_id: ID of the Connect Webview for which you want to list events. + + :param connected_account_id: ID of the connected account for which you want to list events. + + :param customer_key: Customer key for which you want to list events. + + :param device_id: ID of the device for which you want to list events. + + :param device_ids: IDs of the devices for which you want to list events. + + :param event_ids: IDs of the events that you want to list. + + :param event_type: Type of the events that you want to list. + + :param event_types: Types of the events that you want to list. + + :param limit: Numerical limit on the number of events to return. + + :param since: Timestamp to indicate the beginning generation time for the events that you want to list. You must include ``since`` or ``between``. + + :param space_id: ID of the space for which you want to list events. + + :param space_ids: IDs of the spaces for which you want to list events. + + :param unstable_offset: Offset for the events that you want to list. + + :param user_identity_id: ID of the user identity for which you want to list events. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class Events(AbstractEvents): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -328,3 +448,208 @@ def list( res = self.client.get("/events/list", params=params) return [SeamEvent.from_dict(item) for item in res["events"]] + + +class AsyncEvents(AbstractAsyncEvents): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/events/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, + *, + event_id: Optional[str] = None, + device_id: Optional[str] = None, + event_type: Optional[str] = None, + ) -> SeamEvent: + """Returns a specified event. This endpoint returns the same event that would be sent to a `webhook `_, but it enables you to retrieve an event that already took place. + + :param event_id: Unique identifier for the event that you want to get. + + :param device_id: Unique identifier for the device that triggered the event that you want to get. + + :param event_type: Type of the event that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if event_id is not None: + params["event_id"] = event_id + if device_id is not None: + params["device_id"] = device_id + if event_type is not None: + params["event_type"] = event_type + + if not params: + raise ValueError("At least one parameter is required for /events/get") + + res = await self.client.get("/events/get", params=params) + + return SeamEvent.from_dict(res["event"]) + + @route_metadata( + path="/events/list", has_required_parameters=True, has_pagination=False + ) + async def list( + self, + *, + access_code_id: Optional[str] = None, + access_code_ids: Optional[List[str]] = None, + access_grant_id: Optional[str] = None, + access_grant_ids: Optional[List[str]] = None, + access_method_id: Optional[str] = None, + access_method_ids: Optional[List[str]] = None, + acs_access_group_id: Optional[str] = None, + acs_credential_id: Optional[str] = None, + acs_encoder_id: Optional[str] = None, + acs_entrance_id: Optional[str] = None, + acs_system_id: Optional[str] = None, + acs_system_ids: Optional[List[str]] = None, + acs_user_id: Optional[str] = None, + between: Optional[List[str]] = None, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_id: Optional[str] = None, + device_ids: Optional[List[str]] = None, + event_ids: Optional[List[str]] = None, + event_type: Optional[str] = None, + event_types: Optional[List[str]] = None, + limit: Optional[float] = None, + since: Optional[str] = None, + space_id: Optional[str] = None, + space_ids: Optional[List[str]] = None, + unstable_offset: Optional[float] = None, + user_identity_id: Optional[str] = None, + ) -> List[SeamEvent]: + """Returns a list of all events. This endpoint returns the same events that would be sent to a `webhook `_, but it enables you to filter or see events that already took place. + + :param access_code_id: ID of the access code for which you want to list events. + + :param access_code_ids: IDs of the access codes for which you want to list events. + + :param access_grant_id: ID of the access grant for which you want to list events. + + :param access_grant_ids: IDs of the access grants for which you want to list events. + + :param access_method_id: ID of the access method for which you want to list events. + + :param access_method_ids: IDs of the access methods for which you want to list events. + + :param acs_access_group_id: ID of the ACS access group for which you want to list events. + + :param acs_credential_id: ID of the ACS credential for which you want to list events. + + :param acs_encoder_id: ID of the ACS encoder for which you want to list events. + + :param acs_entrance_id: ID of the ACS entrance for which you want to list events. + + :param acs_system_id: ID of the access system for which you want to list events. + + :param acs_system_ids: IDs of the access systems for which you want to list events. + + :param acs_user_id: ID of the ACS user for which you want to list events. + + :param between: Lower and upper timestamps to define an exclusive interval containing the events that you want to list. You must include ``since`` or ``between``. + + :param connect_webview_id: ID of the Connect Webview for which you want to list events. + + :param connected_account_id: ID of the connected account for which you want to list events. + + :param customer_key: Customer key for which you want to list events. + + :param device_id: ID of the device for which you want to list events. + + :param device_ids: IDs of the devices for which you want to list events. + + :param event_ids: IDs of the events that you want to list. + + :param event_type: Type of the events that you want to list. + + :param event_types: Types of the events that you want to list. + + :param limit: Numerical limit on the number of events to return. + + :param since: Timestamp to indicate the beginning generation time for the events that you want to list. You must include ``since`` or ``between``. + + :param space_id: ID of the space for which you want to list events. + + :param space_ids: IDs of the spaces for which you want to list events. + + :param unstable_offset: Offset for the events that you want to list. + + :param user_identity_id: ID of the user identity for which you want to list events. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if access_code_id is not None: + params["access_code_id"] = access_code_id + if access_code_ids is not None: + params["access_code_ids"] = access_code_ids + if access_grant_id is not None: + params["access_grant_id"] = access_grant_id + if access_grant_ids is not None: + params["access_grant_ids"] = access_grant_ids + if access_method_id is not None: + params["access_method_id"] = access_method_id + if access_method_ids is not None: + params["access_method_ids"] = access_method_ids + if acs_access_group_id is not None: + params["acs_access_group_id"] = acs_access_group_id + if acs_credential_id is not None: + params["acs_credential_id"] = acs_credential_id + if acs_encoder_id is not None: + params["acs_encoder_id"] = acs_encoder_id + if acs_entrance_id is not None: + params["acs_entrance_id"] = acs_entrance_id + if acs_system_id is not None: + params["acs_system_id"] = acs_system_id + if acs_system_ids is not None: + params["acs_system_ids"] = acs_system_ids + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if between is not None: + params["between"] = between + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if customer_key is not None: + params["customer_key"] = customer_key + if device_id is not None: + params["device_id"] = device_id + if device_ids is not None: + params["device_ids"] = device_ids + if event_ids is not None: + params["event_ids"] = event_ids + if event_type is not None: + params["event_type"] = event_type + if event_types is not None: + params["event_types"] = event_types + if limit is not None: + params["limit"] = limit + if since is not None: + params["since"] = since + if space_id is not None: + params["space_id"] = space_id + if space_ids is not None: + params["space_ids"] = space_ids + if unstable_offset is not None: + params["unstable_offset"] = unstable_offset + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError("At least one parameter is required for /events/list") + + res = await self.client.get("/events/list", params=params) + + return [SeamEvent.from_dict(item) for item in res["events"]] diff --git a/seam/routes/instant_keys.py b/seam/routes/instant_keys.py index eb3e1a07..5201b243 100644 --- a/seam/routes/instant_keys.py +++ b/seam/routes/instant_keys.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import InstantKey @@ -44,6 +44,45 @@ def list(self, *, user_identity_id: Optional[str] = None) -> List[InstantKey]: raise NotImplementedError() +class AbstractAsyncInstantKeys(abc.ABC): + + @abc.abstractmethod + async def delete(self, *, instant_key_id: str) -> None: + """Deletes a specified `Instant Key `_. + + :param instant_key_id: ID of the Instant Key that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, + *, + instant_key_id: Optional[str] = None, + instant_key_url: Optional[str] = None, + ) -> InstantKey: + """Gets an `instant key `_. + + :param instant_key_id: ID of the instant key to get. + + :param instant_key_url: URL of the instant key to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list(self, *, user_identity_id: Optional[str] = None) -> List[InstantKey]: + """Returns a list of all `instant keys `_. + + :param user_identity_id: ID of the user identity by which you want to filter the list of Instant Keys. + + :returns: OK""" + raise NotImplementedError() + + class InstantKeys(AbstractInstantKeys): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -121,3 +160,82 @@ def list(self, *, user_identity_id: Optional[str] = None) -> List[InstantKey]: res = self.client.get("/instant_keys/list", params=params) return [InstantKey.from_dict(item) for item in res["instant_keys"]] + + +class AsyncInstantKeys(AbstractAsyncInstantKeys): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/instant_keys/delete", has_required_parameters=True, has_pagination=False + ) + async def delete(self, *, instant_key_id: str) -> None: + """Deletes a specified `Instant Key `_. + + :param instant_key_id: ID of the Instant Key that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if instant_key_id is not None: + params["instant_key_id"] = instant_key_id + + if not params: + raise ValueError( + "At least one parameter is required for /instant_keys/delete" + ) + + await self.client.delete("/instant_keys/delete", params=params) + + return None + + @route_metadata( + path="/instant_keys/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, + *, + instant_key_id: Optional[str] = None, + instant_key_url: Optional[str] = None, + ) -> InstantKey: + """Gets an `instant key `_. + + :param instant_key_id: ID of the instant key to get. + + :param instant_key_url: URL of the instant key to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if instant_key_id is not None: + params["instant_key_id"] = instant_key_id + if instant_key_url is not None: + params["instant_key_url"] = instant_key_url + + if not params: + raise ValueError("At least one parameter is required for /instant_keys/get") + + res = await self.client.get("/instant_keys/get", params=params) + + return InstantKey.from_dict(res["instant_key"]) + + @route_metadata( + path="/instant_keys/list", has_required_parameters=False, has_pagination=False + ) + async def list(self, *, user_identity_id: Optional[str] = None) -> List[InstantKey]: + """Returns a list of all `instant keys `_. + + :param user_identity_id: ID of the user identity by which you want to filter the list of Instant Keys. + + :returns: OK""" + params: Dict[str, Any] = {} + + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + res = await self.client.get("/instant_keys/list", params=params) + + return [InstantKey.from_dict(item) for item in res["instant_keys"]] diff --git a/seam/routes/locks.py b/seam/routes/locks.py index 849ab0fc..88ae0d43 100644 --- a/seam/routes/locks.py +++ b/seam/routes/locks.py @@ -1,10 +1,18 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import ActionAttempt, Device -from .locks_simulate import AbstractLocksSimulate, LocksSimulate -from ..modules.action_attempts import resolve_action_attempt +from .locks_simulate import ( + AbstractLocksSimulate, + LocksSimulate, + AbstractAsyncLocksSimulate, + AsyncLocksSimulate, +) +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, +) class AbstractLocks(abc.ABC): @@ -121,6 +129,120 @@ def unlock_door( raise NotImplementedError() +class AbstractAsyncLocks(abc.ABC): + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAsyncLocksSimulate: + raise NotImplementedError() + + @abc.abstractmethod + async def configure_auto_lock( + self, + *, + auto_lock_enabled: bool, + device_id: str, + auto_lock_delay_seconds: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Configures the auto-lock setting for a specified `lock `_. + + :param auto_lock_enabled: Whether to enable or disable auto-lock. + + :param device_id: ID of the lock for which you want to configure the auto-lock. + + :param auto_lock_delay_seconds: Delay in seconds before the lock automatically locks. Required when enabling auto-lock. Must be between 1 and 60. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, *, device_id: Optional[str] = None, name: Optional[str] = None + ) -> Device: + """Returns a specified `lock `_. + + :param device_id: ID of the lock that you want to get. + + :param name: Name of the lock that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided. + + .. deprecated:: + Use ``/devices/get`` instead.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + manufacturer: Optional[str] = None, + ) -> List[Device]: + """Returns a list of all `locks `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param customer_key: Customer key for which you want to list devices. + + :param device_type: Device type of the locks that you want to list. + + :param device_types: Device types of the locks that you want to list. + + :param manufacturer: Manufacturer of the locks that you want to list. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def lock_door( + self, + *, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Locks a `lock `_. See also `Locking and Unlocking Smart Locks `_. + + :param device_id: ID of the lock that you want to lock. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def unlock_door( + self, + *, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Unlocks a `lock `_. See also `Locking and Unlocking Smart Locks `_. + + :param device_id: ID of the lock that you want to unlock. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class Locks(AbstractLocks): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -345,3 +467,229 @@ def unlock_door( action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, ) + + +class AsyncLocks(AbstractAsyncLocks): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._simulate = AsyncLocksSimulate(client=client, defaults=defaults) + + @property + def simulate(self) -> AsyncLocksSimulate: + return self._simulate + + @route_metadata( + path="/locks/configure_auto_lock", + has_required_parameters=True, + has_pagination=False, + ) + async def configure_auto_lock( + self, + *, + auto_lock_enabled: bool, + device_id: str, + auto_lock_delay_seconds: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Configures the auto-lock setting for a specified `lock `_. + + :param auto_lock_enabled: Whether to enable or disable auto-lock. + + :param device_id: ID of the lock for which you want to configure the auto-lock. + + :param auto_lock_delay_seconds: Delay in seconds before the lock automatically locks. Required when enabling auto-lock. Must be between 1 and 60. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if auto_lock_enabled is not None: + json_payload["auto_lock_enabled"] = auto_lock_enabled + if device_id is not None: + json_payload["device_id"] = device_id + if auto_lock_delay_seconds is not None: + json_payload["auto_lock_delay_seconds"] = auto_lock_delay_seconds + + if not json_payload: + raise ValueError( + "At least one parameter is required for /locks/configure_auto_lock" + ) + + res = await self.client.post("/locks/configure_auto_lock", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/locks/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, *, device_id: Optional[str] = None, name: Optional[str] = None + ) -> Device: + """Returns a specified `lock `_. + + :param device_id: ID of the lock that you want to get. + + :param name: Name of the lock that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided. + + .. deprecated:: + Use ``/devices/get`` instead.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + if name is not None: + params["name"] = name + + if not params: + raise ValueError("At least one parameter is required for /locks/get") + + res = await self.client.get("/locks/get", params=params) + + return Device.from_dict(res["device"]) + + @route_metadata( + path="/locks/list", has_required_parameters=False, has_pagination=False + ) + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + manufacturer: Optional[str] = None, + ) -> List[Device]: + """Returns a list of all `locks `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param customer_key: Customer key for which you want to list devices. + + :param device_type: Device type of the locks that you want to list. + + :param device_types: Device types of the locks that you want to list. + + :param manufacturer: Manufacturer of the locks that you want to list. + + :returns: OK""" + params: Dict[str, Any] = {} + + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if customer_key is not None: + params["customer_key"] = customer_key + if device_type is not None: + params["device_type"] = device_type + if device_types is not None: + params["device_types"] = device_types + if manufacturer is not None: + params["manufacturer"] = manufacturer + + res = await self.client.get("/locks/list", params=params) + + return [Device.from_dict(item) for item in res["devices"]] + + @route_metadata( + path="/locks/lock_door", has_required_parameters=True, has_pagination=False + ) + async def lock_door( + self, + *, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Locks a `lock `_. See also `Locking and Unlocking Smart Locks `_. + + :param device_id: ID of the lock that you want to lock. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError("At least one parameter is required for /locks/lock_door") + + res = await self.client.post("/locks/lock_door", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/locks/unlock_door", has_required_parameters=True, has_pagination=False + ) + async def unlock_door( + self, + *, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Unlocks a `lock `_. See also `Locking and Unlocking Smart Locks `_. + + :param device_id: ID of the lock that you want to unlock. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /locks/unlock_door" + ) + + res = await self.client.post("/locks/unlock_door", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) diff --git a/seam/routes/locks_simulate.py b/seam/routes/locks_simulate.py index b91c579f..3c6131ab 100644 --- a/seam/routes/locks_simulate.py +++ b/seam/routes/locks_simulate.py @@ -1,9 +1,12 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import ActionAttempt -from ..modules.action_attempts import resolve_action_attempt +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, +) class AbstractLocksSimulate(abc.ABC): @@ -48,6 +51,48 @@ def manual_lock_via_keypad( raise NotImplementedError() +class AbstractAsyncLocksSimulate(abc.ABC): + + @abc.abstractmethod + async def keypad_code_entry( + self, + *, + code: str, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Simulates the entry of a code on a keypad. You can only perform this action for `August `_ devices within `sandbox workspaces `_. + + :param code: Code that you want to simulate entering on a keypad. + + :param device_id: ID of the device for which you want to simulate a keypad code entry. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def manual_lock_via_keypad( + self, + *, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Simulates a manual lock action using a keypad. You can only perform this action for `August `_ devices within `sandbox workspaces `_. + + :param device_id: ID of the device for which you want to simulate a manual lock action using a keypad. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class LocksSimulate(AbstractLocksSimulate): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -147,3 +192,106 @@ def manual_lock_via_keypad( action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, ) + + +class AsyncLocksSimulate(AbstractAsyncLocksSimulate): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/locks/simulate/keypad_code_entry", + has_required_parameters=True, + has_pagination=False, + ) + async def keypad_code_entry( + self, + *, + code: str, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Simulates the entry of a code on a keypad. You can only perform this action for `August `_ devices within `sandbox workspaces `_. + + :param code: Code that you want to simulate entering on a keypad. + + :param device_id: ID of the device for which you want to simulate a keypad code entry. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if code is not None: + json_payload["code"] = code + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /locks/simulate/keypad_code_entry" + ) + + res = await self.client.post( + "/locks/simulate/keypad_code_entry", json=json_payload + ) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/locks/simulate/manual_lock_via_keypad", + has_required_parameters=True, + has_pagination=False, + ) + async def manual_lock_via_keypad( + self, + *, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Simulates a manual lock action using a keypad. You can only perform this action for `August `_ devices within `sandbox workspaces `_. + + :param device_id: ID of the device for which you want to simulate a manual lock action using a keypad. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /locks/simulate/manual_lock_via_keypad" + ) + + res = await self.client.post( + "/locks/simulate/manual_lock_via_keypad", json=json_payload + ) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) diff --git a/seam/routes/noise_sensors.py b/seam/routes/noise_sensors.py index 2de18976..ef6515b3 100644 --- a/seam/routes/noise_sensors.py +++ b/seam/routes/noise_sensors.py @@ -1,13 +1,20 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import Device from .noise_sensors_noise_thresholds import ( AbstractNoiseSensorsNoiseThresholds, NoiseSensorsNoiseThresholds, + AbstractAsyncNoiseSensorsNoiseThresholds, + AsyncNoiseSensorsNoiseThresholds, +) +from .noise_sensors_simulate import ( + AbstractNoiseSensorsSimulate, + NoiseSensorsSimulate, + AbstractAsyncNoiseSensorsSimulate, + AsyncNoiseSensorsSimulate, ) -from .noise_sensors_simulate import AbstractNoiseSensorsSimulate, NoiseSensorsSimulate class AbstractNoiseSensors(abc.ABC): @@ -51,6 +58,47 @@ def list( raise NotImplementedError() +class AbstractAsyncNoiseSensors(abc.ABC): + + @property + @abc.abstractmethod + def noise_thresholds(self) -> AbstractAsyncNoiseSensorsNoiseThresholds: + raise NotImplementedError() + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAsyncNoiseSensorsSimulate: + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + manufacturer: Optional[str] = None, + ) -> List[Device]: + """Returns a list of all `noise sensors `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param customer_key: Customer key for which you want to list devices. + + :param device_type: Device type of the noise sensors that you want to list. + + :param device_types: Device types of the noise sensors that you want to list. + + :param manufacturer: Manufacturers of the noise sensors that you want to list. + + :returns: OK""" + raise NotImplementedError() + + class NoiseSensors(AbstractNoiseSensors): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -114,3 +162,68 @@ def list( res = self.client.get("/noise_sensors/list", params=params) return [Device.from_dict(item) for item in res["devices"]] + + +class AsyncNoiseSensors(AbstractAsyncNoiseSensors): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._noise_thresholds = AsyncNoiseSensorsNoiseThresholds( + client=client, defaults=defaults + ) + self._simulate = AsyncNoiseSensorsSimulate(client=client, defaults=defaults) + + @property + def noise_thresholds(self) -> AsyncNoiseSensorsNoiseThresholds: + return self._noise_thresholds + + @property + def simulate(self) -> AsyncNoiseSensorsSimulate: + return self._simulate + + @route_metadata( + path="/noise_sensors/list", has_required_parameters=False, has_pagination=False + ) + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + manufacturer: Optional[str] = None, + ) -> List[Device]: + """Returns a list of all `noise sensors `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param customer_key: Customer key for which you want to list devices. + + :param device_type: Device type of the noise sensors that you want to list. + + :param device_types: Device types of the noise sensors that you want to list. + + :param manufacturer: Manufacturers of the noise sensors that you want to list. + + :returns: OK""" + params: Dict[str, Any] = {} + + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if customer_key is not None: + params["customer_key"] = customer_key + if device_type is not None: + params["device_type"] = device_type + if device_types is not None: + params["device_types"] = device_types + if manufacturer is not None: + params["manufacturer"] = manufacturer + + res = await self.client.get("/noise_sensors/list", params=params) + + return [Device.from_dict(item) for item in res["devices"]] diff --git a/seam/routes/noise_sensors_noise_thresholds.py b/seam/routes/noise_sensors_noise_thresholds.py index bc4277b7..8bf54786 100644 --- a/seam/routes/noise_sensors_noise_thresholds.py +++ b/seam/routes/noise_sensors_noise_thresholds.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import NoiseThreshold @@ -102,6 +102,103 @@ def update( raise NotImplementedError() +class AbstractAsyncNoiseSensorsNoiseThresholds(abc.ABC): + + @abc.abstractmethod + async def create( + self, + *, + device_id: str, + ends_daily_at: str, + starts_daily_at: str, + name: Optional[str] = None, + noise_threshold_decibels: Optional[float] = None, + noise_threshold_nrs: Optional[float] = None, + ) -> NoiseThreshold: + """Creates a new `noise threshold `_ for a `noise sensor `_. Thresholds represent the limits of noise tolerated at a property, which can be customized for each hour of the day. Each device has its own default thresholds, but you can use the Seam API to modify them. + + :param device_id: ID of the device for which you want to create a noise threshold. + + :param ends_daily_at: Time at which the new noise threshold should become inactive daily. + + :param starts_daily_at: Time at which the new noise threshold should become active daily. + + :param name: Name of the new noise threshold. + + :param noise_threshold_decibels: Noise level in decibels for the new noise threshold. + + :param noise_threshold_nrs: Noise level in Noiseaware Noise Risk Score (NRS) for the new noise threshold. This parameter is only relevant for `Noiseaware sensors `_. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, device_id: str, noise_threshold_id: str) -> None: + """Deletes a `noise threshold `_ from a `noise sensor `_. + + :param device_id: ID of the device that contains the noise threshold that you want to delete. + + :param noise_threshold_id: ID of the noise threshold that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, noise_threshold_id: str) -> NoiseThreshold: + """Returns a specified `noise threshold `_ for a `noise sensor `_. + + :param noise_threshold_id: ID of the noise threshold that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list(self, *, device_id: str) -> List[NoiseThreshold]: + """Returns a list of all `noise thresholds `_ for a `noise sensor `_. + + :param device_id: ID of the device for which you want to list noise thresholds. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + device_id: str, + noise_threshold_id: str, + ends_daily_at: Optional[str] = None, + name: Optional[str] = None, + noise_threshold_decibels: Optional[float] = None, + noise_threshold_nrs: Optional[float] = None, + starts_daily_at: Optional[str] = None, + ) -> None: + """Updates a `noise threshold `_ for a `noise sensor `_. + + :param device_id: ID of the device that contains the noise threshold that you want to update. + + :param noise_threshold_id: ID of the noise threshold that you want to update. + + :param ends_daily_at: Time at which the noise threshold should become inactive daily. + + :param name: Name of the noise threshold that you want to update. + + :param noise_threshold_decibels: Noise level in decibels for the noise threshold. + + :param noise_threshold_nrs: Noise level in Noiseaware Noise Risk Score (NRS) for the noise threshold. This parameter is only relevant for `Noiseaware sensors `_. + + :param starts_daily_at: Time at which the noise threshold should become active daily. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class NoiseSensorsNoiseThresholds(AbstractNoiseSensorsNoiseThresholds): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -306,3 +403,217 @@ def update( self.client.put("/noise_sensors/noise_thresholds/update", json=json_payload) return None + + +class AsyncNoiseSensorsNoiseThresholds(AbstractAsyncNoiseSensorsNoiseThresholds): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/noise_sensors/noise_thresholds/create", + has_required_parameters=True, + has_pagination=False, + ) + async def create( + self, + *, + device_id: str, + ends_daily_at: str, + starts_daily_at: str, + name: Optional[str] = None, + noise_threshold_decibels: Optional[float] = None, + noise_threshold_nrs: Optional[float] = None, + ) -> NoiseThreshold: + """Creates a new `noise threshold `_ for a `noise sensor `_. Thresholds represent the limits of noise tolerated at a property, which can be customized for each hour of the day. Each device has its own default thresholds, but you can use the Seam API to modify them. + + :param device_id: ID of the device for which you want to create a noise threshold. + + :param ends_daily_at: Time at which the new noise threshold should become inactive daily. + + :param starts_daily_at: Time at which the new noise threshold should become active daily. + + :param name: Name of the new noise threshold. + + :param noise_threshold_decibels: Noise level in decibels for the new noise threshold. + + :param noise_threshold_nrs: Noise level in Noiseaware Noise Risk Score (NRS) for the new noise threshold. This parameter is only relevant for `Noiseaware sensors `_. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if ends_daily_at is not None: + json_payload["ends_daily_at"] = ends_daily_at + if starts_daily_at is not None: + json_payload["starts_daily_at"] = starts_daily_at + if name is not None: + json_payload["name"] = name + if noise_threshold_decibels is not None: + json_payload["noise_threshold_decibels"] = noise_threshold_decibels + if noise_threshold_nrs is not None: + json_payload["noise_threshold_nrs"] = noise_threshold_nrs + + if not json_payload: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/create" + ) + + res = await self.client.post( + "/noise_sensors/noise_thresholds/create", json=json_payload + ) + + return NoiseThreshold.from_dict(res["noise_threshold"]) + + @route_metadata( + path="/noise_sensors/noise_thresholds/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, device_id: str, noise_threshold_id: str) -> None: + """Deletes a `noise threshold `_ from a `noise sensor `_. + + :param device_id: ID of the device that contains the noise threshold that you want to delete. + + :param noise_threshold_id: ID of the noise threshold that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + if noise_threshold_id is not None: + params["noise_threshold_id"] = noise_threshold_id + + if not params: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/delete" + ) + + await self.client.delete( + "/noise_sensors/noise_thresholds/delete", params=params + ) + + return None + + @route_metadata( + path="/noise_sensors/noise_thresholds/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get(self, *, noise_threshold_id: str) -> NoiseThreshold: + """Returns a specified `noise threshold `_ for a `noise sensor `_. + + :param noise_threshold_id: ID of the noise threshold that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if noise_threshold_id is not None: + params["noise_threshold_id"] = noise_threshold_id + + if not params: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/get" + ) + + res = await self.client.get( + "/noise_sensors/noise_thresholds/get", params=params + ) + + return NoiseThreshold.from_dict(res["noise_threshold"]) + + @route_metadata( + path="/noise_sensors/noise_thresholds/list", + has_required_parameters=True, + has_pagination=False, + ) + async def list(self, *, device_id: str) -> List[NoiseThreshold]: + """Returns a list of all `noise thresholds `_ for a `noise sensor `_. + + :param device_id: ID of the device for which you want to list noise thresholds. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + + if not params: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/list" + ) + + res = await self.client.get( + "/noise_sensors/noise_thresholds/list", params=params + ) + + return [NoiseThreshold.from_dict(item) for item in res["noise_thresholds"]] + + @route_metadata( + path="/noise_sensors/noise_thresholds/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + device_id: str, + noise_threshold_id: str, + ends_daily_at: Optional[str] = None, + name: Optional[str] = None, + noise_threshold_decibels: Optional[float] = None, + noise_threshold_nrs: Optional[float] = None, + starts_daily_at: Optional[str] = None, + ) -> None: + """Updates a `noise threshold `_ for a `noise sensor `_. + + :param device_id: ID of the device that contains the noise threshold that you want to update. + + :param noise_threshold_id: ID of the noise threshold that you want to update. + + :param ends_daily_at: Time at which the noise threshold should become inactive daily. + + :param name: Name of the noise threshold that you want to update. + + :param noise_threshold_decibels: Noise level in decibels for the noise threshold. + + :param noise_threshold_nrs: Noise level in Noiseaware Noise Risk Score (NRS) for the noise threshold. This parameter is only relevant for `Noiseaware sensors `_. + + :param starts_daily_at: Time at which the noise threshold should become active daily. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if noise_threshold_id is not None: + json_payload["noise_threshold_id"] = noise_threshold_id + if ends_daily_at is not None: + json_payload["ends_daily_at"] = ends_daily_at + if name is not None: + json_payload["name"] = name + if noise_threshold_decibels is not None: + json_payload["noise_threshold_decibels"] = noise_threshold_decibels + if noise_threshold_nrs is not None: + json_payload["noise_threshold_nrs"] = noise_threshold_nrs + if starts_daily_at is not None: + json_payload["starts_daily_at"] = starts_daily_at + + if not json_payload: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/update" + ) + + await self.client.put( + "/noise_sensors/noise_thresholds/update", json=json_payload + ) + + return None diff --git a/seam/routes/noise_sensors_simulate.py b/seam/routes/noise_sensors_simulate.py index 5f02cb8d..21a7a917 100644 --- a/seam/routes/noise_sensors_simulate.py +++ b/seam/routes/noise_sensors_simulate.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata @@ -16,6 +16,18 @@ def trigger_noise_threshold(self, *, device_id: str) -> None: raise NotImplementedError() +class AbstractAsyncNoiseSensorsSimulate(abc.ABC): + + @abc.abstractmethod + async def trigger_noise_threshold(self, *, device_id: str) -> None: + """Simulates the triggering of a `noise threshold `_ for a `noise sensor `_ in a `sandbox workspace `_. + + :param device_id: ID of the device for which you want to simulate the triggering of a noise threshold. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class NoiseSensorsSimulate(AbstractNoiseSensorsSimulate): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -47,3 +59,36 @@ def trigger_noise_threshold(self, *, device_id: str) -> None: ) return None + + +class AsyncNoiseSensorsSimulate(AbstractAsyncNoiseSensorsSimulate): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/noise_sensors/simulate/trigger_noise_threshold", + has_required_parameters=True, + has_pagination=False, + ) + async def trigger_noise_threshold(self, *, device_id: str) -> None: + """Simulates the triggering of a `noise threshold `_ for a `noise sensor `_ in a `sandbox workspace `_. + + :param device_id: ID of the device for which you want to simulate the triggering of a noise threshold. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /noise_sensors/simulate/trigger_noise_threshold" + ) + + await self.client.post( + "/noise_sensors/simulate/trigger_noise_threshold", json=json_payload + ) + + return None diff --git a/seam/routes/phones.py b/seam/routes/phones.py index c02fa99b..b291296a 100644 --- a/seam/routes/phones.py +++ b/seam/routes/phones.py @@ -1,9 +1,14 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import Phone -from .phones_simulate import AbstractPhonesSimulate, PhonesSimulate +from .phones_simulate import ( + AbstractPhonesSimulate, + PhonesSimulate, + AbstractAsyncPhonesSimulate, + AsyncPhonesSimulate, +) class AbstractPhones(abc.ABC): @@ -50,6 +55,50 @@ def list( raise NotImplementedError() +class AbstractAsyncPhones(abc.ABC): + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAsyncPhonesSimulate: + raise NotImplementedError() + + @abc.abstractmethod + async def deactivate(self, *, device_id: str) -> None: + """Deactivates a phone, which is useful, for example, if a user has lost their phone. For more information, see `App User Lost Phone Process `_. + + :param device_id: Device ID of the phone that you want to deactivate. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, device_id: str) -> Phone: + """Returns a specified `phone `_. + + :param device_id: Device ID of the phone that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + acs_credential_id: Optional[str] = None, + owner_user_identity_id: Optional[str] = None, + ) -> List[Phone]: + """Returns a list of all `phones `_. To filter the list of returned phones by a specific owner user identity or credential, include the ``owner_user_identity_id`` or ``acs_credential_id``, respectively, in the request body. + + :param acs_credential_id: ID of the `credential `_ by which you want to filter the list of returned phones. + + :param owner_user_identity_id: ID of the user identity that represents the owner by which you want to filter the list of returned phones. + + :returns: OK""" + raise NotImplementedError() + + class Phones(AbstractPhones): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -132,3 +181,87 @@ def list( res = self.client.get("/phones/list", params=params) return [Phone.from_dict(item) for item in res["phones"]] + + +class AsyncPhones(AbstractAsyncPhones): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._simulate = AsyncPhonesSimulate(client=client, defaults=defaults) + + @property + def simulate(self) -> AsyncPhonesSimulate: + return self._simulate + + @route_metadata( + path="/phones/deactivate", has_required_parameters=True, has_pagination=False + ) + async def deactivate(self, *, device_id: str) -> None: + """Deactivates a phone, which is useful, for example, if a user has lost their phone. For more information, see `App User Lost Phone Process `_. + + :param device_id: Device ID of the phone that you want to deactivate. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + + if not params: + raise ValueError( + "At least one parameter is required for /phones/deactivate" + ) + + await self.client.delete("/phones/deactivate", params=params) + + return None + + @route_metadata( + path="/phones/get", has_required_parameters=True, has_pagination=False + ) + async def get(self, *, device_id: str) -> Phone: + """Returns a specified `phone `_. + + :param device_id: Device ID of the phone that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + + if not params: + raise ValueError("At least one parameter is required for /phones/get") + + res = await self.client.get("/phones/get", params=params) + + return Phone.from_dict(res["phone"]) + + @route_metadata( + path="/phones/list", has_required_parameters=False, has_pagination=False + ) + async def list( + self, + *, + acs_credential_id: Optional[str] = None, + owner_user_identity_id: Optional[str] = None, + ) -> List[Phone]: + """Returns a list of all `phones `_. To filter the list of returned phones by a specific owner user identity or credential, include the ``owner_user_identity_id`` or ``acs_credential_id``, respectively, in the request body. + + :param acs_credential_id: ID of the `credential `_ by which you want to filter the list of returned phones. + + :param owner_user_identity_id: ID of the user identity that represents the owner by which you want to filter the list of returned phones. + + :returns: OK""" + params: Dict[str, Any] = {} + + if acs_credential_id is not None: + params["acs_credential_id"] = acs_credential_id + if owner_user_identity_id is not None: + params["owner_user_identity_id"] = owner_user_identity_id + + res = await self.client.get("/phones/list", params=params) + + return [Phone.from_dict(item) for item in res["phones"]] diff --git a/seam/routes/phones_simulate.py b/seam/routes/phones_simulate.py index debcd589..8a9f4d36 100644 --- a/seam/routes/phones_simulate.py +++ b/seam/routes/phones_simulate.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import Phone @@ -32,6 +32,33 @@ def create_sandbox_phone( raise NotImplementedError() +class AbstractAsyncPhonesSimulate(abc.ABC): + + @abc.abstractmethod + async def create_sandbox_phone( + self, + *, + user_identity_id: str, + assa_abloy_metadata: Optional[Dict[str, Any]] = None, + custom_sdk_installation_id: Optional[str] = None, + phone_metadata: Optional[Dict[str, Any]] = None, + ) -> Phone: + """Creates a new simulated phone in a `sandbox workspace `_. See also `Creating a Simulated Phone for a User Identity `_. + + :param user_identity_id: ID of the user identity that you want to associate with the simulated phone. + + :param assa_abloy_metadata: ASSA ABLOY metadata that you want to associate with the simulated phone. + + :param custom_sdk_installation_id: ID of the custom SDK installation that you want to use for the simulated phone. + + :param phone_metadata: Metadata that you want to associate with the simulated phone. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class PhonesSimulate(AbstractPhonesSimulate): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -84,3 +111,57 @@ def create_sandbox_phone( ) return Phone.from_dict(res["phone"]) + + +class AsyncPhonesSimulate(AbstractAsyncPhonesSimulate): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/phones/simulate/create_sandbox_phone", + has_required_parameters=True, + has_pagination=False, + ) + async def create_sandbox_phone( + self, + *, + user_identity_id: str, + assa_abloy_metadata: Optional[Dict[str, Any]] = None, + custom_sdk_installation_id: Optional[str] = None, + phone_metadata: Optional[Dict[str, Any]] = None, + ) -> Phone: + """Creates a new simulated phone in a `sandbox workspace `_. See also `Creating a Simulated Phone for a User Identity `_. + + :param user_identity_id: ID of the user identity that you want to associate with the simulated phone. + + :param assa_abloy_metadata: ASSA ABLOY metadata that you want to associate with the simulated phone. + + :param custom_sdk_installation_id: ID of the custom SDK installation that you want to use for the simulated phone. + + :param phone_metadata: Metadata that you want to associate with the simulated phone. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if assa_abloy_metadata is not None: + json_payload["assa_abloy_metadata"] = assa_abloy_metadata + if custom_sdk_installation_id is not None: + json_payload["custom_sdk_installation_id"] = custom_sdk_installation_id + if phone_metadata is not None: + json_payload["phone_metadata"] = phone_metadata + + if not json_payload: + raise ValueError( + "At least one parameter is required for /phones/simulate/create_sandbox_phone" + ) + + res = await self.client.post( + "/phones/simulate/create_sandbox_phone", json=json_payload + ) + + return Phone.from_dict(res["phone"]) diff --git a/seam/routes/spaces.py b/seam/routes/spaces.py index 33edcfde..05a8300d 100644 --- a/seam/routes/spaces.py +++ b/seam/routes/spaces.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import Space, Batch @@ -215,6 +215,217 @@ def update( raise NotImplementedError() +class AbstractAsyncSpaces(abc.ABC): + + @abc.abstractmethod + async def add_acs_entrances( + self, *, acs_entrance_ids: List[str], space_id: str + ) -> None: + """Adds `entrances `_ to a specific space. + + :param acs_entrance_ids: IDs of the entrances that you want to add to the space. + + :param space_id: ID of the space to which you want to add entrances. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def add_connected_account( + self, *, connected_account_id: str, space_id: str + ) -> None: + """Adds a `connected account `_ to a specific space. + + :param connected_account_id: ID of the connected account that you want to add to the space. + + :param space_id: ID of the space to which you want to add the connected account. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def add_devices(self, *, device_ids: List[str], space_id: str) -> None: + """Adds devices to a specific space. + + :param device_ids: IDs of the devices that you want to add to the space. + + :param space_id: ID of the space to which you want to add devices. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def create( + self, + *, + name: str, + acs_entrance_ids: Optional[List[str]] = None, + connected_account_ids: Optional[List[str]] = None, + customer_data: Optional[Dict[str, Any]] = None, + customer_key: Optional[str] = None, + device_ids: Optional[List[str]] = None, + space_key: Optional[str] = None, + ) -> Space: + """Creates a new space. + + :param name: Name of the space that you want to create. + + :param acs_entrance_ids: IDs of the entrances that you want to add to the new space. + + :param connected_account_ids: IDs of connected accounts to associate with the new space. Persisted on seam.location_third_party_account so the UI can show which provider account(s) a space came from. + + :param customer_data: Reservation/stay-related defaults for the space. + + :param customer_key: Customer key for which you want to create the space. + + :param device_ids: IDs of the devices that you want to add to the new space. + + :param space_key: Unique key for the space within the workspace. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, space_id: str) -> None: + """Deletes a space. + + :param space_id: ID of the space that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, *, space_id: Optional[str] = None, space_key: Optional[str] = None + ) -> Space: + """Gets a space. + + :param space_id: ID of the space that you want to get. + + :param space_key: Unique key of the space that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get_related( + self, + *, + exclude: Optional[List[str]] = None, + include: Optional[List[str]] = None, + space_ids: Optional[List[str]] = None, + space_keys: Optional[List[str]] = None, + ) -> Batch: + """Gets all related resources for one or more Spaces. + + :param exclude: + + :param include: + + :param space_ids: IDs of the spaces that you want to get along with their related resources. + + :param space_keys: Keys of the spaces that you want to get along with their related resources. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + customer_key: Optional[str] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + space_key: Optional[str] = None, + ) -> List[Space]: + """Returns a list of all spaces. + + :param customer_key: Customer key for which you want to list spaces. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned spaces to include all records that satisfy a partial match using ``name``, ``space_key``, or ``customer_key``. + + :param space_key: Filter spaces by space_key. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def remove_acs_entrances( + self, *, acs_entrance_ids: List[str], space_id: str + ) -> None: + """Removes `entrances `_ from a specific space. + + :param acs_entrance_ids: IDs of the entrances that you want to remove from the space. + + :param space_id: ID of the space from which you want to remove entrances. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def remove_connected_account( + self, *, connected_account_id: str, space_id: str + ) -> None: + """Removes a `connected account `_ from a specific space. + + :param connected_account_id: ID of the connected account that you want to remove from the space. + + :param space_id: ID of the space from which you want to remove the connected account. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def remove_devices(self, *, device_ids: List[str], space_id: str) -> None: + """Removes devices from a specific space. + + :param device_ids: IDs of the devices that you want to remove from the space. + + :param space_id: ID of the space from which you want to remove devices. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + acs_entrance_ids: Optional[List[str]] = None, + customer_data: Optional[Dict[str, Any]] = None, + device_ids: Optional[List[str]] = None, + name: Optional[str] = None, + space_id: Optional[str] = None, + space_key: Optional[str] = None, + ) -> Space: + """Updates an existing space. + + :param acs_entrance_ids: IDs of the entrances that you want to set for the space. If specified, this will replace all existing entrances. + + :param customer_data: Reservation/stay-related defaults for the space. Only the keys you provide are updated; omit a key to leave it unchanged. Pass null on a key to clear it. + + :param device_ids: IDs of the devices that you want to set for the space. If specified, this will replace all existing devices. + + :param name: Name of the space. + + :param space_id: ID of the space that you want to update. + + :param space_key: Unique key of the space that you want to update. + + :returns: OK""" + raise NotImplementedError() + + class Spaces(AbstractSpaces): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -637,3 +848,429 @@ def update( res = self.client.patch("/spaces/update", json=json_payload) return Space.from_dict(res["space"]) + + +class AsyncSpaces(AbstractAsyncSpaces): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/spaces/add_acs_entrances", + has_required_parameters=True, + has_pagination=False, + ) + async def add_acs_entrances( + self, *, acs_entrance_ids: List[str], space_id: str + ) -> None: + """Adds `entrances `_ to a specific space. + + :param acs_entrance_ids: IDs of the entrances that you want to add to the space. + + :param space_id: ID of the space to which you want to add entrances. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_entrance_ids is not None: + json_payload["acs_entrance_ids"] = acs_entrance_ids + if space_id is not None: + json_payload["space_id"] = space_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/add_acs_entrances" + ) + + await self.client.put("/spaces/add_acs_entrances", json=json_payload) + + return None + + @route_metadata( + path="/spaces/add_connected_account", + has_required_parameters=True, + has_pagination=False, + ) + async def add_connected_account( + self, *, connected_account_id: str, space_id: str + ) -> None: + """Adds a `connected account `_ to a specific space. + + :param connected_account_id: ID of the connected account that you want to add to the space. + + :param space_id: ID of the space to which you want to add the connected account. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if connected_account_id is not None: + json_payload["connected_account_id"] = connected_account_id + if space_id is not None: + json_payload["space_id"] = space_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/add_connected_account" + ) + + await self.client.put("/spaces/add_connected_account", json=json_payload) + + return None + + @route_metadata( + path="/spaces/add_devices", has_required_parameters=True, has_pagination=False + ) + async def add_devices(self, *, device_ids: List[str], space_id: str) -> None: + """Adds devices to a specific space. + + :param device_ids: IDs of the devices that you want to add to the space. + + :param space_id: ID of the space to which you want to add devices. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_ids is not None: + json_payload["device_ids"] = device_ids + if space_id is not None: + json_payload["space_id"] = space_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/add_devices" + ) + + await self.client.put("/spaces/add_devices", json=json_payload) + + return None + + @route_metadata( + path="/spaces/create", has_required_parameters=True, has_pagination=False + ) + async def create( + self, + *, + name: str, + acs_entrance_ids: Optional[List[str]] = None, + connected_account_ids: Optional[List[str]] = None, + customer_data: Optional[Dict[str, Any]] = None, + customer_key: Optional[str] = None, + device_ids: Optional[List[str]] = None, + space_key: Optional[str] = None, + ) -> Space: + """Creates a new space. + + :param name: Name of the space that you want to create. + + :param acs_entrance_ids: IDs of the entrances that you want to add to the new space. + + :param connected_account_ids: IDs of connected accounts to associate with the new space. Persisted on seam.location_third_party_account so the UI can show which provider account(s) a space came from. + + :param customer_data: Reservation/stay-related defaults for the space. + + :param customer_key: Customer key for which you want to create the space. + + :param device_ids: IDs of the devices that you want to add to the new space. + + :param space_key: Unique key for the space within the workspace. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if name is not None: + json_payload["name"] = name + if acs_entrance_ids is not None: + json_payload["acs_entrance_ids"] = acs_entrance_ids + if connected_account_ids is not None: + json_payload["connected_account_ids"] = connected_account_ids + if customer_data is not None: + json_payload["customer_data"] = customer_data + if customer_key is not None: + json_payload["customer_key"] = customer_key + if device_ids is not None: + json_payload["device_ids"] = device_ids + if space_key is not None: + json_payload["space_key"] = space_key + + if not json_payload: + raise ValueError("At least one parameter is required for /spaces/create") + + res = await self.client.post("/spaces/create", json=json_payload) + + return Space.from_dict(res["space"]) + + @route_metadata( + path="/spaces/delete", has_required_parameters=True, has_pagination=False + ) + async def delete(self, *, space_id: str) -> None: + """Deletes a space. + + :param space_id: ID of the space that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if space_id is not None: + params["space_id"] = space_id + + if not params: + raise ValueError("At least one parameter is required for /spaces/delete") + + await self.client.delete("/spaces/delete", params=params) + + return None + + @route_metadata( + path="/spaces/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, *, space_id: Optional[str] = None, space_key: Optional[str] = None + ) -> Space: + """Gets a space. + + :param space_id: ID of the space that you want to get. + + :param space_key: Unique key of the space that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if space_id is not None: + params["space_id"] = space_id + if space_key is not None: + params["space_key"] = space_key + + if not params: + raise ValueError("At least one parameter is required for /spaces/get") + + res = await self.client.get("/spaces/get", params=params) + + return Space.from_dict(res["space"]) + + @route_metadata( + path="/spaces/get_related", has_required_parameters=True, has_pagination=False + ) + async def get_related( + self, + *, + exclude: Optional[List[str]] = None, + include: Optional[List[str]] = None, + space_ids: Optional[List[str]] = None, + space_keys: Optional[List[str]] = None, + ) -> Batch: + """Gets all related resources for one or more Spaces. + + :param exclude: + + :param include: + + :param space_ids: IDs of the spaces that you want to get along with their related resources. + + :param space_keys: Keys of the spaces that you want to get along with their related resources. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if exclude is not None: + params["exclude"] = exclude + if include is not None: + params["include"] = include + if space_ids is not None: + params["space_ids"] = space_ids + if space_keys is not None: + params["space_keys"] = space_keys + + if not params: + raise ValueError( + "At least one parameter is required for /spaces/get_related" + ) + + res = await self.client.get("/spaces/get_related", params=params) + + return Batch.from_dict(res["batch"]) + + @route_metadata( + path="/spaces/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + customer_key: Optional[str] = None, + limit: Optional[float] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + space_key: Optional[str] = None, + ) -> List[Space]: + """Returns a list of all spaces. + + :param customer_key: Customer key for which you want to list spaces. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned spaces to include all records that satisfy a partial match using ``name``, ``space_key``, or ``customer_key``. + + :param space_key: Filter spaces by space_key. + + :returns: OK""" + params: Dict[str, Any] = {} + + if customer_key is not None: + params["customer_key"] = customer_key + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if space_key is not None: + params["space_key"] = space_key + + res = await self.client.get("/spaces/list", params=params) + + return [Space.from_dict(item) for item in res["spaces"]] + + @route_metadata( + path="/spaces/remove_acs_entrances", + has_required_parameters=True, + has_pagination=False, + ) + async def remove_acs_entrances( + self, *, acs_entrance_ids: List[str], space_id: str + ) -> None: + """Removes `entrances `_ from a specific space. + + :param acs_entrance_ids: IDs of the entrances that you want to remove from the space. + + :param space_id: ID of the space from which you want to remove entrances. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_entrance_ids is not None: + params["acs_entrance_ids"] = acs_entrance_ids + if space_id is not None: + params["space_id"] = space_id + + if not params: + raise ValueError( + "At least one parameter is required for /spaces/remove_acs_entrances" + ) + + await self.client.delete("/spaces/remove_acs_entrances", params=params) + + return None + + @route_metadata( + path="/spaces/remove_connected_account", + has_required_parameters=True, + has_pagination=False, + ) + async def remove_connected_account( + self, *, connected_account_id: str, space_id: str + ) -> None: + """Removes a `connected account `_ from a specific space. + + :param connected_account_id: ID of the connected account that you want to remove from the space. + + :param space_id: ID of the space from which you want to remove the connected account. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if space_id is not None: + params["space_id"] = space_id + + if not params: + raise ValueError( + "At least one parameter is required for /spaces/remove_connected_account" + ) + + await self.client.delete("/spaces/remove_connected_account", params=params) + + return None + + @route_metadata( + path="/spaces/remove_devices", + has_required_parameters=True, + has_pagination=False, + ) + async def remove_devices(self, *, device_ids: List[str], space_id: str) -> None: + """Removes devices from a specific space. + + :param device_ids: IDs of the devices that you want to remove from the space. + + :param space_id: ID of the space from which you want to remove devices. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_ids is not None: + params["device_ids"] = device_ids + if space_id is not None: + params["space_id"] = space_id + + if not params: + raise ValueError( + "At least one parameter is required for /spaces/remove_devices" + ) + + await self.client.delete("/spaces/remove_devices", params=params) + + return None + + @route_metadata( + path="/spaces/update", has_required_parameters=False, has_pagination=False + ) + async def update( + self, + *, + acs_entrance_ids: Optional[List[str]] = None, + customer_data: Optional[Dict[str, Any]] = None, + device_ids: Optional[List[str]] = None, + name: Optional[str] = None, + space_id: Optional[str] = None, + space_key: Optional[str] = None, + ) -> Space: + """Updates an existing space. + + :param acs_entrance_ids: IDs of the entrances that you want to set for the space. If specified, this will replace all existing entrances. + + :param customer_data: Reservation/stay-related defaults for the space. Only the keys you provide are updated; omit a key to leave it unchanged. Pass null on a key to clear it. + + :param device_ids: IDs of the devices that you want to set for the space. If specified, this will replace all existing devices. + + :param name: Name of the space. + + :param space_id: ID of the space that you want to update. + + :param space_key: Unique key of the space that you want to update. + + :returns: OK""" + json_payload: Dict[str, Any] = {} + + if acs_entrance_ids is not None: + json_payload["acs_entrance_ids"] = acs_entrance_ids + if customer_data is not None: + json_payload["customer_data"] = customer_data + if device_ids is not None: + json_payload["device_ids"] = device_ids + if name is not None: + json_payload["name"] = name + if space_id is not None: + json_payload["space_id"] = space_id + if space_key is not None: + json_payload["space_key"] = space_key + + res = await self.client.patch("/spaces/update", json=json_payload) + + return Space.from_dict(res["space"]) diff --git a/seam/routes/thermostats.py b/seam/routes/thermostats.py index 4110b1cb..a0574006 100644 --- a/seam/routes/thermostats.py +++ b/seam/routes/thermostats.py @@ -1,16 +1,31 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import ActionAttempt, Device from .thermostats_daily_programs import ( AbstractThermostatsDailyPrograms, ThermostatsDailyPrograms, + AbstractAsyncThermostatsDailyPrograms, + AsyncThermostatsDailyPrograms, +) +from .thermostats_schedules import ( + AbstractThermostatsSchedules, + ThermostatsSchedules, + AbstractAsyncThermostatsSchedules, + AsyncThermostatsSchedules, +) +from .thermostats_simulate import ( + AbstractThermostatsSimulate, + ThermostatsSimulate, + AbstractAsyncThermostatsSimulate, + AsyncThermostatsSimulate, +) +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, ) -from .thermostats_schedules import AbstractThermostatsSchedules, ThermostatsSchedules -from .thermostats_simulate import AbstractThermostatsSimulate, ThermostatsSimulate -from ..modules.action_attempts import resolve_action_attempt class AbstractThermostats(abc.ABC): @@ -243,9 +258,1051 @@ def set_fallback_climate_preset( :param device_id: ID of the thermostat device for which you want to set the fallback climate preset. :raises ValueError: At least one parameter must be provided.""" - raise NotImplementedError() + raise NotImplementedError() + + @abc.abstractmethod + def set_fan_mode( + self, + *, + device_id: str, + fan_mode: Optional[str] = None, + fan_mode_setting: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets the `fan mode setting `_ for a specified `thermostat `_. + + :param device_id: ID of the thermostat device for which you want to set the fan mode. + + :param fan_mode: Deprecated: Use ``fan_mode_setting`` instead. Fan mode setting for the thermostat, such as ``auto``, ``on``, or ``circulate``. + + :param fan_mode_setting: `Fan mode setting `_ that you want to set for the thermostat. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + def set_hvac_mode( + self, + *, + device_id: str, + hvac_mode_setting: str, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets the `HVAC mode `_ for a specified `thermostat `_. + + :param device_id: ID of the thermostat device for which you want to set the HVAC mode. + + :param hvac_mode_setting: + + :param cooling_set_point_celsius: `Cooling set point `_ in °C that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param cooling_set_point_fahrenheit: `Cooling set point `_ in °F that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param heating_set_point_celsius: `Heating set point `_ in °C that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param heating_set_point_fahrenheit: `Heating set point `_ in °F that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + def set_temperature_threshold( + self, + *, + device_id: str, + lower_limit_celsius: Optional[Union[float, Null]] = None, + lower_limit_fahrenheit: Optional[Union[float, Null]] = None, + upper_limit_celsius: Optional[Union[float, Null]] = None, + upper_limit_fahrenheit: Optional[Union[float, Null]] = None, + ) -> None: + """Sets a `temperature threshold `_ for a specified thermostat. Seam emits a ``thermostat.temperature_threshold_exceeded`` event and adds a warning on a thermostat if it reports a temperature outside the threshold range. + + :param device_id: ID of the thermostat device for which you want to set a temperature threshold. + + :param lower_limit_celsius: Lower temperature limit in in °C. Seam alerts you if the reported temperature is lower than this value. You can specify either ``lower_limit`` but not both. + + :param lower_limit_fahrenheit: Lower temperature limit in in °F. Seam alerts you if the reported temperature is lower than this value. You can specify either ``lower_limit`` but not both. + + :param upper_limit_celsius: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. + + :param upper_limit_fahrenheit: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + def update_climate_preset( + self, + *, + climate_preset_key: str, + device_id: str, + climate_preset_mode: Optional[str] = None, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + ecobee_metadata: Optional[Dict[str, Any]] = None, + fan_mode_setting: Optional[str] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + hvac_mode_setting: Optional[str] = None, + manual_override_allowed: Optional[bool] = None, + name: Optional[Union[str, Null]] = None, + ) -> None: + """Updates a specified `climate preset `_ for a specified `thermostat `_. + + :param climate_preset_key: Unique key to identify the `climate preset `_. + + :param device_id: ID of the thermostat device for which you want to update a climate preset. + + :param climate_preset_mode: The climate preset mode for the thermostat, based on the available climate preset modes reported by the device. + + :param cooling_set_point_celsius: Temperature to which the thermostat should cool (in °C). See also `Set Points `_. + + :param cooling_set_point_fahrenheit: Temperature to which the thermostat should cool (in °F). See also `Set Points `_. + + :param ecobee_metadata: Metadata specific to the Ecobee climate, if applicable. + + :param fan_mode_setting: Desired `fan mode setting `_, such as ``on``, ``auto``, or ``circulate``. + + :param heating_set_point_celsius: Temperature to which the thermostat should heat (in °C). See also `Set Points `_. + + :param heating_set_point_fahrenheit: Temperature to which the thermostat should heat (in °F). See also `Set Points `_. + + :param hvac_mode_setting: Desired `HVAC mode `_ setting, such as ``heat``, ``cool``, ``heat_cool``, or ``off``. + + :param manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat can change the thermostat's settings. See `Specifying Manual Override Permissions `_. + + :param name: User-friendly name to identify the `climate preset `_. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + def update_weekly_program( + self, + *, + device_id: str, + friday_program_id: Optional[Union[str, Null]] = None, + monday_program_id: Optional[Union[str, Null]] = None, + saturday_program_id: Optional[Union[str, Null]] = None, + sunday_program_id: Optional[Union[str, Null]] = None, + thursday_program_id: Optional[Union[str, Null]] = None, + tuesday_program_id: Optional[Union[str, Null]] = None, + wednesday_program_id: Optional[Union[str, Null]] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Updates the thermostat weekly program for a thermostat device. To configure a weekly program, specify the ID of the daily program that you want to use for each day of the week. When you update a weekly program, the set of programs that you specify overwrites any previous weekly program for the thermostat. + + :param device_id: ID of the thermostat device for which you want to update the weekly program. + + :param friday_program_id: ID of the thermostat daily program to run on Fridays. + + :param monday_program_id: ID of the thermostat daily program to run on Mondays. + + :param saturday_program_id: ID of the thermostat daily program to run on Saturdays. + + :param sunday_program_id: ID of the thermostat daily program to run on Sundays. + + :param thursday_program_id: ID of the thermostat daily program to run on Thursdays. + + :param tuesday_program_id: ID of the thermostat daily program to run on Tuesdays. + + :param wednesday_program_id: ID of the thermostat daily program to run on Wednesdays. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + +class AbstractAsyncThermostats(abc.ABC): + + @property + @abc.abstractmethod + def daily_programs(self) -> AbstractAsyncThermostatsDailyPrograms: + raise NotImplementedError() + + @property + @abc.abstractmethod + def schedules(self) -> AbstractAsyncThermostatsSchedules: + raise NotImplementedError() + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAsyncThermostatsSimulate: + raise NotImplementedError() + + @abc.abstractmethod + async def activate_climate_preset( + self, + *, + climate_preset_key: str, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Activates a specified `climate preset `_ for a specified `thermostat `_. + + :param climate_preset_key: Climate preset key of the climate preset that you want to activate. + + :param device_id: ID of the thermostat device for which you want to activate a climate preset. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def cool( + self, + *, + device_id: str, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets a specified `thermostat `_ to `cool mode `_. + + :param device_id: ID of the thermostat device that you want to set to cool mode. + + :param cooling_set_point_celsius: `Cooling set point `_ in °C that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param cooling_set_point_fahrenheit: `Cooling set point `_ in °F that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def create_climate_preset( + self, + *, + climate_preset_key: str, + device_id: str, + climate_preset_mode: Optional[str] = None, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + ecobee_metadata: Optional[Dict[str, Any]] = None, + fan_mode_setting: Optional[str] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + hvac_mode_setting: Optional[str] = None, + manual_override_allowed: Optional[bool] = None, + name: Optional[Union[str, Null]] = None, + ) -> None: + """Creates a `climate preset `_ for a specified `thermostat `_. + + :param climate_preset_key: Unique key to identify the `climate preset `_. + + :param device_id: ID of the thermostat device for which you want create a climate preset. + + :param climate_preset_mode: The climate preset mode for the thermostat, based on the available climate preset modes reported by the device. + + :param cooling_set_point_celsius: Temperature to which the thermostat should cool (in °C). See also `Set Points `_. + + :param cooling_set_point_fahrenheit: Temperature to which the thermostat should cool (in °F). See also `Set Points `_. + + :param ecobee_metadata: Metadata specific to the Ecobee climate, if applicable. + + :param fan_mode_setting: Desired `fan mode setting `_, such as ``on``, ``auto``, or ``circulate``. + + :param heating_set_point_celsius: Temperature to which the thermostat should heat (in °C). See also `Set Points `_. + + :param heating_set_point_fahrenheit: Temperature to which the thermostat should heat (in °F). See also `Set Points `_. + + :param hvac_mode_setting: Desired `HVAC mode `_ setting, such as ``heat``, ``cool``, ``heat_cool``, or ``off``. + + :param manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat or using the API can change the thermostat's settings. + + :param name: User-friendly name to identify the `climate preset `_. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete_climate_preset( + self, *, climate_preset_key: str, device_id: str + ) -> None: + """Deletes a specified `climate preset `_ for a specified `thermostat `_. + + :param climate_preset_key: Climate preset key of the climate preset that you want to delete. + + :param device_id: ID of the thermostat device for which you want to delete a climate preset. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def heat( + self, + *, + device_id: str, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets a specified `thermostat `_ to `heat mode `_. + + :param device_id: ID of the thermostat device that you want to set to heat mode. + + :param heating_set_point_celsius: `Heating set point `_ in °C that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param heating_set_point_fahrenheit: `Heating set point `_ in °F that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def heat_cool( + self, + *, + device_id: str, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets a specified `thermostat `_ to `heat-cool ("auto") mode `_. + + :param device_id: ID of the thermostat device that you want to set to heat-cool mode. + + :param cooling_set_point_celsius: `Cooling set point `_ in °C that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param cooling_set_point_fahrenheit: `Cooling set point `_ in °F that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param heating_set_point_celsius: `Heating set point `_ in °C that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param heating_set_point_fahrenheit: `Heating set point `_ in °F that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + manufacturer: Optional[str] = None, + ) -> List[Device]: + """Returns a list of all `thermostats `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param customer_key: Customer key for which you want to list devices. + + :param device_type: Device type by which you want to filter thermostat devices. + + :param device_types: Array of device types by which you want to filter thermostat devices. + + :param manufacturer: Manufacturer by which you want to filter thermostat devices. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def off( + self, + *, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets a specified `thermostat `_ to `"off" mode `_. + + :param device_id: ID of the thermostat device that you want to set to off mode. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def set_fallback_climate_preset( + self, *, climate_preset_key: str, device_id: str + ) -> None: + """Sets a specified `climate preset `_ as the `"fallback" `_ preset for a specified `thermostat `_. + + :param climate_preset_key: Climate preset key of the climate preset that you want to set as the fallback climate preset. + + :param device_id: ID of the thermostat device for which you want to set the fallback climate preset. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def set_fan_mode( + self, + *, + device_id: str, + fan_mode: Optional[str] = None, + fan_mode_setting: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets the `fan mode setting `_ for a specified `thermostat `_. + + :param device_id: ID of the thermostat device for which you want to set the fan mode. + + :param fan_mode: Deprecated: Use ``fan_mode_setting`` instead. Fan mode setting for the thermostat, such as ``auto``, ``on``, or ``circulate``. + + :param fan_mode_setting: `Fan mode setting `_ that you want to set for the thermostat. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def set_hvac_mode( + self, + *, + device_id: str, + hvac_mode_setting: str, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets the `HVAC mode `_ for a specified `thermostat `_. + + :param device_id: ID of the thermostat device for which you want to set the HVAC mode. + + :param hvac_mode_setting: + + :param cooling_set_point_celsius: `Cooling set point `_ in °C that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param cooling_set_point_fahrenheit: `Cooling set point `_ in °F that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param heating_set_point_celsius: `Heating set point `_ in °C that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param heating_set_point_fahrenheit: `Heating set point `_ in °F that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def set_temperature_threshold( + self, + *, + device_id: str, + lower_limit_celsius: Optional[Union[float, Null]] = None, + lower_limit_fahrenheit: Optional[Union[float, Null]] = None, + upper_limit_celsius: Optional[Union[float, Null]] = None, + upper_limit_fahrenheit: Optional[Union[float, Null]] = None, + ) -> None: + """Sets a `temperature threshold `_ for a specified thermostat. Seam emits a ``thermostat.temperature_threshold_exceeded`` event and adds a warning on a thermostat if it reports a temperature outside the threshold range. + + :param device_id: ID of the thermostat device for which you want to set a temperature threshold. + + :param lower_limit_celsius: Lower temperature limit in in °C. Seam alerts you if the reported temperature is lower than this value. You can specify either ``lower_limit`` but not both. + + :param lower_limit_fahrenheit: Lower temperature limit in in °F. Seam alerts you if the reported temperature is lower than this value. You can specify either ``lower_limit`` but not both. + + :param upper_limit_celsius: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. + + :param upper_limit_fahrenheit: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update_climate_preset( + self, + *, + climate_preset_key: str, + device_id: str, + climate_preset_mode: Optional[str] = None, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + ecobee_metadata: Optional[Dict[str, Any]] = None, + fan_mode_setting: Optional[str] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + hvac_mode_setting: Optional[str] = None, + manual_override_allowed: Optional[bool] = None, + name: Optional[Union[str, Null]] = None, + ) -> None: + """Updates a specified `climate preset `_ for a specified `thermostat `_. + + :param climate_preset_key: Unique key to identify the `climate preset `_. + + :param device_id: ID of the thermostat device for which you want to update a climate preset. + + :param climate_preset_mode: The climate preset mode for the thermostat, based on the available climate preset modes reported by the device. + + :param cooling_set_point_celsius: Temperature to which the thermostat should cool (in °C). See also `Set Points `_. + + :param cooling_set_point_fahrenheit: Temperature to which the thermostat should cool (in °F). See also `Set Points `_. + + :param ecobee_metadata: Metadata specific to the Ecobee climate, if applicable. + + :param fan_mode_setting: Desired `fan mode setting `_, such as ``on``, ``auto``, or ``circulate``. + + :param heating_set_point_celsius: Temperature to which the thermostat should heat (in °C). See also `Set Points `_. + + :param heating_set_point_fahrenheit: Temperature to which the thermostat should heat (in °F). See also `Set Points `_. + + :param hvac_mode_setting: Desired `HVAC mode `_ setting, such as ``heat``, ``cool``, ``heat_cool``, or ``off``. + + :param manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat can change the thermostat's settings. See `Specifying Manual Override Permissions `_. + + :param name: User-friendly name to identify the `climate preset `_. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update_weekly_program( + self, + *, + device_id: str, + friday_program_id: Optional[Union[str, Null]] = None, + monday_program_id: Optional[Union[str, Null]] = None, + saturday_program_id: Optional[Union[str, Null]] = None, + sunday_program_id: Optional[Union[str, Null]] = None, + thursday_program_id: Optional[Union[str, Null]] = None, + tuesday_program_id: Optional[Union[str, Null]] = None, + wednesday_program_id: Optional[Union[str, Null]] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Updates the thermostat weekly program for a thermostat device. To configure a weekly program, specify the ID of the daily program that you want to use for each day of the week. When you update a weekly program, the set of programs that you specify overwrites any previous weekly program for the thermostat. + + :param device_id: ID of the thermostat device for which you want to update the weekly program. + + :param friday_program_id: ID of the thermostat daily program to run on Fridays. + + :param monday_program_id: ID of the thermostat daily program to run on Mondays. + + :param saturday_program_id: ID of the thermostat daily program to run on Saturdays. + + :param sunday_program_id: ID of the thermostat daily program to run on Sundays. + + :param thursday_program_id: ID of the thermostat daily program to run on Thursdays. + + :param tuesday_program_id: ID of the thermostat daily program to run on Tuesdays. + + :param wednesday_program_id: ID of the thermostat daily program to run on Wednesdays. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + +class Thermostats(AbstractThermostats): + def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._daily_programs = ThermostatsDailyPrograms( + client=client, defaults=defaults + ) + self._schedules = ThermostatsSchedules(client=client, defaults=defaults) + self._simulate = ThermostatsSimulate(client=client, defaults=defaults) + + @property + def daily_programs(self) -> ThermostatsDailyPrograms: + return self._daily_programs + + @property + def schedules(self) -> ThermostatsSchedules: + return self._schedules + + @property + def simulate(self) -> ThermostatsSimulate: + return self._simulate + + @route_metadata( + path="/thermostats/activate_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) + def activate_climate_preset( + self, + *, + climate_preset_key: str, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Activates a specified `climate preset `_ for a specified `thermostat `_. + + :param climate_preset_key: Climate preset key of the climate preset that you want to activate. + + :param device_id: ID of the thermostat device for which you want to activate a climate preset. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if climate_preset_key is not None: + json_payload["climate_preset_key"] = climate_preset_key + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/activate_climate_preset" + ) + + res = self.client.post( + "/thermostats/activate_climate_preset", json=json_payload + ) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return resolve_action_attempt( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/thermostats/cool", has_required_parameters=True, has_pagination=False + ) + def cool( + self, + *, + device_id: str, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets a specified `thermostat `_ to `cool mode `_. + + :param device_id: ID of the thermostat device that you want to set to cool mode. + + :param cooling_set_point_celsius: `Cooling set point `_ in °C that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param cooling_set_point_fahrenheit: `Cooling set point `_ in °F that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if cooling_set_point_celsius is not None: + json_payload["cooling_set_point_celsius"] = cooling_set_point_celsius + if cooling_set_point_fahrenheit is not None: + json_payload["cooling_set_point_fahrenheit"] = cooling_set_point_fahrenheit + + if not json_payload: + raise ValueError("At least one parameter is required for /thermostats/cool") + + res = self.client.post("/thermostats/cool", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return resolve_action_attempt( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/thermostats/create_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) + def create_climate_preset( + self, + *, + climate_preset_key: str, + device_id: str, + climate_preset_mode: Optional[str] = None, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + ecobee_metadata: Optional[Dict[str, Any]] = None, + fan_mode_setting: Optional[str] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + hvac_mode_setting: Optional[str] = None, + manual_override_allowed: Optional[bool] = None, + name: Optional[Union[str, Null]] = None, + ) -> None: + """Creates a `climate preset `_ for a specified `thermostat `_. + + :param climate_preset_key: Unique key to identify the `climate preset `_. + + :param device_id: ID of the thermostat device for which you want create a climate preset. + + :param climate_preset_mode: The climate preset mode for the thermostat, based on the available climate preset modes reported by the device. + + :param cooling_set_point_celsius: Temperature to which the thermostat should cool (in °C). See also `Set Points `_. + + :param cooling_set_point_fahrenheit: Temperature to which the thermostat should cool (in °F). See also `Set Points `_. + + :param ecobee_metadata: Metadata specific to the Ecobee climate, if applicable. + + :param fan_mode_setting: Desired `fan mode setting `_, such as ``on``, ``auto``, or ``circulate``. + + :param heating_set_point_celsius: Temperature to which the thermostat should heat (in °C). See also `Set Points `_. + + :param heating_set_point_fahrenheit: Temperature to which the thermostat should heat (in °F). See also `Set Points `_. + + :param hvac_mode_setting: Desired `HVAC mode `_ setting, such as ``heat``, ``cool``, ``heat_cool``, or ``off``. + + :param manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat or using the API can change the thermostat's settings. + + :param name: User-friendly name to identify the `climate preset `_. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if climate_preset_key is not None: + json_payload["climate_preset_key"] = climate_preset_key + if device_id is not None: + json_payload["device_id"] = device_id + if climate_preset_mode is not None: + json_payload["climate_preset_mode"] = climate_preset_mode + if cooling_set_point_celsius is not None: + json_payload["cooling_set_point_celsius"] = cooling_set_point_celsius + if cooling_set_point_fahrenheit is not None: + json_payload["cooling_set_point_fahrenheit"] = cooling_set_point_fahrenheit + if ecobee_metadata is not None: + json_payload["ecobee_metadata"] = ecobee_metadata + if fan_mode_setting is not None: + json_payload["fan_mode_setting"] = fan_mode_setting + if heating_set_point_celsius is not None: + json_payload["heating_set_point_celsius"] = heating_set_point_celsius + if heating_set_point_fahrenheit is not None: + json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + if hvac_mode_setting is not None: + json_payload["hvac_mode_setting"] = hvac_mode_setting + if manual_override_allowed is not None: + json_payload["manual_override_allowed"] = manual_override_allowed + if name is not None: + json_payload["name"] = name + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/create_climate_preset" + ) + + self.client.post("/thermostats/create_climate_preset", json=json_payload) + + return None + + @route_metadata( + path="/thermostats/delete_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) + def delete_climate_preset(self, *, climate_preset_key: str, device_id: str) -> None: + """Deletes a specified `climate preset `_ for a specified `thermostat `_. + + :param climate_preset_key: Climate preset key of the climate preset that you want to delete. + + :param device_id: ID of the thermostat device for which you want to delete a climate preset. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if climate_preset_key is not None: + params["climate_preset_key"] = climate_preset_key + if device_id is not None: + params["device_id"] = device_id + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/delete_climate_preset" + ) + + self.client.delete("/thermostats/delete_climate_preset", params=params) + + return None + + @route_metadata( + path="/thermostats/heat", has_required_parameters=True, has_pagination=False + ) + def heat( + self, + *, + device_id: str, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets a specified `thermostat `_ to `heat mode `_. + + :param device_id: ID of the thermostat device that you want to set to heat mode. + + :param heating_set_point_celsius: `Heating set point `_ in °C that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param heating_set_point_fahrenheit: `Heating set point `_ in °F that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if heating_set_point_celsius is not None: + json_payload["heating_set_point_celsius"] = heating_set_point_celsius + if heating_set_point_fahrenheit is not None: + json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + + if not json_payload: + raise ValueError("At least one parameter is required for /thermostats/heat") + + res = self.client.post("/thermostats/heat", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return resolve_action_attempt( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/thermostats/heat_cool", + has_required_parameters=True, + has_pagination=False, + ) + def heat_cool( + self, + *, + device_id: str, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets a specified `thermostat `_ to `heat-cool ("auto") mode `_. + + :param device_id: ID of the thermostat device that you want to set to heat-cool mode. + + :param cooling_set_point_celsius: `Cooling set point `_ in °C that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param cooling_set_point_fahrenheit: `Cooling set point `_ in °F that you want to set for the thermostat. You must set one of the ``cooling_set_point`` parameters. + + :param heating_set_point_celsius: `Heating set point `_ in °C that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param heating_set_point_fahrenheit: `Heating set point `_ in °F that you want to set for the thermostat. You must set one of the ``heating_set_point`` parameters. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if cooling_set_point_celsius is not None: + json_payload["cooling_set_point_celsius"] = cooling_set_point_celsius + if cooling_set_point_fahrenheit is not None: + json_payload["cooling_set_point_fahrenheit"] = cooling_set_point_fahrenheit + if heating_set_point_celsius is not None: + json_payload["heating_set_point_celsius"] = heating_set_point_celsius + if heating_set_point_fahrenheit is not None: + json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/heat_cool" + ) + + res = self.client.post("/thermostats/heat_cool", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return resolve_action_attempt( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/thermostats/list", has_required_parameters=False, has_pagination=False + ) + def list( + self, + *, + connect_webview_id: Optional[str] = None, + connected_account_id: Optional[str] = None, + customer_key: Optional[str] = None, + device_type: Optional[str] = None, + device_types: Optional[List[str]] = None, + manufacturer: Optional[str] = None, + ) -> List[Device]: + """Returns a list of all `thermostats `_. + + :param connect_webview_id: ID of the Connect Webview for which you want to list devices. + + :param connected_account_id: ID of the connected account for which you want to list devices. + + :param customer_key: Customer key for which you want to list devices. + + :param device_type: Device type by which you want to filter thermostat devices. + + :param device_types: Array of device types by which you want to filter thermostat devices. + + :param manufacturer: Manufacturer by which you want to filter thermostat devices. + + :returns: OK""" + params: Dict[str, Any] = {} + + if connect_webview_id is not None: + params["connect_webview_id"] = connect_webview_id + if connected_account_id is not None: + params["connected_account_id"] = connected_account_id + if customer_key is not None: + params["customer_key"] = customer_key + if device_type is not None: + params["device_type"] = device_type + if device_types is not None: + params["device_types"] = device_types + if manufacturer is not None: + params["manufacturer"] = manufacturer + + res = self.client.get("/thermostats/list", params=params) + + return [Device.from_dict(item) for item in res["devices"]] + + @route_metadata( + path="/thermostats/off", has_required_parameters=True, has_pagination=False + ) + def off( + self, + *, + device_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Sets a specified `thermostat `_ to `"off" mode `_. + + :param device_id: ID of the thermostat device that you want to set to off mode. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError("At least one parameter is required for /thermostats/off") + + res = self.client.post("/thermostats/off", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return resolve_action_attempt( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/thermostats/set_fallback_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) + def set_fallback_climate_preset( + self, *, climate_preset_key: str, device_id: str + ) -> None: + """Sets a specified `climate preset `_ as the `"fallback" `_ preset for a specified `thermostat `_. + + :param climate_preset_key: Climate preset key of the climate preset that you want to set as the fallback climate preset. + + :param device_id: ID of the thermostat device for which you want to set the fallback climate preset. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} - @abc.abstractmethod + if climate_preset_key is not None: + json_payload["climate_preset_key"] = climate_preset_key + if device_id is not None: + json_payload["device_id"] = device_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/set_fallback_climate_preset" + ) + + self.client.post("/thermostats/set_fallback_climate_preset", json=json_payload) + + return None + + @route_metadata( + path="/thermostats/set_fan_mode", + has_required_parameters=True, + has_pagination=False, + ) def set_fan_mode( self, *, @@ -267,9 +1324,39 @@ def set_fan_mode( :returns: OK :raises ValueError: At least one parameter must be provided.""" - raise NotImplementedError() + json_payload: Dict[str, Any] = {} - @abc.abstractmethod + if device_id is not None: + json_payload["device_id"] = device_id + if fan_mode is not None: + json_payload["fan_mode"] = fan_mode + if fan_mode_setting is not None: + json_payload["fan_mode_setting"] = fan_mode_setting + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/set_fan_mode" + ) + + res = self.client.post("/thermostats/set_fan_mode", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return resolve_action_attempt( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/thermostats/set_hvac_mode", + has_required_parameters=True, + has_pagination=False, + ) def set_hvac_mode( self, *, @@ -300,9 +1387,45 @@ def set_hvac_mode( :returns: OK :raises ValueError: At least one parameter must be provided.""" - raise NotImplementedError() + json_payload: Dict[str, Any] = {} - @abc.abstractmethod + if device_id is not None: + json_payload["device_id"] = device_id + if hvac_mode_setting is not None: + json_payload["hvac_mode_setting"] = hvac_mode_setting + if cooling_set_point_celsius is not None: + json_payload["cooling_set_point_celsius"] = cooling_set_point_celsius + if cooling_set_point_fahrenheit is not None: + json_payload["cooling_set_point_fahrenheit"] = cooling_set_point_fahrenheit + if heating_set_point_celsius is not None: + json_payload["heating_set_point_celsius"] = heating_set_point_celsius + if heating_set_point_fahrenheit is not None: + json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/set_hvac_mode" + ) + + res = self.client.post("/thermostats/set_hvac_mode", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return resolve_action_attempt( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/thermostats/set_temperature_threshold", + has_required_parameters=True, + has_pagination=False, + ) def set_temperature_threshold( self, *, @@ -325,9 +1448,33 @@ def set_temperature_threshold( :param upper_limit_fahrenheit: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. :raises ValueError: At least one parameter must be provided.""" - raise NotImplementedError() + json_payload: Dict[str, Any] = {} - @abc.abstractmethod + if device_id is not None: + json_payload["device_id"] = device_id + if lower_limit_celsius is not None: + json_payload["lower_limit_celsius"] = lower_limit_celsius + if lower_limit_fahrenheit is not None: + json_payload["lower_limit_fahrenheit"] = lower_limit_fahrenheit + if upper_limit_celsius is not None: + json_payload["upper_limit_celsius"] = upper_limit_celsius + if upper_limit_fahrenheit is not None: + json_payload["upper_limit_fahrenheit"] = upper_limit_fahrenheit + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/set_temperature_threshold" + ) + + self.client.patch("/thermostats/set_temperature_threshold", json=json_payload) + + return None + + @route_metadata( + path="/thermostats/update_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) def update_climate_preset( self, *, @@ -371,9 +1518,47 @@ def update_climate_preset( :param name: User-friendly name to identify the `climate preset `_. :raises ValueError: At least one parameter must be provided.""" - raise NotImplementedError() + json_payload: Dict[str, Any] = {} - @abc.abstractmethod + if climate_preset_key is not None: + json_payload["climate_preset_key"] = climate_preset_key + if device_id is not None: + json_payload["device_id"] = device_id + if climate_preset_mode is not None: + json_payload["climate_preset_mode"] = climate_preset_mode + if cooling_set_point_celsius is not None: + json_payload["cooling_set_point_celsius"] = cooling_set_point_celsius + if cooling_set_point_fahrenheit is not None: + json_payload["cooling_set_point_fahrenheit"] = cooling_set_point_fahrenheit + if ecobee_metadata is not None: + json_payload["ecobee_metadata"] = ecobee_metadata + if fan_mode_setting is not None: + json_payload["fan_mode_setting"] = fan_mode_setting + if heating_set_point_celsius is not None: + json_payload["heating_set_point_celsius"] = heating_set_point_celsius + if heating_set_point_fahrenheit is not None: + json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + if hvac_mode_setting is not None: + json_payload["hvac_mode_setting"] = hvac_mode_setting + if manual_override_allowed is not None: + json_payload["manual_override_allowed"] = manual_override_allowed + if name is not None: + json_payload["name"] = name + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/update_climate_preset" + ) + + self.client.patch("/thermostats/update_climate_preset", json=json_payload) + + return None + + @route_metadata( + path="/thermostats/update_weekly_program", + has_required_parameters=True, + has_pagination=False, + ) def update_weekly_program( self, *, @@ -410,29 +1595,65 @@ def update_weekly_program( :returns: OK :raises ValueError: At least one parameter must be provided.""" - raise NotImplementedError() + json_payload: Dict[str, Any] = {} + if device_id is not None: + json_payload["device_id"] = device_id + if friday_program_id is not None: + json_payload["friday_program_id"] = friday_program_id + if monday_program_id is not None: + json_payload["monday_program_id"] = monday_program_id + if saturday_program_id is not None: + json_payload["saturday_program_id"] = saturday_program_id + if sunday_program_id is not None: + json_payload["sunday_program_id"] = sunday_program_id + if thursday_program_id is not None: + json_payload["thursday_program_id"] = thursday_program_id + if tuesday_program_id is not None: + json_payload["tuesday_program_id"] = tuesday_program_id + if wednesday_program_id is not None: + json_payload["wednesday_program_id"] = wednesday_program_id -class Thermostats(AbstractThermostats): - def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/update_weekly_program" + ) + + res = self.client.post("/thermostats/update_weekly_program", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return resolve_action_attempt( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + +class AsyncThermostats(AbstractAsyncThermostats): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults - self._daily_programs = ThermostatsDailyPrograms( + self._daily_programs = AsyncThermostatsDailyPrograms( client=client, defaults=defaults ) - self._schedules = ThermostatsSchedules(client=client, defaults=defaults) - self._simulate = ThermostatsSimulate(client=client, defaults=defaults) + self._schedules = AsyncThermostatsSchedules(client=client, defaults=defaults) + self._simulate = AsyncThermostatsSimulate(client=client, defaults=defaults) @property - def daily_programs(self) -> ThermostatsDailyPrograms: + def daily_programs(self) -> AsyncThermostatsDailyPrograms: return self._daily_programs @property - def schedules(self) -> ThermostatsSchedules: + def schedules(self) -> AsyncThermostatsSchedules: return self._schedules @property - def simulate(self) -> ThermostatsSimulate: + def simulate(self) -> AsyncThermostatsSimulate: return self._simulate @route_metadata( @@ -440,7 +1661,7 @@ def simulate(self) -> ThermostatsSimulate: has_required_parameters=True, has_pagination=False, ) - def activate_climate_preset( + async def activate_climate_preset( self, *, climate_preset_key: str, @@ -470,7 +1691,7 @@ def activate_climate_preset( "At least one parameter is required for /thermostats/activate_climate_preset" ) - res = self.client.post( + res = await self.client.post( "/thermostats/activate_climate_preset", json=json_payload ) @@ -480,7 +1701,7 @@ def activate_climate_preset( else wait_for_action_attempt ) - return resolve_action_attempt( + return await resolve_action_attempt_async( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, @@ -489,7 +1710,7 @@ def activate_climate_preset( @route_metadata( path="/thermostats/cool", has_required_parameters=True, has_pagination=False ) - def cool( + async def cool( self, *, device_id: str, @@ -522,7 +1743,7 @@ def cool( if not json_payload: raise ValueError("At least one parameter is required for /thermostats/cool") - res = self.client.post("/thermostats/cool", json=json_payload) + res = await self.client.post("/thermostats/cool", json=json_payload) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -530,7 +1751,7 @@ def cool( else wait_for_action_attempt ) - return resolve_action_attempt( + return await resolve_action_attempt_async( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, @@ -541,7 +1762,7 @@ def cool( has_required_parameters=True, has_pagination=False, ) - def create_climate_preset( + async def create_climate_preset( self, *, climate_preset_key: str, @@ -616,7 +1837,7 @@ def create_climate_preset( "At least one parameter is required for /thermostats/create_climate_preset" ) - self.client.post("/thermostats/create_climate_preset", json=json_payload) + await self.client.post("/thermostats/create_climate_preset", json=json_payload) return None @@ -625,7 +1846,9 @@ def create_climate_preset( has_required_parameters=True, has_pagination=False, ) - def delete_climate_preset(self, *, climate_preset_key: str, device_id: str) -> None: + async def delete_climate_preset( + self, *, climate_preset_key: str, device_id: str + ) -> None: """Deletes a specified `climate preset `_ for a specified `thermostat `_. :param climate_preset_key: Climate preset key of the climate preset that you want to delete. @@ -645,14 +1868,14 @@ def delete_climate_preset(self, *, climate_preset_key: str, device_id: str) -> N "At least one parameter is required for /thermostats/delete_climate_preset" ) - self.client.delete("/thermostats/delete_climate_preset", params=params) + await self.client.delete("/thermostats/delete_climate_preset", params=params) return None @route_metadata( path="/thermostats/heat", has_required_parameters=True, has_pagination=False ) - def heat( + async def heat( self, *, device_id: str, @@ -685,7 +1908,7 @@ def heat( if not json_payload: raise ValueError("At least one parameter is required for /thermostats/heat") - res = self.client.post("/thermostats/heat", json=json_payload) + res = await self.client.post("/thermostats/heat", json=json_payload) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -693,7 +1916,7 @@ def heat( else wait_for_action_attempt ) - return resolve_action_attempt( + return await resolve_action_attempt_async( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, @@ -704,7 +1927,7 @@ def heat( has_required_parameters=True, has_pagination=False, ) - def heat_cool( + async def heat_cool( self, *, device_id: str, @@ -749,7 +1972,7 @@ def heat_cool( "At least one parameter is required for /thermostats/heat_cool" ) - res = self.client.post("/thermostats/heat_cool", json=json_payload) + res = await self.client.post("/thermostats/heat_cool", json=json_payload) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -757,7 +1980,7 @@ def heat_cool( else wait_for_action_attempt ) - return resolve_action_attempt( + return await resolve_action_attempt_async( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, @@ -766,7 +1989,7 @@ def heat_cool( @route_metadata( path="/thermostats/list", has_required_parameters=False, has_pagination=False ) - def list( + async def list( self, *, connect_webview_id: Optional[str] = None, @@ -806,14 +2029,14 @@ def list( if manufacturer is not None: params["manufacturer"] = manufacturer - res = self.client.get("/thermostats/list", params=params) + res = await self.client.get("/thermostats/list", params=params) return [Device.from_dict(item) for item in res["devices"]] @route_metadata( path="/thermostats/off", has_required_parameters=True, has_pagination=False ) - def off( + async def off( self, *, device_id: str, @@ -836,7 +2059,7 @@ def off( if not json_payload: raise ValueError("At least one parameter is required for /thermostats/off") - res = self.client.post("/thermostats/off", json=json_payload) + res = await self.client.post("/thermostats/off", json=json_payload) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -844,7 +2067,7 @@ def off( else wait_for_action_attempt ) - return resolve_action_attempt( + return await resolve_action_attempt_async( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, @@ -855,7 +2078,7 @@ def off( has_required_parameters=True, has_pagination=False, ) - def set_fallback_climate_preset( + async def set_fallback_climate_preset( self, *, climate_preset_key: str, device_id: str ) -> None: """Sets a specified `climate preset `_ as the `"fallback" `_ preset for a specified `thermostat `_. @@ -877,7 +2100,9 @@ def set_fallback_climate_preset( "At least one parameter is required for /thermostats/set_fallback_climate_preset" ) - self.client.post("/thermostats/set_fallback_climate_preset", json=json_payload) + await self.client.post( + "/thermostats/set_fallback_climate_preset", json=json_payload + ) return None @@ -886,7 +2111,7 @@ def set_fallback_climate_preset( has_required_parameters=True, has_pagination=False, ) - def set_fan_mode( + async def set_fan_mode( self, *, device_id: str, @@ -921,7 +2146,7 @@ def set_fan_mode( "At least one parameter is required for /thermostats/set_fan_mode" ) - res = self.client.post("/thermostats/set_fan_mode", json=json_payload) + res = await self.client.post("/thermostats/set_fan_mode", json=json_payload) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -929,7 +2154,7 @@ def set_fan_mode( else wait_for_action_attempt ) - return resolve_action_attempt( + return await resolve_action_attempt_async( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, @@ -940,7 +2165,7 @@ def set_fan_mode( has_required_parameters=True, has_pagination=False, ) - def set_hvac_mode( + async def set_hvac_mode( self, *, device_id: str, @@ -990,7 +2215,7 @@ def set_hvac_mode( "At least one parameter is required for /thermostats/set_hvac_mode" ) - res = self.client.post("/thermostats/set_hvac_mode", json=json_payload) + res = await self.client.post("/thermostats/set_hvac_mode", json=json_payload) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -998,7 +2223,7 @@ def set_hvac_mode( else wait_for_action_attempt ) - return resolve_action_attempt( + return await resolve_action_attempt_async( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, @@ -1009,7 +2234,7 @@ def set_hvac_mode( has_required_parameters=True, has_pagination=False, ) - def set_temperature_threshold( + async def set_temperature_threshold( self, *, device_id: str, @@ -1049,7 +2274,9 @@ def set_temperature_threshold( "At least one parameter is required for /thermostats/set_temperature_threshold" ) - self.client.patch("/thermostats/set_temperature_threshold", json=json_payload) + await self.client.patch( + "/thermostats/set_temperature_threshold", json=json_payload + ) return None @@ -1058,7 +2285,7 @@ def set_temperature_threshold( has_required_parameters=True, has_pagination=False, ) - def update_climate_preset( + async def update_climate_preset( self, *, climate_preset_key: str, @@ -1133,7 +2360,7 @@ def update_climate_preset( "At least one parameter is required for /thermostats/update_climate_preset" ) - self.client.patch("/thermostats/update_climate_preset", json=json_payload) + await self.client.patch("/thermostats/update_climate_preset", json=json_payload) return None @@ -1142,7 +2369,7 @@ def update_climate_preset( has_required_parameters=True, has_pagination=False, ) - def update_weekly_program( + async def update_weekly_program( self, *, device_id: str, @@ -1202,7 +2429,9 @@ def update_weekly_program( "At least one parameter is required for /thermostats/update_weekly_program" ) - res = self.client.post("/thermostats/update_weekly_program", json=json_payload) + res = await self.client.post( + "/thermostats/update_weekly_program", json=json_payload + ) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -1210,7 +2439,7 @@ def update_weekly_program( else wait_for_action_attempt ) - return resolve_action_attempt( + return await resolve_action_attempt_async( client=self.client, action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, diff --git a/seam/routes/thermostats_daily_programs.py b/seam/routes/thermostats_daily_programs.py index 24738cc2..c3e23b56 100644 --- a/seam/routes/thermostats_daily_programs.py +++ b/seam/routes/thermostats_daily_programs.py @@ -1,9 +1,12 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import ThermostatDailyProgram, ActionAttempt -from ..modules.action_attempts import resolve_action_attempt +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, +) class AbstractThermostatsDailyPrograms(abc.ABC): @@ -59,6 +62,59 @@ def update( raise NotImplementedError() +class AbstractAsyncThermostatsDailyPrograms(abc.ABC): + + @abc.abstractmethod + async def create( + self, *, device_id: str, name: str, periods: List[Dict[str, Any]] + ) -> ThermostatDailyProgram: + """Creates a new thermostat daily program. A daily program consists of a set of periods, where each period includes a start time and the key of a configured climate preset. Once you have defined a daily program, you can assign it to one or more days within a weekly program. + + :param device_id: ID of the thermostat device for which you want to create a daily program. + + :param name: Name of the thermostat daily program. + + :param periods: Array of thermostat daily program periods. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, thermostat_daily_program_id: str) -> None: + """Deletes a thermostat daily program. + + :param thermostat_daily_program_id: ID of the thermostat daily program that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + name: str, + periods: List[Dict[str, Any]], + thermostat_daily_program_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Updates a specified thermostat daily program. The periods that you specify overwrite any existing periods for the daily program. + + :param name: Name of the thermostat daily program that you want to update. + + :param periods: Array of thermostat daily program periods. The periods that you specify overwrite any existing periods for the daily program. + + :param thermostat_daily_program_id: ID of the thermostat daily program that you want to update. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class ThermostatsDailyPrograms(AbstractThermostatsDailyPrograms): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -179,3 +235,129 @@ def update( action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, ) + + +class AsyncThermostatsDailyPrograms(AbstractAsyncThermostatsDailyPrograms): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/thermostats/daily_programs/create", + has_required_parameters=True, + has_pagination=False, + ) + async def create( + self, *, device_id: str, name: str, periods: List[Dict[str, Any]] + ) -> ThermostatDailyProgram: + """Creates a new thermostat daily program. A daily program consists of a set of periods, where each period includes a start time and the key of a configured climate preset. Once you have defined a daily program, you can assign it to one or more days within a weekly program. + + :param device_id: ID of the thermostat device for which you want to create a daily program. + + :param name: Name of the thermostat daily program. + + :param periods: Array of thermostat daily program periods. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if name is not None: + json_payload["name"] = name + if periods is not None: + json_payload["periods"] = periods + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/daily_programs/create" + ) + + res = await self.client.post( + "/thermostats/daily_programs/create", json=json_payload + ) + + return ThermostatDailyProgram.from_dict(res["thermostat_daily_program"]) + + @route_metadata( + path="/thermostats/daily_programs/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, thermostat_daily_program_id: str) -> None: + """Deletes a thermostat daily program. + + :param thermostat_daily_program_id: ID of the thermostat daily program that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if thermostat_daily_program_id is not None: + params["thermostat_daily_program_id"] = thermostat_daily_program_id + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/daily_programs/delete" + ) + + await self.client.delete("/thermostats/daily_programs/delete", params=params) + + return None + + @route_metadata( + path="/thermostats/daily_programs/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + name: str, + periods: List[Dict[str, Any]], + thermostat_daily_program_id: str, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, + ) -> ActionAttempt: + """Updates a specified thermostat daily program. The periods that you specify overwrite any existing periods for the daily program. + + :param name: Name of the thermostat daily program that you want to update. + + :param periods: Array of thermostat daily program periods. The periods that you specify overwrite any existing periods for the daily program. + + :param thermostat_daily_program_id: ID of the thermostat daily program that you want to update. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if name is not None: + json_payload["name"] = name + if periods is not None: + json_payload["periods"] = periods + if thermostat_daily_program_id is not None: + json_payload["thermostat_daily_program_id"] = thermostat_daily_program_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/daily_programs/update" + ) + + res = await self.client.patch( + "/thermostats/daily_programs/update", json=json_payload + ) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) diff --git a/seam/routes/thermostats_schedules.py b/seam/routes/thermostats_schedules.py index ffb37f5d..84201408 100644 --- a/seam/routes/thermostats_schedules.py +++ b/seam/routes/thermostats_schedules.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import ThermostatSchedule @@ -108,6 +108,108 @@ def update( raise NotImplementedError() +class AbstractAsyncThermostatsSchedules(abc.ABC): + + @abc.abstractmethod + async def create( + self, + *, + climate_preset_key: str, + device_id: str, + ends_at: str, + starts_at: str, + is_override_allowed: Optional[bool] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, + name: Optional[str] = None, + ) -> ThermostatSchedule: + """Creates a new `thermostat schedule `_ for a specified `thermostat `_. + + :param climate_preset_key: Key of the `climate preset `_ to use for the new thermostat schedule. + + :param device_id: ID of the thermostat device for which you want to create a schedule. + + :param ends_at: Date and time at which the new thermostat schedule ends, in `ISO 8601 `_ format. + + :param starts_at: Date and time at which the new thermostat schedule starts, in `ISO 8601 `_ format. + + :param is_override_allowed: Indicates whether a person at the thermostat or using the API can change the thermostat's settings while the new schedule is active. See also `Specifying Manual Override Permissions `_. + + :param max_override_period_minutes: Number of minutes for which a person at the thermostat or using the API can change the thermostat's settings after the activation of the scheduled climate preset. See also `Specifying Manual Override Permissions `_. + + :param name: Name of the thermostat schedule. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, thermostat_schedule_id: str) -> None: + """Deletes a `thermostat schedule `_ for a specified `thermostat `_. + + :param thermostat_schedule_id: ID of the thermostat schedule that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, thermostat_schedule_id: str) -> ThermostatSchedule: + """Returns a specified `thermostat schedule `_. + + :param thermostat_schedule_id: ID of the thermostat schedule that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, *, device_id: str, user_identifier_key: Optional[str] = None + ) -> List[ThermostatSchedule]: + """Returns a list of all `thermostat schedules `_ for a specified `thermostat `_. + + :param device_id: ID of the thermostat device for which you want to list schedules. + + :param user_identifier_key: User identifier key by which to filter the list of returned thermostat schedules. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + thermostat_schedule_id: str, + climate_preset_key: Optional[str] = None, + ends_at: Optional[str] = None, + is_override_allowed: Optional[bool] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, + name: Optional[str] = None, + starts_at: Optional[str] = None, + ) -> None: + """Updates a specified `thermostat schedule `_. + + :param thermostat_schedule_id: ID of the thermostat schedule that you want to update. + + :param climate_preset_key: Key of the `climate preset `_ to use for the thermostat schedule. + + :param ends_at: Date and time at which the thermostat schedule ends, in `ISO 8601 `_ format. + + :param is_override_allowed: Indicates whether a person at the thermostat or using the API can change the thermostat's settings while the schedule is active. See also `Specifying Manual Override Permissions `_. + + :param max_override_period_minutes: Number of minutes for which a person at the thermostat or using the API can change the thermostat's settings after the activation of the scheduled climate preset. See also `Specifying Manual Override Permissions `_. + + :param name: Name of the thermostat schedule. + + :param starts_at: Date and time at which the thermostat schedule starts, in `ISO 8601 `_ format. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class ThermostatsSchedules(AbstractThermostatsSchedules): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -319,3 +421,216 @@ def update( self.client.patch("/thermostats/schedules/update", json=json_payload) return None + + +class AsyncThermostatsSchedules(AbstractAsyncThermostatsSchedules): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/thermostats/schedules/create", + has_required_parameters=True, + has_pagination=False, + ) + async def create( + self, + *, + climate_preset_key: str, + device_id: str, + ends_at: str, + starts_at: str, + is_override_allowed: Optional[bool] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, + name: Optional[str] = None, + ) -> ThermostatSchedule: + """Creates a new `thermostat schedule `_ for a specified `thermostat `_. + + :param climate_preset_key: Key of the `climate preset `_ to use for the new thermostat schedule. + + :param device_id: ID of the thermostat device for which you want to create a schedule. + + :param ends_at: Date and time at which the new thermostat schedule ends, in `ISO 8601 `_ format. + + :param starts_at: Date and time at which the new thermostat schedule starts, in `ISO 8601 `_ format. + + :param is_override_allowed: Indicates whether a person at the thermostat or using the API can change the thermostat's settings while the new schedule is active. See also `Specifying Manual Override Permissions `_. + + :param max_override_period_minutes: Number of minutes for which a person at the thermostat or using the API can change the thermostat's settings after the activation of the scheduled climate preset. See also `Specifying Manual Override Permissions `_. + + :param name: Name of the thermostat schedule. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if climate_preset_key is not None: + json_payload["climate_preset_key"] = climate_preset_key + if device_id is not None: + json_payload["device_id"] = device_id + if ends_at is not None: + json_payload["ends_at"] = ends_at + if starts_at is not None: + json_payload["starts_at"] = starts_at + if is_override_allowed is not None: + json_payload["is_override_allowed"] = is_override_allowed + if max_override_period_minutes is not None: + json_payload["max_override_period_minutes"] = max_override_period_minutes + if name is not None: + json_payload["name"] = name + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/create" + ) + + res = await self.client.post("/thermostats/schedules/create", json=json_payload) + + return ThermostatSchedule.from_dict(res["thermostat_schedule"]) + + @route_metadata( + path="/thermostats/schedules/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, thermostat_schedule_id: str) -> None: + """Deletes a `thermostat schedule `_ for a specified `thermostat `_. + + :param thermostat_schedule_id: ID of the thermostat schedule that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if thermostat_schedule_id is not None: + params["thermostat_schedule_id"] = thermostat_schedule_id + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/delete" + ) + + await self.client.delete("/thermostats/schedules/delete", params=params) + + return None + + @route_metadata( + path="/thermostats/schedules/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get(self, *, thermostat_schedule_id: str) -> ThermostatSchedule: + """Returns a specified `thermostat schedule `_. + + :param thermostat_schedule_id: ID of the thermostat schedule that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if thermostat_schedule_id is not None: + params["thermostat_schedule_id"] = thermostat_schedule_id + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/get" + ) + + res = await self.client.get("/thermostats/schedules/get", params=params) + + return ThermostatSchedule.from_dict(res["thermostat_schedule"]) + + @route_metadata( + path="/thermostats/schedules/list", + has_required_parameters=True, + has_pagination=False, + ) + async def list( + self, *, device_id: str, user_identifier_key: Optional[str] = None + ) -> List[ThermostatSchedule]: + """Returns a list of all `thermostat schedules `_ for a specified `thermostat `_. + + :param device_id: ID of the thermostat device for which you want to list schedules. + + :param user_identifier_key: User identifier key by which to filter the list of returned thermostat schedules. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + if user_identifier_key is not None: + params["user_identifier_key"] = user_identifier_key + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/list" + ) + + res = await self.client.get("/thermostats/schedules/list", params=params) + + return [ + ThermostatSchedule.from_dict(item) for item in res["thermostat_schedules"] + ] + + @route_metadata( + path="/thermostats/schedules/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + thermostat_schedule_id: str, + climate_preset_key: Optional[str] = None, + ends_at: Optional[str] = None, + is_override_allowed: Optional[bool] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, + name: Optional[str] = None, + starts_at: Optional[str] = None, + ) -> None: + """Updates a specified `thermostat schedule `_. + + :param thermostat_schedule_id: ID of the thermostat schedule that you want to update. + + :param climate_preset_key: Key of the `climate preset `_ to use for the thermostat schedule. + + :param ends_at: Date and time at which the thermostat schedule ends, in `ISO 8601 `_ format. + + :param is_override_allowed: Indicates whether a person at the thermostat or using the API can change the thermostat's settings while the schedule is active. See also `Specifying Manual Override Permissions `_. + + :param max_override_period_minutes: Number of minutes for which a person at the thermostat or using the API can change the thermostat's settings after the activation of the scheduled climate preset. See also `Specifying Manual Override Permissions `_. + + :param name: Name of the thermostat schedule. + + :param starts_at: Date and time at which the thermostat schedule starts, in `ISO 8601 `_ format. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if thermostat_schedule_id is not None: + json_payload["thermostat_schedule_id"] = thermostat_schedule_id + if climate_preset_key is not None: + json_payload["climate_preset_key"] = climate_preset_key + if ends_at is not None: + json_payload["ends_at"] = ends_at + if is_override_allowed is not None: + json_payload["is_override_allowed"] = is_override_allowed + if max_override_period_minutes is not None: + json_payload["max_override_period_minutes"] = max_override_period_minutes + if name is not None: + json_payload["name"] = name + if starts_at is not None: + json_payload["starts_at"] = starts_at + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/update" + ) + + await self.client.patch("/thermostats/schedules/update", json=json_payload) + + return None diff --git a/seam/routes/thermostats_simulate.py b/seam/routes/thermostats_simulate.py index 992d7663..89c6f56c 100644 --- a/seam/routes/thermostats_simulate.py +++ b/seam/routes/thermostats_simulate.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata @@ -54,6 +54,56 @@ def temperature_reached( raise NotImplementedError() +class AbstractAsyncThermostatsSimulate(abc.ABC): + + @abc.abstractmethod + async def hvac_mode_adjusted( + self, + *, + device_id: str, + hvac_mode: str, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + ) -> None: + """Simulates having adjusted the `HVAC mode `_ for a `thermostat `_. Only applicable for `sandbox devices `_. See also `Testing Your Thermostat App with Simulate Endpoints `_. + + :param device_id: ID of the thermostat device for which you want to simulate having adjusted the HVAC mode. + + :param hvac_mode: HVAC mode that you want to simulate. + + :param cooling_set_point_celsius: Cooling `set point `_ in °C that you want to simulate. You must set ``cooling_set_point_celsius`` or ``cooling_set_point_fahrenheit``. + + :param cooling_set_point_fahrenheit: Cooling `set point `_ in °F that you want to simulate. You must set ``cooling_set_point_fahrenheit`` or ``cooling_set_point_celsius``. + + :param heating_set_point_celsius: Heating `set point `_ in °C that you want to simulate. You must set ``heating_set_point_celsius`` or ``heating_set_point_fahrenheit``. + + :param heating_set_point_fahrenheit: Heating `set point `_ in °F that you want to simulate. You must set ``heating_set_point_fahrenheit`` or ``heating_set_point_celsius``. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def temperature_reached( + self, + *, + device_id: str, + temperature_celsius: Optional[float] = None, + temperature_fahrenheit: Optional[float] = None, + ) -> None: + """Simulates a `thermostat `_ reaching a specified temperature. Only applicable for `sandbox devices `_. See also `Testing Your Thermostat App with Simulate Endpoints `_. + + :param device_id: ID of the thermostat device that you want to simulate reaching a specified temperature. + + :param temperature_celsius: Temperature in °C that you want simulate the thermostat reaching. You must set ``temperature_celsius`` or ``temperature_fahrenheit``. + + :param temperature_fahrenheit: Temperature in °F that you want simulate the thermostat reaching. You must set ``temperature_fahrenheit`` or ``temperature_celsius``. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class ThermostatsSimulate(AbstractThermostatsSimulate): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -151,3 +201,106 @@ def temperature_reached( self.client.post("/thermostats/simulate/temperature_reached", json=json_payload) return None + + +class AsyncThermostatsSimulate(AbstractAsyncThermostatsSimulate): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/thermostats/simulate/hvac_mode_adjusted", + has_required_parameters=True, + has_pagination=False, + ) + async def hvac_mode_adjusted( + self, + *, + device_id: str, + hvac_mode: str, + cooling_set_point_celsius: Optional[float] = None, + cooling_set_point_fahrenheit: Optional[float] = None, + heating_set_point_celsius: Optional[float] = None, + heating_set_point_fahrenheit: Optional[float] = None, + ) -> None: + """Simulates having adjusted the `HVAC mode `_ for a `thermostat `_. Only applicable for `sandbox devices `_. See also `Testing Your Thermostat App with Simulate Endpoints `_. + + :param device_id: ID of the thermostat device for which you want to simulate having adjusted the HVAC mode. + + :param hvac_mode: HVAC mode that you want to simulate. + + :param cooling_set_point_celsius: Cooling `set point `_ in °C that you want to simulate. You must set ``cooling_set_point_celsius`` or ``cooling_set_point_fahrenheit``. + + :param cooling_set_point_fahrenheit: Cooling `set point `_ in °F that you want to simulate. You must set ``cooling_set_point_fahrenheit`` or ``cooling_set_point_celsius``. + + :param heating_set_point_celsius: Heating `set point `_ in °C that you want to simulate. You must set ``heating_set_point_celsius`` or ``heating_set_point_fahrenheit``. + + :param heating_set_point_fahrenheit: Heating `set point `_ in °F that you want to simulate. You must set ``heating_set_point_fahrenheit`` or ``heating_set_point_celsius``. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if hvac_mode is not None: + json_payload["hvac_mode"] = hvac_mode + if cooling_set_point_celsius is not None: + json_payload["cooling_set_point_celsius"] = cooling_set_point_celsius + if cooling_set_point_fahrenheit is not None: + json_payload["cooling_set_point_fahrenheit"] = cooling_set_point_fahrenheit + if heating_set_point_celsius is not None: + json_payload["heating_set_point_celsius"] = heating_set_point_celsius + if heating_set_point_fahrenheit is not None: + json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/simulate/hvac_mode_adjusted" + ) + + await self.client.post( + "/thermostats/simulate/hvac_mode_adjusted", json=json_payload + ) + + return None + + @route_metadata( + path="/thermostats/simulate/temperature_reached", + has_required_parameters=True, + has_pagination=False, + ) + async def temperature_reached( + self, + *, + device_id: str, + temperature_celsius: Optional[float] = None, + temperature_fahrenheit: Optional[float] = None, + ) -> None: + """Simulates a `thermostat `_ reaching a specified temperature. Only applicable for `sandbox devices `_. See also `Testing Your Thermostat App with Simulate Endpoints `_. + + :param device_id: ID of the thermostat device that you want to simulate reaching a specified temperature. + + :param temperature_celsius: Temperature in °C that you want simulate the thermostat reaching. You must set ``temperature_celsius`` or ``temperature_fahrenheit``. + + :param temperature_fahrenheit: Temperature in °F that you want simulate the thermostat reaching. You must set ``temperature_fahrenheit`` or ``temperature_celsius``. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if temperature_celsius is not None: + json_payload["temperature_celsius"] = temperature_celsius + if temperature_fahrenheit is not None: + json_payload["temperature_fahrenheit"] = temperature_fahrenheit + + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/simulate/temperature_reached" + ) + + await self.client.post( + "/thermostats/simulate/temperature_reached", json=json_payload + ) + + return None diff --git a/seam/routes/user_identities.py b/seam/routes/user_identities.py index 2f42d0a1..6d9902ac 100644 --- a/seam/routes/user_identities.py +++ b/seam/routes/user_identities.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import ( @@ -14,6 +14,8 @@ from .user_identities_unmanaged import ( AbstractUserIdentitiesUnmanaged, UserIdentitiesUnmanaged, + AbstractAsyncUserIdentitiesUnmanaged, + AsyncUserIdentitiesUnmanaged, ) @@ -251,6 +253,246 @@ def update( raise NotImplementedError() +class AbstractAsyncUserIdentities(abc.ABC): + + @property + @abc.abstractmethod + def unmanaged(self) -> AbstractAsyncUserIdentitiesUnmanaged: + raise NotImplementedError() + + @abc.abstractmethod + async def add_acs_user( + self, + *, + acs_user_id: str, + user_identity_id: Optional[str] = None, + user_identity_key: Optional[str] = None, + ) -> None: + """Adds a specified `access system user `_ to a specified `user identity `_. + + You must specify either ``user_identity_id`` or ``user_identity_key`` to identify the user identity. + + If ``user_identity_key`` is provided, but the user identity doesn't exist, a new user identity will be created automatically using information from the ACS user. + + :param acs_user_id: ID of the access system user that you want to add to the user identity. + + :param user_identity_id: ID of the user identity to which you want to add an access system user. + + :param user_identity_key: Key of the user identity to which you want to add an access system user. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def create( + self, + *, + acs_system_ids: Optional[List[str]] = None, + email_address: Optional[Union[str, Null]] = None, + full_name: Optional[Union[str, Null]] = None, + phone_number: Optional[Union[str, Null]] = None, + user_identity_key: Optional[Union[str, Null]] = None, + ) -> UserIdentity: + """Creates a new `user identity `_. + + :param acs_system_ids: List of access system IDs to associate with the new user identity through access system users. If there's no user with the same email address or phone number in the specified access systems, a new access system user is created. If there is an existing user with the same email or phone number in the specified access systems, the user is linked to the user identity. + + :param email_address: Unique email address for the new user identity. + + :param full_name: Full name of the user associated with the new user identity. + + :param phone_number: Unique phone number for the new user identity in E.164 format (for example, +15555550100). + + :param user_identity_key: Unique key for the new user identity. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, user_identity_id: str) -> None: + """Deletes a specified `user identity `_. This deletes the user identity and all associated resources, including any `credentials `_, `acs users `_ and `client sessions `_. + + :param user_identity_id: ID of the user identity that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def generate_instant_key( + self, + *, + user_identity_id: str, + customization_profile_id: Optional[str] = None, + max_use_count: Optional[float] = None, + ) -> InstantKey: + """Generates a new `instant key `_ for a specified `user identity `_. + + :param user_identity_id: ID of the user identity for which you want to generate an instant key. + + :param customization_profile_id: + + :param max_use_count: Maximum number of times the instant key can be used. Default: 1. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get( + self, + *, + user_identity_id: Optional[str] = None, + user_identity_key: Optional[str] = None, + ) -> UserIdentity: + """Returns a specified `user identity `_. + + :param user_identity_id: ID of the user identity that you want to get. + + :param user_identity_key: + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def grant_access_to_device( + self, *, device_id: str, user_identity_id: str + ) -> None: + """Grants a specified `user identity `_ access to a specified `device `_. + + :param device_id: ID of the managed device to which you want to grant access to the user identity. + + :param user_identity_id: ID of the user identity that you want to grant access to a device. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + created_before: Optional[str] = None, + credential_manager_acs_system_id: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identity_ids: Optional[List[str]] = None, + ) -> List[UserIdentity]: + """Returns a list of all `user identities `_. + + :param created_before: Timestamp by which to limit returned user identities. Returns user identities created before this timestamp. + + :param credential_manager_acs_system_id: ``acs_system_id`` of the credential manager by which you want to filter the list of user identities. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned user identities to include all records that satisfy a partial match using ``full_name``, ``phone_number``, ``email_address`` or ``user_identity_id``. + + :param user_identity_ids: Array of user identity IDs by which to filter the list of user identities. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_accessible_devices(self, *, user_identity_id: str) -> List[Device]: + """Returns a list of all `devices `_ associated with a specified `user identity `_. This includes devices derived from the access grants assigned to the user identity and devices directly linked to the user identity. + + :param user_identity_id: ID of the user identity for which you want to retrieve all accessible devices. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_accessible_entrances( + self, *, user_identity_id: str + ) -> List[AcsEntrance]: + """Returns a list of all `ACS entrances `_ accessible to a specified `user identity `_. This includes entrances derived from the access grants assigned to the user identity and entrances accessible through ACS users linked to the user identity. + + :param user_identity_id: ID of the user identity for which you want to retrieve all accessible entrances. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_acs_systems(self, *, user_identity_id: str) -> List[AcsSystem]: + """Returns a list of all `access systems `_ associated with a specified `user identity `_. + + :param user_identity_id: ID of the user identity for which you want to retrieve all access systems. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list_acs_users(self, *, user_identity_id: str) -> List[AcsUser]: + """Returns a list of all `access system users `_ assigned to a specified `user identity `_. + + :param user_identity_id: ID of the user identity for which you want to retrieve all access system users. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def remove_acs_user(self, *, acs_user_id: str, user_identity_id: str) -> None: + """Removes a specified `access system user `_ from a specified `user identity `_. + + :param acs_user_id: ID of the access system user that you want to remove from the user identity.. + + :param user_identity_id: ID of the user identity from which you want to remove an access system user. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def revoke_access_to_device( + self, *, device_id: str, user_identity_id: str + ) -> None: + """Revokes access to a specified `device `_ from a specified `user identity `_. + + :param device_id: ID of the managed device to which you want to revoke access from the user identity. + + :param user_identity_id: ID of the user identity from which you want to revoke access to a device. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + user_identity_id: str, + email_address: Optional[Union[str, Null]] = None, + full_name: Optional[Union[str, Null]] = None, + phone_number: Optional[Union[str, Null]] = None, + user_identity_key: Optional[Union[str, Null]] = None, + ) -> None: + """Updates a specified `user identity `_. + + :param user_identity_id: ID of the user identity that you want to update. + + :param email_address: Unique email address for the user identity. + + :param full_name: Full name of the user associated with the user identity. + + :param phone_number: Unique phone number for the user identity. + + :param user_identity_key: Unique key for the user identity. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class UserIdentities(AbstractUserIdentities): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -744,3 +986,510 @@ def update( self.client.patch("/user_identities/update", json=json_payload) return None + + +class AsyncUserIdentities(AbstractAsyncUserIdentities): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + self._unmanaged = AsyncUserIdentitiesUnmanaged(client=client, defaults=defaults) + + @property + def unmanaged(self) -> AsyncUserIdentitiesUnmanaged: + return self._unmanaged + + @route_metadata( + path="/user_identities/add_acs_user", + has_required_parameters=True, + has_pagination=False, + ) + async def add_acs_user( + self, + *, + acs_user_id: str, + user_identity_id: Optional[str] = None, + user_identity_key: Optional[str] = None, + ) -> None: + """Adds a specified `access system user `_ to a specified `user identity `_. + + You must specify either ``user_identity_id`` or ``user_identity_key`` to identify the user identity. + + If ``user_identity_key`` is provided, but the user identity doesn't exist, a new user identity will be created automatically using information from the ACS user. + + :param acs_user_id: ID of the access system user that you want to add to the user identity. + + :param user_identity_id: ID of the user identity to which you want to add an access system user. + + :param user_identity_key: Key of the user identity to which you want to add an access system user. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if acs_user_id is not None: + json_payload["acs_user_id"] = acs_user_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if user_identity_key is not None: + json_payload["user_identity_key"] = user_identity_key + + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/add_acs_user" + ) + + await self.client.put("/user_identities/add_acs_user", json=json_payload) + + return None + + @route_metadata( + path="/user_identities/create", + has_required_parameters=False, + has_pagination=False, + ) + async def create( + self, + *, + acs_system_ids: Optional[List[str]] = None, + email_address: Optional[Union[str, Null]] = None, + full_name: Optional[Union[str, Null]] = None, + phone_number: Optional[Union[str, Null]] = None, + user_identity_key: Optional[Union[str, Null]] = None, + ) -> UserIdentity: + """Creates a new `user identity `_. + + :param acs_system_ids: List of access system IDs to associate with the new user identity through access system users. If there's no user with the same email address or phone number in the specified access systems, a new access system user is created. If there is an existing user with the same email or phone number in the specified access systems, the user is linked to the user identity. + + :param email_address: Unique email address for the new user identity. + + :param full_name: Full name of the user associated with the new user identity. + + :param phone_number: Unique phone number for the new user identity in E.164 format (for example, +15555550100). + + :param user_identity_key: Unique key for the new user identity. + + :returns: OK""" + json_payload: Dict[str, Any] = {} + + if acs_system_ids is not None: + json_payload["acs_system_ids"] = acs_system_ids + if email_address is not None: + json_payload["email_address"] = email_address + if full_name is not None: + json_payload["full_name"] = full_name + if phone_number is not None: + json_payload["phone_number"] = phone_number + if user_identity_key is not None: + json_payload["user_identity_key"] = user_identity_key + + res = await self.client.post("/user_identities/create", json=json_payload) + + return UserIdentity.from_dict(res["user_identity"]) + + @route_metadata( + path="/user_identities/delete", + has_required_parameters=True, + has_pagination=False, + ) + async def delete(self, *, user_identity_id: str) -> None: + """Deletes a specified `user identity `_. This deletes the user identity and all associated resources, including any `credentials `_, `acs users `_ and `client sessions `_. + + :param user_identity_id: ID of the user identity that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/delete" + ) + + await self.client.delete("/user_identities/delete", params=params) + + return None + + @route_metadata( + path="/user_identities/generate_instant_key", + has_required_parameters=True, + has_pagination=False, + ) + async def generate_instant_key( + self, + *, + user_identity_id: str, + customization_profile_id: Optional[str] = None, + max_use_count: Optional[float] = None, + ) -> InstantKey: + """Generates a new `instant key `_ for a specified `user identity `_. + + :param user_identity_id: ID of the user identity for which you want to generate an instant key. + + :param customization_profile_id: + + :param max_use_count: Maximum number of times the instant key can be used. Default: 1. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if customization_profile_id is not None: + json_payload["customization_profile_id"] = customization_profile_id + if max_use_count is not None: + json_payload["max_use_count"] = max_use_count + + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/generate_instant_key" + ) + + res = await self.client.post( + "/user_identities/generate_instant_key", json=json_payload + ) + + return InstantKey.from_dict(res["instant_key"]) + + @route_metadata( + path="/user_identities/get", has_required_parameters=True, has_pagination=False + ) + async def get( + self, + *, + user_identity_id: Optional[str] = None, + user_identity_key: Optional[str] = None, + ) -> UserIdentity: + """Returns a specified `user identity `_. + + :param user_identity_id: ID of the user identity that you want to get. + + :param user_identity_key: + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + if user_identity_key is not None: + params["user_identity_key"] = user_identity_key + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/get" + ) + + res = await self.client.get("/user_identities/get", params=params) + + return UserIdentity.from_dict(res["user_identity"]) + + @route_metadata( + path="/user_identities/grant_access_to_device", + has_required_parameters=True, + has_pagination=False, + ) + async def grant_access_to_device( + self, *, device_id: str, user_identity_id: str + ) -> None: + """Grants a specified `user identity `_ access to a specified `device `_. + + :param device_id: ID of the managed device to which you want to grant access to the user identity. + + :param user_identity_id: ID of the user identity that you want to grant access to a device. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if device_id is not None: + json_payload["device_id"] = device_id + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/grant_access_to_device" + ) + + await self.client.put( + "/user_identities/grant_access_to_device", json=json_payload + ) + + return None + + @route_metadata( + path="/user_identities/list", has_required_parameters=False, has_pagination=True + ) + async def list( + self, + *, + created_before: Optional[str] = None, + credential_manager_acs_system_id: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + user_identity_ids: Optional[List[str]] = None, + ) -> List[UserIdentity]: + """Returns a list of all `user identities `_. + + :param created_before: Timestamp by which to limit returned user identities. Returns user identities created before this timestamp. + + :param credential_manager_acs_system_id: ``acs_system_id`` of the credential manager by which you want to filter the list of user identities. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned user identities to include all records that satisfy a partial match using ``full_name``, ``phone_number``, ``email_address`` or ``user_identity_id``. + + :param user_identity_ids: Array of user identity IDs by which to filter the list of user identities. + + :returns: OK""" + params: Dict[str, Any] = {} + + if created_before is not None: + params["created_before"] = created_before + if credential_manager_acs_system_id is not None: + params["credential_manager_acs_system_id"] = ( + credential_manager_acs_system_id + ) + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + if user_identity_ids is not None: + params["user_identity_ids"] = user_identity_ids + + res = await self.client.get("/user_identities/list", params=params) + + return [UserIdentity.from_dict(item) for item in res["user_identities"]] + + @route_metadata( + path="/user_identities/list_accessible_devices", + has_required_parameters=True, + has_pagination=False, + ) + async def list_accessible_devices(self, *, user_identity_id: str) -> List[Device]: + """Returns a list of all `devices `_ associated with a specified `user identity `_. This includes devices derived from the access grants assigned to the user identity and devices directly linked to the user identity. + + :param user_identity_id: ID of the user identity for which you want to retrieve all accessible devices. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/list_accessible_devices" + ) + + res = await self.client.get( + "/user_identities/list_accessible_devices", params=params + ) + + return [Device.from_dict(item) for item in res["devices"]] + + @route_metadata( + path="/user_identities/list_accessible_entrances", + has_required_parameters=True, + has_pagination=False, + ) + async def list_accessible_entrances( + self, *, user_identity_id: str + ) -> List[AcsEntrance]: + """Returns a list of all `ACS entrances `_ accessible to a specified `user identity `_. This includes entrances derived from the access grants assigned to the user identity and entrances accessible through ACS users linked to the user identity. + + :param user_identity_id: ID of the user identity for which you want to retrieve all accessible entrances. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/list_accessible_entrances" + ) + + res = await self.client.get( + "/user_identities/list_accessible_entrances", params=params + ) + + return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + + @route_metadata( + path="/user_identities/list_acs_systems", + has_required_parameters=True, + has_pagination=False, + ) + async def list_acs_systems(self, *, user_identity_id: str) -> List[AcsSystem]: + """Returns a list of all `access systems `_ associated with a specified `user identity `_. + + :param user_identity_id: ID of the user identity for which you want to retrieve all access systems. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/list_acs_systems" + ) + + res = await self.client.get("/user_identities/list_acs_systems", params=params) + + return [AcsSystem.from_dict(item) for item in res["acs_systems"]] + + @route_metadata( + path="/user_identities/list_acs_users", + has_required_parameters=True, + has_pagination=False, + ) + async def list_acs_users(self, *, user_identity_id: str) -> List[AcsUser]: + """Returns a list of all `access system users `_ assigned to a specified `user identity `_. + + :param user_identity_id: ID of the user identity for which you want to retrieve all access system users. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/list_acs_users" + ) + + res = await self.client.get("/user_identities/list_acs_users", params=params) + + return [AcsUser.from_dict(item) for item in res["acs_users"]] + + @route_metadata( + path="/user_identities/remove_acs_user", + has_required_parameters=True, + has_pagination=False, + ) + async def remove_acs_user(self, *, acs_user_id: str, user_identity_id: str) -> None: + """Removes a specified `access system user `_ from a specified `user identity `_. + + :param acs_user_id: ID of the access system user that you want to remove from the user identity.. + + :param user_identity_id: ID of the user identity from which you want to remove an access system user. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if acs_user_id is not None: + params["acs_user_id"] = acs_user_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/remove_acs_user" + ) + + await self.client.delete("/user_identities/remove_acs_user", params=params) + + return None + + @route_metadata( + path="/user_identities/revoke_access_to_device", + has_required_parameters=True, + has_pagination=False, + ) + async def revoke_access_to_device( + self, *, device_id: str, user_identity_id: str + ) -> None: + """Revokes access to a specified `device `_ from a specified `user identity `_. + + :param device_id: ID of the managed device to which you want to revoke access from the user identity. + + :param user_identity_id: ID of the user identity from which you want to revoke access to a device. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if device_id is not None: + params["device_id"] = device_id + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/revoke_access_to_device" + ) + + await self.client.delete( + "/user_identities/revoke_access_to_device", params=params + ) + + return None + + @route_metadata( + path="/user_identities/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + user_identity_id: str, + email_address: Optional[Union[str, Null]] = None, + full_name: Optional[Union[str, Null]] = None, + phone_number: Optional[Union[str, Null]] = None, + user_identity_key: Optional[Union[str, Null]] = None, + ) -> None: + """Updates a specified `user identity `_. + + :param user_identity_id: ID of the user identity that you want to update. + + :param email_address: Unique email address for the user identity. + + :param full_name: Full name of the user associated with the user identity. + + :param phone_number: Unique phone number for the user identity. + + :param user_identity_key: Unique key for the user identity. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if email_address is not None: + json_payload["email_address"] = email_address + if full_name is not None: + json_payload["full_name"] = full_name + if phone_number is not None: + json_payload["phone_number"] = phone_number + if user_identity_key is not None: + json_payload["user_identity_key"] = user_identity_key + + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/update" + ) + + await self.client.patch("/user_identities/update", json=json_payload) + + return None diff --git a/seam/routes/user_identities_unmanaged.py b/seam/routes/user_identities_unmanaged.py index 456a3e51..ad224074 100644 --- a/seam/routes/user_identities_unmanaged.py +++ b/seam/routes/user_identities_unmanaged.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import UnmanagedUserIdentity @@ -63,6 +63,63 @@ def update( raise NotImplementedError() +class AbstractAsyncUserIdentitiesUnmanaged(abc.ABC): + + @abc.abstractmethod + async def get(self, *, user_identity_id: str) -> UnmanagedUserIdentity: + """Returns a specified unmanaged `user identity `_ (where is_managed = false). + + :param user_identity_id: ID of the unmanaged user identity that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list( + self, + *, + created_before: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + ) -> List[UnmanagedUserIdentity]: + """Returns a list of all unmanaged `user identities `_ (where is_managed = false). + + :param created_before: Timestamp by which to limit returned unmanaged user identities. Returns user identities created before this timestamp. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned unmanaged user identities to include all records that satisfy a partial match using ``full_name``, ``phone_number``, ``email_address``, ``user_identity_id`` or ``acs_system_id``. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + is_managed: Literal[True], + user_identity_id: str, + user_identity_key: Optional[str] = None, + ) -> None: + """Updates an unmanaged `user identity `_ to make it managed. + + This endpoint can only be used to convert unmanaged user identities to managed ones by setting ``is_managed`` to ``true``. It cannot be used to convert managed user identities back to unmanaged. + + :param is_managed: Must be set to true to convert the unmanaged user identity to managed. + + :param user_identity_id: ID of the unmanaged user identity that you want to update. + + :param user_identity_key: Unique key for the user identity. If not provided, the existing key will be preserved. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class UserIdentitiesUnmanaged(AbstractUserIdentitiesUnmanaged): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -176,3 +233,118 @@ def update( self.client.patch("/user_identities/unmanaged/update", json=json_payload) return None + + +class AsyncUserIdentitiesUnmanaged(AbstractAsyncUserIdentitiesUnmanaged): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/user_identities/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) + async def get(self, *, user_identity_id: str) -> UnmanagedUserIdentity: + """Returns a specified unmanaged `user identity `_ (where is_managed = false). + + :param user_identity_id: ID of the unmanaged user identity that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if user_identity_id is not None: + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/unmanaged/get" + ) + + res = await self.client.get("/user_identities/unmanaged/get", params=params) + + return UnmanagedUserIdentity.from_dict(res["user_identity"]) + + @route_metadata( + path="/user_identities/unmanaged/list", + has_required_parameters=False, + has_pagination=True, + ) + async def list( + self, + *, + created_before: Optional[str] = None, + limit: Optional[int] = None, + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, + ) -> List[UnmanagedUserIdentity]: + """Returns a list of all unmanaged `user identities `_ (where is_managed = false). + + :param created_before: Timestamp by which to limit returned unmanaged user identities. Returns user identities created before this timestamp. + + :param limit: Maximum number of records to return per page. + + :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. + + :param search: String for which to search. Filters returned unmanaged user identities to include all records that satisfy a partial match using ``full_name``, ``phone_number``, ``email_address``, ``user_identity_id`` or ``acs_system_id``. + + :returns: OK""" + params: Dict[str, Any] = {} + + if created_before is not None: + params["created_before"] = created_before + if limit is not None: + params["limit"] = limit + if page_cursor is not None: + params["page_cursor"] = page_cursor + if search is not None: + params["search"] = search + + res = await self.client.get("/user_identities/unmanaged/list", params=params) + + return [ + UnmanagedUserIdentity.from_dict(item) for item in res["user_identities"] + ] + + @route_metadata( + path="/user_identities/unmanaged/update", + has_required_parameters=True, + has_pagination=False, + ) + async def update( + self, + *, + is_managed: Literal[True], + user_identity_id: str, + user_identity_key: Optional[str] = None, + ) -> None: + """Updates an unmanaged `user identity `_ to make it managed. + + This endpoint can only be used to convert unmanaged user identities to managed ones by setting ``is_managed`` to ``true``. It cannot be used to convert managed user identities back to unmanaged. + + :param is_managed: Must be set to true to convert the unmanaged user identity to managed. + + :param user_identity_id: ID of the unmanaged user identity that you want to update. + + :param user_identity_key: Unique key for the user identity. If not provided, the existing key will be preserved. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if is_managed is not None: + json_payload["is_managed"] = is_managed + if user_identity_id is not None: + json_payload["user_identity_id"] = user_identity_id + if user_identity_key is not None: + json_payload["user_identity_key"] = user_identity_key + + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/unmanaged/update" + ) + + await self.client.patch("/user_identities/unmanaged/update", json=json_payload) + + return None diff --git a/seam/routes/webhooks.py b/seam/routes/webhooks.py index 736b1040..749435c4 100644 --- a/seam/routes/webhooks.py +++ b/seam/routes/webhooks.py @@ -1,6 +1,6 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..resources import Webhook @@ -59,6 +59,62 @@ def update(self, *, event_types: List[str], webhook_id: str) -> None: raise NotImplementedError() +class AbstractAsyncWebhooks(abc.ABC): + + @abc.abstractmethod + async def create( + self, *, url: str, event_types: Optional[List[str]] = None + ) -> Webhook: + """Creates a new `webhook `_. + + :param url: URL for the new webhook. + + :param event_types: Types of events that you want the new webhook to receive. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def delete(self, *, webhook_id: str) -> None: + """Deletes a specified `webhook `_. + + :param webhook_id: ID of the webhook that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self, *, webhook_id: str) -> Webhook: + """Gets a specified `webhook `_. + + :param webhook_id: ID of the webhook that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def list(self) -> List[Webhook]: + """Returns a list of all `webhooks `_. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def update(self, *, event_types: List[str], webhook_id: str) -> None: + """Updates a specified `webhook `_. + + :param event_types: Types of events that you want the webhook to receive. + + :param webhook_id: ID of the webhook that you want to update. + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + class Webhooks(AbstractWebhooks): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -172,3 +228,120 @@ def update(self, *, event_types: List[str], webhook_id: str) -> None: self.client.put("/webhooks/update", json=json_payload) return None + + +class AsyncWebhooks(AbstractAsyncWebhooks): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/webhooks/create", has_required_parameters=True, has_pagination=False + ) + async def create( + self, *, url: str, event_types: Optional[List[str]] = None + ) -> Webhook: + """Creates a new `webhook `_. + + :param url: URL for the new webhook. + + :param event_types: Types of events that you want the new webhook to receive. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if url is not None: + json_payload["url"] = url + if event_types is not None: + json_payload["event_types"] = event_types + + if not json_payload: + raise ValueError("At least one parameter is required for /webhooks/create") + + res = await self.client.post("/webhooks/create", json=json_payload) + + return Webhook.from_dict(res["webhook"]) + + @route_metadata( + path="/webhooks/delete", has_required_parameters=True, has_pagination=False + ) + async def delete(self, *, webhook_id: str) -> None: + """Deletes a specified `webhook `_. + + :param webhook_id: ID of the webhook that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if webhook_id is not None: + params["webhook_id"] = webhook_id + + if not params: + raise ValueError("At least one parameter is required for /webhooks/delete") + + await self.client.delete("/webhooks/delete", params=params) + + return None + + @route_metadata( + path="/webhooks/get", has_required_parameters=True, has_pagination=False + ) + async def get(self, *, webhook_id: str) -> Webhook: + """Gets a specified `webhook `_. + + :param webhook_id: ID of the webhook that you want to get. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} + + if webhook_id is not None: + params["webhook_id"] = webhook_id + + if not params: + raise ValueError("At least one parameter is required for /webhooks/get") + + res = await self.client.get("/webhooks/get", params=params) + + return Webhook.from_dict(res["webhook"]) + + @route_metadata( + path="/webhooks/list", has_required_parameters=False, has_pagination=False + ) + async def list(self) -> List[Webhook]: + """Returns a list of all `webhooks `_. + + :returns: OK""" + params: Dict[str, Any] = {} + + res = await self.client.get("/webhooks/list", params=params) + + return [Webhook.from_dict(item) for item in res["webhooks"]] + + @route_metadata( + path="/webhooks/update", has_required_parameters=True, has_pagination=False + ) + async def update(self, *, event_types: List[str], webhook_id: str) -> None: + """Updates a specified `webhook `_. + + :param event_types: Types of events that you want the webhook to receive. + + :param webhook_id: ID of the webhook that you want to update. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if event_types is not None: + json_payload["event_types"] = event_types + if webhook_id is not None: + json_payload["webhook_id"] = webhook_id + + if not json_payload: + raise ValueError("At least one parameter is required for /webhooks/update") + + await self.client.put("/webhooks/update", json=json_payload) + + return None diff --git a/seam/routes/workspaces.py b/seam/routes/workspaces.py index bcfb26d6..2573ea95 100644 --- a/seam/routes/workspaces.py +++ b/seam/routes/workspaces.py @@ -1,10 +1,13 @@ from typing import Optional, Any, List, Dict, Literal, Union import abc -from ..client import SeamHttpClient +from ..client import SeamHttpClient, AsyncSeamHttpClient from ..route import route_metadata from ..null import Null from ..resources import Workspace, ActionAttempt -from ..modules.action_attempts import resolve_action_attempt +from ..modules.action_attempts import ( + resolve_action_attempt, + resolve_action_attempt_async, +) class AbstractWorkspaces(abc.ABC): @@ -104,6 +107,103 @@ def update( raise NotImplementedError() +class AbstractAsyncWorkspaces(abc.ABC): + + @abc.abstractmethod + async def create( + self, + *, + name: str, + company_name: Optional[str] = None, + connect_partner_name: Optional[Union[str, Null]] = None, + connect_webview_customization: Optional[Dict[str, Any]] = None, + is_sandbox: Optional[bool] = None, + organization_id: Optional[str] = None, + webview_logo_shape: Optional[str] = None, + webview_primary_button_color: Optional[str] = None, + webview_primary_button_text_color: Optional[str] = None, + webview_success_message: Optional[str] = None, + ) -> Workspace: + """Creates a new `workspace `_. + + :param name: Name of the new workspace. + + :param company_name: Company name for the new workspace. + + :param connect_partner_name: Deprecated: Use ``company_name`` instead. Connect partner name for the new workspace. + + :param connect_webview_customization: `Connect Webview `_ customizations for the new workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + + :param is_sandbox: Indicates whether the new workspace is a `sandbox workspace `_. + + :param organization_id: ID of the organization to associate with the new workspace. + + :param webview_logo_shape: Deprecated: Use ``connect_webview_customization.webview_logo_shape`` instead. + + :param webview_primary_button_color: Deprecated: Use ``connect_webview_customization.webview_primary_button_color`` instead. + + :param webview_primary_button_text_color: Deprecated: Use ``connect_webview_customization.webview_primary_button_text_color`` instead. + + :param webview_success_message: Deprecated: Use ``connect_webview_customization.webview_success_message`` instead. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + raise NotImplementedError() + + @abc.abstractmethod + async def get(self) -> Workspace: + """Returns the `workspace `_ associated with the authentication value. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def list(self) -> List[Workspace]: + """Returns a list of `workspaces `_ associated with the authentication value. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def reset_sandbox( + self, *, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + ) -> ActionAttempt: + """Resets the `sandbox workspace `_ associated with the authentication value. Note that this endpoint is only available for sandbox workspaces. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK""" + raise NotImplementedError() + + @abc.abstractmethod + async def update( + self, + *, + connect_partner_name: Optional[str] = None, + connect_webview_customization: Optional[Dict[str, Any]] = None, + is_publishable_key_auth_enabled: Optional[bool] = None, + is_suspended: Optional[bool] = None, + name: Optional[str] = None, + organization_id: Optional[str] = None, + ) -> None: + """Updates the `workspace `_ associated with the authentication value. + + :param connect_partner_name: Connect partner name for the workspace. + + :param connect_webview_customization: `Connect Webview `_ customizations for the workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + + :param is_publishable_key_auth_enabled: Indicates whether publishable key authentication is enabled for this workspace. + + :param is_suspended: Indicates whether the workspace is suspended. + + :param name: Name of the workspace. + + :param organization_id: ID of the organization to assign the workspace to. The authenticated user must be the owner of the workspace and an admin of the target organization. + """ + raise NotImplementedError() + + class Workspaces(AbstractWorkspaces): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client @@ -291,3 +391,192 @@ def update( self.client.patch("/workspaces/update", json=json_payload) return None + + +class AsyncWorkspaces(AbstractAsyncWorkspaces): + def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]): + self.client = client + self.defaults = defaults + + @route_metadata( + path="/workspaces/create", has_required_parameters=True, has_pagination=False + ) + async def create( + self, + *, + name: str, + company_name: Optional[str] = None, + connect_partner_name: Optional[Union[str, Null]] = None, + connect_webview_customization: Optional[Dict[str, Any]] = None, + is_sandbox: Optional[bool] = None, + organization_id: Optional[str] = None, + webview_logo_shape: Optional[str] = None, + webview_primary_button_color: Optional[str] = None, + webview_primary_button_text_color: Optional[str] = None, + webview_success_message: Optional[str] = None, + ) -> Workspace: + """Creates a new `workspace `_. + + :param name: Name of the new workspace. + + :param company_name: Company name for the new workspace. + + :param connect_partner_name: Deprecated: Use ``company_name`` instead. Connect partner name for the new workspace. + + :param connect_webview_customization: `Connect Webview `_ customizations for the new workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + + :param is_sandbox: Indicates whether the new workspace is a `sandbox workspace `_. + + :param organization_id: ID of the organization to associate with the new workspace. + + :param webview_logo_shape: Deprecated: Use ``connect_webview_customization.webview_logo_shape`` instead. + + :param webview_primary_button_color: Deprecated: Use ``connect_webview_customization.webview_primary_button_color`` instead. + + :param webview_primary_button_text_color: Deprecated: Use ``connect_webview_customization.webview_primary_button_text_color`` instead. + + :param webview_success_message: Deprecated: Use ``connect_webview_customization.webview_success_message`` instead. + + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} + + if name is not None: + json_payload["name"] = name + if company_name is not None: + json_payload["company_name"] = company_name + if connect_partner_name is not None: + json_payload["connect_partner_name"] = connect_partner_name + if connect_webview_customization is not None: + json_payload["connect_webview_customization"] = ( + connect_webview_customization + ) + if is_sandbox is not None: + json_payload["is_sandbox"] = is_sandbox + if organization_id is not None: + json_payload["organization_id"] = organization_id + if webview_logo_shape is not None: + json_payload["webview_logo_shape"] = webview_logo_shape + if webview_primary_button_color is not None: + json_payload["webview_primary_button_color"] = webview_primary_button_color + if webview_primary_button_text_color is not None: + json_payload["webview_primary_button_text_color"] = ( + webview_primary_button_text_color + ) + if webview_success_message is not None: + json_payload["webview_success_message"] = webview_success_message + + if not json_payload: + raise ValueError( + "At least one parameter is required for /workspaces/create" + ) + + res = await self.client.post("/workspaces/create", json=json_payload) + + return Workspace.from_dict(res["workspace"]) + + @route_metadata( + path="/workspaces/get", has_required_parameters=False, has_pagination=False + ) + async def get(self) -> Workspace: + """Returns the `workspace `_ associated with the authentication value. + + :returns: OK""" + params: Dict[str, Any] = {} + + res = await self.client.get("/workspaces/get", params=params) + + return Workspace.from_dict(res["workspace"]) + + @route_metadata( + path="/workspaces/list", has_required_parameters=False, has_pagination=False + ) + async def list(self) -> List[Workspace]: + """Returns a list of `workspaces `_ associated with the authentication value. + + :returns: OK""" + params: Dict[str, Any] = {} + + res = await self.client.get("/workspaces/list", params=params) + + return [Workspace.from_dict(item) for item in res["workspaces"]] + + @route_metadata( + path="/workspaces/reset_sandbox", + has_required_parameters=False, + has_pagination=False, + ) + async def reset_sandbox( + self, *, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + ) -> ActionAttempt: + """Resets the `sandbox workspace `_ associated with the authentication value. Note that this endpoint is only available for sandbox workspaces. + + :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. + + :returns: OK""" + json_payload: Dict[str, Any] = {} + + res = await self.client.post("/workspaces/reset_sandbox", json=json_payload) + + wait_for_action_attempt = ( + self.defaults.get("wait_for_action_attempt") + if wait_for_action_attempt is None + else wait_for_action_attempt + ) + + return await resolve_action_attempt_async( + client=self.client, + action_attempt=ActionAttempt.from_dict(res["action_attempt"]), + wait_for_action_attempt=wait_for_action_attempt, + ) + + @route_metadata( + path="/workspaces/update", has_required_parameters=False, has_pagination=False + ) + async def update( + self, + *, + connect_partner_name: Optional[str] = None, + connect_webview_customization: Optional[Dict[str, Any]] = None, + is_publishable_key_auth_enabled: Optional[bool] = None, + is_suspended: Optional[bool] = None, + name: Optional[str] = None, + organization_id: Optional[str] = None, + ) -> None: + """Updates the `workspace `_ associated with the authentication value. + + :param connect_partner_name: Connect partner name for the workspace. + + :param connect_webview_customization: `Connect Webview `_ customizations for the workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + + :param is_publishable_key_auth_enabled: Indicates whether publishable key authentication is enabled for this workspace. + + :param is_suspended: Indicates whether the workspace is suspended. + + :param name: Name of the workspace. + + :param organization_id: ID of the organization to assign the workspace to. The authenticated user must be the owner of the workspace and an admin of the target organization. + """ + json_payload: Dict[str, Any] = {} + + if connect_partner_name is not None: + json_payload["connect_partner_name"] = connect_partner_name + if connect_webview_customization is not None: + json_payload["connect_webview_customization"] = ( + connect_webview_customization + ) + if is_publishable_key_auth_enabled is not None: + json_payload["is_publishable_key_auth_enabled"] = ( + is_publishable_key_auth_enabled + ) + if is_suspended is not None: + json_payload["is_suspended"] = is_suspended + if name is not None: + json_payload["name"] = name + if organization_id is not None: + json_payload["organization_id"] = organization_id + + await self.client.patch("/workspaces/update", json=json_payload) + + return None diff --git a/seam/seam.py b/seam/seam.py index 7a42d895..dfe59253 100644 --- a/seam/seam.py +++ b/seam/seam.py @@ -4,10 +4,10 @@ from .constants import DEFAULT_TIMEOUT from .parse_options import parse_options -from .routes import Routes -from .models import AbstractSeam -from .client import SeamHttpClient -from .paginator import SeamPaginator +from .routes import AsyncRoutes, Routes +from .models import AbstractAsyncSeam, AbstractSeam +from .client import AsyncSeamHttpClient, SeamHttpClient +from .paginator import AsyncSeamPaginator, SeamPaginator class Seam(AbstractSeam): @@ -140,6 +140,16 @@ def create_paginator( return SeamPaginator(self.client, request, params) + def close(self) -> None: + """Close the underlying HTTP client and its connection pool.""" + self.client.close() + + def __enter__(self) -> Self: + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.close() + @classmethod def from_api_key( cls, @@ -228,3 +238,229 @@ def from_personal_access_token( timeout=timeout, httpx_options=httpx_options, ) + + +class AsyncSeam(AbstractAsyncSeam): + """Async variant of :class:`Seam` for use inside an event loop. + + Exposes the same route namespaces and method signatures as :class:`Seam`, + but every API method is a coroutine that must be awaited. Use it as an + async context manager, or call :meth:`close` when done, to release the + underlying connection pool. + + :ivar defaults: Default settings for API requests + :vartype defaults: Dict[str, Any] + :ivar client: The async HTTP client used for making API requests + :vartype client: AsyncSeamHttpClient + :ivar wait_for_action_attempt: Controls whether to wait for an action + attempt to complete + :vartype wait_for_action_attempt: Union[bool, Dict[str, float]] + + For more information about the Seam API, visit https://docs.seam.co/ + """ + + def __init__( + self, + api_key: Optional[str] = None, + *, + personal_access_token: Optional[str] = None, + workspace_id: Optional[str] = None, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + retries: Optional[Retry] = None, + timeout: Optional[float] = DEFAULT_TIMEOUT, + httpx_options: Optional[Dict[str, Any]] = None, + ): + """Initialize an AsyncSeam client instance. + + Accepts the same options as :class:`Seam`. The constructor performs no + I/O, so it may be called outside an event loop. + + :param api_key: The API key for authenticating with Seam. Mutually + exclusive with personal_access_token. Read from the SEAM_API_KEY + environment variable when omitted + :type api_key: Optional[str] + :param personal_access_token: A personal access token for + authenticating with Seam. Mutually exclusive with api_key. Read + from the SEAM_PERSONAL_ACCESS_TOKEN environment variable when + omitted + :type personal_access_token: Optional[str] + :param workspace_id: The ID of the workspace to interact with. + Required when using a personal access token. Read from the + SEAM_WORKSPACE_ID environment variable when omitted + :type workspace_id: Optional[str] + :param endpoint: The custom API endpoint URL. If not provided, the + default Seam API endpoint will be used + :type endpoint: Optional[str] + :param wait_for_action_attempt: Controls whether to wait for an + action attempt to complete. Can be a boolean or a dictionary with + 'timeout' and 'poll_interval' keys + :type wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] + :param retries: Configuration for retry behavior on failed requests + :type retries: Optional[httpx_retries.Retry] + :param timeout: The request timeout in seconds. Defaults to 30 + seconds. Pass None for no timeout + :type timeout: Optional[float] + :param httpx_options: Options passed through to the underlying + httpx AsyncClient, for control the other options do not cover + :type httpx_options: Optional[Dict[str, Any]] + + :raises SeamInvalidOptionsError: If neither api_key nor + personal_access_token is provided, or if workspace_id is missing + when using a personal access token + :raises SeamInvalidTokenError: If the provided API key or personal + access token format is invalid + """ + + self.wait_for_action_attempt = wait_for_action_attempt + auth_headers, endpoint = parse_options( + api_key=api_key, + personal_access_token=personal_access_token, + workspace_id=workspace_id, + endpoint=endpoint, + ) + self.defaults = {"wait_for_action_attempt": wait_for_action_attempt} + + self.client = AsyncSeamHttpClient( + base_url=endpoint, + auth_headers=auth_headers, + retries=retries, + timeout=timeout, + httpx_options=httpx_options, + ) + + # AsyncSeam and AsyncRoutes are siblings under AbstractAsyncRoutes + # rather than parent and child, so borrowing this initializer to + # attach the route namespaces passes a self the signature does not + # admit. + AsyncRoutes.__init__(self, client=self.client, defaults=self.defaults) # type: ignore[arg-type] + + def create_paginator( + self, request: Callable, params: Optional[Dict[str, Any]] = None, / + ) -> AsyncSeamPaginator: + """ + Creates an AsyncSeamPaginator instance for iterating through list endpoints. + + This is a helper method to simplify the process of paginating through + API results. + + Args: + request: The API route method function to call for fetching pages + (e.g., connected_accounts.list). + params: Optional dictionary of initial parameters to pass to the request + function. + + Returns: + An initialized paginator object ready to fetch pages. + + Example: + >>> connected_accounts_paginator = seam.create_paginator(seam.connected_accounts.list) + >>> async for connected_account in connected_accounts_paginator.flatten(): + >>> print(connected_account.account_type_display_name) + """ + if not getattr(request, "__seam_has_pagination__", False): + raise ValueError("Cannot create a paginator for a non-paginated endpoint") + + has_required_parameters = getattr( + request, "__seam_has_required_parameters__", False + ) + if has_required_parameters and ( + not params or not any(value is not None for value in params.values()) + ): + path = getattr(request, "__seam_path__", "this endpoint") + raise ValueError(f"At least one parameter is required for {path}") + + return AsyncSeamPaginator(self.client, request, params) + + async def close(self) -> None: + """Close the underlying HTTP client and its connection pool.""" + await self.client.aclose() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, exc_type, exc_value, traceback) -> None: + await self.close() + + @classmethod + def from_api_key( + cls, + api_key: str, + *, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + retries: Optional[Retry] = None, + timeout: Optional[float] = DEFAULT_TIMEOUT, + httpx_options: Optional[Dict[str, Any]] = None, + ) -> Self: + """Create an AsyncSeam instance using an API key. + + :param api_key: The API key for authenticating with Seam + :type api_key: str + :param endpoint: The custom API endpoint URL. If not provided, the + default Seam API endpoint will be used + :type endpoint: Optional[str] + :param wait_for_action_attempt: Controls whether to wait for an + action attempt to complete. Can be a boolean or a dictionary with + 'timeout' and 'poll_interval' keys + :type wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] + :return: A new instance of the AsyncSeam class authenticated with the + provided API key + :rtype: Self + + :Example: + + >>> seam = AsyncSeam.from_api_key("your-api-key-here") + """ + return cls( + api_key, + endpoint=endpoint, + wait_for_action_attempt=wait_for_action_attempt, + retries=retries, + timeout=timeout, + httpx_options=httpx_options, + ) + + @classmethod + def from_personal_access_token( + cls, + personal_access_token: str, + workspace_id: str, + *, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + retries: Optional[Retry] = None, + timeout: Optional[float] = DEFAULT_TIMEOUT, + httpx_options: Optional[Dict[str, Any]] = None, + ) -> Self: + """Create an AsyncSeam instance using a personal access token. + + :param personal_access_token: The personal access token for + authenticating with Seam + :type personal_access_token: str + :param workspace_id: The ID of the workspace to interact with + :type workspace_id: str + :param endpoint: The custom API endpoint URL. If not provided, the + default Seam API endpoint will be used + :type endpoint: Optional[str] + :param wait_for_action_attempt: Controls whether to wait for an + action attempt to complete. Can be a boolean or a dictionary with + 'timeout' and 'poll_interval' keys + :type wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] + :return: A new instance of the AsyncSeam class authenticated with the + provided personal access token + :rtype: Self + + :Example: + + >>> seam = AsyncSeam.from_personal_access_token("your-token-here", "workspace-id") + """ + return cls( + personal_access_token=personal_access_token, + workspace_id=workspace_id, + endpoint=endpoint, + wait_for_action_attempt=wait_for_action_attempt, + retries=retries, + timeout=timeout, + httpx_options=httpx_options, + ) diff --git a/seam/seam_without_workspace.py b/seam/seam_without_workspace.py index 4a38a9bf..9b3fb3aa 100644 --- a/seam/seam_without_workspace.py +++ b/seam/seam_without_workspace.py @@ -4,9 +4,9 @@ from .constants import DEFAULT_TIMEOUT from .parse_options import parse_without_workspace_options -from .client import SeamHttpClient -from .models import AbstractSeamWithoutWorkspace -from .routes.workspaces import Workspaces +from .client import AsyncSeamHttpClient, SeamHttpClient +from .models import AbstractAsyncSeamWithoutWorkspace, AbstractSeamWithoutWorkspace +from .routes.workspaces import AsyncWorkspaces, Workspaces class WorkspacesProxy: @@ -142,3 +142,156 @@ def from_personal_access_token( timeout=timeout, httpx_options=httpx_options, ) + + def close(self) -> None: + """Close the underlying HTTP client and its connection pool.""" + self.client.close() + + def __enter__(self) -> Self: + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.close() + + +class AsyncWorkspacesProxy: + """Proxy to expose only the 'create' and 'list' methods of AsyncWorkspaces.""" + + def __init__(self, workspaces): + self._workspaces = workspaces + + async def list(self, **kwargs): + return await self._workspaces.list(**kwargs) + + async def create(self, **kwargs): + return await self._workspaces.create(**kwargs) + + +class AsyncSeamWithoutWorkspace(AbstractAsyncSeamWithoutWorkspace): + """Async variant of :class:`SeamWithoutWorkspace` for use inside an event loop. + + Exposes the same workspace operations, but every API method is a coroutine + that must be awaited. Use it as an async context manager, or call + :meth:`close` when done, to release the underlying connection pool. + + :ivar wait_for_action_attempt: Controls whether to wait for an action + attempt to complete + :vartype wait_for_action_attempt: Union[bool, Dict[str, float]] + :ivar client: The async HTTP client used for making API requests + :vartype client: AsyncSeamHttpClient + :ivar workspaces: Proxy to access workspace-related operations + :vartype workspaces: AsyncWorkspacesProxy + """ + + def __init__( + self, + personal_access_token: Optional[str] = None, + *, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + retries: Optional[Retry] = None, + timeout: Optional[float] = DEFAULT_TIMEOUT, + httpx_options: Optional[Dict[str, Any]] = None, + ): + """ + Initialize an AsyncSeamWithoutWorkspace client instance. + + Accepts the same options as :class:`SeamWithoutWorkspace`. The + constructor performs no I/O, so it may be called outside an event + loop. + + :param personal_access_token: A personal access token for + authenticating with Seam. Read from the + SEAM_PERSONAL_ACCESS_TOKEN environment variable when omitted + :type personal_access_token: Optional[str] + :param endpoint: The custom API endpoint URL. If not provided, + the default Seam API endpoint will be used + :type endpoint: Optional[str] + :param wait_for_action_attempt: Controls whether to wait for an + action attempt to complete. Can be a boolean or a dictionary with + 'timeout' and 'poll_interval' keys + :type wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] + :param retries: Configuration for retry behavior on failed requests + :type retries: Optional[httpx_retries.Retry] + :param timeout: The request timeout in seconds. Defaults to 30 + seconds. Pass None for no timeout + :type timeout: Optional[float] + :param httpx_options: Options passed through to the underlying + httpx AsyncClient, for control the other options do not cover + :type httpx_options: Optional[Dict[str, Any]] + + :raises SeamInvalidOptionsError: If no personal_access_token is provided + and the SEAM_PERSONAL_ACCESS_TOKEN environment variable is not set + :raises SeamInvalidTokenError: If the provided personal access token format is invalid + """ + + self.wait_for_action_attempt = wait_for_action_attempt + auth_headers, endpoint = parse_without_workspace_options( + personal_access_token=personal_access_token, + endpoint=endpoint, + ) + + self.client = AsyncSeamHttpClient( + base_url=endpoint, + auth_headers=auth_headers, + retries=retries, + timeout=timeout, + httpx_options=httpx_options, + ) + + defaults = {"wait_for_action_attempt": wait_for_action_attempt} + + self._workspaces = AsyncWorkspaces(client=self.client, defaults=defaults) + self.workspaces = AsyncWorkspacesProxy(self._workspaces) + + @classmethod + def from_personal_access_token( + cls, + personal_access_token: str, + *, + endpoint: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, + retries: Optional[Retry] = None, + timeout: Optional[float] = DEFAULT_TIMEOUT, + httpx_options: Optional[Dict[str, Any]] = None, + ) -> Self: + """ + Create an AsyncSeamWithoutWorkspace instance using a personal access token. + + :param personal_access_token: The personal access token for authenticating with Seam + :type personal_access_token: str + :param endpoint: The custom API endpoint URL. If not provided, the default Seam API endpoint will be used + :type endpoint: Optional[str] + :param wait_for_action_attempt: Controls whether to wait for an + action attempt to complete. Can be a boolean or a dictionary with + 'timeout' and 'poll_interval' keys + :type wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] + :param retries: Configuration for retry behavior on failed requests + :type retries: Optional[httpx_retries.Retry] + :return: A new instance of the AsyncSeamWithoutWorkspace class + authenticated with the provided personal access token + :rtype: Self + + :Example: + + >>> seam = AsyncSeamWithoutWorkspace.from_personal_access_token("your-personal-access-token-here") + """ + + return cls( + personal_access_token=personal_access_token, + endpoint=endpoint, + wait_for_action_attempt=wait_for_action_attempt, + retries=retries, + timeout=timeout, + httpx_options=httpx_options, + ) + + async def close(self) -> None: + """Close the underlying HTTP client and its connection pool.""" + await self.client.aclose() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, exc_type, exc_value, traceback) -> None: + await self.close() diff --git a/test/async_seam_test.py b/test/async_seam_test.py new file mode 100644 index 00000000..b3895d51 --- /dev/null +++ b/test/async_seam_test.py @@ -0,0 +1,201 @@ +import asyncio + +import pytest + +from seam import ( + AsyncSeam, + AsyncSeamWithoutWorkspace, + Retry, + SeamHttpApiError, + SeamHttpUnauthorizedError, +) +from seam.paginator import AsyncSeamPaginator + +SERVICE_UNAVAILABLE = (503, "Service Unavailable") +DEVICES = (200, {"devices": [{"device_id": "august_device_1"}]}) + + +async def test_async_seam_from_api_key_gets_a_device(server): + endpoint, seed = server + + async with AsyncSeam.from_api_key( + seed["seam_apikey1_token"], endpoint=endpoint + ) as seam: + device = await seam.devices.get(device_id=seed["august_device_1"]) + + assert device.workspace_id == seed["seed_workspace_1"] + assert device.device_id == seed["august_device_1"] + + +async def test_async_seam_lists_devices(async_seam: AsyncSeam): + devices = await async_seam.devices.list() + + assert len(devices) > 0 + + +async def test_async_seam_runs_requests_concurrently(async_seam: AsyncSeam): + devices, connected_accounts, workspace = await asyncio.gather( + async_seam.devices.list(), + async_seam.connected_accounts.list(), + async_seam.workspaces.get(), + ) + + assert len(devices) > 0 + assert len(connected_accounts) > 0 + assert workspace.workspace_id is not None + + +async def test_async_seam_close_is_idempotent(server): + endpoint, seed = server + seam = AsyncSeam(api_key=seed["seam_apikey1_token"], endpoint=endpoint) + + await seam.devices.list() + + await seam.close() + await seam.close() + + +async def test_async_wait_for_action_attempt_waits_by_default(server): + endpoint, seed = server + + async with AsyncSeam.from_api_key( + seed["seam_apikey1_token"], endpoint=endpoint + ) as seam: + action_attempt = await seam.locks.unlock_door(device_id=seed["august_device_1"]) + + assert action_attempt.status == "success" + + +async def test_async_wait_for_action_attempt_returns_pending_when_disabled(server): + endpoint, seed = server + + async with AsyncSeam.from_api_key( + seed["seam_apikey1_token"], endpoint=endpoint, wait_for_action_attempt=False + ) as seam: + action_attempt = await seam.locks.unlock_door(device_id=seed["august_device_1"]) + + assert action_attempt.status == "pending" + + +async def test_async_wait_for_action_attempt_accepts_per_request_override(server): + endpoint, seed = server + + async with AsyncSeam.from_api_key( + seed["seam_apikey1_token"], endpoint=endpoint, wait_for_action_attempt=False + ) as seam: + action_attempt = await seam.locks.unlock_door( + device_id=seed["august_device_1"], wait_for_action_attempt=True + ) + + assert action_attempt.status == "success" + + +async def test_async_create_paginator_returns_an_async_paginator( + async_seam: AsyncSeam, +): + paginator = async_seam.create_paginator(async_seam.connected_accounts.list) + + assert isinstance(paginator, AsyncSeamPaginator) + + +async def test_async_paginator_first_and_next_page(async_seam: AsyncSeam): + paginator = async_seam.create_paginator( + async_seam.connected_accounts.list, {"limit": 2} + ) + first_page_accounts, pagination = await paginator.first_page() + + assert len(first_page_accounts) == 2 + assert pagination is not None + assert pagination.has_next_page is True + assert pagination.next_page_cursor is not None + + next_page_accounts, next_pagination = await paginator.next_page( + pagination.next_page_cursor + ) + + assert len(next_page_accounts) == 1 + assert next_pagination is not None + assert next_pagination.has_next_page is False + + +async def test_async_paginator_flatten_to_list(async_seam: AsyncSeam): + all_connected_accounts = await async_seam.connected_accounts.list() + + paginator = async_seam.create_paginator( + async_seam.connected_accounts.list, {"limit": 1} + ) + paginated_accounts = await paginator.flatten_to_list() + + assert len(paginated_accounts) > 1 + assert len(paginated_accounts) == len(all_connected_accounts) + + +async def test_async_paginator_flatten(async_seam: AsyncSeam): + all_connected_accounts = await async_seam.connected_accounts.list() + + paginator = async_seam.create_paginator( + async_seam.connected_accounts.list, {"limit": 1} + ) + + collected_accounts = [account async for account in paginator.flatten()] + + assert len(collected_accounts) == len(all_connected_accounts) + + +async def test_async_seam_raises_unauthorized_error(server): + endpoint, _ = server + + async with AsyncSeam(api_key="seam_invalid_api_key", endpoint=endpoint) as seam: + with pytest.raises(SeamHttpUnauthorizedError) as exc_info: + await seam.devices.list() + + assert exc_info.value.status_code == 401 + assert exc_info.value.code == "unauthorized" + + +async def test_async_seam_raises_api_error(async_seam: AsyncSeam): + with pytest.raises(SeamHttpApiError) as exc_info: + await async_seam.devices.get(device_id="unknown-device-id") + + assert exc_info.value.status_code == 404 + assert exc_info.value.code == "device_not_found" + + +async def test_async_seam_retries_service_unavailable_responses(recording_server): + expected_retry_count = 2 + responses = [SERVICE_UNAVAILABLE, SERVICE_UNAVAILABLE, DEVICES] + + with recording_server(responses) as (endpoint, requests): + async with AsyncSeam.from_api_key( + "seam_apikey_token", + endpoint=endpoint, + retries=Retry(total=expected_retry_count, backoff_factor=0.1), + ) as seam: + devices = await seam.devices.list() + + assert len(devices) == 1 + assert len(requests) == expected_retry_count + 1 + + +async def test_async_seam_sends_sdk_headers(recording_server): + with recording_server([DEVICES]) as (endpoint, requests): + async with AsyncSeam.from_api_key( + "seam_apikey_token", endpoint=endpoint + ) as seam: + await seam.devices.list() + + headers = requests[0]["headers"] + + assert headers["seam-sdk-name"] == "seamapi/python" + assert headers["authorization"] == "Bearer seam_apikey_token" + + +async def test_async_seam_without_workspace_lists_workspaces(server): + endpoint, seed = server + + async with AsyncSeamWithoutWorkspace.from_personal_access_token( + seed["seam_at1_token"], endpoint=endpoint + ) as seam: + workspaces = await seam.workspaces.list() + + assert len(workspaces) > 0 diff --git a/test/conftest.py b/test/conftest.py index 0af77f8d..5c3c9710 100755 --- a/test/conftest.py +++ b/test/conftest.py @@ -12,7 +12,7 @@ import pytest -from seam import Seam +from seam import AsyncSeam, Seam SERVER_STARTUP_TIMEOUT = 30 SERVER_SHUTDOWN_TIMEOUT = 10 @@ -43,6 +43,16 @@ def seam_fixture(server): return Seam(api_key=seed["seam_apikey1_token"], endpoint=endpoint) +@pytest.fixture(name="async_seam") +async def async_seam_fixture(server): + """Return an AsyncSeam client authorized against a fake Seam Connect server.""" + + endpoint, seed = server + + async with AsyncSeam(api_key=seed["seam_apikey1_token"], endpoint=endpoint) as seam: + yield seam + + @pytest.fixture(name="recording_server") def recording_server_fixture(): """Return a factory for a server that records requests and replays responses. diff --git a/uv.lock b/uv.lock index 329a9ded..92913f48 100644 --- a/uv.lock +++ b/uv.lock @@ -821,6 +821,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + [[package]] name = "pytest-cov" version = "7.1.0" @@ -957,6 +970,7 @@ dev = [ { name = "mypy" }, { name = "pylint" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-runner" }, { name = "pytest-watch" }, @@ -976,6 +990,7 @@ dev = [ { name = "mypy", specifier = ">=2.3.0,<3" }, { name = "pylint", specifier = ">=4.0.7,<5" }, { name = "pytest", specifier = ">=9.1.1,<10" }, + { name = "pytest-asyncio", specifier = ">=1.0.0,<2" }, { name = "pytest-cov", specifier = ">=7.1.0,<8" }, { name = "pytest-runner", specifier = ">=6.0.1,<7" }, { name = "pytest-watch", specifier = ">=4.2.0,<5" },