Skip to content

Commit b768d1d

Browse files
razor-xclaude
andauthored
Port Python SDK route generation from nextlove to Metalsmith (#581)
* feat: Replace nextlove sdk generator with metalsmith codegen Replace the @seamapi/nextlove-sdk-generator based route generation with the metalsmith + handlebars + @seamapi/blueprint architecture used by seamapi/javascript-http. The generated output under seam/routes/ is byte-identical. The blueprint drives the route, endpoint, and namespace structure. The raw OpenAPI spec is still consulted wherever the nextlove generator derived output from data the blueprint normalizes differently (integer vs number types, resource schema set and order, parameter flattening); each of those spots carries a TODO to migrate to the blueprint once output is allowed to change. - Add codegen/ with the Metalsmith pipeline, plugin, context builders, and Handlebars layouts and partials - Remove generate-routes.js - Pin @seamapi/types at 1.910.0 for output parity and use @seamapi/blueprint 0.55.0 - Bump del to ^8 to satisfy the @seamapi/smith peer range - Ignore *.hbs and CODEGEN.md in a new .prettierignore since the glimmer parser mangles Python-flavored templates - Re-include codegen/lib/ in .gitignore (the Python template ignores lib/) and ignore the generated CODEGEN.md seed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iX1vWXfXKzUyRJTVm8D7s * docs: Mark all deferred codegen workarounds with TODO Every comment describing behavior to address later now leads with TODO so the deferred work is greppable: the parameter comparator precedence quirk and the unwired deeply nested namespaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iX1vWXfXKzUyRJTVm8D7s * refactor: Simplify codegen setup to match seamapi/docs conventions - Trim devDependencies to direct imports only: metalsmith, @metalsmith/layouts, jstransformer-handlebars, del, tsx, typescript, eslint, jiti, and mkdirp are @seamapi/smith peers that npm installs automatically, matching how seamapi/docs consumes smith - Add tsconfig.json extending @seamapi/smith/tsconfig.base.json with a typecheck script and codegen/index.ts stub, mirroring seamapi/docs and seamapi/javascript-http - Widen response type properties for exactOptionalPropertyTypes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iX1vWXfXKzUyRJTVm8D7s * refactor: Align codegen scripts and imports with seamapi/docs - Add lint, postlint, and preformat scripts using @seamapi/smith/eslint-config via a one-line eslint.config.ts, matching seamapi/docs - Replace relative parent imports with lib/* path imports (tsconfig baseUrl and paths), satisfying import/no-relative-parent-imports and matching the smith import sort convention - Mark every file and function that exists only for output parity with a TEMPORARY banner and a TODO to delete it once generated output is allowed to change, so temp code is skippable in review Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iX1vWXfXKzUyRJTVm8D7s * refactor: Drop tsconfig paths in favor of relative imports Match how seamapi/docs consumes @seamapi/smith: extend the base tsconfig with no compiler option overrides and use plain relative imports in codegen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iX1vWXfXKzUyRJTVm8D7s * fix: Move codegen gitignore rules to conventional positions CODEGEN.md moves to the top of the file. The codegen/lib/ negation moves next to the lib/ pattern it negates, since a gitignore negation only takes effect after the pattern it overrides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iX1vWXfXKzUyRJTVm8D7s * fix: Track the codegen content seed The CODEGEN.md gitignore rule matches any depth, so the Metalsmith source seed codegen/content/CODEGEN.md was never committed and CI failed to generate with ENOENT on the missing directory. Force-add the seed exactly as seamapi/javascript-http does; the ignore rule still covers the generated CODEGEN.md at the repository root. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iX1vWXfXKzUyRJTVm8D7s * fix: Remove gitignore comment per review Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iX1vWXfXKzUyRJTVm8D7s * Update .gitignore to include codegen/lib exception Add exception for codegen/lib in .gitignore --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d41bc9e commit b768d1d

34 files changed

Lines changed: 7058 additions & 973 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
# For more details, visit the project page:
66
# https://github.com/github/gitignore
77

8+
# Codegen
9+
CODEGEN.md
10+
811
# Temporary development files
912
tmp
1013

@@ -325,3 +328,6 @@ dist
325328
# yalc
326329
.yalc/
327330
yalc.lock
331+
332+
# Codegen exceptions
333+
!codegen/lib/

.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Prettier exceptions
2+
3+
*.hbs
4+
CODEGEN.md

codegen/content/CODEGEN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Codegen

codegen/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default null

codegen/layouts/default.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{contents}}

codegen/layouts/models.hbs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Any, Dict, List, Optional, Union
2+
from typing_extensions import Self
3+
import abc
4+
from dataclasses import dataclass
5+
from ..utils.deep_attr_dict import DeepAttrDict
6+
7+
{{#each resources}}
8+
9+
{{> model-dataclass}}
10+
{{/each}}
11+
{{#each abstractClasses}}
12+
13+
{{> abstract-route-class}}
14+
{{/each}}
15+
16+
17+
{{> abstract-routes}}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class {{className}}(abc.ABC):
2+
{{#if showPass}}
3+
pass
4+
{{/if}}
5+
{{#each childProperties}}
6+
7+
@property
8+
@abc.abstractmethod
9+
def {{namespace}}(self) -> {{abstractClassName}}:
10+
raise NotImplementedError()
11+
{{/each}}
12+
{{#each methods}}
13+
14+
@abc.abstractmethod
15+
def {{name}}(self,{{#if hasParams}} *,{{/if}} {{signatureParams}}) -> {{returnType}}:
16+
raise NotImplementedError()
17+
{{/each}}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@dataclass
2+
class AbstractRoutes(abc.ABC):
3+
{{#each routesNamespaces}}
4+
{{namespace}}: {{abstractClassName}}
5+
{{/each}}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@dataclass
2+
class {{className}}:
3+
{{#each properties}}
4+
{{name}}: {{type}}
5+
{{/each}}
6+
7+
@staticmethod
8+
def from_dict(d: Dict[str, Any]):
9+
return {{className}}(
10+
{{#each properties}}
11+
{{name}}={{#if isDictParam}}DeepAttrDict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}},
12+
{{/each}}
13+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
def {{name}}(self,{{#if hasParams}} *,{{/if}} {{signatureParams}}) -> {{returnType}}:
2+
json_payload = {}
3+
4+
{{#each params}}
5+
if {{name}} is not None:
6+
json_payload["{{name}}"] = {{name}}
7+
{{/each}}
8+
9+
{{#unless returnsNone}}res = {{/unless}}self.client.post("{{path}}", json=json_payload)
10+
{{#if pollsActionAttempt}}
11+
12+
wait_for_action_attempt = (
13+
self.defaults.get("wait_for_action_attempt")
14+
if wait_for_action_attempt is None
15+
else wait_for_action_attempt
16+
)
17+
18+
return resolve_action_attempt(
19+
client=self.client,
20+
action_attempt=ActionAttempt.from_dict(res["action_attempt"]),
21+
wait_for_action_attempt=wait_for_action_attempt
22+
)
23+
{{else if returnsNone}}
24+
25+
return None
26+
{{else if isList}}
27+
28+
return [{{itemType}}.from_dict(item) for item in {{resAccessor}}]
29+
{{else}}
30+
31+
return {{returnType}}.from_dict({{resAccessor}})
32+
{{/if}}

0 commit comments

Comments
 (0)