diff --git a/codegen/layouts/partials/model-dataclass.hbs b/codegen/layouts/partials/model-dataclass.hbs index 04da1ba..cae5fdc 100644 --- a/codegen/layouts/partials/model-dataclass.hbs +++ b/codegen/layouts/partials/model-dataclass.hbs @@ -1,13 +1,13 @@ @dataclass class {{className}}: {{#each properties}} - {{name}}: {{type}} + {{safeName}}: {{type}} {{/each}} @staticmethod def from_dict(d: Dict[str, Any]): return {{className}}( {{#each properties}} - {{name}}={{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}}, + {{safeName}}={{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}}, {{/each}} ) diff --git a/codegen/lib/layouts/models.ts b/codegen/lib/layouts/models.ts index cd4836e..5a2e80f 100644 --- a/codegen/lib/layouts/models.ts +++ b/codegen/lib/layouts/models.ts @@ -11,10 +11,61 @@ import { flattenObjSchema } from '../openapi/flatten-obj-schema.js' import type { ObjSchema, OpenapiSchema } from '../openapi/types.js' import { getMethodLayoutContext } from './route.js' +// Python hard keywords cannot be used as identifiers. When a property name +// collides with one (e.g. "from"), the dataclass field and keyword argument +// are suffixed with an underscore while the original name is preserved as the +// dict key. No existing property name is a hard keyword, so this leaves all +// other generated output unchanged. +const PYTHON_KEYWORDS = new Set([ + 'False', + 'None', + 'True', + 'and', + 'as', + 'assert', + 'async', + 'await', + 'break', + 'class', + 'continue', + 'def', + 'del', + 'elif', + 'else', + 'except', + 'finally', + 'for', + 'from', + 'global', + 'if', + 'import', + 'in', + 'is', + 'lambda', + 'nonlocal', + 'not', + 'or', + 'pass', + 'raise', + 'return', + 'try', + 'while', + 'with', + 'yield', +]) + +const toSafeIdentifier = (name: string): string => + PYTHON_KEYWORDS.has(name) ? `${name}_` : name + export interface ModelsLayoutContext { resources: Array<{ className: string - properties: Array<{ name: string; type: string; isDictParam: boolean }> + properties: Array<{ + name: string + safeName: string + type: string + isDictParam: boolean + }> }> abstractClasses: Array<{ className: string @@ -55,6 +106,7 @@ export const setModelsLayoutContext = ( const type = mapPythonType(propertySchema) return { name, + safeName: toSafeIdentifier(name), type, isDictParam: type.startsWith('Dict') || name === 'properties', } diff --git a/package-lock.json b/package-lock.json index b4ce9c1..d8f19de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "": { "name": "@seamapi/python", "devDependencies": { - "@seamapi/blueprint": "0.55.0", + "@seamapi/blueprint": "^0.56.0", "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^0.5.2", - "@seamapi/types": "1.910.0", + "@seamapi/types": "1.963.0", "change-case": "^5.4.4", "prettier": "^3.2.5" } @@ -856,9 +856,9 @@ "license": "MIT" }, "node_modules/@seamapi/blueprint": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-0.55.0.tgz", - "integrity": "sha512-2Xk3XQ486WufuZ4ar7aP1XWQXaadg04x1VVjHGWrI1lMpSfWAeOTs8ol3EdRzI/NK6pUdIfgED86aH62KsyQ9w==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-0.56.0.tgz", + "integrity": "sha512-6trN/T2hGnoWE8l1tmHYv+QHexF1NzeG+hiGUm6nfTBIqCsndl457p10VCtfB7ew0CqSUzNkjfhrb7hww3SZPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -940,9 +940,9 @@ } }, "node_modules/@seamapi/types": { - "version": "1.910.0", - "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.910.0.tgz", - "integrity": "sha512-T5e+pZr4VU1nRLlh+R7oa4l+JMEqjs6XglUP7ud6LYagSXjNSrtQlQvjFOCIV6TGSgMZ0n4KUiAaqik4531Ugg==", + "version": "1.963.0", + "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.963.0.tgz", + "integrity": "sha512-wInOSZuPjHFvNH7uUz3o88oCiOvFxT89XHP+y1eFA+VuoqGN90npznXM5N/yCiYqUIWzx+7MAjw3G8ReLi5mRQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 8506e9d..9bcbcee 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ "start": "fake-seam-connect --seed" }, "devDependencies": { - "@seamapi/blueprint": "0.55.0", + "@seamapi/blueprint": "^0.56.0", "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^0.5.2", - "@seamapi/types": "1.910.0", + "@seamapi/types": "1.963.0", "change-case": "^5.4.4", "prettier": "^3.2.5" } diff --git a/seam/routes/access_codes.py b/seam/routes/access_codes.py index c72e663..0f81153 100644 --- a/seam/routes/access_codes.py +++ b/seam/routes/access_codes.py @@ -179,6 +179,7 @@ def list( *, access_code_ids: Optional[List[str]] = None, access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, access_method_id: Optional[str] = None, customer_key: Optional[str] = None, device_id: Optional[str] = None, @@ -193,6 +194,8 @@ def list( json_payload["access_code_ids"] = access_code_ids if access_grant_id is not None: json_payload["access_grant_id"] = access_grant_id + if access_grant_key is not None: + json_payload["access_grant_key"] = access_grant_key if access_method_id is not None: json_payload["access_method_id"] = access_method_id if customer_key is not None: diff --git a/seam/routes/access_grants.py b/seam/routes/access_grants.py index 40b9404..76bd968 100644 --- a/seam/routes/access_grants.py +++ b/seam/routes/access_grants.py @@ -189,7 +189,8 @@ def request_access_methods( def update( self, *, - access_grant_id: str, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, ends_at: Optional[str] = None, name: Optional[str] = None, starts_at: Optional[str] = None @@ -198,6 +199,8 @@ def update( if access_grant_id is not None: json_payload["access_grant_id"] = access_grant_id + if access_grant_key is not None: + json_payload["access_grant_key"] = access_grant_key if ends_at is not None: json_payload["ends_at"] = ends_at if name is not None: diff --git a/seam/routes/devices.py b/seam/routes/devices.py index 7094f29..026d254 100644 --- a/seam/routes/devices.py +++ b/seam/routes/devices.py @@ -119,6 +119,7 @@ def update( self, *, device_id: str, + backup_access_code_pool_enabled: Optional[bool] = None, custom_metadata: Optional[Dict[str, Any]] = None, is_managed: Optional[bool] = None, name: Optional[str] = None, @@ -128,6 +129,10 @@ def update( if device_id is not None: json_payload["device_id"] = device_id + if backup_access_code_pool_enabled is not None: + json_payload["backup_access_code_pool_enabled"] = ( + backup_access_code_pool_enabled + ) if custom_metadata is not None: json_payload["custom_metadata"] = custom_metadata if is_managed is not None: diff --git a/seam/routes/models.py b/seam/routes/models.py index f3a5d81..c64867e 100644 --- a/seam/routes/models.py +++ b/seam/routes/models.py @@ -127,6 +127,7 @@ class AccessMethod: created_at: str customization_profile_id: str display_name: str + errors: List[Dict[str, Any]] instant_key_url: str is_assignment_required: bool is_encoding_required: bool @@ -148,6 +149,7 @@ def from_dict(d: Dict[str, Any]): 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), 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), @@ -1059,12 +1061,17 @@ class SeamEvent: created_at: str device_custom_metadata: Dict[str, Any] device_id: str + event_description: str event_id: str event_type: str occurred_at: str workspace_id: str change_reason: str changed_properties: List[Dict[str, Any]] + description: str + from_: Dict[str, Any] + to: Dict[str, Any] + requested_mutations: List[Dict[str, Any]] code: str access_code_errors: List[Dict[str, Any]] access_code_warnings: List[Dict[str, Any]] @@ -1113,6 +1120,7 @@ class SeamEvent: is_via_nfc: bool method: str user_identity_id: str + reason: Dict[str, Any] climate_preset_key: str is_fallback_climate_preset: bool thermostat_schedule_id: str @@ -1151,12 +1159,17 @@ def from_dict(d: Dict[str, Any]): created_at=d.get("created_at", None), device_custom_metadata=DeepAttrDict(d.get("device_custom_metadata", None)), device_id=d.get("device_id", None), + event_description=d.get("event_description", None), event_id=d.get("event_id", None), event_type=d.get("event_type", None), 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), + description=d.get("description", None), + from_=DeepAttrDict(d.get("from", None)), + to=DeepAttrDict(d.get("to", None)), + requested_mutations=d.get("requested_mutations", None), code=d.get("code", None), access_code_errors=d.get("access_code_errors", None), access_code_warnings=d.get("access_code_warnings", None), @@ -1205,6 +1218,7 @@ 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)), 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), @@ -1871,6 +1885,7 @@ class Workspace: is_sandbox: bool is_suspended: bool name: str + organization_id: str publishable_key: str workspace_id: str @@ -1888,6 +1903,7 @@ def from_dict(d: Dict[str, Any]): is_sandbox=d.get("is_sandbox", None), is_suspended=d.get("is_suspended", None), name=d.get("name", None), + organization_id=d.get("organization_id", None), publishable_key=d.get("publishable_key", None), workspace_id=d.get("workspace_id", None), ) @@ -3233,7 +3249,8 @@ def request_access_methods( def update( self, *, - access_grant_id: str, + access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, ends_at: Optional[str] = None, name: Optional[str] = None, starts_at: Optional[str] = None @@ -3640,6 +3657,7 @@ def list( *, access_code_ids: Optional[List[str]] = None, access_grant_id: Optional[str] = None, + access_grant_key: Optional[str] = None, access_method_id: Optional[str] = None, customer_key: Optional[str] = None, device_id: Optional[str] = None, @@ -3758,6 +3776,7 @@ def update( self, *, device_id: str, + backup_access_code_pool_enabled: Optional[bool] = None, custom_metadata: Optional[Dict[str, Any]] = None, is_managed: Optional[bool] = None, name: Optional[str] = None,