diff --git a/README.rst b/README.rst index 3c6d228..51a49c0 100644 --- a/README.rst +++ b/README.rst @@ -63,6 +63,8 @@ Contents * `Webhooks`_ + * `Omitted params and null params`_ + * `Advanced Usage`_ * `Setting the endpoint`_ @@ -73,6 +75,8 @@ Contents * `Configuring the httpx client`_ + * `Serializing URL search params`_ + * `Development and Testing`_ * `Quickstart`_ @@ -447,6 +451,47 @@ see the `Svix docs for more examples in specific frameworks {{returnType}} \ No newline at end of file +{{name}}(self{{#if params}}, *{{else}}{{#if (eq returnType "ActionAttempt")}}, *{{/if}}{{/if}}{{#each params}}, {{name}}: {{#if required}}{{nullableType type isNullable}}{{else}}Optional[{{nullableType type isNullable}}] = None{{/if}}{{/each}}{{#if (eq returnType "ActionAttempt")}}, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None{{/if}}) -> {{returnType}} \ No newline at end of file diff --git a/codegen/layouts/partials/route-method.hbs b/codegen/layouts/partials/route-method.hbs index d554944..4af50ca 100644 --- a/codegen/layouts/partials/route-method.hbs +++ b/codegen/layouts/partials/route-method.hbs @@ -1,13 +1,13 @@ def {{> method-signature}}: """{{> method-docstring}}""" - json_payload: Dict[str, Any] = {} + {{payloadVar}}: Dict[str, Any] = {} {{#each params}} if {{name}} is not None: - json_payload["{{name}}"] = {{name}} + {{../payloadVar}}["{{name}}"] = {{name}} {{/each}} - {{#unless (eq returnType "None")}}res = {{/unless}}self.client.post("{{path}}", json=json_payload) + {{#unless (eq returnType "None")}}res = {{/unless}}self.client.{{httpVerb}}("{{path}}", {{payloadArg}}={{payloadVar}}) {{#if (eq returnType "ActionAttempt")}} wait_for_action_attempt = ( diff --git a/codegen/layouts/route.hbs b/codegen/layouts/route.hbs index 1793adf..4052627 100644 --- a/codegen/layouts/route.hbs +++ b/codegen/layouts/route.hbs @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null {{#if resourceClasses}} from ..resources import ({{#each resourceClasses}}{{this}}{{#unless @last}},{{/unless}}{{/each}}) {{/if}} diff --git a/codegen/lib/class-model.ts b/codegen/lib/class-model.ts index 9937221..8644ed8 100644 --- a/codegen/lib/class-model.ts +++ b/codegen/lib/class-model.ts @@ -9,11 +9,13 @@ export interface ClassMethodParameter { deprecationMessage: string position?: number | undefined required?: boolean | undefined + isNullable?: boolean | undefined } export interface ClassMethod { methodName: string path: string + semanticMethod: string description: string responseDescription: string isDeprecated: boolean diff --git a/codegen/lib/handlebars-helpers.ts b/codegen/lib/handlebars-helpers.ts index dd4abed..d7d9adc 100644 --- a/codegen/lib/handlebars-helpers.ts +++ b/codegen/lib/handlebars-helpers.ts @@ -56,3 +56,8 @@ export const pythonIdentifier = (name: string): string => export const isListType = (type: string): boolean => type.startsWith('List[') export const listItemType = (type: string): string => type.slice(5, -1) + +// A nullable param accepts the NULL sentinel, which is sent as null. +// A param set to None is omitted from the request instead. +export const nullableType = (type: string, isNullable: boolean): string => + isNullable ? `Union[${type}, Null]` : type diff --git a/codegen/lib/layouts/route.ts b/codegen/lib/layouts/route.ts index 8e57326..24ad350 100644 --- a/codegen/lib/layouts/route.ts +++ b/codegen/lib/layouts/route.ts @@ -11,6 +11,9 @@ import { export interface MethodLayoutContext { name: string path: string + httpVerb: string + payloadVar: string + payloadArg: string description: string responseDescription: string isDeprecated: boolean @@ -22,6 +25,7 @@ export interface MethodLayoutContext { isDeprecated: boolean deprecationMessage: string required: boolean + isNullable: boolean }> returnPath: string[] returnType: string @@ -51,11 +55,26 @@ export interface RouteLayoutContext { methods: MethodLayoutContext[] } +// Only GET carries its params in the query string; every other method, +// including DELETE, reads them from a JSON request body. +const getRequestLayoutContext = ( + semanticMethod: string, +): Pick => { + const httpVerb = semanticMethod.toLowerCase() + + if (semanticMethod === 'GET') { + return { httpVerb, payloadVar: 'params', payloadArg: 'params' } + } + + return { httpVerb, payloadVar: 'json_payload', payloadArg: 'json' } +} + export const getMethodLayoutContext = ( method: ClassMethod, ): MethodLayoutContext => ({ name: method.methodName, path: method.path, + ...getRequestLayoutContext(method.semanticMethod), description: method.description, responseDescription: method.responseDescription, isDeprecated: method.isDeprecated, @@ -67,6 +86,7 @@ export const getMethodLayoutContext = ( isDeprecated: parameter.isDeprecated, deprecationMessage: parameter.deprecationMessage, required: parameter.required ?? false, + isNullable: parameter.isNullable ?? false, })), returnPath: method.returnPath, returnType: method.returnResource, diff --git a/codegen/lib/routes.ts b/codegen/lib/routes.ts index 0cd065d..302e7f8 100644 --- a/codegen/lib/routes.ts +++ b/codegen/lib/routes.ts @@ -89,6 +89,7 @@ export const routes = ( cls.methods.push({ methodName: endpoint.name, path: endpoint.path, + semanticMethod: endpoint.request.semanticMethod, description: endpoint.description, responseDescription: endpoint.response.description, isDeprecated: endpoint.isDeprecated, @@ -101,6 +102,7 @@ export const routes = ( deprecationMessage: parameter.deprecationMessage, position: parameter.name === idParameterName ? 0 : undefined, required: parameter.isRequired, + isNullable: parameter.isNullable, })), ...resolveResponse(response), }) diff --git a/package-lock.json b/package-lock.json index 10183c3..23a77b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "": { "name": "@seamapi/python", "devDependencies": { - "@seamapi/blueprint": "^1.1.0", - "@seamapi/fake-seam-connect": "1.86.0", + "@seamapi/blueprint": "^1.4.0", + "@seamapi/fake-seam-connect": "2.0.3", "@seamapi/smith": "^1.1.0", - "@seamapi/types": "1.983.0", + "@seamapi/types": "1.984.0", "change-case": "^5.4.4", "prettier": "^3.2.5" }, @@ -787,9 +787,9 @@ "license": "MIT" }, "node_modules/@seamapi/blueprint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.1.0.tgz", - "integrity": "sha512-wX1HZkA/IK9hDQ6Qdxw5Mo+Ysfh82p9IEXQJafakO9VMbszW6n1U02eEhZHVY3CfzN/duk6t9h1veX0zRlhWBQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.5.0.tgz", + "integrity": "sha512-UhLlcfgxUbnxoi4GoL45Kwvz3d9kfu64enYmdGYHWDbya1lpvBF5E9NVP52OOR9OjSZpQFyIg9mUV1rjgjTyUA==", "dev": true, "license": "MIT", "dependencies": { @@ -798,41 +798,23 @@ }, "engines": { "node": ">=22.11.0", - "npm": ">=10.9.4" - } - }, - "node_modules/@seamapi/fake-devicedb": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@seamapi/fake-devicedb/-/fake-devicedb-1.6.1.tgz", - "integrity": "sha512-w4Ar/s2kPnE5ExJSlpD3sKL8lkF+rLHRROArIRxtR2reHfnDSVwnDt9TzBYkHqgMP/x4o7LUzlRRpobn2xn24A==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18.12.0", - "npm": ">= 9.0.0" - }, - "optionalDependencies": { - "zod": "^3.21.4", - "zustand": "^4.3.7", - "zustand-hoist": "^2.0.0" + "npm": ">=10.0.0" } }, "node_modules/@seamapi/fake-seam-connect": { - "version": "1.86.0", - "resolved": "https://registry.npmjs.org/@seamapi/fake-seam-connect/-/fake-seam-connect-1.86.0.tgz", - "integrity": "sha512-iO5fwtSIPhzmIiLxrFDtCYF/7HTb+ywcGmc3WzWt8Sr0bLQlmwCyTJ8YYZy++Hx0FsnNpa5AmlJuJGul9Y5gZA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@seamapi/fake-seam-connect/-/fake-seam-connect-2.0.3.tgz", + "integrity": "sha512-XJsdSBvBNpm/k7CUttFSOxM41WlOY/65bTXCdUKewr5k1Pj31g2Dmz2mVUCNB5nPgLTC4gXbzpB+2eyakzkD0A==", "dev": true, "license": "MIT", "bin": { "fake-seam-connect": "dist/server.js" }, "engines": { - "node": ">=18.12.0", - "npm": ">= 9.0.0" + "node": ">=22.12.0", + "npm": ">=10.0.0" }, "optionalDependencies": { - "@seamapi/fake-devicedb": ">=1.0.0-rc.0", "zustand": "^4.3.7", "zustand-hoist": "^2.0.0" } @@ -871,9 +853,9 @@ } }, "node_modules/@seamapi/types": { - "version": "1.983.0", - "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.983.0.tgz", - "integrity": "sha512-SMkfn1SVC70x67mtRAvLMJtpFh/0zaStLatb6LA+kz9n/rV1gBS/UlH8SzBFg7iStE22f/VPjkdltHTIY1paoA==", + "version": "1.984.0", + "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.984.0.tgz", + "integrity": "sha512-qHyux+VxfbQ5FqeOuC/uSmF6nbtBtOeeQsinkU5rdarrhi4L1/euCDDBBRfxxG+7bfsEA0BQqQmA3GU/v31j9A==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 1a81634..f3f0373 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,10 @@ } }, "devDependencies": { - "@seamapi/blueprint": "^1.1.0", - "@seamapi/fake-seam-connect": "1.86.0", + "@seamapi/blueprint": "^1.4.0", + "@seamapi/fake-seam-connect": "2.0.3", "@seamapi/smith": "^1.1.0", - "@seamapi/types": "1.983.0", + "@seamapi/types": "1.984.0", "change-case": "^5.4.4", "prettier": "^3.2.5" } diff --git a/seam/__init__.py b/seam/__init__.py index 30e17b6..662b43e 100644 --- a/seam/__init__.py +++ b/seam/__init__.py @@ -15,3 +15,10 @@ ) from .seam_webhook import SeamWebhook from svix.webhooks import WebhookVerificationError as SeamWebhookVerificationError +from .null import NULL, Null +from .utils.url_search_params_serializer import ( + UnserializableParamError, + UrlSearchParams, + serialize_url_search_params, + update_url_search_params, +) diff --git a/seam/client.py b/seam/client.py index fa88416..85013af 100644 --- a/seam/client.py +++ b/seam/client.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from typing import Any, Dict, Optional from importlib.metadata import version import abc @@ -12,6 +13,8 @@ SeamHttpInvalidInputError, SeamHttpUnauthorizedError, ) +from .null import replace_null +from .utils.url_search_params_serializer import serialize_url_search_params SDK_HEADERS = { "seam-sdk-name": "seamapi/python", @@ -73,13 +76,39 @@ def __init__( # httpx.Client promises, so the verb helpers routed through it have to # say so too. Without these overrides callers see the inherited Response # type and indexing the returned payload does not type check. + # httpx also omits json from its get and delete signatures, though the + # Seam API reads the params of a delete from the request body. def get(self, url, **kwargs) -> Any: return self.request("GET", url, **kwargs) def post(self, url, data=None, json=None, **kwargs) -> Any: return self.request("POST", url, data=data, json=json, **kwargs) + def put(self, url, data=None, json=None, **kwargs) -> Any: + return self.request("PUT", url, data=data, json=json, **kwargs) + + def patch(self, url, data=None, json=None, **kwargs) -> Any: + return self.request("PATCH", url, data=data, json=json, **kwargs) + + def delete(self, url, json=None, **kwargs) -> Any: + return self.request("DELETE", url, json=json, **kwargs) + def request(self, method, url, *args, **kwargs) -> Any: + # Route methods omit params set to None, so any remaining NULL sentinel + # is an explicit null and becomes None for JSON serialization. + if "json" in kwargs: + kwargs["json"] = replace_null(kwargs["json"]) + + # Search params are serialized to the Seam API standard, which httpx + # does not implement. The NULL sentinel is serialized to an empty value. + # The query is set on the URL rather than passed to httpx as params, + # because httpx re-encodes a query string it is given, e.g. it escapes + # "*" and unescapes "~", which the standard does not. + if isinstance(kwargs.get("params"), Mapping): + query = serialize_url_search_params(kwargs.pop("params")) + if query: + url = httpx.URL(url, query=query.encode()) + response = super().request(method, url, *args, **kwargs) return self._handle_response(response) diff --git a/seam/modules/action_attempts.py b/seam/modules/action_attempts.py index d764d3c..07a9efb 100644 --- a/seam/modules/action_attempts.py +++ b/seam/modules/action_attempts.py @@ -10,8 +10,8 @@ def get_action_attempt(client: SeamHttpClient, action_attempt_id: str) -> ActionAttempt: - res = client.post( - "/action_attempts/get", json={"action_attempt_id": action_attempt_id} + res = client.get( + "/action_attempts/get", params={"action_attempt_id": action_attempt_id} ) return ActionAttempt.from_dict(res["action_attempt"]) diff --git a/seam/null.py b/seam/null.py new file mode 100644 index 0000000..fe8bc4d --- /dev/null +++ b/seam/null.py @@ -0,0 +1,95 @@ +"""The explicit null sentinel used by request params. + +Python has a single absence value, ``None``, but the Seam API distinguishes +an omitted param from a param explicitly set to null. For example, in an +update request, an omitted param leaves the current value unchanged, +while a null param unsets the current value. + +Since sending null is rarely intended and unsetting a value cannot be undone, +``None`` means the safe option of omitting the param. +Sending null is explicit and always spelled :data:`NULL`. +""" + +from collections.abc import Mapping +from typing import Any + + +class Null: + """Type of the :data:`NULL` sentinel.""" + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __repr__(self): + return "NULL" + + def __bool__(self): + return False + + +NULL = Null() +"""Sentinel for a param explicitly set to null. + +Params set to this sentinel are sent as null, +whereas params set to ``None`` are omitted from the request. + +Use it wherever the Seam API documents null as a meaningful value, e.g., +to unset a value in an update request, or to filter by an unset value: + +.. code-block:: python + + from seam import NULL, Seam + + seam = Seam() + + # Unsets the name, leaving custom_metadata unchanged. + seam.devices.update(device_id=device_id, name=NULL) + + # Lists only the Access Grants which have no access_grant_key. + seam.access_grants.list(access_grant_key=NULL) + +Route methods accept this sentinel only for params the Seam API +documents as nullable, so passing it to any other param is a type error. +""" + + +def is_null(value: Any) -> bool: + """Returns whether a value is the :data:`NULL` sentinel. + + :param value: The value to check + :type value: Any + + :returns: Whether the value is the ``NULL`` sentinel""" + + return isinstance(value, Null) + + +def replace_null(value: Any) -> Any: + """Recursively replaces the :data:`NULL` sentinel with ``None``. + + Returns a copy, so the given value is never modified. + Use this to prepare a request payload for JSON serialization, + where ``None`` is serialized to null. + + :param value: The value to convert + :type value: Any + + :returns: A copy of the value with every ``NULL`` sentinel replaced""" + + if is_null(value): + return None + + if isinstance(value, Mapping): + return {key: replace_null(item) for key, item in value.items()} + + if isinstance(value, list): + return [replace_null(item) for item in value] + + if isinstance(value, tuple): + return tuple(replace_null(item) for item in value) + + return value diff --git a/seam/resources/action_attempt.py b/seam/resources/action_attempt.py index a9bb54b..3141568 100644 --- a/seam/resources/action_attempt.py +++ b/seam/resources/action_attempt.py @@ -124,7 +124,10 @@ class Result(ResourceMapping): :ivar mode: Access method mode. Supported values: ``code``, ``card``, ``mobile_key``, ``cloud_key``. :ivar pending_mutations: Pending mutations for the `access method `_. Indicates operations that are in progress. - """ + + :ivar access_code: + + :ivar noise_threshold:""" @dataclass class AcsCredentialOnEncoder(ResourceMapping): @@ -785,6 +788,8 @@ def from_dict(cls, d: Any): is_ready_for_encoding: Optional[bool] mode: Optional[str] pending_mutations: Optional[List[PendingMutations]] + access_code: Optional[Dict[str, Any]] + noise_threshold: Optional[Dict[str, Any]] # The payload is decoded JSON, so every value read out of it is untyped. # Typing d as Any keeps that at this boundary instead of casting each @@ -862,6 +867,8 @@ def from_dict(cls, d: Any): cls.PendingMutations.from_dict(i) for i in d.get("pending_mutations") or [] ], + access_code=DeepAttrDict(d.get("access_code", None)), + noise_threshold=DeepAttrDict(d.get("noise_threshold", None)), ) action_attempt_id: str diff --git a/seam/resources/device.py b/seam/resources/device.py index 65c2a77..2c45027 100644 --- a/seam/resources/device.py +++ b/seam/resources/device.py @@ -851,19 +851,6 @@ class DormakabaOracodeMetadata(ResourceMapping): :ivar site_name: Site name for a dormakaba Oracode device.""" - @dataclass - class DeviceId(ResourceMapping): - """Device ID for a dormakaba Oracode device.""" - - # The payload is decoded JSON, so every value read out of it is untyped. - # Typing d as Any keeps that at this boundary instead of casting each - # read, and the dataclass fields carry the real types. - @classmethod - def from_dict(cls, d: Any): - # This shape documents no properties, so there is nothing to read. - # pylint: disable=unused-argument - return cls() - @dataclass class PredefinedTimeSlots(ResourceMapping): """Predefined time slots for a dormakaba Oracode device. @@ -921,7 +908,7 @@ def from_dict(cls, d: Any): prefix=d.get("prefix", None), ) - device_id: Optional[DeviceId] + device_id: Optional[str] door_id: Optional[float] door_is_wireless: Optional[bool] door_name: Optional[str] @@ -936,11 +923,7 @@ def from_dict(cls, d: Any): @classmethod def from_dict(cls, d: Any): return cls( - device_id=( - cls.DeviceId.from_dict(d.get("device_id")) - if d.get("device_id") is not None - else None - ), + device_id=d.get("device_id", None), door_id=d.get("door_id", None), door_is_wireless=d.get("door_is_wireless", None), door_name=d.get("door_name", None), @@ -1834,11 +1817,14 @@ class SensiMetadata(ResourceMapping): :ivar dual_setpoints_not_supported: Set to true when the device does not support the /dual-setpoints API endpoint. + :ivar enforced_setpoint_range_celsius: Enforced setpoint range in Celsius for a Sensi device, derived from an OutOfRange API error. + :ivar product_type: Product type for a Sensi device.""" device_id: Optional[str] device_name: Optional[str] dual_setpoints_not_supported: Optional[bool] + enforced_setpoint_range_celsius: Optional[List[float]] product_type: Optional[str] # The payload is decoded JSON, so every value read out of it is untyped. @@ -1852,6 +1838,9 @@ def from_dict(cls, d: Any): dual_setpoints_not_supported=d.get( "dual_setpoints_not_supported", None ), + enforced_setpoint_range_celsius=d.get( + "enforced_setpoint_range_celsius", None + ), product_type=d.get("product_type", None), ) diff --git a/seam/routes/access_codes.py b/seam/routes/access_codes.py index ddccd47..f940cb6 100644 --- a/seam/routes/access_codes.py +++ b/seam/routes/access_codes.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import AccessCode from .access_codes_simulate import AbstractAccessCodesSimulate, AccessCodesSimulate from .access_codes_unmanaged import AbstractAccessCodesUnmanaged, AccessCodesUnmanaged @@ -195,7 +196,7 @@ def list( customer_key: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identifier_key: Optional[str] = None, ) -> List[AccessCode]: @@ -576,7 +577,7 @@ def create_multiple( if use_backup_access_code_pool is not None: json_payload["use_backup_access_code_pool"] = use_backup_access_code_pool - res = self.client.post("/access_codes/create_multiple", json=json_payload) + res = self.client.put("/access_codes/create_multiple", json=json_payload) return [AccessCode.from_dict(item) for item in res["access_codes"]] @@ -594,7 +595,7 @@ def delete(self, *, access_code_id: str, device_id: Optional[str] = None) -> Non if device_id is not None: json_payload["device_id"] = device_id - self.client.post("/access_codes/delete", json=json_payload) + self.client.delete("/access_codes/delete", json=json_payload) return None @@ -604,12 +605,12 @@ def generate_code(self, *, device_id: str) -> AccessCode: :param device_id: ID of the device for which you want to generate a code. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - res = self.client.post("/access_codes/generate_code", json=json_payload) + res = self.client.get("/access_codes/generate_code", params=params) return AccessCode.from_dict(res["generated_code"]) @@ -631,16 +632,16 @@ def get( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if code is not None: - json_payload["code"] = code + params["code"] = code if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - res = self.client.post("/access_codes/get", json=json_payload) + res = self.client.get("/access_codes/get", params=params) return AccessCode.from_dict(res["access_code"]) @@ -654,7 +655,7 @@ def list( customer_key: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identifier_key: Optional[str] = None, ) -> List[AccessCode]: @@ -683,30 +684,30 @@ def list( :param user_identifier_key: Your user ID for the user by which to filter access codes. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_code_ids is not None: - json_payload["access_code_ids"] = access_code_ids + params["access_code_ids"] = access_code_ids if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if access_grant_key is not None: - json_payload["access_grant_key"] = access_grant_key + params["access_grant_key"] = access_grant_key if access_method_id is not None: - json_payload["access_method_id"] = access_method_id + params["access_method_id"] = access_method_id if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/access_codes/list", json=json_payload) + res = self.client.get("/access_codes/list", params=params) return [AccessCode.from_dict(item) for item in res["access_codes"]] @@ -879,7 +880,7 @@ def update( if use_offline_access_code is not None: json_payload["use_offline_access_code"] = use_offline_access_code - self.client.post("/access_codes/update", json=json_payload) + self.client.put("/access_codes/update", json=json_payload) return None @@ -922,6 +923,6 @@ def update_multiple( if starts_at is not None: json_payload["starts_at"] = starts_at - self.client.post("/access_codes/update_multiple", json=json_payload) + 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 3b0b006..b9c6ef5 100644 --- a/seam/routes/access_codes_simulate.py +++ b/seam/routes/access_codes_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import UnmanagedAccessCode diff --git a/seam/routes/access_codes_unmanaged.py b/seam/routes/access_codes_unmanaged.py index e0dd061..dc94357 100644 --- a/seam/routes/access_codes_unmanaged.py +++ b/seam/routes/access_codes_unmanaged.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import UnmanagedAccessCode @@ -66,7 +67,7 @@ def list( *, device_id: str, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identifier_key: Optional[str] = None, ) -> List[UnmanagedAccessCode]: @@ -150,7 +151,7 @@ def convert_to_managed( is_external_modification_allowed ) - self.client.post( + self.client.patch( "/access_codes/unmanaged/convert_to_managed", json=json_payload ) @@ -166,7 +167,7 @@ def delete(self, *, access_code_id: str) -> None: if access_code_id is not None: json_payload["access_code_id"] = access_code_id - self.client.post("/access_codes/unmanaged/delete", json=json_payload) + self.client.delete("/access_codes/unmanaged/delete", json=json_payload) return None @@ -188,16 +189,16 @@ def get( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if code is not None: - json_payload["code"] = code + params["code"] = code if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - res = self.client.post("/access_codes/unmanaged/get", json=json_payload) + res = self.client.get("/access_codes/unmanaged/get", params=params) return UnmanagedAccessCode.from_dict(res["access_code"]) @@ -206,7 +207,7 @@ def list( *, device_id: str, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identifier_key: Optional[str] = None, ) -> List[UnmanagedAccessCode]: @@ -223,20 +224,20 @@ def list( :param user_identifier_key: Your user ID for the user by which to filter unmanaged access codes. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/access_codes/unmanaged/list", json=json_payload) + res = self.client.get("/access_codes/unmanaged/list", params=params) return [UnmanagedAccessCode.from_dict(item) for item in res["access_codes"]] @@ -276,6 +277,6 @@ def update( is_external_modification_allowed ) - self.client.post("/access_codes/unmanaged/update", json=json_payload) + 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 8865a7f..ceed6a1 100644 --- a/seam/routes/access_grants.py +++ b/seam/routes/access_grants.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import AccessGrant, Batch from .access_grants_unmanaged import ( AbstractAccessGrantsUnmanaged, @@ -26,10 +27,10 @@ def create( acs_entrance_ids: Optional[List[str]] = None, customization_profile_id: Optional[str] = None, device_ids: Optional[List[str]] = None, - ends_at: Optional[str] = None, + ends_at: Optional[Union[str, Null]] = None, location: Optional[Dict[str, Any]] = None, location_ids: Optional[List[str]] = None, - name: Optional[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, @@ -121,14 +122,14 @@ def list( *, access_code_id: Optional[str] = None, access_grant_ids: Optional[List[str]] = None, - access_grant_key: Optional[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[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, @@ -183,8 +184,8 @@ def update( *, access_grant_id: Optional[str] = None, access_grant_key: Optional[str] = None, - ends_at: Optional[str] = None, - name: 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. @@ -222,10 +223,10 @@ def create( acs_entrance_ids: Optional[List[str]] = None, customization_profile_id: Optional[str] = None, device_ids: Optional[List[str]] = None, - ends_at: Optional[str] = None, + ends_at: Optional[Union[str, Null]] = None, location: Optional[Dict[str, Any]] = None, location_ids: Optional[List[str]] = None, - name: Optional[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, @@ -310,7 +311,7 @@ def delete(self, *, access_grant_id: str) -> None: if access_grant_id is not None: json_payload["access_grant_id"] = access_grant_id - self.client.post("/access_grants/delete", json=json_payload) + self.client.delete("/access_grants/delete", json=json_payload) return None @@ -327,14 +328,14 @@ def get( :param access_grant_key: Unique key of Access Grant to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if access_grant_key is not None: - json_payload["access_grant_key"] = access_grant_key + params["access_grant_key"] = access_grant_key - res = self.client.post("/access_grants/get", json=json_payload) + res = self.client.get("/access_grants/get", params=params) return AccessGrant.from_dict(res["access_grant"]) @@ -357,18 +358,18 @@ def get_related( :param include: :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_grant_ids is not None: - json_payload["access_grant_ids"] = access_grant_ids + params["access_grant_ids"] = access_grant_ids if access_grant_keys is not None: - json_payload["access_grant_keys"] = access_grant_keys + params["access_grant_keys"] = access_grant_keys if exclude is not None: - json_payload["exclude"] = exclude + params["exclude"] = exclude if include is not None: - json_payload["include"] = include + params["include"] = include - res = self.client.post("/access_grants/get_related", json=json_payload) + res = self.client.get("/access_grants/get_related", params=params) return Batch.from_dict(res["batch"]) @@ -377,14 +378,14 @@ def list( *, access_code_id: Optional[str] = None, access_grant_ids: Optional[List[str]] = None, - access_grant_key: Optional[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[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, @@ -418,36 +419,36 @@ def list( :param user_identity_id: ID of user identity by which you want to filter the list of Access Grants. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if access_grant_ids is not None: - json_payload["access_grant_ids"] = access_grant_ids + params["access_grant_ids"] = access_grant_ids if access_grant_key is not None: - json_payload["access_grant_key"] = access_grant_key + params["access_grant_key"] = access_grant_key if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if location_id is not None: - json_payload["location_id"] = location_id + params["location_id"] = location_id if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if reservation_key is not None: - json_payload["reservation_key"] = reservation_key + params["reservation_key"] = reservation_key if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/access_grants/list", json=json_payload) + res = self.client.get("/access_grants/list", params=params) return [AccessGrant.from_dict(item) for item in res["access_grants"]] @@ -479,8 +480,8 @@ def update( *, access_grant_id: Optional[str] = None, access_grant_key: Optional[str] = None, - ends_at: Optional[str] = None, - name: 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. @@ -508,6 +509,6 @@ def update( if starts_at is not None: json_payload["starts_at"] = starts_at - self.client.post("/access_grants/update", json=json_payload) + 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 ffa6796..1e38dc2 100644 --- a/seam/routes/access_grants_unmanaged.py +++ b/seam/routes/access_grants_unmanaged.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import UnmanagedAccessGrant @@ -22,7 +23,7 @@ def list( acs_entrance_id: Optional[str] = None, acs_system_id: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, reservation_key: Optional[str] = None, user_identity_id: Optional[str] = None, ) -> List[UnmanagedAccessGrant]: @@ -77,12 +78,12 @@ def get(self, *, access_grant_id: str) -> UnmanagedAccessGrant: :param access_grant_id: ID of unmanaged Access Grant to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id - res = self.client.post("/access_grants/unmanaged/get", json=json_payload) + res = self.client.get("/access_grants/unmanaged/get", params=params) return UnmanagedAccessGrant.from_dict(res["access_grant"]) @@ -92,7 +93,7 @@ def list( acs_entrance_id: Optional[str] = None, acs_system_id: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, reservation_key: Optional[str] = None, user_identity_id: Optional[str] = None, ) -> List[UnmanagedAccessGrant]: @@ -111,22 +112,22 @@ def list( :param user_identity_id: ID of user identity by which you want to filter the list of unmanaged Access Grants. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if reservation_key is not None: - json_payload["reservation_key"] = reservation_key + params["reservation_key"] = reservation_key if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/access_grants/unmanaged/list", json=json_payload) + res = self.client.get("/access_grants/unmanaged/list", params=params) return [UnmanagedAccessGrant.from_dict(item) for item in res["access_grants"]] @@ -158,6 +159,6 @@ def update( if access_grant_key is not None: json_payload["access_grant_key"] = access_grant_key - self.client.post("/access_grants/unmanaged/update", json=json_payload) + 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 87c94a5..771103c 100644 --- a/seam/routes/access_methods.py +++ b/seam/routes/access_methods.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ActionAttempt, AccessMethod, Batch from .access_methods_unmanaged import ( AbstractAccessMethodsUnmanaged, @@ -110,7 +111,7 @@ def list( acs_entrance_id: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, space_id: Optional[str] = None, ) -> List[AccessMethod]: """Lists all access methods, usually filtered by Access Grant. @@ -225,7 +226,7 @@ def delete( if reservation_key is not None: json_payload["reservation_key"] = reservation_key - self.client.post("/access_methods/delete", json=json_payload) + self.client.delete("/access_methods/delete", json=json_payload) return None @@ -272,12 +273,12 @@ def get(self, *, access_method_id: str) -> AccessMethod: :param access_method_id: ID of access method to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_method_id is not None: - json_payload["access_method_id"] = access_method_id + params["access_method_id"] = access_method_id - res = self.client.post("/access_methods/get", json=json_payload) + res = self.client.get("/access_methods/get", params=params) return AccessMethod.from_dict(res["access_method"]) @@ -297,16 +298,16 @@ def get_related( :param include: :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_method_ids is not None: - json_payload["access_method_ids"] = access_method_ids + params["access_method_ids"] = access_method_ids if exclude is not None: - json_payload["exclude"] = exclude + params["exclude"] = exclude if include is not None: - json_payload["include"] = include + params["include"] = include - res = self.client.post("/access_methods/get_related", json=json_payload) + res = self.client.get("/access_methods/get_related", params=params) return Batch.from_dict(res["batch"]) @@ -319,7 +320,7 @@ def list( acs_entrance_id: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, space_id: Optional[str] = None, ) -> List[AccessMethod]: """Lists all access methods, usually filtered by Access Grant. @@ -341,26 +342,26 @@ def list( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if access_grant_key is not None: - json_payload["access_grant_key"] = access_grant_key + params["access_grant_key"] = access_grant_key if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id - res = self.client.post("/access_methods/list", json=json_payload) + res = self.client.get("/access_methods/list", params=params) return [AccessMethod.from_dict(item) for item in res["access_methods"]] diff --git a/seam/routes/access_methods_unmanaged.py b/seam/routes/access_methods_unmanaged.py index bd1e158..0d1e3ce 100644 --- a/seam/routes/access_methods_unmanaged.py +++ b/seam/routes/access_methods_unmanaged.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import UnmanagedAccessMethod @@ -49,12 +50,12 @@ def get(self, *, access_method_id: str) -> UnmanagedAccessMethod: :param access_method_id: ID of unmanaged access method to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_method_id is not None: - json_payload["access_method_id"] = access_method_id + params["access_method_id"] = access_method_id - res = self.client.post("/access_methods/unmanaged/get", json=json_payload) + res = self.client.get("/access_methods/unmanaged/get", params=params) return UnmanagedAccessMethod.from_dict(res["access_method"]) @@ -77,17 +78,17 @@ def list( :param space_id: ID of the space for which you want to retrieve all unmanaged access methods. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id - res = self.client.post("/access_methods/unmanaged/list", json=json_payload) + res = 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 8207cf1..b334da8 100644 --- a/seam/routes/acs.py +++ b/seam/routes/acs.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from .acs_access_groups import AbstractAcsAccessGroups, AcsAccessGroups from .acs_credentials import AbstractAcsCredentials, AcsCredentials from .acs_encoders import AbstractAcsEncoders, AcsEncoders diff --git a/seam/routes/acs_access_groups.py b/seam/routes/acs_access_groups.py index 743ae2a..0eddd16 100644 --- a/seam/routes/acs_access_groups.py +++ b/seam/routes/acs_access_groups.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import AcsAccessGroup, AcsEntrance, AcsUser @@ -130,7 +131,7 @@ def add_user( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/access_groups/add_user", json=json_payload) + self.client.put("/acs/access_groups/add_user", json=json_payload) return None @@ -143,7 +144,7 @@ def delete(self, *, acs_access_group_id: str) -> None: if acs_access_group_id is not None: json_payload["acs_access_group_id"] = acs_access_group_id - self.client.post("/acs/access_groups/delete", json=json_payload) + self.client.delete("/acs/access_groups/delete", json=json_payload) return None @@ -153,12 +154,12 @@ def get(self, *, acs_access_group_id: str) -> AcsAccessGroup: :param acs_access_group_id: ID of the access group that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id - res = self.client.post("/acs/access_groups/get", json=json_payload) + res = self.client.get("/acs/access_groups/get", params=params) return AcsAccessGroup.from_dict(res["acs_access_group"]) @@ -181,18 +182,18 @@ def list( :param user_identity_id: ID of the user identity for which you want to retrieve all access groups. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if search is not None: - json_payload["search"] = search + params["search"] = search if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/acs/access_groups/list", json=json_payload) + res = self.client.get("/acs/access_groups/list", params=params) return [AcsAccessGroup.from_dict(item) for item in res["acs_access_groups"]] @@ -204,13 +205,13 @@ def list_accessible_entrances( :param acs_access_group_id: ID of the access group for which you want to retrieve all accessible entrances. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id - res = self.client.post( - "/acs/access_groups/list_accessible_entrances", json=json_payload + res = self.client.get( + "/acs/access_groups/list_accessible_entrances", params=params ) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] @@ -221,12 +222,12 @@ def list_users(self, *, acs_access_group_id: str) -> List[AcsUser]: :param acs_access_group_id: ID of the access group for which you want to retrieve all access system users. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id - res = self.client.post("/acs/access_groups/list_users", json=json_payload) + res = self.client.get("/acs/access_groups/list_users", params=params) return [AcsUser.from_dict(item) for item in res["acs_users"]] @@ -254,6 +255,6 @@ def remove_user( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/access_groups/remove_user", json=json_payload) + self.client.delete("/acs/access_groups/remove_user", json=json_payload) return None diff --git a/seam/routes/acs_credentials.py b/seam/routes/acs_credentials.py index 2ad1989..52d744b 100644 --- a/seam/routes/acs_credentials.py +++ b/seam/routes/acs_credentials.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import AcsCredential, AcsEntrance @@ -99,7 +100,7 @@ def list( created_before: Optional[str] = None, is_multi_phone_sync_credential: Optional[bool] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, ) -> List[AcsCredential]: """Returns a list of all `credentials `_. @@ -198,7 +199,7 @@ def assign( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/credentials/assign", json=json_payload) + self.client.patch("/acs/credentials/assign", json=json_payload) return None @@ -294,7 +295,7 @@ def delete(self, *, acs_credential_id: str) -> None: if acs_credential_id is not None: json_payload["acs_credential_id"] = acs_credential_id - self.client.post("/acs/credentials/delete", json=json_payload) + self.client.delete("/acs/credentials/delete", json=json_payload) return None @@ -304,12 +305,12 @@ def get(self, *, acs_credential_id: str) -> AcsCredential: :param acs_credential_id: ID of the credential that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id - res = self.client.post("/acs/credentials/get", json=json_payload) + res = self.client.get("/acs/credentials/get", params=params) return AcsCredential.from_dict(res["acs_credential"]) @@ -322,7 +323,7 @@ def list( created_before: Optional[str] = None, is_multi_phone_sync_credential: Optional[bool] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, ) -> List[AcsCredential]: """Returns a list of all `credentials `_. @@ -344,28 +345,26 @@ def list( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if is_multi_phone_sync_credential is not None: - json_payload["is_multi_phone_sync_credential"] = ( - is_multi_phone_sync_credential - ) + params["is_multi_phone_sync_credential"] = is_multi_phone_sync_credential if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search - res = self.client.post("/acs/credentials/list", json=json_payload) + res = self.client.get("/acs/credentials/list", params=params) return [AcsCredential.from_dict(item) for item in res["acs_credentials"]] @@ -375,13 +374,13 @@ def list_accessible_entrances(self, *, acs_credential_id: str) -> List[AcsEntran :param acs_credential_id: ID of the credential for which you want to retrieve all entrances to which the credential grants access. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id - res = self.client.post( - "/acs/credentials/list_accessible_entrances", json=json_payload + res = self.client.get( + "/acs/credentials/list_accessible_entrances", params=params ) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] @@ -410,7 +409,7 @@ def unassign( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/credentials/unassign", json=json_payload) + self.client.patch("/acs/credentials/unassign", json=json_payload) return None @@ -438,6 +437,6 @@ def update( if ends_at is not None: json_payload["ends_at"] = ends_at - self.client.post("/acs/credentials/update", json=json_payload) + 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 1ecc91c..3c6c3d6 100644 --- a/seam/routes/acs_encoders.py +++ b/seam/routes/acs_encoders.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ActionAttempt, AcsEncoder from .acs_encoders_simulate import AbstractAcsEncodersSimulate, AcsEncodersSimulate from ..modules.action_attempts import resolve_action_attempt @@ -52,7 +53,7 @@ def list( acs_system_ids: Optional[List[str]] = None, acs_encoder_ids: Optional[List[str]] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, ) -> List[AcsEncoder]: """Returns a list of all `encoders `_. @@ -172,12 +173,12 @@ def get(self, *, acs_encoder_id: str) -> AcsEncoder: :param acs_encoder_id: ID of the encoder that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_encoder_id is not None: - json_payload["acs_encoder_id"] = acs_encoder_id + params["acs_encoder_id"] = acs_encoder_id - res = self.client.post("/acs/encoders/get", json=json_payload) + res = self.client.get("/acs/encoders/get", params=params) return AcsEncoder.from_dict(res["acs_encoder"]) @@ -188,7 +189,7 @@ def list( acs_system_ids: Optional[List[str]] = None, acs_encoder_ids: Optional[List[str]] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, ) -> List[AcsEncoder]: """Returns a list of all `encoders `_. @@ -203,20 +204,20 @@ def list( :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if acs_system_ids is not None: - json_payload["acs_system_ids"] = acs_system_ids + params["acs_system_ids"] = acs_system_ids if acs_encoder_ids is not None: - json_payload["acs_encoder_ids"] = acs_encoder_ids + params["acs_encoder_ids"] = acs_encoder_ids if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor - res = self.client.post("/acs/encoders/list", json=json_payload) + res = self.client.get("/acs/encoders/list", params=params) return [AcsEncoder.from_dict(item) for item in res["acs_encoders"]] diff --git a/seam/routes/acs_encoders_simulate.py b/seam/routes/acs_encoders_simulate.py index 0255387..2a8a1f9 100644 --- a/seam/routes/acs_encoders_simulate.py +++ b/seam/routes/acs_encoders_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null class AbstractAcsEncodersSimulate(abc.ABC): diff --git a/seam/routes/acs_entrances.py b/seam/routes/acs_entrances.py index 71b462b..b802c33 100644 --- a/seam/routes/acs_entrances.py +++ b/seam/routes/acs_entrances.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import AcsEntrance, AcsCredential, ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -45,8 +46,8 @@ def list( connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, - location_id: Optional[str] = None, - page_cursor: Optional[str] = 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]: @@ -121,12 +122,12 @@ def get(self, *, acs_entrance_id: str) -> AcsEntrance: :param acs_entrance_id: ID of the entrance that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id - res = self.client.post("/acs/entrances/get", json=json_payload) + res = self.client.get("/acs/entrances/get", params=params) return AcsEntrance.from_dict(res["acs_entrance"]) @@ -168,8 +169,8 @@ def list( connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, - location_id: Optional[str] = None, - page_cursor: Optional[str] = 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]: @@ -198,32 +199,32 @@ def list( :param space_id: ID of the space for which you want to list entrances. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_method_id is not None: - json_payload["access_method_id"] = access_method_id + params["access_method_id"] = access_method_id if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id if acs_entrance_ids is not None: - json_payload["acs_entrance_ids"] = acs_entrance_ids + params["acs_entrance_ids"] = acs_entrance_ids if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if location_id is not None: - json_payload["location_id"] = location_id + params["location_id"] = location_id if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id - res = self.client.post("/acs/entrances/list", json=json_payload) + res = self.client.get("/acs/entrances/list", params=params) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] @@ -237,15 +238,15 @@ def list_credentials_with_access( :param include_if: Conditions that credentials must meet to be included in the returned list. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if include_if is not None: - json_payload["include_if"] = include_if + params["include_if"] = include_if - res = self.client.post( - "/acs/entrances/list_credentials_with_access", json=json_payload + res = self.client.get( + "/acs/entrances/list_credentials_with_access", params=params ) return [AcsCredential.from_dict(item) for item in res["acs_credentials"]] diff --git a/seam/routes/acs_systems.py b/seam/routes/acs_systems.py index 144a0c8..0ab0d17 100644 --- a/seam/routes/acs_systems.py +++ b/seam/routes/acs_systems.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import AcsSystem @@ -78,12 +79,12 @@ def get(self, *, acs_system_id: str) -> AcsSystem: :param acs_system_id: ID of the access system that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id - res = self.client.post("/acs/systems/get", json=json_payload) + res = self.client.get("/acs/systems/get", params=params) return AcsSystem.from_dict(res["acs_system"]) @@ -105,16 +106,16 @@ def list( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if search is not None: - json_payload["search"] = search + params["search"] = search - res = self.client.post("/acs/systems/list", json=json_payload) + res = self.client.get("/acs/systems/list", params=params) return [AcsSystem.from_dict(item) for item in res["acs_systems"]] @@ -128,14 +129,13 @@ def list_compatible_credential_manager_acs_systems( :param acs_system_id: ID of the access system for which you want to retrieve all compatible credential manager systems. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id - res = self.client.post( - "/acs/systems/list_compatible_credential_manager_acs_systems", - json=json_payload, + res = self.client.get( + "/acs/systems/list_compatible_credential_manager_acs_systems", params=params ) return [AcsSystem.from_dict(item) for item in res["acs_systems"]] diff --git a/seam/routes/acs_users.py b/seam/routes/acs_users.py index 7f57044..f36bcad 100644 --- a/seam/routes/acs_users.py +++ b/seam/routes/acs_users.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import AcsUser, AcsEntrance @@ -96,7 +97,7 @@ def list( acs_system_id: Optional[str] = None, created_before: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = 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, @@ -218,7 +219,7 @@ def unsuspend( def update( self, *, - access_schedule: Optional[Dict[str, Any]] = None, + 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, @@ -272,7 +273,7 @@ def add_to_access_group( if acs_user_id is not None: json_payload["acs_user_id"] = acs_user_id - self.client.post("/acs/users/add_to_access_group", json=json_payload) + self.client.put("/acs/users/add_to_access_group", json=json_payload) return None @@ -354,7 +355,7 @@ def delete( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/users/delete", json=json_payload) + self.client.delete("/acs/users/delete", json=json_payload) return None @@ -374,16 +375,16 @@ def get( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/acs/users/get", json=json_payload) + res = self.client.get("/acs/users/get", params=params) return AcsUser.from_dict(res["acs_user"]) @@ -393,7 +394,7 @@ def list( acs_system_id: Optional[str] = None, created_before: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = 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, @@ -418,26 +419,26 @@ def list( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if user_identity_email_address is not None: - json_payload["user_identity_email_address"] = user_identity_email_address + params["user_identity_email_address"] = user_identity_email_address if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id if user_identity_phone_number is not None: - json_payload["user_identity_phone_number"] = user_identity_phone_number + params["user_identity_phone_number"] = user_identity_phone_number - res = self.client.post("/acs/users/list", json=json_payload) + res = self.client.get("/acs/users/list", params=params) return [AcsUser.from_dict(item) for item in res["acs_users"]] @@ -457,18 +458,16 @@ def list_accessible_entrances( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post( - "/acs/users/list_accessible_entrances", json=json_payload - ) + res = self.client.get("/acs/users/list_accessible_entrances", params=params) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] @@ -496,7 +495,7 @@ def remove_from_access_group( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/users/remove_from_access_group", json=json_payload) + self.client.delete("/acs/users/remove_from_access_group", json=json_payload) return None @@ -587,7 +586,7 @@ def unsuspend( def update( self, *, - access_schedule: Optional[Dict[str, Any]] = None, + 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, @@ -638,6 +637,6 @@ def update( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/users/update", json=json_payload) + 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 310ff7c..b9d75d5 100644 --- a/seam/routes/action_attempts.py +++ b/seam/routes/action_attempts.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -30,7 +31,7 @@ def list( action_attempt_ids: Optional[List[str]] = None, device_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = 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. @@ -64,12 +65,12 @@ def get( :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] = {} + params: Dict[str, Any] = {} if action_attempt_id is not None: - json_payload["action_attempt_id"] = action_attempt_id + params["action_attempt_id"] = action_attempt_id - res = self.client.post("/action_attempts/get", json=json_payload) + res = self.client.get("/action_attempts/get", params=params) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -89,7 +90,7 @@ def list( action_attempt_ids: Optional[List[str]] = None, device_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = 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. @@ -102,17 +103,17 @@ def list( :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if action_attempt_ids is not None: - json_payload["action_attempt_ids"] = action_attempt_ids + params["action_attempt_ids"] = action_attempt_ids if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor - res = self.client.post("/action_attempts/list", json=json_payload) + res = 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 80ab10f..b629f38 100644 --- a/seam/routes/client_sessions.py +++ b/seam/routes/client_sessions.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ClientSession @@ -208,7 +209,7 @@ def create( if user_identity_ids is not None: json_payload["user_identity_ids"] = user_identity_ids - res = self.client.post("/client_sessions/create", json=json_payload) + res = self.client.put("/client_sessions/create", json=json_payload) return ClientSession.from_dict(res["client_session"]) @@ -221,7 +222,7 @@ def delete(self, *, client_session_id: str) -> None: if client_session_id is not None: json_payload["client_session_id"] = client_session_id - self.client.post("/client_sessions/delete", json=json_payload) + self.client.delete("/client_sessions/delete", json=json_payload) return None @@ -238,14 +239,14 @@ def get( :param user_identifier_key: User identifier key associated with the client session that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if client_session_id is not None: - json_payload["client_session_id"] = client_session_id + params["client_session_id"] = client_session_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/client_sessions/get", json=json_payload) + res = self.client.get("/client_sessions/get", params=params) return ClientSession.from_dict(res["client_session"]) @@ -332,7 +333,7 @@ def grant_access( if user_identity_ids is not None: json_payload["user_identity_ids"] = user_identity_ids - self.client.post("/client_sessions/grant_access", json=json_payload) + self.client.patch("/client_sessions/grant_access", json=json_payload) return None @@ -358,20 +359,20 @@ def list( :param without_user_identifier_key: Indicates whether to retrieve only client sessions without associated user identifier keys. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if client_session_id is not None: - json_payload["client_session_id"] = client_session_id + params["client_session_id"] = client_session_id if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id if without_user_identifier_key is not None: - json_payload["without_user_identifier_key"] = without_user_identifier_key + params["without_user_identifier_key"] = without_user_identifier_key - res = self.client.post("/client_sessions/list", json=json_payload) + res = self.client.get("/client_sessions/list", params=params) return [ClientSession.from_dict(item) for item in res["client_sessions"]] diff --git a/seam/routes/connect_webviews.py b/seam/routes/connect_webviews.py index bb9a7ca..06ef401 100644 --- a/seam/routes/connect_webviews.py +++ b/seam/routes/connect_webviews.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ConnectWebview @@ -79,7 +80,7 @@ def list( custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identifier_key: Optional[str] = None, ) -> List[ConnectWebview]: @@ -189,7 +190,7 @@ def delete(self, *, connect_webview_id: str) -> None: if connect_webview_id is not None: json_payload["connect_webview_id"] = connect_webview_id - self.client.post("/connect_webviews/delete", json=json_payload) + self.client.delete("/connect_webviews/delete", json=json_payload) return None @@ -201,12 +202,12 @@ def get(self, *, connect_webview_id: str) -> ConnectWebview: :param connect_webview_id: ID of the Connect Webview that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id - res = self.client.post("/connect_webviews/get", json=json_payload) + res = self.client.get("/connect_webviews/get", params=params) return ConnectWebview.from_dict(res["connect_webview"]) @@ -216,7 +217,7 @@ def list( custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identifier_key: Optional[str] = None, ) -> List[ConnectWebview]: @@ -235,21 +236,21 @@ def list( :param user_identifier_key: Your user ID for the user by which you want to filter Connect Webviews. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has + params["custom_metadata_has"] = custom_metadata_has if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/connect_webviews/list", json=json_payload) + res = 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 16e9eaf..6ccad18 100644 --- a/seam/routes/connected_accounts.py +++ b/seam/routes/connected_accounts.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ConnectedAccount from .connected_accounts_simulate import ( AbstractConnectedAccountsSimulate, @@ -47,7 +48,7 @@ def list( custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, user_identifier_key: Optional[str] = None, @@ -131,7 +132,7 @@ def delete(self, *, connected_account_id: str) -> None: if connected_account_id is not None: json_payload["connected_account_id"] = connected_account_id - self.client.post("/connected_accounts/delete", json=json_payload) + self.client.delete("/connected_accounts/delete", json=json_payload) return None @@ -145,14 +146,14 @@ def get( :param email: Email address associated with the connected account that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if email is not None: - json_payload["email"] = email + params["email"] = email - res = self.client.post("/connected_accounts/get", json=json_payload) + res = self.client.get("/connected_accounts/get", params=params) return ConnectedAccount.from_dict(res["connected_account"]) @@ -162,7 +163,7 @@ def list( custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, user_identifier_key: Optional[str] = None, @@ -184,24 +185,24 @@ def list( :param user_identifier_key: Your user ID for the user by which you want to filter connected accounts. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has + params["custom_metadata_has"] = custom_metadata_has if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/connected_accounts/list", json=json_payload) + res = self.client.get("/connected_accounts/list", params=params) return [ConnectedAccount.from_dict(item) for item in res["connected_accounts"]] @@ -260,6 +261,6 @@ def update( if display_name is not None: json_payload["display_name"] = display_name - self.client.post("/connected_accounts/update", json=json_payload) + 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 a53f14d..3ce160d 100644 --- a/seam/routes/connected_accounts_simulate.py +++ b/seam/routes/connected_accounts_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null class AbstractConnectedAccountsSimulate(abc.ABC): diff --git a/seam/routes/customers.py b/seam/routes/customers.py index dc46591..cbec118 100644 --- a/seam/routes/customers.py +++ b/seam/routes/customers.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import CustomerPortal @@ -362,7 +363,7 @@ def delete_data( if user_keys is not None: json_payload["user_keys"] = user_keys - self.client.post("/customers/delete_data", json=json_payload) + self.client.delete("/customers/delete_data", json=json_payload) return None diff --git a/seam/routes/devices.py b/seam/routes/devices.py index f11e77f..84eab62 100644 --- a/seam/routes/devices.py +++ b/seam/routes/devices.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import Device, DeviceProvider from .devices_simulate import AbstractDevicesSimulate, DevicesSimulate from .devices_unmanaged import AbstractDevicesUnmanaged, DevicesUnmanaged @@ -48,10 +49,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_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 `_. @@ -121,7 +122,7 @@ def update( backup_access_code_pool_enabled: Optional[bool] = None, custom_metadata: Optional[Dict[str, Any]] = None, is_managed: Optional[bool] = None, - name: Optional[str] = None, + name: Optional[Union[str, Null]] = None, properties: Optional[Dict[str, Any]] = None, ) -> None: """Updates a specified `device `_. @@ -169,14 +170,14 @@ def get( :param name: Name of the device that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if name is not None: - json_payload["name"] = name + params["name"] = name - res = self.client.post("/devices/get", json=json_payload) + res = self.client.get("/devices/get", params=params) return Device.from_dict(res["device"]) @@ -194,10 +195,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_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 `_. @@ -235,42 +236,42 @@ def list( :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if connected_account_ids is not None: - json_payload["connected_account_ids"] = connected_account_ids + params["connected_account_ids"] = connected_account_ids if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has + params["custom_metadata_has"] = custom_metadata_has if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if device_ids is not None: - json_payload["device_ids"] = device_ids + params["device_ids"] = device_ids if device_type is not None: - json_payload["device_type"] = device_type + params["device_type"] = device_type if device_types is not None: - json_payload["device_types"] = device_types + params["device_types"] = device_types if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if manufacturer is not None: - json_payload["manufacturer"] = manufacturer + params["manufacturer"] = manufacturer if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id + params["unstable_location_id"] = unstable_location_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/devices/list", json=json_payload) + res = self.client.get("/devices/list", params=params) return [Device.from_dict(item) for item in res["devices"]] @@ -286,12 +287,12 @@ def list_device_providers( :param provider_category: Category for which you want to list providers. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if provider_category is not None: - json_payload["provider_category"] = provider_category + params["provider_category"] = provider_category - res = self.client.post("/devices/list_device_providers", json=json_payload) + res = self.client.get("/devices/list_device_providers", params=params) return [DeviceProvider.from_dict(item) for item in res["device_providers"]] @@ -315,7 +316,7 @@ def update( backup_access_code_pool_enabled: Optional[bool] = None, custom_metadata: Optional[Dict[str, Any]] = None, is_managed: Optional[bool] = None, - name: Optional[str] = None, + name: Optional[Union[str, Null]] = None, properties: Optional[Dict[str, Any]] = None, ) -> None: """Updates a specified `device `_. @@ -350,6 +351,6 @@ def update( if properties is not None: json_payload["properties"] = properties - self.client.post("/devices/update", json=json_payload) + 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 29a49bc..b82a3fc 100644 --- a/seam/routes/devices_simulate.py +++ b/seam/routes/devices_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null class AbstractDevicesSimulate(abc.ABC): diff --git a/seam/routes/devices_unmanaged.py b/seam/routes/devices_unmanaged.py index d7c6e9c..72ffb7d 100644 --- a/seam/routes/devices_unmanaged.py +++ b/seam/routes/devices_unmanaged.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import UnmanagedDevice @@ -38,10 +39,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, + unstable_location_id: Optional[Union[str, Null]] = None, user_identifier_key: Optional[str] = None, ) -> List[UnmanagedDevice]: """Returns a list of all `unmanaged devices `_. @@ -123,14 +124,14 @@ def get( :param name: Name of the unmanaged device that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if name is not None: - json_payload["name"] = name + params["name"] = name - res = self.client.post("/devices/unmanaged/get", json=json_payload) + res = self.client.get("/devices/unmanaged/get", params=params) return UnmanagedDevice.from_dict(res["device"]) @@ -148,10 +149,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, + unstable_location_id: Optional[Union[str, Null]] = None, user_identifier_key: Optional[str] = None, ) -> List[UnmanagedDevice]: """Returns a list of all `unmanaged devices `_. @@ -191,42 +192,42 @@ def list( :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if connected_account_ids is not None: - json_payload["connected_account_ids"] = connected_account_ids + params["connected_account_ids"] = connected_account_ids if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has + params["custom_metadata_has"] = custom_metadata_has if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if device_ids is not None: - json_payload["device_ids"] = device_ids + params["device_ids"] = device_ids if device_type is not None: - json_payload["device_type"] = device_type + params["device_type"] = device_type if device_types is not None: - json_payload["device_types"] = device_types + params["device_types"] = device_types if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if manufacturer is not None: - json_payload["manufacturer"] = manufacturer + params["manufacturer"] = manufacturer if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id + params["unstable_location_id"] = unstable_location_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/devices/unmanaged/list", json=json_payload) + res = self.client.get("/devices/unmanaged/list", params=params) return [UnmanagedDevice.from_dict(item) for item in res["devices"]] @@ -256,6 +257,6 @@ def update( if is_managed is not None: json_payload["is_managed"] = is_managed - self.client.post("/devices/unmanaged/update", json=json_payload) + self.client.patch("/devices/unmanaged/update", json=json_payload) return None diff --git a/seam/routes/events.py b/seam/routes/events.py index 639fbb7..3d6a68f 100644 --- a/seam/routes/events.py +++ b/seam/routes/events.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import SeamEvent @@ -42,7 +43,7 @@ def list( acs_system_id: Optional[str] = None, acs_system_ids: Optional[List[str]] = None, acs_user_id: Optional[str] = None, - between: Optional[List[Dict[str, Any]]] = None, + between: Optional[List[str]] = None, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, @@ -141,16 +142,16 @@ def get( :param event_type: Type of the event that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if event_id is not None: - json_payload["event_id"] = event_id + params["event_id"] = event_id if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if event_type is not None: - json_payload["event_type"] = event_type + params["event_type"] = event_type - res = self.client.post("/events/get", json=json_payload) + res = self.client.get("/events/get", params=params) return SeamEvent.from_dict(res["event"]) @@ -170,7 +171,7 @@ def list( acs_system_id: Optional[str] = None, acs_system_ids: Optional[List[str]] = None, acs_user_id: Optional[str] = None, - between: Optional[List[Dict[str, Any]]] = None, + between: Optional[List[str]] = None, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, @@ -245,65 +246,65 @@ def list( :param user_identity_id: ID of the user identity for which you want to list events. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if access_code_ids is not None: - json_payload["access_code_ids"] = access_code_ids + params["access_code_ids"] = access_code_ids if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if access_grant_ids is not None: - json_payload["access_grant_ids"] = access_grant_ids + params["access_grant_ids"] = access_grant_ids if access_method_id is not None: - json_payload["access_method_id"] = access_method_id + params["access_method_id"] = access_method_id if access_method_ids is not None: - json_payload["access_method_ids"] = access_method_ids + params["access_method_ids"] = access_method_ids if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id if acs_encoder_id is not None: - json_payload["acs_encoder_id"] = acs_encoder_id + params["acs_encoder_id"] = acs_encoder_id if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if acs_system_ids is not None: - json_payload["acs_system_ids"] = acs_system_ids + params["acs_system_ids"] = acs_system_ids if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if between is not None: - json_payload["between"] = between + params["between"] = between if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if device_ids is not None: - json_payload["device_ids"] = device_ids + params["device_ids"] = device_ids if event_ids is not None: - json_payload["event_ids"] = event_ids + params["event_ids"] = event_ids if event_type is not None: - json_payload["event_type"] = event_type + params["event_type"] = event_type if event_types is not None: - json_payload["event_types"] = event_types + params["event_types"] = event_types if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if since is not None: - json_payload["since"] = since + params["since"] = since if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if space_ids is not None: - json_payload["space_ids"] = space_ids + params["space_ids"] = space_ids if unstable_offset is not None: - json_payload["unstable_offset"] = unstable_offset + params["unstable_offset"] = unstable_offset if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/events/list", json=json_payload) + res = 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 bf3ab43..c82c275 100644 --- a/seam/routes/instant_keys.py +++ b/seam/routes/instant_keys.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import InstantKey @@ -53,7 +54,7 @@ def delete(self, *, instant_key_id: str) -> None: if instant_key_id is not None: json_payload["instant_key_id"] = instant_key_id - self.client.post("/instant_keys/delete", json=json_payload) + self.client.delete("/instant_keys/delete", json=json_payload) return None @@ -70,14 +71,14 @@ def get( :param instant_key_url: URL of the instant key to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if instant_key_id is not None: - json_payload["instant_key_id"] = instant_key_id + params["instant_key_id"] = instant_key_id if instant_key_url is not None: - json_payload["instant_key_url"] = instant_key_url + params["instant_key_url"] = instant_key_url - res = self.client.post("/instant_keys/get", json=json_payload) + res = self.client.get("/instant_keys/get", params=params) return InstantKey.from_dict(res["instant_key"]) @@ -87,11 +88,11 @@ def list(self, *, user_identity_id: Optional[str] = None) -> List[InstantKey]: :param user_identity_id: ID of the user identity by which you want to filter the list of Instant Keys. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/instant_keys/list", json=json_payload) + res = 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 effc53d..2635da9 100644 --- a/seam/routes/locks.py +++ b/seam/routes/locks.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ActionAttempt, Device from .locks_simulate import AbstractLocksSimulate, LocksSimulate from ..modules.action_attempts import resolve_action_attempt @@ -66,10 +67,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_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 `locks `_. @@ -207,14 +208,14 @@ def get( .. deprecated:: Use ``/devices/get`` instead.""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if name is not None: - json_payload["name"] = name + params["name"] = name - res = self.client.post("/locks/get", json=json_payload) + res = self.client.get("/locks/get", params=params) return Device.from_dict(res["device"]) @@ -232,10 +233,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_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 `locks `_. @@ -273,42 +274,42 @@ def list( :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if connected_account_ids is not None: - json_payload["connected_account_ids"] = connected_account_ids + params["connected_account_ids"] = connected_account_ids if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has + params["custom_metadata_has"] = custom_metadata_has if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if device_ids is not None: - json_payload["device_ids"] = device_ids + params["device_ids"] = device_ids if device_type is not None: - json_payload["device_type"] = device_type + params["device_type"] = device_type if device_types is not None: - json_payload["device_types"] = device_types + params["device_types"] = device_types if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if manufacturer is not None: - json_payload["manufacturer"] = manufacturer + params["manufacturer"] = manufacturer if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id + params["unstable_location_id"] = unstable_location_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/locks/list", json=json_payload) + res = self.client.get("/locks/list", params=params) return [Device.from_dict(item) for item in res["devices"]] diff --git a/seam/routes/locks_simulate.py b/seam/routes/locks_simulate.py index d53fe3e..08c31a0 100644 --- a/seam/routes/locks_simulate.py +++ b/seam/routes/locks_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ActionAttempt from ..modules.action_attempts import resolve_action_attempt diff --git a/seam/routes/noise_sensors.py b/seam/routes/noise_sensors.py index 598ce75..9549f49 100644 --- a/seam/routes/noise_sensors.py +++ b/seam/routes/noise_sensors.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import Device from .noise_sensors_noise_thresholds import ( AbstractNoiseSensorsNoiseThresholds, @@ -36,10 +37,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_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 `noise sensors `_. @@ -111,10 +112,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_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 `noise sensors `_. @@ -152,41 +153,41 @@ def list( :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if connected_account_ids is not None: - json_payload["connected_account_ids"] = connected_account_ids + params["connected_account_ids"] = connected_account_ids if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has + params["custom_metadata_has"] = custom_metadata_has if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if device_ids is not None: - json_payload["device_ids"] = device_ids + params["device_ids"] = device_ids if device_type is not None: - json_payload["device_type"] = device_type + params["device_type"] = device_type if device_types is not None: - json_payload["device_types"] = device_types + params["device_types"] = device_types if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if manufacturer is not None: - json_payload["manufacturer"] = manufacturer + params["manufacturer"] = manufacturer if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id + params["unstable_location_id"] = unstable_location_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/noise_sensors/list", json=json_payload) + res = 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 80d5e99..704b8e8 100644 --- a/seam/routes/noise_sensors_noise_thresholds.py +++ b/seam/routes/noise_sensors_noise_thresholds.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import NoiseThreshold @@ -156,7 +157,7 @@ def delete(self, *, device_id: str, noise_threshold_id: str) -> None: if noise_threshold_id is not None: json_payload["noise_threshold_id"] = noise_threshold_id - self.client.post("/noise_sensors/noise_thresholds/delete", json=json_payload) + self.client.delete("/noise_sensors/noise_thresholds/delete", json=json_payload) return None @@ -166,12 +167,12 @@ def get(self, *, noise_threshold_id: str) -> NoiseThreshold: :param noise_threshold_id: ID of the noise threshold that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if noise_threshold_id is not None: - json_payload["noise_threshold_id"] = noise_threshold_id + params["noise_threshold_id"] = noise_threshold_id - res = self.client.post("/noise_sensors/noise_thresholds/get", json=json_payload) + res = self.client.get("/noise_sensors/noise_thresholds/get", params=params) return NoiseThreshold.from_dict(res["noise_threshold"]) @@ -181,14 +182,12 @@ def list(self, *, device_id: str) -> List[NoiseThreshold]: :param device_id: ID of the device for which you want to list noise thresholds. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - res = self.client.post( - "/noise_sensors/noise_thresholds/list", json=json_payload - ) + res = self.client.get("/noise_sensors/noise_thresholds/list", params=params) return [NoiseThreshold.from_dict(item) for item in res["noise_thresholds"]] @@ -236,6 +235,6 @@ def update( if starts_daily_at is not None: json_payload["starts_daily_at"] = starts_daily_at - self.client.post("/noise_sensors/noise_thresholds/update", json=json_payload) + 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 445ae5c..542e053 100644 --- a/seam/routes/noise_sensors_simulate.py +++ b/seam/routes/noise_sensors_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null class AbstractNoiseSensorsSimulate(abc.ABC): diff --git a/seam/routes/phones.py b/seam/routes/phones.py index cc39e6d..f281d71 100644 --- a/seam/routes/phones.py +++ b/seam/routes/phones.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import Phone from .phones_simulate import AbstractPhonesSimulate, PhonesSimulate @@ -64,7 +65,7 @@ def deactivate(self, *, device_id: str) -> None: if device_id is not None: json_payload["device_id"] = device_id - self.client.post("/phones/deactivate", json=json_payload) + self.client.delete("/phones/deactivate", json=json_payload) return None @@ -74,12 +75,12 @@ def get(self, *, device_id: str) -> Phone: :param device_id: Device ID of the phone that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - res = self.client.post("/phones/get", json=json_payload) + res = self.client.get("/phones/get", params=params) return Phone.from_dict(res["phone"]) @@ -96,13 +97,13 @@ def list( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id if owner_user_identity_id is not None: - json_payload["owner_user_identity_id"] = owner_user_identity_id + params["owner_user_identity_id"] = owner_user_identity_id - res = self.client.post("/phones/list", json=json_payload) + res = 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 a1b7af2..5c3fbc7 100644 --- a/seam/routes/phones_simulate.py +++ b/seam/routes/phones_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import Phone diff --git a/seam/routes/spaces.py b/seam/routes/spaces.py index 34e6f99..7a53eaf 100644 --- a/seam/routes/spaces.py +++ b/seam/routes/spaces.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import Space, Batch @@ -115,7 +116,7 @@ def list( *, customer_key: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_key: Optional[str] = None, ) -> List[Space]: @@ -213,7 +214,7 @@ def add_acs_entrances(self, *, acs_entrance_ids: List[str], space_id: str) -> No if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/add_acs_entrances", json=json_payload) + self.client.put("/spaces/add_acs_entrances", json=json_payload) return None @@ -233,7 +234,7 @@ def add_connected_account( if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/add_connected_account", json=json_payload) + self.client.put("/spaces/add_connected_account", json=json_payload) return None @@ -250,7 +251,7 @@ def add_devices(self, *, device_ids: List[str], space_id: str) -> None: if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/add_devices", json=json_payload) + self.client.put("/spaces/add_devices", json=json_payload) return None @@ -312,7 +313,7 @@ def delete(self, *, space_id: str) -> None: if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/delete", json=json_payload) + self.client.delete("/spaces/delete", json=json_payload) return None @@ -326,14 +327,14 @@ def get( :param space_key: Unique key of the space that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if space_key is not None: - json_payload["space_key"] = space_key + params["space_key"] = space_key - res = self.client.post("/spaces/get", json=json_payload) + res = self.client.get("/spaces/get", params=params) return Space.from_dict(res["space"]) @@ -356,18 +357,18 @@ def get_related( :param space_keys: Keys of the spaces that you want to get along with their related resources. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if exclude is not None: - json_payload["exclude"] = exclude + params["exclude"] = exclude if include is not None: - json_payload["include"] = include + params["include"] = include if space_ids is not None: - json_payload["space_ids"] = space_ids + params["space_ids"] = space_ids if space_keys is not None: - json_payload["space_keys"] = space_keys + params["space_keys"] = space_keys - res = self.client.post("/spaces/get_related", json=json_payload) + res = self.client.get("/spaces/get_related", params=params) return Batch.from_dict(res["batch"]) @@ -376,7 +377,7 @@ def list( *, customer_key: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_key: Optional[str] = None, ) -> List[Space]: @@ -393,20 +394,20 @@ def list( :param space_key: Filter spaces by space_key. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_key is not None: - json_payload["space_key"] = space_key + params["space_key"] = space_key - res = self.client.post("/spaces/list", json=json_payload) + res = self.client.get("/spaces/list", params=params) return [Space.from_dict(item) for item in res["spaces"]] @@ -425,7 +426,7 @@ def remove_acs_entrances( if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/remove_acs_entrances", json=json_payload) + self.client.delete("/spaces/remove_acs_entrances", json=json_payload) return None @@ -445,7 +446,7 @@ def remove_connected_account( if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/remove_connected_account", json=json_payload) + self.client.delete("/spaces/remove_connected_account", json=json_payload) return None @@ -462,7 +463,7 @@ def remove_devices(self, *, device_ids: List[str], space_id: str) -> None: if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/remove_devices", json=json_payload) + self.client.delete("/spaces/remove_devices", json=json_payload) return None @@ -506,6 +507,6 @@ def update( if space_key is not None: json_payload["space_key"] = space_key - res = self.client.post("/spaces/update", json=json_payload) + res = 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 338fcfd..e6be6d2 100644 --- a/seam/routes/thermostats.py +++ b/seam/routes/thermostats.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ActionAttempt, Device from .thermostats_daily_programs import ( AbstractThermostatsDailyPrograms, @@ -84,7 +85,7 @@ def create_climate_preset( heating_set_point_fahrenheit: Optional[float] = None, hvac_mode_setting: Optional[str] = None, manual_override_allowed: Optional[bool] = None, - name: Optional[str] = None, + name: Optional[Union[str, Null]] = None, ) -> None: """Creates a `climate preset `_ for a specified `thermostat `_. @@ -189,10 +190,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_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 `thermostats `_. @@ -318,10 +319,10 @@ def set_temperature_threshold( self, *, device_id: str, - lower_limit_celsius: Optional[float] = None, - lower_limit_fahrenheit: Optional[float] = None, - upper_limit_celsius: Optional[float] = None, - upper_limit_fahrenheit: Optional[float] = None, + 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. @@ -352,7 +353,7 @@ def update_climate_preset( heating_set_point_fahrenheit: Optional[float] = None, hvac_mode_setting: Optional[str] = None, manual_override_allowed: Optional[bool] = None, - name: Optional[str] = None, + name: Optional[Union[str, Null]] = None, ) -> None: """Updates a specified `climate preset `_ for a specified `thermostat `_. @@ -387,13 +388,13 @@ def update_weekly_program( self, *, device_id: str, - friday_program_id: Optional[str] = None, - monday_program_id: Optional[str] = None, - saturday_program_id: Optional[str] = None, - sunday_program_id: Optional[str] = None, - thursday_program_id: Optional[str] = None, - tuesday_program_id: Optional[str] = None, - wednesday_program_id: Optional[str] = None, + 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. @@ -537,7 +538,7 @@ def create_climate_preset( heating_set_point_fahrenheit: Optional[float] = None, hvac_mode_setting: Optional[str] = None, manual_override_allowed: Optional[bool] = None, - name: Optional[str] = None, + name: Optional[Union[str, Null]] = None, ) -> None: """Creates a `climate preset `_ for a specified `thermostat `_. @@ -610,7 +611,7 @@ def delete_climate_preset(self, *, climate_preset_key: str, device_id: str) -> N if device_id is not None: json_payload["device_id"] = device_id - self.client.post("/thermostats/delete_climate_preset", json=json_payload) + self.client.delete("/thermostats/delete_climate_preset", json=json_payload) return None @@ -722,10 +723,10 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_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 `thermostats `_. @@ -763,42 +764,42 @@ def list( :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if connected_account_ids is not None: - json_payload["connected_account_ids"] = connected_account_ids + params["connected_account_ids"] = connected_account_ids if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has + params["custom_metadata_has"] = custom_metadata_has if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if device_ids is not None: - json_payload["device_ids"] = device_ids + params["device_ids"] = device_ids if device_type is not None: - json_payload["device_type"] = device_type + params["device_type"] = device_type if device_types is not None: - json_payload["device_types"] = device_types + params["device_types"] = device_types if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if manufacturer is not None: - json_payload["manufacturer"] = manufacturer + params["manufacturer"] = manufacturer if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id + params["unstable_location_id"] = unstable_location_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/thermostats/list", json=json_payload) + res = self.client.get("/thermostats/list", params=params) return [Device.from_dict(item) for item in res["devices"]] @@ -957,10 +958,10 @@ def set_temperature_threshold( self, *, device_id: str, - lower_limit_celsius: Optional[float] = None, - lower_limit_fahrenheit: Optional[float] = None, - upper_limit_celsius: Optional[float] = None, - upper_limit_fahrenheit: Optional[float] = None, + 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. @@ -987,7 +988,7 @@ def set_temperature_threshold( if upper_limit_fahrenheit is not None: json_payload["upper_limit_fahrenheit"] = upper_limit_fahrenheit - self.client.post("/thermostats/set_temperature_threshold", json=json_payload) + self.client.patch("/thermostats/set_temperature_threshold", json=json_payload) return None @@ -1005,7 +1006,7 @@ def update_climate_preset( heating_set_point_fahrenheit: Optional[float] = None, hvac_mode_setting: Optional[str] = None, manual_override_allowed: Optional[bool] = None, - name: Optional[str] = None, + name: Optional[Union[str, Null]] = None, ) -> None: """Updates a specified `climate preset `_ for a specified `thermostat `_. @@ -1060,7 +1061,7 @@ def update_climate_preset( if name is not None: json_payload["name"] = name - self.client.post("/thermostats/update_climate_preset", json=json_payload) + self.client.patch("/thermostats/update_climate_preset", json=json_payload) return None @@ -1068,13 +1069,13 @@ def update_weekly_program( self, *, device_id: str, - friday_program_id: Optional[str] = None, - monday_program_id: Optional[str] = None, - saturday_program_id: Optional[str] = None, - sunday_program_id: Optional[str] = None, - thursday_program_id: Optional[str] = None, - tuesday_program_id: Optional[str] = None, - wednesday_program_id: Optional[str] = None, + 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. diff --git a/seam/routes/thermostats_daily_programs.py b/seam/routes/thermostats_daily_programs.py index d1662ef..7f93018 100644 --- a/seam/routes/thermostats_daily_programs.py +++ b/seam/routes/thermostats_daily_programs.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ThermostatDailyProgram, ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -93,7 +94,7 @@ def delete(self, *, thermostat_daily_program_id: str) -> None: if thermostat_daily_program_id is not None: json_payload["thermostat_daily_program_id"] = thermostat_daily_program_id - self.client.post("/thermostats/daily_programs/delete", json=json_payload) + self.client.delete("/thermostats/daily_programs/delete", json=json_payload) return None @@ -125,7 +126,7 @@ def update( if thermostat_daily_program_id is not None: json_payload["thermostat_daily_program_id"] = thermostat_daily_program_id - res = self.client.post("/thermostats/daily_programs/update", json=json_payload) + res = self.client.patch("/thermostats/daily_programs/update", json=json_payload) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") diff --git a/seam/routes/thermostats_schedules.py b/seam/routes/thermostats_schedules.py index 3b6d234..a7777cd 100644 --- a/seam/routes/thermostats_schedules.py +++ b/seam/routes/thermostats_schedules.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ThermostatSchedule @@ -15,7 +16,7 @@ def create( ends_at: str, starts_at: str, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, name: Optional[str] = None, ) -> ThermostatSchedule: """Creates a new `thermostat schedule `_ for a specified `thermostat `_. @@ -75,7 +76,7 @@ def update( climate_preset_key: Optional[str] = None, ends_at: Optional[str] = None, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, name: Optional[str] = None, starts_at: Optional[str] = None, ) -> None: @@ -111,7 +112,7 @@ def create( ends_at: str, starts_at: str, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, name: Optional[str] = None, ) -> ThermostatSchedule: """Creates a new `thermostat schedule `_ for a specified `thermostat `_. @@ -162,7 +163,7 @@ def delete(self, *, thermostat_schedule_id: str) -> None: if thermostat_schedule_id is not None: json_payload["thermostat_schedule_id"] = thermostat_schedule_id - self.client.post("/thermostats/schedules/delete", json=json_payload) + self.client.delete("/thermostats/schedules/delete", json=json_payload) return None @@ -172,12 +173,12 @@ def get(self, *, thermostat_schedule_id: str) -> ThermostatSchedule: :param thermostat_schedule_id: ID of the thermostat schedule that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if thermostat_schedule_id is not None: - json_payload["thermostat_schedule_id"] = thermostat_schedule_id + params["thermostat_schedule_id"] = thermostat_schedule_id - res = self.client.post("/thermostats/schedules/get", json=json_payload) + res = self.client.get("/thermostats/schedules/get", params=params) return ThermostatSchedule.from_dict(res["thermostat_schedule"]) @@ -191,14 +192,14 @@ def list( :param user_identifier_key: User identifier key by which to filter the list of returned thermostat schedules. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/thermostats/schedules/list", json=json_payload) + res = self.client.get("/thermostats/schedules/list", params=params) return [ ThermostatSchedule.from_dict(item) for item in res["thermostat_schedules"] @@ -211,7 +212,7 @@ def update( climate_preset_key: Optional[str] = None, ends_at: Optional[str] = None, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, name: Optional[str] = None, starts_at: Optional[str] = None, ) -> None: @@ -248,6 +249,6 @@ def update( if starts_at is not None: json_payload["starts_at"] = starts_at - self.client.post("/thermostats/schedules/update", json=json_payload) + 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 8695b23..496ddaf 100644 --- a/seam/routes/thermostats_simulate.py +++ b/seam/routes/thermostats_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null class AbstractThermostatsSimulate(abc.ABC): diff --git a/seam/routes/user_identities.py b/seam/routes/user_identities.py index 948620f..473e129 100644 --- a/seam/routes/user_identities.py +++ b/seam/routes/user_identities.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import ( UserIdentity, InstantKey, @@ -49,10 +50,10 @@ def create( self, *, acs_system_ids: Optional[List[str]] = None, - email_address: Optional[str] = None, - full_name: Optional[str] = None, - phone_number: Optional[str] = None, - user_identity_key: Optional[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 `_. @@ -128,7 +129,7 @@ def list( created_before: Optional[str] = None, credential_manager_acs_system_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identity_ids: Optional[List[str]] = None, ) -> List[UserIdentity]: @@ -210,10 +211,10 @@ def update( self, *, user_identity_id: str, - email_address: Optional[str] = None, - full_name: Optional[str] = None, - phone_number: Optional[str] = None, - user_identity_key: Optional[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, ) -> None: """Updates a specified `user identity `_. @@ -267,7 +268,7 @@ def add_acs_user( if user_identity_key is not None: json_payload["user_identity_key"] = user_identity_key - self.client.post("/user_identities/add_acs_user", json=json_payload) + self.client.put("/user_identities/add_acs_user", json=json_payload) return None @@ -275,10 +276,10 @@ def create( self, *, acs_system_ids: Optional[List[str]] = None, - email_address: Optional[str] = None, - full_name: Optional[str] = None, - phone_number: Optional[str] = None, - user_identity_key: Optional[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 `_. @@ -319,7 +320,7 @@ def delete(self, *, user_identity_id: str) -> None: if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/user_identities/delete", json=json_payload) + self.client.delete("/user_identities/delete", json=json_payload) return None @@ -367,14 +368,14 @@ def get( :param user_identity_key: :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id if user_identity_key is not None: - json_payload["user_identity_key"] = user_identity_key + params["user_identity_key"] = user_identity_key - res = self.client.post("/user_identities/get", json=json_payload) + res = self.client.get("/user_identities/get", params=params) return UserIdentity.from_dict(res["user_identity"]) @@ -392,7 +393,7 @@ def grant_access_to_device(self, *, device_id: str, user_identity_id: str) -> No if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/user_identities/grant_access_to_device", json=json_payload) + self.client.put("/user_identities/grant_access_to_device", json=json_payload) return None @@ -402,7 +403,7 @@ def list( created_before: Optional[str] = None, credential_manager_acs_system_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identity_ids: Optional[List[str]] = None, ) -> List[UserIdentity]: @@ -421,24 +422,24 @@ def list( :param user_identity_ids: Array of user identity IDs by which to filter the list of user identities. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if credential_manager_acs_system_id is not None: - json_payload["credential_manager_acs_system_id"] = ( + params["credential_manager_acs_system_id"] = ( credential_manager_acs_system_id ) if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if user_identity_ids is not None: - json_payload["user_identity_ids"] = user_identity_ids + params["user_identity_ids"] = user_identity_ids - res = self.client.post("/user_identities/list", json=json_payload) + res = self.client.get("/user_identities/list", params=params) return [UserIdentity.from_dict(item) for item in res["user_identities"]] @@ -448,14 +449,12 @@ def list_accessible_devices(self, *, user_identity_id: str) -> List[Device]: :param user_identity_id: ID of the user identity for which you want to retrieve all accessible devices. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post( - "/user_identities/list_accessible_devices", json=json_payload - ) + res = self.client.get("/user_identities/list_accessible_devices", params=params) return [Device.from_dict(item) for item in res["devices"]] @@ -465,13 +464,13 @@ def list_accessible_entrances(self, *, user_identity_id: str) -> List[AcsEntranc :param user_identity_id: ID of the user identity for which you want to retrieve all accessible entrances. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post( - "/user_identities/list_accessible_entrances", json=json_payload + res = self.client.get( + "/user_identities/list_accessible_entrances", params=params ) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] @@ -482,12 +481,12 @@ def list_acs_systems(self, *, user_identity_id: str) -> List[AcsSystem]: :param user_identity_id: ID of the user identity for which you want to retrieve all access systems. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/user_identities/list_acs_systems", json=json_payload) + res = self.client.get("/user_identities/list_acs_systems", params=params) return [AcsSystem.from_dict(item) for item in res["acs_systems"]] @@ -497,12 +496,12 @@ def list_acs_users(self, *, user_identity_id: str) -> List[AcsUser]: :param user_identity_id: ID of the user identity for which you want to retrieve all access system users. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/user_identities/list_acs_users", json=json_payload) + res = self.client.get("/user_identities/list_acs_users", params=params) return [AcsUser.from_dict(item) for item in res["acs_users"]] @@ -520,7 +519,7 @@ def remove_acs_user(self, *, acs_user_id: str, user_identity_id: str) -> None: if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/user_identities/remove_acs_user", json=json_payload) + self.client.delete("/user_identities/remove_acs_user", json=json_payload) return None @@ -538,7 +537,9 @@ def revoke_access_to_device(self, *, device_id: str, user_identity_id: str) -> N if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/user_identities/revoke_access_to_device", json=json_payload) + self.client.delete( + "/user_identities/revoke_access_to_device", json=json_payload + ) return None @@ -546,10 +547,10 @@ def update( self, *, user_identity_id: str, - email_address: Optional[str] = None, - full_name: Optional[str] = None, - phone_number: Optional[str] = None, - user_identity_key: Optional[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, ) -> None: """Updates a specified `user identity `_. @@ -575,6 +576,6 @@ def update( if user_identity_key is not None: json_payload["user_identity_key"] = user_identity_key - self.client.post("/user_identities/update", json=json_payload) + 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 f4de636..9b3bbc4 100644 --- a/seam/routes/user_identities_unmanaged.py +++ b/seam/routes/user_identities_unmanaged.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import UnmanagedUserIdentity @@ -21,7 +22,7 @@ def list( *, created_before: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = 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). @@ -69,12 +70,12 @@ def get(self, *, user_identity_id: str) -> UnmanagedUserIdentity: :param user_identity_id: ID of the unmanaged user identity that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/user_identities/unmanaged/get", json=json_payload) + res = self.client.get("/user_identities/unmanaged/get", params=params) return UnmanagedUserIdentity.from_dict(res["user_identity"]) @@ -83,7 +84,7 @@ def list( *, created_before: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = 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). @@ -97,18 +98,18 @@ def list( :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""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search - res = self.client.post("/user_identities/unmanaged/list", json=json_payload) + res = self.client.get("/user_identities/unmanaged/list", params=params) return [ UnmanagedUserIdentity.from_dict(item) for item in res["user_identities"] @@ -140,6 +141,6 @@ def update( if user_identity_key is not None: json_payload["user_identity_key"] = user_identity_key - self.client.post("/user_identities/unmanaged/update", json=json_payload) + 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 b82cd1f..8ed3ac8 100644 --- a/seam/routes/webhooks.py +++ b/seam/routes/webhooks.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import Webhook @@ -83,7 +84,7 @@ def delete(self, *, webhook_id: str) -> None: if webhook_id is not None: json_payload["webhook_id"] = webhook_id - self.client.post("/webhooks/delete", json=json_payload) + self.client.delete("/webhooks/delete", json=json_payload) return None @@ -93,12 +94,12 @@ def get(self, *, webhook_id: str) -> Webhook: :param webhook_id: ID of the webhook that you want to get. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} if webhook_id is not None: - json_payload["webhook_id"] = webhook_id + params["webhook_id"] = webhook_id - res = self.client.post("/webhooks/get", json=json_payload) + res = self.client.get("/webhooks/get", params=params) return Webhook.from_dict(res["webhook"]) @@ -106,9 +107,9 @@ def list(self) -> List[Webhook]: """Returns a list of all `webhooks `_. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} - res = self.client.post("/webhooks/list", json=json_payload) + res = self.client.get("/webhooks/list", params=params) return [Webhook.from_dict(item) for item in res["webhooks"]] @@ -125,6 +126,6 @@ def update(self, *, event_types: List[str], webhook_id: str) -> None: if webhook_id is not None: json_payload["webhook_id"] = webhook_id - self.client.post("/webhooks/update", json=json_payload) + self.client.put("/webhooks/update", json=json_payload) return None diff --git a/seam/routes/workspaces.py b/seam/routes/workspaces.py index 4dd397d..5cde2c2 100644 --- a/seam/routes/workspaces.py +++ b/seam/routes/workspaces.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..null import Null from ..resources import Workspace, ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -13,7 +14,7 @@ def create( *, name: str, company_name: Optional[str] = None, - connect_partner_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, @@ -110,7 +111,7 @@ def create( *, name: str, company_name: Optional[str] = None, - connect_partner_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, @@ -177,9 +178,9 @@ def get(self) -> Workspace: """Returns the `workspace `_ associated with the authentication value. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} - res = self.client.post("/workspaces/get", json=json_payload) + res = self.client.get("/workspaces/get", params=params) return Workspace.from_dict(res["workspace"]) @@ -187,9 +188,9 @@ def list(self) -> List[Workspace]: """Returns a list of `workspaces `_ associated with the authentication value. :returns: OK""" - json_payload: Dict[str, Any] = {} + params: Dict[str, Any] = {} - res = self.client.post("/workspaces/list", json=json_payload) + res = self.client.get("/workspaces/list", params=params) return [Workspace.from_dict(item) for item in res["workspaces"]] @@ -260,6 +261,6 @@ def update( if organization_id is not None: json_payload["organization_id"] = organization_id - self.client.post("/workspaces/update", json=json_payload) + self.client.patch("/workspaces/update", json=json_payload) return None diff --git a/seam/utils/url_search_params_serializer.py b/seam/utils/url_search_params_serializer.py new file mode 100644 index 0000000..0bdfcde --- /dev/null +++ b/seam/utils/url_search_params_serializer.py @@ -0,0 +1,435 @@ +"""Serializes Python objects to URL search params. + +This is a Python port of the `@seamapi/url-search-params-serializer +`_ reference +implementation, which defines the standard for how the Seam SDKs and other +Seam API consumers serialize objects to URL search params in HTTP GET requests. + +Output is byte-for-byte identical to the reference implementation: +values are encoded with the ``application/x-www-form-urlencoded`` serializer, +params are sorted by name, and numbers are formatted using the +ECMAScript ``Number::toString`` algorithm. + +Type mapping between the reference implementation and this port: + +- JavaScript ``undefined`` is ``None``, or simply an absent key. +- JavaScript ``null`` is :data:`seam.NULL `. + Python has a single absence value, so ``None`` means the safe option of + omitting the param and sending null is always explicit. +- JavaScript ``string`` is ``str``. +- JavaScript ``boolean`` is ``bool``. +- JavaScript ``number`` is ``float`` or ``int``. +- JavaScript ``bigint`` is ``int``. + Python integers are arbitrary precision, so ``int`` covers both cases + and is always serialized in full without exponent notation. +- JavaScript ``Date`` and ``Temporal.Instant`` are + :class:`datetime.datetime`. + A naive ``datetime`` is interpreted as UTC. + Since ``Date`` has millisecond precision, microseconds are truncated. +- JavaScript ``Array`` is ``list`` or ``tuple``. + Unordered collections such as ``set`` are unsupported + because they would not serialize deterministically. +- A JavaScript plain object is any ``Mapping``, e.g., a ``dict``. +""" + +import datetime +import math +import string +from collections.abc import Mapping +from decimal import Decimal +from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union +from urllib.parse import parse_qsl + +from ..null import is_null + +Params = Mapping[str, Any] + + +class UnserializableParamError(Exception): + """Exception raised when a param could not be serialized. + + :ivar name: Name of the param that could not be serialized + :vartype name: str + """ + + def __init__(self, name: str, message: str): + """ + :param name: Name of the param that could not be serialized + :type name: str + :param message: Description of why the param could not be serialized + :type message: str + """ + + super().__init__(f"Could not serialize parameter: '{name}' {message}") + self.name = name + + +class UrlSearchParams: + """A mutable collection of URL search params. + + Implements the parts of the `URLSearchParams + `_ + interface needed to serialize params to a query string. + Unlike a ``dict``, a name may appear more than once, + which is how arrays are serialized. + """ + + def __init__( + self, + init: Optional[Union[str, Params, Sequence[Tuple[str, str]]]] = None, + ): + """ + :param init: A query string, a mapping of names to values, + or a sequence of name-value pairs + :type init: Optional[Union[str, Mapping[str, Any], Sequence[Tuple[str, str]]]] + """ + + self._pairs: List[Tuple[str, str]] = [] + + if init is None: + return + + if isinstance(init, str): + query = init[1:] if init.startswith("?") else init + self._pairs = list(parse_qsl(query, keep_blank_values=True)) + return + + items = init.items() if isinstance(init, Mapping) else init + self._pairs = [(str(name), str(value)) for name, value in items] + + def append(self, name: str, value: str) -> None: + """Appends a name-value pair, keeping any existing pairs with this name. + + :param name: Name of the param + :type name: str + :param value: Value of the param + :type value: str + """ + + self._pairs.append((name, value)) + + def set(self, name: str, value: str) -> None: + """Sets the value associated with a name. + + Replaces the first pair with this name and removes any others. + Appends a new pair if no pair with this name exists. + + :param name: Name of the param + :type name: str + :param value: Value of the param + :type value: str + """ + + if not self.has(name): + self.append(name, value) + return + + pairs: List[Tuple[str, str]] = [] + is_set = False + + for pair in self._pairs: + if pair[0] != name: + pairs.append(pair) + elif not is_set: + pairs.append((name, value)) + is_set = True + + self._pairs = pairs + + def get(self, name: str) -> Optional[str]: + """Returns the value of the first pair with this name. + + :param name: Name of the param + :type name: str + + :returns: The value, or ``None`` if no pair with this name exists + """ + + for existing_name, value in self._pairs: + if existing_name == name: + return value + + return None + + def get_all(self, name: str) -> List[str]: + """Returns the values of all pairs with this name, in insertion order. + + :param name: Name of the param + :type name: str + + :returns: The values""" + + return [value for existing_name, value in self._pairs if existing_name == name] + + def has(self, name: str) -> bool: + """Returns whether a pair with this name exists. + + :param name: Name of the param + :type name: str + + :returns: Whether a pair with this name exists""" + + return any(existing_name == name for existing_name, _ in self._pairs) + + def delete(self, name: str) -> None: + """Removes all pairs with this name. + + :param name: Name of the param + :type name: str + """ + + self._pairs = [pair for pair in self._pairs if pair[0] != name] + + def sort(self) -> None: + """Sorts all pairs by name. + + Sorting is stable, so the relative order of pairs + with the same name is preserved. + Names are compared by UTF-16 code units to match the + `URLSearchParams.sort() + `_ + specification. + """ + + self._pairs.sort(key=lambda pair: pair[0].encode("utf-16-be")) + + def to_string(self) -> str: + """Serializes all pairs to a query string. + + :returns: The query string, without a leading ``?``""" + + return "&".join( + f"{_encode_form_component(name)}={_encode_form_component(value)}" + for name, value in self._pairs + ) + + def __str__(self) -> str: + return self.to_string() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.to_string()!r})" + + def __len__(self) -> int: + return len(self._pairs) + + def __iter__(self) -> Iterator[Tuple[str, str]]: + return iter(self._pairs) + + +def serialize_url_search_params(params: Params) -> str: + """Serializes params to a URL search param query string. + + :param params: The params to serialize + :type params: Mapping[str, Any] + + :returns: The query string, without a leading ``?`` + + :raises UnserializableParamError: If any param could not be serialized + """ + + search_params = UrlSearchParams() + update_url_search_params(search_params, params) + + return search_params.to_string() + + +def update_url_search_params(search_params: UrlSearchParams, params: Params) -> None: + """Updates existing URL search params with serialized params. + + Existing params are preserved unless overwritten by a serialized param. + All params are sorted by name. + + :param search_params: The URL search params to update + :type search_params: UrlSearchParams + :param params: The params to serialize + :type params: Mapping[str, Any] + + :raises UnserializableParamError: If any param could not be serialized + """ + + _nested_update_url_search_params(search_params, params, []) + search_params.sort() + + +def _nested_update_url_search_params( + search_params: UrlSearchParams, params: Params, path: List[str] +) -> None: + for key, value in params.items(): + if not isinstance(key, str): + raise UnserializableParamError( + repr(key), + f"is a {type(key).__name__} which is unsupported as a parameter name", + ) + + if "." in key: + raise UnserializableParamError( + key, + 'contains one or more dots "." in its name which is unsupported', + ) + + current_path = [*path, key] + + if isinstance(value, Mapping): + _nested_update_url_search_params(search_params, value, current_path) + continue + + name = ".".join(current_path) + + if value is None: + continue + + if isinstance(value, str) and len(value) == 0: + continue + + if isinstance(value, (list, tuple)): + _update_url_search_params_from_array(search_params, name, value) + continue + + search_params.set(name, _serialize(name, value)) + + +def _update_url_search_params_from_array( + search_params: UrlSearchParams, name: str, values: Sequence[Any] +) -> None: + if len(values) == 0: + search_params.set(name, "") + return + + if len(values) == 1 and _is_empty_string(values[0]): + raise UnserializableParamError( + name, + "is a single element array containing the empty string which is unsupported", + ) + + if any(_is_empty_string(value) for value in values): + raise UnserializableParamError( + name, + "is an array containing the empty string which is unsupported", + ) + + if any(value is None or is_null(value) for value in values): + raise UnserializableParamError( + name, + "is an array containing null or undefined values which is unsupported", + ) + + for value in values: + search_params.append(name, _serialize(name, value)) + + +def _serialize(name: str, value: Any) -> str: + if is_null(value): + return "" + + if isinstance(value, str): + return value + + if isinstance(value, bool): + return "true" if value else "false" + + if isinstance(value, int): + return str(value) + + if isinstance(value, float): + return _format_number(name, value) + + if isinstance(value, datetime.datetime): + return _format_datetime(value) + + raise UnserializableParamError(name, f"is a {type(value).__name__}") + + +def _is_empty_string(value: Any) -> bool: + return isinstance(value, str) and len(value) == 0 + + +def _format_datetime(value: datetime.datetime) -> str: + if value.tzinfo is None: + value = value.replace(tzinfo=datetime.timezone.utc) + + utc_value = value.astimezone(datetime.timezone.utc) + milliseconds = utc_value.microsecond // 1000 + + return ( + f"{utc_value.year:04d}-{utc_value.month:02d}-{utc_value.day:02d}" + f"T{utc_value.hour:02d}:{utc_value.minute:02d}:{utc_value.second:02d}" + f".{milliseconds:03d}Z" + ) + + +def _format_number(name: str, value: float) -> str: + if math.isnan(value): + raise UnserializableParamError(name, "is NaN") + + if math.isinf(value): + raise UnserializableParamError( + name, "is Infinity" if value > 0 else "is -Infinity" + ) + + if value == 0: + return "0" + + sign = "-" if value < 0 else "" + _, digit_tuple, exponent = Decimal(repr(abs(value))).as_tuple() + + # The shortest digit string that round-trips, and the position of the + # decimal point relative to it, as required by the ECMAScript + # Number::toString algorithm. + digits = "".join(str(digit) for digit in digit_tuple) + point = int(exponent) + len(digits) + digits = digits.rstrip("0") + + return sign + _format_digits(digits, point) + + +def _format_digits(digits: str, point: int) -> str: + """Formats digits and a decimal point position per ECMAScript Number::toString. + + :param digits: Significant digits, without trailing zeros + :type digits: str + :param point: Position of the decimal point relative to the digits + :type point: int + + :returns: The formatted number""" + + count = len(digits) + + if count <= point <= 21: + return digits + "0" * (point - count) + + if 0 < point <= 21: + return f"{digits[:point]}.{digits[point:]}" + + if -6 < point <= 0: + return f"0.{'0' * -point}{digits}" + + exponent = point - 1 + exponent_sign = "+" if exponent >= 0 else "-" + mantissa = digits if count == 1 else f"{digits[0]}.{digits[1:]}" + + return f"{mantissa}e{exponent_sign}{abs(exponent)}" + + +_FORM_SAFE_CHARACTERS = frozenset(f"{string.ascii_letters}{string.digits}*-._") + + +def _encode_form_component(value: str) -> str: + """Percent-encodes a string using the ``application/x-www-form-urlencoded`` serializer. + + :param value: The string to encode + :type value: str + + :returns: The encoded string""" + + encoded = [] + + for byte in value.encode("utf-8"): + character = chr(byte) + if character in _FORM_SAFE_CHARACTERS: + encoded.append(character) + elif character == " ": + encoded.append("+") + else: + encoded.append(f"%{byte:02X}") + + return "".join(encoded) diff --git a/test/client_test.py b/test/client_test.py index e80bd26..360de5f 100644 --- a/test/client_test.py +++ b/test/client_test.py @@ -4,8 +4,8 @@ def test_seam_exposes_a_client_that_can_make_requests(seam: Seam, server): _, seed = server - response = seam.client.post( - "/devices/get", json={"device_id": seed["august_device_1"]} + response = seam.client.get( + "/devices/get", params={"device_id": seed["august_device_1"]} ) assert response["device"]["workspace_id"] == seed["seed_workspace_1"] diff --git a/test/conftest.py b/test/conftest.py index bacc479..1721b68 100755 --- a/test/conftest.py +++ b/test/conftest.py @@ -70,14 +70,16 @@ def recording_server(responses): class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" - # pylint: disable-next=invalid-name - def do_POST(self): # BaseHTTPRequestHandler dispatches on this name. + def record_request(self): content_length = int(self.headers.get("content-length", 0)) raw_body = self.rfile.read(content_length) + path, _, query = self.path.partition("?") requests.append( { - "path": self.path, + "method": self.command, + "path": path, + "query": query, "headers": {k.lower(): v for k, v in self.headers.items()}, "body": json.loads(raw_body) if raw_body else None, } @@ -102,6 +104,15 @@ def do_POST(self): # BaseHTTPRequestHandler dispatches on this name. self.end_headers() self.wfile.write(body) + # Endpoints are served over their semantic method, so record them all. + # BaseHTTPRequestHandler dispatches on these names. + # pylint: disable=invalid-name + do_GET = record_request + do_POST = record_request + do_PUT = record_request + do_PATCH = record_request + do_DELETE = record_request + def log_message(self, *args): pass diff --git a/test/headers_test.py b/test/headers_test.py index a9f47e8..38c3aeb 100644 --- a/test/headers_test.py +++ b/test/headers_test.py @@ -18,8 +18,10 @@ def test_seam_sends_default_headers(recording_server): assert len(requests) == 1 [request] = requests + assert request["method"] == "GET" assert request["path"] == "/devices/get" - assert request["body"] == {"device_id": device_id} + assert request["query"] == f"device_id={device_id}" + assert request["body"] is None assert request["headers"]["seam-sdk-name"] == "seamapi/python" assert request["headers"]["seam-sdk-version"] == version("seam") diff --git a/test/http_error_test.py b/test/http_error_test.py index 44fe2bb..6adc2c5 100644 --- a/test/http_error_test.py +++ b/test/http_error_test.py @@ -39,8 +39,9 @@ def test_seam_http_throws_invalid_input_error(server): seam = Seam(api_key=seed["seam_apikey1_token"], endpoint=endpoint) + # /devices/get requires either device_id or name. with pytest.raises(SeamHttpInvalidInputError) as exc_info: - seam.devices.get(device_id=4242) + seam.devices.get() err = exc_info.value assert err.status_code == 400 assert err.code == "invalid_input" diff --git a/test/null_test.py b/test/null_test.py new file mode 100644 index 0000000..cc06f35 --- /dev/null +++ b/test/null_test.py @@ -0,0 +1,130 @@ +from collections import OrderedDict + +from seam.client import SeamHttpClient +from seam.null import NULL, Null, is_null, replace_null + + +def test_null_is_a_singleton(): + assert Null() is NULL + assert is_null(NULL) + assert is_null(Null()) + + +def test_null_is_not_none(): + assert NULL is not None + assert not is_null(None) + assert not is_null("") + assert not is_null(0) + + +def test_null_is_falsy(): + assert not NULL + + +def test_null_repr(): + assert repr(NULL) == "NULL" + + +def test_replace_null(): + assert replace_null(NULL) is None + assert replace_null(None) is None + assert replace_null("a") == "a" + assert replace_null(0) == 0 + assert replace_null(False) is False + + +def test_replace_null_in_dict(): + assert replace_null({"a": NULL, "b": 1, "c": None}) == { + "a": None, + "b": 1, + "c": None, + } + + +def test_replace_null_in_nested_dict(): + assert replace_null({"a": {"b": {"c": NULL}}}) == {"a": {"b": {"c": None}}} + + +def test_replace_null_in_lists_and_tuples(): + assert replace_null(["a", NULL]) == ["a", None] + assert replace_null(("a", NULL)) == ("a", None) + assert replace_null({"a": [{"b": NULL}]}) == {"a": [{"b": None}]} + + +def test_replace_null_does_not_modify_the_given_value(): + params = {"a": NULL, "b": [NULL]} + replace_null(params) + + assert params == {"a": NULL, "b": [NULL]} + + +def test_replace_null_normalizes_mappings_to_dicts(): + result = replace_null(OrderedDict([("a", NULL)])) + + assert result == {"a": None} + + +def sent_request(recording_server, send): + """Return the single request the given call put on the wire.""" + + with recording_server([(200, {})]) as (endpoint, requests): + send(SeamHttpClient(base_url=endpoint, auth_headers={})) + + [request] = requests + + return request + + +def test_client_sends_null_params_as_json_null(recording_server): + request = sent_request( + recording_server, + lambda client: client.patch( + "/devices/update", json={"device_id": "a", "name": NULL} + ), + ) + + assert request["body"] == {"device_id": "a", "name": None} + + +def test_client_sends_nested_null_params_as_json_null(recording_server): + request = sent_request( + recording_server, + lambda client: client.patch( + "/spaces/update", json={"customer_data": {"check_in": NULL}} + ), + ) + + assert request["body"] == {"customer_data": {"check_in": None}} + + +def test_client_passes_through_payloads_without_null_params(recording_server): + request = sent_request( + recording_server, + lambda client: client.patch( + "/devices/update", json={"device_id": "a", "name": "Front Door"} + ), + ) + + assert request["body"] == {"device_id": "a", "name": "Front Door"} + + +def test_client_sends_null_search_params_as_an_empty_value(recording_server): + request = sent_request( + recording_server, + lambda client: client.get( + "/devices/list", params={"device_id": NULL, "limit": 2} + ), + ) + + assert request["query"] == "device_id=&limit=2" + + +def test_client_omits_none_search_params(recording_server): + request = sent_request( + recording_server, + lambda client: client.get( + "/devices/list", params={"device_id": None, "limit": 2} + ), + ) + + assert request["query"] == "limit=2" diff --git a/test/retry_test.py b/test/retry_test.py index 5a6172f..ce56286 100644 --- a/test/retry_test.py +++ b/test/retry_test.py @@ -7,11 +7,6 @@ DEVICES = (200, {"devices": [{"device_id": "august_device_1"}]}) -# The retries option has no effect on API requests because the Seam API -# uses POST, which httpx-retries does not treat as retryable. A follow-up -# PR will apply the retry policy to API requests without exposing the -# HTTP method in the SDK's public API, then remove the xfail markers. -@pytest.mark.xfail(reason="TODO: Apply the retry policy to API requests") def test_seam_retries_service_unavailable_responses(recording_server): expected_retry_count = 2 responses = [SERVICE_UNAVAILABLE, SERVICE_UNAVAILABLE, DEVICES] @@ -28,7 +23,6 @@ def test_seam_retries_service_unavailable_responses(recording_server): assert len(requests) == expected_retry_count + 1 -@pytest.mark.xfail(reason="TODO: Apply the retry policy to API requests") def test_seam_stops_retrying_once_retries_are_exhausted(recording_server): expected_retry_count = 1 diff --git a/test/serialization_test.py b/test/serialization_test.py index 40d4b0c..1d30a2e 100644 --- a/test/serialization_test.py +++ b/test/serialization_test.py @@ -1,3 +1,5 @@ +from datetime import datetime, timezone + from seam import Seam @@ -40,9 +42,9 @@ def test_serializes_array_params_when_explicitly_using_client(server): endpoint, seed = server seam = Seam.from_api_key(seed["seam_apikey1_token"], endpoint=endpoint) - response = seam.client.post( + response = seam.client.get( "/devices/list", - json={"device_ids": [seed["august_device_1"], seed["ecobee_device_1"]]}, + params={"device_ids": [seed["august_device_1"], seed["ecobee_device_1"]]}, ) device_ids = [device["device_id"] for device in response["devices"]] @@ -50,3 +52,58 @@ def test_serializes_array_params_when_explicitly_using_client(server): assert len(device_ids) == 2 assert seed["august_device_1"] in device_ids assert seed["ecobee_device_1"] in device_ids + + +def test_serializes_array_params_when_empty_and_explicitly_using_get(seam: Seam): + # The empty array is serialized to a single empty value, e.g., device_ids=, + # which the Seam API parses back to the empty array. + response = seam.client.get("/devices/list", params={"device_ids": []}) + + assert len(response["devices"]) == 0 + + +def test_serializes_array_params_when_none_and_explicitly_using_get(seam: Seam): + response = seam.client.get("/devices/list", params={"device_ids": None}) + database = seam.client.get("/_fake/database") + + assert len(response["devices"]) == len(database["devices"]) + + +def test_serializes_string_params_when_explicitly_using_get(server): + endpoint, seed = server + seam = Seam.from_api_key(seed["seam_apikey1_token"], endpoint=endpoint) + + response = seam.client.get( + "/devices/get", params={"device_id": seed["august_device_1"]} + ) + + assert response["device"]["device_id"] == seed["august_device_1"] + + +def test_serializes_number_params_when_explicitly_using_get(seam: Seam): + # A float is serialized as the Seam API expects a number, e.g., limit=2, + # never as 2.0. + response = seam.client.get("/devices/list", params={"limit": 2.0}) + + assert len(response["devices"]) == 2 + + +def test_serializes_datetime_params_when_explicitly_using_get(seam: Seam): + created_before = datetime(2999, 1, 1, tzinfo=timezone.utc) + + response = seam.client.get( + "/devices/list", params={"created_before": created_before} + ) + database = seam.client.get("/_fake/database") + + assert len(response["devices"]) == len(database["devices"]) + + +def test_serializes_params_for_a_route_using_the_semantic_method(server): + # /devices/list is a GET, so its params are serialized to the query string. + endpoint, seed = server + seam = Seam.from_api_key(seed["seam_apikey1_token"], endpoint=endpoint) + + devices = seam.devices.list(device_ids=[seed["august_device_1"]]) + + assert [device.device_id for device in devices] == [seed["august_device_1"]] diff --git a/test/timeout_test.py b/test/timeout_test.py index f30f855..8b82de7 100644 --- a/test/timeout_test.py +++ b/test/timeout_test.py @@ -49,7 +49,7 @@ def test_per_request_timeout_overrides_the_client_timeout(recording_server): with recording_server([(200, {"devices": []})]) as (endpoint, _): seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint, timeout=30) - response = seam.client.post("/devices/list", json={}, timeout=10) + response = seam.client.get("/devices/list", params={}, timeout=10) assert response == {"devices": []} @@ -74,13 +74,17 @@ def slow_server(): class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" - # pylint: disable-next=invalid-name - def do_POST(self): # BaseHTTPRequestHandler dispatches on this name. + def serve_slowly(self): time.sleep(5) self.send_response(200) self.send_header("content-length", "0") self.end_headers() + # BaseHTTPRequestHandler dispatches on these names. + # pylint: disable=invalid-name + do_GET = serve_slowly + do_POST = serve_slowly + def log_message(self, *args): pass diff --git a/test/url_search_params_serializer_test.py b/test/url_search_params_serializer_test.py new file mode 100644 index 0000000..f56a601 --- /dev/null +++ b/test/url_search_params_serializer_test.py @@ -0,0 +1,454 @@ +from collections import OrderedDict +from datetime import date, datetime, timedelta, timezone + +import pytest + +from seam.null import NULL +from seam.utils.url_search_params_serializer import ( + UnserializableParamError, + UrlSearchParams, + serialize_url_search_params, + update_url_search_params, +) + + +def test_serializes_empty_object(): + assert serialize_url_search_params({}) == "" + + +def test_serializes_string(): + assert serialize_url_search_params({"foo": "d"}) == "foo=d" + assert serialize_url_search_params({"foo": "null"}) == "foo=null" + assert serialize_url_search_params({"foo": "None"}) == "foo=None" + assert serialize_url_search_params({"foo": "undefined"}) == "foo=undefined" + assert serialize_url_search_params({"foo": "0"}) == "foo=0" + + +def test_removes_the_empty_string(): + # Serializing the empty string would conflict with NULL. + assert serialize_url_search_params({"foo": ""}) == "" + assert serialize_url_search_params({"foo": "d", "bar": ""}) == "foo=d" + + +def test_serializes_int(): + assert serialize_url_search_params({"foo": 1}) == "foo=1" + assert serialize_url_search_params({"foo": 0}) == "foo=0" + assert serialize_url_search_params({"foo": -42}) == "foo=-42" + + +def test_serializes_arbitrary_precision_int(): + assert ( + serialize_url_search_params({"foo": 9007199254740993}) == "foo=9007199254740993" + ) + assert ( + serialize_url_search_params({"foo": 123456789012345678901234567890}) + == "foo=123456789012345678901234567890" + ) + + +def test_serializes_float(): + assert serialize_url_search_params({"foo": 23.8}) == "foo=23.8" + assert serialize_url_search_params({"foo": -23.8}) == "foo=-23.8" + assert serialize_url_search_params({"foo": 0.30000000000000004}) == ( + "foo=0.30000000000000004" + ) + + +def test_serializes_float_using_the_ecmascript_number_format(): + # A float is serialized exactly as JavaScript would serialize the number, + # which is not always the same as the Python repr. + assert serialize_url_search_params({"foo": 1.0}) == "foo=1" + assert serialize_url_search_params({"foo": -0.0}) == "foo=0" + assert serialize_url_search_params({"foo": 100.0}) == "foo=100" + assert serialize_url_search_params({"foo": 1e16}) == "foo=10000000000000000" + assert serialize_url_search_params({"foo": 1e20}) == "foo=100000000000000000000" + assert serialize_url_search_params({"foo": 1e21}) == "foo=1e%2B21" + assert serialize_url_search_params({"foo": 0.0001}) == "foo=0.0001" + assert serialize_url_search_params({"foo": 1e-6}) == "foo=0.000001" + assert serialize_url_search_params({"foo": 1e-7}) == "foo=1e-7" + assert serialize_url_search_params({"foo": 5e-324}) == "foo=5e-324" + assert serialize_url_search_params({"foo": 1.7976931348623157e308}) == ( + "foo=1.7976931348623157e%2B308" + ) + + +def test_serializes_bool(): + assert serialize_url_search_params({"foo": True}) == "foo=true" + assert serialize_url_search_params({"foo": False}) == "foo=false" + assert serialize_url_search_params({"foo": True, "bar": False}) == ( + "bar=false&foo=true" + ) + + +def test_removes_none_params(): + assert serialize_url_search_params({"bar": None}) == "" + assert serialize_url_search_params({"foo": 1, "bar": None}) == "foo=1" + + +def test_serializes_null_params(): + assert serialize_url_search_params({"bar": NULL}) == "bar=" + assert serialize_url_search_params({"foo": 1, "bar": NULL}) == "bar=&foo=1" + + +def test_removes_none_params_at_any_depth(): + assert serialize_url_search_params({"foo": {"bar": None, "baz": 1}}) == "foo.baz=1" + assert serialize_url_search_params({"foo": {"bar": None}}) == "" + + +def test_serializes_empty_array_params(): + assert serialize_url_search_params({"bar": []}) == "bar=" + assert serialize_url_search_params({"foo": 1, "bar": []}) == "bar=&foo=1" + assert serialize_url_search_params({"bar": ()}) == "bar=" + + +def test_serializes_array_params_with_one_value(): + assert serialize_url_search_params({"bar": ["a"]}) == "bar=a" + assert serialize_url_search_params({"foo": 1, "bar": ["a"]}) == "bar=a&foo=1" + + +def test_serializes_array_params_with_many_values(): + assert serialize_url_search_params({"foo": 1, "bar": ["a", "2"]}) == ( + "bar=a&bar=2&foo=1" + ) + assert serialize_url_search_params( + {"foo": 1, "bar": ["null", "2", "undefined"]} + ) == ("bar=null&bar=2&bar=undefined&foo=1") + + +def test_serializes_tuple_params(): + assert serialize_url_search_params({"bar": ("a", "2")}) == "bar=a&bar=2" + + +def test_serializes_array_params_with_mixed_values(): + assert serialize_url_search_params( + {"bar": [1, "a", True, datetime(1970, 1, 1, tzinfo=timezone.utc)]} + ) == ("bar=1&bar=a&bar=true&bar=1970-01-01T00%3A00%3A00.000Z") + + +def test_serializes_datetime(): + assert serialize_url_search_params( + {"foo": 1, "now": datetime(2025, 2, 24, 18, 44, 39, tzinfo=timezone.utc)} + ) == ("foo=1&now=2025-02-24T18%3A44%3A39.000Z") + + +def test_serializes_datetime_with_milliseconds(): + assert serialize_url_search_params( + { + "now": datetime( + 2025, 2, 24, 18, 44, 39, microsecond=123000, tzinfo=timezone.utc + ) + } + ) == ("now=2025-02-24T18%3A44%3A39.123Z") + + +def test_truncates_datetime_microseconds(): + assert serialize_url_search_params( + { + "now": datetime( + 2025, 2, 24, 18, 44, 39, microsecond=123999, tzinfo=timezone.utc + ) + } + ) == ("now=2025-02-24T18%3A44%3A39.123Z") + + +def test_serializes_datetime_as_utc(): + assert serialize_url_search_params( + {"now": datetime(2025, 2, 24, 13, 44, 39, tzinfo=timezone(timedelta(hours=-5)))} + ) == ("now=2025-02-24T18%3A44%3A39.000Z") + + +def test_serializes_naive_datetime_as_utc(): + assert serialize_url_search_params({"now": datetime(2025, 2, 24, 18, 44, 39)}) == ( + "now=2025-02-24T18%3A44%3A39.000Z" + ) + + +def test_serializes_datetime_before_the_epoch(): + assert serialize_url_search_params( + {"then": datetime(1969, 12, 31, 23, 59, 59, tzinfo=timezone.utc)} + ) == ("then=1969-12-31T23%3A59%3A59.000Z") + + +def test_serializes_dicts(): + assert serialize_url_search_params({"foo": 1, "bar": {"baz": "a"}}) == ( + "bar.baz=a&foo=1" + ) + + assert serialize_url_search_params({"foo": 1, "bar": {"baz": {"x": {"z": 1}}}}) == ( + "bar.baz.x.z=1&foo=1" + ) + + assert serialize_url_search_params( + {"foo": 1, "bar": {"baz": {"x": {"z": NULL}}}} + ) == ("bar.baz.x.z=&foo=1") + + assert serialize_url_search_params({"foo": 1, "bar": {"baz": [1, "a"]}}) == ( + "bar.baz=1&bar.baz=a&foo=1" + ) + + assert serialize_url_search_params({"foo": {}, "bar": 2}) == "bar=2" + + assert serialize_url_search_params({"foo": {"x": {}}, "bar": 2}) == "bar=2" + + assert serialize_url_search_params( + {"foo": {}, "bar": {"baz": {"x": {"z": NULL, "t": {}}, "q": {}}}} + ) == ("bar.baz.x.z=") + + +def test_serializes_dict_subclasses(): + assert serialize_url_search_params( + {"foo": OrderedDict([("bar", 1), ("baz", 2)])} + ) == ("foo.bar=1&foo.baz=2") + + +def test_sorts_params_by_name(): + assert serialize_url_search_params({"b": 1, "a": 2, "c": 3}) == "a=2&b=1&c=3" + assert serialize_url_search_params({"b": 1, "A": 2, "a": 3, "B": 4}) == ( + "A=2&B=4&a=3&b=1" + ) + assert serialize_url_search_params({"a10": 1, "a2": 2, "a1": 3}) == ( + "a1=3&a10=1&a2=2" + ) + assert serialize_url_search_params({"zz": 1, "a": {"z": 2, "b": 3}}) == ( + "a.b=3&a.z=2&zz=1" + ) + assert serialize_url_search_params({"ab": 1, "a": {"b": 2}}) == "a.b=2&ab=1" + + +def test_sorts_params_by_utf_16_code_unit(): + assert serialize_url_search_params({"￿": 1, "\U0001f600": 2}) == ( + "%F0%9F%98%80=2&%EF%BF%BF=1" + ) + + +def test_sorting_preserves_array_order(): + assert serialize_url_search_params({"b": ["3", "1", "2"], "a": 1}) == ( + "a=1&b=3&b=1&b=2" + ) + + +def test_encodes_params_as_form_urlencoded(): + assert serialize_url_search_params({"foo": "a b"}) == "foo=a+b" + assert serialize_url_search_params({"foo": "a+b"}) == "foo=a%2Bb" + assert serialize_url_search_params({"foo": "a~b"}) == "foo=a%7Eb" + assert serialize_url_search_params({"foo": "a*b"}) == "foo=a*b" + assert serialize_url_search_params({"foo": "abcXYZ019*-._"}) == "foo=abcXYZ019*-._" + assert serialize_url_search_params({"foo": "a&b=c?d#e/f"}) == ( + "foo=a%26b%3Dc%3Fd%23e%2Ff" + ) + assert serialize_url_search_params({"foo": "100%"}) == "foo=100%25" + assert serialize_url_search_params({"foo": "a\nb"}) == "foo=a%0Ab" + + +def test_encodes_unicode_params(): + assert serialize_url_search_params({"foo": "héllo wörld"}) == ( + "foo=h%C3%A9llo+w%C3%B6rld" + ) + assert serialize_url_search_params({"foo": "日本語"}) == ( + "foo=%E6%97%A5%E6%9C%AC%E8%AA%9E" + ) + assert serialize_url_search_params({"🔒": "a"}) == "%F0%9F%94%92=a" + assert serialize_url_search_params({"a b": 1}) == "a+b=1" + + +def test_cannot_serialize_keys_containing_a_dot(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo.bar": 1}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": {"bar.baz": 1}}) + + +def test_cannot_serialize_non_string_keys(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({1: "a"}) + + +def test_cannot_serialize_functions(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": lambda: None}) + + +def test_cannot_serialize_number_pointers(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": float("inf")}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": float("-inf")}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": float("nan")}) + + +def test_cannot_serialize_arbitrary_objects(): + class Device: + def __init__(self): + self.device_id = "a" + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": Device()}) + + +def test_cannot_serialize_date(): + # A date is not an instant, so it has no unambiguous serialization. + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": date(2025, 2, 24)}) + + +def test_cannot_serialize_sets(): + # A set would not serialize deterministically. + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": {"a", "b"}}) + + +def test_cannot_serialize_array_params_with_unserializable_values(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": [""]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", None]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", NULL]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", ["s"]]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", []]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", [""]]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", {}]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", {"x": 2}]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", lambda: None]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": 1, "bar": ["", "a", ""]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": 1, "bar": ["", "a", "2"]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": 1, "bar": ["", "", ""]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": [1, float("nan")]}) + + +def test_unserializable_param_error_message(): + with pytest.raises(UnserializableParamError) as error: + serialize_url_search_params({"foo": {"bar.baz": 1}}) + + assert str(error.value) == ( + "Could not serialize parameter: 'bar.baz' contains one or more dots" + ' "." in its name which is unsupported' + ) + assert error.value.name == "bar.baz" + + +def test_unserializable_param_error_message_uses_the_full_path(): + with pytest.raises(UnserializableParamError) as error: + serialize_url_search_params({"foo": {"bar": float("nan")}}) + + assert str(error.value) == "Could not serialize parameter: 'foo.bar' is NaN" + + +def test_update_url_search_params(): + search_params = UrlSearchParams() + update_url_search_params(search_params, {"foo": "d", "bar": 2}) + + assert search_params.to_string() == "bar=2&foo=d" + + +def test_update_url_search_params_preserves_existing_params(): + search_params = UrlSearchParams([("foo", "bar")]) + update_url_search_params( + search_params, + {"name": "Dax", "age": 27, "is_admin": True, "tags": ["cars", "planes"]}, + ) + + assert search_params.to_string() == ( + "age=27&foo=bar&is_admin=true&name=Dax&tags=cars&tags=planes" + ) + + +def test_update_url_search_params_overwrites_existing_params(): + search_params = UrlSearchParams([("foo", "a"), ("bar", "x"), ("foo", "b")]) + update_url_search_params(search_params, {"foo": "new"}) + + assert search_params.to_string() == "bar=x&foo=new" + + +def test_update_url_search_params_appends_array_params(): + search_params = UrlSearchParams([("foo", "old")]) + update_url_search_params(search_params, {"foo": [1, 2]}) + + assert search_params.to_string() == "foo=old&foo=1&foo=2" + + +def test_update_url_search_params_keeps_existing_params_for_absent_values(): + for value in [None, "", {}]: + search_params = UrlSearchParams([("foo", "a")]) + update_url_search_params(search_params, {"foo": value}) + + assert search_params.to_string() == "foo=a" + + +def test_url_search_params_from_query_string(): + search_params = UrlSearchParams("?a=1&b=hello+world&c=%F0%9F%94%92&d") + + assert search_params.get("a") == "1" + assert search_params.get("b") == "hello world" + assert search_params.get("c") == "🔒" + assert search_params.get("d") == "" + assert search_params.to_string() == "a=1&b=hello+world&c=%F0%9F%94%92&d=" + + +def test_url_search_params_from_dict(): + assert UrlSearchParams({"a": "1", "b": "2"}).to_string() == "a=1&b=2" + + +def test_url_search_params_append_and_get(): + search_params = UrlSearchParams() + search_params.append("foo", "a") + search_params.append("foo", "b") + + assert search_params.get("foo") == "a" + assert search_params.get_all("foo") == ["a", "b"] + assert search_params.get("bar") is None + assert search_params.get_all("bar") == [] + assert len(search_params) == 2 + assert list(search_params) == [("foo", "a"), ("foo", "b")] + + +def test_url_search_params_set(): + search_params = UrlSearchParams([("foo", "a"), ("bar", "x"), ("foo", "b")]) + search_params.set("foo", "c") + + assert list(search_params) == [("foo", "c"), ("bar", "x")] + + search_params.set("baz", "y") + + assert search_params.get("baz") == "y" + + +def test_url_search_params_has_and_delete(): + search_params = UrlSearchParams([("foo", "a"), ("foo", "b")]) + + assert search_params.has("foo") + + search_params.delete("foo") + + assert not search_params.has("foo") + assert len(search_params) == 0 + + +def test_url_search_params_str(): + assert str(UrlSearchParams([("foo", "a b")])) == "foo=a+b"