@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,1076 @@
|
|
|
1
|
+
<%_
|
|
2
|
+
/**
|
|
3
|
+
* The declared type of a single success response, `None` for an empty body.
|
|
4
|
+
*/
|
|
5
|
+
const successType = (resp) => {
|
|
6
|
+
const py = resp.type === 'void' ? 'None' : resp.pythonClientType || 'None';
|
|
7
|
+
// A nullable response body may arrive as JSON null.
|
|
8
|
+
return resp.isNullable && py !== 'None' ? `${py} | None` : py;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The union of every type the operation can return.
|
|
13
|
+
*
|
|
14
|
+
* An operation declaring both `200` and `2XX` returns either body, so the
|
|
15
|
+
* signature admits both rather than only the first.
|
|
16
|
+
*/
|
|
17
|
+
const resultType = (op) => {
|
|
18
|
+
const types = [...new Set((op.successResponses || []).map(successType))];
|
|
19
|
+
if (types.length === 0) return 'None';
|
|
20
|
+
if (types.length === 1) return types[0];
|
|
21
|
+
return types.join(' | ');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The keywords, httpx methods and typing names that differ between the sync and
|
|
26
|
+
* async flavours. Everything else below is spelled once.
|
|
27
|
+
*/
|
|
28
|
+
const kw = isAsync
|
|
29
|
+
? { def: 'async def', for: 'async for', with: 'async with', await: 'await ' }
|
|
30
|
+
: { def: 'def', for: 'for', with: 'with', await: '' };
|
|
31
|
+
const hx = isAsync
|
|
32
|
+
? { close: 'aclose', read: 'aread', iterText: 'aiter_text', iterLines: 'aiter_lines' }
|
|
33
|
+
: { close: 'close', read: 'read', iterText: 'iter_text', iterLines: 'iter_lines' };
|
|
34
|
+
const enterName = isAsync ? '__aenter__' : '__enter__';
|
|
35
|
+
const exitName = isAsync ? '__aexit__' : '__exit__';
|
|
36
|
+
const contextManager = isAsync ? 'asynccontextmanager' : 'contextmanager';
|
|
37
|
+
const asyncPrefix = isAsync ? 'Async' : '';
|
|
38
|
+
const clientClassName = `${asyncPrefix}${className}`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The declared type of a streamed operation. Async uses `AsyncGenerator` rather
|
|
42
|
+
* than `AsyncIterator` so a caller who stops early can `aclose()` the stream in
|
|
43
|
+
* a type-safe way; its send type defaults to `None`, so it is left off (ruff
|
|
44
|
+
* `UP043`).
|
|
45
|
+
*/
|
|
46
|
+
const streamType = (item) => `${isAsync ? 'AsyncGenerator' : 'Iterator'}[${item}]`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The `collections.abc` names the module imports. Async needs two — a
|
|
50
|
+
* `AsyncGenerator` for an operation's stream and a separate `AsyncIterator` for
|
|
51
|
+
* the `_stream_with_url` yield type — where sync spells both `Iterator`.
|
|
52
|
+
*/
|
|
53
|
+
const streamAbc = isAsync ? 'AsyncGenerator' : 'Iterator';
|
|
54
|
+
const contextAbc = isAsync ? 'AsyncIterator' : 'Iterator';
|
|
55
|
+
|
|
56
|
+
const resultTypeFor = (op) => {
|
|
57
|
+
if (op.isStreaming) {
|
|
58
|
+
// A void response has no item type, so the raw text lines are yielded.
|
|
59
|
+
if (!op.result || op.result.type === 'void') return streamType('str');
|
|
60
|
+
const jsonl = (op.responses || []).find(r => r.isJsonlStreaming);
|
|
61
|
+
if (jsonl && jsonl.itemSchemaModel) return streamType(jsonl.itemSchemaModel.pythonClientType);
|
|
62
|
+
return streamType(op.result.pythonClientType);
|
|
63
|
+
}
|
|
64
|
+
return resultType(op);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the response's type resolves to a module-level alias (a collection,
|
|
69
|
+
* union or literal) rather than a pydantic class, which decides between
|
|
70
|
+
* `TypeAdapter(X)` and `X.model_validate(...)`.
|
|
71
|
+
*/
|
|
72
|
+
const isPythonAlias = (resp) =>
|
|
73
|
+
!!resp.referencedCollectionKind ||
|
|
74
|
+
['one-of', 'any-of', 'enum', 'tuple'].includes(resp.export) ||
|
|
75
|
+
!!resp.isEnum;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The structured Python type of a model, so these templates can ask what a type
|
|
79
|
+
* is rather than matching on how it is spelled.
|
|
80
|
+
*/
|
|
81
|
+
const pythonTypeOf = (model) => model.pythonTypeTree ?? { kind: 'builtin', name: 'Any' };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether the model's Python type is a single value rather than a container or a
|
|
85
|
+
* generated class — a `str`, an `int`, or a `Literal` from an enum. Such a body
|
|
86
|
+
* has a text form of its own, so a non-JSON media type sends it verbatim.
|
|
87
|
+
*/
|
|
88
|
+
const isPythonScalarType = (model) => {
|
|
89
|
+
const type = pythonTypeOf(model);
|
|
90
|
+
return type.kind === 'literal' || type.kind === 'builtin';
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether the value is validated by calling `model_validate` on a generated
|
|
95
|
+
* pydantic class. Anything else — a primitive, collection, literal or a name
|
|
96
|
+
* bound to a module-level alias — goes through a `TypeAdapter`.
|
|
97
|
+
*/
|
|
98
|
+
const isModelClass = (model) =>
|
|
99
|
+
!needsPythonTypeAdapter(pythonTypeOf(model)) && !isPythonAlias(model);
|
|
100
|
+
|
|
101
|
+
/** Whether a media type carries a JSON payload. */
|
|
102
|
+
const isJsonMediaType = (mediaType) => {
|
|
103
|
+
const base = (mediaType || '').split(';')[0].trim();
|
|
104
|
+
return base === 'application/json' || base.endsWith('+json');
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Whether a response arrives as JSON on the wire. */
|
|
108
|
+
const isJsonWire = (resp) =>
|
|
109
|
+
(resp.mediaTypes || []).some(isJsonMediaType);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Render the expression that turns an httpx response into the operation's
|
|
113
|
+
* declared type.
|
|
114
|
+
*
|
|
115
|
+
* A primitive on a JSON wire arrives JSON-encoded (a string body is `"hello"`,
|
|
116
|
+
* quotes included), so it is parsed rather than read as text; on a non-JSON
|
|
117
|
+
* wire it is coerced from the raw text instead. Anything with structure —
|
|
118
|
+
* a collection, tuple, union or literal alias — is validated whole, since
|
|
119
|
+
* `resp.type` names the *element* type for an inline collection and can't be
|
|
120
|
+
* dispatched on directly.
|
|
121
|
+
*/
|
|
122
|
+
const responseParseExpr = (resp) => {
|
|
123
|
+
if (resp.type === 'void') return 'None';
|
|
124
|
+
if (resp.type === 'binary') return 'response.content';
|
|
125
|
+
const py = resp.pythonClientType || 'Any';
|
|
126
|
+
const isJsonMedia = isJsonWire(resp);
|
|
127
|
+
// A response declaring JSON alongside another media type can arrive as either,
|
|
128
|
+
// and a declared-JSON body can arrive empty, so the body is read through a
|
|
129
|
+
// helper that tolerates both rather than assuming `response.json()` succeeds.
|
|
130
|
+
const jsonBody =
|
|
131
|
+
(resp.mediaTypes || []).length > 1 ? 'self._json_body(response)' : 'response.json()';
|
|
132
|
+
// An enum or union alias is a single value on the wire, so it reads like a
|
|
133
|
+
// scalar even though it renders as a module-level alias. A referenced
|
|
134
|
+
// collection alias resolves to a container, so it is validated as one.
|
|
135
|
+
const isCollection =
|
|
136
|
+
isPythonCollection(pythonTypeOf(resp)) ||
|
|
137
|
+
['array', 'dictionary'].includes(resp.referencedCollectionKind);
|
|
138
|
+
const isScalar = !isCollection;
|
|
139
|
+
const nullable = resp.isNullable ? (t) => `${t} | None` : (t) => t;
|
|
140
|
+
if (isScalar) {
|
|
141
|
+
if (py === 'Any') return isJsonMedia ? jsonBody : 'response.text';
|
|
142
|
+
// A non-JSON wire hands over text, which pydantic coerces to the declared
|
|
143
|
+
// type (`"7"` to 7, `"true"` to True, an ISO string to a date).
|
|
144
|
+
if (!isJsonMedia) {
|
|
145
|
+
return `TypeAdapter(${nullable(py)}).validate_strings(response.text.strip())`;
|
|
146
|
+
}
|
|
147
|
+
// `str` and `Any` need no coercion from a JSON body; every other declared
|
|
148
|
+
// type does (a JSON number to `float`, an ISO string to a `date`).
|
|
149
|
+
if ((py === 'str' || py === 'Any') && !resp.isNullable) {
|
|
150
|
+
return jsonBody;
|
|
151
|
+
}
|
|
152
|
+
if (isModelClass(resp) && !resp.isNullable) {
|
|
153
|
+
return `${py}.model_validate(${jsonBody})`;
|
|
154
|
+
}
|
|
155
|
+
return `TypeAdapter(${nullable(py)}).validate_python(${jsonBody})`;
|
|
156
|
+
}
|
|
157
|
+
if (!isJsonMedia && py === 'Any') return 'response.text';
|
|
158
|
+
return `TypeAdapter(${nullable(py)}).validate_python(${jsonBody})`;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parse one streamed JSON Lines item. A model class validates itself; anything
|
|
163
|
+
* else (a primitive, collection or alias) goes through a `TypeAdapter`.
|
|
164
|
+
*/
|
|
165
|
+
const streamItemParseExpr = (itemModel, lineVar) => {
|
|
166
|
+
const py = itemModel.pythonClientType || 'Any';
|
|
167
|
+
if (py === 'Any') return `json.loads(${lineVar})`;
|
|
168
|
+
if (!isModelClass(itemModel)) {
|
|
169
|
+
return `TypeAdapter(${py}).validate_json(${lineVar})`;
|
|
170
|
+
}
|
|
171
|
+
return `${py}.model_validate_json(${lineVar})`;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Parse one chunk of a non-JSONL (`x-streaming`) response. A chunk is a
|
|
176
|
+
* fragment of the stream rather than a whole document, so a text response
|
|
177
|
+
* passes through; anything else is deserialised to the declared type, matching
|
|
178
|
+
* the return annotation the caller sees.
|
|
179
|
+
*/
|
|
180
|
+
const streamChunkParseExpr = (resp, lineVar) => {
|
|
181
|
+
const py = resp.type === 'void' ? 'None' : resp.pythonClientType || 'Any';
|
|
182
|
+
// A void response has no item type, so the line is yielded as text rather
|
|
183
|
+
// than validated against `None`, which nothing satisfies.
|
|
184
|
+
if (py === 'None' || py === 'str' || py === 'Any') return lineVar;
|
|
185
|
+
if (!isModelClass(resp)) {
|
|
186
|
+
return `TypeAdapter(${py}).validate_json(${lineVar})`;
|
|
187
|
+
}
|
|
188
|
+
return `${py}.model_validate_json(${lineVar})`;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Render text from the spec as a Python docstring body. A description is
|
|
193
|
+
* arbitrary text, so it goes through the same escaping as any other literal
|
|
194
|
+
* (backslash first) with the surrounding quotes stripped.
|
|
195
|
+
*/
|
|
196
|
+
/** A wire name as a Python string literal, so a quote in it cannot end the string. */
|
|
197
|
+
const pyStr = (value) => toPythonLiteral(String(value));
|
|
198
|
+
|
|
199
|
+
const pyDocstring = (text) =>
|
|
200
|
+
toPythonLiteral(String(text ?? '').replace(/\r?\n/g, ' ')).slice(1, -1);
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* An operation's docstring, indented and wrapped to the vended line length so
|
|
204
|
+
* a long description doesn't trip the `E501` the generated project lints with.
|
|
205
|
+
* Returns the empty string when the operation has nothing to say.
|
|
206
|
+
*/
|
|
207
|
+
const methodDocstring = (op, indent) => {
|
|
208
|
+
const text = (op.description || op.summary || '').split('\n')[0];
|
|
209
|
+
if (!text) return '';
|
|
210
|
+
const body = pyDocstring(text);
|
|
211
|
+
const oneLine = `${indent}"""${body}"""`;
|
|
212
|
+
if (oneLine.length <= 120) return `${oneLine}\n`;
|
|
213
|
+
// Greedily fill lines, leaving room for the indent.
|
|
214
|
+
const width = 120 - indent.length;
|
|
215
|
+
const lines = [];
|
|
216
|
+
let current = '';
|
|
217
|
+
for (const word of body.split(' ')) {
|
|
218
|
+
if (current && `${current} ${word}`.length > width) {
|
|
219
|
+
lines.push(current);
|
|
220
|
+
current = word;
|
|
221
|
+
} else {
|
|
222
|
+
current = current ? `${current} ${word}` : word;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (current) lines.push(current);
|
|
226
|
+
return `${indent}"""\n${lines.map((l) => `${indent}${l}`).join('\n')}\n${indent}"""\n`;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const responseStatusCondition = (code) => {
|
|
230
|
+
if (typeof code === 'string' && /^\dXX$/.test(code)) {
|
|
231
|
+
const base = Number(code.charAt(0)) * 100;
|
|
232
|
+
return `${base} <= response.status_code < ${base + 100}`;
|
|
233
|
+
}
|
|
234
|
+
return `response.status_code == ${code}`;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Whether a response is one the operation returns rather than raises for. */
|
|
238
|
+
const isSuccessResponse = (op, resp) =>
|
|
239
|
+
(op.successResponses || []).some((r) => r.code === resp.code);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Whether one branch always runs, making the trailing raise unreachable.
|
|
243
|
+
*
|
|
244
|
+
* Only an error `default` is unconditional: as the success branch it carries a
|
|
245
|
+
* 2xx check, so a non-2xx status still falls through to the raise below.
|
|
246
|
+
*/
|
|
247
|
+
const hasUnconditionalBranch = (op) =>
|
|
248
|
+
(op.responses || []).some(
|
|
249
|
+
(r) => r.code === 'default' && !isSuccessResponse(op, r),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The status check guarding a response branch, or null when none is needed.
|
|
254
|
+
*
|
|
255
|
+
* `default` matches any status the spec did not enumerate, so it needs no check
|
|
256
|
+
* — except where it is the success branch, which only a 2xx status may take.
|
|
257
|
+
*/
|
|
258
|
+
const branchCondition = (op, resp) => {
|
|
259
|
+
if (resp.code !== 'default') return responseStatusCondition(resp.code);
|
|
260
|
+
return isSuccessResponse(op, resp) ? '200 <= response.status_code < 300' : null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Render the error raise expression for a non-success response.
|
|
265
|
+
*
|
|
266
|
+
* The payload is handed over raw and coerced by the wrapper, so a body that
|
|
267
|
+
* doesn't match the schema the spec declares still surfaces as the typed
|
|
268
|
+
* exception rather than a validation error over the error itself.
|
|
269
|
+
*/
|
|
270
|
+
const renderTypedRaise = (op, resp) => {
|
|
271
|
+
const entry = (op.errorShape?.entries || []).find((e) => e.code === resp.code);
|
|
272
|
+
const cls = op.errorShape.exceptionClassName;
|
|
273
|
+
if (!entry) return `raise ${cls}(response.status_code, None)`;
|
|
274
|
+
const payload = resp.type === 'void'
|
|
275
|
+
? 'None'
|
|
276
|
+
: resp.type === 'binary'
|
|
277
|
+
? 'response.content'
|
|
278
|
+
: 'self._error_payload(response)';
|
|
279
|
+
// For exact numeric codes we can pass the literal value directly so the
|
|
280
|
+
// Literal-typed `status` field type-checks with no suppression.
|
|
281
|
+
const statusExpr = entry.isExactCode
|
|
282
|
+
? String(entry.code)
|
|
283
|
+
: 'response.status_code';
|
|
284
|
+
return (
|
|
285
|
+
`raise ${cls}(\n` +
|
|
286
|
+
` response.status_code,\n` +
|
|
287
|
+
` self._error(\n` +
|
|
288
|
+
` types.${entry.className}, ${statusExpr}, ${payload}\n` +
|
|
289
|
+
` ),\n` +
|
|
290
|
+
` )`
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/** Is this operation a single-body call eligible for positional-arg style? */
|
|
295
|
+
const isPositionalBody = (op) => !!op.requestShape?.isSingleBodyInput;
|
|
296
|
+
|
|
297
|
+
/** Build the `def method(self, *, …)` signature from the generic request shape. */
|
|
298
|
+
const signature = (op) => {
|
|
299
|
+
const inputs = op.requestShape?.inputs ?? [];
|
|
300
|
+
if (inputs.length === 0) return 'self';
|
|
301
|
+
if (isPositionalBody(op)) {
|
|
302
|
+
const k = inputs[0];
|
|
303
|
+
return `self,\n ${k.pythonName}: ${k.pythonAnnotation}${k.isRequired ? '' : ' = None'}`;
|
|
304
|
+
}
|
|
305
|
+
const parts = ['self', '*'];
|
|
306
|
+
for (const k of inputs) {
|
|
307
|
+
parts.push(k.isRequired
|
|
308
|
+
? `${k.pythonName}: ${k.pythonAnnotation}`
|
|
309
|
+
: `${k.pythonName}: ${k.pythonAnnotation} = None`);
|
|
310
|
+
}
|
|
311
|
+
return parts.join(',\n ');
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const inputsByKind = (op, kind) =>
|
|
315
|
+
(op.requestShape?.inputs ?? []).filter((k) => k.source.kind === kind);
|
|
316
|
+
|
|
317
|
+
// Derived from the resolved attribute name, which the shared codegen has
|
|
318
|
+
// already made a valid identifier — the raw tag may be any string at all.
|
|
319
|
+
const pythonClassNameForTag = (tag) => {
|
|
320
|
+
const name = pythonTagNames[tag];
|
|
321
|
+
return `_${asyncPrefix}${name.charAt(0).toUpperCase()}${name.slice(1)}Namespace`;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// `operationsByTag` keys are already normalised to identifiers by the shared
|
|
325
|
+
// codegen, and `pythonTagNames` maps each to its snake_case attribute name.
|
|
326
|
+
const tagGroups = operationsByTag;
|
|
327
|
+
// A content-based parameter (declared with `content` rather than `schema`)
|
|
328
|
+
// is serialised to its media type — JSON — as a single value.
|
|
329
|
+
const hasContentJsonParams = allOperations.some(op => (op.parameters || []).some(p => p.in !== 'body' && p.mediaType === 'application/json'));
|
|
330
|
+
const hasPartContentTypes = allOperations.some(op => op.parametersBody && op.parametersBody.partContentTypes);
|
|
331
|
+
const hasAllowReservedParams = allOperations.some(op => (op.parameters || []).some(p => p.in === 'query' && p.allowReserved));
|
|
332
|
+
const abcImports = [...new Set([streamAbc, ...(hasAllowReservedParams ? [contextAbc] : [])])].join(', ');
|
|
333
|
+
// A response declaring several media types is read through `_json_body`.
|
|
334
|
+
const hasMultiMediaResponse = allOperations.some(op => (op.responses || []).some(r => (r.mediaTypes || []).length > 1));
|
|
335
|
+
// An untyped streamed item is decoded with `json.loads`, which needs the import.
|
|
336
|
+
const hasUntypedStreamItems = allOperations.some(op => (op.responses || []).some(r =>
|
|
337
|
+
r.isJsonlStreaming && r.itemSchemaModel && (r.itemSchemaModel.pythonClientType || 'Any') === 'Any'));
|
|
338
|
+
// A structured multipart part is JSON encoded before it becomes a text part.
|
|
339
|
+
const hasMultipartBody = allOperations.some(op => {
|
|
340
|
+
const shape = op.requestShape;
|
|
341
|
+
const body = shape && (shape.bodyFromFields || shape.bodyAsSingleInput);
|
|
342
|
+
return !!body && body.mediaType === 'multipart/form-data';
|
|
343
|
+
});
|
|
344
|
+
// Render the `self._url(...)` expression for an operation, including matrix/
|
|
345
|
+
// label path styles when declared.
|
|
346
|
+
const urlExpr = (op, pathInputs, styledPathInputs) => {
|
|
347
|
+
let expr = `self._url("${op.path}", ${pathInputs.length > 0 ? "path_params" : "{}"}`;
|
|
348
|
+
if (styledPathInputs.length > 0) {
|
|
349
|
+
expr += `, path_styles={${styledPathInputs.map(k => `"${k.source.wireName}": ("${k.model.pathStyle}", ${k.model.pathExplode ? 'True' : 'False'})`).join(', ')}}`;
|
|
350
|
+
}
|
|
351
|
+
return expr + ')';
|
|
352
|
+
};
|
|
353
|
+
_%>
|
|
354
|
+
"""<%- clientClassName %> — AUTO-GENERATED <%- isAsync ? 'asynchronous' : 'synchronous' %> client."""
|
|
355
|
+
|
|
356
|
+
from __future__ import annotations
|
|
357
|
+
|
|
358
|
+
import datetime
|
|
359
|
+
<%_ if (hasContentJsonParams || hasPartContentTypes || hasUntypedStreamItems || hasMultipartBody) { _%>
|
|
360
|
+
import json
|
|
361
|
+
<%_ } _%>
|
|
362
|
+
import warnings
|
|
363
|
+
from collections.abc import <%- abcImports %>
|
|
364
|
+
<%_ if (hasAllowReservedParams) { _%>
|
|
365
|
+
from contextlib import <%- contextManager %>
|
|
366
|
+
<%_ } _%>
|
|
367
|
+
from dataclasses import dataclass, field
|
|
368
|
+
from typing import Any, Literal, TypeVar
|
|
369
|
+
from urllib.parse import quote
|
|
370
|
+
|
|
371
|
+
import httpx
|
|
372
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
373
|
+
|
|
374
|
+
from . import types
|
|
375
|
+
from .errors import (
|
|
376
|
+
ApiError as ApiError,
|
|
377
|
+
<%_ allOperations.forEach((op) => { _%>
|
|
378
|
+
<%- op.errorShape.exceptionClassName %> as <%- op.errorShape.exceptionClassName %>,
|
|
379
|
+
<%_ }); _%>
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
_ErrorT = TypeVar("_ErrorT", bound=BaseModel)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@dataclass
|
|
386
|
+
class <%- clientClassName %>Config:
|
|
387
|
+
"""Client configuration for <%- clientClassName %>."""
|
|
388
|
+
|
|
389
|
+
#: Base URL for the API.
|
|
390
|
+
url: str
|
|
391
|
+
#: Client used for every request. Supply your own to add authentication or
|
|
392
|
+
#: any other middleware — an ``httpx.Auth`` (e.g. a SigV4 signer), an event
|
|
393
|
+
#: hook, a custom transport, timeouts, retries or a proxy all apply, and the
|
|
394
|
+
#: client's own headers and query params are sent alongside each operation's.
|
|
395
|
+
#: A supplied client is never closed by this one, so it can be shared.
|
|
396
|
+
httpx_client: httpx.<%- asyncPrefix %>Client | None = None
|
|
397
|
+
#: Headers added to every request, where the operation sets none itself.
|
|
398
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
399
|
+
#: By default the client sends a Content-Type header set to the media type
|
|
400
|
+
#: the OpenAPI specification declares for the request. Set this to omit it.
|
|
401
|
+
omit_content_type_header: bool = False
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class <%- clientClassName %>:
|
|
405
|
+
"""<%- isAsync ? 'Asynchronous' : 'Synchronous' %> API client for <%- className %>."""
|
|
406
|
+
|
|
407
|
+
def __init__(self, config: <%- clientClassName %>Config) -> None:
|
|
408
|
+
self._config = config
|
|
409
|
+
self._client = config.httpx_client or httpx.<%- asyncPrefix %>Client()
|
|
410
|
+
self._owns_client = config.httpx_client is None
|
|
411
|
+
self._base_url = config.url.rstrip("/")
|
|
412
|
+
<%_ Object.keys(tagGroups).forEach((tag) => { _%>
|
|
413
|
+
self.<%- pythonTagNames[tag] %>: <%- pythonClassNameForTag(tag) %> = <%- pythonClassNameForTag(tag) %>(self)
|
|
414
|
+
<%_ }); _%>
|
|
415
|
+
|
|
416
|
+
<%- kw.def %> <%- enterName %>(self) -> <%- clientClassName %>:
|
|
417
|
+
return self
|
|
418
|
+
|
|
419
|
+
<%- kw.def %> <%- exitName %>(self, *exc: Any) -> None:
|
|
420
|
+
<%- kw.await %>self.<%- hx.close %>()
|
|
421
|
+
|
|
422
|
+
<%- kw.def %> <%- hx.close %>(self) -> None:
|
|
423
|
+
if self._owns_client:
|
|
424
|
+
<%- kw.await %>self._client.<%- hx.close %>()
|
|
425
|
+
|
|
426
|
+
def _url(
|
|
427
|
+
self,
|
|
428
|
+
path: str,
|
|
429
|
+
path_params: dict[str, Any],
|
|
430
|
+
path_styles: dict[str, tuple[str, bool]] | None = None,
|
|
431
|
+
) -> str:
|
|
432
|
+
for key, value in path_params.items():
|
|
433
|
+
style, explode = (path_styles or {}).get(key, ("simple", False))
|
|
434
|
+
# An object expands to its key,value members; a list to its items.
|
|
435
|
+
# Each is encoded on its own so the separators between them stay
|
|
436
|
+
# literal, as RFC 6570 requires.
|
|
437
|
+
dumped = self._dump(value)
|
|
438
|
+
if isinstance(dumped, dict):
|
|
439
|
+
values = [
|
|
440
|
+
part
|
|
441
|
+
for k, v in dumped.items()
|
|
442
|
+
if v is not None
|
|
443
|
+
for part in (k, self._scalar(v))
|
|
444
|
+
]
|
|
445
|
+
elif isinstance(dumped, list):
|
|
446
|
+
values = list(dumped)
|
|
447
|
+
else:
|
|
448
|
+
values = [value]
|
|
449
|
+
if style == "matrix":
|
|
450
|
+
# RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
|
|
451
|
+
if explode:
|
|
452
|
+
rendered = "".join(";" + key + "=" + quote(self._scalar(v), safe="") for v in values)
|
|
453
|
+
else:
|
|
454
|
+
rendered = ";" + key + "=" + ",".join(quote(self._scalar(v), safe="") for v in values)
|
|
455
|
+
elif style == "label":
|
|
456
|
+
# RFC 6570 label expansion: .a,b (explode: .a.b)
|
|
457
|
+
rendered = "." + ("." if explode else ",").join(quote(self._scalar(v), safe="") for v in values)
|
|
458
|
+
else:
|
|
459
|
+
# RFC 6570 simple expansion: a list renders comma-separated.
|
|
460
|
+
rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
|
|
461
|
+
path = path.replace("{" + key + "}", rendered)
|
|
462
|
+
return self._base_url + path
|
|
463
|
+
|
|
464
|
+
<%_ if (hasAllowReservedParams) { _%>
|
|
465
|
+
def _query_string(
|
|
466
|
+
self,
|
|
467
|
+
query_params: dict[str, Any],
|
|
468
|
+
collection_formats: dict[str, str] | None,
|
|
469
|
+
reserved: set[str],
|
|
470
|
+
) -> str:
|
|
471
|
+
# Build the query string manually: httpx percent-encodes reserved
|
|
472
|
+
# characters, which allowReserved parameters must keep literal. The
|
|
473
|
+
# httpx client's own params are folded in here, because a client with
|
|
474
|
+
# params set would otherwise replace this query string wholesale.
|
|
475
|
+
parts: list[str] = [
|
|
476
|
+
quote(str(key), safe="") + "=" + quote(str(value), safe="")
|
|
477
|
+
for key, value in self._client.params.multi_items()
|
|
478
|
+
]
|
|
479
|
+
for key, value in self._query(query_params, collection_formats).items():
|
|
480
|
+
# RFC 3986 reserved characters kept literal for an allowReserved
|
|
481
|
+
# parameter. `#` is excluded: it would start the URL fragment and
|
|
482
|
+
# silently drop this value and every parameter after it.
|
|
483
|
+
safe = "/:?[]@!$&'()*+,;=" if key in reserved else ""
|
|
484
|
+
values = value if isinstance(value, list) else [value]
|
|
485
|
+
for v in values:
|
|
486
|
+
parts.append(quote(str(key), safe="") + "=" + quote(self._scalar(v), safe=safe))
|
|
487
|
+
return "&".join(parts)
|
|
488
|
+
|
|
489
|
+
<%_ } _%>
|
|
490
|
+
def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
|
|
491
|
+
if isinstance(value, BaseModel):
|
|
492
|
+
value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
|
|
493
|
+
if isinstance(value, dict):
|
|
494
|
+
for prop, v in value.items():
|
|
495
|
+
self._deep_object(f"{key}[{prop}]", v, out)
|
|
496
|
+
elif value is not None:
|
|
497
|
+
out[key] = value
|
|
498
|
+
|
|
499
|
+
_COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
|
|
500
|
+
|
|
501
|
+
#: Characters left literal in a cookie value: every RFC 6265 cookie-octet
|
|
502
|
+
#: except the `,` and `;` this client joins members on. A `=` stays literal
|
|
503
|
+
#: too — only the first one separates a cookie's name from its value.
|
|
504
|
+
_COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_`{|}~="
|
|
505
|
+
|
|
506
|
+
def _scalar(self, value: Any) -> str:
|
|
507
|
+
# The wire form of a single parameter value: JSON spellings for bools,
|
|
508
|
+
# ISO-8601 for dates, and the model's own wire shape for anything else.
|
|
509
|
+
if isinstance(value, bool):
|
|
510
|
+
return "true" if value else "false"
|
|
511
|
+
if isinstance(value, (datetime.date, datetime.datetime)):
|
|
512
|
+
return value.isoformat()
|
|
513
|
+
dumped = self._dump(value)
|
|
514
|
+
if isinstance(dumped, str):
|
|
515
|
+
return dumped
|
|
516
|
+
# RFC 6570 renders a list as its comma-joined members and an object as
|
|
517
|
+
# comma-joined key,value pairs. Python's own `str` would emit a repr —
|
|
518
|
+
# `['a', 'b']`, quotes and spaces included.
|
|
519
|
+
if isinstance(dumped, list):
|
|
520
|
+
return ",".join(self._scalar(v) for v in dumped)
|
|
521
|
+
if isinstance(dumped, dict):
|
|
522
|
+
return ",".join(
|
|
523
|
+
f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
|
|
524
|
+
)
|
|
525
|
+
if dumped is None:
|
|
526
|
+
return ""
|
|
527
|
+
return str(dumped)
|
|
528
|
+
|
|
529
|
+
def _query(self, query_params: dict[str, Any], collection_formats: dict[str, str] | None = None) -> dict[str, Any]:
|
|
530
|
+
out: dict[str, Any] = {}
|
|
531
|
+
for key, value in query_params.items():
|
|
532
|
+
if value is None:
|
|
533
|
+
continue
|
|
534
|
+
fmt = (collection_formats or {}).get(key)
|
|
535
|
+
if fmt == "deepObject":
|
|
536
|
+
self._deep_object(key, value, out)
|
|
537
|
+
elif isinstance(value, list):
|
|
538
|
+
delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
|
|
539
|
+
if delimiter is None:
|
|
540
|
+
out[key] = [self._scalar(v) for v in value]
|
|
541
|
+
else:
|
|
542
|
+
out[key] = delimiter.join(self._scalar(v) for v in value)
|
|
543
|
+
elif self._is_object(value):
|
|
544
|
+
# OpenAPI's default query style is form/explode, under which an
|
|
545
|
+
# object expands to one parameter per property rather than
|
|
546
|
+
# being stringified.
|
|
547
|
+
for prop, prop_value in self._as_dict(value).items():
|
|
548
|
+
if prop_value is not None:
|
|
549
|
+
out[prop] = self._scalar(prop_value)
|
|
550
|
+
else:
|
|
551
|
+
out[key] = self._scalar(value)
|
|
552
|
+
return out
|
|
553
|
+
|
|
554
|
+
def _is_object(self, value: Any) -> bool:
|
|
555
|
+
return isinstance(value, (dict, BaseModel))
|
|
556
|
+
|
|
557
|
+
def _as_dict(self, value: Any) -> dict[str, Any]:
|
|
558
|
+
if isinstance(value, BaseModel):
|
|
559
|
+
return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
|
|
560
|
+
return dict(value)
|
|
561
|
+
|
|
562
|
+
def _headers(
|
|
563
|
+
self,
|
|
564
|
+
header_params: dict[str, Any] | None = None,
|
|
565
|
+
collection_formats: dict[str, str] | None = None,
|
|
566
|
+
cookie_params: dict[str, Any] | None = None,
|
|
567
|
+
) -> list[tuple[str, str]]:
|
|
568
|
+
out: list[tuple[str, str]] = []
|
|
569
|
+
for key, value in (header_params or {}).items():
|
|
570
|
+
if value is None:
|
|
571
|
+
continue
|
|
572
|
+
if isinstance(value, list):
|
|
573
|
+
fmt = (collection_formats or {}).get(key, "csv")
|
|
574
|
+
delimiter = self._COLLECTION_DELIMITERS.get(fmt)
|
|
575
|
+
if delimiter is None:
|
|
576
|
+
for item in value:
|
|
577
|
+
out.append((key, self._scalar(item)))
|
|
578
|
+
else:
|
|
579
|
+
out.append((key, delimiter.join(self._scalar(v) for v in value)))
|
|
580
|
+
else:
|
|
581
|
+
out.append((key, self._scalar(value)))
|
|
582
|
+
# A config header only applies where the operation didn't set one:
|
|
583
|
+
# httpx comma-joins repeated names, which would corrupt the value.
|
|
584
|
+
operation_headers = {key.lower() for key, _ in out}
|
|
585
|
+
out.extend(
|
|
586
|
+
(key, value)
|
|
587
|
+
for key, value in self._config.headers.items()
|
|
588
|
+
if key.lower() not in operation_headers
|
|
589
|
+
)
|
|
590
|
+
# The operation's cookies are merged into one `Cookie` header alongside
|
|
591
|
+
# every other source. Passing them to httpx's `cookies=` instead would
|
|
592
|
+
# drop them outright whenever a `Cookie` header is present, since
|
|
593
|
+
# http.cookiejar skips a request that already carries one — and setting
|
|
594
|
+
# the header here would in turn shadow the caller's own jar and header,
|
|
595
|
+
# which httpx resolves in favour of the more specific value.
|
|
596
|
+
cookies = self._cookies(cookie_params or {})
|
|
597
|
+
if cookies:
|
|
598
|
+
pairs = [
|
|
599
|
+
*(
|
|
600
|
+
value
|
|
601
|
+
for source in (self._client.headers, dict(out))
|
|
602
|
+
for key, value in source.items()
|
|
603
|
+
if key.lower() == "cookie"
|
|
604
|
+
),
|
|
605
|
+
*(f"{key}={value}" for key, value in self._client.cookies.items()),
|
|
606
|
+
*(f"{key}={value}" for key, value in cookies.items()),
|
|
607
|
+
]
|
|
608
|
+
out = [(key, value) for key, value in out if key.lower() != "cookie"]
|
|
609
|
+
out.append(("Cookie", "; ".join(pairs)))
|
|
610
|
+
return out
|
|
611
|
+
|
|
612
|
+
def _default_content_type(
|
|
613
|
+
self, header_params: dict[str, Any], media_type: str
|
|
614
|
+
) -> None:
|
|
615
|
+
# The media type the specification declares, applied unless the operation
|
|
616
|
+
# takes a Content-Type of its own and the caller supplied one. Header
|
|
617
|
+
# names are case-insensitive, and a declared header the caller omitted is
|
|
618
|
+
# present holding `None` — neither counts as the caller setting it.
|
|
619
|
+
if any(
|
|
620
|
+
key.lower() == "content-type" and value is not None
|
|
621
|
+
for key, value in header_params.items()
|
|
622
|
+
):
|
|
623
|
+
return
|
|
624
|
+
for key in list(header_params):
|
|
625
|
+
if key.lower() == "content-type":
|
|
626
|
+
header_params[key] = media_type
|
|
627
|
+
return
|
|
628
|
+
header_params["Content-Type"] = media_type
|
|
629
|
+
|
|
630
|
+
def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
|
|
631
|
+
out: dict[str, str] = {}
|
|
632
|
+
for key, value in cookie_params.items():
|
|
633
|
+
if value is None:
|
|
634
|
+
continue
|
|
635
|
+
dumped = self._dump(value)
|
|
636
|
+
# A cookie uses form-style expansion, so a list joins on `,` and an
|
|
637
|
+
# object on `,` between each key and value. Members are encoded so a
|
|
638
|
+
# `;` or `,` inside one can't be read as a delimiter, while the
|
|
639
|
+
# joining commas stay literal.
|
|
640
|
+
if isinstance(dumped, list):
|
|
641
|
+
rendered = ",".join(self._cookie_value(v) for v in dumped)
|
|
642
|
+
elif isinstance(dumped, dict):
|
|
643
|
+
rendered = ",".join(
|
|
644
|
+
self._cookie_value(k) + "," + self._cookie_value(v)
|
|
645
|
+
for k, v in dumped.items()
|
|
646
|
+
if v is not None
|
|
647
|
+
)
|
|
648
|
+
else:
|
|
649
|
+
rendered = self._cookie_value(value)
|
|
650
|
+
out[key] = rendered
|
|
651
|
+
return out
|
|
652
|
+
|
|
653
|
+
def _cookie_value(self, value: Any) -> str:
|
|
654
|
+
# Only the characters a cookie parser treats as structure are escaped, so
|
|
655
|
+
# a timestamp keeps its colons and a base64 token its `=` padding. Servers
|
|
656
|
+
# do not URL-decode cookies, so anything escaped here arrives escaped.
|
|
657
|
+
return quote(self._scalar(value), safe=self._COOKIE_SAFE)
|
|
658
|
+
|
|
659
|
+
def _dump(self, value: Any) -> Any:
|
|
660
|
+
if value is None:
|
|
661
|
+
return None
|
|
662
|
+
if isinstance(value, BaseModel):
|
|
663
|
+
return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
|
|
664
|
+
if isinstance(value, (list, tuple)):
|
|
665
|
+
return [self._dump(v) for v in value]
|
|
666
|
+
if isinstance(value, dict):
|
|
667
|
+
return {k: self._dump(v) for k, v in value.items()}
|
|
668
|
+
if isinstance(value, (datetime.date, datetime.datetime)):
|
|
669
|
+
return value.isoformat()
|
|
670
|
+
return value
|
|
671
|
+
|
|
672
|
+
<%_ if (hasAllowReservedParams) { _%>
|
|
673
|
+
<%- kw.def %> _send_with_url(self, final_url: str, **kwargs: Any) -> httpx.Response:
|
|
674
|
+
# `request()` lets httpx rebuild the query from the client's params,
|
|
675
|
+
# discarding a hand-built one. Building the request first and overriding
|
|
676
|
+
# the URL keeps it, while auth and event hooks still run on send.
|
|
677
|
+
request = self._client.build_request(**kwargs)
|
|
678
|
+
request.url = httpx.URL(final_url)
|
|
679
|
+
return <%- kw.await %>self._client.send(request)
|
|
680
|
+
|
|
681
|
+
@<%- contextManager %>
|
|
682
|
+
<%- kw.def %> _stream_with_url(
|
|
683
|
+
self, final_url: str, **kwargs: Any
|
|
684
|
+
) -> <%- contextAbc %>[httpx.Response]:
|
|
685
|
+
# The streaming counterpart of `_send_with_url`: `stream()` would rebuild
|
|
686
|
+
# the query from the client's params and percent-encode a hand-built one.
|
|
687
|
+
request = self._client.build_request(**kwargs)
|
|
688
|
+
request.url = httpx.URL(final_url)
|
|
689
|
+
response = <%- kw.await %>self._client.send(request, stream=True)
|
|
690
|
+
try:
|
|
691
|
+
yield response
|
|
692
|
+
finally:
|
|
693
|
+
<%- kw.await %>response.<%- hx.close %>()
|
|
694
|
+
|
|
695
|
+
<%_ } _%>
|
|
696
|
+
<%_ if (hasMultipartBody) { _%>
|
|
697
|
+
def _is_file_part(self, value: Any) -> bool:
|
|
698
|
+
# httpx reads a file part as (filename, content[, content_type]), so a
|
|
699
|
+
# tuple only qualifies in that shape — a `prefixItems` field is a tuple
|
|
700
|
+
# too, and passing one to httpx as a file raises a TypeError.
|
|
701
|
+
if isinstance(value, tuple):
|
|
702
|
+
return 2 <= len(value) <= 3 and isinstance(value[0], (str, type(None)))
|
|
703
|
+
return hasattr(value, "read") or isinstance(value, (bytes, bytearray))
|
|
704
|
+
|
|
705
|
+
def _multipart(
|
|
706
|
+
self,
|
|
707
|
+
fields: dict[str, Any],
|
|
708
|
+
part_content_types: dict[str, str] | None = None,
|
|
709
|
+
) -> list[tuple[str, Any]]:
|
|
710
|
+
# Every field becomes an httpx `files=` part: binary values stream as
|
|
711
|
+
# file parts (one per element for a list) and everything else is sent as
|
|
712
|
+
# text, with structured values JSON encoded. Text fields go through
|
|
713
|
+
# `files=` rather than `data=` because httpx only chooses multipart when
|
|
714
|
+
# a file part is present, and only `files=` carries a per-part content
|
|
715
|
+
# type from the request body's `encoding` object.
|
|
716
|
+
parts: list[tuple[str, Any]] = []
|
|
717
|
+
for key, value in fields.items():
|
|
718
|
+
content_type = (part_content_types or {}).get(key)
|
|
719
|
+
values = value if isinstance(value, list) else [value]
|
|
720
|
+
if any(self._is_file_part(item) for item in values):
|
|
721
|
+
parts.extend(
|
|
722
|
+
(key, item if content_type is None else (None, item, content_type))
|
|
723
|
+
for item in values
|
|
724
|
+
)
|
|
725
|
+
continue
|
|
726
|
+
for item in values:
|
|
727
|
+
text = self._part_text(self._dump(item))
|
|
728
|
+
parts.append(
|
|
729
|
+
(key, (None, text) if content_type is None else (None, text, content_type))
|
|
730
|
+
)
|
|
731
|
+
return parts
|
|
732
|
+
|
|
733
|
+
def _part_text(self, value: Any) -> str:
|
|
734
|
+
if isinstance(value, bool):
|
|
735
|
+
return "true" if value else "false"
|
|
736
|
+
if isinstance(value, str):
|
|
737
|
+
return value
|
|
738
|
+
if value is None or isinstance(value, (int, float)):
|
|
739
|
+
return "" if value is None else str(value)
|
|
740
|
+
return json.dumps(value, separators=(",", ":"))
|
|
741
|
+
|
|
742
|
+
<%_ } _%>
|
|
743
|
+
<%_ if (hasMultiMediaResponse) { _%>
|
|
744
|
+
def _json_body(self, response: httpx.Response) -> Any:
|
|
745
|
+
# A response declaring JSON alongside another media type can arrive as
|
|
746
|
+
# either, and a declared-JSON body can arrive empty (a 200 with no
|
|
747
|
+
# content). Both would raise a bare JSONDecodeError out of the client, so
|
|
748
|
+
# the text is handed over instead.
|
|
749
|
+
if not response.content:
|
|
750
|
+
return None
|
|
751
|
+
try:
|
|
752
|
+
return response.json()
|
|
753
|
+
except ValueError:
|
|
754
|
+
return response.text
|
|
755
|
+
|
|
756
|
+
<%_ } _%>
|
|
757
|
+
def _error_payload(self, response: httpx.Response) -> Any:
|
|
758
|
+
# An error body is whatever the server actually sent, which need not be
|
|
759
|
+
# the JSON the spec declares (a proxy's HTML page, say).
|
|
760
|
+
try:
|
|
761
|
+
return response.json()
|
|
762
|
+
except ValueError:
|
|
763
|
+
return response.text
|
|
764
|
+
|
|
765
|
+
def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
|
|
766
|
+
# Coerce to the declared error type, falling back to the raw payload so
|
|
767
|
+
# a body that doesn't match the spec still raises the typed exception.
|
|
768
|
+
try:
|
|
769
|
+
return wrapper.model_validate({"status": status, "error": payload})
|
|
770
|
+
except ValidationError:
|
|
771
|
+
return wrapper.model_construct(status=status, error=payload)
|
|
772
|
+
<%_ allOperations.forEach((op) => {
|
|
773
|
+
const shape = op.requestShape ?? { inputs: [] };
|
|
774
|
+
const pathInputs = inputsByKind(op, 'path');
|
|
775
|
+
const queryInputs = inputsByKind(op, 'query');
|
|
776
|
+
const headerInputs = inputsByKind(op, 'header');
|
|
777
|
+
const cookieInputs = inputsByKind(op, 'cookie');
|
|
778
|
+
const bodyFieldInputs = inputsByKind(op, 'body-field');
|
|
779
|
+
const wholeBodyInputs = inputsByKind(op, 'body');
|
|
780
|
+
const queryCollectionInputs = queryInputs.filter(k => k.source.collectionFormat);
|
|
781
|
+
const headerCollectionInputs = headerInputs.filter(k => k.source.collectionFormat);
|
|
782
|
+
const bodyMediaType = (shape.bodyFromFields && shape.bodyFromFields.mediaType)
|
|
783
|
+
|| (shape.bodyAsSingleInput && shape.bodyAsSingleInput.mediaType)
|
|
784
|
+
|| undefined;
|
|
785
|
+
const bodyIsBinary = wholeBodyInputs.length === 1 && wholeBodyInputs[0].model.type === 'binary';
|
|
786
|
+
const bodyIsMultipart = bodyMediaType === 'multipart/form-data';
|
|
787
|
+
const bodyIsUrlEncoded = bodyMediaType === 'application/x-www-form-urlencoded';
|
|
788
|
+
// A body is JSON-encoded only when its media type says so. Anything else
|
|
789
|
+
// (text/plain, application/xml, ...) carries its own encoding, so a primitive
|
|
790
|
+
// is sent verbatim rather than wrapped in JSON quotes.
|
|
791
|
+
const bodyIsJsonWire = !bodyMediaType || isJsonMediaType(bodyMediaType);
|
|
792
|
+
// A scalar body under a non-JSON media type is sent verbatim. An enum is a
|
|
793
|
+
// scalar on the wire without being `isPrimitive`, so it is admitted too.
|
|
794
|
+
const bodyIsScalarNonJson = !bodyIsJsonWire
|
|
795
|
+
&& wholeBodyInputs.length === 1
|
|
796
|
+
&& (wholeBodyInputs[0].model.isPrimitive
|
|
797
|
+
|| wholeBodyInputs[0].model.isEnum
|
|
798
|
+
|| isPythonScalarType(wholeBodyInputs[0].model));
|
|
799
|
+
const contentJsonInputs = [...queryInputs, ...headerInputs, ...cookieInputs].filter(k => k.model.mediaType === 'application/json');
|
|
800
|
+
const styledPathInputs = pathInputs.filter(k => k.model.pathStyle);
|
|
801
|
+
const reservedInputs = queryInputs.filter(k => k.model.allowReserved);
|
|
802
|
+
const partContentTypes = (op.parametersBody && op.parametersBody.partContentTypes) || undefined;
|
|
803
|
+
// Whether the operation writes a Content-Type into `header_params`, which is
|
|
804
|
+
// also what declares it. A whole binary body is sent as raw content even under
|
|
805
|
+
// a form media type, so it sets the header httpx would otherwise derive from
|
|
806
|
+
// the multipart body it is not building.
|
|
807
|
+
const sendsContentType = !!bodyMediaType && (bodyIsBinary || !bodyIsMultipart);
|
|
808
|
+
// `_headers` also takes the cookies, since it merges them into `Cookie`.
|
|
809
|
+
// Keyword form, so each argument is independent of the others being present.
|
|
810
|
+
const headerArgs = [
|
|
811
|
+
headerInputs.length > 0 || sendsContentType ? 'header_params' : '',
|
|
812
|
+
headerCollectionInputs.length > 0 ? 'collection_formats=header_formats' : '',
|
|
813
|
+
cookieInputs.length > 0 ? 'cookie_params=cookie_params' : '',
|
|
814
|
+
].filter(Boolean).join(', ');
|
|
815
|
+
const hasTag = (op.tags && op.tags.length > 0);
|
|
816
|
+
const methodPrefix = hasTag ? '_' : '';
|
|
817
|
+
_%>
|
|
818
|
+
|
|
819
|
+
<%- kw.def %> <%- methodPrefix %><%- op.pythonMethodName %>(
|
|
820
|
+
<%- signature(op) %>,
|
|
821
|
+
) -> <%- resultTypeFor(op) %>:
|
|
822
|
+
<%- methodDocstring(op, ' ') %><%_ if (op.deprecated) { _%>
|
|
823
|
+
warnings.warn(
|
|
824
|
+
"`<%- op.pythonMethodName %>` is deprecated.",
|
|
825
|
+
DeprecationWarning,
|
|
826
|
+
stacklevel=2,
|
|
827
|
+
)
|
|
828
|
+
<%_ } _%>
|
|
829
|
+
<%_ if (pathInputs.length > 0) { _%>
|
|
830
|
+
path_params: dict[str, Any] = {<%_ pathInputs.forEach((k, i) => { _%><%- i > 0 ? ', ' : '' %><%- pyStr(k.source.wireName) %>: <%- k.pythonName %><%_ }); _%>}
|
|
831
|
+
<%_ } _%>
|
|
832
|
+
<%_ if (queryInputs.length > 0) { _%>
|
|
833
|
+
query_params: dict[str, Any] = {<%_ queryInputs.forEach((k, i) => { _%><%- i > 0 ? ', ' : '' %><%- pyStr(k.source.wireName) %>: <%- k.pythonName %><%_ }); _%>}
|
|
834
|
+
<%_ } _%>
|
|
835
|
+
<%_ if (headerInputs.length > 0 || sendsContentType) { _%>
|
|
836
|
+
header_params: dict[str, Any] = {<%_ headerInputs.forEach((k, i) => { _%><%- i > 0 ? ', ' : '' %><%- pyStr(k.source.wireName) %>: <%- k.pythonName %><%_ }); _%>}
|
|
837
|
+
<%_ } _%>
|
|
838
|
+
<%_ if (cookieInputs.length > 0) { _%>
|
|
839
|
+
cookie_params: dict[str, Any] = {<%_ cookieInputs.forEach((k, i) => { _%><%- i > 0 ? ', ' : '' %><%- pyStr(k.source.wireName) %>: <%- k.pythonName %><%_ }); _%>}
|
|
840
|
+
<%_ } _%>
|
|
841
|
+
<%_ /* A content-based parameter (declared with `content` rather than
|
|
842
|
+
`schema`) is serialised to its media type — JSON — as a single value. */ _%>
|
|
843
|
+
<%_ contentJsonInputs.forEach((k) => {
|
|
844
|
+
const params = k.source.kind === 'query' ? 'query_params' : k.source.kind === 'header' ? 'header_params' : 'cookie_params'; _%>
|
|
845
|
+
if <%- params %>[<%- pyStr(k.source.wireName) %>] is not None:
|
|
846
|
+
<%- params %>[<%- pyStr(k.source.wireName) %>] = json.dumps(
|
|
847
|
+
self._dump(<%- params %>[<%- pyStr(k.source.wireName) %>]), separators=(",", ":")
|
|
848
|
+
)
|
|
849
|
+
<%_ }); _%>
|
|
850
|
+
<%_ /* Query and header formats are kept apart: a query and a header parameter
|
|
851
|
+
may share a name while declaring different collection formats. */ _%>
|
|
852
|
+
<%_ if (queryCollectionInputs.length > 0) { _%>
|
|
853
|
+
query_formats: dict[str, str] | None = {<%_ queryCollectionInputs.forEach((k, i) => { _%><%- i > 0 ? ', ' : '' %><%- pyStr(k.source.wireName) %>: <%- pyStr(k.source.collectionFormat) %><%_ }); _%>}
|
|
854
|
+
<%_ } _%>
|
|
855
|
+
<%_ if (headerCollectionInputs.length > 0) { _%>
|
|
856
|
+
header_formats: dict[str, str] | None = {<%_ headerCollectionInputs.forEach((k, i) => { _%><%- i > 0 ? ', ' : '' %><%- pyStr(k.source.wireName) %>: <%- pyStr(k.source.collectionFormat) %><%_ }); _%>}
|
|
857
|
+
<%_ } _%>
|
|
858
|
+
<%_ if (shape.bodyFromFields) { _%>
|
|
859
|
+
_body_fields: dict[str, Any] = {}
|
|
860
|
+
<%_ bodyFieldInputs.forEach((k) => {
|
|
861
|
+
if (k.isRequired) { _%>
|
|
862
|
+
_body_fields[<%- pyStr(k.source.fieldName) %>] = <%- k.pythonName %>
|
|
863
|
+
<%_ } else { _%>
|
|
864
|
+
if <%- k.pythonName %> is not None:
|
|
865
|
+
_body_fields[<%- pyStr(k.source.fieldName) %>] = <%- k.pythonName %>
|
|
866
|
+
<%_ } }); _%>
|
|
867
|
+
<%_ if (shape.bodyFromFields.mediaType === 'multipart/form-data') { _%>
|
|
868
|
+
# httpx populates the Content-Type (and boundary) automatically when
|
|
869
|
+
# `files=` or `data=` is supplied; binary values stream as file parts.
|
|
870
|
+
_files = self._multipart(_body_fields<%_ if (partContentTypes) { _%>, {<%_ Object.entries(partContentTypes).forEach(([prop, ct], i) => { _%><%- i > 0 ? ', ' : '' %><%- pyStr(prop) %>: <%- pyStr(ct) %><%_ }); _%>}<%_ } _%>)
|
|
871
|
+
request_kwargs: dict[str, Any] = {"files": _files}
|
|
872
|
+
<%_ } else if (shape.bodyFromFields.mediaType === 'application/x-www-form-urlencoded') { _%>
|
|
873
|
+
# httpx URL-encodes `data=` as form key=value pairs (arrays as
|
|
874
|
+
# repeated keys); None fields were already omitted above.
|
|
875
|
+
request_kwargs: dict[str, Any] = {"data": {k: self._dump(v) for k, v in _body_fields.items()}}
|
|
876
|
+
<%_ } else { _%>
|
|
877
|
+
body = types.<%- shape.bodyFromFields.model.pythonClassName %>.model_validate(
|
|
878
|
+
_body_fields
|
|
879
|
+
).model_dump(mode="json", by_alias=True, exclude_unset=True)
|
|
880
|
+
request_kwargs: dict[str, Any] = {"json": body}
|
|
881
|
+
<%_ if (shape.bodyFromFields.mediaType) { _%>
|
|
882
|
+
if not self._config.omit_content_type_header:
|
|
883
|
+
self._default_content_type(header_params, "<%- shape.bodyFromFields.mediaType %>")
|
|
884
|
+
<%_ } _%>
|
|
885
|
+
<%_ } _%>
|
|
886
|
+
<%_ } else if (wholeBodyInputs.length > 0) { _%>
|
|
887
|
+
<%_ if (bodyIsBinary) { _%>
|
|
888
|
+
body = <%- wholeBodyInputs[0].pythonName %>
|
|
889
|
+
request_kwargs: dict[str, Any] = {"content": body}
|
|
890
|
+
<%_ if (bodyMediaType) { _%>
|
|
891
|
+
if body is not None and not self._config.omit_content_type_header:
|
|
892
|
+
self._default_content_type(header_params, "<%- bodyMediaType %>")
|
|
893
|
+
<%_ } _%>
|
|
894
|
+
<%_ } else if (bodyIsMultipart) { _%>
|
|
895
|
+
body = <%- wholeBodyInputs[0].pythonName %>
|
|
896
|
+
# httpx populates the Content-Type (and boundary) automatically
|
|
897
|
+
# when `files=` or `data=` is supplied.
|
|
898
|
+
if isinstance(body, BaseModel):
|
|
899
|
+
body = body.model_dump(mode="python", by_alias=True, exclude_unset=True)
|
|
900
|
+
if isinstance(body, dict):
|
|
901
|
+
request_kwargs: dict[str, Any] = {"files": self._multipart(body)}
|
|
902
|
+
else:
|
|
903
|
+
request_kwargs = {"files": body}
|
|
904
|
+
<%_ } else if (bodyIsUrlEncoded && !wholeBodyInputs[0].model.isPrimitive) { _%>
|
|
905
|
+
body = <%- wholeBodyInputs[0].pythonName %>
|
|
906
|
+
# httpx URL-encodes `data=` as form key=value pairs (arrays as
|
|
907
|
+
# repeated keys).
|
|
908
|
+
_form = self._dump(body)
|
|
909
|
+
request_kwargs: dict[str, Any] = {"data": {k: v for k, v in (_form or {}).items() if v is not None}}
|
|
910
|
+
<%_ } else if (bodyIsUrlEncoded) { _%>
|
|
911
|
+
# A primitive urlencoded body (e.g. a raw pre-encoded string) is sent
|
|
912
|
+
# verbatim; only object bodies are form-encoded.
|
|
913
|
+
body = <%- wholeBodyInputs[0].pythonName %>
|
|
914
|
+
request_kwargs: dict[str, Any] = {"content": None if body is None else str(body)}
|
|
915
|
+
if body is not None and not self._config.omit_content_type_header:
|
|
916
|
+
self._default_content_type(header_params, "<%- bodyMediaType %>")
|
|
917
|
+
<%_ } else if (bodyIsScalarNonJson) { _%>
|
|
918
|
+
# A scalar under a non-JSON media type is its own encoding, so it is
|
|
919
|
+
# sent as-is rather than JSON-encoded.
|
|
920
|
+
body = <%- wholeBodyInputs[0].pythonName %>
|
|
921
|
+
request_kwargs: dict[str, Any] = {"content": None if body is None else str(body)}
|
|
922
|
+
if body is not None and not self._config.omit_content_type_header:
|
|
923
|
+
self._default_content_type(header_params, "<%- bodyMediaType %>")
|
|
924
|
+
<%_ } else if (wholeBodyInputs[0].isRequired && wholeBodyInputs[0].isNullable) { _%>
|
|
925
|
+
# The body is required and may be null, so `None` is the JSON value
|
|
926
|
+
# `null` rather than an absent body — httpx omits `json=None` entirely,
|
|
927
|
+
# which a server cannot tell from no body at all.
|
|
928
|
+
body = self._dump(<%- wholeBodyInputs[0].pythonName %>)
|
|
929
|
+
request_kwargs: dict[str, Any] = (
|
|
930
|
+
{"content": b"null"} if body is None else {"json": body}
|
|
931
|
+
)
|
|
932
|
+
if not self._config.omit_content_type_header:
|
|
933
|
+
self._default_content_type(header_params, "<%- bodyMediaType || 'application/json' %>")
|
|
934
|
+
<%_ } else { _%>
|
|
935
|
+
body = self._dump(<%- wholeBodyInputs[0].pythonName %>)
|
|
936
|
+
request_kwargs: dict[str, Any] = {"json": body}
|
|
937
|
+
<%_ if (bodyMediaType) { _%>
|
|
938
|
+
if body is not None and not self._config.omit_content_type_header:
|
|
939
|
+
self._default_content_type(header_params, "<%- bodyMediaType %>")
|
|
940
|
+
<%_ } _%>
|
|
941
|
+
<%_ } _%>
|
|
942
|
+
<%_ } else { _%>
|
|
943
|
+
request_kwargs: dict[str, Any] = {}
|
|
944
|
+
<%_ } _%>
|
|
945
|
+
<%_ if (reservedInputs.length > 0) { _%>
|
|
946
|
+
<%_ /* allowReserved query parameters keep reserved characters literal, so the
|
|
947
|
+
query string is built manually and appended to the URL — httpx would
|
|
948
|
+
percent-encode values passed via `params=`. */ _%>
|
|
949
|
+
_qs = self._query_string(<%- queryInputs.length > 0 ? "query_params" : "{}" %>, <%- queryCollectionInputs.length > 0 ? "query_formats" : "None" %>, {<%_ reservedInputs.forEach((k, i) => { _%><%- i > 0 ? ', ' : '' %><%- pyStr(k.source.wireName) %><%_ }); _%>})
|
|
950
|
+
_request_url = <%- urlExpr(op, pathInputs, styledPathInputs) %> + ("?" + _qs if _qs else "")
|
|
951
|
+
<%_ } else { _%>
|
|
952
|
+
_request_url = <%- urlExpr(op, pathInputs, styledPathInputs) %>
|
|
953
|
+
<%_ } _%>
|
|
954
|
+
<%_ if (op.isStreaming) { _%>
|
|
955
|
+
<%_ if (reservedInputs.length === 0) { _%>
|
|
956
|
+
<%- kw.with %> self._client.stream(
|
|
957
|
+
"<%- op.method %>",
|
|
958
|
+
_request_url,
|
|
959
|
+
<%_ if (queryInputs.length > 0) { _%>
|
|
960
|
+
params=self._query(query_params<%- queryCollectionInputs.length > 0 ? ", query_formats" : "" %>),
|
|
961
|
+
<%_ } _%>
|
|
962
|
+
<%_ } else { _%>
|
|
963
|
+
<%- kw.with %> self._stream_with_url(
|
|
964
|
+
_request_url,
|
|
965
|
+
method="<%- op.method %>",
|
|
966
|
+
url=_request_url,
|
|
967
|
+
<%_ } _%>
|
|
968
|
+
headers=self._headers(<%- headerArgs %>),
|
|
969
|
+
**request_kwargs,
|
|
970
|
+
) as response:
|
|
971
|
+
<%_ op.responses.forEach((resp) => {
|
|
972
|
+
const isResult = isSuccessResponse(op, resp);
|
|
973
|
+
const cond = branchCondition(op, resp);
|
|
974
|
+
_%>
|
|
975
|
+
<%_ /* A branch body is indented one level under its status check; an
|
|
976
|
+
unconditional branch sits at the enclosing level instead. */ _%>
|
|
977
|
+
<%_ const ind = cond ? ' ' : ' '; _%>
|
|
978
|
+
<%_ if (cond) { _%>
|
|
979
|
+
if <%- cond %>:
|
|
980
|
+
<%_ } _%>
|
|
981
|
+
<%_ if (isResult && resp.isJsonlStreaming) { _%>
|
|
982
|
+
buffer = ""
|
|
983
|
+
<%- kw.for %> chunk in response.<%- hx.iterText %>():
|
|
984
|
+
buffer += chunk
|
|
985
|
+
while "\n" in buffer:
|
|
986
|
+
line, buffer = buffer.split("\n", 1)
|
|
987
|
+
# `iter_text` doesn't translate newlines, so a CRLF
|
|
988
|
+
# stream leaves a trailing carriage return on each line.
|
|
989
|
+
line = line.rstrip("\r")
|
|
990
|
+
if line.strip():
|
|
991
|
+
yield <%- streamItemParseExpr(resp.itemSchemaModel, "line") %>
|
|
992
|
+
if buffer.strip():
|
|
993
|
+
yield <%- streamItemParseExpr(resp.itemSchemaModel, "buffer") %>
|
|
994
|
+
return
|
|
995
|
+
<%_ } else if (isResult) { _%>
|
|
996
|
+
<%- kw.for %> line in response.<%- hx.iterLines %>():
|
|
997
|
+
if line:
|
|
998
|
+
yield <%- streamChunkParseExpr(resp, "line") %>
|
|
999
|
+
return
|
|
1000
|
+
<%_ } else { _%>
|
|
1001
|
+
<%- ind %><%- kw.await %>response.<%- hx.read %>()
|
|
1002
|
+
<%- ind %><%- renderTypedRaise(op, resp) %>
|
|
1003
|
+
<%_ } _%>
|
|
1004
|
+
<%_ }); _%>
|
|
1005
|
+
<%_ if (!hasUnconditionalBranch(op)) { _%>
|
|
1006
|
+
<%- kw.await %>response.<%- hx.read %>()
|
|
1007
|
+
raise <%- op.errorShape.exceptionClassName %>(response.status_code, None)
|
|
1008
|
+
<%_ } _%>
|
|
1009
|
+
<%_ } else { _%>
|
|
1010
|
+
<%_ if (reservedInputs.length === 0) { _%>
|
|
1011
|
+
response = <%- kw.await %>self._client.request(
|
|
1012
|
+
"<%- op.method %>",
|
|
1013
|
+
_request_url,
|
|
1014
|
+
<%_ if (queryInputs.length > 0) { _%>
|
|
1015
|
+
params=self._query(query_params<%- queryCollectionInputs.length > 0 ? ", query_formats" : "" %>),
|
|
1016
|
+
<%_ } _%>
|
|
1017
|
+
<%_ } else { _%>
|
|
1018
|
+
response = <%- kw.await %>self._send_with_url(
|
|
1019
|
+
_request_url,
|
|
1020
|
+
method="<%- op.method %>",
|
|
1021
|
+
url=_request_url,
|
|
1022
|
+
<%_ } _%>
|
|
1023
|
+
headers=self._headers(<%- headerArgs %>),
|
|
1024
|
+
**request_kwargs,
|
|
1025
|
+
)
|
|
1026
|
+
<%_ op.responses.forEach((resp) => {
|
|
1027
|
+
const isSuccess = isSuccessResponse(op, resp);
|
|
1028
|
+
const cond = branchCondition(op, resp);
|
|
1029
|
+
_%>
|
|
1030
|
+
<%_ const ind = cond ? ' ' : ' '; _%>
|
|
1031
|
+
<%_ if (cond) { _%>
|
|
1032
|
+
if <%- cond %>:
|
|
1033
|
+
<%_ } _%>
|
|
1034
|
+
<%- ind %><%- isSuccess ? `return ${responseParseExpr(resp)}` : renderTypedRaise(op, resp) %>
|
|
1035
|
+
<%_ }); _%>
|
|
1036
|
+
<%_ if (!hasUnconditionalBranch(op)) { _%>
|
|
1037
|
+
raise <%- op.errorShape.exceptionClassName %>(response.status_code, None)
|
|
1038
|
+
<%_ } _%>
|
|
1039
|
+
<%_ } _%>
|
|
1040
|
+
<%_ }); _%>
|
|
1041
|
+
<%_ // ────── Tag namespaces ────── _%>
|
|
1042
|
+
<%_ Object.entries(tagGroups).forEach(([tag, ops]) => { _%>
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
class <%- pythonClassNameForTag(tag) %>:
|
|
1046
|
+
"""`<%- tag %>` operations."""
|
|
1047
|
+
|
|
1048
|
+
def __init__(self, parent: <%- clientClassName %>) -> None:
|
|
1049
|
+
self._parent = parent
|
|
1050
|
+
<%_ ops.forEach((op) => {
|
|
1051
|
+
const inputs = op.requestShape?.inputs ?? [];
|
|
1052
|
+
// A streaming delegate is a plain `def` handing back the inner generator, so
|
|
1053
|
+
// closing it reaches that generator: `yield from` forwards `close()`, and
|
|
1054
|
+
// returning the async generator forwards `aclose()`. An `async def` that
|
|
1055
|
+
// re-yielded would wrap it in a second generator `aclose()` never reaches,
|
|
1056
|
+
// holding the connection open until garbage collection.
|
|
1057
|
+
const delegateDef = isAsync && !op.isStreaming ? 'async def' : 'def';
|
|
1058
|
+
const delegate = op.isStreaming
|
|
1059
|
+
? (isAsync ? 'return ' : 'yield from ')
|
|
1060
|
+
: `return ${kw.await}`;
|
|
1061
|
+
_%>
|
|
1062
|
+
|
|
1063
|
+
<%- signature(op).replace(/^self/, `${delegateDef} ${op.pythonMethodName}(\n self`) %>,
|
|
1064
|
+
) -> <%- resultTypeFor(op) %>:
|
|
1065
|
+
<%- methodDocstring(op, ' ') _%>
|
|
1066
|
+
<%- delegate %>self._parent._<%- op.pythonMethodName %>(
|
|
1067
|
+
<%_ if (isPositionalBody(op)) { _%>
|
|
1068
|
+
<%- inputs[0].pythonName %>,
|
|
1069
|
+
<%_ } else { _%>
|
|
1070
|
+
<%_ inputs.forEach((k) => { _%>
|
|
1071
|
+
<%- k.pythonName %>=<%- k.pythonName %>,
|
|
1072
|
+
<%_ }); _%>
|
|
1073
|
+
<%_ } _%>
|
|
1074
|
+
)
|
|
1075
|
+
<%_ }); _%>
|
|
1076
|
+
<%_ }); _%>
|