Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions codegen/layouts/partials/resource-dataclass.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@dataclass
class {{className}}:
class {{className}}{{#if isNested}}(ResourceMapping){{/if}}:
"""{{{indent (pythonDoc description) 4}}}{{#each properties}}

:ivar {{pythonIdentifier name}}: {{#if isDeprecated}}Deprecated{{#if deprecationMessage}}: {{{indent (pythonDoc deprecationMessage) 4}}}{{else}}.{{/if}}{{#if (pythonDoc description)}} {{/if}}{{/if}}{{{indent (pythonDoc description) 4}}}{{/each}}{{#if isDeprecated}}
Expand All @@ -10,10 +10,10 @@ class {{className}}:
{{pythonIdentifier name}}: {{type}}
{{/each}}

@staticmethod
def from_dict(d: Dict[str, Any]):
return {{className}}(
@classmethod
def from_dict(cls, d: Dict[str, Any]):
return cls(
{{#each properties}}
{{pythonIdentifier name}}={{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}},
{{pythonIdentifier name}}={{#if isObject}}{{type}}.from_dict(d.get("{{name}}")) if d.get("{{name}}") is not None else None{{else}}{{#if isObjectList}}[{{listItemType type}}.from_dict(i) for i in d.get("{{name}}") or []]{{else}}{{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}}{{/if}}{{/if}},
{{/each}}
)
5 changes: 4 additions & 1 deletion codegen/layouts/resource.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from typing import Any, Dict, List, Optional, Union
from dataclasses import dataclass
from ..utils.deep_attr_dict import DeepAttrDict
from ..utils.resource_mapping import ResourceMapping


{{#each nestedClasses}}
{{> resource-dataclass isNested=true}}
{{/each}}
{{> resource-dataclass}}
108 changes: 86 additions & 22 deletions codegen/lib/layouts/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Each blueprint resource, along with events, action attempts, and pagination,
// becomes a dataclass in its own module, re-exported from seam/resources/__init__.py.

import type { Blueprint, Property, Resource } from '@seamapi/blueprint'
import type { Blueprint, Property } from '@seamapi/blueprint'
import { pascalCase, snakeCase } from 'change-case'

import { convertCustomResourceName } from '../custom-resource-name-conversions.js'
Expand All @@ -14,14 +14,25 @@ export interface ResourceLayoutContext {
description: string
isDeprecated: boolean
deprecationMessage: string
properties: Array<{
name: string
description: string
isDeprecated: boolean
deprecationMessage: string
type: string
isDictParam: boolean
}>
nestedClasses: ResourceClassLayoutContext[]
properties: ResourcePropertyLayoutContext[]
}

interface ResourceClassLayoutContext {
className: string
description: string
properties: ResourcePropertyLayoutContext[]
}

interface ResourcePropertyLayoutContext {
name: string
description: string
isDeprecated: boolean
deprecationMessage: string
type: string
isDictParam: boolean
isObject: boolean
isObjectList: boolean
}

export interface ResourcesIndexLayoutContext {
Expand All @@ -31,7 +42,9 @@ export interface ResourcesIndexLayoutContext {
// The action attempt and event variants each generate a single dataclass with
// the union of the variant properties. The first occurrence of a property
// name wins.
const mergeResourceProperties = (resources: Resource[]): Property[] => {
const mergeResourceProperties = (
resources: Array<{ properties: Property[] }>,
): Property[] => {
const merged = new Map<string, Property>()
for (const { properties } of resources) {
for (const property of properties) {
Expand Down Expand Up @@ -91,6 +104,67 @@ export const getResourceLayoutContexts = (
const { properties, description, isDeprecated, deprecationMessage } =
model
const className = pascalCase(convertCustomResourceName(name))
const nestedClasses = new Map<string, ResourceClassLayoutContext>()

const buildProperties = (
sourceProperties: Property[],
): ResourcePropertyLayoutContext[] =>
sourceProperties.map((property) => {
let nestedClassName: string | undefined
let nestedProperties: Property[] | undefined
if (property.format === 'object') {
nestedClassName = `${className}${pascalCase(property.name)}`
nestedProperties = property.properties
} else if (
property.format === 'list' &&
property.itemFormat === 'object'
) {
nestedClassName = `${className}${pascalCase(property.name)}`
nestedProperties = property.itemProperties
} else if (
property.format === 'list' &&
property.itemFormat === 'discriminated_object'
) {
nestedClassName = `${className}${pascalCase(property.name)}`
nestedProperties = mergeResourceProperties(property.variants)
}

if (
nestedClassName != null &&
nestedProperties != null &&
!nestedClasses.has(nestedClassName)
) {
// Reserve the name before recursing so colliding/recursive shapes
// cannot register it twice. Reinsert after children for definition
// order: annotations are evaluated when each class is created.
nestedClasses.set(nestedClassName, {
className: nestedClassName,
description: property.description,
properties: [],
})
const childProperties = buildProperties(nestedProperties)
nestedClasses.delete(nestedClassName)
nestedClasses.set(nestedClassName, {
className: nestedClassName,
description: property.description,
properties: childProperties,
})
}

const type = mapPropertyToPythonType(property, nestedClassName)
return {
name: property.name,
description: property.description,
isDeprecated: property.isDeprecated,
deprecationMessage: property.deprecationMessage,
type,
isDictParam: type.startsWith('Dict'),
isObject: nestedClassName != null && property.format === 'object',
isObjectList: nestedClassName != null && property.format === 'list',
}
})

const resourceProperties = buildProperties(properties)
return {
className,
description,
Expand All @@ -100,18 +174,8 @@ export const getResourceLayoutContexts = (
// module always matches the dataclass it exports (e.g. the "event"
// resource becomes SeamEvent in seam_event.py).
moduleName: snakeCase(className),
properties: properties.map((property) => {
const type = mapPropertyToPythonType(property)
return {
name: property.name,
description: property.description,
isDeprecated: property.isDeprecated,
deprecationMessage: property.deprecationMessage,
type,
isDictParam:
type.startsWith('Dict') || property.name === 'properties',
}
}),
nestedClasses: [...nestedClasses.values()],
properties: resourceProperties,
}
})
.sort((a, b) => (a.moduleName < b.moduleName ? -1 : 1))
Expand Down
13 changes: 11 additions & 2 deletions codegen/lib/python-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ export const mapParameterToPythonType = (parameter: Parameter): string => {
return mapScalarFormatToPythonType(parameter.format)
}

export const mapPropertyToPythonType = (property: Property): string => {
export const mapPropertyToPythonType = (
property: Property,
nestedClassName?: string,
): string => {
if (property.format === 'list') {
return `List[${mapListItemFormatToPythonType(property.itemFormat)}]`
return `List[${
nestedClassName ?? mapListItemFormatToPythonType(property.itemFormat)
}]`
}

if (property.format === 'number') {
Expand All @@ -36,6 +41,10 @@ export const mapPropertyToPythonType = (property: Property): string => {
return 'List[Dict[str, Any]]'
}

if (property.format === 'object' && nestedClassName != null) {
return nestedClassName
}

return mapScalarFormatToPythonType(property.format)
}

Expand Down
1 change: 1 addition & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ default: build
poetry run pylint ./seam ./test
poetry run black --check .
poetry run rstcheck README.rst
poetry run mypy seam/resources --disable-error-code=arg-type --disable-error-code=import-not-found

@test:
poetry run pytest --cov=./seam
Expand Down
Loading
Loading