@aws/nx-plugin 1.0.0-rc.98 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) 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 +34 -1
  5. package/package.json +6 -6
  6. package/src/agentcore-gateway/schema.json +1 -0
  7. package/src/agentcore-harness/generator.d.ts +1 -1
  8. package/src/connection/scaffold-catalog.d.ts +4 -0
  9. package/src/connection/scaffold-catalog.js +3 -6
  10. package/src/connection/scaffold-catalog.js.map +1 -1
  11. package/src/infra/app/generator.d.ts +1 -1
  12. package/src/init/generator.d.ts +1 -1
  13. package/src/mcp-server/guide-pipeline.d.ts +2 -1
  14. package/src/mcp-server/guide-pipeline.js +6 -0
  15. package/src/mcp-server/guide-pipeline.js.map +1 -1
  16. package/src/mcp-server/mdx-ast.d.ts +7 -0
  17. package/src/mcp-server/mdx-ast.js +15 -0
  18. package/src/mcp-server/mdx-ast.js.map +1 -1
  19. package/src/mcp-server/schema-registry.d.ts +2 -2
  20. package/src/mcp-server/schema-registry.js +2 -2
  21. package/src/mcp-server/schema-registry.js.map +1 -1
  22. package/src/open-api/py-client/__snapshots__/generator.additional-properties.spec.ts.snap +34 -0
  23. package/src/open-api/py-client/__snapshots__/generator.arrays.spec.ts.snap +1750 -0
  24. package/src/open-api/py-client/__snapshots__/generator.complex-types.spec.ts.snap +82 -0
  25. package/src/open-api/py-client/__snapshots__/generator.composite-types.spec.ts.snap +447 -0
  26. package/src/open-api/py-client/__snapshots__/generator.duplicate-types.spec.ts.snap +404 -0
  27. package/src/open-api/py-client/__snapshots__/generator.errors.spec.ts.snap +430 -0
  28. package/src/open-api/py-client/__snapshots__/generator.fast-api.spec.ts.snap +447 -0
  29. package/src/open-api/py-client/__snapshots__/generator.petstore.spec.ts.snap +3488 -0
  30. package/src/open-api/py-client/__snapshots__/generator.primitive-types.spec.ts.snap +1444 -0
  31. package/src/open-api/py-client/__snapshots__/generator.request.spec.ts.snap +390 -0
  32. package/src/open-api/py-client/__snapshots__/generator.reserved-keywords.spec.ts.snap +364 -0
  33. package/src/open-api/py-client/__snapshots__/generator.response.spec.ts.snap +739 -0
  34. package/src/open-api/py-client/__snapshots__/generator.streaming.spec.ts.snap +384 -0
  35. package/src/open-api/py-client/__snapshots__/generator.tags.spec.ts.snap +414 -0
  36. package/src/open-api/py-client/files/client/__clientModuleName__.py.template +1076 -0
  37. package/src/open-api/py-client/files/shared/__init__.py.template +28 -0
  38. package/src/open-api/py-client/files/shared/errors.py.template +37 -0
  39. package/src/open-api/py-client/files/shared/types.py.template +319 -0
  40. package/src/open-api/py-client/generator.d.ts +53 -0
  41. package/src/open-api/py-client/generator.js +119 -0
  42. package/src/open-api/py-client/generator.js.map +1 -0
  43. package/src/open-api/py-client/schema.d.js +6 -0
  44. package/src/open-api/py-client/schema.d.js.map +1 -0
  45. package/src/open-api/py-client/schema.d.ts +12 -0
  46. package/src/open-api/py-client/schema.json +28 -0
  47. package/src/open-api/py-client/vessel-registry-spec.json +2716 -0
  48. package/src/open-api/ts-client/__snapshots__/generator.edge-cases.spec.ts.snap +274 -0
  49. package/src/open-api/ts-client/petstore-spec.d.ts +6 -0
  50. package/src/open-api/ts-client/petstore-spec.js +1275 -0
  51. package/src/open-api/ts-client/petstore-spec.js.map +1 -0
  52. package/src/open-api/ts-hooks/generator.spec.tsx +1 -1
  53. package/src/open-api/utils/codegen-data/languages.d.ts +68 -2
  54. package/src/open-api/utils/codegen-data/languages.js +433 -21
  55. package/src/open-api/utils/codegen-data/languages.js.map +1 -1
  56. package/src/open-api/utils/codegen-data/types.d.ts +187 -0
  57. package/src/open-api/utils/codegen-data/types.js +10 -1
  58. package/src/open-api/utils/codegen-data/types.js.map +1 -1
  59. package/src/open-api/utils/codegen-data.d.ts +40 -1
  60. package/src/open-api/utils/codegen-data.js +622 -17
  61. package/src/open-api/utils/codegen-data.js.map +1 -1
  62. package/src/open-api/utils/normalise.js +175 -41
  63. package/src/open-api/utils/normalise.js.map +1 -1
  64. package/src/open-api/utils/parser.js +1 -1
  65. package/src/open-api/utils/parser.js.map +1 -1
  66. package/src/preset/__snapshots__/generator.spec.ts.snap +3 -3
  67. package/src/preset/generator.d.ts +1 -1
  68. package/src/py/agent/gateway-connection/generator.js +0 -3
  69. package/src/py/agent/gateway-connection/generator.js.map +1 -1
  70. package/src/py/agent/react-connection/generator.d.ts +1 -1
  71. package/src/py/agent/schema.json +2 -0
  72. package/src/py/fast-api/react/generator.d.ts +1 -1
  73. package/src/py/mcp-server/schema.json +1 -0
  74. package/src/sdk/open-api.d.ts +2 -0
  75. package/src/sdk/open-api.js +1 -0
  76. package/src/sdk/open-api.js.map +1 -1
  77. package/src/smithy/project/generator.d.ts +1 -1
  78. package/src/smithy/react-connection/generator.d.ts +1 -1
  79. package/src/terraform/project/generator.d.ts +1 -1
  80. package/src/trpc/react/generator.d.ts +1 -1
  81. package/src/ts/agent/a2a-connection/generator.d.ts +1 -1
  82. package/src/ts/agent/gateway-connection/generator.d.ts +1 -1
  83. package/src/ts/agent/gateway-connection/generator.js +0 -3
  84. package/src/ts/agent/gateway-connection/generator.js.map +1 -1
  85. package/src/ts/agent/mcp-connection/generator.d.ts +1 -1
  86. package/src/ts/agent/react-connection/generator.d.ts +1 -1
  87. package/src/ts/agent/schema.json +1 -0
  88. package/src/ts/api/schema.json +2 -0
  89. package/src/ts/astro-docs/generator.d.ts +1 -1
  90. package/src/ts/dcr-proxy/generator.d.ts +1 -1
  91. package/src/ts/dynamodb/generator.d.ts +1 -1
  92. package/src/ts/lambda-function/generator.d.ts +1 -1
  93. package/src/ts/lib/generator.d.ts +1 -1
  94. package/src/ts/mcp-server/schema.json +1 -0
  95. package/src/ts/nx-generator/__snapshots__/generator.spec.ts.snap +1 -1
  96. package/src/ts/nx-generator/files/nx-plugin-for-aws/generator/generator.spec.ts.template +1 -1
  97. package/src/ts/nx-generator/generator.d.ts +1 -1
  98. package/src/ts/nx-migration/generator.d.ts +1 -1
  99. package/src/ts/nx-plugin/generator.d.ts +1 -1
  100. package/src/ts/rdb/generator.d.ts +1 -1
  101. package/src/ts/react-website/agui/generator.d.ts +1 -1
  102. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +10 -10
  103. package/src/ts/react-website/app/schema.json +1 -0
  104. package/src/ts/website/app/schema.json +1 -0
  105. package/src/utils/metrics-assertions.d.ts +18 -0
  106. package/src/utils/metrics-assertions.js +42 -0
  107. package/src/utils/metrics-assertions.js.map +1 -0
  108. package/src/utils/test/python-dependencies.d.ts +8 -0
  109. package/src/utils/test/python-dependencies.js +14 -0
  110. package/src/utils/test/python-dependencies.js.map +1 -0
  111. package/src/utils/test/python-worker/worker.py +703 -0
  112. package/src/utils/test/warm-python-cache.d.ts +18 -0
  113. package/src/utils/test/warm-python-cache.js +46 -0
  114. package/src/utils/test/warm-python-cache.js.map +1 -0
  115. package/src/utils/versions.d.ts +10 -9
  116. package/src/utils/versions.js +9 -8
  117. package/src/utils/versions.js.map +1 -1
@@ -0,0 +1,739 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`openApiPyClientGenerator - responses > should handle multiple response status codes > 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
+ CheckApiError as CheckApiError,
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 check(
316
+ self,
317
+ ) -> types.Ok:
318
+ request_kwargs: dict[str, Any] = {}
319
+ _request_url = self._url("/check", {})
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 types.Ok.model_validate(response.json())
328
+ if response.status_code == 400:
329
+ raise CheckApiError(
330
+ response.status_code,
331
+ self._error(types.Check400Error, 400, self._error_payload(response)),
332
+ )
333
+ if response.status_code == 500:
334
+ raise CheckApiError(
335
+ response.status_code,
336
+ self._error(types.Check500Error, 500, self._error_payload(response)),
337
+ )
338
+ raise CheckApiError(response.status_code, None)
339
+ "
340
+ `;
341
+
342
+ exports[`openApiPyClientGenerator - responses > should handle multiple response status codes > types.py 1`] = `
343
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
344
+
345
+ from __future__ import annotations
346
+
347
+ from typing import Literal
348
+
349
+ from pydantic import BaseModel, ConfigDict
350
+
351
+
352
+ class BadReq(BaseModel):
353
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
354
+
355
+ reason: str
356
+
357
+
358
+ class Err(BaseModel):
359
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
360
+
361
+ code: int
362
+
363
+
364
+ class Ok(BaseModel):
365
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
366
+
367
+ message: str
368
+
369
+
370
+ class Check400Error(BaseModel):
371
+ """Error wrapper for Check status 400."""
372
+
373
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
374
+
375
+ status: Literal[400]
376
+ error: BadReq
377
+
378
+
379
+ class Check500Error(BaseModel):
380
+ """Error wrapper for Check status 500."""
381
+
382
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
383
+
384
+ status: Literal[500]
385
+ error: Err
386
+
387
+
388
+ CheckError = Check400Error | Check500Error
389
+ "
390
+ `;
391
+
392
+ exports[`openApiPyClientGenerator - responses > should handle only default response > client.py 1`] = `
393
+ """"TestApi — AUTO-GENERATED synchronous client."""
394
+
395
+ from __future__ import annotations
396
+
397
+ import datetime
398
+ from dataclasses import dataclass, field
399
+ from typing import Any, TypeVar
400
+ from urllib.parse import quote
401
+
402
+ import httpx
403
+ from pydantic import BaseModel, ValidationError
404
+
405
+ from . import types
406
+ from .errors import (
407
+ ApiError as ApiError,
408
+ )
409
+ from .errors import (
410
+ OnlyDefaultApiError as OnlyDefaultApiError,
411
+ )
412
+
413
+ _ErrorT = TypeVar("_ErrorT", bound=BaseModel)
414
+
415
+
416
+ @dataclass
417
+ class TestApiConfig:
418
+ """Client configuration for TestApi."""
419
+
420
+ #: Base URL for the API.
421
+ url: str
422
+ #: Client used for every request. Supply your own to add authentication or
423
+ #: any other middleware — an \`\`httpx.Auth\`\` (e.g. a SigV4 signer), an event
424
+ #: hook, a custom transport, timeouts, retries or a proxy all apply, and the
425
+ #: client's own headers and query params are sent alongside each operation's.
426
+ #: A supplied client is never closed by this one, so it can be shared.
427
+ httpx_client: httpx.Client | None = None
428
+ #: Headers added to every request, where the operation sets none itself.
429
+ headers: dict[str, str] = field(default_factory=dict)
430
+ #: By default the client sends a Content-Type header set to the media type
431
+ #: the OpenAPI specification declares for the request. Set this to omit it.
432
+ omit_content_type_header: bool = False
433
+
434
+
435
+ class TestApi:
436
+ """Synchronous API client for TestApi."""
437
+
438
+ def __init__(self, config: TestApiConfig) -> None:
439
+ self._config = config
440
+ self._client = config.httpx_client or httpx.Client()
441
+ self._owns_client = config.httpx_client is None
442
+ self._base_url = config.url.rstrip("/")
443
+
444
+ def __enter__(self) -> TestApi:
445
+ return self
446
+
447
+ def __exit__(self, *exc: Any) -> None:
448
+ self.close()
449
+
450
+ def close(self) -> None:
451
+ if self._owns_client:
452
+ self._client.close()
453
+
454
+ def _url(
455
+ self,
456
+ path: str,
457
+ path_params: dict[str, Any],
458
+ path_styles: dict[str, tuple[str, bool]] | None = None,
459
+ ) -> str:
460
+ for key, value in path_params.items():
461
+ style, explode = (path_styles or {}).get(key, ("simple", False))
462
+ # An object expands to its key,value members; a list to its items.
463
+ # Each is encoded on its own so the separators between them stay
464
+ # literal, as RFC 6570 requires.
465
+ dumped = self._dump(value)
466
+ if isinstance(dumped, dict):
467
+ values = [
468
+ part
469
+ for k, v in dumped.items()
470
+ if v is not None
471
+ for part in (k, self._scalar(v))
472
+ ]
473
+ elif isinstance(dumped, list):
474
+ values = list(dumped)
475
+ else:
476
+ values = [value]
477
+ if style == "matrix":
478
+ # RFC 6570 path-style expansion: ;key=a,b (explode: ;key=a;key=b)
479
+ if explode:
480
+ rendered = "".join(
481
+ ";" + key + "=" + quote(self._scalar(v), safe="")
482
+ for v in values
483
+ )
484
+ else:
485
+ rendered = (
486
+ ";"
487
+ + key
488
+ + "="
489
+ + ",".join(quote(self._scalar(v), safe="") for v in values)
490
+ )
491
+ elif style == "label":
492
+ # RFC 6570 label expansion: .a,b (explode: .a.b)
493
+ rendered = "." + ("." if explode else ",").join(
494
+ quote(self._scalar(v), safe="") for v in values
495
+ )
496
+ else:
497
+ # RFC 6570 simple expansion: a list renders comma-separated.
498
+ rendered = ",".join(quote(self._scalar(v), safe="") for v in values)
499
+ path = path.replace("{" + key + "}", rendered)
500
+ return self._base_url + path
501
+
502
+ def _deep_object(self, key: str, value: Any, out: dict[str, Any]) -> None:
503
+ if isinstance(value, BaseModel):
504
+ value = value.model_dump(mode="json", by_alias=True, exclude_unset=True)
505
+ if isinstance(value, dict):
506
+ for prop, v in value.items():
507
+ self._deep_object(f"{key}[{prop}]", v, out)
508
+ elif value is not None:
509
+ out[key] = value
510
+
511
+ _COLLECTION_DELIMITERS = {"csv": ",", "ssv": " ", "pipes": "|"}
512
+
513
+ #: Characters left literal in a cookie value: every RFC 6265 cookie-octet
514
+ #: except the \`,\` and \`;\` this client joins members on. A \`=\` stays literal
515
+ #: too — only the first one separates a cookie's name from its value.
516
+ _COOKIE_SAFE = "!#$%&'()*+-./:<>?@[]^_\`{|}~="
517
+
518
+ def _scalar(self, value: Any) -> str:
519
+ # The wire form of a single parameter value: JSON spellings for bools,
520
+ # ISO-8601 for dates, and the model's own wire shape for anything else.
521
+ if isinstance(value, bool):
522
+ return "true" if value else "false"
523
+ if isinstance(value, (datetime.date, datetime.datetime)):
524
+ return value.isoformat()
525
+ dumped = self._dump(value)
526
+ if isinstance(dumped, str):
527
+ return dumped
528
+ # RFC 6570 renders a list as its comma-joined members and an object as
529
+ # comma-joined key,value pairs. Python's own \`str\` would emit a repr —
530
+ # \`['a', 'b']\`, quotes and spaces included.
531
+ if isinstance(dumped, list):
532
+ return ",".join(self._scalar(v) for v in dumped)
533
+ if isinstance(dumped, dict):
534
+ return ",".join(
535
+ f"{k},{self._scalar(v)}" for k, v in dumped.items() if v is not None
536
+ )
537
+ if dumped is None:
538
+ return ""
539
+ return str(dumped)
540
+
541
+ def _query(
542
+ self,
543
+ query_params: dict[str, Any],
544
+ collection_formats: dict[str, str] | None = None,
545
+ ) -> dict[str, Any]:
546
+ out: dict[str, Any] = {}
547
+ for key, value in query_params.items():
548
+ if value is None:
549
+ continue
550
+ fmt = (collection_formats or {}).get(key)
551
+ if fmt == "deepObject":
552
+ self._deep_object(key, value, out)
553
+ elif isinstance(value, list):
554
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt or "multi")
555
+ if delimiter is None:
556
+ out[key] = [self._scalar(v) for v in value]
557
+ else:
558
+ out[key] = delimiter.join(self._scalar(v) for v in value)
559
+ elif self._is_object(value):
560
+ # OpenAPI's default query style is form/explode, under which an
561
+ # object expands to one parameter per property rather than
562
+ # being stringified.
563
+ for prop, prop_value in self._as_dict(value).items():
564
+ if prop_value is not None:
565
+ out[prop] = self._scalar(prop_value)
566
+ else:
567
+ out[key] = self._scalar(value)
568
+ return out
569
+
570
+ def _is_object(self, value: Any) -> bool:
571
+ return isinstance(value, (dict, BaseModel))
572
+
573
+ def _as_dict(self, value: Any) -> dict[str, Any]:
574
+ if isinstance(value, BaseModel):
575
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
576
+ return dict(value)
577
+
578
+ def _headers(
579
+ self,
580
+ header_params: dict[str, Any] | None = None,
581
+ collection_formats: dict[str, str] | None = None,
582
+ cookie_params: dict[str, Any] | None = None,
583
+ ) -> list[tuple[str, str]]:
584
+ out: list[tuple[str, str]] = []
585
+ for key, value in (header_params or {}).items():
586
+ if value is None:
587
+ continue
588
+ if isinstance(value, list):
589
+ fmt = (collection_formats or {}).get(key, "csv")
590
+ delimiter = self._COLLECTION_DELIMITERS.get(fmt)
591
+ if delimiter is None:
592
+ for item in value:
593
+ out.append((key, self._scalar(item)))
594
+ else:
595
+ out.append((key, delimiter.join(self._scalar(v) for v in value)))
596
+ else:
597
+ out.append((key, self._scalar(value)))
598
+ # A config header only applies where the operation didn't set one:
599
+ # httpx comma-joins repeated names, which would corrupt the value.
600
+ operation_headers = {key.lower() for key, _ in out}
601
+ out.extend(
602
+ (key, value)
603
+ for key, value in self._config.headers.items()
604
+ if key.lower() not in operation_headers
605
+ )
606
+ # The operation's cookies are merged into one \`Cookie\` header alongside
607
+ # every other source. Passing them to httpx's \`cookies=\` instead would
608
+ # drop them outright whenever a \`Cookie\` header is present, since
609
+ # http.cookiejar skips a request that already carries one — and setting
610
+ # the header here would in turn shadow the caller's own jar and header,
611
+ # which httpx resolves in favour of the more specific value.
612
+ cookies = self._cookies(cookie_params or {})
613
+ if cookies:
614
+ pairs = [
615
+ *(
616
+ value
617
+ for source in (self._client.headers, dict(out))
618
+ for key, value in source.items()
619
+ if key.lower() == "cookie"
620
+ ),
621
+ *(f"{key}={value}" for key, value in self._client.cookies.items()),
622
+ *(f"{key}={value}" for key, value in cookies.items()),
623
+ ]
624
+ out = [(key, value) for key, value in out if key.lower() != "cookie"]
625
+ out.append(("Cookie", "; ".join(pairs)))
626
+ return out
627
+
628
+ def _default_content_type(
629
+ self, header_params: dict[str, Any], media_type: str
630
+ ) -> None:
631
+ # The media type the specification declares, applied unless the operation
632
+ # takes a Content-Type of its own and the caller supplied one. Header
633
+ # names are case-insensitive, and a declared header the caller omitted is
634
+ # present holding \`None\` — neither counts as the caller setting it.
635
+ if any(
636
+ key.lower() == "content-type" and value is not None
637
+ for key, value in header_params.items()
638
+ ):
639
+ return
640
+ for key in list(header_params):
641
+ if key.lower() == "content-type":
642
+ header_params[key] = media_type
643
+ return
644
+ header_params["Content-Type"] = media_type
645
+
646
+ def _cookies(self, cookie_params: dict[str, Any]) -> dict[str, str]:
647
+ out: dict[str, str] = {}
648
+ for key, value in cookie_params.items():
649
+ if value is None:
650
+ continue
651
+ dumped = self._dump(value)
652
+ # A cookie uses form-style expansion, so a list joins on \`,\` and an
653
+ # object on \`,\` between each key and value. Members are encoded so a
654
+ # \`;\` or \`,\` inside one can't be read as a delimiter, while the
655
+ # joining commas stay literal.
656
+ if isinstance(dumped, list):
657
+ rendered = ",".join(self._cookie_value(v) for v in dumped)
658
+ elif isinstance(dumped, dict):
659
+ rendered = ",".join(
660
+ self._cookie_value(k) + "," + self._cookie_value(v)
661
+ for k, v in dumped.items()
662
+ if v is not None
663
+ )
664
+ else:
665
+ rendered = self._cookie_value(value)
666
+ out[key] = rendered
667
+ return out
668
+
669
+ def _cookie_value(self, value: Any) -> str:
670
+ # Only the characters a cookie parser treats as structure are escaped, so
671
+ # a timestamp keeps its colons and a base64 token its \`=\` padding. Servers
672
+ # do not URL-decode cookies, so anything escaped here arrives escaped.
673
+ return quote(self._scalar(value), safe=self._COOKIE_SAFE)
674
+
675
+ def _dump(self, value: Any) -> Any:
676
+ if value is None:
677
+ return None
678
+ if isinstance(value, BaseModel):
679
+ return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
680
+ if isinstance(value, (list, tuple)):
681
+ return [self._dump(v) for v in value]
682
+ if isinstance(value, dict):
683
+ return {k: self._dump(v) for k, v in value.items()}
684
+ if isinstance(value, (datetime.date, datetime.datetime)):
685
+ return value.isoformat()
686
+ return value
687
+
688
+ def _error_payload(self, response: httpx.Response) -> Any:
689
+ # An error body is whatever the server actually sent, which need not be
690
+ # the JSON the spec declares (a proxy's HTML page, say).
691
+ try:
692
+ return response.json()
693
+ except ValueError:
694
+ return response.text
695
+
696
+ def _error(self, wrapper: type[_ErrorT], status: int, payload: Any) -> _ErrorT:
697
+ # Coerce to the declared error type, falling back to the raw payload so
698
+ # a body that doesn't match the spec still raises the typed exception.
699
+ try:
700
+ return wrapper.model_validate({"status": status, "error": payload})
701
+ except ValidationError:
702
+ return wrapper.model_construct(status=status, error=payload)
703
+
704
+ def only_default(
705
+ self,
706
+ ) -> types.Body:
707
+ request_kwargs: dict[str, Any] = {}
708
+ _request_url = self._url("/only-default", {})
709
+ response = self._client.request(
710
+ "GET",
711
+ _request_url,
712
+ headers=self._headers(),
713
+ **request_kwargs,
714
+ )
715
+ if 200 <= response.status_code < 300:
716
+ return types.Body.model_validate(response.json())
717
+ raise OnlyDefaultApiError(response.status_code, None)
718
+ "
719
+ `;
720
+
721
+ exports[`openApiPyClientGenerator - responses > should handle only default response > types.py 1`] = `
722
+ """"Types for TestApi — AUTO-GENERATED, do not edit."""
723
+
724
+ from __future__ import annotations
725
+
726
+ from typing import Never
727
+
728
+ from pydantic import BaseModel, ConfigDict
729
+
730
+
731
+ class Body(BaseModel):
732
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
733
+
734
+ message: str
735
+
736
+
737
+ OnlyDefaultError = Never
738
+ "
739
+ `;