Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
seam/resources/** linguist-generated
seam/routes/** linguist-generated
7 changes: 6 additions & 1 deletion codegen/layouts/partials/abstract-route-class.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
class {{className}}(abc.ABC):
{{#if isDeprecated}}
""".. deprecated::
This route is deprecated."""
{{/if}}
{{#if showPass}}
pass
{{/if}}
Expand All @@ -12,6 +16,7 @@ class {{className}}(abc.ABC):
{{#each methods}}

@abc.abstractmethod
def {{name}}(self,{{#if hasParams}} *,{{/if}} {{signatureParams}}) -> {{returnType}}:
def {{> method-signature}}:
"""{{> method-docstring}}"""
raise NotImplementedError()
{{/each}}
10 changes: 10 additions & 0 deletions codegen/layouts/partials/method-docstring.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{{{indent (pythonDoc description) 8}}}{{#each params}}

:param {{name}}: {{#if isDeprecated}}Deprecated{{#if deprecationMessage}}: {{{indent (pythonDoc deprecationMessage) 8}}}{{else}}.{{/if}}{{#if (pythonDoc description)}} {{/if}}{{/if}}{{{indent (pythonDoc description) 8}}}{{/each}}{{#if (eq returnType "ActionAttempt")}}

:param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish.{{/if}}{{#unless (eq returnType "None")}}

:returns: {{{indent (pythonDoc responseDescription) 8}}}{{/unless}}{{#if isDeprecated}}

.. deprecated::
{{#if (pythonDoc deprecationMessage)}}{{{indent (pythonDoc deprecationMessage) 8}}}{{else}}This method is deprecated.{{/if}}{{/if}}
1 change: 1 addition & 0 deletions codegen/layouts/partials/method-signature.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{name}}(self{{#if params}}, *{{else}}{{#if (eq returnType "ActionAttempt")}}, *{{/if}}{{/if}}{{#each params}}, {{name}}: {{#if required}}{{type}}{{else}}Optional[{{type}}] = None{{/if}}{{/each}}{{#if (eq returnType "ActionAttempt")}}, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None{{/if}}) -> {{returnType}}
10 changes: 8 additions & 2 deletions codegen/layouts/partials/resource-dataclass.hbs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
@dataclass
class {{className}}:
"""{{{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}}

.. deprecated::
{{#if (pythonDoc deprecationMessage)}}{{{indent (pythonDoc deprecationMessage) 4}}}{{else}}This resource is deprecated.{{/if}}{{/if}}"""
{{#each properties}}
{{safeName}}: {{type}}
{{pythonIdentifier name}}: {{type}}
{{/each}}

@staticmethod
def from_dict(d: Dict[str, Any]):
return {{className}}(
{{#each properties}}
{{safeName}}={{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}},
{{pythonIdentifier name}}={{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}},
{{/each}}
)
15 changes: 8 additions & 7 deletions codegen/layouts/partials/route-method.hbs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
def {{name}}(self,{{#if hasParams}} *,{{/if}} {{signatureParams}}) -> {{returnType}}:
def {{> method-signature}}:
"""{{> method-docstring}}"""
json_payload = {}

{{#each params}}
if {{name}} is not None:
json_payload["{{name}}"] = {{name}}
{{/each}}

{{#unless returnsNone}}res = {{/unless}}self.client.post("{{path}}", json=json_payload)
{{#if pollsActionAttempt}}
{{#unless (eq returnType "None")}}res = {{/unless}}self.client.post("{{path}}", json=json_payload)
{{#if (eq returnType "ActionAttempt")}}

wait_for_action_attempt = (
self.defaults.get("wait_for_action_attempt")
Expand All @@ -20,13 +21,13 @@
action_attempt=ActionAttempt.from_dict(res["action_attempt"]),
wait_for_action_attempt=wait_for_action_attempt
)
{{else if returnsNone}}
{{else if (eq returnType "None")}}

return None
{{else if isList}}
{{else if (isListType returnType)}}

return [{{itemType}}.from_dict(item) for item in {{resAccessor}}]
return [{{listItemType returnType}}.from_dict(item) for item in res{{#each returnPath}}["{{this}}"]{{/each}}]
{{else}}

return {{returnType}}.from_dict({{resAccessor}})
return {{returnType}}.from_dict(res{{#each returnPath}}["{{this}}"]{{/each}})
{{/if}}
8 changes: 6 additions & 2 deletions codegen/layouts/route.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Optional, Any, List, Dict, Union
import abc
from ..client import SeamHttpClient
{{#if resourceImportList}}
from ..resources import ({{resourceImportList}})
{{#if resourceClasses}}
from ..resources import ({{#each resourceClasses}}{{this}}{{#unless @last}},{{/unless}}{{/each}})
{{/if}}
{{#each childClasses}}
from .{{module}} import {{abstractClassName}}, {{className}}
Expand All @@ -16,6 +16,10 @@ from ..modules.action_attempts import resolve_action_attempt


class {{className}}({{abstractClassName}}):
{{#if isDeprecated}}
""".. deprecated::
This route is deprecated."""
{{/if}}
def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]):
self.client = client
self.defaults = defaults
Expand Down
10 changes: 9 additions & 1 deletion codegen/lib/class-model.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
// Holds the class and method data assembled by the routes plugin; all string
// serialization lives in the Handlebars layouts and their context builders.
// serialization lives in the Handlebars layouts and helpers.

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
57 changes: 57 additions & 0 deletions codegen/lib/handlebars-helpers.ts
Original file line number Diff line number Diff line change
@@ -1 +1,58 @@
const PYTHON_KEYWORDS = new Set([
'False',
'None',
'True',
'and',
'as',
'assert',
'async',
'await',
'break',
'class',
'continue',
'def',
'del',
'elif',
'else',
'except',
'finally',
'for',
'from',
'global',
'if',
'import',
'in',
'is',
'lambda',
'nonlocal',
'not',
'or',
'pass',
'raise',
'return',
'try',
'while',
'with',
'yield',
])

export const identity = (x: unknown): unknown => x

// Blueprint descriptions use Markdown, while Python docstrings use
// Sphinx/reStructuredText fields.
export const pythonDoc = (value: string): string =>
value
.trim()
.replaceAll('"""', '\\"\\"\\"')
.replaceAll(/(?<!`)`([^`\n]+)`(?!`)/g, '``$1``')
.replaceAll(/\[([^\]]+)]\(([^)]+)\)/g, '`$1 <$2>`_')

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

export const pythonIdentifier = (name: string): string =>
PYTHON_KEYWORDS.has(name) ? `${name}_` : name

export const isListType = (type: string): boolean => type.startsWith('List[')

export const listItemType = (type: string): string => type.slice(5, -1)
103 changes: 47 additions & 56 deletions codegen/lib/layouts/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,17 @@ import { pascalCase, snakeCase } from 'change-case'
import { convertCustomResourceName } from '../custom-resource-name-conversions.js'
import { mapPropertyToPythonType } from '../python-type.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.
const PYTHON_KEYWORDS = new Set([
'False',
'None',
'True',
'and',
'as',
'assert',
'async',
'await',
'break',
'class',
'continue',
'def',
'del',
'elif',
'else',
'except',
'finally',
'for',
'from',
'global',
'if',
'import',
'in',
'is',
'lambda',
'nonlocal',
'not',
'or',
'pass',
'raise',
'return',
'try',
'while',
'with',
'yield',
])

const toSafeIdentifier = (name: string): string =>
PYTHON_KEYWORDS.has(name) ? `${name}_` : name

export interface ResourceLayoutContext {
className: string
moduleName: string
description: string
isDeprecated: boolean
deprecationMessage: string
properties: Array<{
name: string
safeName: string
description: string
isDeprecated: boolean
deprecationMessage: string
type: string
isDictParam: boolean
}>
Expand All @@ -84,29 +44,58 @@ 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,
description,
isDeprecated,
deprecationMessage,
// 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 All @@ -115,7 +104,9 @@ export const getResourceLayoutContexts = (
const type = mapPropertyToPythonType(property)
return {
name: property.name,
safeName: toSafeIdentifier(property.name),
description: property.description,
isDeprecated: property.isDeprecated,
deprecationMessage: property.deprecationMessage,
type,
isDictParam:
type.startsWith('Dict') || property.name === 'properties',
Expand Down
Loading
Loading