@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,1444 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`openApiPyClientGenerator - primitive types > should generate valid Python for primitive types > 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, ValidationError
15
+
16
+ from . import types
17
+ from .errors import (
18
+ ApiError as ApiError,
19
+ )
20
+ from .errors import (
21
+ ScalarsApiError as ScalarsApiError,
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 scalars(
316
+ self,
317
+ *,
318
+ s: str,
319
+ i: int,
320
+ b: bool,
321
+ f: float,
322
+ ) -> types.Scalars:
323
+ header_params: dict[str, Any] = {}
324
+ _body_fields: dict[str, Any] = {}
325
+ _body_fields["s"] = s
326
+ _body_fields["i"] = i
327
+ _body_fields["b"] = b
328
+ _body_fields["f"] = f
329
+ body = types.Scalars.model_validate(_body_fields).model_dump(
330
+ mode="json", by_alias=True, exclude_unset=True
331
+ )
332
+ request_kwargs: dict[str, Any] = {"json": body}
333
+ if not self._config.omit_content_type_header:
334
+ self._default_content_type(header_params, "application/json")
335
+ _request_url = self._url("/scalars", {})
336
+ response = self._client.request(
337
+ "POST",
338
+ _request_url,
339
+ headers=self._headers(header_params),
340
+ **request_kwargs,
341
+ )
342
+ if response.status_code == 200:
343
+ return types.Scalars.model_validate(response.json())
344
+ raise ScalarsApiError(response.status_code, None)
345
+ "
346
+ `;
347
+
348
+ exports[`openApiPyClientGenerator - primitive types > should generate valid Python for primitive types > types.py 1`] = `
349
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
350
+
351
+ from __future__ import annotations
352
+
353
+ from typing import Never, TypedDict
354
+
355
+ from pydantic import BaseModel, ConfigDict
356
+
357
+
358
+ class Scalars(BaseModel):
359
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
360
+
361
+ s: str
362
+ i: int
363
+ b: bool
364
+ f: float
365
+
366
+
367
+ class ScalarsRequest(TypedDict):
368
+ body: Scalars
369
+
370
+
371
+ ScalarsError = Never
372
+ "
373
+ `;
374
+
375
+ exports[`openApiPyClientGenerator - primitive types > should handle date and date-time formats > client.py 1`] = `
376
+ """"TestApi — AUTO-GENERATED synchronous client."""
377
+
378
+ from __future__ import annotations
379
+
380
+ import datetime
381
+ from dataclasses import dataclass, field
382
+ from typing import Any, TypeVar
383
+ from urllib.parse import quote
384
+
385
+ import httpx
386
+ from pydantic import BaseModel, ValidationError
387
+
388
+ from . import types
389
+ from .errors import (
390
+ ApiError as ApiError,
391
+ )
392
+ from .errors import (
393
+ EventApiError as EventApiError,
394
+ )
395
+
396
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
397
+
398
+
399
+ @dataclass
400
+ class TestApiConfig:
401
+ """Client configuration for TestApi."""
402
+
403
+ #: Base URL for the API.
404
+ url: str
405
+ #: Client used for every request. Supply your own to add authentication or
406
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
407
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
408
+ #: client's own headers and query params are sent alongside each operation's.
409
+ #: A supplied client is never closed by this one, so it can be shared.
410
+ httpx_client: httpx.Client | None = None
411
+ #: Headers added to every request, where the operation sets none itself.
412
+ headers: dict[str, str] = field(default_factory=dict)
413
+ #: By default the client sends a Content-Type header set to the media type
414
+ #: the OpenAPI specification declares for the request. Set this to omit it.
415
+ omit_content_type_header: bool = False
416
+
417
+
418
+ class TestApi:
419
+ """Synchronous API client for TestApi."""
420
+
421
+ def __init__(self, config: TestApiConfig) -> None:
422
+ self._config = config
423
+ self._client = config.httpx_client or httpx.Client()
424
+ self._owns_client = config.httpx_client is None
425
+ self._base_url = config.url.rstrip("/")
426
+
427
+ def __enter__(self) -> TestApi:
428
+ return self
429
+
430
+ def __exit__(self, *exc: Any) -> None:
431
+ self.close()
432
+
433
+ def close(self) -> None:
434
+ if self._owns_client:
435
+ self._client.close()
436
+
437
+ def _url(
438
+ self,
439
+ path: str,
440
+ path_params: dict[str, Any],
441
+ path_styles: dict[str, tuple[str, bool]] | None = None,
442
+ ) -> str:
443
+ for key, value in path_params.items():
444
+ style, explode = (path_styles or {}).get(key, ("simple", False))
445
+ # An object expands to its key,value members; a list to its items.
446
+ # Each is encoded on its own so the separators between them stay
447
+ # literal, as RFC 6570 requires.
448
+ dumped = self._dump(value)
449
+ if isinstance(dumped, dict):
450
+ values = [
451
+ part
452
+ for k, v in dumped.items()
453
+ if v is not None
454
+ for part in (k, self._scalar(v))
455
+ ]
456
+ elif isinstance(dumped, list):
457
+ values = list(dumped)
458
+ else:
459
+ values = [value]
460
+ if style == "matrix":
461
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
462
+ if explode:
463
+ rendered = "".join(
464
+ ";" + key + "=" + quote(self._scalar(v), safe="")
465
+ for v in values
466
+ )
467
+ else:
468
+ rendered = (
469
+ ";"
470
+ + key
471
+ + "="
472
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
473
+ )
474
+ elif style == "label":
475
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
476
+ rendered = "." + ("." if explode else ",").join(
477
+ quote(self._scalar(v), safe="") for v in values
478
+ )
479
+ else:
480
+ # RFC 6570 simple expansion: a list renders comma-separated.
481
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
482
+ path = path.replace("{" + key + "}", rendered)
483
+ return self._base_url + path
484
+
485
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
486
+ if isinstance(value, BaseModel):
487
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
488
+ if isinstance(value, dict):
489
+ for prop, v in value.items():
490
+ self._deep_object(f"{key}[{prop}]", v, out)
491
+ elif value is not None:
492
+ out[key] = value
493
+
494
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
495
+
496
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
497
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
498
+ #: too — only the first one separates a cookie's name from its value.
499
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
500
+
501
+ def _scalar(self, value: Any) -> str:
502
+ # The wire form of a single parameter value: JSON spellings for bools,
503
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
504
+ if isinstance(value, bool):
505
+ return "true" if value else "false"
506
+ if isinstance(value, (datetime.date, datetime.datetime)):
507
+ return value.isoformat()
508
+ dumped = self._dump(value)
509
+ if isinstance(dumped, str):
510
+ return dumped
511
+ # RFC 6570 renders a list as its comma-joined members and an object as
512
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
513
+ # \`['a', 'b']\`, quotes and spaces included.
514
+ if isinstance(dumped, list):
515
+ return ",".join(self._scalar(v) for v in dumped)
516
+ if isinstance(dumped, dict):
517
+ return ",".join(
518
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
519
+ )
520
+ if dumped is None:
521
+ return ""
522
+ return str(dumped)
523
+
524
+ def _query(
525
+ self,
526
+ query_params: dict[str, Any],
527
+ collection_formats: dict[str, str] | None = None,
528
+ ) -> dict[str, Any]:
529
+ out: dict[str, Any] = {}
530
+ for key, value in query_params.items():
531
+ if value is None:
532
+ continue
533
+ fmt = (collection_formats or {}).get(key)
534
+ if fmt == "deepObject":
535
+ self._deep_object(key, value, out)
536
+ elif isinstance(value, list):
537
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
538
+ if delimiter is None:
539
+ out[key] = [self._scalar(v) for v in value]
540
+ else:
541
+ out[key] = delimiter.join(self._scalar(v) for v in value)
542
+ elif self._is_object(value):
543
+ # OpenAPI's default query style is form/explode, under which an
544
+ # object expands to one parameter per property rather than
545
+ # being stringified.
546
+ for prop, prop_value in self._as_dict(value).items():
547
+ if prop_value is not None:
548
+ out[prop] = self._scalar(prop_value)
549
+ else:
550
+ out[key] = self._scalar(value)
551
+ return out
552
+
553
+ def _is_object(self, value: Any) -> bool:
554
+ return isinstance(value, (dict, BaseModel))
555
+
556
+ def _as_dict(self, value: Any) -> dict[str, Any]:
557
+ if isinstance(value, BaseModel):
558
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
559
+ return dict(value)
560
+
561
+ def _headers(
562
+ self,
563
+ header_params: dict[str, Any] | None = None,
564
+ collection_formats: dict[str, str] | None = None,
565
+ cookie_params: dict[str, Any] | None = None,
566
+ ) -> list[tuple[str, str]]:
567
+ out: list[tuple[str, str]] = []
568
+ for key, value in (header_params or {}).items():
569
+ if value is None:
570
+ continue
571
+ if isinstance(value, list):
572
+ fmt = (collection_formats or {}).get(key, "csv")
573
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
574
+ if delimiter is None:
575
+ for item in value:
576
+ out.append((key, self._scalar(item)))
577
+ else:
578
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
579
+ else:
580
+ out.append((key, self._scalar(value)))
581
+ # A config header only applies where the operation didn't set one:
582
+ # httpx comma-joins repeated names, which would corrupt the value.
583
+ operation_headers = {key.lower() for key, _ in out}
584
+ out.extend(
585
+ (key, value)
586
+ for key, value in self._config.headers.items()
587
+ if key.lower() not in operation_headers
588
+ )
589
+ # The operation's cookies are merged into one \`Cookie\` header alongside
590
+ # every other source. Passing them to httpx's \`cookies=\` instead would
591
+ # drop them outright whenever a \`Cookie\` header is present, since
592
+ # http.cookiejar skips a request that already carries one — and setting
593
+ # the header here would in turn shadow the caller's own jar and header,
594
+ # which httpx resolves in favour of the more specific value.
595
+ cookies = self._cookies(cookie_params or {})
596
+ if cookies:
597
+ pairs = [
598
+ *(
599
+ value
600
+ for source in (self._client.headers, dict(out))
601
+ for key, value in source.items()
602
+ if key.lower() == "cookie"
603
+ ),
604
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
605
+ *(f"{key}={value}" for key, value in cookies.items()),
606
+ ]
607
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
608
+ out.append(("Cookie", "; ".join(pairs)))
609
+ return out
610
+
611
+ def _default_content_type(
612
+ self, header_params: dict[str, Any], media_type: str
613
+ ) -> None:
614
+ # The media type the specification declares, applied unless the operation
615
+ # takes a Content-Type of its own and the caller supplied one. Header
616
+ # names are case-insensitive, and a declared header the caller omitted is
617
+ # present holding \`None\` — neither counts as the caller setting it.
618
+ if any(
619
+ key.lower() == "content-type" and value is not None
620
+ for key, value in header_params.items()
621
+ ):
622
+ return
623
+ for key in list(header_params):
624
+ if key.lower() == "content-type":
625
+ header_params[key] = media_type
626
+ return
627
+ header_params["Content-Type"] = media_type
628
+
629
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
630
+ out: dict[str, str] = {}
631
+ for key, value in cookie_params.items():
632
+ if value is None:
633
+ continue
634
+ dumped = self._dump(value)
635
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
636
+ # object on \`,\` between each key and value. Members are encoded so a
637
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
638
+ # joining commas stay literal.
639
+ if isinstance(dumped, list):
640
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
641
+ elif isinstance(dumped, dict):
642
+ rendered = ",".join(
643
+ self._cookie_value(k) + "," + self._cookie_value(v)
644
+ for k, v in dumped.items()
645
+ if v is not None
646
+ )
647
+ else:
648
+ rendered = self._cookie_value(value)
649
+ out[key] = rendered
650
+ return out
651
+
652
+ def _cookie_value(self, value: Any) -> str:
653
+ # Only the characters a cookie parser treats as structure are escaped, so
654
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
655
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
656
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
657
+
658
+ def _dump(self, value: Any) -> Any:
659
+ if value is None:
660
+ return None
661
+ if isinstance(value, BaseModel):
662
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
663
+ if isinstance(value, (list, tuple)):
664
+ return [self._dump(v) for v in value]
665
+ if isinstance(value, dict):
666
+ return {k: self._dump(v) for k, v in value.items()}
667
+ if isinstance(value, (datetime.date, datetime.datetime)):
668
+ return value.isoformat()
669
+ return value
670
+
671
+ def _error_payload(self, response: httpx.Response) -> Any:
672
+ # An error body is whatever the server actually sent, which need not be
673
+ # the JSON the spec declares (a proxy's HTML page, say).
674
+ try:
675
+ return response.json()
676
+ except ValueError:
677
+ return response.text
678
+
679
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
680
+ # Coerce to the declared error type, falling back to the raw payload so
681
+ # a body that doesn't match the spec still raises the typed exception.
682
+ try:
683
+ return wrapper.model_validate({"status": status, "error": payload})
684
+ except ValidationError:
685
+ return wrapper.model_construct(status=status, error=payload)
686
+
687
+ def event(
688
+ self,
689
+ *,
690
+ day: datetime.date,
691
+ moment: datetime.datetime,
692
+ ) -> types.Event:
693
+ header_params: dict[str, Any] = {}
694
+ _body_fields: dict[str, Any] = {}
695
+ _body_fields["day"] = day
696
+ _body_fields["moment"] = moment
697
+ body = types.Event.model_validate(_body_fields).model_dump(
698
+ mode="json", by_alias=True, exclude_unset=True
699
+ )
700
+ request_kwargs: dict[str, Any] = {"json": body}
701
+ if not self._config.omit_content_type_header:
702
+ self._default_content_type(header_params, "application/json")
703
+ _request_url = self._url("/event", {})
704
+ response = self._client.request(
705
+ "POST",
706
+ _request_url,
707
+ headers=self._headers(header_params),
708
+ **request_kwargs,
709
+ )
710
+ if response.status_code == 200:
711
+ return types.Event.model_validate(response.json())
712
+ raise EventApiError(response.status_code, None)
713
+ "
714
+ `;
715
+
716
+ exports[`openApiPyClientGenerator - primitive types > should handle date and date-time formats > types.py 1`] = `
717
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
718
+
719
+ from __future__ import annotations
720
+
721
+ import datetime
722
+ from typing import Never, TypedDict
723
+
724
+ from pydantic import BaseModel, ConfigDict
725
+
726
+
727
+ class Event(BaseModel):
728
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
729
+
730
+ day: datetime.date
731
+ moment: datetime.datetime
732
+
733
+
734
+ class EventRequest(TypedDict):
735
+ body: Event
736
+
737
+
738
+ EventError = Never
739
+ "
740
+ `;
741
+
742
+ exports[`openApiPyClientGenerator - primitive types > should handle enum request and response bodies > client.py 1`] = `
743
+ """"TestApi — AUTO-GENERATED synchronous client."""
744
+
745
+ from __future__ import annotations
746
+
747
+ import datetime
748
+ from dataclasses import dataclass, field
749
+ from typing import Any, TypeVar
750
+ from urllib.parse import quote
751
+
752
+ import httpx
753
+ from pydantic import BaseModel, TypeAdapter, ValidationError
754
+
755
+ from . import types
756
+ from .errors import (
757
+ ApiError as ApiError,
758
+ )
759
+ from .errors import (
760
+ SetStatusApiError as SetStatusApiError,
761
+ )
762
+
763
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
764
+
765
+
766
+ @dataclass
767
+ class TestApiConfig:
768
+ """Client configuration for TestApi."""
769
+
770
+ #: Base URL for the API.
771
+ url: str
772
+ #: Client used for every request. Supply your own to add authentication or
773
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
774
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
775
+ #: client's own headers and query params are sent alongside each operation's.
776
+ #: A supplied client is never closed by this one, so it can be shared.
777
+ httpx_client: httpx.Client | None = None
778
+ #: Headers added to every request, where the operation sets none itself.
779
+ headers: dict[str, str] = field(default_factory=dict)
780
+ #: By default the client sends a Content-Type header set to the media type
781
+ #: the OpenAPI specification declares for the request. Set this to omit it.
782
+ omit_content_type_header: bool = False
783
+
784
+
785
+ class TestApi:
786
+ """Synchronous API client for TestApi."""
787
+
788
+ def __init__(self, config: TestApiConfig) -> None:
789
+ self._config = config
790
+ self._client = config.httpx_client or httpx.Client()
791
+ self._owns_client = config.httpx_client is None
792
+ self._base_url = config.url.rstrip("/")
793
+
794
+ def __enter__(self) -> TestApi:
795
+ return self
796
+
797
+ def __exit__(self, *exc: Any) -> None:
798
+ self.close()
799
+
800
+ def close(self) -> None:
801
+ if self._owns_client:
802
+ self._client.close()
803
+
804
+ def _url(
805
+ self,
806
+ path: str,
807
+ path_params: dict[str, Any],
808
+ path_styles: dict[str, tuple[str, bool]] | None = None,
809
+ ) -> str:
810
+ for key, value in path_params.items():
811
+ style, explode = (path_styles or {}).get(key, ("simple", False))
812
+ # An object expands to its key,value members; a list to its items.
813
+ # Each is encoded on its own so the separators between them stay
814
+ # literal, as RFC 6570 requires.
815
+ dumped = self._dump(value)
816
+ if isinstance(dumped, dict):
817
+ values = [
818
+ part
819
+ for k, v in dumped.items()
820
+ if v is not None
821
+ for part in (k, self._scalar(v))
822
+ ]
823
+ elif isinstance(dumped, list):
824
+ values = list(dumped)
825
+ else:
826
+ values = [value]
827
+ if style == "matrix":
828
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
829
+ if explode:
830
+ rendered = "".join(
831
+ ";" + key + "=" + quote(self._scalar(v), safe="")
832
+ for v in values
833
+ )
834
+ else:
835
+ rendered = (
836
+ ";"
837
+ + key
838
+ + "="
839
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
840
+ )
841
+ elif style == "label":
842
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
843
+ rendered = "." + ("." if explode else ",").join(
844
+ quote(self._scalar(v), safe="") for v in values
845
+ )
846
+ else:
847
+ # RFC 6570 simple expansion: a list renders comma-separated.
848
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
849
+ path = path.replace("{" + key + "}", rendered)
850
+ return self._base_url + path
851
+
852
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
853
+ if isinstance(value, BaseModel):
854
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
855
+ if isinstance(value, dict):
856
+ for prop, v in value.items():
857
+ self._deep_object(f"{key}[{prop}]", v, out)
858
+ elif value is not None:
859
+ out[key] = value
860
+
861
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
862
+
863
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
864
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
865
+ #: too — only the first one separates a cookie's name from its value.
866
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
867
+
868
+ def _scalar(self, value: Any) -> str:
869
+ # The wire form of a single parameter value: JSON spellings for bools,
870
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
871
+ if isinstance(value, bool):
872
+ return "true" if value else "false"
873
+ if isinstance(value, (datetime.date, datetime.datetime)):
874
+ return value.isoformat()
875
+ dumped = self._dump(value)
876
+ if isinstance(dumped, str):
877
+ return dumped
878
+ # RFC 6570 renders a list as its comma-joined members and an object as
879
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
880
+ # \`['a', 'b']\`, quotes and spaces included.
881
+ if isinstance(dumped, list):
882
+ return ",".join(self._scalar(v) for v in dumped)
883
+ if isinstance(dumped, dict):
884
+ return ",".join(
885
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
886
+ )
887
+ if dumped is None:
888
+ return ""
889
+ return str(dumped)
890
+
891
+ def _query(
892
+ self,
893
+ query_params: dict[str, Any],
894
+ collection_formats: dict[str, str] | None = None,
895
+ ) -> dict[str, Any]:
896
+ out: dict[str, Any] = {}
897
+ for key, value in query_params.items():
898
+ if value is None:
899
+ continue
900
+ fmt = (collection_formats or {}).get(key)
901
+ if fmt == "deepObject":
902
+ self._deep_object(key, value, out)
903
+ elif isinstance(value, list):
904
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
905
+ if delimiter is None:
906
+ out[key] = [self._scalar(v) for v in value]
907
+ else:
908
+ out[key] = delimiter.join(self._scalar(v) for v in value)
909
+ elif self._is_object(value):
910
+ # OpenAPI's default query style is form/explode, under which an
911
+ # object expands to one parameter per property rather than
912
+ # being stringified.
913
+ for prop, prop_value in self._as_dict(value).items():
914
+ if prop_value is not None:
915
+ out[prop] = self._scalar(prop_value)
916
+ else:
917
+ out[key] = self._scalar(value)
918
+ return out
919
+
920
+ def _is_object(self, value: Any) -> bool:
921
+ return isinstance(value, (dict, BaseModel))
922
+
923
+ def _as_dict(self, value: Any) -> dict[str, Any]:
924
+ if isinstance(value, BaseModel):
925
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
926
+ return dict(value)
927
+
928
+ def _headers(
929
+ self,
930
+ header_params: dict[str, Any] | None = None,
931
+ collection_formats: dict[str, str] | None = None,
932
+ cookie_params: dict[str, Any] | None = None,
933
+ ) -> list[tuple[str, str]]:
934
+ out: list[tuple[str, str]] = []
935
+ for key, value in (header_params or {}).items():
936
+ if value is None:
937
+ continue
938
+ if isinstance(value, list):
939
+ fmt = (collection_formats or {}).get(key, "csv")
940
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
941
+ if delimiter is None:
942
+ for item in value:
943
+ out.append((key, self._scalar(item)))
944
+ else:
945
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
946
+ else:
947
+ out.append((key, self._scalar(value)))
948
+ # A config header only applies where the operation didn't set one:
949
+ # httpx comma-joins repeated names, which would corrupt the value.
950
+ operation_headers = {key.lower() for key, _ in out}
951
+ out.extend(
952
+ (key, value)
953
+ for key, value in self._config.headers.items()
954
+ if key.lower() not in operation_headers
955
+ )
956
+ # The operation's cookies are merged into one \`Cookie\` header alongside
957
+ # every other source. Passing them to httpx's \`cookies=\` instead would
958
+ # drop them outright whenever a \`Cookie\` header is present, since
959
+ # http.cookiejar skips a request that already carries one — and setting
960
+ # the header here would in turn shadow the caller's own jar and header,
961
+ # which httpx resolves in favour of the more specific value.
962
+ cookies = self._cookies(cookie_params or {})
963
+ if cookies:
964
+ pairs = [
965
+ *(
966
+ value
967
+ for source in (self._client.headers, dict(out))
968
+ for key, value in source.items()
969
+ if key.lower() == "cookie"
970
+ ),
971
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
972
+ *(f"{key}={value}" for key, value in cookies.items()),
973
+ ]
974
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
975
+ out.append(("Cookie", "; ".join(pairs)))
976
+ return out
977
+
978
+ def _default_content_type(
979
+ self, header_params: dict[str, Any], media_type: str
980
+ ) -> None:
981
+ # The media type the specification declares, applied unless the operation
982
+ # takes a Content-Type of its own and the caller supplied one. Header
983
+ # names are case-insensitive, and a declared header the caller omitted is
984
+ # present holding \`None\` — neither counts as the caller setting it.
985
+ if any(
986
+ key.lower() == "content-type" and value is not None
987
+ for key, value in header_params.items()
988
+ ):
989
+ return
990
+ for key in list(header_params):
991
+ if key.lower() == "content-type":
992
+ header_params[key] = media_type
993
+ return
994
+ header_params["Content-Type"] = media_type
995
+
996
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
997
+ out: dict[str, str] = {}
998
+ for key, value in cookie_params.items():
999
+ if value is None:
1000
+ continue
1001
+ dumped = self._dump(value)
1002
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
1003
+ # object on \`,\` between each key and value. Members are encoded so a
1004
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
1005
+ # joining commas stay literal.
1006
+ if isinstance(dumped, list):
1007
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
1008
+ elif isinstance(dumped, dict):
1009
+ rendered = ",".join(
1010
+ self._cookie_value(k) + "," + self._cookie_value(v)
1011
+ for k, v in dumped.items()
1012
+ if v is not None
1013
+ )
1014
+ else:
1015
+ rendered = self._cookie_value(value)
1016
+ out[key] = rendered
1017
+ return out
1018
+
1019
+ def _cookie_value(self, value: Any) -> str:
1020
+ # Only the characters a cookie parser treats as structure are escaped, so
1021
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
1022
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
1023
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
1024
+
1025
+ def _dump(self, value: Any) -> Any:
1026
+ if value is None:
1027
+ return None
1028
+ if isinstance(value, BaseModel):
1029
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1030
+ if isinstance(value, (list, tuple)):
1031
+ return [self._dump(v) for v in value]
1032
+ if isinstance(value, dict):
1033
+ return {k: self._dump(v) for k, v in value.items()}
1034
+ if isinstance(value, (datetime.date, datetime.datetime)):
1035
+ return value.isoformat()
1036
+ return value
1037
+
1038
+ def _error_payload(self, response: httpx.Response) -> Any:
1039
+ # An error body is whatever the server actually sent, which need not be
1040
+ # the JSON the spec declares (a proxy's HTML page, say).
1041
+ try:
1042
+ return response.json()
1043
+ except ValueError:
1044
+ return response.text
1045
+
1046
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
1047
+ # Coerce to the declared error type, falling back to the raw payload so
1048
+ # a body that doesn't match the spec still raises the typed exception.
1049
+ try:
1050
+ return wrapper.model_validate({"status": status, "error": payload})
1051
+ except ValidationError:
1052
+ return wrapper.model_construct(status=status, error=payload)
1053
+
1054
+ def set_status(
1055
+ self,
1056
+ body: types.Status,
1057
+ ) -> types.Status:
1058
+ header_params: dict[str, Any] = {}
1059
+ body = self._dump(body)
1060
+ request_kwargs: dict[str, Any] = {"json": body}
1061
+ if body is not None and not self._config.omit_content_type_header:
1062
+ self._default_content_type(header_params, "application/json")
1063
+ _request_url = self._url("/status", {})
1064
+ response = self._client.request(
1065
+ "POST",
1066
+ _request_url,
1067
+ headers=self._headers(header_params),
1068
+ **request_kwargs,
1069
+ )
1070
+ if response.status_code == 200:
1071
+ return TypeAdapter(types.Status).validate_python(response.json())
1072
+ raise SetStatusApiError(response.status_code, None)
1073
+ "
1074
+ `;
1075
+
1076
+ exports[`openApiPyClientGenerator - primitive types > should handle enum request and response bodies > types.py 1`] = `
1077
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
1078
+
1079
+ from __future__ import annotations
1080
+
1081
+ from typing import Literal, Never, TypedDict
1082
+
1083
+ Status = Literal["placed", "approved", "delivered"]
1084
+
1085
+
1086
+ class SetStatusRequest(TypedDict):
1087
+ body: Status
1088
+
1089
+
1090
+ SetStatusError = Never
1091
+ "
1092
+ `;
1093
+
1094
+ exports[`openApiPyClientGenerator - primitive types > should handle inline integer enums on properties > types.py 1`] = `
1095
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
1096
+
1097
+ from __future__ import annotations
1098
+
1099
+ from typing import Literal, Never, TypedDict
1100
+
1101
+ from pydantic import BaseModel, ConfigDict
1102
+
1103
+
1104
+ class Holder(BaseModel):
1105
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
1106
+
1107
+ p: Literal[1, 2, 3]
1108
+
1109
+
1110
+ class PriorityRequest(TypedDict):
1111
+ body: Holder
1112
+
1113
+
1114
+ PriorityError = Never
1115
+ "
1116
+ `;
1117
+
1118
+ exports[`openApiPyClientGenerator - primitive types > should handle primitive scalar response types (text) > client.py 1`] = `
1119
+ """"TestApi — AUTO-GENERATED synchronous client."""
1120
+
1121
+ from __future__ import annotations
1122
+
1123
+ import datetime
1124
+ from dataclasses import dataclass, field
1125
+ from typing import Any, TypeVar
1126
+ from urllib.parse import quote
1127
+
1128
+ import httpx
1129
+ from pydantic import BaseModel, TypeAdapter, ValidationError
1130
+
1131
+ from .errors import (
1132
+ ApiError as ApiError,
1133
+ )
1134
+ from .errors import (
1135
+ GreetApiError as GreetApiError,
1136
+ )
1137
+
1138
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
1139
+
1140
+
1141
+ @dataclass
1142
+ class TestApiConfig:
1143
+ """Client configuration for TestApi."""
1144
+
1145
+ #: Base URL for the API.
1146
+ url: str
1147
+ #: Client used for every request. Supply your own to add authentication or
1148
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
1149
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
1150
+ #: client's own headers and query params are sent alongside each operation's.
1151
+ #: A supplied client is never closed by this one, so it can be shared.
1152
+ httpx_client: httpx.Client | None = None
1153
+ #: Headers added to every request, where the operation sets none itself.
1154
+ headers: dict[str, str] = field(default_factory=dict)
1155
+ #: By default the client sends a Content-Type header set to the media type
1156
+ #: the OpenAPI specification declares for the request. Set this to omit it.
1157
+ omit_content_type_header: bool = False
1158
+
1159
+
1160
+ class TestApi:
1161
+ """Synchronous API client for TestApi."""
1162
+
1163
+ def __init__(self, config: TestApiConfig) -> None:
1164
+ self._config = config
1165
+ self._client = config.httpx_client or httpx.Client()
1166
+ self._owns_client = config.httpx_client is None
1167
+ self._base_url = config.url.rstrip("/")
1168
+
1169
+ def __enter__(self) -> TestApi:
1170
+ return self
1171
+
1172
+ def __exit__(self, *exc: Any) -> None:
1173
+ self.close()
1174
+
1175
+ def close(self) -> None:
1176
+ if self._owns_client:
1177
+ self._client.close()
1178
+
1179
+ def _url(
1180
+ self,
1181
+ path: str,
1182
+ path_params: dict[str, Any],
1183
+ path_styles: dict[str, tuple[str, bool]] | None = None,
1184
+ ) -> str:
1185
+ for key, value in path_params.items():
1186
+ style, explode = (path_styles or {}).get(key, ("simple", False))
1187
+ # An object expands to its key,value members; a list to its items.
1188
+ # Each is encoded on its own so the separators between them stay
1189
+ # literal, as RFC 6570 requires.
1190
+ dumped = self._dump(value)
1191
+ if isinstance(dumped, dict):
1192
+ values = [
1193
+ part
1194
+ for k, v in dumped.items()
1195
+ if v is not None
1196
+ for part in (k, self._scalar(v))
1197
+ ]
1198
+ elif isinstance(dumped, list):
1199
+ values = list(dumped)
1200
+ else:
1201
+ values = [value]
1202
+ if style == "matrix":
1203
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
1204
+ if explode:
1205
+ rendered = "".join(
1206
+ ";" + key + "=" + quote(self._scalar(v), safe="")
1207
+ for v in values
1208
+ )
1209
+ else:
1210
+ rendered = (
1211
+ ";"
1212
+ + key
1213
+ + "="
1214
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
1215
+ )
1216
+ elif style == "label":
1217
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
1218
+ rendered = "." + ("." if explode else ",").join(
1219
+ quote(self._scalar(v), safe="") for v in values
1220
+ )
1221
+ else:
1222
+ # RFC 6570 simple expansion: a list renders comma-separated.
1223
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
1224
+ path = path.replace("{" + key + "}", rendered)
1225
+ return self._base_url + path
1226
+
1227
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
1228
+ if isinstance(value, BaseModel):
1229
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1230
+ if isinstance(value, dict):
1231
+ for prop, v in value.items():
1232
+ self._deep_object(f"{key}[{prop}]", v, out)
1233
+ elif value is not None:
1234
+ out[key] = value
1235
+
1236
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
1237
+
1238
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
1239
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
1240
+ #: too — only the first one separates a cookie's name from its value.
1241
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
1242
+
1243
+ def _scalar(self, value: Any) -> str:
1244
+ # The wire form of a single parameter value: JSON spellings for bools,
1245
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
1246
+ if isinstance(value, bool):
1247
+ return "true" if value else "false"
1248
+ if isinstance(value, (datetime.date, datetime.datetime)):
1249
+ return value.isoformat()
1250
+ dumped = self._dump(value)
1251
+ if isinstance(dumped, str):
1252
+ return dumped
1253
+ # RFC 6570 renders a list as its comma-joined members and an object as
1254
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
1255
+ # \`['a', 'b']\`, quotes and spaces included.
1256
+ if isinstance(dumped, list):
1257
+ return ",".join(self._scalar(v) for v in dumped)
1258
+ if isinstance(dumped, dict):
1259
+ return ",".join(
1260
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
1261
+ )
1262
+ if dumped is None:
1263
+ return ""
1264
+ return str(dumped)
1265
+
1266
+ def _query(
1267
+ self,
1268
+ query_params: dict[str, Any],
1269
+ collection_formats: dict[str, str] | None = None,
1270
+ ) -> dict[str, Any]:
1271
+ out: dict[str, Any] = {}
1272
+ for key, value in query_params.items():
1273
+ if value is None:
1274
+ continue
1275
+ fmt = (collection_formats or {}).get(key)
1276
+ if fmt == "deepObject":
1277
+ self._deep_object(key, value, out)
1278
+ elif isinstance(value, list):
1279
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
1280
+ if delimiter is None:
1281
+ out[key] = [self._scalar(v) for v in value]
1282
+ else:
1283
+ out[key] = delimiter.join(self._scalar(v) for v in value)
1284
+ elif self._is_object(value):
1285
+ # OpenAPI's default query style is form/explode, under which an
1286
+ # object expands to one parameter per property rather than
1287
+ # being stringified.
1288
+ for prop, prop_value in self._as_dict(value).items():
1289
+ if prop_value is not None:
1290
+ out[prop] = self._scalar(prop_value)
1291
+ else:
1292
+ out[key] = self._scalar(value)
1293
+ return out
1294
+
1295
+ def _is_object(self, value: Any) -> bool:
1296
+ return isinstance(value, (dict, BaseModel))
1297
+
1298
+ def _as_dict(self, value: Any) -> dict[str, Any]:
1299
+ if isinstance(value, BaseModel):
1300
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1301
+ return dict(value)
1302
+
1303
+ def _headers(
1304
+ self,
1305
+ header_params: dict[str, Any] | None = None,
1306
+ collection_formats: dict[str, str] | None = None,
1307
+ cookie_params: dict[str, Any] | None = None,
1308
+ ) -> list[tuple[str, str]]:
1309
+ out: list[tuple[str, str]] = []
1310
+ for key, value in (header_params or {}).items():
1311
+ if value is None:
1312
+ continue
1313
+ if isinstance(value, list):
1314
+ fmt = (collection_formats or {}).get(key, "csv")
1315
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
1316
+ if delimiter is None:
1317
+ for item in value:
1318
+ out.append((key, self._scalar(item)))
1319
+ else:
1320
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
1321
+ else:
1322
+ out.append((key, self._scalar(value)))
1323
+ # A config header only applies where the operation didn't set one:
1324
+ # httpx comma-joins repeated names, which would corrupt the value.
1325
+ operation_headers = {key.lower() for key, _ in out}
1326
+ out.extend(
1327
+ (key, value)
1328
+ for key, value in self._config.headers.items()
1329
+ if key.lower() not in operation_headers
1330
+ )
1331
+ # The operation's cookies are merged into one \`Cookie\` header alongside
1332
+ # every other source. Passing them to httpx's \`cookies=\` instead would
1333
+ # drop them outright whenever a \`Cookie\` header is present, since
1334
+ # http.cookiejar skips a request that already carries one — and setting
1335
+ # the header here would in turn shadow the caller's own jar and header,
1336
+ # which httpx resolves in favour of the more specific value.
1337
+ cookies = self._cookies(cookie_params or {})
1338
+ if cookies:
1339
+ pairs = [
1340
+ *(
1341
+ value
1342
+ for source in (self._client.headers, dict(out))
1343
+ for key, value in source.items()
1344
+ if key.lower() == "cookie"
1345
+ ),
1346
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
1347
+ *(f"{key}={value}" for key, value in cookies.items()),
1348
+ ]
1349
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
1350
+ out.append(("Cookie", "; ".join(pairs)))
1351
+ return out
1352
+
1353
+ def _default_content_type(
1354
+ self, header_params: dict[str, Any], media_type: str
1355
+ ) -> None:
1356
+ # The media type the specification declares, applied unless the operation
1357
+ # takes a Content-Type of its own and the caller supplied one. Header
1358
+ # names are case-insensitive, and a declared header the caller omitted is
1359
+ # present holding \`None\` — neither counts as the caller setting it.
1360
+ if any(
1361
+ key.lower() == "content-type" and value is not None
1362
+ for key, value in header_params.items()
1363
+ ):
1364
+ return
1365
+ for key in list(header_params):
1366
+ if key.lower() == "content-type":
1367
+ header_params[key] = media_type
1368
+ return
1369
+ header_params["Content-Type"] = media_type
1370
+
1371
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
1372
+ out: dict[str, str] = {}
1373
+ for key, value in cookie_params.items():
1374
+ if value is None:
1375
+ continue
1376
+ dumped = self._dump(value)
1377
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
1378
+ # object on \`,\` between each key and value. Members are encoded so a
1379
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
1380
+ # joining commas stay literal.
1381
+ if isinstance(dumped, list):
1382
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
1383
+ elif isinstance(dumped, dict):
1384
+ rendered = ",".join(
1385
+ self._cookie_value(k) + "," + self._cookie_value(v)
1386
+ for k, v in dumped.items()
1387
+ if v is not None
1388
+ )
1389
+ else:
1390
+ rendered = self._cookie_value(value)
1391
+ out[key] = rendered
1392
+ return out
1393
+
1394
+ def _cookie_value(self, value: Any) -> str:
1395
+ # Only the characters a cookie parser treats as structure are escaped, so
1396
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
1397
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
1398
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
1399
+
1400
+ def _dump(self, value: Any) -> Any:
1401
+ if value is None:
1402
+ return None
1403
+ if isinstance(value, BaseModel):
1404
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
1405
+ if isinstance(value, (list, tuple)):
1406
+ return [self._dump(v) for v in value]
1407
+ if isinstance(value, dict):
1408
+ return {k: self._dump(v) for k, v in value.items()}
1409
+ if isinstance(value, (datetime.date, datetime.datetime)):
1410
+ return value.isoformat()
1411
+ return value
1412
+
1413
+ def _error_payload(self, response: httpx.Response) -> Any:
1414
+ # An error body is whatever the server actually sent, which need not be
1415
+ # the JSON the spec declares (a proxy's HTML page, say).
1416
+ try:
1417
+ return response.json()
1418
+ except ValueError:
1419
+ return response.text
1420
+
1421
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
1422
+ # Coerce to the declared error type, falling back to the raw payload so
1423
+ # a body that doesn't match the spec still raises the typed exception.
1424
+ try:
1425
+ return wrapper.model_validate({"status": status, "error": payload})
1426
+ except ValidationError:
1427
+ return wrapper.model_construct(status=status, error=payload)
1428
+
1429
+ def greet(
1430
+ self,
1431
+ ) -> str:
1432
+ request_kwargs: dict[str, Any] = {}
1433
+ _request_url = self._url("/greet", {})
1434
+ response = self._client.request(
1435
+ "GET",
1436
+ _request_url,
1437
+ headers=self._headers(),
1438
+ **request_kwargs,
1439
+ )
1440
+ if response.status_code == 200:
1441
+ return TypeAdapter(str).validate_strings(response.text.strip())
1442
+ raise GreetApiError(response.status_code, None)
1443
+ "
1444
+ `;