@aws/nx-plugin 1.0.0 → 1.0.2

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.
Files changed (124) hide show
  1. package/LICENSE-THIRD-PARTY +11686 -23398
  2. package/README.md +18 -0
  3. package/generators.json +7 -0
  4. package/migrations.json +39 -1
  5. package/package.json +6 -6
  6. package/src/agentcore-gateway/agent-connection/generator.js +6 -0
  7. package/src/agentcore-gateway/agent-connection/generator.js.map +1 -1
  8. package/src/agentcore-gateway/schema.json +1 -0
  9. package/src/agentcore-harness/generator.d.ts +1 -1
  10. package/src/connection/scaffold-catalog.d.ts +21 -0
  11. package/src/connection/scaffold-catalog.js +14 -6
  12. package/src/connection/scaffold-catalog.js.map +1 -1
  13. package/src/infra/app/generator.d.ts +1 -1
  14. package/src/init/generator.d.ts +1 -1
  15. package/src/mcp-server/guide-pipeline.d.ts +2 -1
  16. package/src/mcp-server/guide-pipeline.js +6 -0
  17. package/src/mcp-server/guide-pipeline.js.map +1 -1
  18. package/src/mcp-server/mdx-ast.d.ts +7 -0
  19. package/src/mcp-server/mdx-ast.js +15 -0
  20. package/src/mcp-server/mdx-ast.js.map +1 -1
  21. package/src/mcp-server/schema-registry.d.ts +2 -2
  22. package/src/mcp-server/schema-registry.js +2 -2
  23. package/src/mcp-server/schema-registry.js.map +1 -1
  24. package/src/migrations/latest/terraform-harness-environment-variables/metadata.json +3 -0
  25. package/src/migrations/latest/terraform-harness-environment-variables/migration.d.ts +6 -0
  26. package/src/migrations/latest/terraform-harness-environment-variables/migration.js +54 -0
  27. package/src/migrations/latest/terraform-harness-environment-variables/migration.js.map +1 -0
  28. package/src/open-api/py-client/__snapshots__/generator.additional-properties.spec.ts.snap +34 -0
  29. package/src/open-api/py-client/__snapshots__/generator.arrays.spec.ts.snap +1750 -0
  30. package/src/open-api/py-client/__snapshots__/generator.complex-types.spec.ts.snap +82 -0
  31. package/src/open-api/py-client/__snapshots__/generator.composite-types.spec.ts.snap +447 -0
  32. package/src/open-api/py-client/__snapshots__/generator.duplicate-types.spec.ts.snap +404 -0
  33. package/src/open-api/py-client/__snapshots__/generator.errors.spec.ts.snap +430 -0
  34. package/src/open-api/py-client/__snapshots__/generator.fast-api.spec.ts.snap +447 -0
  35. package/src/open-api/py-client/__snapshots__/generator.petstore.spec.ts.snap +3488 -0
  36. package/src/open-api/py-client/__snapshots__/generator.primitive-types.spec.ts.snap +1444 -0
  37. package/src/open-api/py-client/__snapshots__/generator.request.spec.ts.snap +390 -0
  38. package/src/open-api/py-client/__snapshots__/generator.reserved-keywords.spec.ts.snap +364 -0
  39. package/src/open-api/py-client/__snapshots__/generator.response.spec.ts.snap +739 -0
  40. package/src/open-api/py-client/__snapshots__/generator.streaming.spec.ts.snap +384 -0
  41. package/src/open-api/py-client/__snapshots__/generator.tags.spec.ts.snap +414 -0
  42. package/src/open-api/py-client/files/client/__clientModuleName__.py.template +1076 -0
  43. package/src/open-api/py-client/files/shared/__init__.py.template +28 -0
  44. package/src/open-api/py-client/files/shared/errors.py.template +37 -0
  45. package/src/open-api/py-client/files/shared/types.py.template +319 -0
  46. package/src/open-api/py-client/generator.d.ts +53 -0
  47. package/src/open-api/py-client/generator.js +119 -0
  48. package/src/open-api/py-client/generator.js.map +1 -0
  49. package/src/open-api/py-client/schema.d.js +6 -0
  50. package/src/open-api/py-client/schema.d.js.map +1 -0
  51. package/src/open-api/py-client/schema.d.ts +12 -0
  52. package/src/open-api/py-client/schema.json +28 -0
  53. package/src/open-api/py-client/vessel-registry-spec.json +2716 -0
  54. package/src/open-api/ts-client/__snapshots__/generator.edge-cases.spec.ts.snap +274 -0
  55. package/src/open-api/ts-client/petstore-spec.d.ts +6 -0
  56. package/src/open-api/ts-client/petstore-spec.js +1275 -0
  57. package/src/open-api/ts-client/petstore-spec.js.map +1 -0
  58. package/src/open-api/ts-hooks/generator.spec.tsx +1 -1
  59. package/src/open-api/utils/codegen-data/languages.d.ts +68 -2
  60. package/src/open-api/utils/codegen-data/languages.js +433 -21
  61. package/src/open-api/utils/codegen-data/languages.js.map +1 -1
  62. package/src/open-api/utils/codegen-data/types.d.ts +187 -0
  63. package/src/open-api/utils/codegen-data/types.js +10 -1
  64. package/src/open-api/utils/codegen-data/types.js.map +1 -1
  65. package/src/open-api/utils/codegen-data.d.ts +40 -1
  66. package/src/open-api/utils/codegen-data.js +622 -17
  67. package/src/open-api/utils/codegen-data.js.map +1 -1
  68. package/src/open-api/utils/normalise.js +175 -41
  69. package/src/open-api/utils/normalise.js.map +1 -1
  70. package/src/open-api/utils/parser.js +1 -1
  71. package/src/open-api/utils/parser.js.map +1 -1
  72. package/src/preset/__snapshots__/generator.spec.ts.snap +3 -3
  73. package/src/preset/generator.d.ts +1 -1
  74. package/src/py/agent/gateway-connection/generator.js +0 -3
  75. package/src/py/agent/gateway-connection/generator.js.map +1 -1
  76. package/src/py/agent/react-connection/generator.d.ts +1 -1
  77. package/src/py/agent/schema.json +2 -0
  78. package/src/py/fast-api/react/generator.d.ts +1 -1
  79. package/src/py/mcp-server/schema.json +1 -0
  80. package/src/sdk/open-api.d.ts +2 -0
  81. package/src/sdk/open-api.js +1 -0
  82. package/src/sdk/open-api.js.map +1 -1
  83. package/src/smithy/project/generator.d.ts +1 -1
  84. package/src/smithy/react-connection/generator.d.ts +1 -1
  85. package/src/terraform/project/generator.d.ts +1 -1
  86. package/src/trpc/react/generator.d.ts +1 -1
  87. package/src/ts/agent/a2a-connection/generator.d.ts +1 -1
  88. package/src/ts/agent/gateway-connection/generator.d.ts +1 -1
  89. package/src/ts/agent/gateway-connection/generator.js +0 -3
  90. package/src/ts/agent/gateway-connection/generator.js.map +1 -1
  91. package/src/ts/agent/mcp-connection/generator.d.ts +1 -1
  92. package/src/ts/agent/react-connection/generator.d.ts +1 -1
  93. package/src/ts/agent/schema.json +1 -0
  94. package/src/ts/api/schema.json +2 -0
  95. package/src/ts/astro-docs/generator.d.ts +1 -1
  96. package/src/ts/dcr-proxy/generator.d.ts +1 -1
  97. package/src/ts/dynamodb/generator.d.ts +1 -1
  98. package/src/ts/lambda-function/generator.d.ts +1 -1
  99. package/src/ts/lib/generator.d.ts +1 -1
  100. package/src/ts/mcp-server/schema.json +1 -0
  101. package/src/ts/nx-generator/__snapshots__/generator.spec.ts.snap +1 -1
  102. package/src/ts/nx-generator/files/nx-plugin-for-aws/generator/generator.spec.ts.template +1 -1
  103. package/src/ts/nx-generator/generator.d.ts +1 -1
  104. package/src/ts/nx-migration/generator.d.ts +1 -1
  105. package/src/ts/nx-plugin/generator.d.ts +1 -1
  106. package/src/ts/rdb/generator.d.ts +1 -1
  107. package/src/ts/react-website/agui/generator.d.ts +1 -1
  108. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +10 -10
  109. package/src/ts/react-website/app/schema.json +1 -0
  110. package/src/ts/website/app/schema.json +1 -0
  111. package/src/utils/agent-core-constructs/files/terraform/app/agentcore-harness/__nameKebabCase__/__nameKebabCase__.tf.template +1 -1
  112. package/src/utils/metrics-assertions.d.ts +18 -0
  113. package/src/utils/metrics-assertions.js +42 -0
  114. package/src/utils/metrics-assertions.js.map +1 -0
  115. package/src/utils/test/python-dependencies.d.ts +8 -0
  116. package/src/utils/test/python-dependencies.js +14 -0
  117. package/src/utils/test/python-dependencies.js.map +1 -0
  118. package/src/utils/test/python-worker/worker.py +703 -0
  119. package/src/utils/test/warm-python-cache.d.ts +18 -0
  120. package/src/utils/test/warm-python-cache.js +46 -0
  121. package/src/utils/test/warm-python-cache.js.map +1 -0
  122. package/src/utils/versions.d.ts +10 -9
  123. package/src/utils/versions.js +9 -8
  124. 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
+ <%_ }); _%>