@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,1750 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`openApiPyClientGenerator - arrays > should handle dictionary (additionalProperties) responses > client.py 1`] = `
4
+ """"TestApi — AUTO-GENERATED synchronous client."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import datetime
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, TypeVar
11
+ from urllib.parse import quote
12
+
13
+ import httpx
14
+ from pydantic import BaseModel, TypeAdapter, ValidationError
15
+
16
+ from . import types
17
+ from .errors import (
18
+ ApiError as ApiError,
19
+ )
20
+ from .errors import (
21
+ GetCountsApiError as GetCountsApiError,
22
+ )
23
+
24
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
25
+
26
+
27
+ @dataclass
28
+ class TestApiConfig:
29
+ """Client configuration for TestApi."""
30
+
31
+ #: Base URL for the API.
32
+ url: str
33
+ #: Client used for every request. Supply your own to add authentication or
34
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
35
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
36
+ #: client's own headers and query params are sent alongside each operation's.
37
+ #: A supplied client is never closed by this one, so it can be shared.
38
+ httpx_client: httpx.Client | None = None
39
+ #: Headers added to every request, where the operation sets none itself.
40
+ headers: dict[str, str] = field(default_factory=dict)
41
+ #: By default the client sends a Content-Type header set to the media type
42
+ #: the OpenAPI specification declares for the request. Set this to omit it.
43
+ omit_content_type_header: bool = False
44
+
45
+
46
+ class TestApi:
47
+ """Synchronous API client for TestApi."""
48
+
49
+ def __init__(self, config: TestApiConfig) -> None:
50
+ self._config = config
51
+ self._client = config.httpx_client or httpx.Client()
52
+ self._owns_client = config.httpx_client is None
53
+ self._base_url = config.url.rstrip("/")
54
+
55
+ def __enter__(self) -> TestApi:
56
+ return self
57
+
58
+ def __exit__(self, *exc: Any) -> None:
59
+ self.close()
60
+
61
+ def close(self) -> None:
62
+ if self._owns_client:
63
+ self._client.close()
64
+
65
+ def _url(
66
+ self,
67
+ path: str,
68
+ path_params: dict[str, Any],
69
+ path_styles: dict[str, tuple[str, bool]] | None = None,
70
+ ) -> str:
71
+ for key, value in path_params.items():
72
+ style, explode = (path_styles or {}).get(key, ("simple", False))
73
+ # An object expands to its key,value members; a list to its items.
74
+ # Each is encoded on its own so the separators between them stay
75
+ # literal, as RFC 6570 requires.
76
+ dumped = self._dump(value)
77
+ if isinstance(dumped, dict):
78
+ values = [
79
+ part
80
+ for k, v in dumped.items()
81
+ if v is not None
82
+ for part in (k, self._scalar(v))
83
+ ]
84
+ elif isinstance(dumped, list):
85
+ values = list(dumped)
86
+ else:
87
+ values = [value]
88
+ if style == "matrix":
89
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
90
+ if explode:
91
+ rendered = "".join(
92
+ ";" + key + "=" + quote(self._scalar(v), safe="")
93
+ for v in values
94
+ )
95
+ else:
96
+ rendered = (
97
+ ";"
98
+ + key
99
+ + "="
100
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
101
+ )
102
+ elif style == "label":
103
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
104
+ rendered = "." + ("." if explode else ",").join(
105
+ quote(self._scalar(v), safe="") for v in values
106
+ )
107
+ else:
108
+ # RFC 6570 simple expansion: a list renders comma-separated.
109
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
110
+ path = path.replace("{" + key + "}", rendered)
111
+ return self._base_url + path
112
+
113
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
114
+ if isinstance(value, BaseModel):
115
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
116
+ if isinstance(value, dict):
117
+ for prop, v in value.items():
118
+ self._deep_object(f"{key}[{prop}]", v, out)
119
+ elif value is not None:
120
+ out[key] = value
121
+
122
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
123
+
124
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
125
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
126
+ #: too — only the first one separates a cookie's name from its value.
127
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
128
+
129
+ def _scalar(self, value: Any) -> str:
130
+ # The wire form of a single parameter value: JSON spellings for bools,
131
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
132
+ if isinstance(value, bool):
133
+ return "true" if value else "false"
134
+ if isinstance(value, (datetime.date, datetime.datetime)):
135
+ return value.isoformat()
136
+ dumped = self._dump(value)
137
+ if isinstance(dumped, str):
138
+ return dumped
139
+ # RFC 6570 renders a list as its comma-joined members and an object as
140
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
141
+ # \`['a', 'b']\`, quotes and spaces included.
142
+ if isinstance(dumped, list):
143
+ return ",".join(self._scalar(v) for v in dumped)
144
+ if isinstance(dumped, dict):
145
+ return ",".join(
146
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
147
+ )
148
+ if dumped is None:
149
+ return ""
150
+ return str(dumped)
151
+
152
+ def _query(
153
+ self,
154
+ query_params: dict[str, Any],
155
+ collection_formats: dict[str, str] | None = None,
156
+ ) -> dict[str, Any]:
157
+ out: dict[str, Any] = {}
158
+ for key, value in query_params.items():
159
+ if value is None:
160
+ continue
161
+ fmt = (collection_formats or {}).get(key)
162
+ if fmt == "deepObject":
163
+ self._deep_object(key, value, out)
164
+ elif isinstance(value, list):
165
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
166
+ if delimiter is None:
167
+ out[key] = [self._scalar(v) for v in value]
168
+ else:
169
+ out[key] = delimiter.join(self._scalar(v) for v in value)
170
+ elif self._is_object(value):
171
+ # OpenAPI's default query style is form/explode, under which an
172
+ # object expands to one parameter per property rather than
173
+ # being stringified.
174
+ for prop, prop_value in self._as_dict(value).items():
175
+ if prop_value is not None:
176
+ out[prop] = self._scalar(prop_value)
177
+ else:
178
+ out[key] = self._scalar(value)
179
+ return out
180
+
181
+ def _is_object(self, value: Any) -> bool:
182
+ return isinstance(value, (dict, BaseModel))
183
+
184
+ def _as_dict(self, value: Any) -> dict[str, Any]:
185
+ if isinstance(value, BaseModel):
186
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
187
+ return dict(value)
188
+
189
+ def _headers(
190
+ self,
191
+ header_params: dict[str, Any] | None = None,
192
+ collection_formats: dict[str, str] | None = None,
193
+ cookie_params: dict[str, Any] | None = None,
194
+ ) -> list[tuple[str, str]]:
195
+ out: list[tuple[str, str]] = []
196
+ for key, value in (header_params or {}).items():
197
+ if value is None:
198
+ continue
199
+ if isinstance(value, list):
200
+ fmt = (collection_formats or {}).get(key, "csv")
201
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
202
+ if delimiter is None:
203
+ for item in value:
204
+ out.append((key, self._scalar(item)))
205
+ else:
206
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
207
+ else:
208
+ out.append((key, self._scalar(value)))
209
+ # A config header only applies where the operation didn't set one:
210
+ # httpx comma-joins repeated names, which would corrupt the value.
211
+ operation_headers = {key.lower() for key, _ in out}
212
+ out.extend(
213
+ (key, value)
214
+ for key, value in self._config.headers.items()
215
+ if key.lower() not in operation_headers
216
+ )
217
+ # The operation's cookies are merged into one \`Cookie\` header alongside
218
+ # every other source. Passing them to httpx's \`cookies=\` instead would
219
+ # drop them outright whenever a \`Cookie\` header is present, since
220
+ # http.cookiejar skips a request that already carries one — and setting
221
+ # the header here would in turn shadow the caller's own jar and header,
222
+ # which httpx resolves in favour of the more specific value.
223
+ cookies = self._cookies(cookie_params or {})
224
+ if cookies:
225
+ pairs = [
226
+ *(
227
+ value
228
+ for source in (self._client.headers, dict(out))
229
+ for key, value in source.items()
230
+ if key.lower() == "cookie"
231
+ ),
232
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
233
+ *(f"{key}={value}" for key, value in cookies.items()),
234
+ ]
235
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
236
+ out.append(("Cookie", "; ".join(pairs)))
237
+ return out
238
+
239
+ def _default_content_type(
240
+ self, header_params: dict[str, Any], media_type: str
241
+ ) -> None:
242
+ # The media type the specification declares, applied unless the operation
243
+ # takes a Content-Type of its own and the caller supplied one. Header
244
+ # names are case-insensitive, and a declared header the caller omitted is
245
+ # present holding \`None\` — neither counts as the caller setting it.
246
+ if any(
247
+ key.lower() == "content-type" and value is not None
248
+ for key, value in header_params.items()
249
+ ):
250
+ return
251
+ for key in list(header_params):
252
+ if key.lower() == "content-type":
253
+ header_params[key] = media_type
254
+ return
255
+ header_params["Content-Type"] = media_type
256
+
257
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
258
+ out: dict[str, str] = {}
259
+ for key, value in cookie_params.items():
260
+ if value is None:
261
+ continue
262
+ dumped = self._dump(value)
263
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
264
+ # object on \`,\` between each key and value. Members are encoded so a
265
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
266
+ # joining commas stay literal.
267
+ if isinstance(dumped, list):
268
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
269
+ elif isinstance(dumped, dict):
270
+ rendered = ",".join(
271
+ self._cookie_value(k) + "," + self._cookie_value(v)
272
+ for k, v in dumped.items()
273
+ if v is not None
274
+ )
275
+ else:
276
+ rendered = self._cookie_value(value)
277
+ out[key] = rendered
278
+ return out
279
+
280
+ def _cookie_value(self, value: Any) -> str:
281
+ # Only the characters a cookie parser treats as structure are escaped, so
282
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
283
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
284
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
285
+
286
+ def _dump(self, value: Any) -> Any:
287
+ if value is None:
288
+ return None
289
+ if isinstance(value, BaseModel):
290
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
291
+ if isinstance(value, (list, tuple)):
292
+ return [self._dump(v) for v in value]
293
+ if isinstance(value, dict):
294
+ return {k: self._dump(v) for k, v in value.items()}
295
+ if isinstance(value, (datetime.date, datetime.datetime)):
296
+ return value.isoformat()
297
+ return value
298
+
299
+ def _error_payload(self, response: httpx.Response) -> Any:
300
+ # An error body is whatever the server actually sent, which need not be
301
+ # the JSON the spec declares (a proxy's HTML page, say).
302
+ try:
303
+ return response.json()
304
+ except ValueError:
305
+ return response.text
306
+
307
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
308
+ # Coerce to the declared error type, falling back to the raw payload so
309
+ # a body that doesn't match the spec still raises the typed exception.
310
+ try:
311
+ return wrapper.model_validate({"status": status, "error": payload})
312
+ except ValidationError:
313
+ return wrapper.model_construct(status=status, error=payload)
314
+
315
+ def get_counts(
316
+ self,
317
+ ) -> types.GetCounts200Response:
318
+ request_kwargs: dict[str, Any] = {}
319
+ _request_url = self._url("/counts", {})
320
+ response = self._client.request(
321
+ "GET",
322
+ _request_url,
323
+ headers=self._headers(),
324
+ **request_kwargs,
325
+ )
326
+ if response.status_code == 200:
327
+ return TypeAdapter(types.GetCounts200Response).validate_python(
328
+ response.json()
329
+ )
330
+ raise GetCountsApiError(response.status_code, None)
331
+ "
332
+ `;
333
+
334
+ exports[`openApiPyClientGenerator - arrays > should handle dictionary (additionalProperties) responses > types.py 1`] = `
335
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
336
+
337
+ from __future__ import annotations
338
+
339
+ from typing import Never
340
+
341
+ GetCounts200Response = dict[str, int]
342
+
343
+ GetCountsError = Never
344
+ "
345
+ `;
346
+
347
+ exports[`openApiPyClientGenerator - arrays > should handle operation which accepts an array of objects > client.py 1`] = `
348
+ """"TestApi — AUTO-GENERATED synchronous client."""
349
+
350
+ from __future__ import annotations
351
+
352
+ import datetime
353
+ from dataclasses import dataclass, field
354
+ from typing import Any, TypeVar
355
+ from urllib.parse import quote
356
+
357
+ import httpx
358
+ from pydantic import BaseModel, TypeAdapter, ValidationError
359
+
360
+ from . import types
361
+ from .errors import (
362
+ ApiError as ApiError,
363
+ )
364
+ from .errors import (
365
+ CreateUsersApiError as CreateUsersApiError,
366
+ )
367
+
368
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
369
+
370
+
371
+ @dataclass
372
+ class TestApiConfig:
373
+ """Client configuration for TestApi."""
374
+
375
+ #: Base URL for the API.
376
+ url: str
377
+ #: Client used for every request. Supply your own to add authentication or
378
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
379
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
380
+ #: client's own headers and query params are sent alongside each operation's.
381
+ #: A supplied client is never closed by this one, so it can be shared.
382
+ httpx_client: httpx.Client | None = None
383
+ #: Headers added to every request, where the operation sets none itself.
384
+ headers: dict[str, str] = field(default_factory=dict)
385
+ #: By default the client sends a Content-Type header set to the media type
386
+ #: the OpenAPI specification declares for the request. Set this to omit it.
387
+ omit_content_type_header: bool = False
388
+
389
+
390
+ class TestApi:
391
+ """Synchronous API client for TestApi."""
392
+
393
+ def __init__(self, config: TestApiConfig) -> None:
394
+ self._config = config
395
+ self._client = config.httpx_client or httpx.Client()
396
+ self._owns_client = config.httpx_client is None
397
+ self._base_url = config.url.rstrip("/")
398
+
399
+ def __enter__(self) -> TestApi:
400
+ return self
401
+
402
+ def __exit__(self, *exc: Any) -> None:
403
+ self.close()
404
+
405
+ def close(self) -> None:
406
+ if self._owns_client:
407
+ self._client.close()
408
+
409
+ def _url(
410
+ self,
411
+ path: str,
412
+ path_params: dict[str, Any],
413
+ path_styles: dict[str, tuple[str, bool]] | None = None,
414
+ ) -> str:
415
+ for key, value in path_params.items():
416
+ style, explode = (path_styles or {}).get(key, ("simple", False))
417
+ # An object expands to its key,value members; a list to its items.
418
+ # Each is encoded on its own so the separators between them stay
419
+ # literal, as RFC 6570 requires.
420
+ dumped = self._dump(value)
421
+ if isinstance(dumped, dict):
422
+ values = [
423
+ part
424
+ for k, v in dumped.items()
425
+ if v is not None
426
+ for part in (k, self._scalar(v))
427
+ ]
428
+ elif isinstance(dumped, list):
429
+ values = list(dumped)
430
+ else:
431
+ values = [value]
432
+ if style == "matrix":
433
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
434
+ if explode:
435
+ rendered = "".join(
436
+ ";" + key + "=" + quote(self._scalar(v), safe="")
437
+ for v in values
438
+ )
439
+ else:
440
+ rendered = (
441
+ ";"
442
+ + key
443
+ + "="
444
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
445
+ )
446
+ elif style == "label":
447
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
448
+ rendered = "." + ("." if explode else ",").join(
449
+ quote(self._scalar(v), safe="") for v in values
450
+ )
451
+ else:
452
+ # RFC 6570 simple expansion: a list renders comma-separated.
453
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
454
+ path = path.replace("{" + key + "}", rendered)
455
+ return self._base_url + path
456
+
457
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
458
+ if isinstance(value, BaseModel):
459
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
460
+ if isinstance(value, dict):
461
+ for prop, v in value.items():
462
+ self._deep_object(f"{key}[{prop}]", v, out)
463
+ elif value is not None:
464
+ out[key] = value
465
+
466
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
467
+
468
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
469
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
470
+ #: too — only the first one separates a cookie's name from its value.
471
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
472
+
473
+ def _scalar(self, value: Any) -> str:
474
+ # The wire form of a single parameter value: JSON spellings for bools,
475
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
476
+ if isinstance(value, bool):
477
+ return "true" if value else "false"
478
+ if isinstance(value, (datetime.date, datetime.datetime)):
479
+ return value.isoformat()
480
+ dumped = self._dump(value)
481
+ if isinstance(dumped, str):
482
+ return dumped
483
+ # RFC 6570 renders a list as its comma-joined members and an object as
484
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
485
+ # \`['a', 'b']\`, quotes and spaces included.
486
+ if isinstance(dumped, list):
487
+ return ",".join(self._scalar(v) for v in dumped)
488
+ if isinstance(dumped, dict):
489
+ return ",".join(
490
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
491
+ )
492
+ if dumped is None:
493
+ return ""
494
+ return str(dumped)
495
+
496
+ def _query(
497
+ self,
498
+ query_params: dict[str, Any],
499
+ collection_formats: dict[str, str] | None = None,
500
+ ) -> dict[str, Any]:
501
+ out: dict[str, Any] = {}
502
+ for key, value in query_params.items():
503
+ if value is None:
504
+ continue
505
+ fmt = (collection_formats or {}).get(key)
506
+ if fmt == "deepObject":
507
+ self._deep_object(key, value, out)
508
+ elif isinstance(value, list):
509
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
510
+ if delimiter is None:
511
+ out[key] = [self._scalar(v) for v in value]
512
+ else:
513
+ out[key] = delimiter.join(self._scalar(v) for v in value)
514
+ elif self._is_object(value):
515
+ # OpenAPI's default query style is form/explode, under which an
516
+ # object expands to one parameter per property rather than
517
+ # being stringified.
518
+ for prop, prop_value in self._as_dict(value).items():
519
+ if prop_value is not None:
520
+ out[prop] = self._scalar(prop_value)
521
+ else:
522
+ out[key] = self._scalar(value)
523
+ return out
524
+
525
+ def _is_object(self, value: Any) -> bool:
526
+ return isinstance(value, (dict, BaseModel))
527
+
528
+ def _as_dict(self, value: Any) -> dict[str, Any]:
529
+ if isinstance(value, BaseModel):
530
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
531
+ return dict(value)
532
+
533
+ def _headers(
534
+ self,
535
+ header_params: dict[str, Any] | None = None,
536
+ collection_formats: dict[str, str] | None = None,
537
+ cookie_params: dict[str, Any] | None = None,
538
+ ) -> list[tuple[str, str]]:
539
+ out: list[tuple[str, str]] = []
540
+ for key, value in (header_params or {}).items():
541
+ if value is None:
542
+ continue
543
+ if isinstance(value, list):
544
+ fmt = (collection_formats or {}).get(key, "csv")
545
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
546
+ if delimiter is None:
547
+ for item in value:
548
+ out.append((key, self._scalar(item)))
549
+ else:
550
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
551
+ else:
552
+ out.append((key, self._scalar(value)))
553
+ # A config header only applies where the operation didn't set one:
554
+ # httpx comma-joins repeated names, which would corrupt the value.
555
+ operation_headers = {key.lower() for key, _ in out}
556
+ out.extend(
557
+ (key, value)
558
+ for key, value in self._config.headers.items()
559
+ if key.lower() not in operation_headers
560
+ )
561
+ # The operation's cookies are merged into one \`Cookie\` header alongside
562
+ # every other source. Passing them to httpx's \`cookies=\` instead would
563
+ # drop them outright whenever a \`Cookie\` header is present, since
564
+ # http.cookiejar skips a request that already carries one — and setting
565
+ # the header here would in turn shadow the caller's own jar and header,
566
+ # which httpx resolves in favour of the more specific value.
567
+ cookies = self._cookies(cookie_params or {})
568
+ if cookies:
569
+ pairs = [
570
+ *(
571
+ value
572
+ for source in (self._client.headers, dict(out))
573
+ for key, value in source.items()
574
+ if key.lower() == "cookie"
575
+ ),
576
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
577
+ *(f"{key}={value}" for key, value in cookies.items()),
578
+ ]
579
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
580
+ out.append(("Cookie", "; ".join(pairs)))
581
+ return out
582
+
583
+ def _default_content_type(
584
+ self, header_params: dict[str, Any], media_type: str
585
+ ) -> None:
586
+ # The media type the specification declares, applied unless the operation
587
+ # takes a Content-Type of its own and the caller supplied one. Header
588
+ # names are case-insensitive, and a declared header the caller omitted is
589
+ # present holding \`None\` — neither counts as the caller setting it.
590
+ if any(
591
+ key.lower() == "content-type" and value is not None
592
+ for key, value in header_params.items()
593
+ ):
594
+ return
595
+ for key in list(header_params):
596
+ if key.lower() == "content-type":
597
+ header_params[key] = media_type
598
+ return
599
+ header_params["Content-Type"] = media_type
600
+
601
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
602
+ out: dict[str, str] = {}
603
+ for key, value in cookie_params.items():
604
+ if value is None:
605
+ continue
606
+ dumped = self._dump(value)
607
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
608
+ # object on \`,\` between each key and value. Members are encoded so a
609
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
610
+ # joining commas stay literal.
611
+ if isinstance(dumped, list):
612
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
613
+ elif isinstance(dumped, dict):
614
+ rendered = ",".join(
615
+ self._cookie_value(k) + "," + self._cookie_value(v)
616
+ for k, v in dumped.items()
617
+ if v is not None
618
+ )
619
+ else:
620
+ rendered = self._cookie_value(value)
621
+ out[key] = rendered
622
+ return out
623
+
624
+ def _cookie_value(self, value: Any) -> str:
625
+ # Only the characters a cookie parser treats as structure are escaped, so
626
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
627
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
628
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
629
+
630
+ def _dump(self, value: Any) -> Any:
631
+ if value is None:
632
+ return None
633
+ if isinstance(value, BaseModel):
634
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
635
+ if isinstance(value, (list, tuple)):
636
+ return [self._dump(v) for v in value]
637
+ if isinstance(value, dict):
638
+ return {k: self._dump(v) for k, v in value.items()}
639
+ if isinstance(value, (datetime.date, datetime.datetime)):
640
+ return value.isoformat()
641
+ return value
642
+
643
+ def _error_payload(self, response: httpx.Response) -> Any:
644
+ # An error body is whatever the server actually sent, which need not be
645
+ # the JSON the spec declares (a proxy's HTML page, say).
646
+ try:
647
+ return response.json()
648
+ except ValueError:
649
+ return response.text
650
+
651
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
652
+ # Coerce to the declared error type, falling back to the raw payload so
653
+ # a body that doesn't match the spec still raises the typed exception.
654
+ try:
655
+ return wrapper.model_validate({"status": status, "error": payload})
656
+ except ValidationError:
657
+ return wrapper.model_construct(status=status, error=payload)
658
+
659
+ def create_users(
660
+ self,
661
+ body: list[types.User],
662
+ ) -> list[types.User]:
663
+ header_params: dict[str, Any] = {}
664
+ body = self._dump(body)
665
+ request_kwargs: dict[str, Any] = {"json": body}
666
+ if body is not None and not self._config.omit_content_type_header:
667
+ self._default_content_type(header_params, "application/json")
668
+ _request_url = self._url("/create-users", {})
669
+ response = self._client.request(
670
+ "POST",
671
+ _request_url,
672
+ headers=self._headers(header_params),
673
+ **request_kwargs,
674
+ )
675
+ if response.status_code == 200:
676
+ return TypeAdapter(list[types.User]).validate_python(response.json())
677
+ raise CreateUsersApiError(response.status_code, None)
678
+ "
679
+ `;
680
+
681
+ exports[`openApiPyClientGenerator - arrays > should handle operation which accepts an array of objects > types.py 1`] = `
682
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
683
+
684
+ from __future__ import annotations
685
+
686
+ from typing import Never, TypedDict
687
+
688
+ from pydantic import BaseModel, ConfigDict
689
+
690
+
691
+ class User(BaseModel):
692
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
693
+
694
+ username: str
695
+ email: str | None = None
696
+
697
+
698
+ class CreateUsersRequest(TypedDict):
699
+ body: list[User]
700
+
701
+
702
+ CreateUsersError = Never
703
+ "
704
+ `;
705
+
706
+ exports[`openApiPyClientGenerator - arrays > should handle operation which accepts an array of strings > client.py 1`] = `
707
+ """"TestApi — AUTO-GENERATED synchronous client."""
708
+
709
+ from __future__ import annotations
710
+
711
+ import datetime
712
+ from dataclasses import dataclass, field
713
+ from typing import Any, TypeVar
714
+ from urllib.parse import quote
715
+
716
+ import httpx
717
+ from pydantic import BaseModel, ValidationError
718
+
719
+ from . import types
720
+ from .errors import (
721
+ ApiError as ApiError,
722
+ )
723
+ from .errors import (
724
+ ProcessTagsApiError as ProcessTagsApiError,
725
+ )
726
+
727
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
728
+
729
+
730
+ @dataclass
731
+ class TestApiConfig:
732
+ """Client configuration for TestApi."""
733
+
734
+ #: Base URL for the API.
735
+ url: str
736
+ #: Client used for every request. Supply your own to add authentication or
737
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
738
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
739
+ #: client's own headers and query params are sent alongside each operation's.
740
+ #: A supplied client is never closed by this one, so it can be shared.
741
+ httpx_client: httpx.Client | None = None
742
+ #: Headers added to every request, where the operation sets none itself.
743
+ headers: dict[str, str] = field(default_factory=dict)
744
+ #: By default the client sends a Content-Type header set to the media type
745
+ #: the OpenAPI specification declares for the request. Set this to omit it.
746
+ omit_content_type_header: bool = False
747
+
748
+
749
+ class TestApi:
750
+ """Synchronous API client for TestApi."""
751
+
752
+ def __init__(self, config: TestApiConfig) -> None:
753
+ self._config = config
754
+ self._client = config.httpx_client or httpx.Client()
755
+ self._owns_client = config.httpx_client is None
756
+ self._base_url = config.url.rstrip("/")
757
+
758
+ def __enter__(self) -> TestApi:
759
+ return self
760
+
761
+ def __exit__(self, *exc: Any) -> None:
762
+ self.close()
763
+
764
+ def close(self) -> None:
765
+ if self._owns_client:
766
+ self._client.close()
767
+
768
+ def _url(
769
+ self,
770
+ path: str,
771
+ path_params: dict[str, Any],
772
+ path_styles: dict[str, tuple[str, bool]] | None = None,
773
+ ) -> str:
774
+ for key, value in path_params.items():
775
+ style, explode = (path_styles or {}).get(key, ("simple", False))
776
+ # An object expands to its key,value members; a list to its items.
777
+ # Each is encoded on its own so the separators between them stay
778
+ # literal, as RFC 6570 requires.
779
+ dumped = self._dump(value)
780
+ if isinstance(dumped, dict):
781
+ values = [
782
+ part
783
+ for k, v in dumped.items()
784
+ if v is not None
785
+ for part in (k, self._scalar(v))
786
+ ]
787
+ elif isinstance(dumped, list):
788
+ values = list(dumped)
789
+ else:
790
+ values = [value]
791
+ if style == "matrix":
792
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
793
+ if explode:
794
+ rendered = "".join(
795
+ ";" + key + "=" + quote(self._scalar(v), safe="")
796
+ for v in values
797
+ )
798
+ else:
799
+ rendered = (
800
+ ";"
801
+ + key
802
+ + "="
803
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
804
+ )
805
+ elif style == "label":
806
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
807
+ rendered = "." + ("." if explode else ",").join(
808
+ quote(self._scalar(v), safe="") for v in values
809
+ )
810
+ else:
811
+ # RFC 6570 simple expansion: a list renders comma-separated.
812
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
813
+ path = path.replace("{" + key + "}", rendered)
814
+ return self._base_url + path
815
+
816
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
817
+ if isinstance(value, BaseModel):
818
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
819
+ if isinstance(value, dict):
820
+ for prop, v in value.items():
821
+ self._deep_object(f"{key}[{prop}]", v, out)
822
+ elif value is not None:
823
+ out[key] = value
824
+
825
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
826
+
827
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
828
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
829
+ #: too — only the first one separates a cookie's name from its value.
830
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
831
+
832
+ def _scalar(self, value: Any) -> str:
833
+ # The wire form of a single parameter value: JSON spellings for bools,
834
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
835
+ if isinstance(value, bool):
836
+ return "true" if value else "false"
837
+ if isinstance(value, (datetime.date, datetime.datetime)):
838
+ return value.isoformat()
839
+ dumped = self._dump(value)
840
+ if isinstance(dumped, str):
841
+ return dumped
842
+ # RFC 6570 renders a list as its comma-joined members and an object as
843
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
844
+ # \`['a', 'b']\`, quotes and spaces included.
845
+ if isinstance(dumped, list):
846
+ return ",".join(self._scalar(v) for v in dumped)
847
+ if isinstance(dumped, dict):
848
+ return ",".join(
849
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
850
+ )
851
+ if dumped is None:
852
+ return ""
853
+ return str(dumped)
854
+
855
+ def _query(
856
+ self,
857
+ query_params: dict[str, Any],
858
+ collection_formats: dict[str, str] | None = None,
859
+ ) -> dict[str, Any]:
860
+ out: dict[str, Any] = {}
861
+ for key, value in query_params.items():
862
+ if value is None:
863
+ continue
864
+ fmt = (collection_formats or {}).get(key)
865
+ if fmt == "deepObject":
866
+ self._deep_object(key, value, out)
867
+ elif isinstance(value, list):
868
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
869
+ if delimiter is None:
870
+ out[key] = [self._scalar(v) for v in value]
871
+ else:
872
+ out[key] = delimiter.join(self._scalar(v) for v in value)
873
+ elif self._is_object(value):
874
+ # OpenAPI's default query style is form/explode, under which an
875
+ # object expands to one parameter per property rather than
876
+ # being stringified.
877
+ for prop, prop_value in self._as_dict(value).items():
878
+ if prop_value is not None:
879
+ out[prop] = self._scalar(prop_value)
880
+ else:
881
+ out[key] = self._scalar(value)
882
+ return out
883
+
884
+ def _is_object(self, value: Any) -> bool:
885
+ return isinstance(value, (dict, BaseModel))
886
+
887
+ def _as_dict(self, value: Any) -> dict[str, Any]:
888
+ if isinstance(value, BaseModel):
889
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
890
+ return dict(value)
891
+
892
+ def _headers(
893
+ self,
894
+ header_params: dict[str, Any] | None = None,
895
+ collection_formats: dict[str, str] | None = None,
896
+ cookie_params: dict[str, Any] | None = None,
897
+ ) -> list[tuple[str, str]]:
898
+ out: list[tuple[str, str]] = []
899
+ for key, value in (header_params or {}).items():
900
+ if value is None:
901
+ continue
902
+ if isinstance(value, list):
903
+ fmt = (collection_formats or {}).get(key, "csv")
904
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
905
+ if delimiter is None:
906
+ for item in value:
907
+ out.append((key, self._scalar(item)))
908
+ else:
909
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
910
+ else:
911
+ out.append((key, self._scalar(value)))
912
+ # A config header only applies where the operation didn't set one:
913
+ # httpx comma-joins repeated names, which would corrupt the value.
914
+ operation_headers = {key.lower() for key, _ in out}
915
+ out.extend(
916
+ (key, value)
917
+ for key, value in self._config.headers.items()
918
+ if key.lower() not in operation_headers
919
+ )
920
+ # The operation's cookies are merged into one \`Cookie\` header alongside
921
+ # every other source. Passing them to httpx's \`cookies=\` instead would
922
+ # drop them outright whenever a \`Cookie\` header is present, since
923
+ # http.cookiejar skips a request that already carries one — and setting
924
+ # the header here would in turn shadow the caller's own jar and header,
925
+ # which httpx resolves in favour of the more specific value.
926
+ cookies = self._cookies(cookie_params or {})
927
+ if cookies:
928
+ pairs = [
929
+ *(
930
+ value
931
+ for source in (self._client.headers, dict(out))
932
+ for key, value in source.items()
933
+ if key.lower() == "cookie"
934
+ ),
935
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
936
+ *(f"{key}={value}" for key, value in cookies.items()),
937
+ ]
938
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
939
+ out.append(("Cookie", "; ".join(pairs)))
940
+ return out
941
+
942
+ def _default_content_type(
943
+ self, header_params: dict[str, Any], media_type: str
944
+ ) -> None:
945
+ # The media type the specification declares, applied unless the operation
946
+ # takes a Content-Type of its own and the caller supplied one. Header
947
+ # names are case-insensitive, and a declared header the caller omitted is
948
+ # present holding \`None\` — neither counts as the caller setting it.
949
+ if any(
950
+ key.lower() == "content-type" and value is not None
951
+ for key, value in header_params.items()
952
+ ):
953
+ return
954
+ for key in list(header_params):
955
+ if key.lower() == "content-type":
956
+ header_params[key] = media_type
957
+ return
958
+ header_params["Content-Type"] = media_type
959
+
960
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
961
+ out: dict[str, str] = {}
962
+ for key, value in cookie_params.items():
963
+ if value is None:
964
+ continue
965
+ dumped = self._dump(value)
966
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
967
+ # object on \`,\` between each key and value. Members are encoded so a
968
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
969
+ # joining commas stay literal.
970
+ if isinstance(dumped, list):
971
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
972
+ elif isinstance(dumped, dict):
973
+ rendered = ",".join(
974
+ self._cookie_value(k) + "," + self._cookie_value(v)
975
+ for k, v in dumped.items()
976
+ if v is not None
977
+ )
978
+ else:
979
+ rendered = self._cookie_value(value)
980
+ out[key] = rendered
981
+ return out
982
+
983
+ def _cookie_value(self, value: Any) -> str:
984
+ # Only the characters a cookie parser treats as structure are escaped, so
985
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
986
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
987
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
988
+
989
+ def _dump(self, value: Any) -> Any:
990
+ if value is None:
991
+ return None
992
+ if isinstance(value, BaseModel):
993
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
994
+ if isinstance(value, (list, tuple)):
995
+ return [self._dump(v) for v in value]
996
+ if isinstance(value, dict):
997
+ return {k: self._dump(v) for k, v in value.items()}
998
+ if isinstance(value, (datetime.date, datetime.datetime)):
999
+ return value.isoformat()
1000
+ return value
1001
+
1002
+ def _error_payload(self, response: httpx.Response) -> Any:
1003
+ # An error body is whatever the server actually sent, which need not be
1004
+ # the JSON the spec declares (a proxy's HTML page, say).
1005
+ try:
1006
+ return response.json()
1007
+ except ValueError:
1008
+ return response.text
1009
+
1010
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
1011
+ # Coerce to the declared error type, falling back to the raw payload so
1012
+ # a body that doesn't match the spec still raises the typed exception.
1013
+ try:
1014
+ return wrapper.model_validate({"status": status, "error": payload})
1015
+ except ValidationError:
1016
+ return wrapper.model_construct(status=status, error=payload)
1017
+
1018
+ def process_tags(
1019
+ self,
1020
+ body: list[str],
1021
+ ) -> types.ProcessTags200Response:
1022
+ header_params: dict[str, Any] = {}
1023
+ body = self._dump(body)
1024
+ request_kwargs: dict[str, Any] = {"json": body}
1025
+ if body is not None and not self._config.omit_content_type_header:
1026
+ self._default_content_type(header_params, "application/json")
1027
+ _request_url = self._url("/process-tags", {})
1028
+ response = self._client.request(
1029
+ "POST",
1030
+ _request_url,
1031
+ headers=self._headers(header_params),
1032
+ **request_kwargs,
1033
+ )
1034
+ if response.status_code == 200:
1035
+ return types.ProcessTags200Response.model_validate(response.json())
1036
+ raise ProcessTagsApiError(response.status_code, None)
1037
+ "
1038
+ `;
1039
+
1040
+ exports[`openApiPyClientGenerator - arrays > should handle operation which accepts an array of strings > types.py 1`] = `
1041
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
1042
+
1043
+ from __future__ import annotations
1044
+
1045
+ from typing import Never, TypedDict
1046
+
1047
+ from pydantic import BaseModel, ConfigDict
1048
+
1049
+
1050
+ class ProcessTags200Response(BaseModel):
1051
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
1052
+
1053
+ processed: int
1054
+
1055
+
1056
+ class ProcessTagsRequest(TypedDict):
1057
+ body: list[str]
1058
+
1059
+
1060
+ ProcessTagsError = Never
1061
+ "
1062
+ `;
1063
+
1064
+ exports[`openApiPyClientGenerator - arrays > should handle operation which returns a nested array of strings > client.py 1`] = `
1065
+ """"TestApi — AUTO-GENERATED synchronous client."""
1066
+
1067
+ from __future__ import annotations
1068
+
1069
+ import datetime
1070
+ from dataclasses import dataclass, field
1071
+ from typing import Any, TypeVar
1072
+ from urllib.parse import quote
1073
+
1074
+ import httpx
1075
+ from pydantic import BaseModel, TypeAdapter, ValidationError
1076
+
1077
+ from .errors import (
1078
+ ApiError as ApiError,
1079
+ )
1080
+ from .errors import (
1081
+ GetMatrixApiError as GetMatrixApiError,
1082
+ )
1083
+
1084
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
1085
+
1086
+
1087
+ @dataclass
1088
+ class TestApiConfig:
1089
+ """Client configuration for TestApi."""
1090
+
1091
+ #: Base URL for the API.
1092
+ url: str
1093
+ #: Client used for every request. Supply your own to add authentication or
1094
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
1095
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
1096
+ #: client's own headers and query params are sent alongside each operation's.
1097
+ #: A supplied client is never closed by this one, so it can be shared.
1098
+ httpx_client: httpx.Client | None = None
1099
+ #: Headers added to every request, where the operation sets none itself.
1100
+ headers: dict[str, str] = field(default_factory=dict)
1101
+ #: By default the client sends a Content-Type header set to the media type
1102
+ #: the OpenAPI specification declares for the request. Set this to omit it.
1103
+ omit_content_type_header: bool = False
1104
+
1105
+
1106
+ class TestApi:
1107
+ """Synchronous API client for TestApi."""
1108
+
1109
+ def __init__(self, config: TestApiConfig) -> None:
1110
+ self._config = config
1111
+ self._client = config.httpx_client or httpx.Client()
1112
+ self._owns_client = config.httpx_client is None
1113
+ self._base_url = config.url.rstrip("/")
1114
+
1115
+ def __enter__(self) -> TestApi:
1116
+ return self
1117
+
1118
+ def __exit__(self, *exc: Any) -> None:
1119
+ self.close()
1120
+
1121
+ def close(self) -> None:
1122
+ if self._owns_client:
1123
+ self._client.close()
1124
+
1125
+ def _url(
1126
+ self,
1127
+ path: str,
1128
+ path_params: dict[str, Any],
1129
+ path_styles: dict[str, tuple[str, bool]] | None = None,
1130
+ ) -> str:
1131
+ for key, value in path_params.items():
1132
+ style, explode = (path_styles or {}).get(key, ("simple", False))
1133
+ # An object expands to its key,value members; a list to its items.
1134
+ # Each is encoded on its own so the separators between them stay
1135
+ # literal, as RFC 6570 requires.
1136
+ dumped = self._dump(value)
1137
+ if isinstance(dumped, dict):
1138
+ values = [
1139
+ part
1140
+ for k, v in dumped.items()
1141
+ if v is not None
1142
+ for part in (k, self._scalar(v))
1143
+ ]
1144
+ elif isinstance(dumped, list):
1145
+ values = list(dumped)
1146
+ else:
1147
+ values = [value]
1148
+ if style == "matrix":
1149
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
1150
+ if explode:
1151
+ rendered = "".join(
1152
+ ";" + key + "=" + quote(self._scalar(v), safe="")
1153
+ for v in values
1154
+ )
1155
+ else:
1156
+ rendered = (
1157
+ ";"
1158
+ + key
1159
+ + "="
1160
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
1161
+ )
1162
+ elif style == "label":
1163
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
1164
+ rendered = "." + ("." if explode else ",").join(
1165
+ quote(self._scalar(v), safe="") for v in values
1166
+ )
1167
+ else:
1168
+ # RFC 6570 simple expansion: a list renders comma-separated.
1169
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
1170
+ path = path.replace("{" + key + "}", rendered)
1171
+ return self._base_url + path
1172
+
1173
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
1174
+ if isinstance(value, BaseModel):
1175
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1176
+ if isinstance(value, dict):
1177
+ for prop, v in value.items():
1178
+ self._deep_object(f"{key}[{prop}]", v, out)
1179
+ elif value is not None:
1180
+ out[key] = value
1181
+
1182
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
1183
+
1184
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
1185
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
1186
+ #: too — only the first one separates a cookie's name from its value.
1187
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
1188
+
1189
+ def _scalar(self, value: Any) -> str:
1190
+ # The wire form of a single parameter value: JSON spellings for bools,
1191
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
1192
+ if isinstance(value, bool):
1193
+ return "true" if value else "false"
1194
+ if isinstance(value, (datetime.date, datetime.datetime)):
1195
+ return value.isoformat()
1196
+ dumped = self._dump(value)
1197
+ if isinstance(dumped, str):
1198
+ return dumped
1199
+ # RFC 6570 renders a list as its comma-joined members and an object as
1200
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
1201
+ # \`['a', 'b']\`, quotes and spaces included.
1202
+ if isinstance(dumped, list):
1203
+ return ",".join(self._scalar(v) for v in dumped)
1204
+ if isinstance(dumped, dict):
1205
+ return ",".join(
1206
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
1207
+ )
1208
+ if dumped is None:
1209
+ return ""
1210
+ return str(dumped)
1211
+
1212
+ def _query(
1213
+ self,
1214
+ query_params: dict[str, Any],
1215
+ collection_formats: dict[str, str] | None = None,
1216
+ ) -> dict[str, Any]:
1217
+ out: dict[str, Any] = {}
1218
+ for key, value in query_params.items():
1219
+ if value is None:
1220
+ continue
1221
+ fmt = (collection_formats or {}).get(key)
1222
+ if fmt == "deepObject":
1223
+ self._deep_object(key, value, out)
1224
+ elif isinstance(value, list):
1225
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
1226
+ if delimiter is None:
1227
+ out[key] = [self._scalar(v) for v in value]
1228
+ else:
1229
+ out[key] = delimiter.join(self._scalar(v) for v in value)
1230
+ elif self._is_object(value):
1231
+ # OpenAPI's default query style is form/explode, under which an
1232
+ # object expands to one parameter per property rather than
1233
+ # being stringified.
1234
+ for prop, prop_value in self._as_dict(value).items():
1235
+ if prop_value is not None:
1236
+ out[prop] = self._scalar(prop_value)
1237
+ else:
1238
+ out[key] = self._scalar(value)
1239
+ return out
1240
+
1241
+ def _is_object(self, value: Any) -> bool:
1242
+ return isinstance(value, (dict, BaseModel))
1243
+
1244
+ def _as_dict(self, value: Any) -> dict[str, Any]:
1245
+ if isinstance(value, BaseModel):
1246
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1247
+ return dict(value)
1248
+
1249
+ def _headers(
1250
+ self,
1251
+ header_params: dict[str, Any] | None = None,
1252
+ collection_formats: dict[str, str] | None = None,
1253
+ cookie_params: dict[str, Any] | None = None,
1254
+ ) -> list[tuple[str, str]]:
1255
+ out: list[tuple[str, str]] = []
1256
+ for key, value in (header_params or {}).items():
1257
+ if value is None:
1258
+ continue
1259
+ if isinstance(value, list):
1260
+ fmt = (collection_formats or {}).get(key, "csv")
1261
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
1262
+ if delimiter is None:
1263
+ for item in value:
1264
+ out.append((key, self._scalar(item)))
1265
+ else:
1266
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
1267
+ else:
1268
+ out.append((key, self._scalar(value)))
1269
+ # A config header only applies where the operation didn't set one:
1270
+ # httpx comma-joins repeated names, which would corrupt the value.
1271
+ operation_headers = {key.lower() for key, _ in out}
1272
+ out.extend(
1273
+ (key, value)
1274
+ for key, value in self._config.headers.items()
1275
+ if key.lower() not in operation_headers
1276
+ )
1277
+ # The operation's cookies are merged into one \`Cookie\` header alongside
1278
+ # every other source. Passing them to httpx's \`cookies=\` instead would
1279
+ # drop them outright whenever a \`Cookie\` header is present, since
1280
+ # http.cookiejar skips a request that already carries one — and setting
1281
+ # the header here would in turn shadow the caller's own jar and header,
1282
+ # which httpx resolves in favour of the more specific value.
1283
+ cookies = self._cookies(cookie_params or {})
1284
+ if cookies:
1285
+ pairs = [
1286
+ *(
1287
+ value
1288
+ for source in (self._client.headers, dict(out))
1289
+ for key, value in source.items()
1290
+ if key.lower() == "cookie"
1291
+ ),
1292
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
1293
+ *(f"{key}={value}" for key, value in cookies.items()),
1294
+ ]
1295
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
1296
+ out.append(("Cookie", "; ".join(pairs)))
1297
+ return out
1298
+
1299
+ def _default_content_type(
1300
+ self, header_params: dict[str, Any], media_type: str
1301
+ ) -> None:
1302
+ # The media type the specification declares, applied unless the operation
1303
+ # takes a Content-Type of its own and the caller supplied one. Header
1304
+ # names are case-insensitive, and a declared header the caller omitted is
1305
+ # present holding \`None\` — neither counts as the caller setting it.
1306
+ if any(
1307
+ key.lower() == "content-type" and value is not None
1308
+ for key, value in header_params.items()
1309
+ ):
1310
+ return
1311
+ for key in list(header_params):
1312
+ if key.lower() == "content-type":
1313
+ header_params[key] = media_type
1314
+ return
1315
+ header_params["Content-Type"] = media_type
1316
+
1317
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
1318
+ out: dict[str, str] = {}
1319
+ for key, value in cookie_params.items():
1320
+ if value is None:
1321
+ continue
1322
+ dumped = self._dump(value)
1323
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
1324
+ # object on \`,\` between each key and value. Members are encoded so a
1325
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
1326
+ # joining commas stay literal.
1327
+ if isinstance(dumped, list):
1328
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
1329
+ elif isinstance(dumped, dict):
1330
+ rendered = ",".join(
1331
+ self._cookie_value(k) + "," + self._cookie_value(v)
1332
+ for k, v in dumped.items()
1333
+ if v is not None
1334
+ )
1335
+ else:
1336
+ rendered = self._cookie_value(value)
1337
+ out[key] = rendered
1338
+ return out
1339
+
1340
+ def _cookie_value(self, value: Any) -> str:
1341
+ # Only the characters a cookie parser treats as structure are escaped, so
1342
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
1343
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
1344
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
1345
+
1346
+ def _dump(self, value: Any) -> Any:
1347
+ if value is None:
1348
+ return None
1349
+ if isinstance(value, BaseModel):
1350
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1351
+ if isinstance(value, (list, tuple)):
1352
+ return [self._dump(v) for v in value]
1353
+ if isinstance(value, dict):
1354
+ return {k: self._dump(v) for k, v in value.items()}
1355
+ if isinstance(value, (datetime.date, datetime.datetime)):
1356
+ return value.isoformat()
1357
+ return value
1358
+
1359
+ def _error_payload(self, response: httpx.Response) -> Any:
1360
+ # An error body is whatever the server actually sent, which need not be
1361
+ # the JSON the spec declares (a proxy's HTML page, say).
1362
+ try:
1363
+ return response.json()
1364
+ except ValueError:
1365
+ return response.text
1366
+
1367
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
1368
+ # Coerce to the declared error type, falling back to the raw payload so
1369
+ # a body that doesn't match the spec still raises the typed exception.
1370
+ try:
1371
+ return wrapper.model_validate({"status": status, "error": payload})
1372
+ except ValidationError:
1373
+ return wrapper.model_construct(status=status, error=payload)
1374
+
1375
+ def get_matrix(
1376
+ self,
1377
+ ) -> list[list[str]]:
1378
+ request_kwargs: dict[str, Any] = {}
1379
+ _request_url = self._url("/matrix", {})
1380
+ response = self._client.request(
1381
+ "GET",
1382
+ _request_url,
1383
+ headers=self._headers(),
1384
+ **request_kwargs,
1385
+ )
1386
+ if response.status_code == 200:
1387
+ return TypeAdapter(list[list[str]]).validate_python(response.json())
1388
+ raise GetMatrixApiError(response.status_code, None)
1389
+ "
1390
+ `;
1391
+
1392
+ exports[`openApiPyClientGenerator - arrays > should handle operation which returns a nested array of strings > types.py 1`] = `
1393
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
1394
+
1395
+ from __future__ import annotations
1396
+
1397
+ from typing import Never
1398
+
1399
+ GetMatrixError = Never
1400
+ "
1401
+ `;
1402
+
1403
+ exports[`openApiPyClientGenerator - arrays > should handle operation which returns an array of objects > client.py 1`] = `
1404
+ """"TestApi — AUTO-GENERATED synchronous client."""
1405
+
1406
+ from __future__ import annotations
1407
+
1408
+ import datetime
1409
+ from dataclasses import dataclass, field
1410
+ from typing import Any, TypeVar
1411
+ from urllib.parse import quote
1412
+
1413
+ import httpx
1414
+ from pydantic import BaseModel, TypeAdapter, ValidationError
1415
+
1416
+ from . import types
1417
+ from .errors import (
1418
+ ApiError as ApiError,
1419
+ )
1420
+ from .errors import (
1421
+ ListUsersApiError as ListUsersApiError,
1422
+ )
1423
+
1424
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
1425
+
1426
+
1427
+ @dataclass
1428
+ class TestApiConfig:
1429
+ """Client configuration for TestApi."""
1430
+
1431
+ #: Base URL for the API.
1432
+ url: str
1433
+ #: Client used for every request. Supply your own to add authentication or
1434
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
1435
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
1436
+ #: client's own headers and query params are sent alongside each operation's.
1437
+ #: A supplied client is never closed by this one, so it can be shared.
1438
+ httpx_client: httpx.Client | None = None
1439
+ #: Headers added to every request, where the operation sets none itself.
1440
+ headers: dict[str, str] = field(default_factory=dict)
1441
+ #: By default the client sends a Content-Type header set to the media type
1442
+ #: the OpenAPI specification declares for the request. Set this to omit it.
1443
+ omit_content_type_header: bool = False
1444
+
1445
+
1446
+ class TestApi:
1447
+ """Synchronous API client for TestApi."""
1448
+
1449
+ def __init__(self, config: TestApiConfig) -> None:
1450
+ self._config = config
1451
+ self._client = config.httpx_client or httpx.Client()
1452
+ self._owns_client = config.httpx_client is None
1453
+ self._base_url = config.url.rstrip("/")
1454
+
1455
+ def __enter__(self) -> TestApi:
1456
+ return self
1457
+
1458
+ def __exit__(self, *exc: Any) -> None:
1459
+ self.close()
1460
+
1461
+ def close(self) -> None:
1462
+ if self._owns_client:
1463
+ self._client.close()
1464
+
1465
+ def _url(
1466
+ self,
1467
+ path: str,
1468
+ path_params: dict[str, Any],
1469
+ path_styles: dict[str, tuple[str, bool]] | None = None,
1470
+ ) -> str:
1471
+ for key, value in path_params.items():
1472
+ style, explode = (path_styles or {}).get(key, ("simple", False))
1473
+ # An object expands to its key,value members; a list to its items.
1474
+ # Each is encoded on its own so the separators between them stay
1475
+ # literal, as RFC 6570 requires.
1476
+ dumped = self._dump(value)
1477
+ if isinstance(dumped, dict):
1478
+ values = [
1479
+ part
1480
+ for k, v in dumped.items()
1481
+ if v is not None
1482
+ for part in (k, self._scalar(v))
1483
+ ]
1484
+ elif isinstance(dumped, list):
1485
+ values = list(dumped)
1486
+ else:
1487
+ values = [value]
1488
+ if style == "matrix":
1489
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
1490
+ if explode:
1491
+ rendered = "".join(
1492
+ ";" + key + "=" + quote(self._scalar(v), safe="")
1493
+ for v in values
1494
+ )
1495
+ else:
1496
+ rendered = (
1497
+ ";"
1498
+ + key
1499
+ + "="
1500
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
1501
+ )
1502
+ elif style == "label":
1503
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
1504
+ rendered = "." + ("." if explode else ",").join(
1505
+ quote(self._scalar(v), safe="") for v in values
1506
+ )
1507
+ else:
1508
+ # RFC 6570 simple expansion: a list renders comma-separated.
1509
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
1510
+ path = path.replace("{" + key + "}", rendered)
1511
+ return self._base_url + path
1512
+
1513
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
1514
+ if isinstance(value, BaseModel):
1515
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1516
+ if isinstance(value, dict):
1517
+ for prop, v in value.items():
1518
+ self._deep_object(f"{key}[{prop}]", v, out)
1519
+ elif value is not None:
1520
+ out[key] = value
1521
+
1522
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
1523
+
1524
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
1525
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
1526
+ #: too — only the first one separates a cookie's name from its value.
1527
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
1528
+
1529
+ def _scalar(self, value: Any) -> str:
1530
+ # The wire form of a single parameter value: JSON spellings for bools,
1531
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
1532
+ if isinstance(value, bool):
1533
+ return "true" if value else "false"
1534
+ if isinstance(value, (datetime.date, datetime.datetime)):
1535
+ return value.isoformat()
1536
+ dumped = self._dump(value)
1537
+ if isinstance(dumped, str):
1538
+ return dumped
1539
+ # RFC 6570 renders a list as its comma-joined members and an object as
1540
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
1541
+ # \`['a', 'b']\`, quotes and spaces included.
1542
+ if isinstance(dumped, list):
1543
+ return ",".join(self._scalar(v) for v in dumped)
1544
+ if isinstance(dumped, dict):
1545
+ return ",".join(
1546
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
1547
+ )
1548
+ if dumped is None:
1549
+ return ""
1550
+ return str(dumped)
1551
+
1552
+ def _query(
1553
+ self,
1554
+ query_params: dict[str, Any],
1555
+ collection_formats: dict[str, str] | None = None,
1556
+ ) -> dict[str, Any]:
1557
+ out: dict[str, Any] = {}
1558
+ for key, value in query_params.items():
1559
+ if value is None:
1560
+ continue
1561
+ fmt = (collection_formats or {}).get(key)
1562
+ if fmt == "deepObject":
1563
+ self._deep_object(key, value, out)
1564
+ elif isinstance(value, list):
1565
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
1566
+ if delimiter is None:
1567
+ out[key] = [self._scalar(v) for v in value]
1568
+ else:
1569
+ out[key] = delimiter.join(self._scalar(v) for v in value)
1570
+ elif self._is_object(value):
1571
+ # OpenAPI's default query style is form/explode, under which an
1572
+ # object expands to one parameter per property rather than
1573
+ # being stringified.
1574
+ for prop, prop_value in self._as_dict(value).items():
1575
+ if prop_value is not None:
1576
+ out[prop] = self._scalar(prop_value)
1577
+ else:
1578
+ out[key] = self._scalar(value)
1579
+ return out
1580
+
1581
+ def _is_object(self, value: Any) -> bool:
1582
+ return isinstance(value, (dict, BaseModel))
1583
+
1584
+ def _as_dict(self, value: Any) -> dict[str, Any]:
1585
+ if isinstance(value, BaseModel):
1586
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1587
+ return dict(value)
1588
+
1589
+ def _headers(
1590
+ self,
1591
+ header_params: dict[str, Any] | None = None,
1592
+ collection_formats: dict[str, str] | None = None,
1593
+ cookie_params: dict[str, Any] | None = None,
1594
+ ) -> list[tuple[str, str]]:
1595
+ out: list[tuple[str, str]] = []
1596
+ for key, value in (header_params or {}).items():
1597
+ if value is None:
1598
+ continue
1599
+ if isinstance(value, list):
1600
+ fmt = (collection_formats or {}).get(key, "csv")
1601
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
1602
+ if delimiter is None:
1603
+ for item in value:
1604
+ out.append((key, self._scalar(item)))
1605
+ else:
1606
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
1607
+ else:
1608
+ out.append((key, self._scalar(value)))
1609
+ # A config header only applies where the operation didn't set one:
1610
+ # httpx comma-joins repeated names, which would corrupt the value.
1611
+ operation_headers = {key.lower() for key, _ in out}
1612
+ out.extend(
1613
+ (key, value)
1614
+ for key, value in self._config.headers.items()
1615
+ if key.lower() not in operation_headers
1616
+ )
1617
+ # The operation's cookies are merged into one \`Cookie\` header alongside
1618
+ # every other source. Passing them to httpx's \`cookies=\` instead would
1619
+ # drop them outright whenever a \`Cookie\` header is present, since
1620
+ # http.cookiejar skips a request that already carries one — and setting
1621
+ # the header here would in turn shadow the caller's own jar and header,
1622
+ # which httpx resolves in favour of the more specific value.
1623
+ cookies = self._cookies(cookie_params or {})
1624
+ if cookies:
1625
+ pairs = [
1626
+ *(
1627
+ value
1628
+ for source in (self._client.headers, dict(out))
1629
+ for key, value in source.items()
1630
+ if key.lower() == "cookie"
1631
+ ),
1632
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
1633
+ *(f"{key}={value}" for key, value in cookies.items()),
1634
+ ]
1635
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
1636
+ out.append(("Cookie", "; ".join(pairs)))
1637
+ return out
1638
+
1639
+ def _default_content_type(
1640
+ self, header_params: dict[str, Any], media_type: str
1641
+ ) -> None:
1642
+ # The media type the specification declares, applied unless the operation
1643
+ # takes a Content-Type of its own and the caller supplied one. Header
1644
+ # names are case-insensitive, and a declared header the caller omitted is
1645
+ # present holding \`None\` — neither counts as the caller setting it.
1646
+ if any(
1647
+ key.lower() == "content-type" and value is not None
1648
+ for key, value in header_params.items()
1649
+ ):
1650
+ return
1651
+ for key in list(header_params):
1652
+ if key.lower() == "content-type":
1653
+ header_params[key] = media_type
1654
+ return
1655
+ header_params["Content-Type"] = media_type
1656
+
1657
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
1658
+ out: dict[str, str] = {}
1659
+ for key, value in cookie_params.items():
1660
+ if value is None:
1661
+ continue
1662
+ dumped = self._dump(value)
1663
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
1664
+ # object on \`,\` between each key and value. Members are encoded so a
1665
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
1666
+ # joining commas stay literal.
1667
+ if isinstance(dumped, list):
1668
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
1669
+ elif isinstance(dumped, dict):
1670
+ rendered = ",".join(
1671
+ self._cookie_value(k) + "," + self._cookie_value(v)
1672
+ for k, v in dumped.items()
1673
+ if v is not None
1674
+ )
1675
+ else:
1676
+ rendered = self._cookie_value(value)
1677
+ out[key] = rendered
1678
+ return out
1679
+
1680
+ def _cookie_value(self, value: Any) -> str:
1681
+ # Only the characters a cookie parser treats as structure are escaped, so
1682
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
1683
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
1684
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
1685
+
1686
+ def _dump(self, value: Any) -> Any:
1687
+ if value is None:
1688
+ return None
1689
+ if isinstance(value, BaseModel):
1690
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1691
+ if isinstance(value, (list, tuple)):
1692
+ return [self._dump(v) for v in value]
1693
+ if isinstance(value, dict):
1694
+ return {k: self._dump(v) for k, v in value.items()}
1695
+ if isinstance(value, (datetime.date, datetime.datetime)):
1696
+ return value.isoformat()
1697
+ return value
1698
+
1699
+ def _error_payload(self, response: httpx.Response) -> Any:
1700
+ # An error body is whatever the server actually sent, which need not be
1701
+ # the JSON the spec declares (a proxy's HTML page, say).
1702
+ try:
1703
+ return response.json()
1704
+ except ValueError:
1705
+ return response.text
1706
+
1707
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
1708
+ # Coerce to the declared error type, falling back to the raw payload so
1709
+ # a body that doesn't match the spec still raises the typed exception.
1710
+ try:
1711
+ return wrapper.model_validate({"status": status, "error": payload})
1712
+ except ValidationError:
1713
+ return wrapper.model_construct(status=status, error=payload)
1714
+
1715
+ def list_users(
1716
+ self,
1717
+ ) -> list[types.User]:
1718
+ request_kwargs: dict[str, Any] = {}
1719
+ _request_url = self._url("/users", {})
1720
+ response = self._client.request(
1721
+ "GET",
1722
+ _request_url,
1723
+ headers=self._headers(),
1724
+ **request_kwargs,
1725
+ )
1726
+ if response.status_code == 200:
1727
+ return TypeAdapter(list[types.User]).validate_python(response.json())
1728
+ raise ListUsersApiError(response.status_code, None)
1729
+ "
1730
+ `;
1731
+
1732
+ exports[`openApiPyClientGenerator - arrays > should handle operation which returns an array of objects > types.py 1`] = `
1733
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
1734
+
1735
+ from __future__ import annotations
1736
+
1737
+ from typing import Never
1738
+
1739
+ from pydantic import BaseModel, ConfigDict
1740
+
1741
+
1742
+ class User(BaseModel):
1743
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
1744
+
1745
+ id: int
1746
+
1747
+
1748
+ ListUsersError = Never
1749
+ "
1750
+ `;