Skip to content
Closed
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
4 changes: 4 additions & 0 deletions codegen/layouts/partials/abstract-route-class.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class {{className}}(abc.ABC):
{{#if docstring}}
"""{{{docstring}}}"""
{{/if}}
{{#if showPass}}
pass
{{/if}}
Expand All @@ -13,5 +16,6 @@ class {{className}}(abc.ABC):

@abc.abstractmethod
def {{name}}(self,{{#if hasParams}} *,{{/if}} {{signatureParams}}) -> {{returnType}}:
"""{{{docstring}}}"""
raise NotImplementedError()
{{/each}}
1 change: 1 addition & 0 deletions codegen/layouts/partials/resource-dataclass.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@dataclass
class {{className}}:
"""{{{docstring}}}"""
{{#each properties}}
{{safeName}}: {{type}}
{{/each}}
Expand Down
1 change: 1 addition & 0 deletions codegen/layouts/partials/route-method.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
def {{name}}(self,{{#if hasParams}} *,{{/if}} {{signatureParams}}) -> {{returnType}}:
"""{{{docstring}}}"""
json_payload = {}

{{#each params}}
Expand Down
3 changes: 3 additions & 0 deletions codegen/layouts/route.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ from ..modules.action_attempts import resolve_action_attempt


class {{className}}({{abstractClassName}}):
{{#if docstring}}
"""{{{docstring}}}"""
{{/if}}
def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]):
self.client = client
self.defaults = defaults
Expand Down
8 changes: 8 additions & 0 deletions codegen/lib/class-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
export interface ClassMethodParameter {
name: string
type: string
description: string
isDeprecated: boolean
deprecationMessage: string
position?: number | undefined
required?: boolean | undefined
}

export interface ClassMethod {
methodName: string
path: string
description: string
responseDescription: string
isDeprecated: boolean
deprecationMessage: string
parameters: ClassMethodParameter[]
returnPath: string[]
returnResource: string
Expand All @@ -24,6 +31,7 @@ export interface ChildClassIdentifier {
export interface ClassModel {
name: string
namespace: string
isDeprecated: boolean
methods: ClassMethod[]
childClassIdentifiers: ChildClassIdentifier[]
}
Expand Down
86 changes: 77 additions & 9 deletions codegen/lib/layouts/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Blueprint, Property, Resource } from '@seamapi/blueprint'
import { pascalCase, snakeCase } from 'change-case'

import { convertCustomResourceName } from '../custom-resource-name-conversions.js'
import { formatPythonDoc } from '../python-docstring.js'
import { mapPropertyToPythonType } from '../python-type.js'

// Python hard keywords cannot be used as identifiers. When a property name
Expand Down Expand Up @@ -56,6 +57,7 @@ const toSafeIdentifier = (name: string): string =>
export interface ResourceLayoutContext {
className: string
moduleName: string
docstring: string
properties: Array<{
name: string
safeName: string
Expand All @@ -64,6 +66,40 @@ export interface ResourceLayoutContext {
}>
}

const createResourceDocstring = (
description: string,
isDeprecated: boolean,
deprecationMessage: string,
properties: Property[],
): string => {
const lines = [formatPythonDoc(description)]
for (const property of properties) {
const deprecated = property.isDeprecated
? `Deprecated${property.deprecationMessage === '' ? '.' : `: ${formatPythonDoc(property.deprecationMessage)}`}`
: ''
lines.push(
'',
`:ivar ${toSafeIdentifier(property.name)}: ${[
deprecated,
formatPythonDoc(property.description),
]
.filter(Boolean)
.join(' ')}`,
)
}
if (isDeprecated) {
lines.push(
'',
'.. deprecated::',
` ${formatPythonDoc(deprecationMessage) || 'This resource is deprecated.'}`,
)
}
return lines
.filter((line, index) => line !== '' || index !== 0)
.join('\n')
.replaceAll('\n', '\n ')
}

export interface ResourcesIndexLayoutContext {
resources: Array<{ className: string; moduleName: string }>
}
Expand All @@ -84,29 +120,61 @@ const mergeResourceProperties = (resources: Resource[]): Property[] => {
export const getResourceLayoutContexts = (
blueprint: Blueprint,
): ResourceLayoutContext[] => {
const models = new Map<string, Property[]>()
const models = new Map<
string,
{
properties: Property[]
description: string
isDeprecated: boolean
deprecationMessage: string
}
>()

for (const resource of blueprint.resources) {
models.set(resource.resourceType, resource.properties)
models.set(resource.resourceType, resource)
}

// 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))
const actionAttemptModel = models.get('action_attempt')
models.set('action_attempt', {
properties: mergeResourceProperties(blueprint.actionAttempts),
description:
actionAttemptModel?.description ??
'An attempt to perform an action in the Seam API.',
isDeprecated: actionAttemptModel?.isDeprecated ?? false,
deprecationMessage: actionAttemptModel?.deprecationMessage ?? '',
})
const eventModel = models.get('event')
models.set('event', {
properties: mergeResourceProperties(blueprint.events),
description: eventModel?.description ?? 'An event emitted by the Seam API.',
isDeprecated: eventModel?.isDeprecated ?? false,
deprecationMessage: eventModel?.deprecationMessage ?? '',
})

if (blueprint.pagination != null) {
models.set('pagination', blueprint.pagination.properties)
models.set('pagination', {
properties: blueprint.pagination.properties,
description: blueprint.pagination.description,
isDeprecated: false,
deprecationMessage: '',
})
}

return [...models.entries()]
.map(([name, properties]) => {
.map(([name, model]) => {
const { properties, description, isDeprecated, deprecationMessage } =
model
const className = pascalCase(convertCustomResourceName(name))
return {
className,
docstring: createResourceDocstring(
description,
isDeprecated,
deprecationMessage,
properties,
),
// Derived from the class name rather than the resource type so the
// module always matches the dataclass it exports (e.g. the "event"
// resource becomes SeamEvent in seam_event.py).
Expand Down
57 changes: 55 additions & 2 deletions codegen/lib/layouts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
type ClassModel,
sortClassMethodParameters,
} from '../class-model.js'
import { formatPythonDoc } from '../python-docstring.js'

export interface MethodLayoutContext {
name: string
path: string
docstring: string
hasParams: boolean
signatureParams: string
params: Array<{ name: string }>
Expand All @@ -25,19 +27,22 @@ export interface MethodLayoutContext {

export interface AbstractClassLayoutContext {
className: string
docstring: string
showPass: boolean
childProperties: Array<{ namespace: string; abstractClassName: string }>
methods: Array<{
name: string
hasParams: boolean
signatureParams: string
returnType: string
docstring: string
}>
}

export interface RouteLayoutContext {
className: string
abstractClassName: string
docstring: string
abstractClass: AbstractClassLayoutContext
resourceImportList: string
childClasses: Array<{
Expand All @@ -53,6 +58,48 @@ export interface RouteLayoutContext {
const waitForActionAttemptParameter =
'wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None'

const indentDoc = (value: string, spaces: number): string =>
value.replaceAll('\n', `\n${' '.repeat(spaces)}`)

const methodDocstring = (
method: ClassMethod,
sortedParameters: ClassMethod['parameters'],
): string => {
const lines = [formatPythonDoc(method.description)]

for (const parameter of sortedParameters) {
const deprecated = parameter.isDeprecated
? `Deprecated${parameter.deprecationMessage === '' ? '.' : `: ${formatPythonDoc(parameter.deprecationMessage)}`}`
: ''
const description = formatPythonDoc(parameter.description)
lines.push(
'',
`:param ${parameter.name}: ${[deprecated, description].filter(Boolean).join(' ')}`,
)
}

if (method.returnResource === 'ActionAttempt') {
lines.push(
'',
':param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish.',
)
}

if (method.returnResource !== 'None') {
lines.push('', `:returns: ${formatPythonDoc(method.responseDescription)}`)
}

if (method.isDeprecated) {
lines.push(
'',
'.. deprecated::',
` ${formatPythonDoc(method.deprecationMessage) || 'This method is deprecated.'}`,
)
}

return lines.filter((line, index) => line !== '' || index !== 0).join('\n')
}

export const getMethodLayoutContext = (
method: ClassMethod,
): MethodLayoutContext => {
Expand Down Expand Up @@ -83,6 +130,7 @@ export const getMethodLayoutContext = (
return {
name: methodName,
path,
docstring: indentDoc(methodDocstring(method, sortedParameters), 8),
hasParams,
signatureParams,
params: sortedParameters.map(({ name }) => ({ name })),
Expand Down Expand Up @@ -110,22 +158,27 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => {
)

const abstractClassName = `Abstract${cls.name}`
const classDocstring = cls.isDeprecated
? indentDoc('.. deprecated::\n This route is deprecated.', 4)
: ''

return {
className: cls.name,
abstractClassName,
docstring: classDocstring,
abstractClass: {
className: abstractClassName,
docstring: classDocstring,
showPass:
cls.methods.length === 0 && cls.childClassIdentifiers.length === 0,
childProperties: cls.childClassIdentifiers.map((i) => ({
namespace: i.namespace,
abstractClassName: `Abstract${i.className}`,
})),
methods: cls.methods.map((method) => {
const { name, hasParams, signatureParams, returnType } =
const { name, hasParams, signatureParams, returnType, docstring } =
getMethodLayoutContext(method)
return { name, hasParams, signatureParams, returnType }
return { name, hasParams, signatureParams, returnType, docstring }
}),
},
resourceImportList: resourceClasses.join(','),
Expand Down
9 changes: 9 additions & 0 deletions codegen/lib/python-docstring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Blueprint descriptions use Markdown, while the generated Python docstrings
// use Sphinx/reStructuredText fields. Normalize the Markdown constructs that
// would otherwise be displayed literally (or interpreted as invalid roles).
export const formatPythonDoc = (value: string): string =>
value
.trim()
.replaceAll('"""', '\\"\\"\\"')
.replaceAll(/(?<!`)`([^`\n]+)`(?!`)/g, '``$1``')
.replaceAll(/\[([^\]]+)]\(([^)]+)\)/g, '`$1 <$2>`_')
14 changes: 13 additions & 1 deletion codegen/lib/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export const routes = (
// 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, ...blueprint.routes]
.map(({ path, parentPath }) => ({ path, parentPath }))
.map(({ path, parentPath, isDeprecated }) => ({
path,
parentPath,
isDeprecated,
}))
.sort((a, b) => (a.path < b.path ? -1 : 1))

const classMap = new Map<string, ClassModel>()
Expand All @@ -55,6 +59,7 @@ export const routes = (
classMap.set(className, {
name: className,
namespace,
isDeprecated: entry.isDeprecated,
methods: [],
childClassIdentifiers: classEntries
.filter((child) => child.parentPath === entry.path)
Expand Down Expand Up @@ -84,9 +89,16 @@ export const routes = (
cls.methods.push({
methodName: endpoint.name,
path: endpoint.path,
description: endpoint.description,
responseDescription: endpoint.response.description,
isDeprecated: endpoint.isDeprecated,
deprecationMessage: endpoint.deprecationMessage,
parameters: endpoint.request.parameters.map((parameter) => ({
name: parameter.name,
type: mapParameterToPythonType(parameter),
description: parameter.description,
isDeprecated: parameter.isDeprecated,
deprecationMessage: parameter.deprecationMessage,
position: parameter.name === idParameterName ? 0 : undefined,
required: parameter.isRequired,
})),
Expand Down
Loading
Loading