From 017a12f1fd92b7e251632c15b4861bb0f391a605 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 14:43:59 +0000 Subject: [PATCH 1/2] feat: Generate SDK from blueprint only, removing OpenAPI parsing Remove all TEMPORARY output-parity workarounds and their TODOs from the codegen. The generator now consumes only the Blueprint from @seamapi/blueprint; the raw OpenAPI spec is no longer parsed anywhere in this repository's codegen (it is only passed to the blueprint plugin, which builds the Blueprint from it). - Delete codegen/lib/openapi/, map-python-type.ts, and endpoint-rules.ts. - Filter endpoints on blueprint isUndocumented flags instead of raw x-undocumented/x-fern-sdk-group-name extensions and hardcoded path lists. - Derive namespaces and parent/child class wiring from blueprint route and namespace paths, which now also wires deeply nested namespaces (seam.acs.encoders.simulate) to their parent class. - Derive method parameters from endpoint.request.parameters and return types from endpoint.response via a new blueprint-format-to-Python-type mapping (codegen/lib/python-type.ts). - Build models.py from blueprint resources, events, action attempts, and pagination, merging event and action attempt variant properties into single dataclasses. - Fix the parameter sort comparator precedence so an explicit position sorts a parameter first. Expected generated output changes: - integer parameters and properties are now typed float (the blueprint collapses integer into number) - new workspaces.update() method returning None - seam.acs.encoders.simulate is now reachable - {resource}_id parameter sorts first in get() methods - PhoneRegistration model removed (blueprint omits it; it was referenced nowhere) - ActionAttempt.result is now Dict[str, Any] with DeepAttrDict access - Batch fields are now List[Dict[str, Any]]; Device.device_type is now str Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011TdqYTYvyrK8rrR7FL1Aax --- codegen/lib/class-model.ts | 28 +- codegen/lib/endpoint-rules.ts | 23 -- codegen/lib/layouts/models.ts | 85 ++++--- codegen/lib/layouts/route.ts | 3 +- codegen/lib/map-python-type.ts | 46 ---- .../deep-flatten-one-of-and-all-of-schema.ts | 138 ---------- codegen/lib/openapi/flatten-obj-schema.ts | 129 ---------- codegen/lib/openapi/get-filtered-routes.ts | 23 -- .../get-parameter-and-response-schema.ts | 139 ---------- .../map-parent-to-children-resource.ts | 39 --- codegen/lib/openapi/types.ts | 68 ----- codegen/lib/python-type.ts | 51 ++++ codegen/lib/routes.ts | 239 +++++++----------- seam/routes/access_codes.py | 6 +- seam/routes/acs_encoders.py | 7 +- seam/routes/acs_entrances.py | 2 +- seam/routes/acs_users.py | 8 +- seam/routes/action_attempts.py | 2 +- seam/routes/connected_accounts.py | 2 +- seam/routes/events.py | 8 +- seam/routes/models.py | 235 ++++++++--------- seam/routes/thermostats_schedules.py | 4 +- seam/routes/user_identities.py | 2 +- seam/routes/user_identities_unmanaged.py | 2 +- seam/routes/workspaces.py | 33 +++ 25 files changed, 380 insertions(+), 942 deletions(-) delete mode 100644 codegen/lib/endpoint-rules.ts delete mode 100644 codegen/lib/map-python-type.ts delete mode 100644 codegen/lib/openapi/deep-flatten-one-of-and-all-of-schema.ts delete mode 100644 codegen/lib/openapi/flatten-obj-schema.ts delete mode 100644 codegen/lib/openapi/get-filtered-routes.ts delete mode 100644 codegen/lib/openapi/get-parameter-and-response-schema.ts delete mode 100644 codegen/lib/openapi/map-parent-to-children-resource.ts delete mode 100644 codegen/lib/openapi/types.ts create mode 100644 codegen/lib/python-type.ts diff --git a/codegen/lib/class-model.ts b/codegen/lib/class-model.ts index 2d52061d..2b7832a0 100644 --- a/codegen/lib/class-model.ts +++ b/codegen/lib/class-model.ts @@ -1,6 +1,5 @@ -// Ported from @seamapi/nextlove-sdk-generator lib/generate-python-sdk/class-file.ts. -// Holds the data previously carried by the nextlove ClassFile; all string -// serialization moved to the Handlebars layouts and their context builders. +// Holds the class and method data assembled by the routes plugin; all string +// serialization lives in the Handlebars layouts and their context builders. export interface ClassMethodParameter { name: string @@ -13,7 +12,7 @@ export interface ClassMethod { methodName: string path: string parameters: ClassMethodParameter[] - returnPath: Array + returnPath: string[] returnResource: string } @@ -29,21 +28,12 @@ export interface ClassModel { childClassIdentifiers: ChildClassIdentifier[] } -// Verbatim port of the nextlove parameter comparator. The original -// expression `(a.position ?? a.required ? 1000 : 9999)` parses as -// `(a.position ?? a.required) ? 1000 : 9999`, so a parameter with -// position 0 is falsy and lands in the 9999 tier together with the -// optional parameters. Combined with a stable sort this yields: required -// parameters first (in schema order), then everything else (in schema -// order). -// TODO: Fix the operator precedence so position sorts a parameter first -// as originally intended, once generated output is allowed to change. -// Until then, do not "fix" it: the generated output must stay identical. +// Sort tiers: an explicit position first, then required parameters, then +// optional parameters. Array#sort is stable, so ties keep schema order. +const sortTier = ({ position, required }: ClassMethodParameter): number => + position ?? ((required ?? false) ? 1000 : 9999) + export const sortClassMethodParameters = ( parameters: ClassMethodParameter[], ): ClassMethodParameter[] => - [...parameters].sort( - (a, b) => - ((a.position ?? a.required) ? 1000 : 9999) - - ((b.position ?? b.required) ? 1000 : 9999), - ) + [...parameters].sort((a, b) => sortTier(a) - sortTier(b)) diff --git a/codegen/lib/endpoint-rules.ts b/codegen/lib/endpoint-rules.ts deleted file mode 100644 index 9e7e9d3c..00000000 --- a/codegen/lib/endpoint-rules.ts +++ /dev/null @@ -1,23 +0,0 @@ -// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator -// lib/endpoint-rules.ts. These lists only preserve legacy generated output: -// the ignored paths reproduce the previous endpoint filtering, and the -// deprecated action attempt list keeps those endpoints returning None. -// TODO: Delete this file once generated output is allowed to change; filter -// on blueprint undocumented flags instead and let the deprecated endpoints -// return their real response types. - -export const endpointsReturningDeprecatedActionAttempt = [ - '/access_codes/delete', - '/access_codes/unmanaged/delete', - '/access_codes/update', - '/noise_sensors/noise_thresholds/delete', - '/noise_sensors/noise_thresholds/update', - '/thermostats/climate_setting_schedules/update', -] - -export const ignoredEndpointPaths = [ - '/health', - '/health/get_health', - '/health/get_service_health', - '/health/service/[service_name]', -] diff --git a/codegen/lib/layouts/models.ts b/codegen/lib/layouts/models.ts index 5a2e80f3..7e946cc3 100644 --- a/codegen/lib/layouts/models.ts +++ b/codegen/lib/layouts/models.ts @@ -1,21 +1,19 @@ // Builds the template context for seam/routes/models.py. -// Mirrors the models.py assembly in the nextlove generate-python-sdk.ts plus -// ClassFile#serializeToAbstractClassWithoutImports. +// Dataclasses come from the blueprint resources, events, action attempts, and +// pagination; the abstract route classes mirror the generated route classes. +import type { Blueprint, Property, Resource } from '@seamapi/blueprint' import { pascalCase } from 'change-case' import type { ClassModel } from '../class-model.js' import { convertCustomResourceName } from '../custom-resource-name-conversions.js' -import { mapPythonType } from '../map-python-type.js' -import { flattenObjSchema } from '../openapi/flatten-obj-schema.js' -import type { ObjSchema, OpenapiSchema } from '../openapi/types.js' +import { mapPropertyToPythonType } from '../python-type.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. +// dict key. const PYTHON_KEYWORDS = new Set([ 'False', 'None', @@ -81,37 +79,56 @@ export interface ModelsLayoutContext { routesNamespaces: Array<{ namespace: string; abstractClassName: string }> } +// 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 merged = new Map() + for (const { properties } of resources) { + for (const property of properties) { + if (!merged.has(property.name)) merged.set(property.name, property) + } + } + return [...merged.values()] +} + export const setModelsLayoutContext = ( - openapi: OpenapiSchema, + blueprint: Blueprint, classMap: Map, topLevelNamespaces: string[], ): ModelsLayoutContext => { - // TODO: Use blueprint.resources, blueprint.events, and - // blueprint.actionAttempts once generated output is allowed to change. - // Blueprint currently omits some schemas (e.g. pagination and - // phone_registration), reorders others, and collapses integer to number, - // so the raw OpenAPI schemas are used to keep the output identical. - const resources = Object.entries(openapi.components.schemas) - .map( - ([schemaName, schema]) => - [schemaName, flattenObjSchema(schema as ObjSchema)] as [ - string, - ObjSchema, - ], - ) - .map(([schemaName, schema]) => ({ - className: pascalCase(convertCustomResourceName(schemaName)), - properties: Object.entries(schema.properties).map( - ([name, propertySchema]) => { - const type = mapPythonType(propertySchema) - return { - name, - safeName: toSafeIdentifier(name), - type, - isDictParam: type.startsWith('Dict') || name === 'properties', - } - }, - ), + const models = new Map() + + for (const resource of blueprint.resources) { + models.set(resource.resourceType, resource.properties) + } + + // The event and action attempt variants merge into a single dataclass with + // the union of the variant properties, overriding the base resource schema. + models.set( + 'action_attempt', + mergeResourceProperties(blueprint.actionAttempts), + ) + models.set('event', mergeResourceProperties(blueprint.events)) + + if (blueprint.pagination != null) { + models.set('pagination', blueprint.pagination.properties) + } + + const resources = [...models.entries()] + .sort(([a], [b]) => (a < b ? -1 : 1)) + .map(([name, properties]) => ({ + className: pascalCase(convertCustomResourceName(name)), + properties: properties.map((property) => { + const type = mapPropertyToPythonType(property) + return { + name: property.name, + safeName: toSafeIdentifier(property.name), + type, + isDictParam: + type.startsWith('Dict') || property.name === 'properties', + } + }), })) const abstractClasses = [...classMap.values()] diff --git a/codegen/lib/layouts/route.ts b/codegen/lib/layouts/route.ts index 1ba04803..ba7a6c6a 100644 --- a/codegen/lib/layouts/route.ts +++ b/codegen/lib/layouts/route.ts @@ -71,7 +71,8 @@ export const getMethodLayoutContext = ( pollsActionAttempt, isList, itemType: returnResourceItem, - resAccessor: `res["${returnPath.join('"]["')}"]`, + resAccessor: + returnPath.length > 0 ? `res["${returnPath.join('"]["')}"]` : '', } } diff --git a/codegen/lib/map-python-type.ts b/codegen/lib/map-python-type.ts deleted file mode 100644 index 88cb4acd..00000000 --- a/codegen/lib/map-python-type.ts +++ /dev/null @@ -1,46 +0,0 @@ -// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator -// lib/generate-python-sdk/map-python-type.ts. This is a frozen output-parity -// workaround: it exists only so the generated output stays byte-identical to -// the previous generator. Do not review, refactor, or improve it. -// TODO: Delete this file and derive types from @seamapi/blueprint parameter -// and property formats once generated output is allowed to change. Blueprint -// collapses the OpenAPI integer type into its number format, but the -// generated output distinguishes int from float, so the raw OpenAPI schema is -// used here instead. - -import { deepFlattenOneOfAndAllOfSchema } from './openapi/deep-flatten-one-of-and-all-of-schema.js' -import type { PrimitiveSchema, PropertySchema } from './openapi/types.js' - -// TODO literals? -export const mapPythonType = (propertySchema: PropertySchema): string => { - if (typeof propertySchema !== 'object') { - return 'Any' - } - - if ('type' in propertySchema && propertySchema.type === 'string') { - // TODO handle format="date-time" - return 'str' - } - if ('type' in propertySchema && propertySchema.type === 'integer') { - return 'int' - } - if ('type' in propertySchema && propertySchema.type === 'boolean') { - return 'bool' - } - if ('type' in propertySchema && propertySchema.type === 'number') { - return 'float' - } - if ('type' in propertySchema && propertySchema.type === 'array') { - const arrayItemType: string = - 'type' in propertySchema.items - ? mapPythonType({ type: propertySchema.items.type } as PrimitiveSchema) - : mapPythonType(deepFlattenOneOfAndAllOfSchema(propertySchema.items)) - - return `List[${arrayItemType}]` - } // TODO, make more specific - if ('type' in propertySchema && propertySchema.type === 'object') { - return 'Dict[str, Any]' // TODO, make more specific - } - - return 'Any' -} diff --git a/codegen/lib/openapi/deep-flatten-one-of-and-all-of-schema.ts b/codegen/lib/openapi/deep-flatten-one-of-and-all-of-schema.ts deleted file mode 100644 index 3e585947..00000000 --- a/codegen/lib/openapi/deep-flatten-one-of-and-all-of-schema.ts +++ /dev/null @@ -1,138 +0,0 @@ -// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator -// lib/openapi/deep-flatten-one-of-and-all-of-schema.ts. This OpenAPI parsing -// is a frozen output-parity workaround: it exists only so the generated -// output stays byte-identical to the previous generator. Do not review, -// refactor, or improve it. -// TODO: Delete this file and use parameter formats from @seamapi/blueprint -// once generated output is allowed to change. - -import type { - AllOfSchema, - ArraySchema, - ObjSchema, - OneOfSchema, - PrimitiveSchema, - PropertySchema, -} from './types.js' - -export function deepFlattenOneOfAndAllOfSchema( - schema: PropertySchema, -): PropertySchema { - if ('oneOf' in schema) { - return flattenOneOf(schema) - } else if ('allOf' in schema) { - return flattenAllOf(schema) - } else if ( - 'type' in schema && - schema.type === 'object' && - schema.properties != null - ) { - return flattenObject(schema) - } else if ( - 'type' in schema && - schema.type === 'array' && - schema.items != null - ) { - return flattenArray(schema) - } else { - // For primitive types, return the schema as is - return schema - } -} - -function flattenOneOf(oneOfSchema: OneOfSchema): ObjSchema | PrimitiveSchema { - const flattenedSchema: ObjSchema = { - type: 'object', - properties: {}, - required: [], - } - - for (const subSchema of oneOfSchema.oneOf) { - const flattenedSubSchema = deepFlattenOneOfAndAllOfSchema(subSchema) - - // Check if the sub-schema is a primitive schema - if ('type' in flattenedSubSchema && !('properties' in flattenedSubSchema)) { - return { type: flattenedSubSchema.type } as PrimitiveSchema - } - - if ('$ref' in flattenedSubSchema) { - // eslint-disable-next-line no-console - console.error('$ref not currently supported when flattening oneOf') - continue - } - - const subObj = flattenedSubSchema as ObjSchema - - // Merge properties - Object.assign(flattenedSchema.properties, subObj.properties) - - // Update required array with common properties - flattenedSchema.required = - flattenedSchema.required.length === 0 - ? subObj.required - : flattenedSchema.required.filter((prop) => - subObj.required.includes(prop), - ) - } - - return flattenedSchema -} - -function flattenAllOf(allOfSchema: AllOfSchema): ObjSchema | PrimitiveSchema { - const flattenedSchema: ObjSchema = { - type: 'object', - properties: {}, - required: [], - } - - for (const subSchema of allOfSchema.allOf) { - const flattenedSubSchema = deepFlattenOneOfAndAllOfSchema(subSchema) - - // Check if the sub-schema is a primitive schema - if ('type' in flattenedSubSchema && !('properties' in flattenedSubSchema)) { - return { type: flattenedSubSchema.type } as PrimitiveSchema - } - - if ('$ref' in flattenedSubSchema) { - // eslint-disable-next-line no-console - console.error('$ref not currently supported when flattening allOf') - continue - } - - const subObj = flattenedSubSchema as ObjSchema - - // Merge properties - Object.assign(flattenedSchema.properties, subObj.properties) - - // Merge required array - flattenedSchema.required = [ - ...new Set([...flattenedSchema.required, ...subObj.required]), - ] - } - - return flattenedSchema -} - -function flattenObject(objSchema: ObjSchema): ObjSchema { - const flattenedSchema: ObjSchema = { - type: 'object', - properties: {}, - required: objSchema.required ?? [], - } - - for (const prop in objSchema.properties) { - const propSchema = objSchema.properties[prop] - if (propSchema == null) continue - flattenedSchema.properties[prop] = - deepFlattenOneOfAndAllOfSchema(propSchema) - } - - return flattenedSchema -} - -function flattenArray(arraySchema: ArraySchema): ArraySchema { - return { - type: 'array', - items: deepFlattenOneOfAndAllOfSchema(arraySchema.items), - } -} diff --git a/codegen/lib/openapi/flatten-obj-schema.ts b/codegen/lib/openapi/flatten-obj-schema.ts deleted file mode 100644 index 7e2eb809..00000000 --- a/codegen/lib/openapi/flatten-obj-schema.ts +++ /dev/null @@ -1,129 +0,0 @@ -// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator -// lib/openapi/flatten-obj-schema.ts (with the lodash intersectionWith(isEqual) -// call replaced by a plain string-array intersection; required arrays only -// ever contain strings, so the semantics are identical). This is a frozen -// output-parity workaround: it exists only so the generated output stays -// byte-identical to the previous generator. Do not review, refactor, or -// improve it. -// TODO: Delete this file and use resource properties from @seamapi/blueprint -// once generated output is allowed to change. - -import type { - AllOfSchema, - ArraySchema, - ObjSchema, - PrimitiveSchema, - PropertySchema, -} from './types.js' - -const intersection = (a: string[], b: string[]): string[] => - a.filter((x) => b.includes(x)) - -export const flattenObjSchema = ( - s: ObjSchema | { oneOf: ObjSchema[] } | { allOf: ObjSchema[] }, -): ObjSchema => { - if ('type' in s && s.type === 'object') return s - - if ('oneOf' in s) { - if (s.oneOf[0] == null) { - throw new Error('oneOf must have at least one element') - } - - const superObj: ObjSchema = { - type: 'object', - properties: {}, - required: [...s.oneOf[0].required], - } - for (const obj of s.oneOf) { - for (const [k, v] of Object.entries(obj.properties)) { - superObj.properties[k] = v - } - superObj.required = intersection(superObj.required, obj.required) - } - return superObj - } - - if ('allOf' in s) { - return deepFlattenAllOfSchema(s) as ObjSchema - } - - throw new Error(`Unknown schema type "${(s as { type: string }).type}"`) -} - -export const deepFlattenAllOfSchema = ( - s: AllOfSchema, -): Exclude | undefined => { - if (s.allOf.length === 1 && s.allOf[0] != null) { - const recursive = s.allOf[0] - - if ('allOf' in recursive) { - return deepFlattenAllOfSchema(recursive) - } - - return recursive as Exclude - } - - const properties: Record = {} - const required = new Set() - const primitives: Array = [] - - for (let subschema of s.allOf) { - if ('allOf' in subschema) { - const recursive = deepFlattenAllOfSchema(subschema as AllOfSchema) - if (recursive == null) continue - - subschema = recursive - } - - if ('oneOf' in subschema) { - subschema = flattenObjSchema(subschema as { oneOf: ObjSchema[] }) - } - - if ('$ref' in subschema) { - // eslint-disable-next-line no-console - console.error('$ref not currently supported when flattening allOf') - continue - } - - if (subschema.type === 'object') { - const objSchema = subschema as ObjSchema - for (const [k, v] of Object.entries(objSchema.properties)) { - if (properties[k] == null) properties[k] = [] - properties[k]?.push(v) - - if (objSchema.required?.includes(k) ?? false) required.add(k) - } - - continue - } - - primitives.push(subschema as PrimitiveSchema | ArraySchema) - } - - if (Object.keys(properties).length > 0 && primitives.length > 0) { - // eslint-disable-next-line no-console - console.error( - 'Found invalid allOf schema with both properties and primitives', - new Error(JSON.stringify(s, null, 2)), - ) - } - - if (primitives.length > 0) { - // TODO: check that all primitives are the same, then merge nullability/unions - return primitives[0] - } - - if (Object.keys(properties).length > 0) { - return { - type: 'object', - required: [...required], - properties: Object.fromEntries( - Object.entries(properties) - .map(([k, v]) => [k, deepFlattenAllOfSchema({ allOf: v })]) - .filter(([, v]) => v != null), - ), - } - } - - return undefined -} diff --git a/codegen/lib/openapi/get-filtered-routes.ts b/codegen/lib/openapi/get-filtered-routes.ts deleted file mode 100644 index 289a06af..00000000 --- a/codegen/lib/openapi/get-filtered-routes.ts +++ /dev/null @@ -1,23 +0,0 @@ -// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator -// lib/openapi/get-filtered-routes.ts. This is a frozen output-parity -// workaround: it exists only so the generated output stays byte-identical to -// the previous generator. Do not review, refactor, or improve it. -// TODO: Delete this file and use route.isUndocumented from @seamapi/blueprint -// once generated output is allowed to change. - -import type { OpenapiSchema, Route } from './types.js' - -export function getFilteredRoutes(openapi: OpenapiSchema): Route[] { - return Object.entries(openapi.paths) - .filter(([, pathSchema]) => { - const post = pathSchema.post - if (post == null) return false - const summary = post.summary ?? '' - - const isDocumented = post['x-undocumented'] == null - const isSeamInternalRoute = summary.startsWith('/seam/') - - return isDocumented && !isSeamInternalRoute - }) - .map(([path, route]) => ({ path, ...route }) as Route) -} diff --git a/codegen/lib/openapi/get-parameter-and-response-schema.ts b/codegen/lib/openapi/get-parameter-and-response-schema.ts deleted file mode 100644 index cf3cd546..00000000 --- a/codegen/lib/openapi/get-parameter-and-response-schema.ts +++ /dev/null @@ -1,139 +0,0 @@ -// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator -// lib/openapi/get-parameter-and-response-schema.ts. This OpenAPI parsing is a -// frozen output-parity workaround: it exists only so the generated output -// stays byte-identical to the previous generator. Do not review, refactor, or -// improve it. -// TODO: Delete this file and use endpoint.request.parameters and -// endpoint.response from @seamapi/blueprint once generated output is allowed -// to change. - -import { deepFlattenOneOfAndAllOfSchema } from './deep-flatten-one-of-and-all-of-schema.js' -import { flattenObjSchema } from './flatten-obj-schema.js' -import type { ObjSchema, Route } from './types.js' - -export const getParameterAndResponseSchema = ( - route: Route, -): { - parameterSchema?: ObjSchema - responseObjType?: string | undefined - responseArrType?: string | undefined -} => { - const responseSchema = - route.post.responses['200']?.content?.['application/json']?.schema - - if (responseSchema == null) { - return {} - } - - if (route.post.requestBody == null) { - route.post.requestBody = { - content: { - 'application/json': { schema: { type: 'object', properties: {} } }, - }, - } - } - - if (route.post.requestBody.content?.['application/json'] == null) { - return {} - } - - const parameterSchema = processParameterSchema( - route.post.requestBody.content['application/json'].schema, - ) - - const resReturnSchema = - responseSchema.properties?.[route.post['x-response-key'] ?? ''] - - const responseObjRef = resReturnSchema?.$ref - const responseArrRef = resReturnSchema?.items?.$ref - - if (route.post['x-response-key'] === 'batch') { - return { - responseObjType: route.post['x-response-key'], - responseArrType: undefined, - parameterSchema, - } - } else if (responseObjRef == null && responseArrRef == null) { - return { - responseObjType: undefined, - responseArrType: undefined, - parameterSchema, - } - } else { - return { - responseObjType: responseObjRef?.split('/')?.pop(), - responseArrType: responseArrRef?.split('/')?.pop(), - parameterSchema, - } - } -} - -function processParameterSchema( - schema: ObjSchema | { oneOf: ObjSchema[] } | { allOf: ObjSchema[] }, -): ObjSchema { - const parameterSchema = flattenObjSchema(schema) - - for (const [paramName, paramValue] of Object.entries( - parameterSchema.properties, - )) { - if ('oneOf' in paramValue || 'allOf' in paramValue) { - parameterSchema.properties[paramName] = - deepFlattenOneOfAndAllOfSchema(paramValue) - } - } - - return stripUndocumentedProperties(parameterSchema) -} - -function stripUndocumentedProperties(schema: ObjSchema): ObjSchema { - const properties = Object.fromEntries( - Object.entries(schema.properties).flatMap(([name, propertySchema]) => { - const filteredProperty = stripUndocumentedPropertySchema(propertySchema) - - return filteredProperty != null ? [[name, filteredProperty]] : [] - }), - ) - - return { - ...schema, - properties, - required: (schema.required ?? []).filter((name) => name in properties), - } -} - -function stripUndocumentedPropertySchema(propertySchema: any): any { - if (propertySchema?.['x-undocumented'] != null) { - return undefined - } - - if (propertySchema?.type === 'object' && propertySchema.properties != null) { - const properties = Object.fromEntries( - Object.entries(propertySchema.properties).flatMap( - ([name, nestedSchema]) => { - const filteredProperty = stripUndocumentedPropertySchema(nestedSchema) - - return filteredProperty != null ? [[name, filteredProperty]] : [] - }, - ), - ) - - return { - ...propertySchema, - properties, - required: (propertySchema.required ?? []).filter( - (name: string) => name in properties, - ), - } - } - - if (propertySchema?.type === 'array' && propertySchema.items != null) { - const items = stripUndocumentedPropertySchema(propertySchema.items) - - return { - ...propertySchema, - items: items ?? propertySchema.items, - } - } - - return propertySchema -} diff --git a/codegen/lib/openapi/map-parent-to-children-resource.ts b/codegen/lib/openapi/map-parent-to-children-resource.ts deleted file mode 100644 index 780a3f1c..00000000 --- a/codegen/lib/openapi/map-parent-to-children-resource.ts +++ /dev/null @@ -1,39 +0,0 @@ -// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator -// lib/openapi/map-parent-to-children-resource.ts. This is a frozen -// output-parity workaround: it exists only so the generated output stays -// byte-identical to the previous generator. Do not review, refactor, or -// improve it. Only the first two segments of x-fern-sdk-group-name are -// considered, so deeply nested namespaces (e.g. acs.encoders.simulate) are -// generated as standalone classes but never wired to a parent property. -// TODO: Delete this file and use blueprint.namespaces parent/child -// relationships once generated output is allowed to change, wiring deeply -// nested namespaces to a property on their parent class at the same time. - -import { ignoredEndpointPaths } from '../endpoint-rules.js' -import type { Route } from './types.js' - -export const mapParentToChildResources = ( - routes: Route[], -): Record => - routes.reduce((acc: Record, route) => { - if (route.post?.['x-fern-sdk-group-name'] == null) return acc - if (ignoredEndpointPaths.includes(route.path)) return acc - - const [parentResourceName, childResourceName] = - route.post['x-fern-sdk-group-name'] - - if (parentResourceName == null) return acc - - if (acc[parentResourceName] == null) { - acc[parentResourceName] = [] - } - - if ( - childResourceName != null && - !(acc[parentResourceName]?.includes(childResourceName) ?? false) - ) { - acc[parentResourceName]?.push(childResourceName) - } - - return acc - }, {}) diff --git a/codegen/lib/openapi/types.ts b/codegen/lib/openapi/types.ts deleted file mode 100644 index 8cfe0285..00000000 --- a/codegen/lib/openapi/types.ts +++ /dev/null @@ -1,68 +0,0 @@ -// TEMPORARY: Minimal OpenAPI types ported from @seamapi/nextlove-sdk-generator. -// These only support the frozen output-parity workarounds in this directory. -// Do not review, refactor, or improve them. -// TODO: Delete this file and use @seamapi/blueprint types once generated -// output is allowed to change. - -export interface PrimitiveSchema { - type: 'string' | 'integer' | 'boolean' | 'number' - enum?: string[] - format?: string -} - -export interface ArraySchema { - type: 'array' - items: PropertySchema -} - -export interface ObjSchema { - type: 'object' - properties: Record - required: string[] -} - -export interface OneOfSchema { - oneOf: PropertySchema[] -} - -export interface AllOfSchema { - allOf: PropertySchema[] -} - -export interface RefSchema { - $ref: string -} - -export type PropertySchema = - | PrimitiveSchema - | ArraySchema - | ObjSchema - | OneOfSchema - | AllOfSchema - | RefSchema - -export interface RoutePost { - summary?: string - requestBody?: { - content?: Record - } - responses: Record }> - 'x-fern-sdk-group-name'?: string[] - 'x-fern-sdk-method-name'?: string - 'x-fern-sdk-return-value'?: string - 'x-response-key'?: string | null - 'x-undocumented'?: string - [key: string]: any -} - -export interface Route { - path: string - post: RoutePost - [key: string]: any -} - -export interface OpenapiSchema { - info: { title: string } - paths: Record - components: { schemas: Record } -} diff --git a/codegen/lib/python-type.ts b/codegen/lib/python-type.ts new file mode 100644 index 00000000..9090ce73 --- /dev/null +++ b/codegen/lib/python-type.ts @@ -0,0 +1,51 @@ +// Maps blueprint parameter and property formats to Python types. + +import type { Parameter, Property } from '@seamapi/blueprint' + +type ScalarFormat = Exclude<(Parameter | Property)['format'], 'list'> + +type ListItemFormat = Extract< + Parameter | Property, + { format: 'list' } +>['itemFormat'] + +export const mapParameterToPythonType = (parameter: Parameter): string => + parameter.format === 'list' + ? `List[${mapListItemFormatToPythonType(parameter.itemFormat)}]` + : mapScalarFormatToPythonType(parameter.format) + +export const mapPropertyToPythonType = (property: Property): string => { + if (property.format === 'list') { + return `List[${mapListItemFormatToPythonType(property.itemFormat)}]` + } + + // Batch resource properties are lists of the named resource on the wire, + // though the blueprint types them as records. + if (property.format === 'record' && 'resourceType' in property) { + return 'List[Dict[str, Any]]' + } + + return mapScalarFormatToPythonType(property.format) +} + +const mapScalarFormatToPythonType = (format: ScalarFormat): string => { + switch (format) { + case 'string': + case 'datetime': + case 'id': + case 'enum': + return 'str' + case 'number': + return 'float' + case 'boolean': + return 'bool' + case 'record': + case 'object': + return 'Dict[str, Any]' + } +} + +const mapListItemFormatToPythonType = (itemFormat: ListItemFormat): string => + itemFormat === 'discriminated_object' + ? 'Dict[str, Any]' + : mapScalarFormatToPythonType(itemFormat) diff --git a/codegen/lib/routes.ts b/codegen/lib/routes.ts index 3f8e3733..16b7d814 100644 --- a/codegen/lib/routes.ts +++ b/codegen/lib/routes.ts @@ -1,35 +1,21 @@ // The Metalsmith plugin that generates the Python SDK route files. -// Ported from @seamapi/nextlove-sdk-generator lib/generate-python-sdk/generate-python-sdk.ts, -// restructured to mirror the javascript-http codegen plugin (lib/connect.ts). // -// The blueprint from @seamapi/blueprint drives the iteration order and the -// route, endpoint, and namespace structure. The raw OpenAPI spec is still -// consulted wherever the previous nextlove generator derived output from data -// the blueprint normalizes differently; each of those spots is marked with a -// TODO so they can migrate to the blueprint once output is allowed to change, -// and the supporting code lives in files marked TEMPORARY that will be -// deleted with them. - -import type { Blueprint } from '@seamapi/blueprint' -import * as types from '@seamapi/types/connect' +// The generator is driven entirely by the Blueprint that the @seamapi/smith +// blueprint plugin places in the Metalsmith metadata; no OpenAPI parsing +// happens here. Documented blueprint routes and namespaces map to route +// classes, endpoints to methods, and the blueprint resources, events, action +// attempts, and pagination to the dataclasses in the models module. + +import type { Blueprint, Response } from '@seamapi/blueprint' import { pascalCase } from 'change-case' import type Metalsmith from 'metalsmith' import type { ClassModel } from './class-model.js' import { convertCustomResourceName } from './custom-resource-name-conversions.js' -import { - endpointsReturningDeprecatedActionAttempt, - ignoredEndpointPaths, -} from './endpoint-rules.js' import { setModelsLayoutContext } from './layouts/models.js' import { setRouteLayoutContext } from './layouts/route.js' import { setRoutesIndexLayoutContext } from './layouts/routes-index.js' -import { mapPythonType } from './map-python-type.js' -import { deepFlattenOneOfAndAllOfSchema } from './openapi/deep-flatten-one-of-and-all-of-schema.js' -import { getFilteredRoutes } from './openapi/get-filtered-routes.js' -import { getParameterAndResponseSchema } from './openapi/get-parameter-and-response-schema.js' -import { mapParentToChildResources } from './openapi/map-parent-to-children-resource.js' -import type { OpenapiSchema, PropertySchema } from './openapi/types.js' +import { mapParameterToPythonType } from './python-type.js' interface Metadata { blueprint: Blueprint @@ -37,7 +23,7 @@ interface Metadata { const rootPath = 'seam/routes' -const openapi = types.openapi as unknown as OpenapiSchema +const toNamespace = (path: string): string => path.slice(1).replaceAll('/', '_') export const routes = ( files: Metalsmith.Files, @@ -46,125 +32,76 @@ export const routes = ( const metadata = metalsmith.metadata() as Metadata const { blueprint } = metadata - // TODO: Derive the parent to child resource map from blueprint.namespaces - // once generated output is allowed to change. - const rawRoutes = getFilteredRoutes(openapi) - const parentToChildResourcesMap = mapParentToChildResources(rawRoutes) + const documentedRoutes = blueprint.routes.filter( + (route) => !route.isUndocumented, + ) + + // Namespaces group routes without endpoints of their own (e.g. /acs) but + // still produce a route class so their child classes are reachable. + const classEntries = [ + ...blueprint.namespaces.filter((namespace) => !namespace.isUndocumented), + ...documentedRoutes, + ] + .map(({ path, parentPath }) => ({ path, parentPath })) + .sort((a, b) => (a.path < b.path ? -1 : 1)) const classMap = new Map() - const namespaces: string[][] = [] - const processClass = (resourceName: string): void => { - const childClassIdentifiers = ( - parentToChildResourcesMap[resourceName] ?? [] - ).map((childResource) => ({ - className: pascalCase(`${resourceName} ${childResource}`), - namespace: childResource, - })) - const className = pascalCase(resourceName) + for (const entry of classEntries) { + const namespace = toNamespace(entry.path) + const className = pascalCase(namespace) + if (classMap.has(className)) continue classMap.set(className, { name: className, - namespace: resourceName, + namespace, methods: [], - childClassIdentifiers, + childClassIdentifiers: classEntries + .filter((child) => child.parentPath === entry.path) + .map((child) => ({ + className: pascalCase(toNamespace(child.path)), + // The property name on the parent class. The route layout derives + // the child module name as `${parent.namespace}_${namespace}`, so + // this must be the child path relative to the parent path. + namespace: child.path + .slice(entry.path.length + 1) + .replaceAll('/', '_'), + })), }) } - for (const route of blueprint.routes) { + for (const route of documentedRoutes) { + const cls = classMap.get(pascalCase(toNamespace(route.path))) + if (cls == null) continue + for (const endpoint of route.endpoints) { - const post = openapi.paths[endpoint.path]?.post - if (post == null) continue - - // TODO: Filter on endpoint.isUndocumented and route.isUndocumented from - // the blueprint once generated output is allowed to change. The raw - // OpenAPI extensions are used here to exclude exactly the same endpoint - // set as the previous nextlove generator. - if (post['x-undocumented'] != null) continue - if ((post.summary ?? '').startsWith('/seam/')) continue - if (post['x-fern-sdk-group-name'] == null) continue - if (ignoredEndpointPaths.includes(endpoint.path)) continue - - const groupNames = [...post['x-fern-sdk-group-name']] - const [baseResource] = groupNames - const namespace = groupNames.join('_') - const className = pascalCase(namespace) - - if (!classMap.has(className)) { - namespaces.push(post['x-fern-sdk-group-name']) - - processClass(namespace) - } - - /* - Special case when we don't have routes for a base resource - and thus a respective x-fern-sdk-group-name for ex. /noise_sensors - */ - if (baseResource != null && !classMap.has(pascalCase(baseResource))) { - namespaces.push([baseResource]) - - processClass(baseResource) - } - - const cls = classMap.get(className) - - if (cls == null) { - // eslint-disable-next-line no-console - console.warn(`No class for "${endpoint.path}", skipping`) - continue - } - - const { parameterSchema, responseObjType, responseArrType } = - getParameterAndResponseSchema({ path: endpoint.path, post }) - - if (parameterSchema == null) { - // eslint-disable-next-line no-console - console.warn(`No parameter schema for "${endpoint.path}", skipping`) - continue - } + if (endpoint.isUndocumented) continue + + const { response } = endpoint + const idParameterName = + endpoint.name === 'get' && response.responseType !== 'void' + ? `${response.responseKey}_id` + : null cls.methods.push({ methodName: endpoint.name, path: endpoint.path, - // TODO: Use endpoint.request.parameters from the blueprint once - // generated output is allowed to change. The blueprint collapses - // integer to number and flattens unions differently, so parameters - // are derived from the raw OpenAPI schema for identical output. - parameters: Object.entries(parameterSchema.properties) - .filter( - ([, paramVal]) => - 'type' in paramVal || - ('oneOf' in paramVal && 'type' in (paramVal.oneOf[0] ?? {})), - ) - .map(([paramName, paramVal]) => ({ - name: paramName, - type: mapPythonType( - 'type' in paramVal - ? (paramVal as PropertySchema) - : deepFlattenOneOfAndAllOfSchema(paramVal as PropertySchema), - ), - position: - endpoint.name === 'get' && - paramName === `${post['x-fern-sdk-return-value']}_id` - ? 0 - : undefined, - required: parameterSchema.required?.includes(paramName), + parameters: endpoint.request.parameters + .filter((parameter) => !parameter.isUndocumented) + .map((parameter) => ({ + name: parameter.name, + type: mapParameterToPythonType(parameter), + position: parameter.name === idParameterName ? 0 : undefined, + required: parameter.isRequired, })), - // TODO: Use endpoint.response.responseKey from the blueprint once - // generated output is allowed to change. - returnPath: [post['x-fern-sdk-return-value']], - returnResource: determineReturnResource({ - routePath: endpoint.path, - responseObjType, - responseArrType, - }), + ...resolveResponse(response), }) } } - const topLevelNamespaces = namespaces - .map((ns) => (ns.length === 1 ? ns[0] : null)) - .filter((ns): ns is string => ns != null) + const topLevelNamespaces = classEntries + .filter((entry) => entry.parentPath == null) + .map((entry) => toNamespace(entry.path)) for (const cls of classMap.values()) { const k = `${rootPath}/${cls.namespace}.py` @@ -178,7 +115,7 @@ export const routes = ( files[`${rootPath}/models.py`] = { contents: Buffer.from('\n'), layout: 'models.hbs', - ...setModelsLayoutContext(openapi, classMap, topLevelNamespaces), + ...setModelsLayoutContext(blueprint, classMap, topLevelNamespaces), } files[`${rootPath}/__init__.py`] = { @@ -188,34 +125,42 @@ export const routes = ( } } -// TEMPORARY: Verbatim port of determineReturnResource from the nextlove -// generate-python-sdk.ts. Frozen output-parity workaround; do not review, -// refactor, or improve it. -// TODO: Delete this function and derive the return resource from -// endpoint.response once generated output is allowed to change. -const determineReturnResource = ({ - routePath, - responseObjType, - responseArrType, -}: { - routePath: string - responseObjType?: string | undefined - responseArrType?: string | undefined -}): string => { - if (endpointsReturningDeprecatedActionAttempt.includes(routePath)) { - return 'None' +const resolveResponse = ( + response: Response, +): { returnPath: string[]; returnResource: string } => { + if (response.responseType === 'void') { + return { returnPath: [], returnResource: 'None' } } - const responseType = responseObjType ?? responseArrType + const returnPath = [response.responseKey] + + if (response.responseType === 'resource') { + if (response.resourceType === 'action_attempt') { + return { returnPath, returnResource: 'ActionAttempt' } + } - if (responseType != null) { - const convertedResponseType = convertCustomResourceName(responseType) - const formattedResponseType = pascalCase(convertedResponseType) + // Batch responses report resourceType as 'unknown'; the batch resource + // itself is named by the response key. + if (response.batchResourceTypes != null) { + return { returnPath, returnResource: pascalCase(response.responseKey) } + } + } - return responseObjType != null - ? formattedResponseType - : `List[${formattedResponseType}]` + // Some endpoints respond with a resource the blueprint has no schema for + // (e.g. unmanaged variants); there is no model class to deserialize into. + if (response.resourceType === 'unknown') { + return { returnPath: [], returnResource: 'None' } } - return 'None' + const returnResource = pascalCase( + convertCustomResourceName(response.resourceType), + ) + + return { + returnPath, + returnResource: + response.responseType === 'resource_list' + ? `List[${returnResource}]` + : returnResource, + } } diff --git a/seam/routes/access_codes.py b/seam/routes/access_codes.py index 0f811531..0a20b955 100644 --- a/seam/routes/access_codes.py +++ b/seam/routes/access_codes.py @@ -231,9 +231,9 @@ def report_device_constraints( self, *, device_id: str, - max_code_length: Optional[int] = None, - min_code_length: Optional[int] = None, - supported_code_lengths: Optional[List[int]] = None + max_code_length: Optional[float] = None, + min_code_length: Optional[float] = None, + supported_code_lengths: Optional[List[float]] = None ) -> None: json_payload = {} diff --git a/seam/routes/acs_encoders.py b/seam/routes/acs_encoders.py index 5909336d..f6892c18 100644 --- a/seam/routes/acs_encoders.py +++ b/seam/routes/acs_encoders.py @@ -1,7 +1,7 @@ from typing import Optional, Any, List, Dict, Union from ..client import SeamHttpClient from .models import AbstractAcsEncoders, ActionAttempt, AcsEncoder - +from .acs_encoders_simulate import AcsEncodersSimulate from ..modules.action_attempts import resolve_action_attempt @@ -9,6 +9,11 @@ class AcsEncoders(AbstractAcsEncoders): def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + self._simulate = AcsEncodersSimulate(client=client, defaults=defaults) + + @property + def simulate(self) -> AcsEncodersSimulate: + return self._simulate def encode_credential( self, diff --git a/seam/routes/acs_entrances.py b/seam/routes/acs_entrances.py index 4e96150b..f02a65d3 100644 --- a/seam/routes/acs_entrances.py +++ b/seam/routes/acs_entrances.py @@ -48,7 +48,7 @@ def list( acs_system_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, location_id: Optional[str] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, diff --git a/seam/routes/acs_users.py b/seam/routes/acs_users.py index 9452bd1c..c6b47d4a 100644 --- a/seam/routes/acs_users.py +++ b/seam/routes/acs_users.py @@ -80,16 +80,16 @@ def delete( def get( self, *, - acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, + acs_system_id: Optional[str] = None, user_identity_id: Optional[str] = None ) -> AcsUser: json_payload = {} - if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id if acs_user_id is not None: json_payload["acs_user_id"] = acs_user_id + if acs_system_id is not None: + json_payload["acs_system_id"] = acs_system_id if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id @@ -102,7 +102,7 @@ def list( *, acs_system_id: Optional[str] = None, created_before: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, user_identity_email_address: Optional[str] = None, diff --git a/seam/routes/action_attempts.py b/seam/routes/action_attempts.py index 9be0575a..0a864918 100644 --- a/seam/routes/action_attempts.py +++ b/seam/routes/action_attempts.py @@ -40,7 +40,7 @@ def list( *, action_attempt_ids: Optional[List[str]] = None, device_id: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None ) -> List[ActionAttempt]: json_payload = {} diff --git a/seam/routes/connected_accounts.py b/seam/routes/connected_accounts.py index 05a55b8f..5ae276fc 100644 --- a/seam/routes/connected_accounts.py +++ b/seam/routes/connected_accounts.py @@ -43,7 +43,7 @@ def list( *, custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, space_id: Optional[str] = None, diff --git a/seam/routes/events.py b/seam/routes/events.py index b4b46667..b806dc59 100644 --- a/seam/routes/events.py +++ b/seam/routes/events.py @@ -11,16 +11,16 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): def get( self, *, - device_id: Optional[str] = None, event_id: Optional[str] = None, + device_id: Optional[str] = None, event_type: Optional[str] = None ) -> SeamEvent: json_payload = {} - if device_id is not None: - json_payload["device_id"] = device_id if event_id is not None: json_payload["event_id"] = event_id + if device_id is not None: + json_payload["device_id"] = device_id if event_type is not None: json_payload["event_type"] = event_type @@ -44,7 +44,7 @@ def list( acs_system_id: Optional[str] = None, acs_system_ids: Optional[List[str]] = None, acs_user_id: Optional[str] = None, - between: Optional[List[str]] = None, + between: Optional[List[Dict[str, Any]]] = None, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, diff --git a/seam/routes/models.py b/seam/routes/models.py index c64867e7..e65f3999 100644 --- a/seam/routes/models.py +++ b/seam/routes/models.py @@ -527,7 +527,7 @@ class ActionAttempt: action_attempt_id: str action_type: str error: Dict[str, Any] - result: Any + result: Dict[str, Any] status: str @staticmethod @@ -536,41 +536,41 @@ def from_dict(d: Dict[str, Any]): action_attempt_id=d.get("action_attempt_id", None), action_type=d.get("action_type", None), error=DeepAttrDict(d.get("error", None)), - result=d.get("result", None), + result=DeepAttrDict(d.get("result", None)), status=d.get("status", None), ) @dataclass class Batch: - access_codes: List[Any] - access_grants: List[Any] - access_methods: List[Any] - acs_access_groups: List[Any] - acs_credentials: List[Any] - acs_encoders: List[Any] - acs_entrances: List[Any] - acs_systems: List[Any] - acs_users: List[Any] - action_attempts: List[Any] - client_sessions: List[Any] - connect_webviews: List[Any] - connected_accounts: List[Any] - customization_profiles: List[Any] - devices: List[Any] - events: List[Any] - instant_keys: List[Any] - noise_thresholds: List[Any] - spaces: List[Any] - thermostat_daily_programs: List[Any] - thermostat_schedules: List[Any] - unmanaged_access_codes: List[Any] - unmanaged_acs_access_groups: List[Any] - unmanaged_acs_credentials: List[Any] - unmanaged_acs_users: List[Any] - unmanaged_devices: List[Any] - user_identities: List[Any] - workspaces: List[Any] + access_codes: List[Dict[str, Any]] + access_grants: List[Dict[str, Any]] + access_methods: List[Dict[str, Any]] + acs_access_groups: List[Dict[str, Any]] + acs_credentials: List[Dict[str, Any]] + acs_encoders: List[Dict[str, Any]] + acs_entrances: List[Dict[str, Any]] + acs_systems: List[Dict[str, Any]] + acs_users: List[Dict[str, Any]] + action_attempts: List[Dict[str, Any]] + client_sessions: List[Dict[str, Any]] + connect_webviews: List[Dict[str, Any]] + connected_accounts: List[Dict[str, Any]] + customization_profiles: List[Dict[str, Any]] + devices: List[Dict[str, Any]] + events: List[Dict[str, Any]] + instant_keys: List[Dict[str, Any]] + noise_thresholds: List[Dict[str, Any]] + spaces: List[Dict[str, Any]] + thermostat_daily_programs: List[Dict[str, Any]] + thermostat_schedules: List[Dict[str, Any]] + unmanaged_access_codes: List[Dict[str, Any]] + unmanaged_acs_access_groups: List[Dict[str, Any]] + unmanaged_acs_credentials: List[Dict[str, Any]] + unmanaged_acs_users: List[Dict[str, Any]] + unmanaged_devices: List[Dict[str, Any]] + user_identities: List[Dict[str, Any]] + workspaces: List[Dict[str, Any]] @staticmethod def from_dict(d: Dict[str, Any]): @@ -893,13 +893,13 @@ class Device: device_id: str device_manufacturer: Dict[str, Any] device_provider: Dict[str, Any] - device_type: Any + device_type: str display_name: str errors: List[Dict[str, Any]] is_managed: bool location: Dict[str, Any] nickname: str - properties: Any + properties: Dict[str, Any] space_ids: List[str] warnings: List[Dict[str, Any]] workspace_id: str @@ -1363,23 +1363,6 @@ def from_dict(d: Dict[str, Any]): ) -@dataclass -class PhoneRegistration: - is_being_activated: bool - phone_registration_id: str - provider_name: str - provider_state: Any - - @staticmethod - def from_dict(d: Dict[str, Any]): - return PhoneRegistration( - is_being_activated=d.get("is_being_activated", None), - phone_registration_id=d.get("phone_registration_id", None), - provider_name=d.get("provider_name", None), - provider_state=d.get("provider_state", None), - ) - - @dataclass class PhoneSession: is_sandbox_workspace: bool @@ -1498,7 +1481,7 @@ class ThermostatSchedule: ends_at: str errors: List[Dict[str, Any]] is_override_allowed: bool - max_override_period_minutes: int + max_override_period_minutes: float name: str starts_at: str thermostat_schedule_id: str @@ -1768,7 +1751,7 @@ class UnmanagedDevice: created_at: str custom_metadata: Dict[str, Any] device_id: str - device_type: Any + device_type: str errors: List[Dict[str, Any]] is_managed: bool location: Dict[str, Any] @@ -2150,58 +2133,6 @@ def update( raise NotImplementedError() -class AbstractAcsEncoders(abc.ABC): - - @abc.abstractmethod - def encode_credential( - self, - *, - acs_encoder_id: str, - access_method_id: Optional[str] = None, - acs_credential_id: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None - ) -> ActionAttempt: - raise NotImplementedError() - - @abc.abstractmethod - def get(self, *, acs_encoder_id: str) -> AcsEncoder: - raise NotImplementedError() - - @abc.abstractmethod - def list( - self, - *, - acs_system_id: Optional[str] = None, - acs_system_ids: Optional[List[str]] = None, - acs_encoder_ids: Optional[List[str]] = None, - limit: Optional[float] = None, - page_cursor: Optional[str] = None - ) -> List[AcsEncoder]: - raise NotImplementedError() - - @abc.abstractmethod - def scan_credential( - self, - *, - acs_encoder_id: str, - salto_ks_metadata: Optional[Dict[str, Any]] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None - ) -> ActionAttempt: - raise NotImplementedError() - - @abc.abstractmethod - def scan_to_assign_credential( - self, - *, - acs_encoder_id: str, - acs_user_id: Optional[str] = None, - salto_ks_metadata: Optional[Dict[str, Any]] = None, - user_identity_id: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None - ) -> ActionAttempt: - raise NotImplementedError() - - class AbstractAcsEncodersSimulate(abc.ABC): @abc.abstractmethod @@ -2266,7 +2197,7 @@ def list( acs_system_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, location_id: Optional[str] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, @@ -2361,8 +2292,8 @@ def delete( def get( self, *, - acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, + acs_system_id: Optional[str] = None, user_identity_id: Optional[str] = None ) -> AcsUser: raise NotImplementedError() @@ -2373,7 +2304,7 @@ def list( *, acs_system_id: Optional[str] = None, created_before: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, user_identity_email_address: Optional[str] = None, @@ -2466,7 +2397,7 @@ def list( *, action_attempt_ids: Optional[List[str]] = None, device_id: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None ) -> List[ActionAttempt]: raise NotImplementedError() @@ -2740,8 +2671,8 @@ class AbstractEvents(abc.ABC): def get( self, *, - device_id: Optional[str] = None, event_id: Optional[str] = None, + device_id: Optional[str] = None, event_type: Optional[str] = None ) -> SeamEvent: raise NotImplementedError() @@ -2763,7 +2694,7 @@ def list( acs_system_id: Optional[str] = None, acs_system_ids: Optional[List[str]] = None, acs_user_id: Optional[str] = None, - between: Optional[List[str]] = None, + between: Optional[List[Dict[str, Any]]] = None, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, @@ -3015,7 +2946,7 @@ def create( ends_at: str, starts_at: str, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[float] = None, name: Optional[str] = None ) -> ThermostatSchedule: raise NotImplementedError() @@ -3042,7 +2973,7 @@ def update( climate_preset_key: Optional[str] = None, ends_at: Optional[str] = None, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[float] = None, name: Optional[str] = None, starts_at: Optional[str] = None ) -> None: @@ -3086,7 +3017,7 @@ def list( self, *, created_before: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None, search: Optional[str] = None ) -> None: @@ -3165,6 +3096,19 @@ def reset_sandbox( ) -> ActionAttempt: raise NotImplementedError() + @abc.abstractmethod + def update( + self, + *, + connect_partner_name: Optional[str] = None, + connect_webview_customization: Optional[Dict[str, Any]] = None, + is_publishable_key_auth_enabled: Optional[bool] = None, + is_suspended: Optional[bool] = None, + name: Optional[str] = None, + organization_id: Optional[str] = None + ) -> None: + raise NotImplementedError() + class AbstractAccessGrants(abc.ABC): @@ -3333,6 +3277,63 @@ def unlock_door( raise NotImplementedError() +class AbstractAcsEncoders(abc.ABC): + + @property + @abc.abstractmethod + def simulate(self) -> AbstractAcsEncodersSimulate: + raise NotImplementedError() + + @abc.abstractmethod + def encode_credential( + self, + *, + acs_encoder_id: str, + access_method_id: Optional[str] = None, + acs_credential_id: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + ) -> ActionAttempt: + raise NotImplementedError() + + @abc.abstractmethod + def get(self, *, acs_encoder_id: str) -> AcsEncoder: + raise NotImplementedError() + + @abc.abstractmethod + def list( + self, + *, + acs_system_id: Optional[str] = None, + acs_system_ids: Optional[List[str]] = None, + acs_encoder_ids: Optional[List[str]] = None, + limit: Optional[float] = None, + page_cursor: Optional[str] = None + ) -> List[AcsEncoder]: + raise NotImplementedError() + + @abc.abstractmethod + def scan_credential( + self, + *, + acs_encoder_id: str, + salto_ks_metadata: Optional[Dict[str, Any]] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + ) -> ActionAttempt: + raise NotImplementedError() + + @abc.abstractmethod + def scan_to_assign_credential( + self, + *, + acs_encoder_id: str, + acs_user_id: Optional[str] = None, + salto_ks_metadata: Optional[Dict[str, Any]] = None, + user_identity_id: Optional[str] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + ) -> ActionAttempt: + raise NotImplementedError() + + class AbstractConnectedAccounts(abc.ABC): @property @@ -3356,7 +3357,7 @@ def list( *, custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, space_id: Optional[str] = None, @@ -3535,7 +3536,7 @@ def list( *, created_before: Optional[str] = None, credential_manager_acs_system_id: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, user_identity_ids: Optional[List[str]] = None @@ -3677,9 +3678,9 @@ def report_device_constraints( self, *, device_id: str, - max_code_length: Optional[int] = None, - min_code_length: Optional[int] = None, - supported_code_lengths: Optional[List[int]] = None + max_code_length: Optional[float] = None, + min_code_length: Optional[float] = None, + supported_code_lengths: Optional[List[float]] = None ) -> None: raise NotImplementedError() diff --git a/seam/routes/thermostats_schedules.py b/seam/routes/thermostats_schedules.py index 1632d587..3f7a5562 100644 --- a/seam/routes/thermostats_schedules.py +++ b/seam/routes/thermostats_schedules.py @@ -16,7 +16,7 @@ def create( ends_at: str, starts_at: str, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[float] = None, name: Optional[str] = None ) -> ThermostatSchedule: json_payload = {} @@ -83,7 +83,7 @@ def update( climate_preset_key: Optional[str] = None, ends_at: Optional[str] = None, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[float] = None, name: Optional[str] = None, starts_at: Optional[str] = None ) -> None: diff --git a/seam/routes/user_identities.py b/seam/routes/user_identities.py index fb138dbe..367fccf1 100644 --- a/seam/routes/user_identities.py +++ b/seam/routes/user_identities.py @@ -134,7 +134,7 @@ def list( *, created_before: Optional[str] = None, credential_manager_acs_system_id: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, user_identity_ids: Optional[List[str]] = None diff --git a/seam/routes/user_identities_unmanaged.py b/seam/routes/user_identities_unmanaged.py index 70b503b8..350f1b7c 100644 --- a/seam/routes/user_identities_unmanaged.py +++ b/seam/routes/user_identities_unmanaged.py @@ -22,7 +22,7 @@ def list( self, *, created_before: Optional[str] = None, - limit: Optional[int] = None, + limit: Optional[float] = None, page_cursor: Optional[str] = None, search: Optional[str] = None ) -> None: diff --git a/seam/routes/workspaces.py b/seam/routes/workspaces.py index 0ae97173..c622a14a 100644 --- a/seam/routes/workspaces.py +++ b/seam/routes/workspaces.py @@ -91,3 +91,36 @@ def reset_sandbox( action_attempt=ActionAttempt.from_dict(res["action_attempt"]), wait_for_action_attempt=wait_for_action_attempt, ) + + def update( + self, + *, + connect_partner_name: Optional[str] = None, + connect_webview_customization: Optional[Dict[str, Any]] = None, + is_publishable_key_auth_enabled: Optional[bool] = None, + is_suspended: Optional[bool] = None, + name: Optional[str] = None, + organization_id: Optional[str] = None + ) -> None: + json_payload = {} + + if connect_partner_name is not None: + json_payload["connect_partner_name"] = connect_partner_name + if connect_webview_customization is not None: + json_payload["connect_webview_customization"] = ( + connect_webview_customization + ) + if is_publishable_key_auth_enabled is not None: + json_payload["is_publishable_key_auth_enabled"] = ( + is_publishable_key_auth_enabled + ) + if is_suspended is not None: + json_payload["is_suspended"] = is_suspended + if name is not None: + json_payload["name"] = name + if organization_id is not None: + json_payload["organization_id"] = organization_id + + self.client.post("/workspaces/update", json=json_payload) + + return None From 14b2ec85ec938c45f7de8f613c49ab7ebfa816f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 15:56:24 +0000 Subject: [PATCH 2/2] feat: Use blueprint 0.58 isInt flag for integer types Upgrade @seamapi/blueprint to 0.58.0 and map number parameters and properties with isInt to Python int, restoring the integer types that were collapsed to float when the codegen moved off the raw OpenAPI schemas. List items carry no isInt flag, so numbers inside lists remain float. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011TdqYTYvyrK8rrR7FL1Aax --- codegen/lib/python-type.ts | 20 ++++++++++++++++---- package-lock.json | 8 ++++---- package.json | 2 +- seam/routes/access_codes.py | 4 ++-- seam/routes/acs_entrances.py | 2 +- seam/routes/acs_users.py | 2 +- seam/routes/action_attempts.py | 2 +- seam/routes/connected_accounts.py | 2 +- seam/routes/models.py | 22 +++++++++++----------- seam/routes/thermostats_schedules.py | 4 ++-- seam/routes/user_identities.py | 2 +- seam/routes/user_identities_unmanaged.py | 2 +- 12 files changed, 42 insertions(+), 30 deletions(-) diff --git a/codegen/lib/python-type.ts b/codegen/lib/python-type.ts index 9090ce73..4c45255e 100644 --- a/codegen/lib/python-type.ts +++ b/codegen/lib/python-type.ts @@ -9,16 +9,27 @@ type ListItemFormat = Extract< { format: 'list' } >['itemFormat'] -export const mapParameterToPythonType = (parameter: Parameter): string => - parameter.format === 'list' - ? `List[${mapListItemFormatToPythonType(parameter.itemFormat)}]` - : mapScalarFormatToPythonType(parameter.format) +export const mapParameterToPythonType = (parameter: Parameter): string => { + if (parameter.format === 'list') { + return `List[${mapListItemFormatToPythonType(parameter.itemFormat)}]` + } + + if (parameter.format === 'number') { + return parameter.isInt ? 'int' : 'float' + } + + return mapScalarFormatToPythonType(parameter.format) +} export const mapPropertyToPythonType = (property: Property): string => { if (property.format === 'list') { return `List[${mapListItemFormatToPythonType(property.itemFormat)}]` } + if (property.format === 'number') { + return property.isInt ? 'int' : 'float' + } + // Batch resource properties are lists of the named resource on the wire, // though the blueprint types them as records. if (property.format === 'record' && 'resourceType' in property) { @@ -35,6 +46,7 @@ const mapScalarFormatToPythonType = (format: ScalarFormat): string => { case 'id': case 'enum': return 'str' + // List items have no isInt flag, so numbers in lists stay floats. case 'number': return 'float' case 'boolean': diff --git a/package-lock.json b/package-lock.json index d8f19de8..0166a0ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "@seamapi/python", "devDependencies": { - "@seamapi/blueprint": "^0.56.0", + "@seamapi/blueprint": "^0.58.0", "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^0.5.2", "@seamapi/types": "1.963.0", @@ -856,9 +856,9 @@ "license": "MIT" }, "node_modules/@seamapi/blueprint": { - "version": "0.56.0", - "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-0.56.0.tgz", - "integrity": "sha512-6trN/T2hGnoWE8l1tmHYv+QHexF1NzeG+hiGUm6nfTBIqCsndl457p10VCtfB7ew0CqSUzNkjfhrb7hww3SZPQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-0.58.0.tgz", + "integrity": "sha512-9CrIV3pcLheTg+B3Kw5AvipIAD8SXQO/Xjxa0daFlIJBjMTyxUyqcikUiKspUddedsCGudlW8ty4OShCedb9/g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9bcbceeb..8a88aa65 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "start": "fake-seam-connect --seed" }, "devDependencies": { - "@seamapi/blueprint": "^0.56.0", + "@seamapi/blueprint": "^0.58.0", "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^0.5.2", "@seamapi/types": "1.963.0", diff --git a/seam/routes/access_codes.py b/seam/routes/access_codes.py index 0a20b955..cd684b8f 100644 --- a/seam/routes/access_codes.py +++ b/seam/routes/access_codes.py @@ -231,8 +231,8 @@ def report_device_constraints( self, *, device_id: str, - max_code_length: Optional[float] = None, - min_code_length: Optional[float] = None, + max_code_length: Optional[int] = None, + min_code_length: Optional[int] = None, supported_code_lengths: Optional[List[float]] = None ) -> None: json_payload = {} diff --git a/seam/routes/acs_entrances.py b/seam/routes/acs_entrances.py index f02a65d3..4e96150b 100644 --- a/seam/routes/acs_entrances.py +++ b/seam/routes/acs_entrances.py @@ -48,7 +48,7 @@ def list( acs_system_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, location_id: Optional[str] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, diff --git a/seam/routes/acs_users.py b/seam/routes/acs_users.py index c6b47d4a..5e13889e 100644 --- a/seam/routes/acs_users.py +++ b/seam/routes/acs_users.py @@ -102,7 +102,7 @@ def list( *, acs_system_id: Optional[str] = None, created_before: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, user_identity_email_address: Optional[str] = None, diff --git a/seam/routes/action_attempts.py b/seam/routes/action_attempts.py index 0a864918..9be0575a 100644 --- a/seam/routes/action_attempts.py +++ b/seam/routes/action_attempts.py @@ -40,7 +40,7 @@ def list( *, action_attempt_ids: Optional[List[str]] = None, device_id: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None ) -> List[ActionAttempt]: json_payload = {} diff --git a/seam/routes/connected_accounts.py b/seam/routes/connected_accounts.py index 5ae276fc..05a55b8f 100644 --- a/seam/routes/connected_accounts.py +++ b/seam/routes/connected_accounts.py @@ -43,7 +43,7 @@ def list( *, custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, space_id: Optional[str] = None, diff --git a/seam/routes/models.py b/seam/routes/models.py index e65f3999..9a5dc216 100644 --- a/seam/routes/models.py +++ b/seam/routes/models.py @@ -1481,7 +1481,7 @@ class ThermostatSchedule: ends_at: str errors: List[Dict[str, Any]] is_override_allowed: bool - max_override_period_minutes: float + max_override_period_minutes: int name: str starts_at: str thermostat_schedule_id: str @@ -2197,7 +2197,7 @@ def list( acs_system_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, location_id: Optional[str] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, @@ -2304,7 +2304,7 @@ def list( *, acs_system_id: Optional[str] = None, created_before: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, user_identity_email_address: Optional[str] = None, @@ -2397,7 +2397,7 @@ def list( *, action_attempt_ids: Optional[List[str]] = None, device_id: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None ) -> List[ActionAttempt]: raise NotImplementedError() @@ -2946,7 +2946,7 @@ def create( ends_at: str, starts_at: str, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[float] = None, + max_override_period_minutes: Optional[int] = None, name: Optional[str] = None ) -> ThermostatSchedule: raise NotImplementedError() @@ -2973,7 +2973,7 @@ def update( climate_preset_key: Optional[str] = None, ends_at: Optional[str] = None, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[float] = None, + max_override_period_minutes: Optional[int] = None, name: Optional[str] = None, starts_at: Optional[str] = None ) -> None: @@ -3017,7 +3017,7 @@ def list( self, *, created_before: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None, search: Optional[str] = None ) -> None: @@ -3357,7 +3357,7 @@ def list( *, custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, space_id: Optional[str] = None, @@ -3536,7 +3536,7 @@ def list( *, created_before: Optional[str] = None, credential_manager_acs_system_id: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, user_identity_ids: Optional[List[str]] = None @@ -3678,8 +3678,8 @@ def report_device_constraints( self, *, device_id: str, - max_code_length: Optional[float] = None, - min_code_length: Optional[float] = None, + max_code_length: Optional[int] = None, + min_code_length: Optional[int] = None, supported_code_lengths: Optional[List[float]] = None ) -> None: raise NotImplementedError() diff --git a/seam/routes/thermostats_schedules.py b/seam/routes/thermostats_schedules.py index 3f7a5562..1632d587 100644 --- a/seam/routes/thermostats_schedules.py +++ b/seam/routes/thermostats_schedules.py @@ -16,7 +16,7 @@ def create( ends_at: str, starts_at: str, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[float] = None, + max_override_period_minutes: Optional[int] = None, name: Optional[str] = None ) -> ThermostatSchedule: json_payload = {} @@ -83,7 +83,7 @@ def update( climate_preset_key: Optional[str] = None, ends_at: Optional[str] = None, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[float] = None, + max_override_period_minutes: Optional[int] = None, name: Optional[str] = None, starts_at: Optional[str] = None ) -> None: diff --git a/seam/routes/user_identities.py b/seam/routes/user_identities.py index 367fccf1..fb138dbe 100644 --- a/seam/routes/user_identities.py +++ b/seam/routes/user_identities.py @@ -134,7 +134,7 @@ def list( *, created_before: Optional[str] = None, credential_manager_acs_system_id: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None, search: Optional[str] = None, user_identity_ids: Optional[List[str]] = None diff --git a/seam/routes/user_identities_unmanaged.py b/seam/routes/user_identities_unmanaged.py index 350f1b7c..70b503b8 100644 --- a/seam/routes/user_identities_unmanaged.py +++ b/seam/routes/user_identities_unmanaged.py @@ -22,7 +22,7 @@ def list( self, *, created_before: Optional[str] = None, - limit: Optional[float] = None, + limit: Optional[int] = None, page_cursor: Optional[str] = None, search: Optional[str] = None ) -> None: