alt-stack-openapi-pydantic 1.6.2__py3-none-any.whl

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.
@@ -0,0 +1,725 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from .dependencies import topological_sort_schemas
7
+ from .lowering import LoweringContext, NamedSchema, lower_named_schema
8
+ from .rendering import (
9
+ RenderContext,
10
+ decode_component_ref,
11
+ extra_field_annotation,
12
+ is_object_model_schema,
13
+ root_model_annotation,
14
+ schema_to_type_expr,
15
+ )
16
+ from .routes import (
17
+ RouteInfo,
18
+ build_route_path_name,
19
+ build_route_schema_name,
20
+ generate_route_schema_names,
21
+ parse_openapi_paths,
22
+ )
23
+ from .schema_dedup import (
24
+ NamedSchema as DedupNamedSchema,
25
+ )
26
+ from .schema_dedup import (
27
+ create_schema_registry,
28
+ find_common_schemas,
29
+ get_schema_fingerprint,
30
+ pre_register_schema,
31
+ register_schema,
32
+ )
33
+ from .types import AnySchema
34
+
35
+ _VALID_IDENTIFIER_CHARS = set("_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class EmissionState:
40
+ render_context: RenderContext = field(default_factory=RenderContext)
41
+ rebuild_names: list[str] = field(default_factory=list)
42
+
43
+
44
+ def openapi_to_pydantic_code(
45
+ openapi: dict[str, Any],
46
+ custom_import_lines: list[str] | None = None,
47
+ options: dict[str, Any] | None = None,
48
+ ) -> str:
49
+ include_routes = bool(options.get("include_routes") if options else False)
50
+ components = openapi.get("components")
51
+ schemas = components.get("schemas") if isinstance(components, dict) else {}
52
+ if not isinstance(schemas, dict):
53
+ schemas = {}
54
+
55
+ lowering_context = LoweringContext(set(schemas.keys()))
56
+ lowered_components: list[NamedSchema] = []
57
+ for name in topological_sort_schemas(schemas):
58
+ schema = schemas.get(name)
59
+ if schema is None:
60
+ continue
61
+ lowered_components.extend(lower_named_schema(lowering_context, name, schema))
62
+
63
+ lines = _build_module_preamble(custom_import_lines)
64
+ state = EmissionState()
65
+ registry = create_schema_registry()
66
+
67
+ for named_schema in lowered_components:
68
+ lines.extend(_emit_named_schema(named_schema.name, named_schema.schema, state))
69
+ pre_register_schema(
70
+ registry,
71
+ named_schema.name,
72
+ get_schema_fingerprint(named_schema.schema),
73
+ )
74
+
75
+ if include_routes:
76
+ routes = parse_openapi_paths(openapi)
77
+ if routes:
78
+ route_lines, schema_name_to_canonical = _emit_route_schemas(
79
+ routes,
80
+ lowering_context,
81
+ registry,
82
+ state,
83
+ )
84
+ if route_lines:
85
+ lines.append("# Route Schemas")
86
+ lines.extend(route_lines)
87
+ lines.append("")
88
+
89
+ lines.extend(_emit_model_rebuilds(state.rebuild_names))
90
+ if route_lines:
91
+ lines.append("")
92
+ lines.extend(_generate_request_response_objects(routes, schema_name_to_canonical))
93
+ return _finalize_module(lines)
94
+
95
+ lines.extend(_emit_model_rebuilds(state.rebuild_names))
96
+ return _finalize_module(lines)
97
+
98
+
99
+ _ALL_OF_HELPER = [
100
+ "def all_of(*types: Any) -> Any:",
101
+ " adapters = [TypeAdapter(t) for t in types]",
102
+ "",
103
+ " def validate(value: Any) -> Any:",
104
+ " for adapter in adapters:",
105
+ " adapter.validate_python(value)",
106
+ " return value",
107
+ "",
108
+ " return Annotated[Any, BeforeValidator(validate)]",
109
+ "",
110
+ ]
111
+
112
+
113
+ def _finalize_module(lines: list[str]) -> str:
114
+ """Inline the ``all_of`` intersection helper when the module uses it.
115
+
116
+ Generated modules depend only on pydantic and ``alt_stack_http_client_httpx`` at runtime,
117
+ not on the generator package.
118
+ """
119
+ anchor = "_omit_not_null = BeforeValidator(_reject_explicit_none)"
120
+ if any("all_of(" in line for line in lines) and anchor in lines:
121
+ index = lines.index(anchor) + 1
122
+ lines[index:index] = ["", *_ALL_OF_HELPER[:-1]]
123
+ return "\n".join(lines)
124
+
125
+
126
+ def _build_module_preamble(custom_import_lines: list[str] | None) -> list[str]:
127
+ lines = [
128
+ "# This file was automatically generated from OpenAPI schema",
129
+ "# Do not manually edit this file",
130
+ "from __future__ import annotations",
131
+ "",
132
+ "from typing import Any, Annotated, Final, Literal, Optional, TypedDict, Union",
133
+ "from typing import cast, overload",
134
+ "from datetime import date, datetime",
135
+ "from uuid import UUID",
136
+ "",
137
+ "from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, RootModel",
138
+ "from pydantic import TypeAdapter",
139
+ "from pydantic import AnyUrl, EmailStr",
140
+ "import alt_stack_http_client_httpx as _client",
141
+ "",
142
+ "def _reject_explicit_none(value: Any) -> Any:",
143
+ " if value is None:",
144
+ ' raise ValueError("Field may be omitted but may not be null")',
145
+ " return value",
146
+ "",
147
+ "_omit_not_null = BeforeValidator(_reject_explicit_none)",
148
+ "",
149
+ ]
150
+ if custom_import_lines:
151
+ lines.extend(custom_import_lines)
152
+ lines.append("")
153
+ return lines
154
+
155
+
156
+ def _emit_named_schema(name: str, schema: AnySchema, state: EmissionState) -> list[str]:
157
+ if is_object_model_schema(schema) and not _has_root_model_allof_base(schema, state):
158
+ return _emit_object_model(name, schema, state)
159
+ return _emit_root_model(name, schema, state)
160
+
161
+
162
+ def _has_root_model_allof_base(schema: AnySchema, state: EmissionState) -> bool:
163
+ if not isinstance(schema, dict):
164
+ return False
165
+ all_of = schema.get("allOf")
166
+ if not isinstance(all_of, list):
167
+ return False
168
+ return any(
169
+ isinstance(part, dict)
170
+ and isinstance(part.get("$ref"), str)
171
+ and decode_component_ref(part["$ref"]) in state.render_context.root_model_names
172
+ for part in all_of
173
+ )
174
+
175
+
176
+ def _emit_object_model(name: str, schema: AnySchema, state: EmissionState) -> list[str]:
177
+ all_of = schema.get("allOf") if isinstance(schema, dict) else None
178
+ if isinstance(all_of, list):
179
+ base_classes, merged_schema = _merge_allof_object_schema(all_of)
180
+ model_lines = _build_model_lines(name, merged_schema, base_classes=base_classes or None)
181
+ model_lines.insert(1, f" __openapi_allof__ = {all_of!r}")
182
+ else:
183
+ model_lines = _build_model_lines(name, schema)
184
+
185
+ state.rebuild_names.append(name)
186
+ return [*model_lines, ""]
187
+
188
+
189
+ def _emit_root_model(name: str, schema: AnySchema, state: EmissionState) -> list[str]:
190
+ state.render_context.root_model_names.add(name)
191
+ type_expr = schema_to_type_expr(schema, context=state.render_context)
192
+ state.rebuild_names.append(name)
193
+ return [
194
+ f"class {name}({root_model_annotation(type_expr)}):",
195
+ " pass",
196
+ "",
197
+ ]
198
+
199
+
200
+ def _merge_allof_object_schema(all_of: list[Any]) -> tuple[list[str], dict[str, Any]]:
201
+ base_classes: list[str] = []
202
+ inline_properties: dict[str, Any] = {}
203
+ inline_required: list[str] = []
204
+ additional_properties: Any = None
205
+
206
+ for part in all_of:
207
+ if not isinstance(part, dict):
208
+ continue
209
+ if isinstance(part.get("$ref"), str):
210
+ base_classes.append(decode_component_ref(part["$ref"]))
211
+ continue
212
+
213
+ if part.get("type") == "object" or "properties" in part:
214
+ props = part.get("properties")
215
+ if isinstance(props, dict):
216
+ inline_properties.update(props)
217
+
218
+ required = part.get("required")
219
+ if isinstance(required, list):
220
+ for item in required:
221
+ if item not in inline_required:
222
+ inline_required.append(item)
223
+
224
+ additional = part.get("additionalProperties")
225
+ if additional is False:
226
+ additional_properties = False
227
+ elif isinstance(additional, dict):
228
+ additional_properties = additional
229
+
230
+ merged_schema: dict[str, Any] = {
231
+ "type": "object",
232
+ "properties": inline_properties,
233
+ "required": inline_required,
234
+ }
235
+ if additional_properties is not None:
236
+ merged_schema["additionalProperties"] = additional_properties
237
+ return base_classes, merged_schema
238
+
239
+
240
+ def _build_model_lines(
241
+ name: str,
242
+ schema: AnySchema,
243
+ *,
244
+ base_classes: list[str] | None = None,
245
+ ) -> list[str]:
246
+ properties = schema.get("properties") if isinstance(schema, dict) else None
247
+ required = set(schema.get("required", [])) if isinstance(schema, dict) else set()
248
+ additional = schema.get("additionalProperties") if isinstance(schema, dict) else None
249
+
250
+ bases = base_classes or ["BaseModel"]
251
+ lines = [f"class {name}({', '.join(bases)}):"]
252
+
253
+ alias_used = False
254
+ field_lines: list[str] = []
255
+ used_names: set[str] = set()
256
+
257
+ if isinstance(properties, dict):
258
+ for prop_name, prop_schema in properties.items():
259
+ attr_name = _dedupe_name(_sanitize_identifier(prop_name), used_names)
260
+ alias = prop_name if attr_name != prop_name else None
261
+ if alias:
262
+ alias_used = True
263
+
264
+ type_expr = schema_to_type_expr(prop_schema)
265
+ is_required = prop_name in required
266
+ if is_required:
267
+ default_expr = f" = Field(alias={alias!r})" if alias else ""
268
+ else:
269
+ if not isinstance(prop_schema, dict) or prop_schema.get("nullable") is not True:
270
+ type_expr = f"Annotated[Optional[{type_expr}], _omit_not_null]"
271
+ default_expr = f" = Field(default=None, alias={alias!r})" if alias else " = None"
272
+
273
+ field_lines.append(f" {attr_name}: {type_expr}{default_expr}")
274
+
275
+ config_args: list[str] = []
276
+ extra_line: str | None = None
277
+ if alias_used:
278
+ config_args.append("populate_by_name=True")
279
+
280
+ if additional is False:
281
+ config_args.append("extra='forbid'")
282
+ elif additional is True or additional is None:
283
+ config_args.append("extra='allow'")
284
+ elif isinstance(additional, dict):
285
+ config_args.append("extra='allow'")
286
+ extra_type = schema_to_type_expr(additional)
287
+ extra_line = (
288
+ f" __pydantic_extra__: {extra_field_annotation(extra_type)} = Field(init=False)"
289
+ )
290
+
291
+ if config_args:
292
+ lines.append(f" model_config = ConfigDict({', '.join(config_args)})")
293
+ if extra_line:
294
+ lines.append(extra_line)
295
+
296
+ if field_lines:
297
+ lines.extend(field_lines)
298
+ elif len(lines) == 1:
299
+ lines.append(" pass")
300
+
301
+ return lines
302
+
303
+
304
+ def _emit_model_rebuilds(names: list[str]) -> list[str]:
305
+ if not names:
306
+ return []
307
+ return [*(f"{name}.model_rebuild()" for name in names), ""]
308
+
309
+
310
+ def _emit_route_schemas(
311
+ routes: list[RouteInfo],
312
+ lowering_context: LoweringContext,
313
+ registry: Any,
314
+ state: EmissionState,
315
+ ) -> tuple[list[str], dict[str, str]]:
316
+ lines: list[str] = []
317
+ route_named_schemas: list[NamedSchema] = []
318
+
319
+ schema_name_to_canonical: dict[str, str] = {}
320
+
321
+ def register_named_schema_batch(named_schemas: list[NamedSchema]) -> None:
322
+ local_canonical: dict[str, str] = {}
323
+ for named_schema in named_schemas:
324
+ canonical_schema = _rewrite_schema_refs(named_schema.schema, local_canonical)
325
+ result = register_schema(registry, named_schema.name, canonical_schema)
326
+ schema_name_to_canonical[named_schema.name] = result.canonical_name
327
+ local_canonical[named_schema.name] = result.canonical_name
328
+ if result.is_new:
329
+ route_named_schemas.append(
330
+ NamedSchema(name=named_schema.name, schema=canonical_schema)
331
+ )
332
+
333
+ for common in find_common_schemas(_collect_route_schemas(routes), 2):
334
+ if common.fingerprint in registry.fingerprint_to_name:
335
+ continue
336
+ register_named_schema_batch(
337
+ lower_named_schema(lowering_context, common.name, common.schema)
338
+ )
339
+
340
+ def register_route_schema(name: str, schema: AnySchema) -> None:
341
+ if _route_schema_ref_target(schema):
342
+ return
343
+ register_named_schema_batch(lower_named_schema(lowering_context, name, schema))
344
+
345
+ for route in routes:
346
+ names = generate_route_schema_names(route)
347
+ path_params = [p for p in route.parameters if p.location == "path"]
348
+ query_params = [p for p in route.parameters if p.location == "query"]
349
+ header_params = [p for p in route.parameters if p.location == "header"]
350
+
351
+ if names.params_schema_name and path_params:
352
+ register_route_schema(
353
+ names.params_schema_name,
354
+ _build_openapi_object_schema(
355
+ [{"name": p.name, "schema": p.schema, "required": True} for p in path_params]
356
+ ),
357
+ )
358
+
359
+ if names.query_schema_name and query_params:
360
+ register_route_schema(
361
+ names.query_schema_name,
362
+ _build_openapi_object_schema(
363
+ [
364
+ {"name": p.name, "schema": p.schema, "required": p.required}
365
+ for p in query_params
366
+ ]
367
+ ),
368
+ )
369
+
370
+ if names.headers_schema_name and header_params:
371
+ register_route_schema(
372
+ names.headers_schema_name,
373
+ _build_openapi_object_schema(
374
+ [
375
+ {"name": p.name, "schema": p.schema, "required": p.required}
376
+ for p in header_params
377
+ ]
378
+ ),
379
+ )
380
+
381
+ if names.body_schema_name and route.request_body is not None:
382
+ register_route_schema(names.body_schema_name, route.request_body)
383
+
384
+ for status_code, response_schema in route.responses.items():
385
+ if not response_schema:
386
+ continue
387
+ suffix = "Response" if status_code.startswith("2") else "ErrorResponse"
388
+ register_route_schema(
389
+ build_route_schema_name(route.path, route.method, f"{status_code}{suffix}"),
390
+ response_schema,
391
+ )
392
+
393
+ for named_schema in route_named_schemas:
394
+ lines.extend(_emit_named_schema(named_schema.name, named_schema.schema, state))
395
+
396
+ return lines, schema_name_to_canonical
397
+
398
+
399
+ def _collect_route_schemas(routes: list[RouteInfo]) -> list[DedupNamedSchema]:
400
+ collected: list[DedupNamedSchema] = []
401
+ for route in routes:
402
+ for status_code, response_schema in route.responses.items():
403
+ if not response_schema:
404
+ continue
405
+ suffix = "Response" if status_code.startswith("2") else "ErrorResponse"
406
+ collected.append(
407
+ {
408
+ "name": build_route_schema_name(
409
+ route.path,
410
+ route.method,
411
+ f"{status_code}{suffix}",
412
+ ),
413
+ "schema": response_schema,
414
+ }
415
+ )
416
+ return collected
417
+
418
+
419
+ RouteMap = dict[str, dict[str, dict[str, str]]]
420
+ """path -> METHOD -> request part or status code -> model class name."""
421
+
422
+
423
+ def _generate_request_response_objects(
424
+ routes: list[RouteInfo],
425
+ schema_name_to_canonical: dict[str, str],
426
+ ) -> list[str]:
427
+ request_paths: RouteMap = {}
428
+ response_paths: RouteMap = {}
429
+
430
+ def resolve_schema_name(name: str, schema: AnySchema) -> str:
431
+ ref_target = _route_schema_ref_target(schema)
432
+ if ref_target:
433
+ return ref_target
434
+ return schema_name_to_canonical.get(name, name)
435
+
436
+ for route in routes:
437
+ names = generate_route_schema_names(route)
438
+ path_params = [p for p in route.parameters if p.location == "path"]
439
+ query_params = [p for p in route.parameters if p.location == "query"]
440
+ header_params = [p for p in route.parameters if p.location == "header"]
441
+
442
+ request_parts: dict[str, str] = {}
443
+
444
+ if names.params_schema_name and path_params:
445
+ schema = _build_openapi_object_schema(
446
+ [{"name": p.name, "schema": p.schema, "required": True} for p in path_params]
447
+ )
448
+ request_parts["params"] = resolve_schema_name(names.params_schema_name, schema)
449
+
450
+ if names.query_schema_name and query_params:
451
+ schema = _build_openapi_object_schema(
452
+ [{"name": p.name, "schema": p.schema, "required": p.required} for p in query_params]
453
+ )
454
+ request_parts["query"] = resolve_schema_name(names.query_schema_name, schema)
455
+
456
+ if names.headers_schema_name and header_params:
457
+ schema = _build_openapi_object_schema(
458
+ [
459
+ {"name": p.name, "schema": p.schema, "required": p.required}
460
+ for p in header_params
461
+ ]
462
+ )
463
+ request_parts["headers"] = resolve_schema_name(names.headers_schema_name, schema)
464
+
465
+ if names.body_schema_name and route.request_body is not None:
466
+ request_parts["body"] = resolve_schema_name(names.body_schema_name, route.request_body)
467
+
468
+ # Bare methods keep an empty entry so `Request[path][METHOD]` is a closed,
469
+ # statically known shape, matching the TypeScript `as const` maps.
470
+ request_paths.setdefault(route.path, {})[route.method] = request_parts
471
+
472
+ response_statuses: dict[str, str] = {}
473
+ for status_code, response_schema in route.responses.items():
474
+ if not response_schema:
475
+ continue
476
+ suffix = "Response" if status_code.startswith("2") else "ErrorResponse"
477
+ schema_name = build_route_schema_name(
478
+ route.path,
479
+ route.method,
480
+ f"{status_code}{suffix}",
481
+ )
482
+ response_statuses[status_code] = resolve_schema_name(schema_name, response_schema)
483
+ response_paths.setdefault(route.path, {})[route.method] = response_statuses
484
+
485
+ used_type_names: set[str] = set()
486
+ lines = _emit_route_map("Request", request_paths, used_type_names)
487
+ lines.append("")
488
+ lines.extend(_emit_route_map("Response", response_paths, used_type_names))
489
+ lines.append("")
490
+ lines.extend(_emit_typed_client(request_paths, response_paths))
491
+ return lines
492
+
493
+
494
+ def _emit_route_map(name: str, paths: RouteMap, used_type_names: set[str]) -> list[str]:
495
+ """Emit a closed ``TypedDict`` shape for a route map plus its runtime dictionary.
496
+
497
+ Every level of the map gets its own ``TypedDict`` so a type checker resolves
498
+ ``Map[path][METHOD][key]`` to the exact ``type[Model]`` and rejects keys that are
499
+ not in the OpenAPI document. The runtime value stays a plain nested ``dict``.
500
+ """
501
+ lines: list[str] = []
502
+ path_type_names: dict[str, str] = {}
503
+
504
+ for path, methods in paths.items():
505
+ method_type_names: dict[str, str] = {}
506
+ for method, leaves in methods.items():
507
+ leaf_type_name = _dedupe_name(
508
+ f"_{build_route_schema_name(path, method, name)}", used_type_names
509
+ )
510
+ method_type_names[method] = leaf_type_name
511
+ lines.extend(
512
+ _emit_typed_dict(
513
+ leaf_type_name,
514
+ {key: f"type[{model_name}]" for key, model_name in leaves.items()},
515
+ )
516
+ )
517
+
518
+ path_type_name = _dedupe_name(
519
+ f"_{build_route_path_name(path)}{name}Methods", used_type_names
520
+ )
521
+ path_type_names[path] = path_type_name
522
+ lines.extend(_emit_typed_dict(path_type_name, method_type_names))
523
+
524
+ map_type_name = _dedupe_name(f"_{name}Map", used_type_names)
525
+ lines.extend(_emit_typed_dict(map_type_name, path_type_names))
526
+ lines.append("")
527
+
528
+ lines.append(f"{name}: Final[{map_type_name}] = {{")
529
+ for path, methods in paths.items():
530
+ lines.append(f" {path!r}: {{")
531
+ for method, leaves in methods.items():
532
+ if not leaves:
533
+ lines.append(f" {method!r}: {{}},")
534
+ continue
535
+ lines.append(f" {method!r}: {{")
536
+ for key, model_name in leaves.items():
537
+ lines.append(f" {key!r}: {model_name},")
538
+ lines.append(" },")
539
+ lines.append(" },")
540
+ lines.append("}")
541
+ return lines
542
+
543
+
544
+ _CLIENT_METHOD_ORDER = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")
545
+
546
+
547
+ def _emit_typed_client(request_paths: RouteMap, response_paths: RouteMap) -> list[str]:
548
+ """Emit ``{Method}{Path}Result`` unions and an ``ApiClient`` with typed route methods.
549
+
550
+ Python cannot derive a call signature from the ``TypedDict`` maps the way TypeScript
551
+ maps over ``typeof Request``, so each route gets an explicit ``Literal`` path overload
552
+ carrying its exact request models and result union. Methods with a single route are
553
+ emitted as a plain typed signature because a lone ``@overload`` is invalid.
554
+ """
555
+ lines: list[str] = ["# Typed Client"]
556
+ routes_by_method: dict[str, list[tuple[str, dict[str, str], str]]] = {}
557
+
558
+ for path, methods in request_paths.items():
559
+ for method, parts in methods.items():
560
+ result_name = build_route_schema_name(path, method, "Result")
561
+ statuses = response_paths.get(path, {}).get(method, {})
562
+ lines.extend(_emit_result_alias(result_name, statuses))
563
+ routes_by_method.setdefault(method, []).append((path, parts, result_name))
564
+
565
+ lines.append("")
566
+ lines.append("# Typed route methods; construct HttpxApiClient(url, request_map=Request,")
567
+ lines.append("# response_map=Response) or ApiClient(url, transport=..., request_map=..., ...).")
568
+ lines.append("class ApiClient(_client.BaseApiClient):")
569
+ for index, method in enumerate(m for m in _CLIENT_METHOD_ORDER if m in routes_by_method):
570
+ if index:
571
+ lines.append("")
572
+ lines.extend(_emit_client_method(method, routes_by_method[method]))
573
+
574
+ lines.append("")
575
+ lines.append("class HttpxApiClient(ApiClient, _client.HttpxApiClient):")
576
+ lines.append(" pass")
577
+ return lines
578
+
579
+
580
+ def _emit_result_alias(result_name: str, statuses: dict[str, str]) -> list[str]:
581
+ members: list[str] = []
582
+ for status_code, model_name in statuses.items():
583
+ result_class = "ApiSuccess" if status_code.startswith("2") else "ApiFailure"
584
+ members.append(f"_client.{result_class}[Literal[{status_code!r}], {model_name}]")
585
+ if not any(status_code.startswith("2") for status_code in statuses):
586
+ # No documented 2xx body (for example 204): success carries the raw payload.
587
+ members.append("_client.ApiSuccess[str, Any]")
588
+ members.append("_client.ApiUnexpectedError")
589
+ return [f"{result_name} = Union[", *(f" {member}," for member in members), "]"]
590
+
591
+
592
+ def _emit_client_method(method: str, routes: list[tuple[str, dict[str, str], str]]) -> list[str]:
593
+ method_name = method.lower()
594
+ lines: list[str] = []
595
+
596
+ if len(routes) == 1:
597
+ path, parts, result_name = routes[0]
598
+ lines.extend(_emit_route_signature(method_name, path, parts, result_name, overload=False))
599
+ forwarded = ", ".join(f"{key}={key}" for key in parts if key != "headers")
600
+ call_args = ", ".join(filter(None, [f"{method!r}", "path", forwarded, "options=options"]))
601
+ lines.append(" return cast(")
602
+ lines.append(f" {result_name},")
603
+ lines.append(f" await self.request({call_args}),")
604
+ lines.append(" )")
605
+ return lines
606
+
607
+ for path, parts, result_name in routes:
608
+ lines.extend(_emit_route_signature(method_name, path, parts, result_name, overload=True))
609
+ lines.append("")
610
+ lines.append(f" async def {method_name}(")
611
+ lines.append(" self,")
612
+ lines.append(" path: str,")
613
+ lines.append(" *,")
614
+ lines.append(" params: Any = None,")
615
+ lines.append(" query: Any = None,")
616
+ lines.append(" body: Any = None,")
617
+ lines.append(" options: _client.RequestOptions | None = None,")
618
+ lines.append(" ) -> Any:")
619
+ lines.append(" return await self.request(")
620
+ lines.append(
621
+ f" {method!r}, path, params=params, query=query, body=body, options=options"
622
+ )
623
+ lines.append(" )")
624
+ return lines
625
+
626
+
627
+ def _emit_route_signature(
628
+ method_name: str,
629
+ path: str,
630
+ parts: dict[str, str],
631
+ result_name: str,
632
+ *,
633
+ overload: bool,
634
+ ) -> list[str]:
635
+ lines: list[str] = []
636
+ if overload:
637
+ lines.append(" @overload")
638
+ lines.append(f" async def {method_name}(")
639
+ lines.append(" self,")
640
+ lines.append(f" path: Literal[{path!r}],")
641
+ lines.append(" *,")
642
+ if "params" in parts:
643
+ lines.append(f" params: {parts['params']},")
644
+ if "query" in parts:
645
+ lines.append(f" query: {parts['query']} | None = None,")
646
+ if "body" in parts:
647
+ lines.append(f" body: {parts['body']},")
648
+ lines.append(" options: _client.RequestOptions | None = None,")
649
+ lines.append(f" ) -> {result_name}:{' ...' if overload else ''}")
650
+ return lines
651
+
652
+
653
+ def _emit_typed_dict(type_name: str, fields: dict[str, str]) -> list[str]:
654
+ if not fields:
655
+ return [f"{type_name} = TypedDict({type_name!r}, {{}})"]
656
+ return [
657
+ f"{type_name} = TypedDict({type_name!r}, {{",
658
+ *(f" {key!r}: {value}," for key, value in fields.items()),
659
+ "})",
660
+ ]
661
+
662
+
663
+ def _build_openapi_object_schema(params: list[dict[str, Any]]) -> dict[str, Any]:
664
+ return {
665
+ "type": "object",
666
+ "properties": {param["name"]: param["schema"] for param in params},
667
+ "required": [param["name"] for param in params if param.get("required")],
668
+ "additionalProperties": False,
669
+ }
670
+
671
+
672
+ def _route_schema_ref_target(schema: AnySchema) -> str | None:
673
+ if not isinstance(schema, dict):
674
+ return None
675
+ ref = schema.get("$ref")
676
+ if not isinstance(ref, str) or schema.get("nullable") is True:
677
+ return None
678
+ if set(schema.keys()) != {"$ref"}:
679
+ return None
680
+ return decode_component_ref(ref)
681
+
682
+
683
+ def _rewrite_schema_refs(schema: AnySchema, aliases: dict[str, str]) -> AnySchema:
684
+ if not isinstance(schema, dict):
685
+ return schema
686
+
687
+ rewritten = dict(schema)
688
+ ref = rewritten.get("$ref")
689
+ if isinstance(ref, str):
690
+ name = decode_component_ref(ref)
691
+ canonical = aliases.get(name)
692
+ if canonical and canonical != name:
693
+ rewritten["$ref"] = f"#/components/schemas/{canonical}"
694
+
695
+ for key, value in list(rewritten.items()):
696
+ if isinstance(value, dict):
697
+ rewritten[key] = _rewrite_schema_refs(value, aliases)
698
+ elif isinstance(value, list):
699
+ rewritten[key] = [
700
+ _rewrite_schema_refs(item, aliases) if isinstance(item, dict) else item
701
+ for item in value
702
+ ]
703
+
704
+ return rewritten
705
+
706
+
707
+ def _sanitize_identifier(name: str) -> str:
708
+ result = "".join(ch if ch in _VALID_IDENTIFIER_CHARS else "_" for ch in name)
709
+ if result and result[0].isupper() and result[1:].islower():
710
+ result = result[0].lower() + result[1:]
711
+ if not result or result[0].isdigit():
712
+ result = f"_{result}"
713
+ return f"{result}_" if result in {"class", "from", "global", "pass"} else result
714
+
715
+
716
+ def _dedupe_name(name: str, used: set[str]) -> str:
717
+ if name not in used:
718
+ used.add(name)
719
+ return name
720
+ index = 2
721
+ while f"{name}_{index}" in used:
722
+ index += 1
723
+ deduped = f"{name}_{index}"
724
+ used.add(deduped)
725
+ return deduped