diff --git a/codegen/lib/class-model.ts b/codegen/lib/class-model.ts index 2d52061..2b7832a 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 9e7e9d3..0000000 --- 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 5a2e80f..7e946cc 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 1ba0480..ba7a6c6 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 88cb4ac..0000000 --- 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 3e58594..0000000 --- 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 7e2eb80..0000000 --- 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 289a06a..0000000 --- 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 cf3cd54..0000000 --- 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 780a3f1..0000000 --- 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 8cfe028..0000000 --- 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 0000000..4c45255 --- /dev/null +++ b/codegen/lib/python-type.ts @@ -0,0 +1,63 @@ +// 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 => { + 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) { + 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' + // List items have no isInt flag, so numbers in lists stay floats. + 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 3f8e373..16b7d81 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 0f81153..cd684b8 100644 --- a/seam/routes/access_codes.py +++ b/seam/routes/access_codes.py @@ -233,7 +233,7 @@ def report_device_constraints( device_id: str, max_code_length: Optional[int] = None, min_code_length: Optional[int] = None, - supported_code_lengths: Optional[List[int]] = 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 5909336..f6892c1 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_users.py b/seam/routes/acs_users.py index 9452bd1..5e13889 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 diff --git a/seam/routes/events.py b/seam/routes/events.py index b4b4666..b806dc5 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 5568b47..e72b4f4 100644 --- a/seam/routes/models.py +++ b/seam/routes/models.py @@ -529,7 +529,7 @@ class ActionAttempt: action_attempt_id: str action_type: str error: Dict[str, Any] - result: Any + result: Dict[str, Any] status: str @staticmethod @@ -538,41 +538,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]): @@ -895,13 +895,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 @@ -1365,23 +1365,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 @@ -1772,7 +1755,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] @@ -2154,58 +2137,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 @@ -2365,8 +2296,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() @@ -2744,8 +2675,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() @@ -2767,7 +2698,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, @@ -3169,6 +3100,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): @@ -3337,6 +3281,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 @@ -3683,7 +3684,7 @@ def report_device_constraints( device_id: str, max_code_length: Optional[int] = None, min_code_length: Optional[int] = None, - supported_code_lengths: Optional[List[int]] = None + supported_code_lengths: Optional[List[float]] = None ) -> None: raise NotImplementedError() diff --git a/seam/routes/workspaces.py b/seam/routes/workspaces.py index 0ae9717..c622a14 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