diff --git a/codegen/lib/layouts/resources.ts b/codegen/lib/layouts/resources.ts index 8b22638..9e51d72 100644 --- a/codegen/lib/layouts/resources.ts +++ b/codegen/lib/layouts/resources.ts @@ -120,6 +120,25 @@ const mergeOccurrences = (occurrences: Property[], path: string): Property => { return merged } + if (first.format === 'record' && 'valueTypes' in first) { + const valueTypes = occurrences.some( + (occurrence) => + !('valueTypes' in occurrence) || occurrence.valueTypes == null, + ) + ? undefined + : [ + ...new Set( + occurrences.flatMap((occurrence) => + 'valueTypes' in occurrence ? (occurrence.valueTypes ?? []) : [], + ), + ), + ] + const merged = { ...first, ...docs } + if (valueTypes == null) delete merged.valueTypes + else merged.valueTypes = valueTypes + return merged + } + if (first.format === 'object') { return { ...first, diff --git a/codegen/lib/python-type.ts b/codegen/lib/python-type.ts index 83b2e96..8e015a7 100644 --- a/codegen/lib/python-type.ts +++ b/codegen/lib/python-type.ts @@ -9,6 +9,10 @@ type ListItemFormat = Extract< { format: 'list' } >['itemFormat'] +type RecordValueType = NonNullable< + Extract['valueTypes'] +>[number] + export const mapParameterToPythonType = (parameter: Parameter): string => { if (parameter.format === 'list') { return `List[${mapListItemFormatToPythonType(parameter.itemFormat)}]` @@ -22,6 +26,10 @@ export const mapParameterToPythonType = (parameter: Parameter): string => { return mapBooleanToPythonType(parameter.values) } + if (parameter.format === 'record') { + return mapRecordToPythonType(parameter.valueTypes) + } + return mapScalarFormatToPythonType(parameter.format) } @@ -65,6 +73,10 @@ export const mapRequiredPropertyToPythonType = ( return 'List[Dict[str, Any]]' } + if (property.format === 'record' && 'valueTypes' in property) { + return mapRecordToPythonType(property.valueTypes) + } + if (property.format === 'object' && nestedClassName != null) { return nestedClassName } @@ -77,6 +89,36 @@ const mapBooleanToPythonType = (values?: boolean[]): string => ? 'bool' : `Literal[${values.map((value) => (value ? 'True' : 'False')).join(', ')}]` +const mapRecordToPythonType = (valueTypes?: RecordValueType[]): string => { + const types = valueTypes?.map(mapJsonSchemaTypeToPythonType) ?? [] + const valueType = + types.length === 0 + ? 'Any' + : types.length === 1 + ? (types[0] ?? 'Any') + : `Union[${types.join(', ')}]` + return `Dict[str, ${valueType}]` +} + +const mapJsonSchemaTypeToPythonType = (type: RecordValueType): string => { + switch (type) { + case 'string': + return 'str' + case 'number': + return 'float' + case 'integer': + return 'int' + case 'boolean': + return 'bool' + case 'object': + return 'Dict[str, Any]' + case 'array': + return 'List[Any]' + default: + throw new Error(`Unsupported JSON Schema type: ${type}`) + } +} + const mapScalarFormatToPythonType = (format: ScalarFormat): string => { switch (format) { case 'string': diff --git a/package-lock.json b/package-lock.json index 322e2d7..d2ef893 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "@seamapi/python", "devDependencies": { - "@seamapi/blueprint": "^1.6.0", + "@seamapi/blueprint": "^1.7.0", "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^1.1.0", "@seamapi/types": "1.1001.0", @@ -787,9 +787,9 @@ "license": "MIT" }, "node_modules/@seamapi/blueprint": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.6.0.tgz", - "integrity": "sha512-+lIriSgog0E0pUjXz/tdRZiVECiSiBcnZ3uU4JRN45RTLs2BQVbR+Ris6/mXwBV1wkpMetdVD3CP1BB1agf6Lg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.7.0.tgz", + "integrity": "sha512-on9pREQKyFgIaS99KLcck5K6AqLEO8Iy5h1heuJ2sEC2A7wXk5s/TL8iDOB3+4eeaAwuHRPgze9/Omo84KjJkw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ef374a1..fa3bddf 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ } }, "devDependencies": { - "@seamapi/blueprint": "^1.6.0", + "@seamapi/blueprint": "^1.7.0", "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^1.1.0", "@seamapi/types": "1.1001.0", diff --git a/seam/resources/connect_webview.py b/seam/resources/connect_webview.py index 36d2910..15f8f28 100644 --- a/seam/resources/connect_webview.py +++ b/seam/resources/connect_webview.py @@ -64,7 +64,7 @@ class ConnectWebview: connect_webview_id: str connected_account_id: Optional[str] created_at: str - custom_metadata: Dict[str, Any] + custom_metadata: Dict[str, Union[str, bool]] custom_redirect_failure_url: Optional[str] custom_redirect_url: Optional[str] customer_key: Optional[str] diff --git a/seam/resources/connected_account.py b/seam/resources/connected_account.py index d027b8f..fc7d9c5 100644 --- a/seam/resources/connected_account.py +++ b/seam/resources/connected_account.py @@ -243,7 +243,7 @@ def from_dict(cls, d: Any): automatically_manage_new_devices: bool connected_account_id: str created_at: Optional[str] - custom_metadata: Dict[str, Any] + custom_metadata: Dict[str, Union[str, bool]] customer_key: Optional[str] default_checkin_time: Optional[str] default_checkout_time: Optional[str] diff --git a/seam/resources/device.py b/seam/resources/device.py index 5946700..ad04335 100644 --- a/seam/resources/device.py +++ b/seam/resources/device.py @@ -3275,7 +3275,7 @@ def from_dict(cls, d: Any): capabilities_supported: List[str] connected_account_id: str created_at: str - custom_metadata: Dict[str, Any] + custom_metadata: Dict[str, Union[str, bool]] device_id: str device_manufacturer: Optional[DeviceManufacturer] device_provider: Optional[DeviceProvider] diff --git a/seam/resources/phone.py b/seam/resources/phone.py index 67cfbb3..2ea4b78 100644 --- a/seam/resources/phone.py +++ b/seam/resources/phone.py @@ -162,7 +162,7 @@ def from_dict(cls, d: Any): ) created_at: str - custom_metadata: Dict[str, Any] + custom_metadata: Dict[str, Union[str, bool]] device_id: str device_type: str display_name: str diff --git a/seam/resources/seam_event.py b/seam/resources/seam_event.py index 0d089e8..045a33d 100644 --- a/seam/resources/seam_event.py +++ b/seam/resources/seam_event.py @@ -487,10 +487,10 @@ def from_dict(cls, d: Any): ) access_code_id: Optional[str] - connected_account_custom_metadata: Optional[Dict[str, Any]] + connected_account_custom_metadata: Optional[Dict[str, Union[str, bool]]] connected_account_id: Optional[str] created_at: str - device_custom_metadata: Optional[Dict[str, Any]] + device_custom_metadata: Optional[Dict[str, Union[str, bool]]] device_id: Optional[str] event_description: Optional[str] event_id: str diff --git a/seam/resources/unmanaged_device.py b/seam/resources/unmanaged_device.py index 6b42932..3952059 100644 --- a/seam/resources/unmanaged_device.py +++ b/seam/resources/unmanaged_device.py @@ -363,7 +363,7 @@ def from_dict(cls, d: Any): capabilities_supported: List[str] connected_account_id: str created_at: str - custom_metadata: Dict[str, Any] + custom_metadata: Dict[str, Union[str, bool]] device_id: str device_type: str errors: List[Errors] diff --git a/seam/routes/connect_webviews.py b/seam/routes/connect_webviews.py index 45300e0..69a0e88 100644 --- a/seam/routes/connect_webviews.py +++ b/seam/routes/connect_webviews.py @@ -15,7 +15,7 @@ def create( accepted_capabilities: Optional[List[str]] = None, accepted_providers: Optional[List[str]] = None, automatically_manage_new_devices: Optional[bool] = None, - custom_metadata: Optional[Dict[str, Any]] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, custom_redirect_failure_url: Optional[str] = None, custom_redirect_url: Optional[str] = None, customer_key: Optional[str] = None, @@ -82,7 +82,7 @@ def get(self, *, connect_webview_id: str) -> ConnectWebview: def list( self, *, - custom_metadata_has: Optional[Dict[str, Any]] = None, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, customer_key: Optional[str] = None, limit: Optional[float] = None, page_cursor: Optional[Union[str, Null]] = None, @@ -123,7 +123,7 @@ def create( accepted_capabilities: Optional[List[str]] = None, accepted_providers: Optional[List[str]] = None, automatically_manage_new_devices: Optional[bool] = None, - custom_metadata: Optional[Dict[str, Any]] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, custom_redirect_failure_url: Optional[str] = None, custom_redirect_url: Optional[str] = None, customer_key: Optional[str] = None, @@ -251,7 +251,7 @@ def get(self, *, connect_webview_id: str) -> ConnectWebview: def list( self, *, - custom_metadata_has: Optional[Dict[str, Any]] = None, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, customer_key: Optional[str] = None, limit: Optional[float] = None, page_cursor: Optional[Union[str, Null]] = None, diff --git a/seam/routes/connected_accounts.py b/seam/routes/connected_accounts.py index a803c0f..8aef31d 100644 --- a/seam/routes/connected_accounts.py +++ b/seam/routes/connected_accounts.py @@ -49,7 +49,7 @@ def get( def list( self, *, - custom_metadata_has: Optional[Dict[str, Any]] = None, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, page_cursor: Optional[Union[str, Null]] = None, @@ -92,7 +92,7 @@ def update( connected_account_id: str, accepted_capabilities: Optional[List[str]] = None, automatically_manage_new_devices: Optional[bool] = None, - custom_metadata: Optional[Dict[str, Any]] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, customer_key: Optional[str] = None, display_name: Optional[str] = None, ) -> None: @@ -194,7 +194,7 @@ def get( def list( self, *, - custom_metadata_has: Optional[Dict[str, Any]] = None, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, page_cursor: Optional[Union[str, Null]] = None, @@ -276,7 +276,7 @@ def update( connected_account_id: str, accepted_capabilities: Optional[List[str]] = None, automatically_manage_new_devices: Optional[bool] = None, - custom_metadata: Optional[Dict[str, Any]] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, customer_key: Optional[str] = None, display_name: Optional[str] = None, ) -> None: diff --git a/seam/routes/devices.py b/seam/routes/devices.py index ff88d25..7afdaa7 100644 --- a/seam/routes/devices.py +++ b/seam/routes/devices.py @@ -45,7 +45,7 @@ def list( connected_account_id: Optional[str] = None, connected_account_ids: Optional[List[str]] = None, created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, customer_key: Optional[str] = None, device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, @@ -125,7 +125,7 @@ def update( *, device_id: str, backup_access_code_pool_enabled: Optional[bool] = None, - custom_metadata: Optional[Dict[str, Any]] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, is_managed: Optional[bool] = None, name: Optional[Union[str, Null]] = None, properties: Optional[Dict[str, Any]] = None, @@ -206,7 +206,7 @@ def list( connected_account_id: Optional[str] = None, connected_account_ids: Optional[List[str]] = None, created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, + custom_metadata_has: Optional[Dict[str, Union[str, bool]]] = None, customer_key: Optional[str] = None, device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, @@ -352,7 +352,7 @@ def update( *, device_id: str, backup_access_code_pool_enabled: Optional[bool] = None, - custom_metadata: Optional[Dict[str, Any]] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, is_managed: Optional[bool] = None, name: Optional[Union[str, Null]] = None, properties: Optional[Dict[str, Any]] = None, diff --git a/seam/routes/devices_unmanaged.py b/seam/routes/devices_unmanaged.py index 37fd90e..670e355 100644 --- a/seam/routes/devices_unmanaged.py +++ b/seam/routes/devices_unmanaged.py @@ -80,7 +80,7 @@ def update( self, *, device_id: str, - custom_metadata: Optional[Dict[str, Any]] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, is_managed: Optional[Literal[True]] = None, ) -> None: """Updates a specified `unmanaged device `_. To convert an unmanaged device to managed, set ``is_managed`` to ``true``. @@ -229,7 +229,7 @@ def update( self, *, device_id: str, - custom_metadata: Optional[Dict[str, Any]] = None, + custom_metadata: Optional[Dict[str, Union[str, bool]]] = None, is_managed: Optional[Literal[True]] = None, ) -> None: """Updates a specified `unmanaged device `_. To convert an unmanaged device to managed, set ``is_managed`` to ``true``. diff --git a/test/resource_types_test.py b/test/resource_types_test.py index 6646e45..a7d29d8 100644 --- a/test/resource_types_test.py +++ b/test/resource_types_test.py @@ -1,8 +1,14 @@ # mypy: warn_unused_ignores=True -from typing import Literal, assert_type +from typing import Any, Dict, Literal, Union, assert_type -from seam.resources import AccessCode, ActionAttempt, UnmanagedAccessCode +from seam.resources import ( + AccessCode, + ActionAttempt, + Device, + SeamEvent, + UnmanagedAccessCode, +) def _assert_access_code_narrowing( @@ -25,6 +31,15 @@ def _assert_boolean_shapes( assert_type(credential.is_managed, Literal[True, False]) +def _assert_record_value_types(device: Device, event: SeamEvent) -> None: + assert_type(device.custom_metadata, Dict[str, Union[str, bool]]) + assert_type( + event.connected_account_custom_metadata, + Dict[str, Union[str, bool]] | None, + ) + assert_type(event.minut_metadata, Dict[str, Any] | None) + + def _assert_opposite_literal_is_rejected(code: UnmanagedAccessCode) -> None: code.is_managed = True # type: ignore[assignment] @@ -32,4 +47,5 @@ def _assert_opposite_literal_is_rejected(code: UnmanagedAccessCode) -> None: def test_access_code_resources_narrow_on_is_managed(): assert callable(_assert_access_code_narrowing) assert callable(_assert_boolean_shapes) + assert callable(_assert_record_value_types) assert callable(_assert_opposite_literal_is_rejected)