diff --git a/codegen/layouts/partials/resource-dataclass.hbs b/codegen/layouts/partials/resource-dataclass.hbs index 9ea27150..acce063f 100644 --- a/codegen/layouts/partials/resource-dataclass.hbs +++ b/codegen/layouts/partials/resource-dataclass.hbs @@ -1,5 +1,5 @@ @dataclass -class {{className}}: +class {{className}}{{#if isNested}}(ResourceMapping){{/if}}: """{{{indent (pythonDoc description) 4}}}{{#each properties}} :ivar {{pythonIdentifier name}}: {{#if isDeprecated}}Deprecated{{#if deprecationMessage}}: {{{indent (pythonDoc deprecationMessage) 4}}}{{else}}.{{/if}}{{#if (pythonDoc description)}} {{/if}}{{/if}}{{{indent (pythonDoc description) 4}}}{{/each}}{{#if isDeprecated}} @@ -10,10 +10,10 @@ class {{className}}: {{pythonIdentifier name}}: {{type}} {{/each}} - @staticmethod - def from_dict(d: Dict[str, Any]): - return {{className}}( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( {{#each properties}} - {{pythonIdentifier name}}={{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}}, + {{pythonIdentifier name}}={{#if isObject}}{{type}}.from_dict(d.get("{{name}}")) if d.get("{{name}}") is not None else None{{else}}{{#if isObjectList}}[{{listItemType type}}.from_dict(i) for i in d.get("{{name}}") or []]{{else}}{{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}}{{/if}}{{/if}}, {{/each}} ) diff --git a/codegen/layouts/resource.hbs b/codegen/layouts/resource.hbs index 6d8a10fb..74cfc660 100644 --- a/codegen/layouts/resource.hbs +++ b/codegen/layouts/resource.hbs @@ -1,6 +1,9 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping - +{{#each nestedClasses}} +{{> resource-dataclass isNested=true}} +{{/each}} {{> resource-dataclass}} diff --git a/codegen/lib/layouts/resources.ts b/codegen/lib/layouts/resources.ts index bda68351..8bc552d7 100644 --- a/codegen/lib/layouts/resources.ts +++ b/codegen/lib/layouts/resources.ts @@ -2,7 +2,7 @@ // Each blueprint resource, along with events, action attempts, and pagination, // becomes a dataclass in its own module, re-exported from seam/resources/__init__.py. -import type { Blueprint, Property, Resource } from '@seamapi/blueprint' +import type { Blueprint, Property } from '@seamapi/blueprint' import { pascalCase, snakeCase } from 'change-case' import { convertCustomResourceName } from '../custom-resource-name-conversions.js' @@ -14,14 +14,25 @@ export interface ResourceLayoutContext { description: string isDeprecated: boolean deprecationMessage: string - properties: Array<{ - name: string - description: string - isDeprecated: boolean - deprecationMessage: string - type: string - isDictParam: boolean - }> + nestedClasses: ResourceClassLayoutContext[] + properties: ResourcePropertyLayoutContext[] +} + +interface ResourceClassLayoutContext { + className: string + description: string + properties: ResourcePropertyLayoutContext[] +} + +interface ResourcePropertyLayoutContext { + name: string + description: string + isDeprecated: boolean + deprecationMessage: string + type: string + isDictParam: boolean + isObject: boolean + isObjectList: boolean } export interface ResourcesIndexLayoutContext { @@ -31,7 +42,9 @@ export interface ResourcesIndexLayoutContext { // The action attempt and event variants each generate a single dataclass with // the union of the variant properties. The first occurrence of a property // name wins. -const mergeResourceProperties = (resources: Resource[]): Property[] => { +const mergeResourceProperties = ( + resources: Array<{ properties: Property[] }>, +): Property[] => { const merged = new Map() for (const { properties } of resources) { for (const property of properties) { @@ -91,6 +104,67 @@ export const getResourceLayoutContexts = ( const { properties, description, isDeprecated, deprecationMessage } = model const className = pascalCase(convertCustomResourceName(name)) + const nestedClasses = new Map() + + const buildProperties = ( + sourceProperties: Property[], + ): ResourcePropertyLayoutContext[] => + sourceProperties.map((property) => { + let nestedClassName: string | undefined + let nestedProperties: Property[] | undefined + if (property.format === 'object') { + nestedClassName = `${className}${pascalCase(property.name)}` + nestedProperties = property.properties + } else if ( + property.format === 'list' && + property.itemFormat === 'object' + ) { + nestedClassName = `${className}${pascalCase(property.name)}` + nestedProperties = property.itemProperties + } else if ( + property.format === 'list' && + property.itemFormat === 'discriminated_object' + ) { + nestedClassName = `${className}${pascalCase(property.name)}` + nestedProperties = mergeResourceProperties(property.variants) + } + + if ( + nestedClassName != null && + nestedProperties != null && + !nestedClasses.has(nestedClassName) + ) { + // Reserve the name before recursing so colliding/recursive shapes + // cannot register it twice. Reinsert after children for definition + // order: annotations are evaluated when each class is created. + nestedClasses.set(nestedClassName, { + className: nestedClassName, + description: property.description, + properties: [], + }) + const childProperties = buildProperties(nestedProperties) + nestedClasses.delete(nestedClassName) + nestedClasses.set(nestedClassName, { + className: nestedClassName, + description: property.description, + properties: childProperties, + }) + } + + const type = mapPropertyToPythonType(property, nestedClassName) + return { + name: property.name, + description: property.description, + isDeprecated: property.isDeprecated, + deprecationMessage: property.deprecationMessage, + type, + isDictParam: type.startsWith('Dict'), + isObject: nestedClassName != null && property.format === 'object', + isObjectList: nestedClassName != null && property.format === 'list', + } + }) + + const resourceProperties = buildProperties(properties) return { className, description, @@ -100,18 +174,8 @@ export const getResourceLayoutContexts = ( // module always matches the dataclass it exports (e.g. the "event" // resource becomes SeamEvent in seam_event.py). moduleName: snakeCase(className), - properties: properties.map((property) => { - const type = mapPropertyToPythonType(property) - return { - name: property.name, - description: property.description, - isDeprecated: property.isDeprecated, - deprecationMessage: property.deprecationMessage, - type, - isDictParam: - type.startsWith('Dict') || property.name === 'properties', - } - }), + nestedClasses: [...nestedClasses.values()], + properties: resourceProperties, } }) .sort((a, b) => (a.moduleName < b.moduleName ? -1 : 1)) diff --git a/codegen/lib/python-type.ts b/codegen/lib/python-type.ts index 4c45255e..feed09b5 100644 --- a/codegen/lib/python-type.ts +++ b/codegen/lib/python-type.ts @@ -21,9 +21,14 @@ export const mapParameterToPythonType = (parameter: Parameter): string => { return mapScalarFormatToPythonType(parameter.format) } -export const mapPropertyToPythonType = (property: Property): string => { +export const mapPropertyToPythonType = ( + property: Property, + nestedClassName?: string, +): string => { if (property.format === 'list') { - return `List[${mapListItemFormatToPythonType(property.itemFormat)}]` + return `List[${ + nestedClassName ?? mapListItemFormatToPythonType(property.itemFormat) + }]` } if (property.format === 'number') { @@ -36,6 +41,10 @@ export const mapPropertyToPythonType = (property: Property): string => { return 'List[Dict[str, Any]]' } + if (property.format === 'object' && nestedClassName != null) { + return nestedClassName + } + return mapScalarFormatToPythonType(property.format) } diff --git a/justfile b/justfile index 2b4b6962..d55b9a0b 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,7 @@ default: build poetry run pylint ./seam ./test poetry run black --check . poetry run rstcheck README.rst + poetry run mypy seam/resources --disable-error-code=arg-type --disable-error-code=import-not-found @test: poetry run pytest --cov=./seam diff --git a/poetry.lock b/poetry.lock index 273d8766..f7cbcb72 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -576,6 +576,107 @@ files = [ {file = "kiss_headers-2.4.3.tar.gz", hash = "sha256:70c689ce167ac83146f094ea916b40a3767d67c2e05a4cb95b0fd2e33bf243f1"}, ] +[[package]] +name = "librt" +version = "0.13.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5"}, + {file = "librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547"}, + {file = "librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2"}, + {file = "librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929"}, + {file = "librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a"}, + {file = "librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac"}, + {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7"}, + {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40"}, + {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a"}, + {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde"}, + {file = "librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8"}, + {file = "librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc"}, + {file = "librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082"}, + {file = "librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14"}, + {file = "librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79"}, + {file = "librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176"}, + {file = "librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89"}, + {file = "librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f"}, + {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d"}, + {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd"}, + {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588"}, + {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1"}, + {file = "librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21"}, + {file = "librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b"}, + {file = "librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c"}, + {file = "librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0"}, + {file = "librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5"}, + {file = "librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9"}, + {file = "librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b"}, + {file = "librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03"}, + {file = "librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e"}, + {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd"}, + {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348"}, + {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7"}, + {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82"}, + {file = "librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3"}, + {file = "librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa"}, + {file = "librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1"}, + {file = "librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3"}, + {file = "librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c"}, + {file = "librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c"}, + {file = "librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b"}, + {file = "librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9"}, + {file = "librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db"}, + {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6"}, + {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7"}, + {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1"}, + {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a"}, + {file = "librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628"}, + {file = "librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927"}, + {file = "librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650"}, + {file = "librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566"}, + {file = "librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71"}, + {file = "librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6"}, + {file = "librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f"}, + {file = "librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180"}, + {file = "librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6"}, + {file = "librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9"}, + {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007"}, + {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0"}, + {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1"}, + {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d"}, + {file = "librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16"}, + {file = "librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37"}, + {file = "librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39"}, + {file = "librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6"}, + {file = "librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5"}, + {file = "librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46"}, + {file = "librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e"}, + {file = "librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a"}, + {file = "librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22"}, + {file = "librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c"}, + {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0"}, + {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04"}, + {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61"}, + {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9"}, + {file = "librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18"}, + {file = "librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259"}, + {file = "librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99"}, + {file = "librt-0.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa"}, + {file = "librt-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390"}, + {file = "librt-0.13.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c"}, + {file = "librt-0.13.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4"}, + {file = "librt-0.13.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005"}, + {file = "librt-0.13.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94"}, + {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360"}, + {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b"}, + {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1"}, + {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97"}, + {file = "librt-0.13.0-cp39-cp39-win32.whl", hash = "sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a"}, + {file = "librt-0.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f"}, + {file = "librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781"}, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -641,6 +742,67 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -1477,4 +1639,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10.0" -content-hash = "fd2519084b0d659169ef9a16e040e029f73e53d1daf6c71a57bfba2aaaa5bf34" +content-hash = "5e6069a97d8f774c1413f8e09c6a4c3d639296a5f3797c0416cb23b38d407a77" diff --git a/pyproject.toml b/pyproject.toml index 3a8b535f..de41975d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ readme = "README.rst" homepage = "https://github.com/seamapi/python" repository = "https://github.com/seamapi/python" exclude = ["**/*_test.py"] +include = ["seam/py.typed"] [tool.poetry.dependencies] python = "^3.10.0" @@ -23,6 +24,7 @@ pytest-cov = "^5.0.0" pytest-runner = "^6.0.0" pytest-watch = "^4.2.0" rstcheck = "^6.1.2" +mypy = "^1.17.0" [build-system] requires = ["poetry>=1.8"] diff --git a/seam/py.typed b/seam/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/seam/resources/access_code.py b/seam/resources/access_code.py index ead1a49a..3f16459f 100644 --- a/seam/resources/access_code.py +++ b/seam/resources/access_code.py @@ -1,6 +1,237 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AccessCodeDormakabaOracodeMetadata(ResourceMapping): + """Metadata for a dormakaba Oracode managed access code. Only present for access codes from dormakaba Oracode devices. + + :ivar is_cancellable: Indicates whether the stay can be cancelled via the Dormakaba Oracode API. + + :ivar is_early_checkin_able: Indicates whether early check-in is available for this stay. + + :ivar is_extendable: Indicates whether the stay can be extended via the Dormakaba Oracode API. + + :ivar is_overridable: Indicates whether the access code can be overridden. When false, the maximum number of overrides has been reached. + + :ivar site_name: Dormakaba Oracode site name associated with this access code. + + :ivar stay_id: Dormakaba Oracode stay ID associated with this access code. + + :ivar user_level_id: Dormakaba Oracode user level ID associated with this access code. + + :ivar user_level_name: Dormakaba Oracode user level name associated with this access code. + """ + + is_cancellable: bool + is_early_checkin_able: bool + is_extendable: bool + is_overridable: bool + site_name: str + stay_id: float + user_level_id: str + user_level_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + is_cancellable=d.get("is_cancellable", None), + is_early_checkin_able=d.get("is_early_checkin_able", None), + is_extendable=d.get("is_extendable", None), + is_overridable=d.get("is_overridable", None), + site_name=d.get("site_name", None), + stay_id=d.get("stay_id", None), + user_level_id=d.get("user_level_id", None), + user_level_name=d.get("user_level_name", None), + ) + + +@dataclass +class AccessCodeModifiedFields(ResourceMapping): + """List of fields that were changed externally, with their previous and new values. + + :ivar field: The name of the field that was changed (e.g. ``code``, ``starts_at``, ``ends_at``). + + :ivar from_: The previous value of the field. + + :ivar to: The new value of the field.""" + + field: str + from_: str + to: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + field=d.get("field", None), + from_=d.get("from", None), + to=d.get("to", None), + ) + + +@dataclass +class AccessCodeErrors(ResourceMapping): + """Errors associated with the `access code `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar is_access_code_error: Indicates that this is an access code error. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar managed_access_code_id: ID of the managed access code that conflicts with this managed access code, when Seam can identify it. + + :ivar unmanaged_access_code_id: ID of the unmanaged access code that conflicts with this managed access code, when Seam can identify it. + + :ivar change_type: Indicates the type of external modification. ``modified`` means the code's PIN or schedule was changed. ``removed`` means the code was deleted from the device. + + :ivar modified_fields: List of fields that were changed externally, with their previous and new values. + + :ivar is_connected_account_error: Indicates that the error is a `connected account `_ error. + + :ivar is_device_error: Indicates that the error is not a device error. + + :ivar is_bridge_error: Indicates whether the error is related to `Seam Bridge `_. + """ + + created_at: str + error_code: str + is_access_code_error: bool + message: str + managed_access_code_id: str + unmanaged_access_code_id: str + change_type: str + modified_fields: List[AccessCodeModifiedFields] + is_connected_account_error: bool + is_device_error: bool + is_bridge_error: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + is_access_code_error=d.get("is_access_code_error", None), + message=d.get("message", None), + managed_access_code_id=d.get("managed_access_code_id", None), + unmanaged_access_code_id=d.get("unmanaged_access_code_id", None), + change_type=d.get("change_type", None), + modified_fields=[ + AccessCodeModifiedFields.from_dict(i) + for i in d.get("modified_fields") or [] + ], + is_connected_account_error=d.get("is_connected_account_error", None), + is_device_error=d.get("is_device_error", None), + is_bridge_error=d.get("is_bridge_error", None), + ) + + +@dataclass +class AccessCodeFrom(ResourceMapping): + """Previous code configuration. + + :ivar code: Previous PIN code.""" + + code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + code=d.get("code", None), + ) + + +@dataclass +class AccessCodeTo(ResourceMapping): + """New code configuration. + + :ivar code: New PIN code.""" + + code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + code=d.get("code", None), + ) + + +@dataclass +class AccessCodePendingMutations(ResourceMapping): + """Collection of pending mutations for the access code. Indicates changes that Seam is in the process of pushing to the device. + + :ivar created_at: Date and time at which the mutation was created. + + :ivar message: Detailed description of the mutation. + + :ivar mutation_code: Mutation code to indicate that Seam is in the process of setting an access code on the device. + + :ivar scheduled_at: Date and time at which Seam will attempt to program this access code on the device. + + :ivar from_: Previous code configuration. + + :ivar to: New code configuration.""" + + created_at: str + message: str + mutation_code: str + scheduled_at: str + from_: AccessCodeFrom + to: AccessCodeTo + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + mutation_code=d.get("mutation_code", None), + scheduled_at=d.get("scheduled_at", None), + from_=( + AccessCodeFrom.from_dict(d.get("from")) + if d.get("from") is not None + else None + ), + to=AccessCodeTo.from_dict(d.get("to")) if d.get("to") is not None else None, + ) + + +@dataclass +class AccessCodeWarnings(ResourceMapping): + """Warnings associated with the `access code `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar change_type: Indicates the type of external modification. ``modified`` means the code's PIN or schedule was changed. ``removed`` means the code was deleted from the device. + + :ivar modified_fields: List of fields that were changed externally, with their previous and new values. + """ + + created_at: str + message: str + warning_code: str + change_type: str + modified_fields: List[AccessCodeModifiedFields] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + change_type=d.get("change_type", None), + modified_fields=[ + AccessCodeModifiedFields.from_dict(i) + for i in d.get("modified_fields") or [] + ], + ) @dataclass @@ -69,9 +300,9 @@ class AccessCode: common_code_key: str created_at: str device_id: str - dormakaba_oracode_metadata: Dict[str, Any] + dormakaba_oracode_metadata: AccessCodeDormakabaOracodeMetadata ends_at: str - errors: List[Dict[str, Any]] + errors: List[AccessCodeErrors] is_backup: bool is_backup_access_code_available: bool is_external_modification_allowed: bool @@ -81,27 +312,31 @@ class AccessCode: is_scheduled_on_device: bool is_waiting_for_code_assignment: bool name: str - pending_mutations: List[Dict[str, Any]] + pending_mutations: List[AccessCodePendingMutations] pulled_backup_access_code_id: str starts_at: str status: str type: str - warnings: List[Dict[str, Any]] + warnings: List[AccessCodeWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return AccessCode( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_code_id=d.get("access_code_id", None), code=d.get("code", None), common_code_key=d.get("common_code_key", None), created_at=d.get("created_at", None), device_id=d.get("device_id", None), - dormakaba_oracode_metadata=DeepAttrDict( - d.get("dormakaba_oracode_metadata", None) + dormakaba_oracode_metadata=( + AccessCodeDormakabaOracodeMetadata.from_dict( + d.get("dormakaba_oracode_metadata") + ) + if d.get("dormakaba_oracode_metadata") is not None + else None ), ends_at=d.get("ends_at", None), - errors=d.get("errors", None), + errors=[AccessCodeErrors.from_dict(i) for i in d.get("errors") or []], is_backup=d.get("is_backup", None), is_backup_access_code_available=d.get( "is_backup_access_code_available", None @@ -117,11 +352,14 @@ def from_dict(d: Dict[str, Any]): "is_waiting_for_code_assignment", None ), name=d.get("name", None), - pending_mutations=d.get("pending_mutations", None), + pending_mutations=[ + AccessCodePendingMutations.from_dict(i) + for i in d.get("pending_mutations") or [] + ], pulled_backup_access_code_id=d.get("pulled_backup_access_code_id", None), starts_at=d.get("starts_at", None), status=d.get("status", None), type=d.get("type", None), - warnings=d.get("warnings", None), + warnings=[AccessCodeWarnings.from_dict(i) for i in d.get("warnings") or []], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/access_grant.py b/seam/resources/access_grant.py index ad2b7426..0ae6222c 100644 --- a/seam/resources/access_grant.py +++ b/seam/resources/access_grant.py @@ -1,6 +1,222 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AccessGrantErrors(ResourceMapping): + """Errors associated with the `access grant `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar missing_device_ids: IDs of the devices that did not receive an access code at grant creation. Use these to identify which specific devices failed when the message reports a partial failure. + """ + + created_at: str + error_code: str + message: str + missing_device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + missing_device_ids=d.get("missing_device_ids", None), + ) + + +@dataclass +class AccessGrantFrom(ResourceMapping): + """Previous location configuration. + + :ivar device_ids: Previous device IDs where access codes existed.""" + + device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_ids=d.get("device_ids", None), + ) + + +@dataclass +class AccessGrantTo(ResourceMapping): + """New location configuration. + + :ivar common_code_key: Common code key to ensure PIN code reuse across devices. + + :ivar device_ids: New device IDs where access codes should be created.""" + + common_code_key: str + device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + common_code_key=d.get("common_code_key", None), + device_ids=d.get("device_ids", None), + ) + + +@dataclass +class AccessGrantPendingMutations(ResourceMapping): + """List of pending mutations for the access grant. This shows updates that are in progress. + + :ivar created_at: Date and time at which the mutation was created. + + :ivar from_: Previous location configuration. + + :ivar message: Detailed description of the mutation. + + :ivar mutation_code: Mutation code to indicate that Seam is in the process of updating the spaces (devices) associated with this access grant. + + :ivar to: New location configuration. + + :ivar access_method_ids: IDs of the access methods being updated.""" + + created_at: str + from_: AccessGrantFrom + message: str + mutation_code: str + to: AccessGrantTo + access_method_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + from_=( + AccessGrantFrom.from_dict(d.get("from")) + if d.get("from") is not None + else None + ), + message=d.get("message", None), + mutation_code=d.get("mutation_code", None), + to=( + AccessGrantTo.from_dict(d.get("to")) + if d.get("to") is not None + else None + ), + access_method_ids=d.get("access_method_ids", None), + ) + + +@dataclass +class AccessGrantRequestedAccessMethods(ResourceMapping): + """Access methods that the user requested for the Access Grant. + + :ivar code: Specific PIN code to use for this access method. Only applicable when mode is 'code'. + + :ivar created_access_method_ids: IDs of the access methods created for the requested access method. + + :ivar created_at: Date and time at which the requested access method was added to the Access Grant. + + :ivar display_name: Display name of the access method. + + :ivar instant_key_max_use_count: Maximum number of times the instant key can be used. Only applicable when mode is 'mobile_key'. Defaults to 1 if not specified. + + :ivar mode: Access method mode. Supported values: ``code``, ``card``, ``mobile_key``, ``cloud_key``. + """ + + code: str + created_access_method_ids: List[str] + created_at: str + display_name: str + instant_key_max_use_count: int + mode: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + code=d.get("code", None), + created_access_method_ids=d.get("created_access_method_ids", None), + created_at=d.get("created_at", None), + display_name=d.get("display_name", None), + instant_key_max_use_count=d.get("instant_key_max_use_count", None), + mode=d.get("mode", None), + ) + + +@dataclass +class AccessGrantFailedDevices(ResourceMapping): + """Devices whose access codes could not be revoked during reconciliation. Present when the provider does not support revoking an offline access code (e.g. Dormakaba oracode with exhausted override budget). + + :ivar device_id: Device whose access code could not be revoked. + + :ivar error_code: Reason the access code could not be revoked (e.g. ``offline_access_code_not_revocable``). + + :ivar message: Human-readable description of why revocation failed.""" + + device_id: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class AccessGrantWarnings(ResourceMapping): + """Warnings associated with the `access grant `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar failed_devices: Devices whose access codes could not be revoked during reconciliation. Present when the provider does not support revoking an offline access code (e.g. Dormakaba oracode with exhausted override budget). + + :ivar access_method_ids: IDs of the access methods being updated. + + :ivar device_id: ID of the device where the requested code was unavailable. + + :ivar new_code: The new PIN code that was assigned instead. + + :ivar original_code: The originally requested PIN code that was unavailable. + + :ivar reason: Specific reason why the grant's times are not programmable on the device. + """ + + created_at: str + message: str + warning_code: str + failed_devices: List[AccessGrantFailedDevices] + access_method_ids: List[str] + device_id: str + new_code: str + original_code: str + reason: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + failed_devices=[ + AccessGrantFailedDevices.from_dict(i) + for i in d.get("failed_devices") or [] + ], + access_method_ids=d.get("access_method_ids", None), + device_id=d.get("device_id", None), + new_code=d.get("new_code", None), + original_code=d.get("original_code", None), + reason=d.get("reason", None), + ) @dataclass @@ -55,22 +271,22 @@ class AccessGrant: customization_profile_id: str display_name: str ends_at: str - errors: List[Dict[str, Any]] + errors: List[AccessGrantErrors] instant_key_url: str location_ids: List[str] name: str - pending_mutations: List[Dict[str, Any]] - requested_access_methods: List[Dict[str, Any]] + pending_mutations: List[AccessGrantPendingMutations] + requested_access_methods: List[AccessGrantRequestedAccessMethods] reservation_key: str space_ids: List[str] starts_at: str user_identity_id: str - warnings: List[Dict[str, Any]] + warnings: List[AccessGrantWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return AccessGrant( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_grant_id=d.get("access_grant_id", None), access_grant_key=d.get("access_grant_key", None), access_method_ids=d.get("access_method_ids", None), @@ -79,16 +295,24 @@ def from_dict(d: Dict[str, Any]): customization_profile_id=d.get("customization_profile_id", None), display_name=d.get("display_name", None), ends_at=d.get("ends_at", None), - errors=d.get("errors", None), + errors=[AccessGrantErrors.from_dict(i) for i in d.get("errors") or []], instant_key_url=d.get("instant_key_url", None), location_ids=d.get("location_ids", None), name=d.get("name", None), - pending_mutations=d.get("pending_mutations", None), - requested_access_methods=d.get("requested_access_methods", None), + pending_mutations=[ + AccessGrantPendingMutations.from_dict(i) + for i in d.get("pending_mutations") or [] + ], + requested_access_methods=[ + AccessGrantRequestedAccessMethods.from_dict(i) + for i in d.get("requested_access_methods") or [] + ], reservation_key=d.get("reservation_key", None), space_ids=d.get("space_ids", None), starts_at=d.get("starts_at", None), user_identity_id=d.get("user_identity_id", None), - warnings=d.get("warnings", None), + warnings=[ + AccessGrantWarnings.from_dict(i) for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/access_method.py b/seam/resources/access_method.py index 415f0ff2..b193c8f8 100644 --- a/seam/resources/access_method.py +++ b/seam/resources/access_method.py @@ -1,6 +1,128 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AccessMethodErrors(ResourceMapping): + """Errors associated with the `access method `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class AccessMethodFrom(ResourceMapping): + """Previous device configuration. + + :ivar device_ids: Previous device IDs where access was provisioned.""" + + device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_ids=d.get("device_ids", None), + ) + + +@dataclass +class AccessMethodTo(ResourceMapping): + """New device configuration. + + :ivar device_ids: New device IDs where access is being provisioned.""" + + device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_ids=d.get("device_ids", None), + ) + + +@dataclass +class AccessMethodPendingMutations(ResourceMapping): + """Pending mutations for the `access method `_. Indicates operations that are in progress. + + :ivar created_at: Date and time at which the mutation was created. + + :ivar from_: Previous device configuration. + + :ivar message: Detailed description of the mutation. + + :ivar mutation_code: Mutation code to indicate that Seam is in the process of provisioning access for this access method on new devices. + + :ivar to: New device configuration.""" + + created_at: str + from_: AccessMethodFrom + message: str + mutation_code: str + to: AccessMethodTo + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + from_=( + AccessMethodFrom.from_dict(d.get("from")) + if d.get("from") is not None + else None + ), + message=d.get("message", None), + mutation_code=d.get("mutation_code", None), + to=( + AccessMethodTo.from_dict(d.get("to")) + if d.get("to") is not None + else None + ), + ) + + +@dataclass +class AccessMethodWarnings(ResourceMapping): + """Warnings associated with the `access method `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar original_access_method_id: ID of the original access method from which this backup access method was split, if applicable. + """ + + created_at: str + message: str + warning_code: str + original_access_method_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + original_access_method_id=d.get("original_access_method_id", None), + ) @dataclass @@ -49,7 +171,7 @@ class AccessMethod: created_at: str customization_profile_id: str display_name: str - errors: List[Dict[str, Any]] + errors: List[AccessMethodErrors] instant_key_url: str is_assignment_required: bool is_encoding_required: bool @@ -58,20 +180,20 @@ class AccessMethod: is_ready_for_encoding: bool issued_at: str mode: str - pending_mutations: List[Dict[str, Any]] - warnings: List[Dict[str, Any]] + pending_mutations: List[AccessMethodPendingMutations] + warnings: List[AccessMethodWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return AccessMethod( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_method_id=d.get("access_method_id", None), client_session_token=d.get("client_session_token", None), code=d.get("code", None), created_at=d.get("created_at", None), customization_profile_id=d.get("customization_profile_id", None), display_name=d.get("display_name", None), - errors=d.get("errors", None), + errors=[AccessMethodErrors.from_dict(i) for i in d.get("errors") or []], instant_key_url=d.get("instant_key_url", None), is_assignment_required=d.get("is_assignment_required", None), is_encoding_required=d.get("is_encoding_required", None), @@ -80,7 +202,12 @@ def from_dict(d: Dict[str, Any]): is_ready_for_encoding=d.get("is_ready_for_encoding", None), issued_at=d.get("issued_at", None), mode=d.get("mode", None), - pending_mutations=d.get("pending_mutations", None), - warnings=d.get("warnings", None), + pending_mutations=[ + AccessMethodPendingMutations.from_dict(i) + for i in d.get("pending_mutations") or [] + ], + warnings=[ + AccessMethodWarnings.from_dict(i) for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/acs_access_group.py b/seam/resources/acs_access_group.py index a11b9463..4e626b6a 100644 --- a/seam/resources/acs_access_group.py +++ b/seam/resources/acs_access_group.py @@ -1,6 +1,153 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AcsAccessGroupAccessSchedule(ResourceMapping): + """``starts_at`` and ``ends_at`` timestamps for the access group's access. + + :ivar ends_at: Date and time at which the user's access ends, in `ISO 8601 `_ format. + + :ivar starts_at: Date and time at which the user's access starts, in `ISO 8601 `_ format. + """ + + ends_at: str + starts_at: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + ends_at=d.get("ends_at", None), + starts_at=d.get("starts_at", None), + ) + + +@dataclass +class AcsAccessGroupErrors(ResourceMapping): + """Errors associated with the ``acs_access_group``. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class AcsAccessGroupFrom(ResourceMapping): + """Old access group information. + + :ivar name: Name of the access group.""" + + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + name=d.get("name", None), + ) + + +@dataclass +class AcsAccessGroupTo(ResourceMapping): + """New access group information. + + :ivar name: Name of the access group.""" + + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + name=d.get("name", None), + ) + + +@dataclass +class AcsAccessGroupPendingMutations(ResourceMapping): + """Collection of pending mutations for the access group. Represents operations that have been requested but not yet completed on the integrated access system. + + :ivar created_at: Date and time at which the mutation was created. + + :ivar message: Detailed description of the mutation. + + :ivar mutation_code: Mutation code to indicate that Seam is in the process of pushing an access group creation to the integrated access system. + + :ivar from_: Old access group information. + + :ivar to: New access group information. + + :ivar acs_user_id: ID of the user involved in the scheduled change. + + :ivar variant: Whether the user is scheduled to be added to or removed from this access group. + """ + + created_at: str + message: str + mutation_code: str + from_: AcsAccessGroupFrom + to: AcsAccessGroupTo + acs_user_id: str + variant: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + mutation_code=d.get("mutation_code", None), + from_=( + AcsAccessGroupFrom.from_dict(d.get("from")) + if d.get("from") is not None + else None + ), + to=( + AcsAccessGroupTo.from_dict(d.get("to")) + if d.get("to") is not None + else None + ), + acs_user_id=d.get("acs_user_id", None), + variant=d.get("variant", None), + ) + + +@dataclass +class AcsAccessGroupWarnings(ResourceMapping): + """Warnings associated with the ``acs_access_group``. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) @dataclass @@ -45,40 +192,49 @@ class AcsAccessGroup: access_group_type: str access_group_type_display_name: str - access_schedule: Dict[str, Any] + access_schedule: AcsAccessGroupAccessSchedule acs_access_group_id: str acs_system_id: str connected_account_id: str created_at: str display_name: str - errors: List[Dict[str, Any]] + errors: List[AcsAccessGroupErrors] external_type: str external_type_display_name: str is_managed: bool name: str - pending_mutations: List[Dict[str, Any]] - warnings: List[Dict[str, Any]] + pending_mutations: List[AcsAccessGroupPendingMutations] + warnings: List[AcsAccessGroupWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return AcsAccessGroup( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_group_type=d.get("access_group_type", None), access_group_type_display_name=d.get( "access_group_type_display_name", None ), - access_schedule=DeepAttrDict(d.get("access_schedule", None)), + access_schedule=( + AcsAccessGroupAccessSchedule.from_dict(d.get("access_schedule")) + if d.get("access_schedule") is not None + else None + ), acs_access_group_id=d.get("acs_access_group_id", None), acs_system_id=d.get("acs_system_id", None), connected_account_id=d.get("connected_account_id", None), created_at=d.get("created_at", None), display_name=d.get("display_name", None), - errors=d.get("errors", None), + errors=[AcsAccessGroupErrors.from_dict(i) for i in d.get("errors") or []], external_type=d.get("external_type", None), external_type_display_name=d.get("external_type_display_name", None), is_managed=d.get("is_managed", None), name=d.get("name", None), - pending_mutations=d.get("pending_mutations", None), - warnings=d.get("warnings", None), + pending_mutations=[ + AcsAccessGroupPendingMutations.from_dict(i) + for i in d.get("pending_mutations") or [] + ], + warnings=[ + AcsAccessGroupWarnings.from_dict(i) for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/acs_credential.py b/seam/resources/acs_credential.py index 64356ddd..f6a63e95 100644 --- a/seam/resources/acs_credential.py +++ b/seam/resources/acs_credential.py @@ -1,6 +1,136 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AcsCredentialAssaAbloyVostioMetadata(ResourceMapping): + """Vostio-specific metadata for the `credential `_. + + :ivar auto_join: Indicates whether the credential should auto-join. For an auto-join credential, Seam automatically issues an override card if there are no other cards and a joiner card if there are existing cards on the doors. + + :ivar door_names: Names of the doors to which to grant access in the Vostio access system. + + :ivar endpoint_id: Endpoint ID in the Vostio access system. + + :ivar key_id: Key ID in the Vostio access system. + + :ivar key_issuing_request_id: Key issuing request ID in the Vostio access system. + + :ivar override_guest_acs_entrance_ids: IDs of the guest entrances to override in the Vostio access system. + """ + + auto_join: bool + door_names: List[str] + endpoint_id: str + key_id: str + key_issuing_request_id: str + override_guest_acs_entrance_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + auto_join=d.get("auto_join", None), + door_names=d.get("door_names", None), + endpoint_id=d.get("endpoint_id", None), + key_id=d.get("key_id", None), + key_issuing_request_id=d.get("key_issuing_request_id", None), + override_guest_acs_entrance_ids=d.get( + "override_guest_acs_entrance_ids", None + ), + ) + + +@dataclass +class AcsCredentialErrors(ResourceMapping): + """Errors associated with the `credential `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: + + :ivar message:""" + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class AcsCredentialVisionlineMetadata(ResourceMapping): + """Visionline-specific metadata for the `credential `_. + + :ivar auto_join: Indicates whether the credential should auto-join. For an auto-join credential, Seam automatically issues an override card if there are no other cards and a joiner card if there are existing cards on the doors. + + :ivar card_function_type: Card function type in the Visionline access system. + + :ivar card_id: ID of the card in the Visionline access system. + + :ivar common_acs_entrance_ids: Common entrance IDs in the Visionline access system. + + :ivar credential_id: ID of the credential in the Visionline access system. + + :ivar guest_acs_entrance_ids: Guest entrance IDs in the Visionline access system. + + :ivar is_valid: Indicates whether the credential is valid. + + :ivar joiner_acs_credential_ids: IDs of the credentials to which you want to join. + """ + + auto_join: bool + card_function_type: str + card_id: str + common_acs_entrance_ids: List[str] + credential_id: str + guest_acs_entrance_ids: List[str] + is_valid: bool + joiner_acs_credential_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + auto_join=d.get("auto_join", None), + card_function_type=d.get("card_function_type", None), + card_id=d.get("card_id", None), + common_acs_entrance_ids=d.get("common_acs_entrance_ids", None), + credential_id=d.get("credential_id", None), + guest_acs_entrance_ids=d.get("guest_acs_entrance_ids", None), + is_valid=d.get("is_valid", None), + joiner_acs_credential_ids=d.get("joiner_acs_credential_ids", None), + ) + + +@dataclass +class AcsCredentialWarnings(ResourceMapping): + """Warnings associated with the `credential `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) @dataclass @@ -75,14 +205,14 @@ class AcsCredential: acs_credential_pool_id: str acs_system_id: str acs_user_id: str - assa_abloy_vostio_metadata: Dict[str, Any] + assa_abloy_vostio_metadata: AcsCredentialAssaAbloyVostioMetadata card_number: str code: str connected_account_id: str created_at: str display_name: str ends_at: str - errors: List[Dict[str, Any]] + errors: List[AcsCredentialErrors] external_type: str external_type_display_name: str is_issued: bool @@ -95,20 +225,24 @@ class AcsCredential: parent_acs_credential_id: str starts_at: str user_identity_id: str - visionline_metadata: Dict[str, Any] - warnings: List[Dict[str, Any]] + visionline_metadata: AcsCredentialVisionlineMetadata + warnings: List[AcsCredentialWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return AcsCredential( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_method=d.get("access_method", None), acs_credential_id=d.get("acs_credential_id", None), acs_credential_pool_id=d.get("acs_credential_pool_id", None), acs_system_id=d.get("acs_system_id", None), acs_user_id=d.get("acs_user_id", None), - assa_abloy_vostio_metadata=DeepAttrDict( - d.get("assa_abloy_vostio_metadata", None) + assa_abloy_vostio_metadata=( + AcsCredentialAssaAbloyVostioMetadata.from_dict( + d.get("assa_abloy_vostio_metadata") + ) + if d.get("assa_abloy_vostio_metadata") is not None + else None ), card_number=d.get("card_number", None), code=d.get("code", None), @@ -116,7 +250,7 @@ def from_dict(d: Dict[str, Any]): created_at=d.get("created_at", None), display_name=d.get("display_name", None), ends_at=d.get("ends_at", None), - errors=d.get("errors", None), + errors=[AcsCredentialErrors.from_dict(i) for i in d.get("errors") or []], external_type=d.get("external_type", None), external_type_display_name=d.get("external_type_display_name", None), is_issued=d.get("is_issued", None), @@ -135,7 +269,13 @@ def from_dict(d: Dict[str, Any]): parent_acs_credential_id=d.get("parent_acs_credential_id", None), starts_at=d.get("starts_at", None), user_identity_id=d.get("user_identity_id", None), - visionline_metadata=DeepAttrDict(d.get("visionline_metadata", None)), - warnings=d.get("warnings", None), + visionline_metadata=( + AcsCredentialVisionlineMetadata.from_dict(d.get("visionline_metadata")) + if d.get("visionline_metadata") is not None + else None + ), + warnings=[ + AcsCredentialWarnings.from_dict(i) for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/acs_encoder.py b/seam/resources/acs_encoder.py index f27c4b2e..daeb201a 100644 --- a/seam/resources/acs_encoder.py +++ b/seam/resources/acs_encoder.py @@ -1,6 +1,31 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AcsEncoderErrors(ResourceMapping): + """Errors associated with the `encoder `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) @dataclass @@ -40,17 +65,17 @@ class AcsEncoder: connected_account_id: str created_at: str display_name: str - errors: List[Dict[str, Any]] + errors: List[AcsEncoderErrors] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return AcsEncoder( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( acs_encoder_id=d.get("acs_encoder_id", None), acs_system_id=d.get("acs_system_id", None), connected_account_id=d.get("connected_account_id", None), created_at=d.get("created_at", None), display_name=d.get("display_name", None), - errors=d.get("errors", None), + errors=[AcsEncoderErrors.from_dict(i) for i in d.get("errors") or []], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/acs_entrance.py b/seam/resources/acs_entrance.py index 02b76571..95e78d1a 100644 --- a/seam/resources/acs_entrance.py +++ b/seam/resources/acs_entrance.py @@ -1,6 +1,400 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AcsEntranceActions(ResourceMapping): + """Actions the gadget exposes (for example, open). + + :ivar id: ID of the gadget action. + + :ivar name: Name of the gadget action.""" + + id: str + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + id=d.get("id", None), + name=d.get("name", None), + ) + + +@dataclass +class AcsEntranceAkilesMetadata(ResourceMapping): + """Akiles-specific metadata associated with the `entrance `_. + + :ivar actions: Actions the gadget exposes (for example, open). + + :ivar gadget_id: ID of the Akiles gadget. + + :ivar site_id: ID of the Akiles site the gadget belongs to. + + :ivar site_name: Name of the Akiles site the gadget belongs to.""" + + actions: List[AcsEntranceActions] + gadget_id: str + site_id: str + site_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + actions=[AcsEntranceActions.from_dict(i) for i in d.get("actions") or []], + gadget_id=d.get("gadget_id", None), + site_id=d.get("site_id", None), + site_name=d.get("site_name", None), + ) + + +@dataclass +class AcsEntranceAssaAbloyVostioMetadata(ResourceMapping): + """ASSA ABLOY Vostio-specific metadata associated with the `entrance `_. + + :ivar door_name: Name of the door in the Vostio access system. + + :ivar door_number: Number of the door in the Vostio access system. + + :ivar door_type: Type of the door in the Vostio access system. + + :ivar pms_id: PMS ID of the door in the Vostio access system. + + :ivar stand_open: Indicates whether keys are allowed to set the door in stand open mode in the Vostio access system. + """ + + door_name: str + door_number: float + door_type: str + pms_id: str + stand_open: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + door_name=d.get("door_name", None), + door_number=d.get("door_number", None), + door_type=d.get("door_type", None), + pms_id=d.get("pms_id", None), + stand_open=d.get("stand_open", None), + ) + + +@dataclass +class AcsEntranceAvigilonAltaMetadata(ResourceMapping): + """Avigilon Alta-specific metadata associated with the `entrance `_. + + :ivar entry_name: Entry name for an Avigilon Alta system. + + :ivar entry_relays_total_count: Total count of entry relays for an Avigilon Alta system. + + :ivar org_name: Organization name for an Avigilon Alta system. + + :ivar site_id: Site ID for an Avigilon Alta system. + + :ivar site_name: Site name for an Avigilon Alta system. + + :ivar zone_id: Zone ID for an Avigilon Alta system. + + :ivar zone_name: Zone name for an Avigilon Alta system.""" + + entry_name: str + entry_relays_total_count: float + org_name: str + site_id: float + site_name: str + zone_id: float + zone_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + entry_name=d.get("entry_name", None), + entry_relays_total_count=d.get("entry_relays_total_count", None), + org_name=d.get("org_name", None), + site_id=d.get("site_id", None), + site_name=d.get("site_name", None), + zone_id=d.get("zone_id", None), + zone_name=d.get("zone_name", None), + ) + + +@dataclass +class AcsEntranceBrivoMetadata(ResourceMapping): + """Brivo-specific metadata associated with the `entrance `_. + + :ivar access_point_id: ID of the access point in the Brivo access system. + + :ivar site_id: ID of the site that the access point belongs to. + + :ivar site_name: Name of the site that the access point belongs to.""" + + access_point_id: str + site_id: float + site_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + access_point_id=d.get("access_point_id", None), + site_id=d.get("site_id", None), + site_name=d.get("site_name", None), + ) + + +@dataclass +class AcsEntranceDormakabaAmbianceMetadata(ResourceMapping): + """dormakaba Ambiance-specific metadata associated with the `entrance `_. + + :ivar access_point_name: Name of the access point in the dormakaba Ambiance access system. + """ + + access_point_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + access_point_name=d.get("access_point_name", None), + ) + + +@dataclass +class AcsEntranceDormakabaCommunityMetadata(ResourceMapping): + """dormakaba Community-specific metadata associated with the `entrance `_. + + :ivar access_point_profile: Type of access point profile in the dormakaba Community access system. + """ + + access_point_profile: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + access_point_profile=d.get("access_point_profile", None), + ) + + +@dataclass +class AcsEntranceErrors(ResourceMapping): + """Errors associated with the `entrance `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class AcsEntranceHotekMetadata(ResourceMapping): + """Hotek-specific metadata associated with the `entrance `_. + + :ivar common_area_name: Display name of the entrance. + + :ivar common_area_number: Display name of the entrance. + + :ivar room_number: Room number of the entrance.""" + + common_area_name: str + common_area_number: str + room_number: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + common_area_name=d.get("common_area_name", None), + common_area_number=d.get("common_area_number", None), + room_number=d.get("room_number", None), + ) + + +@dataclass +class AcsEntranceLatchMetadata(ResourceMapping): + """Latch-specific metadata associated with the `entrance `_. + + :ivar accessibility_type: Accessibility type in the Latch access system. + + :ivar door_name: Name of the door in the Latch access system. + + :ivar door_type: Type of the door in the Latch access system. + + :ivar is_connected: Indicates whether the entrance is connected.""" + + accessibility_type: str + door_name: str + door_type: str + is_connected: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + accessibility_type=d.get("accessibility_type", None), + door_name=d.get("door_name", None), + door_type=d.get("door_type", None), + is_connected=d.get("is_connected", None), + ) + + +@dataclass +class AcsEntranceSaltoKsMetadata(ResourceMapping): + """Salto KS-specific metadata associated with the `entrance `_. + + :ivar battery_level: Battery level of the door access device. + + :ivar door_name: Name of the door in the Salto KS access system. + + :ivar intrusion_alarm: Indicates whether an intrusion alarm is active on the door. + + :ivar left_open_alarm: Indicates whether the door is left open. + + :ivar lock_type: Type of the lock in the Salto KS access system. + + :ivar locked_state: Locked state of the door in the Salto KS access system. + + :ivar online: Indicates whether the door access device is online. + + :ivar privacy_mode: Indicates whether privacy mode is enabled for the lock.""" + + battery_level: str + door_name: str + intrusion_alarm: bool + left_open_alarm: bool + lock_type: str + locked_state: str + online: bool + privacy_mode: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + battery_level=d.get("battery_level", None), + door_name=d.get("door_name", None), + intrusion_alarm=d.get("intrusion_alarm", None), + left_open_alarm=d.get("left_open_alarm", None), + lock_type=d.get("lock_type", None), + locked_state=d.get("locked_state", None), + online=d.get("online", None), + privacy_mode=d.get("privacy_mode", None), + ) + + +@dataclass +class AcsEntranceSaltoSpaceMetadata(ResourceMapping): + """Salto Space-specific metadata associated with the `entrance `_. + + :ivar audit_on_keys: Indicates whether AuditOnKeys is enabled for the door in the Salto Space access system. + + :ivar door_description: Description of the door in the Salto Space access system. + + :ivar door_id: Door ID in the Salto Space access system. + + :ivar door_name: Name of the door in the Salto Space access system. + + :ivar room_description: Description of the room in the Salto Space access system. + + :ivar room_name: Name of the room in the Salto Space access system.""" + + audit_on_keys: bool + door_description: str + door_id: str + door_name: str + room_description: str + room_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + audit_on_keys=d.get("audit_on_keys", None), + door_description=d.get("door_description", None), + door_id=d.get("door_id", None), + door_name=d.get("door_name", None), + room_description=d.get("room_description", None), + room_name=d.get("room_name", None), + ) + + +@dataclass +class AcsEntranceProfiles(ResourceMapping): + """Profile for the door in the Visionline access system. + + :ivar visionline_door_profile_id: Door profile ID in the Visionline access system. + + :ivar visionline_door_profile_type: Door profile type in the Visionline access system. + """ + + visionline_door_profile_id: str + visionline_door_profile_type: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + visionline_door_profile_id=d.get("visionline_door_profile_id", None), + visionline_door_profile_type=d.get("visionline_door_profile_type", None), + ) + + +@dataclass +class AcsEntranceVisionlineMetadata(ResourceMapping): + """Visionline-specific metadata associated with the `entrance `_. + + :ivar door_category: Category of the door in the Visionline access system. + + :ivar door_name: Name of the door in the Visionline access system. + + :ivar profiles: Profile for the door in the Visionline access system.""" + + door_category: str + door_name: str + profiles: List[AcsEntranceProfiles] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + door_category=d.get("door_category", None), + door_name=d.get("door_name", None), + profiles=[ + AcsEntranceProfiles.from_dict(i) for i in d.get("profiles") or [] + ], + ) + + +@dataclass +class AcsEntranceWarnings(ResourceMapping): + """Warnings associated with the `entrance `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) @dataclass @@ -62,10 +456,10 @@ class AcsEntrance: acs_entrance_id: str acs_system_id: str - akiles_metadata: Dict[str, Any] - assa_abloy_vostio_metadata: Dict[str, Any] - avigilon_alta_metadata: Dict[str, Any] - brivo_metadata: Dict[str, Any] + akiles_metadata: AcsEntranceAkilesMetadata + assa_abloy_vostio_metadata: AcsEntranceAssaAbloyVostioMetadata + avigilon_alta_metadata: AcsEntranceAvigilonAltaMetadata + brivo_metadata: AcsEntranceBrivoMetadata can_belong_to_reservation: bool can_unlock_with_card: bool can_unlock_with_cloud_key: bool @@ -74,29 +468,47 @@ class AcsEntrance: connected_account_id: str created_at: str display_name: str - dormakaba_ambiance_metadata: Dict[str, Any] - dormakaba_community_metadata: Dict[str, Any] - errors: List[Dict[str, Any]] - hotek_metadata: Dict[str, Any] + dormakaba_ambiance_metadata: AcsEntranceDormakabaAmbianceMetadata + dormakaba_community_metadata: AcsEntranceDormakabaCommunityMetadata + errors: List[AcsEntranceErrors] + hotek_metadata: AcsEntranceHotekMetadata is_locked: bool - latch_metadata: Dict[str, Any] - salto_ks_metadata: Dict[str, Any] - salto_space_metadata: Dict[str, Any] + latch_metadata: AcsEntranceLatchMetadata + salto_ks_metadata: AcsEntranceSaltoKsMetadata + salto_space_metadata: AcsEntranceSaltoSpaceMetadata space_ids: List[str] - visionline_metadata: Dict[str, Any] - warnings: List[Dict[str, Any]] + visionline_metadata: AcsEntranceVisionlineMetadata + warnings: List[AcsEntranceWarnings] - @staticmethod - def from_dict(d: Dict[str, Any]): - return AcsEntrance( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( acs_entrance_id=d.get("acs_entrance_id", None), acs_system_id=d.get("acs_system_id", None), - akiles_metadata=DeepAttrDict(d.get("akiles_metadata", None)), - assa_abloy_vostio_metadata=DeepAttrDict( - d.get("assa_abloy_vostio_metadata", None) + akiles_metadata=( + AcsEntranceAkilesMetadata.from_dict(d.get("akiles_metadata")) + if d.get("akiles_metadata") is not None + else None + ), + assa_abloy_vostio_metadata=( + AcsEntranceAssaAbloyVostioMetadata.from_dict( + d.get("assa_abloy_vostio_metadata") + ) + if d.get("assa_abloy_vostio_metadata") is not None + else None + ), + avigilon_alta_metadata=( + AcsEntranceAvigilonAltaMetadata.from_dict( + d.get("avigilon_alta_metadata") + ) + if d.get("avigilon_alta_metadata") is not None + else None + ), + brivo_metadata=( + AcsEntranceBrivoMetadata.from_dict(d.get("brivo_metadata")) + if d.get("brivo_metadata") is not None + else None ), - avigilon_alta_metadata=DeepAttrDict(d.get("avigilon_alta_metadata", None)), - brivo_metadata=DeepAttrDict(d.get("brivo_metadata", None)), can_belong_to_reservation=d.get("can_belong_to_reservation", None), can_unlock_with_card=d.get("can_unlock_with_card", None), can_unlock_with_cloud_key=d.get("can_unlock_with_cloud_key", None), @@ -105,19 +517,49 @@ def from_dict(d: Dict[str, Any]): connected_account_id=d.get("connected_account_id", None), created_at=d.get("created_at", None), display_name=d.get("display_name", None), - dormakaba_ambiance_metadata=DeepAttrDict( - d.get("dormakaba_ambiance_metadata", None) + dormakaba_ambiance_metadata=( + AcsEntranceDormakabaAmbianceMetadata.from_dict( + d.get("dormakaba_ambiance_metadata") + ) + if d.get("dormakaba_ambiance_metadata") is not None + else None ), - dormakaba_community_metadata=DeepAttrDict( - d.get("dormakaba_community_metadata", None) + dormakaba_community_metadata=( + AcsEntranceDormakabaCommunityMetadata.from_dict( + d.get("dormakaba_community_metadata") + ) + if d.get("dormakaba_community_metadata") is not None + else None + ), + errors=[AcsEntranceErrors.from_dict(i) for i in d.get("errors") or []], + hotek_metadata=( + AcsEntranceHotekMetadata.from_dict(d.get("hotek_metadata")) + if d.get("hotek_metadata") is not None + else None ), - errors=d.get("errors", None), - hotek_metadata=DeepAttrDict(d.get("hotek_metadata", None)), is_locked=d.get("is_locked", None), - latch_metadata=DeepAttrDict(d.get("latch_metadata", None)), - salto_ks_metadata=DeepAttrDict(d.get("salto_ks_metadata", None)), - salto_space_metadata=DeepAttrDict(d.get("salto_space_metadata", None)), + latch_metadata=( + AcsEntranceLatchMetadata.from_dict(d.get("latch_metadata")) + if d.get("latch_metadata") is not None + else None + ), + salto_ks_metadata=( + AcsEntranceSaltoKsMetadata.from_dict(d.get("salto_ks_metadata")) + if d.get("salto_ks_metadata") is not None + else None + ), + salto_space_metadata=( + AcsEntranceSaltoSpaceMetadata.from_dict(d.get("salto_space_metadata")) + if d.get("salto_space_metadata") is not None + else None + ), space_ids=d.get("space_ids", None), - visionline_metadata=DeepAttrDict(d.get("visionline_metadata", None)), - warnings=d.get("warnings", None), + visionline_metadata=( + AcsEntranceVisionlineMetadata.from_dict(d.get("visionline_metadata")) + if d.get("visionline_metadata") is not None + else None + ), + warnings=[ + AcsEntranceWarnings.from_dict(i) for i in d.get("warnings") or [] + ], ) diff --git a/seam/resources/acs_system.py b/seam/resources/acs_system.py index 6b9d6b36..06ae3b15 100644 --- a/seam/resources/acs_system.py +++ b/seam/resources/acs_system.py @@ -1,6 +1,104 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AcsSystemErrors(ResourceMapping): + """Errors associated with the `access control system `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar is_bridge_error: Indicates whether the error is related to the `Seam Bridge `_. + """ + + created_at: str + error_code: str + message: str + is_bridge_error: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + is_bridge_error=d.get("is_bridge_error", None), + ) + + +@dataclass +class AcsSystemLocation(ResourceMapping): + """Location information for the `access control system `_. + + :ivar time_zone: Time zone in which the `access control system `_ is located. + """ + + time_zone: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + time_zone=d.get("time_zone", None), + ) + + +@dataclass +class AcsSystemVisionlineMetadata(ResourceMapping): + """Visionline-specific metadata for the `access control system `_. + + :ivar lan_address: IP address or hostname of the main Visionline server relative to `Seam Bridge `_ on the local network. + + :ivar mobile_access_uuid: Keyset loaded into a reader. Mobile keys and reader administration tools securely authenticate only with readers programmed with a matching keyset. + + :ivar system_id: Unique ID assigned by the ASSA ABLOY licensing team that identifies each hotel in your credential manager. + """ + + lan_address: str + mobile_access_uuid: str + system_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + lan_address=d.get("lan_address", None), + mobile_access_uuid=d.get("mobile_access_uuid", None), + system_id=d.get("system_id", None), + ) + + +@dataclass +class AcsSystemWarnings(ResourceMapping): + """Warnings associated with the `access control system `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar misconfigured_acs_entrance_ids: Deprecated: this field is deprecated.""" + + created_at: str + message: str + warning_code: str + misconfigured_acs_entrance_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + misconfigured_acs_entrance_ids=d.get( + "misconfigured_acs_entrance_ids", None + ), + ) @dataclass @@ -59,23 +157,23 @@ class AcsSystem: connected_account_ids: List[str] created_at: str default_credential_manager_acs_system_id: str - errors: List[Dict[str, Any]] + errors: List[AcsSystemErrors] external_type: str external_type_display_name: str image_alt_text: str image_url: str is_credential_manager: bool - location: Dict[str, Any] + location: AcsSystemLocation name: str system_type: str system_type_display_name: str - visionline_metadata: Dict[str, Any] - warnings: List[Dict[str, Any]] + visionline_metadata: AcsSystemVisionlineMetadata + warnings: List[AcsSystemWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return AcsSystem( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( acs_access_group_count=d.get("acs_access_group_count", None), acs_system_id=d.get("acs_system_id", None), acs_user_count=d.get("acs_user_count", None), @@ -85,17 +183,25 @@ def from_dict(d: Dict[str, Any]): default_credential_manager_acs_system_id=d.get( "default_credential_manager_acs_system_id", None ), - errors=d.get("errors", None), + errors=[AcsSystemErrors.from_dict(i) for i in d.get("errors") or []], external_type=d.get("external_type", None), external_type_display_name=d.get("external_type_display_name", None), image_alt_text=d.get("image_alt_text", None), image_url=d.get("image_url", None), is_credential_manager=d.get("is_credential_manager", None), - location=DeepAttrDict(d.get("location", None)), + location=( + AcsSystemLocation.from_dict(d.get("location")) + if d.get("location") is not None + else None + ), name=d.get("name", None), system_type=d.get("system_type", None), system_type_display_name=d.get("system_type_display_name", None), - visionline_metadata=DeepAttrDict(d.get("visionline_metadata", None)), - warnings=d.get("warnings", None), + visionline_metadata=( + AcsSystemVisionlineMetadata.from_dict(d.get("visionline_metadata")) + if d.get("visionline_metadata") is not None + else None + ), + warnings=[AcsSystemWarnings.from_dict(i) for i in d.get("warnings") or []], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/acs_user.py b/seam/resources/acs_user.py index 6be4dac6..8f2004ee 100644 --- a/seam/resources/acs_user.py +++ b/seam/resources/acs_user.py @@ -1,6 +1,203 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class AcsUserAccessSchedule(ResourceMapping): + """``starts_at`` and ``ends_at`` timestamps for the `access system user's `_ access. + + :ivar ends_at: Date and time at which the user's access ends, in `ISO 8601 `_ format. + + :ivar starts_at: Date and time at which the user's access starts, in `ISO 8601 `_ format. + """ + + ends_at: str + starts_at: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + ends_at=d.get("ends_at", None), + starts_at=d.get("starts_at", None), + ) + + +@dataclass +class AcsUserErrors(ResourceMapping): + """Errors associated with the `access system user `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class AcsUserFrom(ResourceMapping): + """Old access system user information. + + :ivar email_address: Email address of the access system user. + + :ivar full_name: Full name of the access system user. + + :ivar phone_number: Phone number of the access system user.""" + + email_address: str + full_name: str + phone_number: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + email_address=d.get("email_address", None), + full_name=d.get("full_name", None), + phone_number=d.get("phone_number", None), + ) + + +@dataclass +class AcsUserTo(ResourceMapping): + """New access system user information. + + :ivar email_address: Email address of the access system user. + + :ivar full_name: Full name of the access system user. + + :ivar phone_number: Phone number of the access system user.""" + + email_address: str + full_name: str + phone_number: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + email_address=d.get("email_address", None), + full_name=d.get("full_name", None), + phone_number=d.get("phone_number", None), + ) + + +@dataclass +class AcsUserPendingMutations(ResourceMapping): + """Pending mutations associated with the `access system user `_. Seam is in the process of pushing these mutations to the integrated access system. + + :ivar created_at: Date and time at which the mutation was created. + + :ivar message: Detailed description of the mutation. + + :ivar mutation_code: Mutation code to indicate that Seam is in the process of pushing a user creation to the integrated access system. + + :ivar scheduled_at: Optional: When the user creation is scheduled to occur. + + :ivar from_: Old access system user information. + + :ivar to: New access system user information. + + :ivar acs_access_group_id: ID of the access group involved in the scheduled change. + + :ivar variant: Whether the user is scheduled to be added to or removed from the access group. + """ + + created_at: str + message: str + mutation_code: str + scheduled_at: str + from_: AcsUserFrom + to: AcsUserTo + acs_access_group_id: str + variant: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + mutation_code=d.get("mutation_code", None), + scheduled_at=d.get("scheduled_at", None), + from_=( + AcsUserFrom.from_dict(d.get("from")) + if d.get("from") is not None + else None + ), + to=AcsUserTo.from_dict(d.get("to")) if d.get("to") is not None else None, + acs_access_group_id=d.get("acs_access_group_id", None), + variant=d.get("variant", None), + ) + + +@dataclass +class AcsUserSaltoKsMetadata(ResourceMapping): + """Salto KS-specific metadata associated with the `access system user `_. + + :ivar is_subscribed: Indicates whether the user holds an active subscription slot on the Salto KS site. Only subscribed users can unlock doors and count against the site's user-subscription limit. A user may not be subscribed because their access schedule has not started or has ended, the site has reached its subscription limit, or they were manually unsubscribed. This is distinct from ``is_suspended``, which reflects whether the user has been explicitly blocked. + """ + + is_subscribed: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + is_subscribed=d.get("is_subscribed", None), + ) + + +@dataclass +class AcsUserSaltoSpaceMetadata(ResourceMapping): + """Salto Space-specific metadata associated with the `access system user `_. + + :ivar audit_openings: Indicates whether AuditOpenings is enabled for the user in the Salto Space access system. + + :ivar user_id: User ID in the Salto Space access system.""" + + audit_openings: bool + user_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + audit_openings=d.get("audit_openings", None), + user_id=d.get("user_id", None), + ) + + +@dataclass +class AcsUserWarnings(ResourceMapping): + """Warnings associated with the `access system user `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code:""" + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) @dataclass @@ -62,7 +259,7 @@ class AcsUser: :ivar workspace_id: ID of the workspace that contains the `access system user `_. """ - access_schedule: Dict[str, Any] + access_schedule: AcsUserAccessSchedule acs_system_id: str acs_user_id: str connected_account_id: str @@ -70,28 +267,32 @@ class AcsUser: display_name: str email: str email_address: str - errors: List[Dict[str, Any]] + errors: List[AcsUserErrors] external_type: str external_type_display_name: str full_name: str hid_acs_system_id: str is_managed: bool is_suspended: bool - pending_mutations: List[Dict[str, Any]] + pending_mutations: List[AcsUserPendingMutations] phone_number: str - salto_ks_metadata: Dict[str, Any] - salto_space_metadata: Dict[str, Any] + salto_ks_metadata: AcsUserSaltoKsMetadata + salto_space_metadata: AcsUserSaltoSpaceMetadata user_identity_email_address: str user_identity_full_name: str user_identity_id: str user_identity_phone_number: str - warnings: List[Dict[str, Any]] + warnings: List[AcsUserWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return AcsUser( - access_schedule=DeepAttrDict(d.get("access_schedule", None)), + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + access_schedule=( + AcsUserAccessSchedule.from_dict(d.get("access_schedule")) + if d.get("access_schedule") is not None + else None + ), acs_system_id=d.get("acs_system_id", None), acs_user_id=d.get("acs_user_id", None), connected_account_id=d.get("connected_account_id", None), @@ -99,21 +300,32 @@ def from_dict(d: Dict[str, Any]): display_name=d.get("display_name", None), email=d.get("email", None), email_address=d.get("email_address", None), - errors=d.get("errors", None), + errors=[AcsUserErrors.from_dict(i) for i in d.get("errors") or []], external_type=d.get("external_type", None), external_type_display_name=d.get("external_type_display_name", None), full_name=d.get("full_name", None), hid_acs_system_id=d.get("hid_acs_system_id", None), is_managed=d.get("is_managed", None), is_suspended=d.get("is_suspended", None), - pending_mutations=d.get("pending_mutations", None), + pending_mutations=[ + AcsUserPendingMutations.from_dict(i) + for i in d.get("pending_mutations") or [] + ], phone_number=d.get("phone_number", None), - salto_ks_metadata=DeepAttrDict(d.get("salto_ks_metadata", None)), - salto_space_metadata=DeepAttrDict(d.get("salto_space_metadata", None)), + salto_ks_metadata=( + AcsUserSaltoKsMetadata.from_dict(d.get("salto_ks_metadata")) + if d.get("salto_ks_metadata") is not None + else None + ), + salto_space_metadata=( + AcsUserSaltoSpaceMetadata.from_dict(d.get("salto_space_metadata")) + if d.get("salto_space_metadata") is not None + else None + ), user_identity_email_address=d.get("user_identity_email_address", None), user_identity_full_name=d.get("user_identity_full_name", None), user_identity_id=d.get("user_identity_id", None), user_identity_phone_number=d.get("user_identity_phone_number", None), - warnings=d.get("warnings", None), + warnings=[AcsUserWarnings.from_dict(i) for i in d.get("warnings") or []], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/action_attempt.py b/seam/resources/action_attempt.py index 0dd38a6a..3e9d8531 100644 --- a/seam/resources/action_attempt.py +++ b/seam/resources/action_attempt.py @@ -1,6 +1,42 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class ActionAttemptError(ResourceMapping): + """Error associated with the action. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar type: Type of the error.""" + + message: str + type: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + message=d.get("message", None), + type=d.get("type", None), + ) + + +@dataclass +class ActionAttemptResult(ResourceMapping): + """Result of the action. + + :ivar was_confirmed_by_device: Indicates whether the device confirmed that the lock action occurred. + """ + + was_confirmed_by_device: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + was_confirmed_by_device=d.get("was_confirmed_by_device", None), + ) @dataclass @@ -19,16 +55,24 @@ class ActionAttempt: action_attempt_id: str action_type: str - error: Dict[str, Any] - result: Dict[str, Any] + error: ActionAttemptError + result: ActionAttemptResult status: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return ActionAttempt( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( action_attempt_id=d.get("action_attempt_id", None), action_type=d.get("action_type", None), - error=DeepAttrDict(d.get("error", None)), - result=DeepAttrDict(d.get("result", None)), + error=( + ActionAttemptError.from_dict(d.get("error")) + if d.get("error") is not None + else None + ), + result=( + ActionAttemptResult.from_dict(d.get("result")) + if d.get("result") is not None + else None + ), status=d.get("status", None), ) diff --git a/seam/resources/batch.py b/seam/resources/batch.py index 23216596..1313eb3e 100644 --- a/seam/resources/batch.py +++ b/seam/resources/batch.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping @dataclass @@ -158,9 +159,9 @@ class Batch: user_identities: List[Dict[str, Any]] workspaces: List[Dict[str, Any]] - @staticmethod - def from_dict(d: Dict[str, Any]): - return Batch( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_codes=d.get("access_codes", None), access_grants=d.get("access_grants", None), access_methods=d.get("access_methods", None), diff --git a/seam/resources/client_session.py b/seam/resources/client_session.py index a23ebcd0..fb289c16 100644 --- a/seam/resources/client_session.py +++ b/seam/resources/client_session.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping @dataclass @@ -52,9 +53,9 @@ class ClientSession: user_identity_ids: List[str] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return ClientSession( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( client_session_id=d.get("client_session_id", None), connect_webview_ids=d.get("connect_webview_ids", None), connected_account_ids=d.get("connected_account_ids", None), diff --git a/seam/resources/connect_webview.py b/seam/resources/connect_webview.py index bb436eb9..fc948390 100644 --- a/seam/resources/connect_webview.py +++ b/seam/resources/connect_webview.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping @dataclass @@ -75,9 +76,9 @@ class ConnectWebview: wait_for_device_creation: bool workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return ConnectWebview( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( accepted_capabilities=d.get("accepted_capabilities", None), accepted_providers=d.get("accepted_providers", None), any_provider_allowed=d.get("any_provider_allowed", None), diff --git a/seam/resources/connected_account.py b/seam/resources/connected_account.py index 2a0e775e..aa65103c 100644 --- a/seam/resources/connected_account.py +++ b/seam/resources/connected_account.py @@ -1,6 +1,154 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class ConnectedAccountSites(ResourceMapping): + """Salto sites associated with the connected account that has an error. + + :ivar site_id: ID of a Salto site associated with the connected account that has an error. + + :ivar site_name: Name of a Salto site associated with the connected account that has an error. + + :ivar site_user_subscription_limit: Subscription limit of site users for a Salto site associated with the connected account that has an error. + + :ivar subscribed_site_user_count: Count of subscribed site users for a Salto site associated with the connected account that has an error. + """ + + site_id: str + site_name: str + site_user_subscription_limit: int + subscribed_site_user_count: int + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + site_id=d.get("site_id", None), + site_name=d.get("site_name", None), + site_user_subscription_limit=d.get("site_user_subscription_limit", None), + subscribed_site_user_count=d.get("subscribed_site_user_count", None), + ) + + +@dataclass +class ConnectedAccountSaltoKsMetadata(ResourceMapping): + """Salto KS metadata associated with the connected account that has an error. + + :ivar sites: Salto sites associated with the connected account that has an error.""" + + sites: List[ConnectedAccountSites] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + sites=[ConnectedAccountSites.from_dict(i) for i in d.get("sites") or []], + ) + + +@dataclass +class ConnectedAccountErrors(ResourceMapping): + """Errors associated with the connected account. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar is_bridge_error: Indicates whether the error is related to `Seam Bridge `_. + + :ivar is_connected_account_error: Indicates whether the error is related specifically to the connected account. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar salto_ks_metadata: Salto KS metadata associated with the connected account that has an error. + """ + + created_at: str + error_code: str + is_bridge_error: bool + is_connected_account_error: bool + message: str + salto_ks_metadata: ConnectedAccountSaltoKsMetadata + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + is_bridge_error=d.get("is_bridge_error", None), + is_connected_account_error=d.get("is_connected_account_error", None), + message=d.get("message", None), + salto_ks_metadata=( + ConnectedAccountSaltoKsMetadata.from_dict(d.get("salto_ks_metadata")) + if d.get("salto_ks_metadata") is not None + else None + ), + ) + + +@dataclass +class ConnectedAccountUserIdentifier(ResourceMapping): + """User identifier associated with the connected account. + + :ivar api_url: API URL for the user identifier associated with the connected account. + + :ivar email: Email address of the user identifier associated with the connected account. + + :ivar exclusive: Indicates whether the user identifier associated with the connected account is exclusive. + + :ivar phone: Phone number of the user identifier associated with the connected account. + + :ivar username: Username of the user identifier associated with the connected account. + """ + + api_url: str + email: str + exclusive: bool + phone: str + username: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + api_url=d.get("api_url", None), + email=d.get("email", None), + exclusive=d.get("exclusive", None), + phone=d.get("phone", None), + username=d.get("username", None), + ) + + +@dataclass +class ConnectedAccountWarnings(ResourceMapping): + """Warnings associated with the connected account. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar salto_ks_metadata: Salto KS metadata associated with the connected account that has a warning. + """ + + created_at: str + message: str + warning_code: str + salto_ks_metadata: ConnectedAccountSaltoKsMetadata + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + salto_ks_metadata=( + ConnectedAccountSaltoKsMetadata.from_dict(d.get("salto_ks_metadata")) + if d.get("salto_ks_metadata") is not None + else None + ), + ) @dataclass @@ -54,17 +202,17 @@ class ConnectedAccount: default_checkin_time: str default_checkout_time: str display_name: str - errors: List[Dict[str, Any]] + errors: List[ConnectedAccountErrors] ical_feed_origin: str ical_url: str image_url: str time_zone: str - user_identifier: Dict[str, Any] - warnings: List[Dict[str, Any]] + user_identifier: ConnectedAccountUserIdentifier + warnings: List[ConnectedAccountWarnings] - @staticmethod - def from_dict(d: Dict[str, Any]): - return ConnectedAccount( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( accepted_capabilities=d.get("accepted_capabilities", None), account_type=d.get("account_type", None), account_type_display_name=d.get("account_type_display_name", None), @@ -78,11 +226,17 @@ def from_dict(d: Dict[str, Any]): default_checkin_time=d.get("default_checkin_time", None), default_checkout_time=d.get("default_checkout_time", None), display_name=d.get("display_name", None), - errors=d.get("errors", None), + errors=[ConnectedAccountErrors.from_dict(i) for i in d.get("errors") or []], ical_feed_origin=d.get("ical_feed_origin", None), ical_url=d.get("ical_url", None), image_url=d.get("image_url", None), time_zone=d.get("time_zone", None), - user_identifier=DeepAttrDict(d.get("user_identifier", None)), - warnings=d.get("warnings", None), + user_identifier=( + ConnectedAccountUserIdentifier.from_dict(d.get("user_identifier")) + if d.get("user_identifier") is not None + else None + ), + warnings=[ + ConnectedAccountWarnings.from_dict(i) for i in d.get("warnings") or [] + ], ) diff --git a/seam/resources/customer_portal.py b/seam/resources/customer_portal.py index 3a625060..120cbc7a 100644 --- a/seam/resources/customer_portal.py +++ b/seam/resources/customer_portal.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping @dataclass @@ -27,9 +28,9 @@ class CustomerPortal: url: str workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return CustomerPortal( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( created_at=d.get("created_at", None), customer_key=d.get("customer_key", None), expires_at=d.get("expires_at", None), diff --git a/seam/resources/device.py b/seam/resources/device.py index 7813fd17..d87e48bf 100644 --- a/seam/resources/device.py +++ b/seam/resources/device.py @@ -1,6 +1,3002 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class DeviceDeviceManufacturer(ResourceMapping): + """Manufacturer of the device. Represents the hardware brand, which may differ from the provider. + + :ivar display_name: Display name for the manufacturer, such as ``August``, ``Yale``, ``Salto``, and so on. + + :ivar image_url: Image URL for the manufacturer logo. + + :ivar manufacturer: Manufacturer identifier, such as ``august``, ``yale``, ``salto``, and so on. + """ + + display_name: str + image_url: str + manufacturer: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + display_name=d.get("display_name", None), + image_url=d.get("image_url", None), + manufacturer=d.get("manufacturer", None), + ) + + +@dataclass +class DeviceDeviceProvider(ResourceMapping): + """Provider of the device. Represents the third-party service through which the device is controlled. + + :ivar device_provider_name: Device provider name. Corresponds to the integration type, such as ``august``, ``schlage``, ``yale_access``, and so on. + + :ivar display_name: Display name for the device provider type. + + :ivar image_url: Image URL for the device provider. + + :ivar provider_category: Provider category. Indicates the third-party provider type, such as ``stable``, for stable integrations, or ``internal``, for internal integrations. + """ + + device_provider_name: str + display_name: str + image_url: str + provider_category: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_provider_name=d.get("device_provider_name", None), + display_name=d.get("display_name", None), + image_url=d.get("image_url", None), + provider_category=d.get("provider_category", None), + ) + + +@dataclass +class DeviceErrors(ResourceMapping): + """Array of errors associated with the device. Each error object within the array contains two fields: ``error_code`` and ``message``. ``error_code`` is a string that uniquely identifies the type of error, enabling quick recognition and categorization of the issue. ``message`` provides a more detailed description of the error, offering insights into the issue and potentially how to rectify it. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar is_connected_account_error: Indicates that the error is a `connected account `_ error. + + :ivar is_device_error: Indicates that the error is not a device error. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar is_bridge_error: Indicates whether the error is related to `Seam Bridge `_. + """ + + created_at: str + error_code: str + is_connected_account_error: bool + is_device_error: bool + message: str + is_bridge_error: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + is_connected_account_error=d.get("is_connected_account_error", None), + is_device_error=d.get("is_device_error", None), + message=d.get("message", None), + is_bridge_error=d.get("is_bridge_error", None), + ) + + +@dataclass +class DeviceLocation(ResourceMapping): + """Location information for the device. + + :ivar location_name: Name of the device location. + + :ivar time_zone: Time zone of the device location. + + :ivar timezone: Deprecated: Use ``time_zone`` instead. Time zone of the device location. + """ + + location_name: str + time_zone: str + timezone: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + location_name=d.get("location_name", None), + time_zone=d.get("time_zone", None), + timezone=d.get("timezone", None), + ) + + +@dataclass +class DeviceBattery(ResourceMapping): + """Keypad battery properties. + + :ivar level:""" + + level: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + level=d.get("level", None), + ) + + +@dataclass +class DeviceAccessoryKeypad(ResourceMapping): + """Accessory keypad properties and state. + + :ivar battery: Keypad battery properties. + + :ivar is_connected: Indicates if an accessory keypad is connected to the device.""" + + battery: DeviceBattery + is_connected: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + battery=( + DeviceBattery.from_dict(d.get("battery")) + if d.get("battery") is not None + else None + ), + is_connected=d.get("is_connected", None), + ) + + +@dataclass +class DeviceAppearance(ResourceMapping): + """Appearance-related properties, as reported by the device. + + :ivar name: Name of the device as seen from the provider API and application, not settable through Seam. + """ + + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + name=d.get("name", None), + ) + + +@dataclass +class DeviceModel(ResourceMapping): + """Device model-related properties. + + :ivar accessory_keypad_supported: Deprecated: use device.properties.model.can_connect_accessory_keypad + + :ivar can_connect_accessory_keypad: Indicates whether the device can connect a accessory keypad. + + :ivar display_name: Display name of the device model. + + :ivar has_built_in_keypad: Indicates whether the device has a built in accessory keypad. + + :ivar manufacturer_display_name: Display name that corresponds to the manufacturer-specific terminology for the device. + + :ivar offline_access_codes_supported: Deprecated: use device.can_program_offline_access_codes. + + :ivar online_access_codes_supported: Deprecated: use device.can_program_online_access_codes. + """ + + accessory_keypad_supported: bool + can_connect_accessory_keypad: bool + display_name: str + has_built_in_keypad: bool + manufacturer_display_name: str + offline_access_codes_supported: bool + online_access_codes_supported: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + accessory_keypad_supported=d.get("accessory_keypad_supported", None), + can_connect_accessory_keypad=d.get("can_connect_accessory_keypad", None), + display_name=d.get("display_name", None), + has_built_in_keypad=d.get("has_built_in_keypad", None), + manufacturer_display_name=d.get("manufacturer_display_name", None), + offline_access_codes_supported=d.get( + "offline_access_codes_supported", None + ), + online_access_codes_supported=d.get("online_access_codes_supported", None), + ) + + +@dataclass +class DeviceEndpoints(ResourceMapping): + """Endpoints associated with the phone. + + :ivar endpoint_id: ID of the associated endpoint. + + :ivar is_active: Indicated whether the endpoint is active.""" + + endpoint_id: str + is_active: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + endpoint_id=d.get("endpoint_id", None), + is_active=d.get("is_active", None), + ) + + +@dataclass +class DeviceAssaAbloyCredentialServiceMetadata(ResourceMapping): + """ASSA ABLOY Credential Service metadata for the phone. + + :ivar endpoints: Endpoints associated with the phone. + + :ivar has_active_endpoint: Indicates whether the credential service has active endpoints associated with the phone. + """ + + endpoints: List[DeviceEndpoints] + has_active_endpoint: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + endpoints=[DeviceEndpoints.from_dict(i) for i in d.get("endpoints") or []], + has_active_endpoint=d.get("has_active_endpoint", None), + ) + + +@dataclass +class DeviceSaltoSpaceCredentialServiceMetadata(ResourceMapping): + """Salto Space credential service metadata for the phone. + + :ivar has_active_phone: Indicates whether the credential service has an active associated phone. + """ + + has_active_phone: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + has_active_phone=d.get("has_active_phone", None), + ) + + +@dataclass +class DeviceAkilesMetadata(ResourceMapping): + """Metadata for an Akiles device. + + :ivar _member_group_id: Group ID to which to add users for an Akiles device. + + :ivar gadget_id: Gadget ID for an Akiles device. + + :ivar gadget_name: Gadget name for an Akiles device. + + :ivar product_name: Product name for an Akiles device.""" + + _member_group_id: str + gadget_id: str + gadget_name: str + product_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + _member_group_id=d.get("_member_group_id", None), + gadget_id=d.get("gadget_id", None), + gadget_name=d.get("gadget_name", None), + product_name=d.get("product_name", None), + ) + + +@dataclass +class DeviceAqaraMetadata(ResourceMapping): + """Metadata for an Aqara device. + + :ivar device_name: Device name for an Aqara device. + + :ivar did: Device ID (did) for an Aqara device. + + :ivar firmware_version: Firmware version for an Aqara device. + + :ivar model: Model identifier for an Aqara device. + + :ivar model_type: Model type for an Aqara device. + + :ivar parent_did: Parent gateway device ID for an Aqara device. + + :ivar position_id: Position (room) ID for an Aqara device. + + :ivar time_zone: Time zone reported for an Aqara device (e.g. GMT-07:00).""" + + device_name: str + did: str + firmware_version: str + model: str + model_type: float + parent_did: str + position_id: str + time_zone: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_name=d.get("device_name", None), + did=d.get("did", None), + firmware_version=d.get("firmware_version", None), + model=d.get("model", None), + model_type=d.get("model_type", None), + parent_did=d.get("parent_did", None), + position_id=d.get("position_id", None), + time_zone=d.get("time_zone", None), + ) + + +@dataclass +class DeviceAssaAbloyVostioMetadata(ResourceMapping): + """Metadata for an ASSA ABLOY Vostio system. + + :ivar encoder_name: Encoder name for an ASSA ABLOY Vostio system.""" + + encoder_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + encoder_name=d.get("encoder_name", None), + ) + + +@dataclass +class DeviceAugustMetadata(ResourceMapping): + """Metadata for an August device. + + :ivar has_keypad: Indicates whether an August device has a keypad. + + :ivar house_id: House ID for an August device. + + :ivar house_name: House name for an August device. + + :ivar keypad_battery_level: Keypad battery level for an August device. + + :ivar lock_id: Lock ID for an August device. + + :ivar lock_name: Lock name for an August device. + + :ivar model: Model for an August device.""" + + has_keypad: bool + house_id: str + house_name: str + keypad_battery_level: str + lock_id: str + lock_name: str + model: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + has_keypad=d.get("has_keypad", None), + house_id=d.get("house_id", None), + house_name=d.get("house_name", None), + keypad_battery_level=d.get("keypad_battery_level", None), + lock_id=d.get("lock_id", None), + lock_name=d.get("lock_name", None), + model=d.get("model", None), + ) + + +@dataclass +class DeviceAvigilonAltaMetadata(ResourceMapping): + """Metadata for an Avigilon Alta system. + + :ivar entry_name: Entry name for an Avigilon Alta system. + + :ivar entry_relays_total_count: Total count of entry relays for an Avigilon Alta system. + + :ivar org_name: Organization name for an Avigilon Alta system. + + :ivar site_id: Site ID for an Avigilon Alta system. + + :ivar site_name: Site name for an Avigilon Alta system. + + :ivar zone_id: Zone ID for an Avigilon Alta system. + + :ivar zone_name: Zone name for an Avigilon Alta system.""" + + entry_name: str + entry_relays_total_count: float + org_name: str + site_id: float + site_name: str + zone_id: float + zone_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + entry_name=d.get("entry_name", None), + entry_relays_total_count=d.get("entry_relays_total_count", None), + org_name=d.get("org_name", None), + site_id=d.get("site_id", None), + site_name=d.get("site_name", None), + zone_id=d.get("zone_id", None), + zone_name=d.get("zone_name", None), + ) + + +@dataclass +class DeviceBrivoMetadata(ResourceMapping): + """Metadata for a Brivo device. + + :ivar activation_enabled: Indicates whether the Brivo access point has activation (remote unlock) enabled. + + :ivar device_name: Device name for a Brivo device.""" + + activation_enabled: bool + device_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + activation_enabled=d.get("activation_enabled", None), + device_name=d.get("device_name", None), + ) + + +@dataclass +class DeviceControlbywebMetadata(ResourceMapping): + """Metadata for a ControlByWeb device. + + :ivar device_id: Device ID for a ControlByWeb device. + + :ivar device_name: Device name for a ControlByWeb device. + + :ivar relay_name: Relay name for a ControlByWeb device.""" + + device_id: str + device_name: str + relay_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + relay_name=d.get("relay_name", None), + ) + + +@dataclass +class DeviceDeviceId(ResourceMapping): + """Device ID for a dormakaba Oracode device.""" + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls() + + +@dataclass +class DevicePredefinedTimeSlots(ResourceMapping): + """Predefined time slots for a dormakaba Oracode device. + + :ivar check_in_time: Check in time for a time slot for a dormakaba Oracode device. + + :ivar check_out_time: Checkout time for a time slot for a dormakaba Oracode device. + + :ivar dormakaba_oracode_user_level_id: ID of a user level for a dormakaba Oracode device. + + :ivar dormakaba_oracode_user_level_prefix: Prefix for a user level for a dormakaba Oracode device. + + :ivar is_24_hour: Indicates whether a time slot for a dormakaba Oracode device is a 24-hour time slot. + + :ivar is_biweekly_mode: Indicates whether a time slot for a dormakaba Oracode device is in biweekly mode. + + :ivar is_master: Indicates whether a time slot for a dormakaba Oracode device is a master time slot. + + :ivar is_one_shot: Indicates whether a time slot for a dormakaba Oracode device is a one-shot time slot. + + :ivar name: Name of a time slot for a dormakaba Oracode device. + + :ivar prefix: Prefix for a time slot for a dormakaba Oracode device.""" + + check_in_time: str + check_out_time: str + dormakaba_oracode_user_level_id: str + dormakaba_oracode_user_level_prefix: float + is_24_hour: bool + is_biweekly_mode: bool + is_master: bool + is_one_shot: bool + name: str + prefix: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + check_in_time=d.get("check_in_time", None), + check_out_time=d.get("check_out_time", None), + dormakaba_oracode_user_level_id=d.get( + "dormakaba_oracode_user_level_id", None + ), + dormakaba_oracode_user_level_prefix=d.get( + "dormakaba_oracode_user_level_prefix", None + ), + is_24_hour=d.get("is_24_hour", None), + is_biweekly_mode=d.get("is_biweekly_mode", None), + is_master=d.get("is_master", None), + is_one_shot=d.get("is_one_shot", None), + name=d.get("name", None), + prefix=d.get("prefix", None), + ) + + +@dataclass +class DeviceDormakabaOracodeMetadata(ResourceMapping): + """Metadata for a dormakaba Oracode device. + + :ivar device_id: Device ID for a dormakaba Oracode device. + + :ivar door_id: Door ID for a dormakaba Oracode device. + + :ivar door_is_wireless: Indicates whether a door is wireless for a dormakaba Oracode device. + + :ivar door_name: Door name for a dormakaba Oracode device. + + :ivar iana_timezone: IANA time zone for a dormakaba Oracode device. + + :ivar predefined_time_slots: Predefined time slots for a dormakaba Oracode device. + + :ivar site_id: Deprecated: Previously marked as "@DEPRECATED." Site ID for a dormakaba Oracode device. + + :ivar site_name: Site name for a dormakaba Oracode device.""" + + device_id: DeviceDeviceId + door_id: float + door_is_wireless: bool + door_name: str + iana_timezone: str + predefined_time_slots: List[DevicePredefinedTimeSlots] + site_id: float + site_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=( + DeviceDeviceId.from_dict(d.get("device_id")) + if d.get("device_id") is not None + else None + ), + door_id=d.get("door_id", None), + door_is_wireless=d.get("door_is_wireless", None), + door_name=d.get("door_name", None), + iana_timezone=d.get("iana_timezone", None), + predefined_time_slots=[ + DevicePredefinedTimeSlots.from_dict(i) + for i in d.get("predefined_time_slots") or [] + ], + site_id=d.get("site_id", None), + site_name=d.get("site_name", None), + ) + + +@dataclass +class DeviceEcobeeMetadata(ResourceMapping): + """Metadata for an ecobee device. + + :ivar device_name: Device name for an ecobee device. + + :ivar ecobee_device_id: Device ID for an ecobee device.""" + + device_name: str + ecobee_device_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_name=d.get("device_name", None), + ecobee_device_id=d.get("ecobee_device_id", None), + ) + + +@dataclass +class DeviceFourSuitesMetadata(ResourceMapping): + """Metadata for a 4SUITES device. + + :ivar device_id: Device ID for a 4SUITES device. + + :ivar device_name: Device name for a 4SUITES device. + + :ivar reclose_delay_in_seconds: Reclose delay, in seconds, for a 4SUITES device.""" + + device_id: float + device_name: str + reclose_delay_in_seconds: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + reclose_delay_in_seconds=d.get("reclose_delay_in_seconds", None), + ) + + +@dataclass +class DeviceGenieMetadata(ResourceMapping): + """Metadata for a Genie device. + + :ivar device_name: Lock name for a Genie device. + + :ivar door_name: Door name for a Genie device.""" + + device_name: str + door_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_name=d.get("device_name", None), + door_name=d.get("door_name", None), + ) + + +@dataclass +class DeviceHoneywellResideoMetadata(ResourceMapping): + """Metadata for a Honeywell Resideo device. + + :ivar device_name: Device name for a Honeywell Resideo device. + + :ivar honeywell_resideo_device_id: Device ID for a Honeywell Resideo device.""" + + device_name: str + honeywell_resideo_device_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_name=d.get("device_name", None), + honeywell_resideo_device_id=d.get("honeywell_resideo_device_id", None), + ) + + +@dataclass +class DeviceIglooMetadata(ResourceMapping): + """Metadata for an igloo device. + + :ivar bridge_id: Bridge ID for an igloo device. + + :ivar device_id: Device ID for an igloo device. + + :ivar model: Model for an igloo device.""" + + bridge_id: str + device_id: str + model: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + bridge_id=d.get("bridge_id", None), + device_id=d.get("device_id", None), + model=d.get("model", None), + ) + + +@dataclass +class DeviceIgloohomeMetadata(ResourceMapping): + """Metadata for an igloohome device. + + :ivar bridge_id: Bridge ID for an igloohome device. + + :ivar bridge_name: Bridge name for an igloohome device. + + :ivar device_id: Device ID for an igloohome device. + + :ivar device_name: Device name for an igloohome device. + + :ivar is_accessory_keypad_linked_to_bridge: Indicates whether a keypad is linked to a bridge for an igloohome device. + + :ivar keypad_id: Keypad ID for an igloohome device.""" + + bridge_id: str + bridge_name: str + device_id: str + device_name: str + is_accessory_keypad_linked_to_bridge: bool + keypad_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + bridge_id=d.get("bridge_id", None), + bridge_name=d.get("bridge_name", None), + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + is_accessory_keypad_linked_to_bridge=d.get( + "is_accessory_keypad_linked_to_bridge", None + ), + keypad_id=d.get("keypad_id", None), + ) + + +@dataclass +class DeviceKeynestMetadata(ResourceMapping): + """Metadata for a KeyNest device. + + :ivar address: Address for a KeyNest device. + + :ivar current_or_last_store_id: Current or last store ID for a KeyNest device. + + :ivar current_status: Current status for a KeyNest device. + + :ivar current_user_company: Current user company for a KeyNest device. + + :ivar current_user_email: Current user email for a KeyNest device. + + :ivar current_user_name: Current user name for a KeyNest device. + + :ivar current_user_phone_number: Current user phone number for a KeyNest device. + + :ivar default_office_id: Default office ID for a KeyNest device. + + :ivar device_name: Device name for a KeyNest device. + + :ivar fob_id: Fob ID for a KeyNest device. + + :ivar handover_method: Handover method for a KeyNest device. + + :ivar has_photo: Whether the KeyNest device has a photo. + + :ivar is_quadient_locker: Whether the key is in a locker that does not support the access codes API. + + :ivar key_id: Key ID for a KeyNest device. + + :ivar key_notes: Key notes for a KeyNest device. + + :ivar keynest_app_user: KeyNest app user for a KeyNest device. + + :ivar last_movement: Last movement timestamp for a KeyNest device. + + :ivar property_id: Property ID for a KeyNest device. + + :ivar property_postcode: Property postcode for a KeyNest device. + + :ivar status_type: Status type for a KeyNest device. + + :ivar subscription_plan: Subscription plan for a KeyNest device.""" + + address: str + current_or_last_store_id: float + current_status: str + current_user_company: str + current_user_email: str + current_user_name: str + current_user_phone_number: str + default_office_id: float + device_name: str + fob_id: float + handover_method: str + has_photo: bool + is_quadient_locker: bool + key_id: str + key_notes: str + keynest_app_user: str + last_movement: str + property_id: str + property_postcode: str + status_type: str + subscription_plan: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + address=d.get("address", None), + current_or_last_store_id=d.get("current_or_last_store_id", None), + current_status=d.get("current_status", None), + current_user_company=d.get("current_user_company", None), + current_user_email=d.get("current_user_email", None), + current_user_name=d.get("current_user_name", None), + current_user_phone_number=d.get("current_user_phone_number", None), + default_office_id=d.get("default_office_id", None), + device_name=d.get("device_name", None), + fob_id=d.get("fob_id", None), + handover_method=d.get("handover_method", None), + has_photo=d.get("has_photo", None), + is_quadient_locker=d.get("is_quadient_locker", None), + key_id=d.get("key_id", None), + key_notes=d.get("key_notes", None), + keynest_app_user=d.get("keynest_app_user", None), + last_movement=d.get("last_movement", None), + property_id=d.get("property_id", None), + property_postcode=d.get("property_postcode", None), + status_type=d.get("status_type", None), + subscription_plan=d.get("subscription_plan", None), + ) + + +@dataclass +class DeviceKisiMetadata(ResourceMapping): + """Metadata for a Kisi device. + + :ivar description: Description for a Kisi device. + + :ivar lock_id: Lock ID for a Kisi device. + + :ivar lock_name: Lock name for a Kisi device. + + :ivar place_name: Place name for a Kisi device.""" + + description: str + lock_id: float + lock_name: str + place_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + description=d.get("description", None), + lock_id=d.get("lock_id", None), + lock_name=d.get("lock_name", None), + place_name=d.get("place_name", None), + ) + + +@dataclass +class DeviceKorelockMetadata(ResourceMapping): + """Metadata for a Korelock device. + + :ivar device_id: Device ID for a Korelock device. + + :ivar device_name: Device name for a Korelock device. + + :ivar firmware_version: Firmware version for a Korelock device. + + :ivar location_id: Location ID for a Korelock device. Required for timebound access codes. + + :ivar model_code: Model code for a Korelock device. + + :ivar serial_number: Serial number for a Korelock device. + + :ivar wifi_signal_strength: WiFi signal strength (0-1) for a Korelock device.""" + + device_id: str + device_name: str + firmware_version: str + location_id: str + model_code: str + serial_number: str + wifi_signal_strength: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + firmware_version=d.get("firmware_version", None), + location_id=d.get("location_id", None), + model_code=d.get("model_code", None), + serial_number=d.get("serial_number", None), + wifi_signal_strength=d.get("wifi_signal_strength", None), + ) + + +@dataclass +class DeviceKwiksetMetadata(ResourceMapping): + """Metadata for a Kwikset device. + + :ivar device_id: Device ID for a Kwikset device. + + :ivar device_name: Device name for a Kwikset device. + + :ivar model_number: Model number for a Kwikset device.""" + + device_id: str + device_name: str + model_number: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + model_number=d.get("model_number", None), + ) + + +@dataclass +class DeviceLocklyMetadata(ResourceMapping): + """Metadata for a Lockly device. + + :ivar device_id: Device ID for a Lockly device. + + :ivar device_name: Device name for a Lockly device. + + :ivar model: Model for a Lockly device.""" + + device_id: str + device_name: str + model: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + model=d.get("model", None), + ) + + +@dataclass +class DeviceAccelerometerZ(ResourceMapping): + """Latest accelerometer Z-axis reading for a Minut device. + + :ivar time: Time of latest accelerometer Z-axis reading for a Minut device. + + :ivar value: Value of latest accelerometer Z-axis reading for a Minut device.""" + + time: str + value: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + time=d.get("time", None), + value=d.get("value", None), + ) + + +@dataclass +class DeviceHumidity(ResourceMapping): + """Latest humidity reading for a Minut device. + + :ivar time: Time of latest humidity reading for a Minut device. + + :ivar value: Value of latest humidity reading for a Minut device.""" + + time: str + value: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + time=d.get("time", None), + value=d.get("value", None), + ) + + +@dataclass +class DevicePressure(ResourceMapping): + """Latest pressure reading for a Minut device. + + :ivar time: Time of latest pressure reading for a Minut device. + + :ivar value: Value of latest pressure reading for a Minut device.""" + + time: str + value: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + time=d.get("time", None), + value=d.get("value", None), + ) + + +@dataclass +class DeviceSound(ResourceMapping): + """Latest sound reading for a Minut device. + + :ivar time: Time of latest sound reading for a Minut device. + + :ivar value: Value of latest sound reading for a Minut device.""" + + time: str + value: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + time=d.get("time", None), + value=d.get("value", None), + ) + + +@dataclass +class DeviceTemperature(ResourceMapping): + """Latest temperature reading for a Minut device. + + :ivar time: Time of latest temperature reading for a Minut device. + + :ivar value: Value of latest temperature reading for a Minut device.""" + + time: str + value: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + time=d.get("time", None), + value=d.get("value", None), + ) + + +@dataclass +class DeviceLatestSensorValues(ResourceMapping): + """Latest sensor values for a Minut device. + + :ivar accelerometer_z: Latest accelerometer Z-axis reading for a Minut device. + + :ivar humidity: Latest humidity reading for a Minut device. + + :ivar pressure: Latest pressure reading for a Minut device. + + :ivar sound: Latest sound reading for a Minut device. + + :ivar temperature: Latest temperature reading for a Minut device.""" + + accelerometer_z: DeviceAccelerometerZ + humidity: DeviceHumidity + pressure: DevicePressure + sound: DeviceSound + temperature: DeviceTemperature + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + accelerometer_z=( + DeviceAccelerometerZ.from_dict(d.get("accelerometer_z")) + if d.get("accelerometer_z") is not None + else None + ), + humidity=( + DeviceHumidity.from_dict(d.get("humidity")) + if d.get("humidity") is not None + else None + ), + pressure=( + DevicePressure.from_dict(d.get("pressure")) + if d.get("pressure") is not None + else None + ), + sound=( + DeviceSound.from_dict(d.get("sound")) + if d.get("sound") is not None + else None + ), + temperature=( + DeviceTemperature.from_dict(d.get("temperature")) + if d.get("temperature") is not None + else None + ), + ) + + +@dataclass +class DeviceMinutMetadata(ResourceMapping): + """Metadata for a Minut device. + + :ivar device_id: Device ID for a Minut device. + + :ivar device_name: Device name for a Minut device. + + :ivar latest_sensor_values: Latest sensor values for a Minut device.""" + + device_id: str + device_name: str + latest_sensor_values: DeviceLatestSensorValues + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + latest_sensor_values=( + DeviceLatestSensorValues.from_dict(d.get("latest_sensor_values")) + if d.get("latest_sensor_values") is not None + else None + ), + ) + + +@dataclass +class DeviceNestMetadata(ResourceMapping): + """Metadata for a Google Nest device. + + :ivar device_custom_name: Custom device name for a Google Nest device. The device owner sets this value. + + :ivar device_name: Device name for a Google Nest device. Google sets this value. + + :ivar display_name: Display name for a Google Nest device. + + :ivar nest_device_id: Device ID for a Google Nest device.""" + + device_custom_name: str + device_name: str + display_name: str + nest_device_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_custom_name=d.get("device_custom_name", None), + device_name=d.get("device_name", None), + display_name=d.get("display_name", None), + nest_device_id=d.get("nest_device_id", None), + ) + + +@dataclass +class DeviceNoiseawareMetadata(ResourceMapping): + """Metadata for a NoiseAware device. + + :ivar device_id: Device ID for a NoiseAware device. + + :ivar device_model: Device model for a NoiseAware device. + + :ivar device_name: Device name for a NoiseAware device. + + :ivar noise_level_decibel: Noise level, in decibels, for a NoiseAware device. + + :ivar noise_level_nrs: Noise level, expressed as a Noise Risk Score (NRS), for a NoiseAware device. + """ + + device_id: str + device_model: str + device_name: str + noise_level_decibel: float + noise_level_nrs: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_model=d.get("device_model", None), + device_name=d.get("device_name", None), + noise_level_decibel=d.get("noise_level_decibel", None), + noise_level_nrs=d.get("noise_level_nrs", None), + ) + + +@dataclass +class DeviceNukiMetadata(ResourceMapping): + """Metadata for a Nuki device. + + :ivar device_id: Device ID for a Nuki device. + + :ivar device_name: Device name for a Nuki device. + + :ivar keypad_2_paired: Indicates whether keypad 2 is paired for a Nuki device. + + :ivar keypad_battery_critical: Indicates whether the keypad battery is in a critical state for a Nuki device. + + :ivar keypad_paired: Indicates whether the keypad is paired for a Nuki device.""" + + device_id: str + device_name: str + keypad_2_paired: bool + keypad_battery_critical: bool + keypad_paired: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + keypad_2_paired=d.get("keypad_2_paired", None), + keypad_battery_critical=d.get("keypad_battery_critical", None), + keypad_paired=d.get("keypad_paired", None), + ) + + +@dataclass +class DeviceOmnitecMetadata(ResourceMapping): + """Metadata for an Omnitec device. + + :ivar has_gateway: Whether the Omnitec lock has a connected gateway for remote operations. + + :ivar lock_alias: Operator-assigned alias for an Omnitec device. + + :ivar lock_id: Lock ID for an Omnitec device. + + :ivar lock_mac: Bluetooth MAC address for an Omnitec device. + + :ivar lock_name: Lock name for an Omnitec device. + + :ivar time_zone: IANA time zone for the Omnitec device, used to schedule time-bound access codes at the correct local time (accounting for DST). + + :ivar timezone_raw_offset_ms: Static UTC offset of the Omnitec lock in milliseconds. Does not account for DST. + """ + + has_gateway: bool + lock_alias: str + lock_id: float + lock_mac: str + lock_name: str + time_zone: str + timezone_raw_offset_ms: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + has_gateway=d.get("has_gateway", None), + lock_alias=d.get("lock_alias", None), + lock_id=d.get("lock_id", None), + lock_mac=d.get("lock_mac", None), + lock_name=d.get("lock_name", None), + time_zone=d.get("time_zone", None), + timezone_raw_offset_ms=d.get("timezone_raw_offset_ms", None), + ) + + +@dataclass +class DeviceRingMetadata(ResourceMapping): + """Metadata for a Ring device. + + :ivar device_id: Device ID for a Ring device. + + :ivar device_name: Device name for a Ring device.""" + + device_id: str + device_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + ) + + +@dataclass +class DeviceSaltoKsMetadata(ResourceMapping): + """Metadata for a Salto KS device. + + :ivar battery_level: Battery level for a Salto KS device. + + :ivar customer_reference: Customer reference for a Salto KS device. + + :ivar has_custom_pin_subscription: Indicates whether the site has a Salto KS subscription that supports custom PINs. + + :ivar lock_id: Lock ID for a Salto KS device. + + :ivar lock_type: Lock type for a Salto KS device. + + :ivar locked_state: Locked state for a Salto KS device. + + :ivar model: Model for a Salto KS device. + + :ivar site_id: Site ID for the Salto KS site to which the device belongs. + + :ivar site_name: Site name for the Salto KS site to which the device belongs.""" + + battery_level: str + customer_reference: str + has_custom_pin_subscription: bool + lock_id: str + lock_type: str + locked_state: str + model: str + site_id: str + site_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + battery_level=d.get("battery_level", None), + customer_reference=d.get("customer_reference", None), + has_custom_pin_subscription=d.get("has_custom_pin_subscription", None), + lock_id=d.get("lock_id", None), + lock_type=d.get("lock_type", None), + locked_state=d.get("locked_state", None), + model=d.get("model", None), + site_id=d.get("site_id", None), + site_name=d.get("site_name", None), + ) + + +@dataclass +class DeviceSaltoMetadata(ResourceMapping): + """Metada for a Salto device. + + :ivar battery_level: Battery level for a Salto device. + + :ivar customer_reference: Customer reference for a Salto device. + + :ivar lock_id: Lock ID for a Salto device. + + :ivar lock_type: Lock type for a Salto device. + + :ivar locked_state: Locked state for a Salto device. + + :ivar model: Model for a Salto device. + + :ivar site_id: Site ID for the Salto KS site to which the device belongs. + + :ivar site_name: Site name for the Salto KS site to which the device belongs.""" + + battery_level: str + customer_reference: str + lock_id: str + lock_type: str + locked_state: str + model: str + site_id: str + site_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + battery_level=d.get("battery_level", None), + customer_reference=d.get("customer_reference", None), + lock_id=d.get("lock_id", None), + lock_type=d.get("lock_type", None), + locked_state=d.get("locked_state", None), + model=d.get("model", None), + site_id=d.get("site_id", None), + site_name=d.get("site_name", None), + ) + + +@dataclass +class DeviceSchlageMetadata(ResourceMapping): + """Metadata for a Schlage device. + + :ivar device_id: Device ID for a Schlage device. + + :ivar device_name: Device name for a Schlage device. + + :ivar model: Model for a Schlage device.""" + + device_id: str + device_name: str + model: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + model=d.get("model", None), + ) + + +@dataclass +class DeviceSeamBridgeMetadata(ResourceMapping): + """Metadata for Seam Bridge. + + :ivar device_num: Device number for Seam Bridge. + + :ivar name: Name for Seam Bridge. + + :ivar unlock_method: Unlock method for Seam Bridge.""" + + device_num: float + name: str + unlock_method: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_num=d.get("device_num", None), + name=d.get("name", None), + unlock_method=d.get("unlock_method", None), + ) + + +@dataclass +class DeviceSensiMetadata(ResourceMapping): + """Metadata for a Sensi device. + + :ivar device_id: Device ID for a Sensi device. + + :ivar device_name: Device name for a Sensi device. + + :ivar dual_setpoints_not_supported: Set to true when the device does not support the /dual-setpoints API endpoint. + + :ivar product_type: Product type for a Sensi device.""" + + device_id: str + device_name: str + dual_setpoints_not_supported: bool + product_type: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + dual_setpoints_not_supported=d.get("dual_setpoints_not_supported", None), + product_type=d.get("product_type", None), + ) + + +@dataclass +class DeviceSmartthingsMetadata(ResourceMapping): + """Metadata for a SmartThings device. + + :ivar device_id: Device ID for a SmartThings device. + + :ivar device_name: Device name for a SmartThings device. + + :ivar location_id: Location ID for a SmartThings device. + + :ivar model: Model for a SmartThings device.""" + + device_id: str + device_name: str + location_id: str + model: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + location_id=d.get("location_id", None), + model=d.get("model", None), + ) + + +@dataclass +class DeviceTadoMetadata(ResourceMapping): + """Metadata for a tado° device. + + :ivar device_type: Device type for a tado° device. + + :ivar serial_no: Serial number for a tado° device.""" + + device_type: str + serial_no: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_type=d.get("device_type", None), + serial_no=d.get("serial_no", None), + ) + + +@dataclass +class DeviceTedeeMetadata(ResourceMapping): + """Metadata for a Tedee device. + + :ivar bridge_id: Bridge ID for a Tedee device. + + :ivar bridge_name: Bridge name for a Tedee device. + + :ivar device_id: Device ID for a Tedee device. + + :ivar device_model: Device model for a Tedee device. + + :ivar device_name: Device name for a Tedee device. + + :ivar keypad_id: Keypad ID for a Tedee device. + + :ivar serial_number: Serial number for a Tedee device.""" + + bridge_id: float + bridge_name: str + device_id: float + device_model: str + device_name: str + keypad_id: float + serial_number: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + bridge_id=d.get("bridge_id", None), + bridge_name=d.get("bridge_name", None), + device_id=d.get("device_id", None), + device_model=d.get("device_model", None), + device_name=d.get("device_name", None), + keypad_id=d.get("keypad_id", None), + serial_number=d.get("serial_number", None), + ) + + +@dataclass +class DeviceFeatures(ResourceMapping): + """Features for a TTLock device. + + :ivar auto_lock_time_config: Indicates whether a TTLock device supports auto-lock time configuration. + + :ivar incomplete_keyboard_passcode: Indicates whether a TTLock device supports an incomplete keyboard passcode. + + :ivar lock_command: Indicates whether a TTLock device supports the lock command. + + :ivar passcode: Indicates whether a TTLock device supports a passcode. + + :ivar passcode_management: Indicates whether a TTLock device supports passcode management. + + :ivar unlock_via_gateway: Indicates whether a TTLock device supports unlock via gateway. + + :ivar wifi: Indicates whether a TTLock device supports Wi-Fi.""" + + auto_lock_time_config: bool + incomplete_keyboard_passcode: bool + lock_command: bool + passcode: bool + passcode_management: bool + unlock_via_gateway: bool + wifi: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + auto_lock_time_config=d.get("auto_lock_time_config", None), + incomplete_keyboard_passcode=d.get("incomplete_keyboard_passcode", None), + lock_command=d.get("lock_command", None), + passcode=d.get("passcode", None), + passcode_management=d.get("passcode_management", None), + unlock_via_gateway=d.get("unlock_via_gateway", None), + wifi=d.get("wifi", None), + ) + + +@dataclass +class DeviceWirelessKeypads(ResourceMapping): + """Wireless keypads for a TTLock device. + + :ivar wireless_keypad_id: ID for a wireless keypad for a TTLock device. + + :ivar wireless_keypad_name: Name for a wireless keypad for a TTLock device.""" + + wireless_keypad_id: float + wireless_keypad_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + wireless_keypad_id=d.get("wireless_keypad_id", None), + wireless_keypad_name=d.get("wireless_keypad_name", None), + ) + + +@dataclass +class DeviceTtlockMetadata(ResourceMapping): + """Metadata for a TTLock device. + + :ivar feature_value: Feature value for a TTLock device. + + :ivar features: Features for a TTLock device. + + :ivar has_gateway: Indicates whether a TTLock device has a gateway. + + :ivar lock_alias: Lock alias for a TTLock device. + + :ivar lock_id: Lock ID for a TTLock device. + + :ivar timezone_raw_offset_ms: Lock-side timezone offset in milliseconds east of UTC, as configured in the TTLock app. Source of truth for the lock's wall-clock interpretation of access code start/end times — a misconfigured value here is the typical cause of customer "codes offset by N hours" reports. Diagnostic only; Seam does not convert times based on this value. + + :ivar wireless_keypads: Wireless keypads for a TTLock device.""" + + feature_value: str + features: DeviceFeatures + has_gateway: bool + lock_alias: str + lock_id: float + timezone_raw_offset_ms: float + wireless_keypads: List[DeviceWirelessKeypads] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + feature_value=d.get("feature_value", None), + features=( + DeviceFeatures.from_dict(d.get("features")) + if d.get("features") is not None + else None + ), + has_gateway=d.get("has_gateway", None), + lock_alias=d.get("lock_alias", None), + lock_id=d.get("lock_id", None), + timezone_raw_offset_ms=d.get("timezone_raw_offset_ms", None), + wireless_keypads=[ + DeviceWirelessKeypads.from_dict(i) + for i in d.get("wireless_keypads") or [] + ], + ) + + +@dataclass +class DeviceTwoNMetadata(ResourceMapping): + """Metadata for a 2N device. + + :ivar device_id: Device ID for a 2N device. + + :ivar device_name: Device name for a 2N device.""" + + device_id: float + device_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + ) + + +@dataclass +class DeviceUltraloqMetadata(ResourceMapping): + """Metadata for an Ultraloq device. + + :ivar device_id: Device ID for an Ultraloq device. + + :ivar device_name: Device name for an Ultraloq device. + + :ivar device_type: Device type for an Ultraloq device. + + :ivar time_zone: IANA timezone for the Ultraloq device.""" + + device_id: str + device_name: str + device_type: str + time_zone: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + device_type=d.get("device_type", None), + time_zone=d.get("time_zone", None), + ) + + +@dataclass +class DeviceVisionlineMetadata(ResourceMapping): + """Metadata for an ASSA ABLOY Visionline system. + + :ivar encoder_id: Encoder ID for an ASSA ABLOY Visionline system.""" + + encoder_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + encoder_id=d.get("encoder_id", None), + ) + + +@dataclass +class DeviceWyzeMetadata(ResourceMapping): + """Metadata for a Wyze device. + + :ivar device_id: Device ID for a Wyze device. + + :ivar device_info_model: Device information model for a Wyze device. + + :ivar device_name: Device name for a Wyze device. + + :ivar keypad_uuid: Keypad UUID for a Wyze device. + + :ivar locker_status_hardlock: Locker status (hardlock) for a Wyze device. + + :ivar product_model: Product model for a Wyze device. + + :ivar product_name: Product name for a Wyze device. + + :ivar product_type: Product type for a Wyze device.""" + + device_id: str + device_info_model: str + device_name: str + keypad_uuid: str + locker_status_hardlock: float + product_model: str + product_name: str + product_type: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + device_info_model=d.get("device_info_model", None), + device_name=d.get("device_name", None), + keypad_uuid=d.get("keypad_uuid", None), + locker_status_hardlock=d.get("locker_status_hardlock", None), + product_model=d.get("product_model", None), + product_name=d.get("product_name", None), + product_type=d.get("product_type", None), + ) + + +@dataclass +class DeviceCodeConstraints(ResourceMapping): + """Constraints on access codes for the device. Seam represents each constraint as an object with a ``constraint_type`` property. Depending on the constraint type, there may also be additional properties. Note that some constraints are manufacturer- or device-specific. + + :ivar constraint_type: + + :ivar max_length: Maximum name length constraint for access codes. + + :ivar min_length: Minimum name length constraint for access codes.""" + + constraint_type: str + max_length: float + min_length: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + constraint_type=d.get("constraint_type", None), + max_length=d.get("max_length", None), + min_length=d.get("min_length", None), + ) + + +@dataclass +class DeviceKeypadBattery(ResourceMapping): + """Keypad battery status. + + :ivar level: Keypad battery charge level.""" + + level: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + level=d.get("level", None), + ) + + +@dataclass +class DeviceTimePairs(ResourceMapping): + """Fixed start/end time pairings the caller chooses from. Mutually exclusive with ``matching_start_end_time``. + + :ivar display_name: Label for the start/end time pairing. + + :ivar end_time: End time of day as a 24-hour ``HH:MM`` value, interpreted in the option's ``time_zone``. An ``end_time`` earlier on the clock than ``start_time`` means the end falls on a later date. + + :ivar start_time: Start time of day as a 24-hour ``HH:MM`` value, interpreted in the option's ``time_zone``. + """ + + display_name: str + end_time: str + start_time: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + display_name=d.get("display_name", None), + end_time=d.get("end_time", None), + start_time=d.get("start_time", None), + ) + + +@dataclass +class DeviceOfflineTimeFrameOptions(ResourceMapping): + """Time frames that may be requested when creating an offline access code, expressed as a list of options. The caller picks one option (by matching the requested duration when the options' duration ranges do not overlap, or by ``display_name`` when they do) and satisfies that one option's rules. When ``undefined``, any time frame works. + + :ivar display_name: Label for this option. For a single-option device, the product name (for example, ``algoPIN`` or ``SmartPIN``); for a multi-option device, a label that distinguishes it (for example, ``Hourly`` or ``Fixed start times``). + + :ivar end_date_recurrence_rule: iCalendar recurrence rule (RRULE) that the end date must fall on. Constrains which calendar dates are selectable, independent of the time-of-day rules. + + :ivar matching_start_end_time: When ``true``, the start and end must fall at the same time of day (the caller picks which). Mutually exclusive with ``time_pairs``. + + :ivar max_duration: Maximum duration this option covers, as an ISO 8601 duration (for example, ``PT672H`` or ``P367D``). Omitted when there is no maximum. + + :ivar min_duration: Minimum duration this option covers, as an ISO 8601 duration (for example, ``PT1H`` or ``P29D``). Omitted when there is no minimum. + + :ivar start_date_recurrence_rule: iCalendar recurrence rule (RRULE) that the start date must fall on (for example, ``FREQ=MONTHLY;BYDAY=1MO,3MO``). Constrains which calendar dates are selectable, independent of the time-of-day rules. + + :ivar time_pairs: Fixed start/end time pairings the caller chooses from. Mutually exclusive with ``matching_start_end_time``. + + :ivar time_zone: IANA time zone for interpreting ``time_pairs`` and the date recurrence rules. Present only when the option fixes times or dates. + """ + + display_name: str + end_date_recurrence_rule: str + matching_start_end_time: bool + max_duration: str + min_duration: str + start_date_recurrence_rule: str + time_pairs: List[DeviceTimePairs] + time_zone: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + display_name=d.get("display_name", None), + end_date_recurrence_rule=d.get("end_date_recurrence_rule", None), + matching_start_end_time=d.get("matching_start_end_time", None), + max_duration=d.get("max_duration", None), + min_duration=d.get("min_duration", None), + start_date_recurrence_rule=d.get("start_date_recurrence_rule", None), + time_pairs=[ + DeviceTimePairs.from_dict(i) for i in d.get("time_pairs") or [] + ], + time_zone=d.get("time_zone", None), + ) + + +@dataclass +class DeviceOnlineTimeFrameOptions(ResourceMapping): + """Time frames that may be requested when creating an online access code, expressed as a list of options. The caller picks one option (by matching the requested duration when the options' duration ranges do not overlap, or by ``display_name`` when they do) and satisfies that one option's rules. When ``undefined``, any time frame works. + + :ivar display_name: Label for this option. For a single-option device, the product name (for example, ``algoPIN`` or ``SmartPIN``); for a multi-option device, a label that distinguishes it (for example, ``Hourly`` or ``Fixed start times``). + + :ivar end_date_recurrence_rule: iCalendar recurrence rule (RRULE) that the end date must fall on. Constrains which calendar dates are selectable, independent of the time-of-day rules. + + :ivar matching_start_end_time: When ``true``, the start and end must fall at the same time of day (the caller picks which). Mutually exclusive with ``time_pairs``. + + :ivar max_duration: Maximum duration this option covers, as an ISO 8601 duration (for example, ``PT672H`` or ``P367D``). Omitted when there is no maximum. + + :ivar min_duration: Minimum duration this option covers, as an ISO 8601 duration (for example, ``PT1H`` or ``P29D``). Omitted when there is no minimum. + + :ivar start_date_recurrence_rule: iCalendar recurrence rule (RRULE) that the start date must fall on (for example, ``FREQ=MONTHLY;BYDAY=1MO,3MO``). Constrains which calendar dates are selectable, independent of the time-of-day rules. + + :ivar time_pairs: Fixed start/end time pairings the caller chooses from. Mutually exclusive with ``matching_start_end_time``. + + :ivar time_zone: IANA time zone for interpreting ``time_pairs`` and the date recurrence rules. Present only when the option fixes times or dates. + """ + + display_name: str + end_date_recurrence_rule: str + matching_start_end_time: bool + max_duration: str + min_duration: str + start_date_recurrence_rule: str + time_pairs: List[DeviceTimePairs] + time_zone: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + display_name=d.get("display_name", None), + end_date_recurrence_rule=d.get("end_date_recurrence_rule", None), + matching_start_end_time=d.get("matching_start_end_time", None), + max_duration=d.get("max_duration", None), + min_duration=d.get("min_duration", None), + start_date_recurrence_rule=d.get("start_date_recurrence_rule", None), + time_pairs=[ + DeviceTimePairs.from_dict(i) for i in d.get("time_pairs") or [] + ], + time_zone=d.get("time_zone", None), + ) + + +@dataclass +class DeviceActiveThermostatSchedule(ResourceMapping): + """Active `thermostat schedule `_. + + :ivar climate_preset_key: Key of the `climate preset `_ to use for the `thermostat schedule `_. + + :ivar created_at: Date and time at which the `thermostat schedule `_ was created. + + :ivar device_id: ID of the desired `thermostat `_ device. + + :ivar ends_at: Date and time at which the `thermostat schedule `_ ends, in `ISO 8601 `_ format. + + :ivar errors: Errors associated with the `thermostat schedule `_. + + :ivar is_override_allowed: Indicates whether a person at the thermostat can change the thermostat's settings after the `thermostat schedule `_ starts. + + :ivar max_override_period_minutes: Number of minutes for which a person at the thermostat can change the thermostat's settings after the activation of the scheduled `climate preset `_. See also `Specifying Manual Override Permissions `_. + + :ivar name: User-friendly name to identify the `thermostat schedule `_. + + :ivar starts_at: Date and time at which the `thermostat schedule `_ starts, in `ISO 8601 `_ format. + + :ivar thermostat_schedule_id: ID of the `thermostat schedule `_. + + :ivar workspace_id: ID of the workspace that contains the thermostat schedule.""" + + climate_preset_key: str + created_at: str + device_id: str + ends_at: str + errors: List[DeviceErrors] + is_override_allowed: bool + max_override_period_minutes: int + name: str + starts_at: str + thermostat_schedule_id: str + workspace_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + climate_preset_key=d.get("climate_preset_key", None), + created_at=d.get("created_at", None), + device_id=d.get("device_id", None), + ends_at=d.get("ends_at", None), + errors=[DeviceErrors.from_dict(i) for i in d.get("errors") or []], + is_override_allowed=d.get("is_override_allowed", None), + max_override_period_minutes=d.get("max_override_period_minutes", None), + name=d.get("name", None), + starts_at=d.get("starts_at", None), + thermostat_schedule_id=d.get("thermostat_schedule_id", None), + workspace_id=d.get("workspace_id", None), + ) + + +@dataclass +class DeviceAvailableClimatePresets(ResourceMapping): + """Available `climate presets `_ for the thermostat. + + :ivar can_delete: Indicates whether the `climate preset `_ key can be deleted. + + :ivar can_edit: Indicates whether the `climate preset `_ key can be edited. + + :ivar can_use_with_thermostat_daily_programs: Indicates whether the `climate preset `_ key can be programmed in a thermostat daily program. + + :ivar climate_preset_key: Unique key to identify the `climate preset `_. + + :ivar climate_preset_mode: The climate preset mode for the thermostat, based on the available climate preset modes reported by the device. + + :ivar cooling_set_point_celsius: Temperature to which the thermostat should cool (in °C). See also `Set Points `_. + + :ivar cooling_set_point_fahrenheit: Temperature to which the thermostat should cool (in °F). See also `Set Points `_. + + :ivar display_name: Display name for the `climate preset `_. + + :ivar ecobee_metadata: Metadata specific to the Ecobee climate, if applicable. + + :ivar fan_mode_setting: Desired `fan mode setting `_, such as ``on``, ``auto``, or ``circulate``. + + :ivar heating_set_point_celsius: Temperature to which the thermostat should heat (in °C). See also `Set Points `_. + + :ivar heating_set_point_fahrenheit: Temperature to which the thermostat should heat (in °F). See also `Set Points `_. + + :ivar hvac_mode_setting: Desired `HVAC mode `_ setting, such as ``heat``, ``cool``, ``heat_cool``, or ``off``. + + :ivar manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat can change the thermostat's settings. See `Specifying Manual Override Permissions `_. + + :ivar name: User-friendly name to identify the `climate preset `_. + """ + + can_delete: bool + can_edit: bool + can_use_with_thermostat_daily_programs: bool + climate_preset_key: str + climate_preset_mode: str + cooling_set_point_celsius: float + cooling_set_point_fahrenheit: float + display_name: str + ecobee_metadata: DeviceEcobeeMetadata + fan_mode_setting: str + heating_set_point_celsius: float + heating_set_point_fahrenheit: float + hvac_mode_setting: str + manual_override_allowed: bool + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + can_delete=d.get("can_delete", None), + can_edit=d.get("can_edit", None), + can_use_with_thermostat_daily_programs=d.get( + "can_use_with_thermostat_daily_programs", None + ), + climate_preset_key=d.get("climate_preset_key", None), + climate_preset_mode=d.get("climate_preset_mode", None), + cooling_set_point_celsius=d.get("cooling_set_point_celsius", None), + cooling_set_point_fahrenheit=d.get("cooling_set_point_fahrenheit", None), + display_name=d.get("display_name", None), + ecobee_metadata=( + DeviceEcobeeMetadata.from_dict(d.get("ecobee_metadata")) + if d.get("ecobee_metadata") is not None + else None + ), + fan_mode_setting=d.get("fan_mode_setting", None), + heating_set_point_celsius=d.get("heating_set_point_celsius", None), + heating_set_point_fahrenheit=d.get("heating_set_point_fahrenheit", None), + hvac_mode_setting=d.get("hvac_mode_setting", None), + manual_override_allowed=d.get("manual_override_allowed", None), + name=d.get("name", None), + ) + + +@dataclass +class DeviceCurrentClimateSetting(ResourceMapping): + """Current climate setting. + + :ivar can_delete: Indicates whether the `climate preset `_ key can be deleted. + + :ivar can_edit: Indicates whether the `climate preset `_ key can be edited. + + :ivar can_use_with_thermostat_daily_programs: Indicates whether the `climate preset `_ key can be programmed in a thermostat daily program. + + :ivar climate_preset_key: Unique key to identify the `climate preset `_. + + :ivar climate_preset_mode: The climate preset mode for the thermostat, based on the available climate preset modes reported by the device. + + :ivar cooling_set_point_celsius: Temperature to which the thermostat should cool (in °C). See also `Set Points `_. + + :ivar cooling_set_point_fahrenheit: Temperature to which the thermostat should cool (in °F). See also `Set Points `_. + + :ivar display_name: Display name for the `climate preset `_. + + :ivar ecobee_metadata: Metadata specific to the Ecobee climate, if applicable. + + :ivar fan_mode_setting: Desired `fan mode setting `_, such as ``on``, ``auto``, or ``circulate``. + + :ivar heating_set_point_celsius: Temperature to which the thermostat should heat (in °C). See also `Set Points `_. + + :ivar heating_set_point_fahrenheit: Temperature to which the thermostat should heat (in °F). See also `Set Points `_. + + :ivar hvac_mode_setting: Desired `HVAC mode `_ setting, such as ``heat``, ``cool``, ``heat_cool``, or ``off``. + + :ivar manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat can change the thermostat's settings. See `Specifying Manual Override Permissions `_. + + :ivar name: User-friendly name to identify the `climate preset `_. + """ + + can_delete: bool + can_edit: bool + can_use_with_thermostat_daily_programs: bool + climate_preset_key: str + climate_preset_mode: str + cooling_set_point_celsius: float + cooling_set_point_fahrenheit: float + display_name: str + ecobee_metadata: DeviceEcobeeMetadata + fan_mode_setting: str + heating_set_point_celsius: float + heating_set_point_fahrenheit: float + hvac_mode_setting: str + manual_override_allowed: bool + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + can_delete=d.get("can_delete", None), + can_edit=d.get("can_edit", None), + can_use_with_thermostat_daily_programs=d.get( + "can_use_with_thermostat_daily_programs", None + ), + climate_preset_key=d.get("climate_preset_key", None), + climate_preset_mode=d.get("climate_preset_mode", None), + cooling_set_point_celsius=d.get("cooling_set_point_celsius", None), + cooling_set_point_fahrenheit=d.get("cooling_set_point_fahrenheit", None), + display_name=d.get("display_name", None), + ecobee_metadata=( + DeviceEcobeeMetadata.from_dict(d.get("ecobee_metadata")) + if d.get("ecobee_metadata") is not None + else None + ), + fan_mode_setting=d.get("fan_mode_setting", None), + heating_set_point_celsius=d.get("heating_set_point_celsius", None), + heating_set_point_fahrenheit=d.get("heating_set_point_fahrenheit", None), + hvac_mode_setting=d.get("hvac_mode_setting", None), + manual_override_allowed=d.get("manual_override_allowed", None), + name=d.get("name", None), + ) + + +@dataclass +class DeviceDefaultClimateSetting(ResourceMapping): + """ + + :ivar can_delete: Indicates whether the `climate preset `_ key can be deleted. + + :ivar can_edit: Indicates whether the `climate preset `_ key can be edited. + + :ivar can_use_with_thermostat_daily_programs: Indicates whether the `climate preset `_ key can be programmed in a thermostat daily program. + + :ivar climate_preset_key: Unique key to identify the `climate preset `_. + + :ivar climate_preset_mode: The climate preset mode for the thermostat, based on the available climate preset modes reported by the device. + + :ivar cooling_set_point_celsius: Temperature to which the thermostat should cool (in °C). See also `Set Points `_. + + :ivar cooling_set_point_fahrenheit: Temperature to which the thermostat should cool (in °F). See also `Set Points `_. + + :ivar display_name: Display name for the `climate preset `_. + + :ivar ecobee_metadata: Metadata specific to the Ecobee climate, if applicable. + + :ivar fan_mode_setting: Desired `fan mode setting `_, such as ``on``, ``auto``, or ``circulate``. + + :ivar heating_set_point_celsius: Temperature to which the thermostat should heat (in °C). See also `Set Points `_. + + :ivar heating_set_point_fahrenheit: Temperature to which the thermostat should heat (in °F). See also `Set Points `_. + + :ivar hvac_mode_setting: Desired `HVAC mode `_ setting, such as ``heat``, ``cool``, ``heat_cool``, or ``off``. + + :ivar manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat can change the thermostat's settings. See `Specifying Manual Override Permissions `_. + + :ivar name: User-friendly name to identify the `climate preset `_. + """ + + can_delete: bool + can_edit: bool + can_use_with_thermostat_daily_programs: bool + climate_preset_key: str + climate_preset_mode: str + cooling_set_point_celsius: float + cooling_set_point_fahrenheit: float + display_name: str + ecobee_metadata: DeviceEcobeeMetadata + fan_mode_setting: str + heating_set_point_celsius: float + heating_set_point_fahrenheit: float + hvac_mode_setting: str + manual_override_allowed: bool + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + can_delete=d.get("can_delete", None), + can_edit=d.get("can_edit", None), + can_use_with_thermostat_daily_programs=d.get( + "can_use_with_thermostat_daily_programs", None + ), + climate_preset_key=d.get("climate_preset_key", None), + climate_preset_mode=d.get("climate_preset_mode", None), + cooling_set_point_celsius=d.get("cooling_set_point_celsius", None), + cooling_set_point_fahrenheit=d.get("cooling_set_point_fahrenheit", None), + display_name=d.get("display_name", None), + ecobee_metadata=( + DeviceEcobeeMetadata.from_dict(d.get("ecobee_metadata")) + if d.get("ecobee_metadata") is not None + else None + ), + fan_mode_setting=d.get("fan_mode_setting", None), + heating_set_point_celsius=d.get("heating_set_point_celsius", None), + heating_set_point_fahrenheit=d.get("heating_set_point_fahrenheit", None), + hvac_mode_setting=d.get("hvac_mode_setting", None), + manual_override_allowed=d.get("manual_override_allowed", None), + name=d.get("name", None), + ) + + +@dataclass +class DeviceTemperatureThreshold(ResourceMapping): + """Current `temperature threshold `_ set for the thermostat. + + :ivar lower_limit_celsius: Lower limit in °C within the current `temperature threshold `_ set for the thermostat. + + :ivar lower_limit_fahrenheit: Lower limit in °F within the current `temperature threshold `_ set for the thermostat. + + :ivar upper_limit_celsius: Upper limit in °C within the current `temperature threshold `_ set for the thermostat. + + :ivar upper_limit_fahrenheit: Upper limit in °F within the current `temperature threshold `_ set for the thermostat. + """ + + lower_limit_celsius: float + lower_limit_fahrenheit: float + upper_limit_celsius: float + upper_limit_fahrenheit: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + lower_limit_celsius=d.get("lower_limit_celsius", None), + lower_limit_fahrenheit=d.get("lower_limit_fahrenheit", None), + upper_limit_celsius=d.get("upper_limit_celsius", None), + upper_limit_fahrenheit=d.get("upper_limit_fahrenheit", None), + ) + + +@dataclass +class DevicePeriods(ResourceMapping): + """Array of thermostat daily program periods. + + :ivar climate_preset_key: Key of the `climate preset `_ to activate at the ``starts_at_time``. + + :ivar starts_at_time: Time at which the thermostat daily program period starts, in `ISO 8601 `_ format. + """ + + climate_preset_key: str + starts_at_time: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + climate_preset_key=d.get("climate_preset_key", None), + starts_at_time=d.get("starts_at_time", None), + ) + + +@dataclass +class DeviceThermostatDailyPrograms(ResourceMapping): + """Configured `daily programs `_ for the thermostat. + + :ivar created_at: Date and time at which the thermostat daily program was created. + + :ivar device_id: ID of the thermostat device on which the thermostat daily program is configured. + + :ivar name: User-friendly name to identify the thermostat daily program. + + :ivar periods: Array of thermostat daily program periods. + + :ivar thermostat_daily_program_id: ID of the thermostat daily program. + + :ivar workspace_id: ID of the workspace that contains the thermostat daily program. + """ + + created_at: str + device_id: str + name: str + periods: List[DevicePeriods] + thermostat_daily_program_id: str + workspace_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + device_id=d.get("device_id", None), + name=d.get("name", None), + periods=[DevicePeriods.from_dict(i) for i in d.get("periods") or []], + thermostat_daily_program_id=d.get("thermostat_daily_program_id", None), + workspace_id=d.get("workspace_id", None), + ) + + +@dataclass +class DeviceThermostatWeeklyProgram(ResourceMapping): + """Current `weekly program `_ for the thermostat. + + :ivar created_at: Date and time at which the thermostat weekly program was created. + + :ivar friday_program_id: ID of the thermostat daily program to run on Fridays. + + :ivar monday_program_id: ID of the thermostat daily program to run on Mondays. + + :ivar saturday_program_id: ID of the thermostat daily program to run on Saturdays. + + :ivar sunday_program_id: ID of the thermostat daily program to run on Sundays. + + :ivar thursday_program_id: ID of the thermostat daily program to run on Thursdays. + + :ivar tuesday_program_id: ID of the thermostat daily program to run on Tuesdays. + + :ivar wednesday_program_id: ID of the thermostat daily program to run on Wednesdays. + """ + + created_at: str + friday_program_id: str + monday_program_id: str + saturday_program_id: str + sunday_program_id: str + thursday_program_id: str + tuesday_program_id: str + wednesday_program_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + friday_program_id=d.get("friday_program_id", None), + monday_program_id=d.get("monday_program_id", None), + saturday_program_id=d.get("saturday_program_id", None), + sunday_program_id=d.get("sunday_program_id", None), + thursday_program_id=d.get("thursday_program_id", None), + tuesday_program_id=d.get("tuesday_program_id", None), + wednesday_program_id=d.get("wednesday_program_id", None), + ) + + +@dataclass +class DeviceProperties(ResourceMapping): + """Properties of the device. + + :ivar accessory_keypad: Accessory keypad properties and state. + + :ivar appearance: Appearance-related properties, as reported by the device. + + :ivar battery: Represents the current status of the battery charge level. + + :ivar battery_level: Indicates the battery level of the device as a decimal value between 0 and 1, inclusive. + + :ivar currently_triggering_noise_threshold_ids: Array of noise threshold IDs that are currently triggering. + + :ivar has_direct_power: Indicates whether the device has direct power. + + :ivar image_alt_text: Alt text for the device image. + + :ivar image_url: Image URL for the device. + + :ivar manufacturer: Manufacturer of the device. When a device, such as a smart lock, is connected through a smart hub, the manufacturer of the device might be different from that of the smart hub. + + :ivar model: Device model-related properties. + + :ivar name: Deprecated: use device.display_name instead Name of the device. + + :ivar noise_level_decibels: Indicates current noise level in decibels, if the device supports noise detection. + + :ivar offline_access_codes_enabled: Deprecated: use device.can_program_offline_access_codes Indicates whether it is currently possible to use offline access codes for the device. + + :ivar online: Indicates whether the device is online. + + :ivar online_access_codes_enabled: Deprecated: use device.can_program_online_access_codes Indicates whether it is currently possible to use online access codes for the device. + + :ivar serial_number: Serial number of the device. + + :ivar supports_accessory_keypad: Deprecated: use device.properties.model.can_connect_accessory_keypad + + :ivar supports_offline_access_codes: Deprecated: use offline_access_codes_enabled + + :ivar assa_abloy_credential_service_metadata: ASSA ABLOY Credential Service metadata for the phone. + + :ivar salto_space_credential_service_metadata: Salto Space credential service metadata for the phone. + + :ivar akiles_metadata: Metadata for an Akiles device. + + :ivar aqara_metadata: Metadata for an Aqara device. + + :ivar assa_abloy_vostio_metadata: Metadata for an ASSA ABLOY Vostio system. + + :ivar august_metadata: Metadata for an August device. + + :ivar avigilon_alta_metadata: Metadata for an Avigilon Alta system. + + :ivar brivo_metadata: Metadata for a Brivo device. + + :ivar controlbyweb_metadata: Metadata for a ControlByWeb device. + + :ivar dormakaba_oracode_metadata: Metadata for a dormakaba Oracode device. + + :ivar ecobee_metadata: Metadata for an ecobee device. + + :ivar four_suites_metadata: Metadata for a 4SUITES device. + + :ivar genie_metadata: Metadata for a Genie device. + + :ivar honeywell_resideo_metadata: Metadata for a Honeywell Resideo device. + + :ivar igloo_metadata: Metadata for an igloo device. + + :ivar igloohome_metadata: Metadata for an igloohome device. + + :ivar keynest_metadata: Metadata for a KeyNest device. + + :ivar kisi_metadata: Metadata for a Kisi device. + + :ivar korelock_metadata: Metadata for a Korelock device. + + :ivar kwikset_metadata: Metadata for a Kwikset device. + + :ivar lockly_metadata: Metadata for a Lockly device. + + :ivar minut_metadata: Metadata for a Minut device. + + :ivar nest_metadata: Metadata for a Google Nest device. + + :ivar noiseaware_metadata: Metadata for a NoiseAware device. + + :ivar nuki_metadata: Metadata for a Nuki device. + + :ivar omnitec_metadata: Metadata for an Omnitec device. + + :ivar ring_metadata: Metadata for a Ring device. + + :ivar salto_ks_metadata: Metadata for a Salto KS device. + + :ivar salto_metadata: Deprecated: Use ``salto_ks_metadata `` instead. Metada for a Salto device. + + :ivar schlage_metadata: Metadata for a Schlage device. + + :ivar seam_bridge_metadata: Metadata for Seam Bridge. + + :ivar sensi_metadata: Metadata for a Sensi device. + + :ivar smartthings_metadata: Metadata for a SmartThings device. + + :ivar tado_metadata: Metadata for a tado° device. + + :ivar tedee_metadata: Metadata for a Tedee device. + + :ivar ttlock_metadata: Metadata for a TTLock device. + + :ivar two_n_metadata: Metadata for a 2N device. + + :ivar ultraloq_metadata: Metadata for an Ultraloq device. + + :ivar visionline_metadata: Metadata for an ASSA ABLOY Visionline system. + + :ivar wyze_metadata: Metadata for a Wyze device. + + :ivar auto_lock_delay_seconds: The delay in seconds before the lock automatically locks after being unlocked. + + :ivar auto_lock_enabled: Indicates whether automatic locking is enabled. + + :ivar backup_access_code_pool_enabled: Indicates whether the `backup access code pool `_ is currently enabled for the device. To disable it, set this to ``false`` using `/devices/update `_. + + :ivar code_constraints: Constraints on access codes for the device. Seam represents each constraint as an object with a ``constraint_type`` property. Depending on the constraint type, there may also be additional properties. Note that some constraints are manufacturer- or device-specific. + + :ivar door_open: Indicates whether the door is open. + + :ivar has_native_entry_events: Indicates whether the device supports native entry events. + + :ivar keypad_battery: Keypad battery status. + + :ivar locked: Indicates whether the lock is locked. + + :ivar max_active_codes_supported: Maximum number of active access codes that the device supports. + + :ivar offline_time_frame_options: Time frames that may be requested when creating an offline access code, expressed as a list of options. The caller picks one option (by matching the requested duration when the options' duration ranges do not overlap, or by ``display_name`` when they do) and satisfies that one option's rules. When ``undefined``, any time frame works. + + :ivar online_time_frame_options: Time frames that may be requested when creating an online access code, expressed as a list of options. The caller picks one option (by matching the requested duration when the options' duration ranges do not overlap, or by ``display_name`` when they do) and satisfies that one option's rules. When ``undefined``, any time frame works. + + :ivar supported_code_lengths: Supported code lengths for access codes. + + :ivar supports_backup_access_code_pool: Indicates whether the device supports a `backup access code pool `_. + + :ivar active_thermostat_schedule: Deprecated: Use ``active_thermostat_schedule_id`` with ``/thermostats/schedules/get`` instead. Active `thermostat schedule `_. + + :ivar active_thermostat_schedule_id: ID of the active `thermostat schedule `_. + + :ivar available_climate_preset_modes: Climate preset modes that the thermostat supports, such as "home", "away", "wake", "sleep", "occupied", and "unoccupied". + + :ivar available_climate_presets: Available `climate presets `_ for the thermostat. + + :ivar available_fan_mode_settings: Fan mode settings that the thermostat supports. + + :ivar available_hvac_mode_settings: HVAC mode settings that the thermostat supports. + + :ivar current_climate_setting: Current climate setting. + + :ivar default_climate_setting: Deprecated: use fallback_climate_preset_key to specify a fallback climate preset instead. + + :ivar fallback_climate_preset_key: Key of the `fallback climate preset `_ for the thermostat. + + :ivar fan_mode_setting: Deprecated: Use ``current_climate_setting.fan_mode_setting`` instead. + + :ivar is_cooling: Indicates whether the connected HVAC system is currently cooling, as reported by the thermostat. + + :ivar is_fan_running: Indicates whether the fan in the connected HVAC system is currently running, as reported by the thermostat. + + :ivar is_heating: Indicates whether the connected HVAC system is currently heating, as reported by the thermostat. + + :ivar is_temporary_manual_override_active: Indicates whether the current thermostat settings differ from the most recent active program or schedule that Seam activated. For this condition to occur, ``current_climate_setting.manual_override_allowed`` must also be ``true``. + + :ivar max_cooling_set_point_celsius: Maximum `cooling set point `_ in °C. + + :ivar max_cooling_set_point_fahrenheit: Maximum `cooling set point `_ in °F. + + :ivar max_heating_set_point_celsius: Maximum `heating set point `_ in °C. + + :ivar max_heating_set_point_fahrenheit: Maximum `heating set point `_ in °F. + + :ivar max_thermostat_daily_program_periods_per_day: Maximum number of periods that the thermostat can support per day. For example, if the thermostat supports 4 periods per day, this value is 4. + + :ivar max_unique_climate_presets_per_thermostat_weekly_program: Maximum number of climate presets that the thermostat can support for weekly programming. + + :ivar min_cooling_set_point_celsius: Minimum `cooling set point `_ in °C. + + :ivar min_cooling_set_point_fahrenheit: Minimum `cooling set point `_ in °F. + + :ivar min_heating_cooling_delta_celsius: Minimum `temperature difference `_ in °C between the cooling and heating set points when in heat-cool (auto) mode. + + :ivar min_heating_cooling_delta_fahrenheit: Minimum `temperature difference `_ in °F between the cooling and heating set points when in heat-cool (auto) mode. + + :ivar min_heating_set_point_celsius: Minimum `heating set point `_ in °C. + + :ivar min_heating_set_point_fahrenheit: Minimum `heating set point `_ in °F. + + :ivar relative_humidity: Reported relative humidity, as a value between 0 and 1, inclusive. + + :ivar temperature_celsius: Reported temperature in °C. + + :ivar temperature_fahrenheit: Reported temperature in °F. + + :ivar temperature_threshold: Current `temperature threshold `_ set for the thermostat. + + :ivar thermostat_daily_program_period_precision_minutes: Precision of the thermostat's period in minutes. For example, if the thermostat supports 15-minute periods, this value is 15. All values are relative to the top of the hour, so for 15 minutes, the periods would be 0, 15, 30, and 45 minutes past the hour. + + :ivar thermostat_daily_programs: Configured `daily programs `_ for the thermostat. + + :ivar thermostat_weekly_program: Current `weekly program `_ for the thermostat. + """ + + accessory_keypad: DeviceAccessoryKeypad + appearance: DeviceAppearance + battery: DeviceBattery + battery_level: float + currently_triggering_noise_threshold_ids: List[str] + has_direct_power: bool + image_alt_text: str + image_url: str + manufacturer: str + model: DeviceModel + name: str + noise_level_decibels: float + offline_access_codes_enabled: bool + online: bool + online_access_codes_enabled: bool + serial_number: str + supports_accessory_keypad: bool + supports_offline_access_codes: bool + assa_abloy_credential_service_metadata: DeviceAssaAbloyCredentialServiceMetadata + salto_space_credential_service_metadata: DeviceSaltoSpaceCredentialServiceMetadata + akiles_metadata: DeviceAkilesMetadata + aqara_metadata: DeviceAqaraMetadata + assa_abloy_vostio_metadata: DeviceAssaAbloyVostioMetadata + august_metadata: DeviceAugustMetadata + avigilon_alta_metadata: DeviceAvigilonAltaMetadata + brivo_metadata: DeviceBrivoMetadata + controlbyweb_metadata: DeviceControlbywebMetadata + dormakaba_oracode_metadata: DeviceDormakabaOracodeMetadata + ecobee_metadata: DeviceEcobeeMetadata + four_suites_metadata: DeviceFourSuitesMetadata + genie_metadata: DeviceGenieMetadata + honeywell_resideo_metadata: DeviceHoneywellResideoMetadata + igloo_metadata: DeviceIglooMetadata + igloohome_metadata: DeviceIgloohomeMetadata + keynest_metadata: DeviceKeynestMetadata + kisi_metadata: DeviceKisiMetadata + korelock_metadata: DeviceKorelockMetadata + kwikset_metadata: DeviceKwiksetMetadata + lockly_metadata: DeviceLocklyMetadata + minut_metadata: DeviceMinutMetadata + nest_metadata: DeviceNestMetadata + noiseaware_metadata: DeviceNoiseawareMetadata + nuki_metadata: DeviceNukiMetadata + omnitec_metadata: DeviceOmnitecMetadata + ring_metadata: DeviceRingMetadata + salto_ks_metadata: DeviceSaltoKsMetadata + salto_metadata: DeviceSaltoMetadata + schlage_metadata: DeviceSchlageMetadata + seam_bridge_metadata: DeviceSeamBridgeMetadata + sensi_metadata: DeviceSensiMetadata + smartthings_metadata: DeviceSmartthingsMetadata + tado_metadata: DeviceTadoMetadata + tedee_metadata: DeviceTedeeMetadata + ttlock_metadata: DeviceTtlockMetadata + two_n_metadata: DeviceTwoNMetadata + ultraloq_metadata: DeviceUltraloqMetadata + visionline_metadata: DeviceVisionlineMetadata + wyze_metadata: DeviceWyzeMetadata + auto_lock_delay_seconds: float + auto_lock_enabled: bool + backup_access_code_pool_enabled: bool + code_constraints: List[DeviceCodeConstraints] + door_open: bool + has_native_entry_events: bool + keypad_battery: DeviceKeypadBattery + locked: bool + max_active_codes_supported: float + offline_time_frame_options: List[DeviceOfflineTimeFrameOptions] + online_time_frame_options: List[DeviceOnlineTimeFrameOptions] + supported_code_lengths: List[float] + supports_backup_access_code_pool: bool + active_thermostat_schedule: DeviceActiveThermostatSchedule + active_thermostat_schedule_id: str + available_climate_preset_modes: List[str] + available_climate_presets: List[DeviceAvailableClimatePresets] + available_fan_mode_settings: List[str] + available_hvac_mode_settings: List[str] + current_climate_setting: DeviceCurrentClimateSetting + default_climate_setting: DeviceDefaultClimateSetting + fallback_climate_preset_key: str + fan_mode_setting: str + is_cooling: bool + is_fan_running: bool + is_heating: bool + is_temporary_manual_override_active: bool + max_cooling_set_point_celsius: float + max_cooling_set_point_fahrenheit: float + max_heating_set_point_celsius: float + max_heating_set_point_fahrenheit: float + max_thermostat_daily_program_periods_per_day: float + max_unique_climate_presets_per_thermostat_weekly_program: float + min_cooling_set_point_celsius: float + min_cooling_set_point_fahrenheit: float + min_heating_cooling_delta_celsius: float + min_heating_cooling_delta_fahrenheit: float + min_heating_set_point_celsius: float + min_heating_set_point_fahrenheit: float + relative_humidity: float + temperature_celsius: float + temperature_fahrenheit: float + temperature_threshold: DeviceTemperatureThreshold + thermostat_daily_program_period_precision_minutes: float + thermostat_daily_programs: List[DeviceThermostatDailyPrograms] + thermostat_weekly_program: DeviceThermostatWeeklyProgram + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + accessory_keypad=( + DeviceAccessoryKeypad.from_dict(d.get("accessory_keypad")) + if d.get("accessory_keypad") is not None + else None + ), + appearance=( + DeviceAppearance.from_dict(d.get("appearance")) + if d.get("appearance") is not None + else None + ), + battery=( + DeviceBattery.from_dict(d.get("battery")) + if d.get("battery") is not None + else None + ), + battery_level=d.get("battery_level", None), + currently_triggering_noise_threshold_ids=d.get( + "currently_triggering_noise_threshold_ids", None + ), + has_direct_power=d.get("has_direct_power", None), + image_alt_text=d.get("image_alt_text", None), + image_url=d.get("image_url", None), + manufacturer=d.get("manufacturer", None), + model=( + DeviceModel.from_dict(d.get("model")) + if d.get("model") is not None + else None + ), + name=d.get("name", None), + noise_level_decibels=d.get("noise_level_decibels", None), + offline_access_codes_enabled=d.get("offline_access_codes_enabled", None), + online=d.get("online", None), + online_access_codes_enabled=d.get("online_access_codes_enabled", None), + serial_number=d.get("serial_number", None), + supports_accessory_keypad=d.get("supports_accessory_keypad", None), + supports_offline_access_codes=d.get("supports_offline_access_codes", None), + assa_abloy_credential_service_metadata=( + DeviceAssaAbloyCredentialServiceMetadata.from_dict( + d.get("assa_abloy_credential_service_metadata") + ) + if d.get("assa_abloy_credential_service_metadata") is not None + else None + ), + salto_space_credential_service_metadata=( + DeviceSaltoSpaceCredentialServiceMetadata.from_dict( + d.get("salto_space_credential_service_metadata") + ) + if d.get("salto_space_credential_service_metadata") is not None + else None + ), + akiles_metadata=( + DeviceAkilesMetadata.from_dict(d.get("akiles_metadata")) + if d.get("akiles_metadata") is not None + else None + ), + aqara_metadata=( + DeviceAqaraMetadata.from_dict(d.get("aqara_metadata")) + if d.get("aqara_metadata") is not None + else None + ), + assa_abloy_vostio_metadata=( + DeviceAssaAbloyVostioMetadata.from_dict( + d.get("assa_abloy_vostio_metadata") + ) + if d.get("assa_abloy_vostio_metadata") is not None + else None + ), + august_metadata=( + DeviceAugustMetadata.from_dict(d.get("august_metadata")) + if d.get("august_metadata") is not None + else None + ), + avigilon_alta_metadata=( + DeviceAvigilonAltaMetadata.from_dict(d.get("avigilon_alta_metadata")) + if d.get("avigilon_alta_metadata") is not None + else None + ), + brivo_metadata=( + DeviceBrivoMetadata.from_dict(d.get("brivo_metadata")) + if d.get("brivo_metadata") is not None + else None + ), + controlbyweb_metadata=( + DeviceControlbywebMetadata.from_dict(d.get("controlbyweb_metadata")) + if d.get("controlbyweb_metadata") is not None + else None + ), + dormakaba_oracode_metadata=( + DeviceDormakabaOracodeMetadata.from_dict( + d.get("dormakaba_oracode_metadata") + ) + if d.get("dormakaba_oracode_metadata") is not None + else None + ), + ecobee_metadata=( + DeviceEcobeeMetadata.from_dict(d.get("ecobee_metadata")) + if d.get("ecobee_metadata") is not None + else None + ), + four_suites_metadata=( + DeviceFourSuitesMetadata.from_dict(d.get("four_suites_metadata")) + if d.get("four_suites_metadata") is not None + else None + ), + genie_metadata=( + DeviceGenieMetadata.from_dict(d.get("genie_metadata")) + if d.get("genie_metadata") is not None + else None + ), + honeywell_resideo_metadata=( + DeviceHoneywellResideoMetadata.from_dict( + d.get("honeywell_resideo_metadata") + ) + if d.get("honeywell_resideo_metadata") is not None + else None + ), + igloo_metadata=( + DeviceIglooMetadata.from_dict(d.get("igloo_metadata")) + if d.get("igloo_metadata") is not None + else None + ), + igloohome_metadata=( + DeviceIgloohomeMetadata.from_dict(d.get("igloohome_metadata")) + if d.get("igloohome_metadata") is not None + else None + ), + keynest_metadata=( + DeviceKeynestMetadata.from_dict(d.get("keynest_metadata")) + if d.get("keynest_metadata") is not None + else None + ), + kisi_metadata=( + DeviceKisiMetadata.from_dict(d.get("kisi_metadata")) + if d.get("kisi_metadata") is not None + else None + ), + korelock_metadata=( + DeviceKorelockMetadata.from_dict(d.get("korelock_metadata")) + if d.get("korelock_metadata") is not None + else None + ), + kwikset_metadata=( + DeviceKwiksetMetadata.from_dict(d.get("kwikset_metadata")) + if d.get("kwikset_metadata") is not None + else None + ), + lockly_metadata=( + DeviceLocklyMetadata.from_dict(d.get("lockly_metadata")) + if d.get("lockly_metadata") is not None + else None + ), + minut_metadata=( + DeviceMinutMetadata.from_dict(d.get("minut_metadata")) + if d.get("minut_metadata") is not None + else None + ), + nest_metadata=( + DeviceNestMetadata.from_dict(d.get("nest_metadata")) + if d.get("nest_metadata") is not None + else None + ), + noiseaware_metadata=( + DeviceNoiseawareMetadata.from_dict(d.get("noiseaware_metadata")) + if d.get("noiseaware_metadata") is not None + else None + ), + nuki_metadata=( + DeviceNukiMetadata.from_dict(d.get("nuki_metadata")) + if d.get("nuki_metadata") is not None + else None + ), + omnitec_metadata=( + DeviceOmnitecMetadata.from_dict(d.get("omnitec_metadata")) + if d.get("omnitec_metadata") is not None + else None + ), + ring_metadata=( + DeviceRingMetadata.from_dict(d.get("ring_metadata")) + if d.get("ring_metadata") is not None + else None + ), + salto_ks_metadata=( + DeviceSaltoKsMetadata.from_dict(d.get("salto_ks_metadata")) + if d.get("salto_ks_metadata") is not None + else None + ), + salto_metadata=( + DeviceSaltoMetadata.from_dict(d.get("salto_metadata")) + if d.get("salto_metadata") is not None + else None + ), + schlage_metadata=( + DeviceSchlageMetadata.from_dict(d.get("schlage_metadata")) + if d.get("schlage_metadata") is not None + else None + ), + seam_bridge_metadata=( + DeviceSeamBridgeMetadata.from_dict(d.get("seam_bridge_metadata")) + if d.get("seam_bridge_metadata") is not None + else None + ), + sensi_metadata=( + DeviceSensiMetadata.from_dict(d.get("sensi_metadata")) + if d.get("sensi_metadata") is not None + else None + ), + smartthings_metadata=( + DeviceSmartthingsMetadata.from_dict(d.get("smartthings_metadata")) + if d.get("smartthings_metadata") is not None + else None + ), + tado_metadata=( + DeviceTadoMetadata.from_dict(d.get("tado_metadata")) + if d.get("tado_metadata") is not None + else None + ), + tedee_metadata=( + DeviceTedeeMetadata.from_dict(d.get("tedee_metadata")) + if d.get("tedee_metadata") is not None + else None + ), + ttlock_metadata=( + DeviceTtlockMetadata.from_dict(d.get("ttlock_metadata")) + if d.get("ttlock_metadata") is not None + else None + ), + two_n_metadata=( + DeviceTwoNMetadata.from_dict(d.get("two_n_metadata")) + if d.get("two_n_metadata") is not None + else None + ), + ultraloq_metadata=( + DeviceUltraloqMetadata.from_dict(d.get("ultraloq_metadata")) + if d.get("ultraloq_metadata") is not None + else None + ), + visionline_metadata=( + DeviceVisionlineMetadata.from_dict(d.get("visionline_metadata")) + if d.get("visionline_metadata") is not None + else None + ), + wyze_metadata=( + DeviceWyzeMetadata.from_dict(d.get("wyze_metadata")) + if d.get("wyze_metadata") is not None + else None + ), + auto_lock_delay_seconds=d.get("auto_lock_delay_seconds", None), + auto_lock_enabled=d.get("auto_lock_enabled", None), + backup_access_code_pool_enabled=d.get( + "backup_access_code_pool_enabled", None + ), + code_constraints=[ + DeviceCodeConstraints.from_dict(i) + for i in d.get("code_constraints") or [] + ], + door_open=d.get("door_open", None), + has_native_entry_events=d.get("has_native_entry_events", None), + keypad_battery=( + DeviceKeypadBattery.from_dict(d.get("keypad_battery")) + if d.get("keypad_battery") is not None + else None + ), + locked=d.get("locked", None), + max_active_codes_supported=d.get("max_active_codes_supported", None), + offline_time_frame_options=[ + DeviceOfflineTimeFrameOptions.from_dict(i) + for i in d.get("offline_time_frame_options") or [] + ], + online_time_frame_options=[ + DeviceOnlineTimeFrameOptions.from_dict(i) + for i in d.get("online_time_frame_options") or [] + ], + supported_code_lengths=d.get("supported_code_lengths", None), + supports_backup_access_code_pool=d.get( + "supports_backup_access_code_pool", None + ), + active_thermostat_schedule=( + DeviceActiveThermostatSchedule.from_dict( + d.get("active_thermostat_schedule") + ) + if d.get("active_thermostat_schedule") is not None + else None + ), + active_thermostat_schedule_id=d.get("active_thermostat_schedule_id", None), + available_climate_preset_modes=d.get( + "available_climate_preset_modes", None + ), + available_climate_presets=[ + DeviceAvailableClimatePresets.from_dict(i) + for i in d.get("available_climate_presets") or [] + ], + available_fan_mode_settings=d.get("available_fan_mode_settings", None), + available_hvac_mode_settings=d.get("available_hvac_mode_settings", None), + current_climate_setting=( + DeviceCurrentClimateSetting.from_dict(d.get("current_climate_setting")) + if d.get("current_climate_setting") is not None + else None + ), + default_climate_setting=( + DeviceDefaultClimateSetting.from_dict(d.get("default_climate_setting")) + if d.get("default_climate_setting") is not None + else None + ), + fallback_climate_preset_key=d.get("fallback_climate_preset_key", None), + fan_mode_setting=d.get("fan_mode_setting", None), + is_cooling=d.get("is_cooling", None), + is_fan_running=d.get("is_fan_running", None), + is_heating=d.get("is_heating", None), + is_temporary_manual_override_active=d.get( + "is_temporary_manual_override_active", None + ), + max_cooling_set_point_celsius=d.get("max_cooling_set_point_celsius", None), + max_cooling_set_point_fahrenheit=d.get( + "max_cooling_set_point_fahrenheit", None + ), + max_heating_set_point_celsius=d.get("max_heating_set_point_celsius", None), + max_heating_set_point_fahrenheit=d.get( + "max_heating_set_point_fahrenheit", None + ), + max_thermostat_daily_program_periods_per_day=d.get( + "max_thermostat_daily_program_periods_per_day", None + ), + max_unique_climate_presets_per_thermostat_weekly_program=d.get( + "max_unique_climate_presets_per_thermostat_weekly_program", None + ), + min_cooling_set_point_celsius=d.get("min_cooling_set_point_celsius", None), + min_cooling_set_point_fahrenheit=d.get( + "min_cooling_set_point_fahrenheit", None + ), + min_heating_cooling_delta_celsius=d.get( + "min_heating_cooling_delta_celsius", None + ), + min_heating_cooling_delta_fahrenheit=d.get( + "min_heating_cooling_delta_fahrenheit", None + ), + min_heating_set_point_celsius=d.get("min_heating_set_point_celsius", None), + min_heating_set_point_fahrenheit=d.get( + "min_heating_set_point_fahrenheit", None + ), + relative_humidity=d.get("relative_humidity", None), + temperature_celsius=d.get("temperature_celsius", None), + temperature_fahrenheit=d.get("temperature_fahrenheit", None), + temperature_threshold=( + DeviceTemperatureThreshold.from_dict(d.get("temperature_threshold")) + if d.get("temperature_threshold") is not None + else None + ), + thermostat_daily_program_period_precision_minutes=d.get( + "thermostat_daily_program_period_precision_minutes", None + ), + thermostat_daily_programs=[ + DeviceThermostatDailyPrograms.from_dict(i) + for i in d.get("thermostat_daily_programs") or [] + ], + thermostat_weekly_program=( + DeviceThermostatWeeklyProgram.from_dict( + d.get("thermostat_weekly_program") + ) + if d.get("thermostat_weekly_program") is not None + else None + ), + ) + + +@dataclass +class DeviceWarnings(ResourceMapping): + """Array of warnings associated with the device. Each warning object within the array contains two fields: ``warning_code`` and ``message``. ``warning_code`` is a string that uniquely identifies the type of warning, enabling quick recognition and categorization of the issue. ``message`` provides a more detailed description of the warning, offering insights into the issue and potentially how to rectify it. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar active_access_code_count: Number of active access codes on the device when the warning was set. + + :ivar max_active_access_code_count: Maximum number of active access codes supported by the device. + """ + + created_at: str + message: str + warning_code: str + active_access_code_count: int + max_active_access_code_count: int + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + active_access_code_count=d.get("active_access_code_count", None), + max_active_access_code_count=d.get("max_active_access_code_count", None), + ) @dataclass @@ -107,22 +3103,22 @@ class Device: created_at: str custom_metadata: Dict[str, Any] device_id: str - device_manufacturer: Dict[str, Any] - device_provider: Dict[str, Any] + device_manufacturer: DeviceDeviceManufacturer + device_provider: DeviceDeviceProvider device_type: str display_name: str - errors: List[Dict[str, Any]] + errors: List[DeviceErrors] is_managed: bool - location: Dict[str, Any] + location: DeviceLocation nickname: str - properties: Dict[str, Any] + properties: DeviceProperties space_ids: List[str] - warnings: List[Dict[str, Any]] + warnings: List[DeviceWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return Device( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( can_configure_auto_lock=d.get("can_configure_auto_lock", None), can_hvac_cool=d.get("can_hvac_cool", None), can_hvac_heat=d.get("can_hvac_heat", None), @@ -162,16 +3158,32 @@ def from_dict(d: Dict[str, Any]): created_at=d.get("created_at", None), custom_metadata=DeepAttrDict(d.get("custom_metadata", None)), device_id=d.get("device_id", None), - device_manufacturer=DeepAttrDict(d.get("device_manufacturer", None)), - device_provider=DeepAttrDict(d.get("device_provider", None)), + device_manufacturer=( + DeviceDeviceManufacturer.from_dict(d.get("device_manufacturer")) + if d.get("device_manufacturer") is not None + else None + ), + device_provider=( + DeviceDeviceProvider.from_dict(d.get("device_provider")) + if d.get("device_provider") is not None + else None + ), device_type=d.get("device_type", None), display_name=d.get("display_name", None), - errors=d.get("errors", None), + errors=[DeviceErrors.from_dict(i) for i in d.get("errors") or []], is_managed=d.get("is_managed", None), - location=DeepAttrDict(d.get("location", None)), + location=( + DeviceLocation.from_dict(d.get("location")) + if d.get("location") is not None + else None + ), nickname=d.get("nickname", None), - properties=DeepAttrDict(d.get("properties", None)), + properties=( + DeviceProperties.from_dict(d.get("properties")) + if d.get("properties") is not None + else None + ), space_ids=d.get("space_ids", None), - warnings=d.get("warnings", None), + warnings=[DeviceWarnings.from_dict(i) for i in d.get("warnings") or []], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/device_provider.py b/seam/resources/device_provider.py index 1097d964..2b498134 100644 --- a/seam/resources/device_provider.py +++ b/seam/resources/device_provider.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping @dataclass @@ -81,9 +82,9 @@ class DeviceProvider: image_url: str provider_categories: List[str] - @staticmethod - def from_dict(d: Dict[str, Any]): - return DeviceProvider( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( can_configure_auto_lock=d.get("can_configure_auto_lock", None), can_hvac_cool=d.get("can_hvac_cool", None), can_hvac_heat=d.get("can_hvac_heat", None), diff --git a/seam/resources/instant_key.py b/seam/resources/instant_key.py index a6350694..422cf765 100644 --- a/seam/resources/instant_key.py +++ b/seam/resources/instant_key.py @@ -1,6 +1,30 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class InstantKeyCustomization(ResourceMapping): + """Customization applied to the Instant Key UI. + + :ivar logo_url: URL of the logo displayed on the Instant Key. + + :ivar primary_color: Primary color used in the Instant Key UI. + + :ivar secondary_color: Secondary color used in the Instant Key UI.""" + + logo_url: str + primary_color: str + secondary_color: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + logo_url=d.get("logo_url", None), + primary_color=d.get("primary_color", None), + secondary_color=d.get("secondary_color", None), + ) @dataclass @@ -29,7 +53,7 @@ class InstantKey: client_session_id: str created_at: str - customization: Dict[str, Any] + customization: InstantKeyCustomization customization_profile_id: str expires_at: str instant_key_id: str @@ -37,12 +61,16 @@ class InstantKey: user_identity_id: str workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return InstantKey( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( client_session_id=d.get("client_session_id", None), created_at=d.get("created_at", None), - customization=DeepAttrDict(d.get("customization", None)), + customization=( + InstantKeyCustomization.from_dict(d.get("customization")) + if d.get("customization") is not None + else None + ), customization_profile_id=d.get("customization_profile_id", None), expires_at=d.get("expires_at", None), instant_key_id=d.get("instant_key_id", None), diff --git a/seam/resources/noise_threshold.py b/seam/resources/noise_threshold.py index 0c8eee12..e0eee750 100644 --- a/seam/resources/noise_threshold.py +++ b/seam/resources/noise_threshold.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping @dataclass @@ -30,9 +31,9 @@ class NoiseThreshold: noise_threshold_nrs: float starts_daily_at: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return NoiseThreshold( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( device_id=d.get("device_id", None), ends_daily_at=d.get("ends_daily_at", None), name=d.get("name", None), diff --git a/seam/resources/pagination.py b/seam/resources/pagination.py index 164cac47..a2180092 100644 --- a/seam/resources/pagination.py +++ b/seam/resources/pagination.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping @dataclass @@ -17,9 +18,9 @@ class Pagination: next_page_cursor: str next_page_url: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return Pagination( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( has_next_page=d.get("has_next_page", None), next_page_cursor=d.get("next_page_cursor", None), next_page_url=d.get("next_page_url", None), diff --git a/seam/resources/phone.py b/seam/resources/phone.py index a9830ef6..f8e7d43d 100644 --- a/seam/resources/phone.py +++ b/seam/resources/phone.py @@ -1,6 +1,140 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class PhoneErrors(ResourceMapping): + """Errors associated with the phone. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. + + :ivar message: Detailed description of the error.""" + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class PhoneEndpoints(ResourceMapping): + """Endpoints associated with the phone. + + :ivar endpoint_id: ID of the associated endpoint. + + :ivar is_active: Indicated whether the endpoint is active.""" + + endpoint_id: str + is_active: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + endpoint_id=d.get("endpoint_id", None), + is_active=d.get("is_active", None), + ) + + +@dataclass +class PhoneAssaAbloyCredentialServiceMetadata(ResourceMapping): + """ASSA ABLOY Credential Service metadata for the phone. + + :ivar endpoints: Endpoints associated with the phone. + + :ivar has_active_endpoint: Indicates whether the credential service has active endpoints associated with the phone. + """ + + endpoints: List[PhoneEndpoints] + has_active_endpoint: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + endpoints=[PhoneEndpoints.from_dict(i) for i in d.get("endpoints") or []], + has_active_endpoint=d.get("has_active_endpoint", None), + ) + + +@dataclass +class PhoneSaltoSpaceCredentialServiceMetadata(ResourceMapping): + """Salto Space credential service metadata for the phone. + + :ivar has_active_phone: Indicates whether the credential service has an active associated phone. + """ + + has_active_phone: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + has_active_phone=d.get("has_active_phone", None), + ) + + +@dataclass +class PhoneProperties(ResourceMapping): + """Properties of the phone. + + :ivar assa_abloy_credential_service_metadata: ASSA ABLOY Credential Service metadata for the phone. + + :ivar salto_space_credential_service_metadata: Salto Space credential service metadata for the phone. + """ + + assa_abloy_credential_service_metadata: PhoneAssaAbloyCredentialServiceMetadata + salto_space_credential_service_metadata: PhoneSaltoSpaceCredentialServiceMetadata + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + assa_abloy_credential_service_metadata=( + PhoneAssaAbloyCredentialServiceMetadata.from_dict( + d.get("assa_abloy_credential_service_metadata") + ) + if d.get("assa_abloy_credential_service_metadata") is not None + else None + ), + salto_space_credential_service_metadata=( + PhoneSaltoSpaceCredentialServiceMetadata.from_dict( + d.get("salto_space_credential_service_metadata") + ) + if d.get("salto_space_credential_service_metadata") is not None + else None + ), + ) + + +@dataclass +class PhoneWarnings(ResourceMapping): + """Warnings associated with the phone. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. + + :ivar warning_code: Unique identifier of the type of warning.""" + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) @dataclass @@ -32,23 +166,27 @@ class Phone: device_id: str device_type: str display_name: str - errors: List[Dict[str, Any]] + errors: List[PhoneErrors] nickname: str - properties: Dict[str, Any] - warnings: List[Dict[str, Any]] + properties: PhoneProperties + warnings: List[PhoneWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return Phone( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( created_at=d.get("created_at", None), custom_metadata=DeepAttrDict(d.get("custom_metadata", None)), device_id=d.get("device_id", None), device_type=d.get("device_type", None), display_name=d.get("display_name", None), - errors=d.get("errors", None), + errors=[PhoneErrors.from_dict(i) for i in d.get("errors") or []], nickname=d.get("nickname", None), - properties=DeepAttrDict(d.get("properties", None)), - warnings=d.get("warnings", None), + properties=( + PhoneProperties.from_dict(d.get("properties")) + if d.get("properties") is not None + else None + ), + warnings=[PhoneWarnings.from_dict(i) for i in d.get("warnings") or []], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/seam_event.py b/seam/resources/seam_event.py index 6d851989..88dc7a79 100644 --- a/seam/resources/seam_event.py +++ b/seam/resources/seam_event.py @@ -1,6 +1,296 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class SeamEventChangedProperties(ResourceMapping): + """List of properties that changed on the access code. + + :ivar from_: Previous value of the property, or null if not set. + + :ivar property: Name of the property that changed (e.g. ``code``). + + :ivar to: New value of the property, or null if cleared.""" + + from_: str + property: str + to: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + from_=d.get("from", None), + property=d.get("property", None), + to=d.get("to", None), + ) + + +@dataclass +class SeamEventFrom(ResourceMapping): + """Previous access code name configuration. + + :ivar name: Previous name of the access code.""" + + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + name=d.get("name", None), + ) + + +@dataclass +class SeamEventTo(ResourceMapping): + """New access code name configuration. + + :ivar name: New name of the access code.""" + + name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + name=d.get("name", None), + ) + + +@dataclass +class SeamEventRequestedMutations(ResourceMapping): + """Array of mutations requested on the access code, each containing the mutation type and from/to values. + + :ivar from_: Previous property values before the requested change. Keys depend on the mutation type. Absent for non-property mutations like ``deleting``. + + :ivar mutation_code: Code identifying the type of mutation requested, such as ``updating_name``, ``updating_code``, ``updating_time_frame``, or ``deleting``. + + :ivar to: New property values after the requested change. Keys depend on the mutation type. Absent for non-property mutations like ``deleting``. + """ + + from_: Dict[str, Any] + mutation_code: str + to: Dict[str, Any] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + from_=DeepAttrDict(d.get("from", None)), + mutation_code=d.get("mutation_code", None), + to=DeepAttrDict(d.get("to", None)), + ) + + +@dataclass +class SeamEventAccessCodeErrors(ResourceMapping): + """Errors associated with the access code. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class SeamEventAccessCodeWarnings(ResourceMapping): + """Warnings associated with the access code. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) + + +@dataclass +class SeamEventConnectedAccountErrors(ResourceMapping): + """Errors associated with the connected account. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class SeamEventConnectedAccountWarnings(ResourceMapping): + """Warnings associated with the connected account. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) + + +@dataclass +class SeamEventDeviceErrors(ResourceMapping): + """Errors associated with the device. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class SeamEventDeviceWarnings(ResourceMapping): + """Warnings associated with the device. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) + + +@dataclass +class SeamEventAcsSystemErrors(ResourceMapping): + """Errors associated with the access control system. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class SeamEventAcsSystemWarnings(ResourceMapping): + """Warnings associated with the access control system. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) + + +@dataclass +class SeamEventReason(ResourceMapping): + """Why access was denied, when the provider reports a determinable cause. Omitted when unknown. + + :ivar message: Human-readable explanation of why access was denied. + + :ivar reason_code: Normalized reason a lock denied access. Provider-agnostic; not all providers report every value. + """ + + message: str + reason_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + message=d.get("message", None), + reason_code=d.get("reason_code", None), + ) @dataclass @@ -203,18 +493,18 @@ class SeamEvent: occurred_at: str workspace_id: str change_reason: str - changed_properties: List[Dict[str, Any]] + changed_properties: List[SeamEventChangedProperties] description: str - from_: Dict[str, Any] - to: Dict[str, Any] - requested_mutations: List[Dict[str, Any]] + from_: SeamEventFrom + to: SeamEventTo + requested_mutations: List[SeamEventRequestedMutations] code: str - access_code_errors: List[Dict[str, Any]] - access_code_warnings: List[Dict[str, Any]] - connected_account_errors: List[Dict[str, Any]] - connected_account_warnings: List[Dict[str, Any]] - device_errors: List[Dict[str, Any]] - device_warnings: List[Dict[str, Any]] + access_code_errors: List[SeamEventAccessCodeErrors] + access_code_warnings: List[SeamEventAccessCodeWarnings] + connected_account_errors: List[SeamEventConnectedAccountErrors] + connected_account_warnings: List[SeamEventConnectedAccountWarnings] + device_errors: List[SeamEventDeviceErrors] + device_warnings: List[SeamEventDeviceWarnings] backup_access_code_id: str access_grant_id: str acs_entrance_id: str @@ -228,8 +518,8 @@ class SeamEvent: access_method_id: str is_backup_code: bool acs_system_id: str - acs_system_errors: List[Dict[str, Any]] - acs_system_warnings: List[Dict[str, Any]] + acs_system_errors: List[SeamEventAcsSystemErrors] + acs_system_warnings: List[SeamEventAcsSystemWarnings] acs_credential_id: str acs_user_id: str acs_encoder_id: str @@ -256,7 +546,7 @@ class SeamEvent: is_via_nfc: bool method: str user_identity_id: str - reason: Dict[str, Any] + reason: SeamEventReason climate_preset_key: str is_fallback_climate_preset: bool thermostat_schedule_id: str @@ -283,9 +573,9 @@ class SeamEvent: space_id: str space_key: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return SeamEvent( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_code_id=d.get("access_code_id", None), connected_account_custom_metadata=DeepAttrDict( d.get("connected_account_custom_metadata", None) @@ -300,18 +590,45 @@ def from_dict(d: Dict[str, Any]): occurred_at=d.get("occurred_at", None), workspace_id=d.get("workspace_id", None), change_reason=d.get("change_reason", None), - changed_properties=d.get("changed_properties", None), + changed_properties=[ + SeamEventChangedProperties.from_dict(i) + for i in d.get("changed_properties") or [] + ], description=d.get("description", None), - from_=DeepAttrDict(d.get("from", None)), - to=DeepAttrDict(d.get("to", None)), - requested_mutations=d.get("requested_mutations", None), + from_=( + SeamEventFrom.from_dict(d.get("from")) + if d.get("from") is not None + else None + ), + to=SeamEventTo.from_dict(d.get("to")) if d.get("to") is not None else None, + requested_mutations=[ + SeamEventRequestedMutations.from_dict(i) + for i in d.get("requested_mutations") or [] + ], code=d.get("code", None), - access_code_errors=d.get("access_code_errors", None), - access_code_warnings=d.get("access_code_warnings", None), - connected_account_errors=d.get("connected_account_errors", None), - connected_account_warnings=d.get("connected_account_warnings", None), - device_errors=d.get("device_errors", None), - device_warnings=d.get("device_warnings", None), + access_code_errors=[ + SeamEventAccessCodeErrors.from_dict(i) + for i in d.get("access_code_errors") or [] + ], + access_code_warnings=[ + SeamEventAccessCodeWarnings.from_dict(i) + for i in d.get("access_code_warnings") or [] + ], + connected_account_errors=[ + SeamEventConnectedAccountErrors.from_dict(i) + for i in d.get("connected_account_errors") or [] + ], + connected_account_warnings=[ + SeamEventConnectedAccountWarnings.from_dict(i) + for i in d.get("connected_account_warnings") or [] + ], + device_errors=[ + SeamEventDeviceErrors.from_dict(i) for i in d.get("device_errors") or [] + ], + device_warnings=[ + SeamEventDeviceWarnings.from_dict(i) + for i in d.get("device_warnings") or [] + ], backup_access_code_id=d.get("backup_access_code_id", None), access_grant_id=d.get("access_grant_id", None), acs_entrance_id=d.get("acs_entrance_id", None), @@ -325,8 +642,14 @@ def from_dict(d: Dict[str, Any]): access_method_id=d.get("access_method_id", None), is_backup_code=d.get("is_backup_code", None), acs_system_id=d.get("acs_system_id", None), - acs_system_errors=d.get("acs_system_errors", None), - acs_system_warnings=d.get("acs_system_warnings", None), + acs_system_errors=[ + SeamEventAcsSystemErrors.from_dict(i) + for i in d.get("acs_system_errors") or [] + ], + acs_system_warnings=[ + SeamEventAcsSystemWarnings.from_dict(i) + for i in d.get("acs_system_warnings") or [] + ], acs_credential_id=d.get("acs_credential_id", None), acs_user_id=d.get("acs_user_id", None), acs_encoder_id=d.get("acs_encoder_id", None), @@ -353,7 +676,11 @@ def from_dict(d: Dict[str, Any]): is_via_nfc=d.get("is_via_nfc", None), method=d.get("method", None), user_identity_id=d.get("user_identity_id", None), - reason=DeepAttrDict(d.get("reason", None)), + reason=( + SeamEventReason.from_dict(d.get("reason")) + if d.get("reason") is not None + else None + ), climate_preset_key=d.get("climate_preset_key", None), is_fallback_climate_preset=d.get("is_fallback_climate_preset", None), thermostat_schedule_id=d.get("thermostat_schedule_id", None), diff --git a/seam/resources/space.py b/seam/resources/space.py index 4eaa9017..1dbc7d7b 100644 --- a/seam/resources/space.py +++ b/seam/resources/space.py @@ -1,6 +1,53 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class SpaceCustomerData(ResourceMapping): + """Reservation/stay-related defaults for the space. Also carries the provider/PMS-supplied name under a ``_name`` key (e.g. ``guesty_name``), which Seam preserves when you rename the space (read-only — managed by Seam). + + :ivar address: Postal address for the space. + + :ivar default_checkin_time: Default check-in time for reservations at the space, as HH:mm or HH:mm:ss. + + :ivar default_checkout_time: Default check-out time for reservations at the space, as HH:mm or HH:mm:ss. + + :ivar time_zone: IANA time zone for the space, e.g. America/Los_Angeles.""" + + address: str + default_checkin_time: str + default_checkout_time: str + time_zone: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + address=d.get("address", None), + default_checkin_time=d.get("default_checkin_time", None), + default_checkout_time=d.get("default_checkout_time", None), + time_zone=d.get("time_zone", None), + ) + + +@dataclass +class SpaceGeolocation(ResourceMapping): + """Geographic coordinates (latitude and longitude) of the space. + + :ivar latitude: Latitude of the space, in decimal degrees. + + :ivar longitude: Longitude of the space, in decimal degrees.""" + + latitude: float + longitude: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + latitude=d.get("latitude", None), + longitude=d.get("longitude", None), + ) @dataclass @@ -31,26 +78,34 @@ class Space: acs_entrance_count: float created_at: str - customer_data: Dict[str, Any] + customer_data: SpaceCustomerData customer_key: str device_count: float display_name: str - geolocation: Dict[str, Any] + geolocation: SpaceGeolocation name: str space_id: str space_key: str workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return Space( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( acs_entrance_count=d.get("acs_entrance_count", None), created_at=d.get("created_at", None), - customer_data=DeepAttrDict(d.get("customer_data", None)), + customer_data=( + SpaceCustomerData.from_dict(d.get("customer_data")) + if d.get("customer_data") is not None + else None + ), customer_key=d.get("customer_key", None), device_count=d.get("device_count", None), display_name=d.get("display_name", None), - geolocation=DeepAttrDict(d.get("geolocation", None)), + geolocation=( + SpaceGeolocation.from_dict(d.get("geolocation")) + if d.get("geolocation") is not None + else None + ), name=d.get("name", None), space_id=d.get("space_id", None), space_key=d.get("space_key", None), diff --git a/seam/resources/thermostat_daily_program.py b/seam/resources/thermostat_daily_program.py index dbc703f5..00dab4de 100644 --- a/seam/resources/thermostat_daily_program.py +++ b/seam/resources/thermostat_daily_program.py @@ -1,6 +1,27 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class ThermostatDailyProgramPeriods(ResourceMapping): + """Array of thermostat daily program periods. + + :ivar climate_preset_key: Key of the `climate preset `_ to activate at the ``starts_at_time``. + + :ivar starts_at_time: Time at which the thermostat daily program period starts, in `ISO 8601 `_ format. + """ + + climate_preset_key: str + starts_at_time: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + climate_preset_key=d.get("climate_preset_key", None), + starts_at_time=d.get("starts_at_time", None), + ) @dataclass @@ -23,17 +44,20 @@ class ThermostatDailyProgram: created_at: str device_id: str name: str - periods: List[Dict[str, Any]] + periods: List[ThermostatDailyProgramPeriods] thermostat_daily_program_id: str workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return ThermostatDailyProgram( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( created_at=d.get("created_at", None), device_id=d.get("device_id", None), name=d.get("name", None), - periods=d.get("periods", None), + periods=[ + ThermostatDailyProgramPeriods.from_dict(i) + for i in d.get("periods") or [] + ], thermostat_daily_program_id=d.get("thermostat_daily_program_id", None), workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/thermostat_schedule.py b/seam/resources/thermostat_schedule.py index 13d9ae96..c7adbbc2 100644 --- a/seam/resources/thermostat_schedule.py +++ b/seam/resources/thermostat_schedule.py @@ -1,6 +1,31 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class ThermostatScheduleErrors(ResourceMapping): + """Errors associated with the `thermostat schedule `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) @dataclass @@ -33,7 +58,7 @@ class ThermostatSchedule: created_at: str device_id: str ends_at: str - errors: List[Dict[str, Any]] + errors: List[ThermostatScheduleErrors] is_override_allowed: bool max_override_period_minutes: int name: str @@ -41,14 +66,16 @@ class ThermostatSchedule: thermostat_schedule_id: str workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return ThermostatSchedule( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( climate_preset_key=d.get("climate_preset_key", None), created_at=d.get("created_at", None), device_id=d.get("device_id", None), ends_at=d.get("ends_at", None), - errors=d.get("errors", None), + errors=[ + ThermostatScheduleErrors.from_dict(i) for i in d.get("errors") or [] + ], is_override_allowed=d.get("is_override_allowed", None), max_override_period_minutes=d.get("max_override_period_minutes", None), name=d.get("name", None), diff --git a/seam/resources/unmanaged_access_code.py b/seam/resources/unmanaged_access_code.py index 84b43d60..4a27e11c 100644 --- a/seam/resources/unmanaged_access_code.py +++ b/seam/resources/unmanaged_access_code.py @@ -1,6 +1,168 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class UnmanagedAccessCodeDormakabaOracodeMetadata(ResourceMapping): + """Metadata for a dormakaba Oracode unmanaged access code. Only present for unmanaged access codes from dormakaba Oracode devices. + + :ivar is_cancellable: Indicates whether the stay can be cancelled via the Dormakaba Oracode API. + + :ivar is_early_checkin_able: Indicates whether early check-in is available for this stay. + + :ivar is_extendable: Indicates whether the stay can be extended via the Dormakaba Oracode API. + + :ivar is_overridable: Indicates whether the access code can be overridden. When false, the maximum number of overrides has been reached. + + :ivar site_name: Dormakaba Oracode site name associated with this access code. + + :ivar stay_id: Dormakaba Oracode stay ID associated with this access code. + + :ivar user_level_id: Dormakaba Oracode user level ID associated with this access code. + + :ivar user_level_name: Dormakaba Oracode user level name associated with this access code. + """ + + is_cancellable: bool + is_early_checkin_able: bool + is_extendable: bool + is_overridable: bool + site_name: str + stay_id: float + user_level_id: str + user_level_name: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + is_cancellable=d.get("is_cancellable", None), + is_early_checkin_able=d.get("is_early_checkin_able", None), + is_extendable=d.get("is_extendable", None), + is_overridable=d.get("is_overridable", None), + site_name=d.get("site_name", None), + stay_id=d.get("stay_id", None), + user_level_id=d.get("user_level_id", None), + user_level_name=d.get("user_level_name", None), + ) + + +@dataclass +class UnmanagedAccessCodeModifiedFields(ResourceMapping): + """List of fields that were changed externally, with their previous and new values. + + :ivar field: The name of the field that was changed (e.g. ``code``, ``starts_at``, ``ends_at``). + + :ivar from_: The previous value of the field. + + :ivar to: The new value of the field.""" + + field: str + from_: str + to: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + field=d.get("field", None), + from_=d.get("from", None), + to=d.get("to", None), + ) + + +@dataclass +class UnmanagedAccessCodeErrors(ResourceMapping): + """Errors associated with the `access code `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar is_access_code_error: Indicates that this is an access code error. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar managed_access_code_id: ID of the managed access code that conflicts with this managed access code, when Seam can identify it. + + :ivar unmanaged_access_code_id: ID of the unmanaged access code that conflicts with this managed access code, when Seam can identify it. + + :ivar change_type: Indicates the type of external modification. ``modified`` means the code's PIN or schedule was changed. ``removed`` means the code was deleted from the device. + + :ivar modified_fields: List of fields that were changed externally, with their previous and new values. + + :ivar is_connected_account_error: Indicates that the error is a `connected account `_ error. + + :ivar is_device_error: Indicates that the error is not a device error. + + :ivar is_bridge_error: Indicates whether the error is related to `Seam Bridge `_. + """ + + created_at: str + error_code: str + is_access_code_error: bool + message: str + managed_access_code_id: str + unmanaged_access_code_id: str + change_type: str + modified_fields: List[UnmanagedAccessCodeModifiedFields] + is_connected_account_error: bool + is_device_error: bool + is_bridge_error: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + is_access_code_error=d.get("is_access_code_error", None), + message=d.get("message", None), + managed_access_code_id=d.get("managed_access_code_id", None), + unmanaged_access_code_id=d.get("unmanaged_access_code_id", None), + change_type=d.get("change_type", None), + modified_fields=[ + UnmanagedAccessCodeModifiedFields.from_dict(i) + for i in d.get("modified_fields") or [] + ], + is_connected_account_error=d.get("is_connected_account_error", None), + is_device_error=d.get("is_device_error", None), + is_bridge_error=d.get("is_bridge_error", None), + ) + + +@dataclass +class UnmanagedAccessCodeWarnings(ResourceMapping): + """Warnings associated with the `access code `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar change_type: Indicates the type of external modification. ``modified`` means the code's PIN or schedule was changed. ``removed`` means the code was deleted from the device. + + :ivar modified_fields: List of fields that were changed externally, with their previous and new values. + """ + + created_at: str + message: str + warning_code: str + change_type: str + modified_fields: List[UnmanagedAccessCodeModifiedFields] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + change_type=d.get("change_type", None), + modified_fields=[ + UnmanagedAccessCodeModifiedFields.from_dict(i) + for i in d.get("modified_fields") or [] + ], + ) @dataclass @@ -56,20 +218,20 @@ class UnmanagedAccessCode: code: str created_at: str device_id: str - dormakaba_oracode_metadata: Dict[str, Any] + dormakaba_oracode_metadata: UnmanagedAccessCodeDormakabaOracodeMetadata ends_at: str - errors: List[Dict[str, Any]] + errors: List[UnmanagedAccessCodeErrors] is_managed: bool name: str starts_at: str status: str type: str - warnings: List[Dict[str, Any]] + warnings: List[UnmanagedAccessCodeWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return UnmanagedAccessCode( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_code_id=d.get("access_code_id", None), cannot_be_managed=d.get("cannot_be_managed", None), cannot_delete_unmanaged_access_code=d.get( @@ -78,16 +240,25 @@ def from_dict(d: Dict[str, Any]): code=d.get("code", None), created_at=d.get("created_at", None), device_id=d.get("device_id", None), - dormakaba_oracode_metadata=DeepAttrDict( - d.get("dormakaba_oracode_metadata", None) + dormakaba_oracode_metadata=( + UnmanagedAccessCodeDormakabaOracodeMetadata.from_dict( + d.get("dormakaba_oracode_metadata") + ) + if d.get("dormakaba_oracode_metadata") is not None + else None ), ends_at=d.get("ends_at", None), - errors=d.get("errors", None), + errors=[ + UnmanagedAccessCodeErrors.from_dict(i) for i in d.get("errors") or [] + ], is_managed=d.get("is_managed", None), name=d.get("name", None), starts_at=d.get("starts_at", None), status=d.get("status", None), type=d.get("type", None), - warnings=d.get("warnings", None), + warnings=[ + UnmanagedAccessCodeWarnings.from_dict(i) + for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/unmanaged_access_grant.py b/seam/resources/unmanaged_access_grant.py index 9d2496d6..eb79a1c5 100644 --- a/seam/resources/unmanaged_access_grant.py +++ b/seam/resources/unmanaged_access_grant.py @@ -1,6 +1,222 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class UnmanagedAccessGrantErrors(ResourceMapping): + """Errors associated with the `access grant `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar missing_device_ids: IDs of the devices that did not receive an access code at grant creation. Use these to identify which specific devices failed when the message reports a partial failure. + """ + + created_at: str + error_code: str + message: str + missing_device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + missing_device_ids=d.get("missing_device_ids", None), + ) + + +@dataclass +class UnmanagedAccessGrantFrom(ResourceMapping): + """Previous location configuration. + + :ivar device_ids: Previous device IDs where access codes existed.""" + + device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_ids=d.get("device_ids", None), + ) + + +@dataclass +class UnmanagedAccessGrantTo(ResourceMapping): + """New location configuration. + + :ivar common_code_key: Common code key to ensure PIN code reuse across devices. + + :ivar device_ids: New device IDs where access codes should be created.""" + + common_code_key: str + device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + common_code_key=d.get("common_code_key", None), + device_ids=d.get("device_ids", None), + ) + + +@dataclass +class UnmanagedAccessGrantPendingMutations(ResourceMapping): + """List of pending mutations for the access grant. This shows updates that are in progress. + + :ivar created_at: Date and time at which the mutation was created. + + :ivar from_: Previous location configuration. + + :ivar message: Detailed description of the mutation. + + :ivar mutation_code: Mutation code to indicate that Seam is in the process of updating the spaces (devices) associated with this access grant. + + :ivar to: New location configuration. + + :ivar access_method_ids: IDs of the access methods being updated.""" + + created_at: str + from_: UnmanagedAccessGrantFrom + message: str + mutation_code: str + to: UnmanagedAccessGrantTo + access_method_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + from_=( + UnmanagedAccessGrantFrom.from_dict(d.get("from")) + if d.get("from") is not None + else None + ), + message=d.get("message", None), + mutation_code=d.get("mutation_code", None), + to=( + UnmanagedAccessGrantTo.from_dict(d.get("to")) + if d.get("to") is not None + else None + ), + access_method_ids=d.get("access_method_ids", None), + ) + + +@dataclass +class UnmanagedAccessGrantRequestedAccessMethods(ResourceMapping): + """Access methods that the user requested for the Access Grant. + + :ivar code: Specific PIN code to use for this access method. Only applicable when mode is 'code'. + + :ivar created_access_method_ids: IDs of the access methods created for the requested access method. + + :ivar created_at: Date and time at which the requested access method was added to the Access Grant. + + :ivar display_name: Display name of the access method. + + :ivar instant_key_max_use_count: Maximum number of times the instant key can be used. Only applicable when mode is 'mobile_key'. Defaults to 1 if not specified. + + :ivar mode: Access method mode. Supported values: ``code``, ``card``, ``mobile_key``, ``cloud_key``. + """ + + code: str + created_access_method_ids: List[str] + created_at: str + display_name: str + instant_key_max_use_count: int + mode: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + code=d.get("code", None), + created_access_method_ids=d.get("created_access_method_ids", None), + created_at=d.get("created_at", None), + display_name=d.get("display_name", None), + instant_key_max_use_count=d.get("instant_key_max_use_count", None), + mode=d.get("mode", None), + ) + + +@dataclass +class UnmanagedAccessGrantFailedDevices(ResourceMapping): + """Devices whose access codes could not be revoked during reconciliation. Present when the provider does not support revoking an offline access code (e.g. Dormakaba oracode with exhausted override budget). + + :ivar device_id: Device whose access code could not be revoked. + + :ivar error_code: Reason the access code could not be revoked (e.g. ``offline_access_code_not_revocable``). + + :ivar message: Human-readable description of why revocation failed.""" + + device_id: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_id=d.get("device_id", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class UnmanagedAccessGrantWarnings(ResourceMapping): + """Warnings associated with the `access grant `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar failed_devices: Devices whose access codes could not be revoked during reconciliation. Present when the provider does not support revoking an offline access code (e.g. Dormakaba oracode with exhausted override budget). + + :ivar access_method_ids: IDs of the access methods being updated. + + :ivar device_id: ID of the device where the requested code was unavailable. + + :ivar new_code: The new PIN code that was assigned instead. + + :ivar original_code: The originally requested PIN code that was unavailable. + + :ivar reason: Specific reason why the grant's times are not programmable on the device. + """ + + created_at: str + message: str + warning_code: str + failed_devices: List[UnmanagedAccessGrantFailedDevices] + access_method_ids: List[str] + device_id: str + new_code: str + original_code: str + reason: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + failed_devices=[ + UnmanagedAccessGrantFailedDevices.from_dict(i) + for i in d.get("failed_devices") or [] + ], + access_method_ids=d.get("access_method_ids", None), + device_id=d.get("device_id", None), + new_code=d.get("new_code", None), + original_code=d.get("original_code", None), + reason=d.get("reason", None), + ) @dataclass @@ -44,35 +260,46 @@ class UnmanagedAccessGrant: created_at: str display_name: str ends_at: str - errors: List[Dict[str, Any]] + errors: List[UnmanagedAccessGrantErrors] location_ids: List[str] name: str - pending_mutations: List[Dict[str, Any]] - requested_access_methods: List[Dict[str, Any]] + pending_mutations: List[UnmanagedAccessGrantPendingMutations] + requested_access_methods: List[UnmanagedAccessGrantRequestedAccessMethods] reservation_key: str space_ids: List[str] starts_at: str user_identity_id: str - warnings: List[Dict[str, Any]] + warnings: List[UnmanagedAccessGrantWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return UnmanagedAccessGrant( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_grant_id=d.get("access_grant_id", None), access_method_ids=d.get("access_method_ids", None), created_at=d.get("created_at", None), display_name=d.get("display_name", None), ends_at=d.get("ends_at", None), - errors=d.get("errors", None), + errors=[ + UnmanagedAccessGrantErrors.from_dict(i) for i in d.get("errors") or [] + ], location_ids=d.get("location_ids", None), name=d.get("name", None), - pending_mutations=d.get("pending_mutations", None), - requested_access_methods=d.get("requested_access_methods", None), + pending_mutations=[ + UnmanagedAccessGrantPendingMutations.from_dict(i) + for i in d.get("pending_mutations") or [] + ], + requested_access_methods=[ + UnmanagedAccessGrantRequestedAccessMethods.from_dict(i) + for i in d.get("requested_access_methods") or [] + ], reservation_key=d.get("reservation_key", None), space_ids=d.get("space_ids", None), starts_at=d.get("starts_at", None), user_identity_id=d.get("user_identity_id", None), - warnings=d.get("warnings", None), + warnings=[ + UnmanagedAccessGrantWarnings.from_dict(i) + for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/unmanaged_access_method.py b/seam/resources/unmanaged_access_method.py index 1ab0aa80..bd1dc63e 100644 --- a/seam/resources/unmanaged_access_method.py +++ b/seam/resources/unmanaged_access_method.py @@ -1,6 +1,128 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class UnmanagedAccessMethodErrors(ResourceMapping): + """Errors associated with the `access method `_. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class UnmanagedAccessMethodFrom(ResourceMapping): + """Previous device configuration. + + :ivar device_ids: Previous device IDs where access was provisioned.""" + + device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_ids=d.get("device_ids", None), + ) + + +@dataclass +class UnmanagedAccessMethodTo(ResourceMapping): + """New device configuration. + + :ivar device_ids: New device IDs where access is being provisioned.""" + + device_ids: List[str] + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + device_ids=d.get("device_ids", None), + ) + + +@dataclass +class UnmanagedAccessMethodPendingMutations(ResourceMapping): + """Pending mutations for the `access method `_. Indicates operations that are in progress. + + :ivar created_at: Date and time at which the mutation was created. + + :ivar from_: Previous device configuration. + + :ivar message: Detailed description of the mutation. + + :ivar mutation_code: Mutation code to indicate that Seam is in the process of provisioning access for this access method on new devices. + + :ivar to: New device configuration.""" + + created_at: str + from_: UnmanagedAccessMethodFrom + message: str + mutation_code: str + to: UnmanagedAccessMethodTo + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + from_=( + UnmanagedAccessMethodFrom.from_dict(d.get("from")) + if d.get("from") is not None + else None + ), + message=d.get("message", None), + mutation_code=d.get("mutation_code", None), + to=( + UnmanagedAccessMethodTo.from_dict(d.get("to")) + if d.get("to") is not None + else None + ), + ) + + +@dataclass +class UnmanagedAccessMethodWarnings(ResourceMapping): + """Warnings associated with the `access method `_. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar original_access_method_id: ID of the original access method from which this backup access method was split, if applicable. + """ + + created_at: str + message: str + warning_code: str + original_access_method_id: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + original_access_method_id=d.get("original_access_method_id", None), + ) @dataclass @@ -41,7 +163,7 @@ class UnmanagedAccessMethod: code: str created_at: str display_name: str - errors: List[Dict[str, Any]] + errors: List[UnmanagedAccessMethodErrors] is_assignment_required: bool is_encoding_required: bool is_issued: bool @@ -49,18 +171,20 @@ class UnmanagedAccessMethod: is_ready_for_encoding: bool issued_at: str mode: str - pending_mutations: List[Dict[str, Any]] - warnings: List[Dict[str, Any]] + pending_mutations: List[UnmanagedAccessMethodPendingMutations] + warnings: List[UnmanagedAccessMethodWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return UnmanagedAccessMethod( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( access_method_id=d.get("access_method_id", None), code=d.get("code", None), created_at=d.get("created_at", None), display_name=d.get("display_name", None), - errors=d.get("errors", None), + errors=[ + UnmanagedAccessMethodErrors.from_dict(i) for i in d.get("errors") or [] + ], is_assignment_required=d.get("is_assignment_required", None), is_encoding_required=d.get("is_encoding_required", None), is_issued=d.get("is_issued", None), @@ -68,7 +192,13 @@ def from_dict(d: Dict[str, Any]): is_ready_for_encoding=d.get("is_ready_for_encoding", None), issued_at=d.get("issued_at", None), mode=d.get("mode", None), - pending_mutations=d.get("pending_mutations", None), - warnings=d.get("warnings", None), + pending_mutations=[ + UnmanagedAccessMethodPendingMutations.from_dict(i) + for i in d.get("pending_mutations") or [] + ], + warnings=[ + UnmanagedAccessMethodWarnings.from_dict(i) + for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/unmanaged_device.py b/seam/resources/unmanaged_device.py index c6be5b7e..683f45fa 100644 --- a/seam/resources/unmanaged_device.py +++ b/seam/resources/unmanaged_device.py @@ -1,6 +1,247 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class UnmanagedDeviceErrors(ResourceMapping): + """Array of errors associated with the device. Each error object within the array contains two fields: ``error_code`` and ``message``. ``error_code`` is a string that uniquely identifies the type of error, enabling quick recognition and categorization of the issue. ``message`` provides a more detailed description of the error, offering insights into the issue and potentially how to rectify it. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar is_connected_account_error: Indicates that the error is a `connected account `_ error. + + :ivar is_device_error: Indicates that the error is not a device error. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + + :ivar is_bridge_error: Indicates whether the error is related to `Seam Bridge `_. + """ + + created_at: str + error_code: str + is_connected_account_error: bool + is_device_error: bool + message: str + is_bridge_error: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + is_connected_account_error=d.get("is_connected_account_error", None), + is_device_error=d.get("is_device_error", None), + message=d.get("message", None), + is_bridge_error=d.get("is_bridge_error", None), + ) + + +@dataclass +class UnmanagedDeviceLocation(ResourceMapping): + """Location information for the device. + + :ivar location_name: Name of the device location. + + :ivar time_zone: Time zone of the device location. + + :ivar timezone: Deprecated: Use ``time_zone`` instead. Time zone of the device location. + """ + + location_name: str + time_zone: str + timezone: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + location_name=d.get("location_name", None), + time_zone=d.get("time_zone", None), + timezone=d.get("timezone", None), + ) + + +@dataclass +class UnmanagedDeviceBattery(ResourceMapping): + """Keypad battery properties. + + :ivar level:""" + + level: float + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + level=d.get("level", None), + ) + + +@dataclass +class UnmanagedDeviceAccessoryKeypad(ResourceMapping): + """Accessory keypad properties and state. + + :ivar battery: Keypad battery properties. + + :ivar is_connected: Indicates if an accessory keypad is connected to the device.""" + + battery: UnmanagedDeviceBattery + is_connected: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + battery=( + UnmanagedDeviceBattery.from_dict(d.get("battery")) + if d.get("battery") is not None + else None + ), + is_connected=d.get("is_connected", None), + ) + + +@dataclass +class UnmanagedDeviceModel(ResourceMapping): + """Device model-related properties. + + :ivar accessory_keypad_supported: Deprecated: use device.properties.model.can_connect_accessory_keypad + + :ivar can_connect_accessory_keypad: Indicates whether the device can connect a accessory keypad. + + :ivar display_name: Display name of the device model. + + :ivar has_built_in_keypad: Indicates whether the device has a built in accessory keypad. + + :ivar manufacturer_display_name: Display name that corresponds to the manufacturer-specific terminology for the device. + + :ivar offline_access_codes_supported: Deprecated: use device.can_program_offline_access_codes. + + :ivar online_access_codes_supported: Deprecated: use device.can_program_online_access_codes. + """ + + accessory_keypad_supported: bool + can_connect_accessory_keypad: bool + display_name: str + has_built_in_keypad: bool + manufacturer_display_name: str + offline_access_codes_supported: bool + online_access_codes_supported: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + accessory_keypad_supported=d.get("accessory_keypad_supported", None), + can_connect_accessory_keypad=d.get("can_connect_accessory_keypad", None), + display_name=d.get("display_name", None), + has_built_in_keypad=d.get("has_built_in_keypad", None), + manufacturer_display_name=d.get("manufacturer_display_name", None), + offline_access_codes_supported=d.get( + "offline_access_codes_supported", None + ), + online_access_codes_supported=d.get("online_access_codes_supported", None), + ) + + +@dataclass +class UnmanagedDeviceProperties(ResourceMapping): + """properties of the device. + + :ivar accessory_keypad: Accessory keypad properties and state. + + :ivar battery: Represents the current status of the battery charge level. + + :ivar battery_level: Indicates the battery level of the device as a decimal value between 0 and 1, inclusive. + + :ivar image_alt_text: Alt text for the device image. + + :ivar image_url: Image URL for the device. + + :ivar manufacturer: Manufacturer of the device. When a device, such as a smart lock, is connected through a smart hub, the manufacturer of the device might be different from that of the smart hub. + + :ivar model: Device model-related properties. + + :ivar name: Deprecated: use device.display_name instead Name of the device. + + :ivar offline_access_codes_enabled: Deprecated: use device.can_program_offline_access_codes Indicates whether it is currently possible to use offline access codes for the device. + + :ivar online: Indicates whether the device is online. + + :ivar online_access_codes_enabled: Deprecated: use device.can_program_online_access_codes Indicates whether it is currently possible to use online access codes for the device. + """ + + accessory_keypad: UnmanagedDeviceAccessoryKeypad + battery: UnmanagedDeviceBattery + battery_level: float + image_alt_text: str + image_url: str + manufacturer: str + model: UnmanagedDeviceModel + name: str + offline_access_codes_enabled: bool + online: bool + online_access_codes_enabled: bool + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + accessory_keypad=( + UnmanagedDeviceAccessoryKeypad.from_dict(d.get("accessory_keypad")) + if d.get("accessory_keypad") is not None + else None + ), + battery=( + UnmanagedDeviceBattery.from_dict(d.get("battery")) + if d.get("battery") is not None + else None + ), + battery_level=d.get("battery_level", None), + image_alt_text=d.get("image_alt_text", None), + image_url=d.get("image_url", None), + manufacturer=d.get("manufacturer", None), + model=( + UnmanagedDeviceModel.from_dict(d.get("model")) + if d.get("model") is not None + else None + ), + name=d.get("name", None), + offline_access_codes_enabled=d.get("offline_access_codes_enabled", None), + online=d.get("online", None), + online_access_codes_enabled=d.get("online_access_codes_enabled", None), + ) + + +@dataclass +class UnmanagedDeviceWarnings(ResourceMapping): + """Array of warnings associated with the device. Each warning object within the array contains two fields: ``warning_code`` and ``message``. ``warning_code`` is a string that uniquely identifies the type of warning, enabling quick recognition and categorization of the issue. ``message`` provides a more detailed description of the warning, offering insights into the issue and potentially how to rectify it. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar active_access_code_count: Number of active access codes on the device when the warning was set. + + :ivar max_active_access_code_count: Maximum number of active access codes supported by the device. + """ + + created_at: str + message: str + warning_code: str + active_access_code_count: int + max_active_access_code_count: int + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + active_access_code_count=d.get("active_access_code_count", None), + max_active_access_code_count=d.get("max_active_access_code_count", None), + ) @dataclass @@ -98,16 +339,16 @@ class UnmanagedDevice: custom_metadata: Dict[str, Any] device_id: str device_type: str - errors: List[Dict[str, Any]] + errors: List[UnmanagedDeviceErrors] is_managed: bool - location: Dict[str, Any] - properties: Dict[str, Any] - warnings: List[Dict[str, Any]] + location: UnmanagedDeviceLocation + properties: UnmanagedDeviceProperties + warnings: List[UnmanagedDeviceWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return UnmanagedDevice( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( can_configure_auto_lock=d.get("can_configure_auto_lock", None), can_hvac_cool=d.get("can_hvac_cool", None), can_hvac_heat=d.get("can_hvac_heat", None), @@ -148,10 +389,20 @@ def from_dict(d: Dict[str, Any]): custom_metadata=DeepAttrDict(d.get("custom_metadata", None)), device_id=d.get("device_id", None), device_type=d.get("device_type", None), - errors=d.get("errors", None), + errors=[UnmanagedDeviceErrors.from_dict(i) for i in d.get("errors") or []], is_managed=d.get("is_managed", None), - location=DeepAttrDict(d.get("location", None)), - properties=DeepAttrDict(d.get("properties", None)), - warnings=d.get("warnings", None), + location=( + UnmanagedDeviceLocation.from_dict(d.get("location")) + if d.get("location") is not None + else None + ), + properties=( + UnmanagedDeviceProperties.from_dict(d.get("properties")) + if d.get("properties") is not None + else None + ), + warnings=[ + UnmanagedDeviceWarnings.from_dict(i) for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/unmanaged_user_identity.py b/seam/resources/unmanaged_user_identity.py index 6b41dd19..4d9a9384 100644 --- a/seam/resources/unmanaged_user_identity.py +++ b/seam/resources/unmanaged_user_identity.py @@ -1,6 +1,63 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class UnmanagedUserIdentityErrors(ResourceMapping): + """Array of errors associated with the user identity. Each error object within the array contains fields like "error_code" and "message." "error_code" is a string that uniquely identifies the type of error, enabling quick recognition and categorization of the issue. "message" provides a more detailed description of the error, offering insights into the issue and potentially how to rectify it. + + :ivar acs_system_id: ID of the access system that the user identity is associated with. + + :ivar acs_user_id: ID of the access system user that has an issue. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + acs_system_id: str + acs_user_id: str + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + acs_system_id=d.get("acs_system_id", None), + acs_user_id=d.get("acs_user_id", None), + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class UnmanagedUserIdentityWarnings(ResourceMapping): + """Array of warnings associated with the user identity. Each warning object within the array contains two fields: "warning_code" and "message." "warning_code" is a string that uniquely identifies the type of warning, enabling quick recognition and categorization of the issue. "message" provides a more detailed description of the warning, offering insights into the issue and potentially how to rectify it. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) @dataclass @@ -31,24 +88,29 @@ class UnmanagedUserIdentity: created_at: str display_name: str email_address: str - errors: List[Dict[str, Any]] + errors: List[UnmanagedUserIdentityErrors] full_name: str phone_number: str user_identity_id: str - warnings: List[Dict[str, Any]] + warnings: List[UnmanagedUserIdentityWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return UnmanagedUserIdentity( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( acs_user_ids=d.get("acs_user_ids", None), created_at=d.get("created_at", None), display_name=d.get("display_name", None), email_address=d.get("email_address", None), - errors=d.get("errors", None), + errors=[ + UnmanagedUserIdentityErrors.from_dict(i) for i in d.get("errors") or [] + ], full_name=d.get("full_name", None), phone_number=d.get("phone_number", None), user_identity_id=d.get("user_identity_id", None), - warnings=d.get("warnings", None), + warnings=[ + UnmanagedUserIdentityWarnings.from_dict(i) + for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/user_identity.py b/seam/resources/user_identity.py index d2d173dd..4758e160 100644 --- a/seam/resources/user_identity.py +++ b/seam/resources/user_identity.py @@ -1,6 +1,63 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class UserIdentityErrors(ResourceMapping): + """Array of errors associated with the user identity. Each error object within the array contains fields like "error_code" and "message." "error_code" is a string that uniquely identifies the type of error, enabling quick recognition and categorization of the issue. "message" provides a more detailed description of the error, offering insights into the issue and potentially how to rectify it. + + :ivar acs_system_id: ID of the access system that the user identity is associated with. + + :ivar acs_user_id: ID of the access system user that has an issue. + + :ivar created_at: Date and time at which Seam created the error. + + :ivar error_code: Unique identifier of the type of error. Enables quick recognition and categorization of the issue. + + :ivar message: Detailed description of the error. Provides insights into the issue and potentially how to rectify it. + """ + + acs_system_id: str + acs_user_id: str + created_at: str + error_code: str + message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + acs_system_id=d.get("acs_system_id", None), + acs_user_id=d.get("acs_user_id", None), + created_at=d.get("created_at", None), + error_code=d.get("error_code", None), + message=d.get("message", None), + ) + + +@dataclass +class UserIdentityWarnings(ResourceMapping): + """Array of warnings associated with the user identity. Each warning object within the array contains two fields: "warning_code" and "message." "warning_code" is a string that uniquely identifies the type of warning, enabling quick recognition and categorization of the issue. "message" provides a more detailed description of the warning, offering insights into the issue and potentially how to rectify it. + + :ivar created_at: Date and time at which Seam created the warning. + + :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + + :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + """ + + created_at: str + message: str + warning_code: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + created_at=d.get("created_at", None), + message=d.get("message", None), + warning_code=d.get("warning_code", None), + ) @dataclass @@ -33,26 +90,28 @@ class UserIdentity: created_at: str display_name: str email_address: str - errors: List[Dict[str, Any]] + errors: List[UserIdentityErrors] full_name: str phone_number: str user_identity_id: str user_identity_key: str - warnings: List[Dict[str, Any]] + warnings: List[UserIdentityWarnings] workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return UserIdentity( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( acs_user_ids=d.get("acs_user_ids", None), created_at=d.get("created_at", None), display_name=d.get("display_name", None), email_address=d.get("email_address", None), - errors=d.get("errors", None), + errors=[UserIdentityErrors.from_dict(i) for i in d.get("errors") or []], full_name=d.get("full_name", None), phone_number=d.get("phone_number", None), user_identity_id=d.get("user_identity_id", None), user_identity_key=d.get("user_identity_key", None), - warnings=d.get("warnings", None), + warnings=[ + UserIdentityWarnings.from_dict(i) for i in d.get("warnings") or [] + ], workspace_id=d.get("workspace_id", None), ) diff --git a/seam/resources/webhook.py b/seam/resources/webhook.py index 9f78d13f..fba1c282 100644 --- a/seam/resources/webhook.py +++ b/seam/resources/webhook.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping @dataclass @@ -20,9 +21,9 @@ class Webhook: url: str webhook_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return Webhook( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( event_types=d.get("event_types", None), secret=d.get("secret", None), url=d.get("url", None), diff --git a/seam/resources/workspace.py b/seam/resources/workspace.py index 1f2ddd70..5d06d8f4 100644 --- a/seam/resources/workspace.py +++ b/seam/resources/workspace.py @@ -1,6 +1,39 @@ from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from ..utils.deep_attr_dict import DeepAttrDict +from ..utils.resource_mapping import ResourceMapping + + +@dataclass +class WorkspaceConnectWebviewCustomization(ResourceMapping): + """ + + :ivar inviter_logo_url: URL of the inviter logo for `Connect Webviews `_ in the workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + + :ivar logo_shape: Logo shape for `Connect Webviews `_ in the workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + + :ivar primary_button_color: Primary button color for `Connect Webviews `_ in the workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + + :ivar primary_button_text_color: Primary button text color for `Connect Webviews `_ in the workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + + :ivar success_message: Success message for `Connect Webviews `_ in the workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. + """ + + inviter_logo_url: str + logo_shape: str + primary_button_color: str + primary_button_text_color: str + success_message: str + + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( + inviter_logo_url=d.get("inviter_logo_url", None), + logo_shape=d.get("logo_shape", None), + primary_button_color=d.get("primary_button_color", None), + primary_button_text_color=d.get("primary_button_text_color", None), + success_message=d.get("success_message", None), + ) @dataclass @@ -29,7 +62,7 @@ class Workspace: company_name: str connect_partner_name: str - connect_webview_customization: Dict[str, Any] + connect_webview_customization: WorkspaceConnectWebviewCustomization is_publishable_key_auth_enabled: bool is_sandbox: bool is_suspended: bool @@ -38,13 +71,17 @@ class Workspace: publishable_key: str workspace_id: str - @staticmethod - def from_dict(d: Dict[str, Any]): - return Workspace( + @classmethod + def from_dict(cls, d: Dict[str, Any]): + return cls( company_name=d.get("company_name", None), connect_partner_name=d.get("connect_partner_name", None), - connect_webview_customization=DeepAttrDict( - d.get("connect_webview_customization", None) + connect_webview_customization=( + WorkspaceConnectWebviewCustomization.from_dict( + d.get("connect_webview_customization") + ) + if d.get("connect_webview_customization") is not None + else None ), is_publishable_key_auth_enabled=d.get( "is_publishable_key_auth_enabled", None diff --git a/seam/utils/resource_mapping.py b/seam/utils/resource_mapping.py new file mode 100644 index 00000000..097a556c --- /dev/null +++ b/seam/utils/resource_mapping.py @@ -0,0 +1,24 @@ +"""Mapping compatibility for generated nested resource dataclasses.""" + +from typing import Any, ClassVar, Iterator + + +class ResourceMapping: + """Provide legacy dictionary-style reads for a nested resource object.""" + + def __getitem__(self, key: str) -> Any: + return getattr(self, key) + + def get(self, key: str, default: Any = None) -> Any: + return getattr(self, key, default) + + def __contains__(self, key: object) -> bool: + return isinstance(key, str) and key in self.__dataclass_fields__ + + def __iter__(self) -> Iterator[str]: + return iter(self.keys()) + + def keys(self) -> Iterator[str]: + return iter(self.__dataclass_fields__) + + __dataclass_fields__: ClassVar[dict[str, Any]] diff --git a/test/nested_resource_test.py b/test/nested_resource_test.py new file mode 100644 index 00000000..da8db8af --- /dev/null +++ b/test/nested_resource_test.py @@ -0,0 +1,61 @@ +"""Regression tests for generated nested resource types.""" + +import pytest + +from seam.resources.action_attempt import ( + ActionAttempt, + ActionAttemptError, + ActionAttemptResult, +) +from seam.resources.device import Device, DeviceErrors, DeviceProperties + + +def test_nested_objects_are_typed_and_drop_unknown_fields(): + device = Device.from_dict( + { + "properties": {"locked": True, "future_api_field": "ignored"}, + "errors": [{"error_code": "offline", "message": "Offline"}], + "custom_metadata": {"arbitrary": {"future": True}}, + } + ) + + assert isinstance(device.properties, DeviceProperties) + assert device.properties.locked is True + assert not hasattr(device.properties, "future_api_field") + assert isinstance(device.errors[0], DeviceErrors) + assert device.errors[0].error_code == "offline" + assert device.custom_metadata["arbitrary"]["future"] is True + + +def test_nested_objects_keep_dictionary_style_reads(): + properties = DeviceProperties.from_dict({"locked": True}) + + assert properties["locked"] is True + assert properties.get("locked") is True + assert properties.get("missing", "default") == "default" + assert "locked" in properties + assert "locked" in properties.keys() + assert "locked" in list(properties) + with pytest.raises(AttributeError): + _ = properties.typo + + +def test_missing_nested_values_use_stable_defaults(): + device = Device.from_dict({"errors": None}) + + assert device.properties is None + assert device.errors == [] + + +def test_action_attempt_union_hydrates_nested_result_and_error(): + attempt = ActionAttempt.from_dict( + { + "result": {"was_confirmed_by_device": True}, + "error": {"message": "failed", "type": "device_error"}, + } + ) + + assert isinstance(attempt.result, ActionAttemptResult) + assert attempt.result.was_confirmed_by_device is True + assert isinstance(attempt.error, ActionAttemptError) + assert attempt.error.message == "failed"