@aws/nx-plugin 1.0.0-rc.98 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-THIRD-PARTY +11686 -23398
- package/README.md +18 -0
- package/generators.json +7 -0
- package/migrations.json +34 -1
- package/package.json +6 -6
- package/src/agentcore-gateway/schema.json +1 -0
- package/src/agentcore-harness/generator.d.ts +1 -1
- package/src/connection/scaffold-catalog.d.ts +4 -0
- package/src/connection/scaffold-catalog.js +3 -6
- package/src/connection/scaffold-catalog.js.map +1 -1
- package/src/infra/app/generator.d.ts +1 -1
- package/src/init/generator.d.ts +1 -1
- package/src/mcp-server/guide-pipeline.d.ts +2 -1
- package/src/mcp-server/guide-pipeline.js +6 -0
- package/src/mcp-server/guide-pipeline.js.map +1 -1
- package/src/mcp-server/mdx-ast.d.ts +7 -0
- package/src/mcp-server/mdx-ast.js +15 -0
- package/src/mcp-server/mdx-ast.js.map +1 -1
- package/src/mcp-server/schema-registry.d.ts +2 -2
- package/src/mcp-server/schema-registry.js +2 -2
- package/src/mcp-server/schema-registry.js.map +1 -1
- package/src/open-api/py-client/__snapshots__/generator.additional-properties.spec.ts.snap +34 -0
- package/src/open-api/py-client/__snapshots__/generator.arrays.spec.ts.snap +1750 -0
- package/src/open-api/py-client/__snapshots__/generator.complex-types.spec.ts.snap +82 -0
- package/src/open-api/py-client/__snapshots__/generator.composite-types.spec.ts.snap +447 -0
- package/src/open-api/py-client/__snapshots__/generator.duplicate-types.spec.ts.snap +404 -0
- package/src/open-api/py-client/__snapshots__/generator.errors.spec.ts.snap +430 -0
- package/src/open-api/py-client/__snapshots__/generator.fast-api.spec.ts.snap +447 -0
- package/src/open-api/py-client/__snapshots__/generator.petstore.spec.ts.snap +3488 -0
- package/src/open-api/py-client/__snapshots__/generator.primitive-types.spec.ts.snap +1444 -0
- package/src/open-api/py-client/__snapshots__/generator.request.spec.ts.snap +390 -0
- package/src/open-api/py-client/__snapshots__/generator.reserved-keywords.spec.ts.snap +364 -0
- package/src/open-api/py-client/__snapshots__/generator.response.spec.ts.snap +739 -0
- package/src/open-api/py-client/__snapshots__/generator.streaming.spec.ts.snap +384 -0
- package/src/open-api/py-client/__snapshots__/generator.tags.spec.ts.snap +414 -0
- package/src/open-api/py-client/files/client/__clientModuleName__.py.template +1076 -0
- package/src/open-api/py-client/files/shared/__init__.py.template +28 -0
- package/src/open-api/py-client/files/shared/errors.py.template +37 -0
- package/src/open-api/py-client/files/shared/types.py.template +319 -0
- package/src/open-api/py-client/generator.d.ts +53 -0
- package/src/open-api/py-client/generator.js +119 -0
- package/src/open-api/py-client/generator.js.map +1 -0
- package/src/open-api/py-client/schema.d.js +6 -0
- package/src/open-api/py-client/schema.d.js.map +1 -0
- package/src/open-api/py-client/schema.d.ts +12 -0
- package/src/open-api/py-client/schema.json +28 -0
- package/src/open-api/py-client/vessel-registry-spec.json +2716 -0
- package/src/open-api/ts-client/__snapshots__/generator.edge-cases.spec.ts.snap +274 -0
- package/src/open-api/ts-client/petstore-spec.d.ts +6 -0
- package/src/open-api/ts-client/petstore-spec.js +1275 -0
- package/src/open-api/ts-client/petstore-spec.js.map +1 -0
- package/src/open-api/ts-hooks/generator.spec.tsx +1 -1
- package/src/open-api/utils/codegen-data/languages.d.ts +68 -2
- package/src/open-api/utils/codegen-data/languages.js +433 -21
- package/src/open-api/utils/codegen-data/languages.js.map +1 -1
- package/src/open-api/utils/codegen-data/types.d.ts +187 -0
- package/src/open-api/utils/codegen-data/types.js +10 -1
- package/src/open-api/utils/codegen-data/types.js.map +1 -1
- package/src/open-api/utils/codegen-data.d.ts +40 -1
- package/src/open-api/utils/codegen-data.js +622 -17
- package/src/open-api/utils/codegen-data.js.map +1 -1
- package/src/open-api/utils/normalise.js +175 -41
- package/src/open-api/utils/normalise.js.map +1 -1
- package/src/open-api/utils/parser.js +1 -1
- package/src/open-api/utils/parser.js.map +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +3 -3
- package/src/preset/generator.d.ts +1 -1
- package/src/py/agent/gateway-connection/generator.js +0 -3
- package/src/py/agent/gateway-connection/generator.js.map +1 -1
- package/src/py/agent/react-connection/generator.d.ts +1 -1
- package/src/py/agent/schema.json +2 -0
- package/src/py/fast-api/react/generator.d.ts +1 -1
- package/src/py/mcp-server/schema.json +1 -0
- package/src/sdk/open-api.d.ts +2 -0
- package/src/sdk/open-api.js +1 -0
- package/src/sdk/open-api.js.map +1 -1
- package/src/smithy/project/generator.d.ts +1 -1
- package/src/smithy/react-connection/generator.d.ts +1 -1
- package/src/terraform/project/generator.d.ts +1 -1
- package/src/trpc/react/generator.d.ts +1 -1
- package/src/ts/agent/a2a-connection/generator.d.ts +1 -1
- package/src/ts/agent/gateway-connection/generator.d.ts +1 -1
- package/src/ts/agent/gateway-connection/generator.js +0 -3
- package/src/ts/agent/gateway-connection/generator.js.map +1 -1
- package/src/ts/agent/mcp-connection/generator.d.ts +1 -1
- package/src/ts/agent/react-connection/generator.d.ts +1 -1
- package/src/ts/agent/schema.json +1 -0
- package/src/ts/api/schema.json +2 -0
- package/src/ts/astro-docs/generator.d.ts +1 -1
- package/src/ts/dcr-proxy/generator.d.ts +1 -1
- package/src/ts/dynamodb/generator.d.ts +1 -1
- package/src/ts/lambda-function/generator.d.ts +1 -1
- package/src/ts/lib/generator.d.ts +1 -1
- package/src/ts/mcp-server/schema.json +1 -0
- package/src/ts/nx-generator/__snapshots__/generator.spec.ts.snap +1 -1
- package/src/ts/nx-generator/files/nx-plugin-for-aws/generator/generator.spec.ts.template +1 -1
- package/src/ts/nx-generator/generator.d.ts +1 -1
- package/src/ts/nx-migration/generator.d.ts +1 -1
- package/src/ts/nx-plugin/generator.d.ts +1 -1
- package/src/ts/rdb/generator.d.ts +1 -1
- package/src/ts/react-website/agui/generator.d.ts +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +10 -10
- package/src/ts/react-website/app/schema.json +1 -0
- package/src/ts/website/app/schema.json +1 -0
- package/src/utils/metrics-assertions.d.ts +18 -0
- package/src/utils/metrics-assertions.js +42 -0
- package/src/utils/metrics-assertions.js.map +1 -0
- package/src/utils/test/python-dependencies.d.ts +8 -0
- package/src/utils/test/python-dependencies.js +14 -0
- package/src/utils/test/python-dependencies.js.map +1 -0
- package/src/utils/test/python-worker/worker.py +703 -0
- package/src/utils/test/warm-python-cache.d.ts +18 -0
- package/src/utils/test/warm-python-cache.js +46 -0
- package/src/utils/test/warm-python-cache.js.map +1 -0
- package/src/utils/versions.d.ts +10 -9
- package/src/utils/versions.js +9 -8
- package/src/utils/versions.js.map +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""<%- className %> client — AUTO-GENERATED, do not edit."""
|
|
2
|
+
|
|
3
|
+
from . import types as types
|
|
4
|
+
from .errors import ApiError as ApiError
|
|
5
|
+
<%_ if (clientType === 'sync' || clientType === 'both') { _%>
|
|
6
|
+
from .client import <%- className %> as <%- className %>
|
|
7
|
+
from .client import <%- className %>Config as <%- className %>Config
|
|
8
|
+
<%_ } _%>
|
|
9
|
+
<%_ if (clientType === 'async' || clientType === 'both') { _%>
|
|
10
|
+
from .async_client import Async<%- className %> as Async<%- className %>
|
|
11
|
+
from .async_client import Async<%- className %>Config as Async<%- className %>Config
|
|
12
|
+
<%_ } _%>
|
|
13
|
+
|
|
14
|
+
# `ApiError` catches any failure from either client. The per-operation
|
|
15
|
+
# subclasses in `.errors` narrow `.error` to the responses one operation
|
|
16
|
+
# declares — import them from there when you want that.
|
|
17
|
+
__all__ = [
|
|
18
|
+
<%_ if (clientType === 'sync' || clientType === 'both') { _%>
|
|
19
|
+
"<%- className %>",
|
|
20
|
+
"<%- className %>Config",
|
|
21
|
+
<%_ } _%>
|
|
22
|
+
<%_ if (clientType === 'async' || clientType === 'both') { _%>
|
|
23
|
+
"Async<%- className %>",
|
|
24
|
+
"Async<%- className %>Config",
|
|
25
|
+
<%_ } _%>
|
|
26
|
+
"ApiError",
|
|
27
|
+
"types",
|
|
28
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<%_ // An operation with no error responses has `Never` as its error type. _%>
|
|
2
|
+
<%_ const hasNeverErrorType = allOperations.some(op => !op.errorShape || op.errorShape.entries.length === 0); _%>
|
|
3
|
+
"""Errors raised by <%- className %> — AUTO-GENERATED, do not edit."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any<%- hasNeverErrorType ? ', Never' : '' %>
|
|
8
|
+
|
|
9
|
+
from . import types
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApiError(Exception):
|
|
13
|
+
"""Base class for all API errors.
|
|
14
|
+
|
|
15
|
+
Subclasses are generated per-operation so `error` can be narrowed by the
|
|
16
|
+
type checker via `isinstance` or `match`. Catch `ApiError` to handle any
|
|
17
|
+
status code across operations, from either the sync or the async client.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
status: int
|
|
21
|
+
error: Any
|
|
22
|
+
|
|
23
|
+
def __init__(self, status: int, error: Any = None) -> None:
|
|
24
|
+
super().__init__(f"API returned status {status}")
|
|
25
|
+
self.status = status
|
|
26
|
+
self.error = error
|
|
27
|
+
|
|
28
|
+
<%_ allOperations.forEach((op) => {
|
|
29
|
+
const shape = op.errorShape;
|
|
30
|
+
_%>
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class <%- shape.exceptionClassName %>(ApiError):
|
|
34
|
+
"""Raised when `<%- op.operationIdSnakeCase || op.name %>` returns a non-success status."""
|
|
35
|
+
|
|
36
|
+
error: <% if (shape.entries.length === 0) { %>Never<% } else { %>types.<%- shape.unionTypeName %><% } %>
|
|
37
|
+
<%_ }); _%>
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
<%_
|
|
2
|
+
// An operation with no error responses has `Never` as its error type.
|
|
3
|
+
const hasNeverErrorType = allOperations.some(op => !op.errorShape || op.errorShape.entries.length === 0);
|
|
4
|
+
const pyEnumValue = (member) => toPythonLiteral(member.value);
|
|
5
|
+
/**
|
|
6
|
+
* A discriminated subtype's tag property. pydantic dispatches a tagged union
|
|
7
|
+
* on a bare `Literal` field that every member declares, so the tag is emitted
|
|
8
|
+
* as required whether or not the schema lists it in `required`: its value is
|
|
9
|
+
* fixed by union membership, and a missing tag is a request the server always
|
|
10
|
+
* rejects.
|
|
11
|
+
*/
|
|
12
|
+
const isDiscriminatorTag = (prop) => !!prop.discriminatorValue;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Render text from the spec as a Python docstring body. A description is
|
|
16
|
+
* arbitrary text: a backslash would start an escape sequence and a trailing one
|
|
17
|
+
* would escape the closing quotes, so it goes through the same escaping as any
|
|
18
|
+
* other literal, with the surrounding quotes stripped.
|
|
19
|
+
*/
|
|
20
|
+
const pyDocstring = (text) =>
|
|
21
|
+
toPythonLiteral(String(text ?? '').replace(/\r?\n/g, ' ')).slice(1, -1);
|
|
22
|
+
|
|
23
|
+
/** A model's docstring text, noting deprecation where the spec declares it. */
|
|
24
|
+
const modelDocstring = (model) =>
|
|
25
|
+
pyDocstring(model.description || '') + (model.deprecated ? ' (deprecated)' : '');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A docstring, indented and wrapped to the vended line length so a long
|
|
29
|
+
* description doesn't trip the `E501` the generated project lints with. Returns
|
|
30
|
+
* the empty string when there is nothing to say.
|
|
31
|
+
*/
|
|
32
|
+
const docstring = (text, indent) => {
|
|
33
|
+
const body = String(text ?? '');
|
|
34
|
+
if (!body) return '';
|
|
35
|
+
const oneLine = `${indent}"""${body}"""`;
|
|
36
|
+
if (oneLine.length <= 120) return `${oneLine}\n`;
|
|
37
|
+
const width = 120 - indent.length;
|
|
38
|
+
const lines = [];
|
|
39
|
+
let current = '';
|
|
40
|
+
for (const word of body.split(' ')) {
|
|
41
|
+
if (current && `${current} ${word}`.length > width) {
|
|
42
|
+
lines.push(current);
|
|
43
|
+
current = word;
|
|
44
|
+
} else {
|
|
45
|
+
current = current ? `${current} ${word}` : word;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (current) lines.push(current);
|
|
49
|
+
return `${indent}"""\n${lines.map((l) => `${indent}${l}`).join('\n')}\n${indent}"""\n`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** A model's docstring, wrapped as above. */
|
|
53
|
+
const docstringBlock = (model, indent) =>
|
|
54
|
+
model.description || model.deprecated
|
|
55
|
+
? docstring(modelDocstring(model), indent)
|
|
56
|
+
: '';
|
|
57
|
+
|
|
58
|
+
const fieldAnnotation = (prop) => {
|
|
59
|
+
let base;
|
|
60
|
+
if (prop.isEnum && Array.isArray(prop.enum) && prop.enum.length > 0) {
|
|
61
|
+
base = `Literal[${prop.enum.map(pyEnumValue).join(', ')}]`;
|
|
62
|
+
} else {
|
|
63
|
+
base = prop.pythonType || 'Any';
|
|
64
|
+
}
|
|
65
|
+
if (isDiscriminatorTag(prop)) return base;
|
|
66
|
+
if (!prop.isRequired || prop.isNullable) return `${base} | None`;
|
|
67
|
+
return base;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render a pydantic `Field(...)` call for a model property — alias, default,
|
|
72
|
+
* description, deprecated, frozen. Returns null when no Field(...) is needed
|
|
73
|
+
* so the template can emit a bare `name: T` or `name: T = None`.
|
|
74
|
+
*/
|
|
75
|
+
const fieldCallFor = (property) => {
|
|
76
|
+
const snake = property.pythonName || property.name;
|
|
77
|
+
const aliasNeeded = snake !== property.name;
|
|
78
|
+
const args = [];
|
|
79
|
+
const needsDefault = !isDiscriminatorTag(property) && !property.isRequired;
|
|
80
|
+
if (needsDefault) args.push('default=None');
|
|
81
|
+
if (aliasNeeded) args.push(`alias=${toPythonLiteral(property.name)}`);
|
|
82
|
+
if (property.description) {
|
|
83
|
+
args.push(`description=${toPythonLiteral(property.description.replace(/\r?\n/g, ' '))}`);
|
|
84
|
+
}
|
|
85
|
+
if (property.isReadOnly) args.push('frozen=True');
|
|
86
|
+
if (property.deprecated) args.push('deprecated=True');
|
|
87
|
+
// Only emit Field(...) if something beyond a bare type annotation is needed.
|
|
88
|
+
if (args.length === 0) return null;
|
|
89
|
+
// If the only argument is default=None and there is no alias/desc/frozen/deprecated,
|
|
90
|
+
// prefer the shorter `= None` form.
|
|
91
|
+
if (args.length === 1 && args[0] === 'default=None') return null;
|
|
92
|
+
return `Field(${args.join(', ')})`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const renderProperty = (property) => {
|
|
96
|
+
const snake = property.pythonName || property.name;
|
|
97
|
+
const typeAnnotation = fieldAnnotation(property);
|
|
98
|
+
const fieldCall = fieldCallFor(property);
|
|
99
|
+
const needsDefault = !isDiscriminatorTag(property) && !property.isRequired;
|
|
100
|
+
if (fieldCall) {
|
|
101
|
+
return ` ${snake}: ${typeAnnotation} = ${fieldCall}`;
|
|
102
|
+
}
|
|
103
|
+
if (needsDefault) {
|
|
104
|
+
return ` ${snake}: ${typeAnnotation} = None`;
|
|
105
|
+
}
|
|
106
|
+
return ` ${snake}: ${typeAnnotation}`;
|
|
107
|
+
};
|
|
108
|
+
_%>
|
|
109
|
+
"""Types for <%- className %> — AUTO-GENERATED, do not edit."""
|
|
110
|
+
|
|
111
|
+
from __future__ import annotations
|
|
112
|
+
|
|
113
|
+
import datetime
|
|
114
|
+
from typing import Annotated, Any, Literal<%- hasNeverErrorType ? ', Never' : '' %>, TypedDict
|
|
115
|
+
|
|
116
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
117
|
+
|
|
118
|
+
<%_
|
|
119
|
+
/**
|
|
120
|
+
* Classify a model by how it's rendered in the module.
|
|
121
|
+
*
|
|
122
|
+
* `class` models emit as `class Foo(BaseModel)` and are self-contained — they
|
|
123
|
+
* reference other types only via forward-ref-quoted annotations that pydantic
|
|
124
|
+
* resolves lazily.
|
|
125
|
+
*
|
|
126
|
+
* `alias` models emit as `Foo = A | B / Literal[...] / list[...] / ...`.
|
|
127
|
+
* These are module-level expressions evaluated at import time, so every
|
|
128
|
+
* type they reference must already be defined. Emit them *after* all class
|
|
129
|
+
* definitions so `TypeAdapter(Foo)` can resolve references without relying
|
|
130
|
+
* on forward-ref strings.
|
|
131
|
+
*/
|
|
132
|
+
const classifyModel = (model) => {
|
|
133
|
+
if (model.isInlinedByAllOf) return 'skip';
|
|
134
|
+
if (model.export === 'enum') return 'alias';
|
|
135
|
+
if (model.export === 'one-of' || model.export === 'any-of') return 'alias';
|
|
136
|
+
// An object with no properties and no `additionalProperties` parses as a
|
|
137
|
+
// dictionary whose value type is the model itself, which would render as the
|
|
138
|
+
// self-referential `X = dict[str, X]`. It carries no value type at all, so it
|
|
139
|
+
// is emitted as an empty class — which is what the schema describes.
|
|
140
|
+
if (model.export === 'dictionary') {
|
|
141
|
+
const isEmptyObject =
|
|
142
|
+
!model.link &&
|
|
143
|
+
!model.hasAdditionalProperties &&
|
|
144
|
+
!model.hasPatternProperties &&
|
|
145
|
+
model.type === model.name;
|
|
146
|
+
return isEmptyObject ? 'class' : 'alias';
|
|
147
|
+
}
|
|
148
|
+
if (model.export === 'array') return 'alias';
|
|
149
|
+
if (model.export === 'tuple') return 'alias';
|
|
150
|
+
// `all-of` is rendered as a `BaseModel` subclass with flattened fields.
|
|
151
|
+
return 'class';
|
|
152
|
+
};
|
|
153
|
+
_%>
|
|
154
|
+
|
|
155
|
+
<%_ /* ────── Pass 1: all class definitions ────── */ _%>
|
|
156
|
+
<%_ models.forEach((model) => {
|
|
157
|
+
if (classifyModel(model) !== 'class') return;
|
|
158
|
+
_%>
|
|
159
|
+
<%_ if (model.export === 'all-of') {
|
|
160
|
+
// `effectiveProperties` is the shared flattening of the composition,
|
|
161
|
+
// recursing through members that are themselves allOf composites.
|
|
162
|
+
const flattened = model.effectiveProperties || [];
|
|
163
|
+
_%>
|
|
164
|
+
|
|
165
|
+
class <%- model.pythonClassName %>(BaseModel):
|
|
166
|
+
<%- docstringBlock(model, ' ') %> model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
167
|
+
|
|
168
|
+
<%_ if (flattened.length === 0) { _%>
|
|
169
|
+
pass
|
|
170
|
+
<%_ } _%>
|
|
171
|
+
<%_ flattened.forEach((property) => { _%>
|
|
172
|
+
<%- renderProperty(property) %>
|
|
173
|
+
<%_ }); _%>
|
|
174
|
+
<%_ } else { _%>
|
|
175
|
+
|
|
176
|
+
class <%- model.pythonClassName %>(BaseModel):
|
|
177
|
+
<%- docstringBlock(model, ' ') %> model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
178
|
+
|
|
179
|
+
<%_ if ((model.properties || []).length === 0 && !model.hasAdditionalProperties) { _%>
|
|
180
|
+
pass
|
|
181
|
+
<%_ } _%>
|
|
182
|
+
<%_ (model.properties || []).forEach((property) => { _%>
|
|
183
|
+
<%- renderProperty(property) %>
|
|
184
|
+
<%_ }); _%>
|
|
185
|
+
<%_ } _%>
|
|
186
|
+
<%_ }); _%>
|
|
187
|
+
|
|
188
|
+
<%_
|
|
189
|
+
/* ────── Pass 2: all non-class aliases ──────
|
|
190
|
+
* An alias is a module-level expression evaluated at import time, so unlike a
|
|
191
|
+
* class-body annotation (which `from __future__ import annotations` defers)
|
|
192
|
+
* every alias it references must already be bound. Emit them in dependency
|
|
193
|
+
* order via a topological sort: an alias that names another alias follows it.
|
|
194
|
+
* Classes are all defined above, so only alias-to-alias edges matter.
|
|
195
|
+
*/
|
|
196
|
+
const aliasModels = models.filter((m) => classifyModel(m) === 'alias');
|
|
197
|
+
const aliasByClassName = new Map(aliasModels.map((m) => [m.pythonClassName, m]));
|
|
198
|
+
/** The aliases the given alias's rendered type expression names. */
|
|
199
|
+
const aliasDependencies = (model) => {
|
|
200
|
+
const rendered = model.export === 'enum'
|
|
201
|
+
? ''
|
|
202
|
+
: [model.pythonType || '', ...(model.properties || []).map((p) => p.pythonType || '')].join(' ');
|
|
203
|
+
const found = new Set();
|
|
204
|
+
for (const identifier of rendered.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []) {
|
|
205
|
+
const dependency = aliasByClassName.get(identifier);
|
|
206
|
+
if (dependency && dependency !== model) found.add(dependency);
|
|
207
|
+
}
|
|
208
|
+
return found;
|
|
209
|
+
};
|
|
210
|
+
const sortedAliasModels = [];
|
|
211
|
+
const aliasState = new Map();
|
|
212
|
+
const visitAlias = (model) => {
|
|
213
|
+
// 'visiting' marks a cycle: emit it where first reached and let the reference
|
|
214
|
+
// that closed the loop resolve against the earlier binding.
|
|
215
|
+
if (aliasState.get(model)) return;
|
|
216
|
+
aliasState.set(model, 'visiting');
|
|
217
|
+
for (const dependency of aliasDependencies(model)) {
|
|
218
|
+
visitAlias(dependency);
|
|
219
|
+
}
|
|
220
|
+
aliasState.set(model, 'done');
|
|
221
|
+
sortedAliasModels.push(model);
|
|
222
|
+
};
|
|
223
|
+
aliasModels
|
|
224
|
+
.slice()
|
|
225
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
226
|
+
.forEach(visitAlias);
|
|
227
|
+
_%>
|
|
228
|
+
<%_ sortedAliasModels.forEach((model) => { _%>
|
|
229
|
+
|
|
230
|
+
<%_ if (model.export === 'enum') { _%>
|
|
231
|
+
<%- model.pythonClassName %> = Literal[<%_ model.enum.forEach((m, i) => { _%><%- i > 0 ? ', ' : '' %><%- pyEnumValue(m) %><%_ }); _%>]
|
|
232
|
+
<%- docstringBlock(model, '') %><%_ } else if (model.export === 'one-of' || model.export === 'any-of') { _%>
|
|
233
|
+
<%_ /* Classes are already defined above, so references can be unquoted —
|
|
234
|
+
which matters here, since pydantic's `TypeAdapter(X)` resolves an alias
|
|
235
|
+
at runtime and cannot see through a string forward-ref. */ _%>
|
|
236
|
+
<%_ /* A discriminated union renders as a pydantic tagged union so parsing
|
|
237
|
+
dispatches directly to the matching branch (never merging branches).
|
|
238
|
+
Requires every member to carry the discriminator as a Literal-typed
|
|
239
|
+
field; otherwise fall back to a plain union. */ _%>
|
|
240
|
+
<%_ /* pydantic requires the tag to be a bare Literal field on every member,
|
|
241
|
+
else TypeAdapter construction fails at import time. `renderProperty`
|
|
242
|
+
emits a tag that way whether or not the schema requires it. */
|
|
243
|
+
const memberModels = (model.composedModels || []);
|
|
244
|
+
const isTagged = model.discriminator && memberModels.length > 0
|
|
245
|
+
&& memberModels.length === model.properties.filter(p => !p.isPrimitive).length
|
|
246
|
+
&& (model.composedPrimitives || []).length === 0
|
|
247
|
+
&& memberModels.every(m => (m.properties || []).some(p =>
|
|
248
|
+
p.name === model.discriminator.propertyName && p.discriminatorValue)); _%>
|
|
249
|
+
<%_ if (isTagged) { _%>
|
|
250
|
+
<%- model.pythonClassName %> = Annotated[<%_ model.properties.forEach((p, i) => { _%><%- i > 0 ? ' | ' : '' %><%- p.pythonType || 'Any' %><%_ }); _%>, Field(discriminator="<%- model.discriminator.pythonPropertyName %>")]
|
|
251
|
+
<%_ } else { _%>
|
|
252
|
+
<%- model.pythonClassName %> = <% model.properties.forEach((p, i) => { %><%- i > 0 ? ' | ' : '' %><%- p.pythonType || 'Any' %><% }); %>
|
|
253
|
+
<%_ } _%>
|
|
254
|
+
<%- docstringBlock(model, '') %><%_ } else if (model.export === 'dictionary' || model.export === 'array' || model.export === 'tuple') { _%>
|
|
255
|
+
<%- model.pythonClassName %> = <%- model.pythonType %>
|
|
256
|
+
<%- docstringBlock(model, '') %><%_ } _%>
|
|
257
|
+
<%_ }); _%>
|
|
258
|
+
<%_ // ────── Per-operation request TypedDicts ────── _%>
|
|
259
|
+
<%_ allOperations.forEach((op) => { _%>
|
|
260
|
+
<%_ if (op.parameters && op.parameters.length > 0) { _%>
|
|
261
|
+
<%_ const required = op.parameters.filter(p => p.isRequired); _%>
|
|
262
|
+
<%_ const optional = op.parameters.filter(p => !p.isRequired); _%>
|
|
263
|
+
<%_ if (required.length > 0 && optional.length === 0) { _%>
|
|
264
|
+
|
|
265
|
+
class <%- op.pythonRequestTypeName %>(TypedDict):
|
|
266
|
+
<%- docstring(pyDocstring(op.description || op.summary), ' ') %><%_ op.parameters.forEach((p) => { _%>
|
|
267
|
+
<%- p.pythonName %>: <%- p.pythonType || 'Any' %>
|
|
268
|
+
<%_ }); _%>
|
|
269
|
+
<%_ } else if (required.length === 0) { _%>
|
|
270
|
+
|
|
271
|
+
class <%- op.pythonRequestTypeName %>(TypedDict, total=False):
|
|
272
|
+
<%- docstring(pyDocstring(op.description || op.summary), ' ') %><%_ op.parameters.forEach((p) => { _%>
|
|
273
|
+
<%- p.pythonName %>: <%- p.pythonType || 'Any' %>
|
|
274
|
+
<%_ }); _%>
|
|
275
|
+
<%_ } else { _%>
|
|
276
|
+
|
|
277
|
+
class <%- op.pythonRequestTypeName %>Required(TypedDict):
|
|
278
|
+
<%_ required.forEach((p) => { _%>
|
|
279
|
+
<%- p.pythonName %>: <%- p.pythonType || 'Any' %>
|
|
280
|
+
<%_ }); _%>
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class <%- op.pythonRequestTypeName %>(<%- op.pythonRequestTypeName %>Required, total=False):
|
|
284
|
+
<%- docstring(pyDocstring(op.description || op.summary), ' ') %><%_ optional.forEach((p) => { _%>
|
|
285
|
+
<%- p.pythonName %>: <%- p.pythonType || 'Any' %>
|
|
286
|
+
<%_ }); _%>
|
|
287
|
+
<%_ } _%>
|
|
288
|
+
<%_ } _%>
|
|
289
|
+
<%_ }); _%>
|
|
290
|
+
<%_ // ────── Per-operation error taxonomy ────── _%>
|
|
291
|
+
<%_ allOperations.forEach((op) => {
|
|
292
|
+
const shape = op.errorShape;
|
|
293
|
+
if (!shape || shape.entries.length === 0) {
|
|
294
|
+
_%>
|
|
295
|
+
|
|
296
|
+
<%- shape.unionTypeName %> = Never
|
|
297
|
+
<%_
|
|
298
|
+
} else {
|
|
299
|
+
_%>
|
|
300
|
+
<%_ shape.entries.forEach((entry) => {
|
|
301
|
+
// Inside types.py the error type is defined locally; strip the client
|
|
302
|
+
// module's `types.` prefix the response model's pythonClientType carries.
|
|
303
|
+
const payloadType = entry.responseModel.type === 'void'
|
|
304
|
+
? 'None'
|
|
305
|
+
: (entry.responseModel.pythonClientType || 'Any').replace(/\btypes\./g, '');
|
|
306
|
+
_%>
|
|
307
|
+
|
|
308
|
+
class <%- entry.className %>(BaseModel):
|
|
309
|
+
"""Error wrapper for <%- op.operationIdPascalCase %> status <%- entry.code %>."""
|
|
310
|
+
|
|
311
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
312
|
+
|
|
313
|
+
status: <%- entry.statusAnnotation %>
|
|
314
|
+
error: <%- payloadType %>
|
|
315
|
+
<%_ }); _%>
|
|
316
|
+
|
|
317
|
+
<%- shape.unionTypeName %> = <% shape.entries.forEach((e, i) => { %><%- i > 0 ? ' | ' : '' %><%- e.className %><% }); %>
|
|
318
|
+
<%_ } _%>
|
|
319
|
+
<%_ }); _%>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type Tree } from '@nx/devkit';
|
|
6
|
+
import { type NxGeneratorInfo } from '../../utils/nx.js';
|
|
7
|
+
import { type CodeGenData } from '../utils/codegen-data/types.js';
|
|
8
|
+
import type { OpenApiPyClientGeneratorSchema } from './schema.js';
|
|
9
|
+
export declare const OPEN_API_PY_CLIENT_GENERATOR_INFO: NxGeneratorInfo;
|
|
10
|
+
/**
|
|
11
|
+
* Python packages a generated client imports at runtime. A generator that
|
|
12
|
+
* emits a client into a project spreads these into its own declaration and
|
|
13
|
+
* adds them to that project's pyproject.
|
|
14
|
+
*/
|
|
15
|
+
export declare const OPEN_API_PY_CLIENT_DEPENDENCIES: import("../../utils/declared-dependencies.js").DependencyDeclaration<readonly import("../../utils/declared-dependencies.js").DeclaredTsDependency<"@ag-ui/client" | "@ag-ui/core" | "@ag-ui/encoder" | "@biomejs/biome" | "@a2a-js/sdk" | "@aws/aws-distro-opentelemetry-node-autoinstrumentation" | "@opentelemetry/propagator-jaeger" | "minimatch" | "deepmerge-ts" | "mysql2" | "@aws-sdk/client-dynamodb" | "@aws-sdk/client-api-gateway" | "@aws-sdk/client-iam" | "@aws-sdk/client-bedrock-agentcore" | "@aws-sdk/client-bedrock-runtime" | "@aws-sdk/client-s3" | "@aws-sdk/client-sts" | "@aws-sdk/client-cognito-identity-provider" | "@aws-sdk/credential-providers" | "@aws-sdk/credential-provider-cognito-identity" | "@aws-sdk/client-secrets-manager" | "@aws-sdk/rds-signer" | "@smithy/server-apigateway" | "@smithy/server-node" | "@aws-lambda-powertools/logger" | "@aws-lambda-powertools/metrics" | "@aws-lambda-powertools/parameters" | "@aws-lambda-powertools/tracer" | "@aws-lambda-powertools/parser" | "@aws-sdk/client-appconfigdata" | "@middy/core" | "@nxlv/python" | "@nx-extend/terraform" | "nx" | "@nx/devkit" | "@nx/js" | "@nx/react" | "@nx/vite" | "@nx/vitest" | "@nx/workspace" | "create-nx-workspace" | "@swc-node/register" | "@swc/core" | "@modelcontextprotocol/sdk" | "@modelcontextprotocol/inspector" | "@ag-ui/a2ui-toolkit" | "@ag-ui/aws-strands" | "agent-chat-cli" | "@copilotkit/react-core" | "rxjs" | "@strands-agents/sdk" | "@tanstack/react-router" | "@tanstack/router-plugin" | "@tanstack/router-generator" | "@tanstack/virtual-file-routes" | "@tanstack/router-utils" | "@cloudscape-design/board-components" | "@cloudscape-design/chat-components" | "@cloudscape-design/components" | "@cloudscape-design/global-styles" | "@tanstack/react-query" | "@tanstack/react-query-devtools" | "@trpc/tanstack-react-query" | "@trpc/client" | "@trpc/server" | "@types/node" | "@types/aws-lambda" | "@types/cors" | "@types/pg" | "@types/ws" | "@types/express" | "@smithy/config-resolver" | "@smithy/node-config-provider" | "@smithy/node-http-handler" | "@smithy/types" | "@vitest/coverage-v8" | "@vitest/ui" | "@astrojs/react" | "@astrojs/starlight" | "astro" | "cookie" | "aws4fetch" | "aws-cdk" | "aws-cdk-lib" | "aws-xray-sdk-core" | "constructs" | "cors" | "chalk" | "class-variance-authority" | "cn" | "commander" | "electrodb" | "esbuild" | "event-source-polyfill" | "@types/event-source-polyfill" | "@prisma/adapter-mariadb" | "@prisma/adapter-pg" | "@prisma/client" | "ejs" | "@types/ejs" | "express" | "fast-glob" | "husky" | "fs-extra" | "@types/fs-extra" | "mariadb" | "mise" | "npm" | "npm-check-updates" | "oidc-client-ts" | "pg" | "prisma" | "react-oidc-context" | "react" | "react-dom" | "rolldown" | "rolldown-plugin-dts" | "shx" | "simple-git" | "source-map-support" | "starlight-blog" | "tailwindcss" | "@tailwindcss/vite" | "tsx" | "lucide-react" | "radix-ui" | "shadcn" | "tw-animate-css" | "vite" | "typescript" | "vitest" | "jsdom" | "zod" | "ws", Record<string, never>>[], readonly [{
|
|
16
|
+
readonly name: "httpx";
|
|
17
|
+
}, {
|
|
18
|
+
readonly name: "pydantic";
|
|
19
|
+
}]>;
|
|
20
|
+
/**
|
|
21
|
+
* Generate a Python httpx-based client from an OpenAPI spec.
|
|
22
|
+
*
|
|
23
|
+
* Emits:
|
|
24
|
+
* - types.py — pydantic v2 models + per-op error classes and TypedDicts
|
|
25
|
+
* - client.py — sync client (httpx.Client) when clientType includes 'sync'
|
|
26
|
+
* - async_client.py — async client (httpx.AsyncClient) when clientType includes 'async'
|
|
27
|
+
*
|
|
28
|
+
* The shape mirrors `open-api#ts-client`, but the two decide a few things
|
|
29
|
+
* separately rather than through shared code: this generator reads the neutral
|
|
30
|
+
* `requestShape` from `codegen-data`, while the TypeScript templates still
|
|
31
|
+
* compute inlining themselves. Where they differ today, for the same spec:
|
|
32
|
+
*
|
|
33
|
+
* - A discriminated object body: TypeScript inlines its fields, Python passes
|
|
34
|
+
* the body whole so marshalling can dispatch on the discriminator.
|
|
35
|
+
* - A body carrying `additionalProperties`, a union, or `patternProperties`, or
|
|
36
|
+
* a body property literally named `body`: TypeScript wraps, Python flattens.
|
|
37
|
+
* - An optional (`required: false`) object body: Python flattens its required
|
|
38
|
+
* fields into required keyword arguments, so the body is always sent.
|
|
39
|
+
* - A primitive `application/json` body: TypeScript sends the raw text, Python
|
|
40
|
+
* sends it JSON-encoded.
|
|
41
|
+
* - The media type chosen for a body offering several: TypeScript prefers any
|
|
42
|
+
* `+json` type, Python only an exact `application/json`.
|
|
43
|
+
*
|
|
44
|
+
* Migrating the TypeScript templates onto `requestShape` would collapse these
|
|
45
|
+
* into one decision; until then they are differences of behaviour, not of
|
|
46
|
+
* correctness, and are covered by this generator's own tests.
|
|
47
|
+
*/
|
|
48
|
+
export declare const openApiPyClientGenerator: (tree: Tree, options: OpenApiPyClientGeneratorSchema) => Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Generate an OpenAPI Python client in the target directory
|
|
51
|
+
*/
|
|
52
|
+
export declare const generateOpenApiPyClient: (tree: Tree, data: CodeGenData, outputPath: string, clientType?: "sync" | "async" | "both") => void;
|
|
53
|
+
export default openApiPyClientGenerator;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { generateFiles } from "@nx/devkit";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { declareDependencies } from "../../utils/declared-dependencies.js";
|
|
7
|
+
import { formatFilesInSubtree } from "../../utils/format.js";
|
|
8
|
+
import { updateGitIgnore } from "../../utils/git.js";
|
|
9
|
+
import { addGeneratorMetricsIfApplicable } from "../../utils/metrics.js";
|
|
10
|
+
import { getGeneratorInfo } from "../../utils/nx.js";
|
|
11
|
+
import { buildOpenApiCodeGenerationData } from "../ts-client/generator.js";
|
|
12
|
+
import { toPythonClassName, toPythonLiteral } from "../utils/codegen-data/languages.js";
|
|
13
|
+
import { isPythonCollection, needsPythonTypeAdapter } from "../utils/codegen-data/types.js";
|
|
14
|
+
import { annotatePythonData, assertNoClashingPythonNames } from "../utils/codegen-data.js";
|
|
15
|
+
export const OPEN_API_PY_CLIENT_GENERATOR_INFO = getGeneratorInfo(import.meta.filename);
|
|
16
|
+
/**
|
|
17
|
+
* Python packages a generated client imports at runtime. A generator that
|
|
18
|
+
* emits a client into a project spreads these into its own declaration and
|
|
19
|
+
* adds them to that project's pyproject.
|
|
20
|
+
*/ export const OPEN_API_PY_CLIENT_DEPENDENCIES = declareDependencies()({
|
|
21
|
+
py: [
|
|
22
|
+
{
|
|
23
|
+
name: 'httpx'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'pydantic'
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Generate a Python httpx-based client from an OpenAPI spec.
|
|
32
|
+
*
|
|
33
|
+
* Emits:
|
|
34
|
+
* - types.py — pydantic v2 models + per-op error classes and TypedDicts
|
|
35
|
+
* - client.py — sync client (httpx.Client) when clientType includes 'sync'
|
|
36
|
+
* - async_client.py — async client (httpx.AsyncClient) when clientType includes 'async'
|
|
37
|
+
*
|
|
38
|
+
* The shape mirrors `open-api#ts-client`, but the two decide a few things
|
|
39
|
+
* separately rather than through shared code: this generator reads the neutral
|
|
40
|
+
* `requestShape` from `codegen-data`, while the TypeScript templates still
|
|
41
|
+
* compute inlining themselves. Where they differ today, for the same spec:
|
|
42
|
+
*
|
|
43
|
+
* - A discriminated object body: TypeScript inlines its fields, Python passes
|
|
44
|
+
* the body whole so marshalling can dispatch on the discriminator.
|
|
45
|
+
* - A body carrying `additionalProperties`, a union, or `patternProperties`, or
|
|
46
|
+
* a body property literally named `body`: TypeScript wraps, Python flattens.
|
|
47
|
+
* - An optional (`required: false`) object body: Python flattens its required
|
|
48
|
+
* fields into required keyword arguments, so the body is always sent.
|
|
49
|
+
* - A primitive `application/json` body: TypeScript sends the raw text, Python
|
|
50
|
+
* sends it JSON-encoded.
|
|
51
|
+
* - The media type chosen for a body offering several: TypeScript prefers any
|
|
52
|
+
* `+json` type, Python only an exact `application/json`.
|
|
53
|
+
*
|
|
54
|
+
* Migrating the TypeScript templates onto `requestShape` would collapse these
|
|
55
|
+
* into one decision; until then they are differences of behaviour, not of
|
|
56
|
+
* correctness, and are covered by this generator's own tests.
|
|
57
|
+
*/ export const openApiPyClientGenerator = async (tree, options)=>{
|
|
58
|
+
const data = await buildOpenApiCodeGenerationData(tree, options.openApiSpecPath);
|
|
59
|
+
const clientType = options.clientType ?? 'both';
|
|
60
|
+
// Derived here rather than in the shared pipeline, so a TypeScript consumer of
|
|
61
|
+
// the same spec pays for neither these fields nor a Python-specific name clash.
|
|
62
|
+
annotatePythonData(data);
|
|
63
|
+
for (const model of data.models){
|
|
64
|
+
assertNoClashingPythonNames(model);
|
|
65
|
+
}
|
|
66
|
+
generateOpenApiPyClient(tree, data, options.outputPath, clientType);
|
|
67
|
+
// The client is regenerated from the spec, so it is ignored by default.
|
|
68
|
+
// Remove the entry to check it in instead.
|
|
69
|
+
updateGitIgnore(tree, '.', (patterns)=>[
|
|
70
|
+
...patterns,
|
|
71
|
+
options.outputPath
|
|
72
|
+
]);
|
|
73
|
+
await addGeneratorMetricsIfApplicable(tree, [
|
|
74
|
+
OPEN_API_PY_CLIENT_GENERATOR_INFO
|
|
75
|
+
]);
|
|
76
|
+
await formatFilesInSubtree(tree);
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Generate an OpenAPI Python client in the target directory
|
|
80
|
+
*/ export const generateOpenApiPyClient = (tree, data, outputPath, clientType = 'both')=>{
|
|
81
|
+
// `toPythonLiteral` is shared with the type renderer so a value is escaped
|
|
82
|
+
// the same way wherever a template spells it out. The type predicates let a
|
|
83
|
+
// template ask what a Python type is, rather than matching its spelling.
|
|
84
|
+
const base = {
|
|
85
|
+
...data,
|
|
86
|
+
// The shared `className` comes from the spec title via `toClassName`, which
|
|
87
|
+
// is a valid TypeScript name but not always a Python one: a title with no
|
|
88
|
+
// alphanumerics renders nothing (`class :`) and one spelt `None` renders a
|
|
89
|
+
// keyword. It also has to clear the names these modules export themselves,
|
|
90
|
+
// so a title of `ApiError` cannot shadow the base exception.
|
|
91
|
+
className: toPythonClassName(data.className),
|
|
92
|
+
clientType,
|
|
93
|
+
toPythonLiteral,
|
|
94
|
+
isPythonCollection,
|
|
95
|
+
needsPythonTypeAdapter
|
|
96
|
+
};
|
|
97
|
+
generateFiles(tree, path.join(import.meta.dirname, 'files', 'shared'), outputPath, base);
|
|
98
|
+
// Both clients render from one template: they differ only in how they await,
|
|
99
|
+
// and keeping two copies is how a fix lands in one and not the other.
|
|
100
|
+
// `clientModuleName` names the emitted file, `isAsync` selects the keywords.
|
|
101
|
+
const clientDir = path.join(import.meta.dirname, 'files', 'client');
|
|
102
|
+
if (clientType === 'sync' || clientType === 'both') {
|
|
103
|
+
generateFiles(tree, clientDir, outputPath, {
|
|
104
|
+
...base,
|
|
105
|
+
isAsync: false,
|
|
106
|
+
clientModuleName: 'client'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (clientType === 'async' || clientType === 'both') {
|
|
110
|
+
generateFiles(tree, clientDir, outputPath, {
|
|
111
|
+
...base,
|
|
112
|
+
isAsync: true,
|
|
113
|
+
clientModuleName: 'async_client'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
export default openApiPyClientGenerator;
|
|
118
|
+
|
|
119
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/open-api/py-client/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport { generateFiles, type Tree } from '@nx/devkit';\nimport * as path from 'path';\nimport { declareDependencies } from '../../utils/declared-dependencies.js';\nimport { formatFilesInSubtree } from '../../utils/format.js';\nimport { updateGitIgnore } from '../../utils/git.js';\nimport { addGeneratorMetricsIfApplicable } from '../../utils/metrics.js';\nimport { getGeneratorInfo, type NxGeneratorInfo } from '../../utils/nx.js';\nimport { buildOpenApiCodeGenerationData } from '../ts-client/generator.js';\nimport {\n toPythonClassName,\n toPythonLiteral,\n} from '../utils/codegen-data/languages.js';\nimport {\n type CodeGenData,\n isPythonCollection,\n needsPythonTypeAdapter,\n} from '../utils/codegen-data/types.js';\nimport {\n annotatePythonData,\n assertNoClashingPythonNames,\n} from '../utils/codegen-data.js';\nimport type { OpenApiPyClientGeneratorSchema } from './schema.js';\n\nexport const OPEN_API_PY_CLIENT_GENERATOR_INFO: NxGeneratorInfo =\n getGeneratorInfo(import.meta.filename);\n\n/**\n * Python packages a generated client imports at runtime. A generator that\n * emits a client into a project spreads these into its own declaration and\n * adds them to that project's pyproject.\n */\nexport const OPEN_API_PY_CLIENT_DEPENDENCIES = declareDependencies()({\n py: [{ name: 'httpx' }, { name: 'pydantic' }],\n});\n\n/**\n * Generate a Python httpx-based client from an OpenAPI spec.\n *\n * Emits:\n * - types.py — pydantic v2 models + per-op error classes and TypedDicts\n * - client.py — sync client (httpx.Client) when clientType includes 'sync'\n * - async_client.py — async client (httpx.AsyncClient) when clientType includes 'async'\n *\n * The shape mirrors `open-api#ts-client`, but the two decide a few things\n * separately rather than through shared code: this generator reads the neutral\n * `requestShape` from `codegen-data`, while the TypeScript templates still\n * compute inlining themselves. Where they differ today, for the same spec:\n *\n * - A discriminated object body: TypeScript inlines its fields, Python passes\n * the body whole so marshalling can dispatch on the discriminator.\n * - A body carrying `additionalProperties`, a union, or `patternProperties`, or\n * a body property literally named `body`: TypeScript wraps, Python flattens.\n * - An optional (`required: false`) object body: Python flattens its required\n * fields into required keyword arguments, so the body is always sent.\n * - A primitive `application/json` body: TypeScript sends the raw text, Python\n * sends it JSON-encoded.\n * - The media type chosen for a body offering several: TypeScript prefers any\n * `+json` type, Python only an exact `application/json`.\n *\n * Migrating the TypeScript templates onto `requestShape` would collapse these\n * into one decision; until then they are differences of behaviour, not of\n * correctness, and are covered by this generator's own tests.\n */\nexport const openApiPyClientGenerator = async (\n tree: Tree,\n options: OpenApiPyClientGeneratorSchema,\n) => {\n const data = await buildOpenApiCodeGenerationData(\n tree,\n options.openApiSpecPath,\n );\n const clientType = options.clientType ?? 'both';\n\n // Derived here rather than in the shared pipeline, so a TypeScript consumer of\n // the same spec pays for neither these fields nor a Python-specific name clash.\n annotatePythonData(data);\n\n for (const model of data.models) {\n assertNoClashingPythonNames(model);\n }\n\n generateOpenApiPyClient(tree, data, options.outputPath, clientType);\n\n // The client is regenerated from the spec, so it is ignored by default.\n // Remove the entry to check it in instead.\n updateGitIgnore(tree, '.', (patterns) => [...patterns, options.outputPath]);\n\n await addGeneratorMetricsIfApplicable(tree, [\n OPEN_API_PY_CLIENT_GENERATOR_INFO,\n ]);\n\n await formatFilesInSubtree(tree);\n};\n\n/**\n * Generate an OpenAPI Python client in the target directory\n */\nexport const generateOpenApiPyClient = (\n tree: Tree,\n data: CodeGenData,\n outputPath: string,\n clientType: 'sync' | 'async' | 'both' = 'both',\n) => {\n // `toPythonLiteral` is shared with the type renderer so a value is escaped\n // the same way wherever a template spells it out. The type predicates let a\n // template ask what a Python type is, rather than matching its spelling.\n const base = {\n ...data,\n // The shared `className` comes from the spec title via `toClassName`, which\n // is a valid TypeScript name but not always a Python one: a title with no\n // alphanumerics renders nothing (`class :`) and one spelt `None` renders a\n // keyword. It also has to clear the names these modules export themselves,\n // so a title of `ApiError` cannot shadow the base exception.\n className: toPythonClassName(data.className),\n clientType,\n toPythonLiteral,\n isPythonCollection,\n needsPythonTypeAdapter,\n };\n\n generateFiles(\n tree,\n path.join(import.meta.dirname, 'files', 'shared'),\n outputPath,\n base,\n );\n\n // Both clients render from one template: they differ only in how they await,\n // and keeping two copies is how a fix lands in one and not the other.\n // `clientModuleName` names the emitted file, `isAsync` selects the keywords.\n const clientDir = path.join(import.meta.dirname, 'files', 'client');\n\n if (clientType === 'sync' || clientType === 'both') {\n generateFiles(tree, clientDir, outputPath, {\n ...base,\n isAsync: false,\n clientModuleName: 'client',\n });\n }\n if (clientType === 'async' || clientType === 'both') {\n generateFiles(tree, clientDir, outputPath, {\n ...base,\n isAsync: true,\n clientModuleName: 'async_client',\n });\n }\n};\n\nexport default openApiPyClientGenerator;\n"],"names":["generateFiles","path","declareDependencies","formatFilesInSubtree","updateGitIgnore","addGeneratorMetricsIfApplicable","getGeneratorInfo","buildOpenApiCodeGenerationData","toPythonClassName","toPythonLiteral","isPythonCollection","needsPythonTypeAdapter","annotatePythonData","assertNoClashingPythonNames","OPEN_API_PY_CLIENT_GENERATOR_INFO","filename","OPEN_API_PY_CLIENT_DEPENDENCIES","py","name","openApiPyClientGenerator","tree","options","data","openApiSpecPath","clientType","model","models","generateOpenApiPyClient","outputPath","patterns","base","className","join","dirname","clientDir","isAsync","clientModuleName"],"mappings":"AAAA;;;CAGC,GACD,SAASA,aAAa,QAAmB,aAAa;AACtD,YAAYC,UAAU,OAAO;AAC7B,SAASC,mBAAmB,QAAQ,uCAAuC;AAC3E,SAASC,oBAAoB,QAAQ,wBAAwB;AAC7D,SAASC,eAAe,QAAQ,qBAAqB;AACrD,SAASC,+BAA+B,QAAQ,yBAAyB;AACzE,SAASC,gBAAgB,QAA8B,oBAAoB;AAC3E,SAASC,8BAA8B,QAAQ,4BAA4B;AAC3E,SACEC,iBAAiB,EACjBC,eAAe,QACV,qCAAqC;AAC5C,SAEEC,kBAAkB,EAClBC,sBAAsB,QACjB,iCAAiC;AACxC,SACEC,kBAAkB,EAClBC,2BAA2B,QACtB,2BAA2B;AAGlC,OAAO,MAAMC,oCACXR,iBAAiB,YAAYS,QAAQ,EAAE;AAEzC;;;;CAIC,GACD,OAAO,MAAMC,kCAAkCd,sBAAsB;IACnEe,IAAI;QAAC;YAAEC,MAAM;QAAQ;QAAG;YAAEA,MAAM;QAAW;KAAE;AAC/C,GAAG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,MAAMC,2BAA2B,OACtCC,MACAC;IAEA,MAAMC,OAAO,MAAMf,+BACjBa,MACAC,QAAQE,eAAe;IAEzB,MAAMC,aAAaH,QAAQG,UAAU,IAAI;IAEzC,+EAA+E;IAC/E,gFAAgF;IAChFZ,mBAAmBU;IAEnB,KAAK,MAAMG,SAASH,KAAKI,MAAM,CAAE;QAC/Bb,4BAA4BY;IAC9B;IAEAE,wBAAwBP,MAAME,MAAMD,QAAQO,UAAU,EAAEJ;IAExD,wEAAwE;IACxE,2CAA2C;IAC3CpB,gBAAgBgB,MAAM,KAAK,CAACS,WAAa;eAAIA;YAAUR,QAAQO,UAAU;SAAC;IAE1E,MAAMvB,gCAAgCe,MAAM;QAC1CN;KACD;IAED,MAAMX,qBAAqBiB;AAC7B,EAAE;AAEF;;CAEC,GACD,OAAO,MAAMO,0BAA0B,CACrCP,MACAE,MACAM,YACAJ,aAAwC,MAAM;IAE9C,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,MAAMM,OAAO;QACX,GAAGR,IAAI;QACP,4EAA4E;QAC5E,0EAA0E;QAC1E,2EAA2E;QAC3E,2EAA2E;QAC3E,6DAA6D;QAC7DS,WAAWvB,kBAAkBc,KAAKS,SAAS;QAC3CP;QACAf;QACAC;QACAC;IACF;IAEAX,cACEoB,MACAnB,KAAK+B,IAAI,CAAC,YAAYC,OAAO,EAAE,SAAS,WACxCL,YACAE;IAGF,6EAA6E;IAC7E,sEAAsE;IACtE,6EAA6E;IAC7E,MAAMI,YAAYjC,KAAK+B,IAAI,CAAC,YAAYC,OAAO,EAAE,SAAS;IAE1D,IAAIT,eAAe,UAAUA,eAAe,QAAQ;QAClDxB,cAAcoB,MAAMc,WAAWN,YAAY;YACzC,GAAGE,IAAI;YACPK,SAAS;YACTC,kBAAkB;QACpB;IACF;IACA,IAAIZ,eAAe,WAAWA,eAAe,QAAQ;QACnDxB,cAAcoB,MAAMc,WAAWN,YAAY;YACzC,GAAGE,IAAI;YACPK,SAAS;YACTC,kBAAkB;QACpB;IACF;AACF,EAAE;AAEF,eAAejB,yBAAyB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/open-api/py-client/schema.d.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nexport type OpenApiPyClientClientType = 'sync' | 'async' | 'both';\n\nexport interface OpenApiPyClientGeneratorSchema {\n openApiSpecPath: string;\n outputPath: string;\n clientType?: OpenApiPyClientClientType;\n}\n"],"names":[],"mappings":"AAAA;;;CAGC,GAID,WAIC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type OpenApiPyClientClientType = 'sync' | 'async' | 'both';
|
|
7
|
+
|
|
8
|
+
export interface OpenApiPyClientGeneratorSchema {
|
|
9
|
+
openApiSpecPath: string;
|
|
10
|
+
outputPath: string;
|
|
11
|
+
clientType?: OpenApiPyClientClientType;
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "OpenApiPyClient",
|
|
4
|
+
"title": "OpenAPI Python Client",
|
|
5
|
+
"description": "Generate a Python httpx client from an OpenAPI specification",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"openApiSpecPath": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Path to the OpenAPI specification relative to the monorepo root",
|
|
11
|
+
"x-priority": "important"
|
|
12
|
+
},
|
|
13
|
+
"outputPath": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Path to the directory in which to generate the client relative to the monorepo root",
|
|
16
|
+
"x-priority": "important"
|
|
17
|
+
},
|
|
18
|
+
"clientType": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Which clients to emit. 'sync' emits client.py using httpx.Client, 'async' emits async_client.py using httpx.AsyncClient, 'both' emits both.",
|
|
21
|
+
"enum": ["sync", "async", "both"],
|
|
22
|
+
"x-prompt": "Which clients would you like to emit?",
|
|
23
|
+
"default": "both",
|
|
24
|
+
"x-priority": "important"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["openApiSpecPath", "outputPath"]
|
|
28
|
+
}
|