pyopenapi-gen 2.7.4__py3-none-any.whl → 3.0.1__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.
pyopenapi_gen/__init__.py CHANGED
@@ -50,7 +50,7 @@ __all__ = [
50
50
  ]
51
51
 
52
52
  # Semantic version of the generator core – automatically managed by semantic-release.
53
- __version__: str = "2.7.4"
53
+ __version__: str = "3.0.1"
54
54
 
55
55
  # ---------------------------------------------------------------------------
56
56
  # Lazy-loading and autocompletion support (This part remains)
@@ -307,17 +307,34 @@ converter.register_unstructure_hook(date, unstructure_date)
307
307
 
308
308
  def _is_union_type(t: Any) -> bool:
309
309
  """
310
- Check if a type is a Union type.
310
+ Check if a type is a Union type (including Annotated[Union, ...]).
311
311
 
312
312
  Scenario:
313
- Detect Union types including both typing.Union and Python 3.10+ X | Y syntax.
313
+ Detect Union types including both typing.Union and Python 3.10+ X | Y syntax,
314
+ as well as Annotated[Union, ...] for discriminated unions.
314
315
 
315
316
  Expected Outcome:
316
317
  Returns True for Union types, False otherwise.
317
318
  """
318
319
  origin = get_origin(t)
319
320
  # Handle both typing.Union and types.UnionType (Python 3.10+ X | Y syntax)
320
- return origin is Union or (hasattr(types, "UnionType") and isinstance(t, types.UnionType))
321
+ if origin is Union or (hasattr(types, "UnionType") and isinstance(t, types.UnionType)):
322
+ return True
323
+
324
+ # Also handle Annotated[Union, ...] for discriminated unions
325
+ # Import Annotated here to avoid module-level import
326
+ try:
327
+ from typing import Annotated
328
+
329
+ if origin is Annotated:
330
+ # Check if the first arg is a Union
331
+ args = get_args(t)
332
+ if args and _is_union_type(args[0]):
333
+ return True
334
+ except ImportError:
335
+ pass
336
+
337
+ return False
321
338
 
322
339
 
323
340
  def _truncate_data_repr(data: Any, max_length: int = 200) -> str:
@@ -353,10 +370,11 @@ def _structure_union(data: Any, union_type: type) -> Any:
353
370
 
354
371
  Strategy:
355
372
  1. If data is None and NoneType is in the union, return None
356
- 2. If data is a dict, try each dataclass variant
357
- 3. Try structuring with other variants (generic types, registered hooks)
358
- 4. Fall back to dict[str, Any] if present
359
- 5. Raise error if no variant matches
373
+ 2. Check for discriminator metadata for O(1) variant lookup
374
+ 3. If data is a dict, try each dataclass variant
375
+ 4. Try structuring with other variants (generic types, registered hooks)
376
+ 5. Fall back to dict[str, Any] if present
377
+ 6. Raise error if no variant matches
360
378
 
361
379
  Args:
362
380
  data: The raw data to structure
@@ -369,7 +387,24 @@ def _structure_union(data: Any, union_type: type) -> Any:
369
387
  TypeError: If data is None but NoneType not in union
370
388
  ValueError: If no Union variant matches the data
371
389
  """
372
- args = get_args(union_type)
390
+ # If this is Annotated[Union[...], metadata], extract the Union and metadata
391
+ origin = get_origin(union_type)
392
+ try:
393
+ from typing import Annotated
394
+
395
+ if origin is Annotated:
396
+ # Extract Union type and metadata from Annotated
397
+ annotated_args = get_args(union_type)
398
+ if annotated_args:
399
+ # First arg is the actual Union, rest are metadata
400
+ actual_union = annotated_args[0]
401
+ args = get_args(actual_union)
402
+ else:
403
+ args = get_args(union_type)
404
+ else:
405
+ args = get_args(union_type)
406
+ except ImportError:
407
+ args = get_args(union_type)
373
408
 
374
409
  # Handle None explicitly
375
410
  if data is None:
@@ -377,6 +412,49 @@ def _structure_union(data: Any, union_type: type) -> Any:
377
412
  return None
378
413
  raise TypeError(f"None is not valid for {union_type}")
379
414
 
415
+ # Check for discriminator metadata (from Annotated[Union[...], discriminator])
416
+ # This enables O(1) lookup instead of O(n) sequential tries
417
+ if hasattr(union_type, "__metadata__"):
418
+ for metadata in union_type.__metadata__:
419
+ # Check if this is discriminator metadata (has property_name and get_mapping method)
420
+ if hasattr(metadata, "property_name") and hasattr(metadata, "get_mapping"):
421
+ if isinstance(data, dict) and metadata.property_name in data:
422
+ discriminator_value = data[metadata.property_name]
423
+
424
+ # Get the mapping using the get_mapping method
425
+ mapping = metadata.get_mapping()
426
+
427
+ if mapping and discriminator_value in mapping:
428
+ variant = mapping[discriminator_value]
429
+
430
+ # Handle forward reference strings
431
+ if isinstance(variant, str):
432
+ # Try to find the actual type from union args
433
+ for arg in args:
434
+ if hasattr(arg, "__name__") and arg.__name__ == variant:
435
+ variant = arg
436
+ break
437
+
438
+ # Attempt to structure with the discriminated variant
439
+ try:
440
+ _register_structure_hooks_recursively(variant)
441
+ return converter.structure(data, variant)
442
+ except Exception as e:
443
+ # Provide clear error message for discriminated variant failure
444
+ raise ValueError(
445
+ f"Failed to deserialize as {variant.__name__} "
446
+ f"(discriminator {metadata.property_name}={discriminator_value!r}): {e}"
447
+ ) from e
448
+ elif mapping:
449
+ # Unknown discriminator value
450
+ valid_values = list(mapping.keys())
451
+ raise ValueError(
452
+ f"Unknown discriminator value {discriminator_value!r} "
453
+ f"for field {metadata.property_name!r}. "
454
+ f"Expected one of: {valid_values}"
455
+ )
456
+ # If discriminator property not in data, fall through to sequential try
457
+
380
458
  # Separate variants by type category
381
459
  dataclass_variants: list[type[Any]] = []
382
460
  dict_any_fallback = False
@@ -37,12 +37,16 @@ def build_schemas(raw_schemas: dict[str, Mapping[str, Any]], raw_components: Map
37
37
 
38
38
  # Build initial IR for all schemas found in components
39
39
  for n, nd in raw_schemas.items():
40
- if n not in context.parsed_schemas:
40
+ # Check if schema is already registered (either by original name or sanitized name)
41
+ sanitized_n = NameSanitizer.sanitize_class_name(n)
42
+ if n not in context.parsed_schemas and sanitized_n not in context.parsed_schemas:
41
43
  _parse_schema(n, nd, context, allow_self_reference=True)
42
44
 
43
- # Post-condition check
44
- if not all(n in context.parsed_schemas for n in raw_schemas):
45
- raise RuntimeError("Not all schemas were parsed")
45
+ # Post-condition check: each raw schema must be registered under either its original or sanitized name
46
+ for n in raw_schemas:
47
+ sanitized_n = NameSanitizer.sanitize_class_name(n)
48
+ if n not in context.parsed_schemas and sanitized_n not in context.parsed_schemas:
49
+ raise RuntimeError(f"Schema '{n}' (sanitized: '{sanitized_n}') was not parsed")
46
50
 
47
51
  return context
48
52
 
@@ -192,6 +196,13 @@ def extract_inline_enums(schemas: dict[str, IRSchema]) -> dict[str, IRSchema]:
192
196
 
193
197
  # Extract inline enums from properties
194
198
  for prop_name, prop_schema in list(schema.properties.items()):
199
+ # Debug: log property state
200
+ logger.debug(
201
+ f"Processing {schema_name}.{prop_name}: "
202
+ f"type={prop_schema.type}, enum={prop_schema.enum}, "
203
+ f"name={prop_schema.name}, generation_name={prop_schema.generation_name}"
204
+ )
205
+
195
206
  # Check if this property has an inline enum that needs extraction
196
207
  # An inline enum needs extraction if:
197
208
  # 1. It has enum values defined
@@ -204,6 +215,7 @@ def extract_inline_enums(schemas: dict[str, IRSchema]) -> dict[str, IRSchema]:
204
215
  # Check if the enum was already extracted or is a named reference
205
216
  # Case 1: generation_name exists in schemas dict (already extracted)
206
217
  # Case 2: property name itself is a schema reference (e.g., ExistingStatusEnum)
218
+ # Case 3: property.type is already an enum name (created during parsing)
207
219
  enum_already_extracted = (
208
220
  (
209
221
  prop_schema.generation_name
@@ -224,9 +236,28 @@ def extract_inline_enums(schemas: dict[str, IRSchema]) -> dict[str, IRSchema]:
224
236
  and "_" not in prop_schema.name
225
237
  and prop_schema.name != prop_name # Name differs from property key
226
238
  )
239
+ or (
240
+ # Property.type is already an enum schema name (created during parsing)
241
+ prop_schema.type
242
+ and prop_schema.type in schemas
243
+ and schemas[prop_schema.type].enum
244
+ )
227
245
  )
228
246
 
229
247
  if has_inline_enum and not enum_already_extracted:
248
+ # Check if this enum was already created during parsing
249
+ # If prop_schema.type is already an enum name (not a primitive type), reuse it
250
+ if prop_schema.type and prop_schema.type in schemas and schemas[prop_schema.type].enum:
251
+ # Enum already exists - just ensure property references it correctly
252
+ enum_name = prop_schema.type
253
+ logger.debug(f"Reusing existing enum {enum_name} for {schema_name}.{prop_name}")
254
+ # Don't create a new enum, just update property references
255
+ prop_schema.name = enum_name
256
+ prop_schema.generation_name = schemas[enum_name].generation_name or enum_name
257
+ prop_schema.final_module_stem = schemas[enum_name].final_module_stem
258
+ prop_schema.enum = None # Clear inline enum
259
+ continue # Skip creating new enum
260
+
230
261
  # Use property's existing generation_name if set, otherwise create a new name
231
262
  # This keeps naming consistent with what the type resolver already assigned
232
263
  if prop_schema.generation_name:
@@ -10,6 +10,7 @@ from typing import Any, Callable, List, Mapping, Set, Tuple
10
10
 
11
11
  from pyopenapi_gen import IRSchema
12
12
  from pyopenapi_gen.core.utils import NameSanitizer
13
+ from pyopenapi_gen.ir import IRDiscriminator
13
14
 
14
15
  from .context import ParsingContext
15
16
  from .keywords.all_of_parser import _process_all_of
@@ -86,8 +87,16 @@ def _parse_composition_keywords(
86
87
  context: ParsingContext,
87
88
  max_depth: int,
88
89
  parse_fn: Callable[[str | None, Mapping[str, Any] | None, ParsingContext, int | None], IRSchema],
89
- ) -> Tuple[List[IRSchema] | None, List[IRSchema] | None, List[IRSchema] | None, dict[str, IRSchema], Set[str], bool]:
90
- """Parse composition keywords (anyOf, oneOf, allOf) from a schema node.
90
+ ) -> Tuple[
91
+ List[IRSchema] | None,
92
+ List[IRSchema] | None,
93
+ List[IRSchema] | None,
94
+ dict[str, IRSchema],
95
+ Set[str],
96
+ bool,
97
+ IRDiscriminator | None,
98
+ ]:
99
+ """Parse composition keywords (anyOf, oneOf, allOf) and discriminator from a schema node.
91
100
 
92
101
  Contracts:
93
102
  Pre-conditions:
@@ -97,7 +106,7 @@ def _parse_composition_keywords(
97
106
  - parse_fn is a callable for parsing schemas
98
107
  Post-conditions:
99
108
  - Returns a tuple of (any_of_schemas, one_of_schemas, all_of_components,
100
- properties, required_fields, is_nullable)
109
+ properties, required_fields, is_nullable, discriminator)
101
110
  """
102
111
  any_of_schemas: List[IRSchema] | None = None
103
112
  one_of_schemas: List[IRSchema] | None = None
@@ -105,6 +114,7 @@ def _parse_composition_keywords(
105
114
  merged_properties: dict[str, IRSchema] = {}
106
115
  merged_required_set: Set[str] = set()
107
116
  is_nullable: bool = False
117
+ discriminator: IRDiscriminator | None = None
108
118
 
109
119
  if "anyOf" in node:
110
120
  parsed_sub_schemas, nullable_from_sub, _ = _parse_any_of_schemas(node["anyOf"], context, max_depth, parse_fn)
@@ -116,6 +126,18 @@ def _parse_composition_keywords(
116
126
  one_of_schemas = parsed_sub_schemas
117
127
  is_nullable = is_nullable or nullable_from_sub
118
128
 
129
+ # Extract discriminator if present (applies to oneOf or anyOf)
130
+ if "discriminator" in node and isinstance(node["discriminator"], Mapping):
131
+ disc_node = node["discriminator"]
132
+ property_name = disc_node.get("propertyName")
133
+ if property_name:
134
+ # Extract mapping if present
135
+ mapping = None
136
+ if "mapping" in disc_node and isinstance(disc_node["mapping"], Mapping):
137
+ mapping = dict(disc_node["mapping"])
138
+
139
+ discriminator = IRDiscriminator(property_name=property_name, mapping=mapping)
140
+
119
141
  if "allOf" in node:
120
142
  merged_properties, merged_required_set, parsed_all_of_components = _process_all_of(
121
143
  node, name, context, parse_fn, max_depth=max_depth
@@ -123,7 +145,15 @@ def _parse_composition_keywords(
123
145
  else:
124
146
  merged_required_set = set(node.get("required", []))
125
147
 
126
- return any_of_schemas, one_of_schemas, parsed_all_of_components, merged_properties, merged_required_set, is_nullable
148
+ return (
149
+ any_of_schemas,
150
+ one_of_schemas,
151
+ parsed_all_of_components,
152
+ merged_properties,
153
+ merged_required_set,
154
+ is_nullable,
155
+ discriminator,
156
+ )
127
157
 
128
158
 
129
159
  def _parse_properties(
@@ -190,7 +220,21 @@ def _parse_properties(
190
220
  and not promoted_ir_schema._max_depth_exceeded_marker
191
221
  and not promoted_ir_schema._is_circular_ref
192
222
  ):
193
- context.parsed_schemas[promoted_schema_name] = promoted_ir_schema
223
+ # CRITICAL: Register using the schema's sanitized name to prevent duplicate keys
224
+ registration_key = promoted_ir_schema.name if promoted_ir_schema.name else promoted_schema_name
225
+
226
+ # Check for collision
227
+ if registration_key in context.parsed_schemas:
228
+ existing_schema = context.parsed_schemas[registration_key]
229
+ if existing_schema is not promoted_ir_schema:
230
+ # Collision - use original name
231
+ registration_key = promoted_schema_name
232
+ logger.debug(
233
+ f"Promoted schema collision: {promoted_ir_schema.name!r} already registered. "
234
+ f"Using original name {promoted_schema_name!r}."
235
+ )
236
+
237
+ context.parsed_schemas[registration_key] = promoted_ir_schema
194
238
  else:
195
239
  # Directly parse other inline types (string, number, array of simple types, etc.)
196
240
  # or objects that are not being promoted (e.g. if parent_schema_name is None)
@@ -248,8 +292,18 @@ def _parse_properties(
248
292
  )
249
293
  )
250
294
 
251
- # Use a sanitized version of prop_name as context name for this sub-parse
252
- prop_context_name = NameSanitizer.sanitize_class_name(prop_name)
295
+ # Use a sanitized version of prop_name combined with parent schema name for unique context
296
+ # This ensures properties with the same name in different schemas get unique enum names
297
+ if parent_schema_name:
298
+ sanitized_prop_name = NameSanitizer.sanitize_class_name(prop_name)
299
+ # Avoid redundant prefixing if the property name already starts with the parent schema name
300
+ # e.g., Entry + entry_specific_role -> EntrySpecificRole (not EntryEntrySpecificRole)
301
+ if sanitized_prop_name.lower().startswith(parent_schema_name.lower()):
302
+ prop_context_name = sanitized_prop_name
303
+ else:
304
+ prop_context_name = f"{parent_schema_name}{sanitized_prop_name}"
305
+ else:
306
+ prop_context_name = NameSanitizer.sanitize_class_name(prop_name)
253
307
 
254
308
  # For simple primitives and simple arrays, avoid creating separate schemas
255
309
  if (is_simple_primitive or is_simple_array) and prop_context_name in context.parsed_schemas:
@@ -301,7 +355,7 @@ def _parse_properties(
301
355
  prop_is_nullable = True
302
356
 
303
357
  property_holder_ir = IRSchema(
304
- name=prop_name, # The actual property name
358
+ name=None, # Property name is the dict key, not stored in the schema object
305
359
  # Type is the name of the (potentially registered) anonymous schema
306
360
  type=parsed_prop_schema_ir.name,
307
361
  description=prop_schema_node.get("description", parsed_prop_schema_ir.description),
@@ -317,27 +371,63 @@ def _parse_properties(
317
371
  else:
318
372
  # Simpler type, or error placeholder. Assign directly but ensure original prop_name is used.
319
373
  # Also, try to respect original node's description, default, example, nullable if available.
320
- final_prop_ir = parsed_prop_schema_ir
321
- # Always assign the property name - this is the property's name in the parent object
322
- final_prop_ir.name = prop_name
323
- if isinstance(prop_schema_node, Mapping):
324
- final_prop_ir.description = prop_schema_node.get(
325
- "description", parsed_prop_schema_ir.description
326
- )
327
- final_prop_ir.default = prop_schema_node.get("default", parsed_prop_schema_ir.default)
328
- final_prop_ir.example = prop_schema_node.get("example", parsed_prop_schema_ir.example)
329
- current_prop_node_nullable = prop_schema_node.get("nullable", False)
330
- type_list_nullable = (
331
- isinstance(prop_schema_node.get("type"), list) and "null" in prop_schema_node["type"]
332
- )
333
- final_prop_ir.is_nullable = (
334
- final_prop_ir.is_nullable or current_prop_node_nullable or type_list_nullable
335
- )
336
- # If the sub-parse didn't pick up an enum (e.g. for simple types), take it from prop_schema_node
337
- if not final_prop_ir.enum and "enum" in prop_schema_node:
338
- final_prop_ir.enum = prop_schema_node["enum"]
339
374
 
340
- parsed_props[prop_name] = final_prop_ir
375
+ # CRITICAL: If the schema was registered as a standalone schema (e.g., inline enum with parent context name),
376
+ # we must NOT modify its name attribute. Instead, create a reference holder.
377
+ schema_was_registered_standalone = (
378
+ schema_name_for_parsing is not None
379
+ and parsed_prop_schema_ir.name == schema_name_for_parsing
380
+ and context.is_schema_parsed(schema_name_for_parsing)
381
+ )
382
+
383
+ if schema_was_registered_standalone:
384
+ # Schema is registered - create a reference holder instead of modifying the schema
385
+ # CRITICAL: Do NOT set 'name' field for property holders - the property name is the dict key.
386
+ # Setting 'name' would cause __post_init__ to sanitize it, changing 'sender_role' to 'SenderRole'.
387
+ prop_is_nullable = False
388
+ if isinstance(prop_schema_node, Mapping):
389
+ if "nullable" in prop_schema_node:
390
+ prop_is_nullable = prop_schema_node["nullable"]
391
+ elif isinstance(prop_schema_node.get("type"), list) and "null" in prop_schema_node["type"]:
392
+ prop_is_nullable = True
393
+ elif parsed_prop_schema_ir.is_nullable:
394
+ prop_is_nullable = True
395
+
396
+ property_holder_ir = IRSchema(
397
+ name=None, # Property name is the dict key, not stored in the schema object
398
+ type=parsed_prop_schema_ir.name, # Reference to the registered schema
399
+ description=prop_schema_node.get("description", parsed_prop_schema_ir.description),
400
+ is_nullable=prop_is_nullable,
401
+ default=prop_schema_node.get("default"),
402
+ example=prop_schema_node.get("example"),
403
+ enum=prop_schema_node.get("enum") if not parsed_prop_schema_ir.enum else None,
404
+ format=parsed_prop_schema_ir.format,
405
+ _refers_to_schema=parsed_prop_schema_ir,
406
+ )
407
+ parsed_props[prop_name] = property_holder_ir
408
+ else:
409
+ # Schema was not registered - safe to modify its name
410
+ final_prop_ir = parsed_prop_schema_ir
411
+ # Always assign the property name - this is the property's name in the parent object
412
+ final_prop_ir.name = prop_name
413
+ if isinstance(prop_schema_node, Mapping):
414
+ final_prop_ir.description = prop_schema_node.get(
415
+ "description", parsed_prop_schema_ir.description
416
+ )
417
+ final_prop_ir.default = prop_schema_node.get("default", parsed_prop_schema_ir.default)
418
+ final_prop_ir.example = prop_schema_node.get("example", parsed_prop_schema_ir.example)
419
+ current_prop_node_nullable = prop_schema_node.get("nullable", False)
420
+ type_list_nullable = (
421
+ isinstance(prop_schema_node.get("type"), list) and "null" in prop_schema_node["type"]
422
+ )
423
+ final_prop_ir.is_nullable = (
424
+ final_prop_ir.is_nullable or current_prop_node_nullable or type_list_nullable
425
+ )
426
+ # If the sub-parse didn't pick up an enum (e.g. for simple types), take it from prop_schema_node
427
+ if not final_prop_ir.enum and "enum" in prop_schema_node:
428
+ final_prop_ir.enum = prop_schema_node["enum"]
429
+
430
+ parsed_props[prop_name] = final_prop_ir
341
431
  return parsed_props
342
432
 
343
433
 
@@ -400,11 +490,15 @@ def _parse_schema(
400
490
 
401
491
  # If we reach here, detection_result.action == CycleAction.CONTINUE_PARSING
402
492
 
493
+ # Sanitize schema name early for consistent use throughout parsing
494
+ # This ensures property enums get named correctly with the sanitized parent name
495
+ sanitized_schema_name = NameSanitizer.sanitize_class_name(schema_name) if schema_name else None
496
+
403
497
  try: # Ensure exit_schema is called
404
498
  if schema_node is None:
405
499
  # Create empty schema for null schema nodes
406
500
  # Do NOT set generation_name - null schemas should resolve to Any inline, not generate separate files
407
- return IRSchema(name=NameSanitizer.sanitize_class_name(schema_name) if schema_name else None)
501
+ return IRSchema(name=sanitized_schema_name)
408
502
 
409
503
  if not isinstance(schema_node, Mapping):
410
504
  raise TypeError(
@@ -473,14 +567,20 @@ def _parse_schema(
473
567
  elif is_nullable_from_type_field:
474
568
  extracted_type = "null"
475
569
 
476
- any_of_irs, one_of_irs, all_of_components_irs, props_from_comp, req_from_comp, nullable_from_comp = (
477
- _parse_composition_keywords(
478
- schema_node,
479
- schema_name,
480
- context,
481
- ENV_MAX_DEPTH,
482
- lambda n, sn, c, md: _parse_schema(n, sn, c, md, allow_self_reference),
483
- )
570
+ (
571
+ any_of_irs,
572
+ one_of_irs,
573
+ all_of_components_irs,
574
+ props_from_comp,
575
+ req_from_comp,
576
+ nullable_from_comp,
577
+ discriminator_ir,
578
+ ) = _parse_composition_keywords(
579
+ schema_node,
580
+ schema_name,
581
+ context,
582
+ ENV_MAX_DEPTH,
583
+ lambda n, sn, c, md: _parse_schema(n, sn, c, md, allow_self_reference),
484
584
  )
485
585
 
486
586
  # Check for direct nullable field (OpenAPI 3.0 Swagger extension)
@@ -526,7 +626,7 @@ def _parse_schema(
526
626
  if "properties" in schema_node:
527
627
  final_properties_for_ir = _parse_properties(
528
628
  schema_node["properties"],
529
- schema_name,
629
+ sanitized_schema_name, # Use sanitized name for consistent enum naming
530
630
  props_from_comp, # these are from allOf merge
531
631
  context,
532
632
  max_depth_override,
@@ -641,7 +741,8 @@ def _parse_schema(
641
741
  )
642
742
  items_ir = IRSchema(type="object")
643
743
 
644
- schema_ir_name_attr = NameSanitizer.sanitize_class_name(schema_name) if schema_name else None
744
+ # Use the already-sanitized schema name
745
+ schema_ir_name_attr = sanitized_schema_name
645
746
 
646
747
  # Parse additionalProperties field
647
748
  additional_properties_value: bool | IRSchema | None = None
@@ -666,6 +767,7 @@ def _parse_schema(
666
767
  any_of=any_of_irs,
667
768
  one_of=one_of_irs,
668
769
  all_of=all_of_components_irs,
770
+ discriminator=discriminator_ir,
669
771
  required=sorted(list(final_required_fields_set)),
670
772
  description=schema_node.get("description"),
671
773
  format=schema_node.get("format") if isinstance(schema_node.get("format"), str) else None,
@@ -762,7 +864,27 @@ def _parse_schema(
762
864
  and not is_synthetic_primitive
763
865
  )
764
866
  if should_register and schema_name:
765
- context.parsed_schemas[schema_name] = schema_ir
867
+ # CRITICAL: Register using the schema's sanitized name (schema_ir.name) to prevent duplicate keys.
868
+ # The schema object's name attribute is already sanitized, so we use that as the key.
869
+ # This prevents the post-processor from registering the same schema again under a different key.
870
+ # However, if multiple raw schemas sanitize to the same name (e.g., "Foo-Bar" and "FooBar" both -> "FooBar"),
871
+ # we need to detect collisions and use the original raw schema name as a fallback.
872
+ registration_key = schema_ir.name if schema_ir.name else schema_name
873
+
874
+ # Check if this is a collision between different raw schemas
875
+ if registration_key in context.parsed_schemas:
876
+ existing_schema = context.parsed_schemas[registration_key]
877
+ # If it's the same schema object (e.g., from cycle detection returning existing), don't re-register
878
+ if existing_schema is not schema_ir:
879
+ # Collision detected - use the original raw schema name as key to keep both schemas
880
+ # This preserves backward compatibility with the collision handling in ModelsEmitter
881
+ registration_key = schema_name
882
+ logger.debug(
883
+ f"Schema name collision detected: {schema_ir.name!r} already registered. "
884
+ f"Using original raw name {schema_name!r} as key."
885
+ )
886
+
887
+ context.parsed_schemas[registration_key] = schema_ir
766
888
 
767
889
  # Set generation_name and final_module_stem for schemas that will be generated as separate files
768
890
  # Skip for synthetic primitives (inline types) - they should remain without these attributes
@@ -125,7 +125,8 @@ class NameSanitizer:
125
125
 
126
126
  @staticmethod
127
127
  def sanitize_module_name(name: str) -> str:
128
- """Convert a raw name into a valid Python module name in snake_case, splitting camel case and PascalCase."""
128
+ """Convert a raw name into a valid Python module name in snake_case,
129
+ splitting camel case and PascalCase."""
129
130
  # # <<< Add Check for problematic input >>>
130
131
  # if '[' in name or ']' in name or ',' in name:
131
132
  # logger.error(f"sanitize_module_name received potentially invalid input: '{name}'")
@@ -195,7 +196,8 @@ class NameSanitizer:
195
196
 
196
197
  @staticmethod
197
198
  def sanitize_method_name(name: str) -> str:
198
- """Convert a raw name into a valid Python method name in snake_case, splitting camelCase and PascalCase."""
199
+ """Convert a raw name into a valid Python method name in snake_case,
200
+ splitting camelCase and PascalCase."""
199
201
  # Remove curly braces
200
202
  name = re.sub(r"[{}]", "", name)
201
203
  # Split camelCase and PascalCase to snake_case
@@ -261,7 +263,8 @@ class KwargsBuilder:
261
263
 
262
264
 
263
265
  class Formatter:
264
- """Helper to format code using Black, falling back to unformatted content if Black is unavailable or errors."""
266
+ """Helper to format code using Black, falling back to unformatted content if
267
+ Black is unavailable or errors."""
265
268
 
266
269
  def __init__(self) -> None:
267
270
  from typing import Any, Callable
@@ -329,7 +332,8 @@ class DataclassSerializer:
329
332
  The serialized object with dataclasses converted to dictionaries.
330
333
 
331
334
  Handles:
332
- - Dataclass instances with Meta.key_transform_with_dump: Applies field name mapping (snake_case → camelCase)
335
+ - Dataclass instances with Meta.key_transform_with_dump: Applies field
336
+ name mapping (snake_case → camelCase)
333
337
  - Legacy BaseSchema instances: Falls back to to_dict() if present (backward compatibility)
334
338
  - Regular dataclass instances: Converted to dictionaries using field names
335
339
  - Lists: Recursively serialize each item
@@ -389,7 +393,28 @@ class DataclassSerializer:
389
393
  if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
390
394
  visited.add(obj_id)
391
395
  try:
392
- # Build dict from dataclass fields without recursive unstructuring
396
+ # Check if there's a custom cattrs unstructure hook for this type
397
+ try:
398
+ from pyopenapi_gen.core.cattrs_converter import converter
399
+
400
+ obj_type = type(obj)
401
+ # Get the unstructure hook for this type
402
+ hook = converter.get_unstructure_hook(obj_type)
403
+
404
+ # Check if it's a custom hook (not auto-generated by cattrs)
405
+ # Auto-generated hooks have names like "unstructure_ClassName"
406
+ expected_auto_gen_name = f"unstructure_{obj_type.__name__}"
407
+ is_custom_hook = hook.__name__ != expected_auto_gen_name
408
+ if is_custom_hook:
409
+ # Custom unstructure hook exists - use it
410
+ unstructured = converter.unstructure(obj)
411
+ # Recursively serialize the result
412
+ return DataclassSerializer._serialize_with_tracking(unstructured, visited)
413
+ except Exception: # nosec B110
414
+ # cattrs_converter not available or check failed - fall through to manual iteration
415
+ pass
416
+
417
+ # Manual field iteration (for dataclasses without custom hooks or when cattrs fails)
393
418
  data: dict[str, Any] = {}
394
419
  for field in dataclasses.fields(obj):
395
420
  value = getattr(obj, field.name)
@@ -31,12 +31,24 @@ class PythonConstructRenderer:
31
31
  - Generic classes (with bases, docstrings, and body)
32
32
  """
33
33
 
34
+ @staticmethod
35
+ def _to_module_name(class_name: str) -> str:
36
+ """Convert PascalCase class name to snake_case module name."""
37
+ import re
38
+
39
+ # Insert underscore before uppercase letters (except first)
40
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", class_name)
41
+ # Insert underscore before uppercase letters followed by lowercase
42
+ s2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1)
43
+ return s2.lower()
44
+
34
45
  def render_alias(
35
46
  self,
36
47
  alias_name: str,
37
48
  target_type: str,
38
49
  description: str | None,
39
50
  context: RenderContext,
51
+ discriminator: "IRDiscriminator | None" = None, # type: ignore[name-defined]
40
52
  ) -> str:
41
53
  """
42
54
  Render a type alias assignment as Python code.
@@ -46,6 +58,7 @@ class PythonConstructRenderer:
46
58
  target_type: The target type expression
47
59
  description: Optional description for the docstring
48
60
  context: The rendering context for import registration
61
+ discriminator: Optional discriminator metadata for Union types
49
62
 
50
63
  Returns:
51
64
  Formatted Python code for the type alias
@@ -64,10 +77,70 @@ class PythonConstructRenderer:
64
77
  context.add_typing_imports_for_type(target_type)
65
78
 
66
79
  # Add __all__ export
67
- writer.write_line(f'__all__ = ["{alias_name}"]')
80
+ exports = [alias_name]
81
+ if discriminator:
82
+ # Also export the discriminator metadata class
83
+ exports.append(f"{alias_name}Discriminator")
84
+ writer.write_line(f"__all__ = {exports!r}")
68
85
  writer.write_line("") # Add a blank line for separation
69
86
 
70
- writer.write_line(f"{alias_name}: TypeAlias = {target_type}")
87
+ # If there's a discriminator, generate the metadata class and use Annotated
88
+ if discriminator and target_type.startswith("Union["):
89
+ # Extract the union variants
90
+ context.add_import("typing", "Annotated")
91
+ context.add_import("dataclasses", "dataclass")
92
+ context.add_import("dataclasses", "field")
93
+
94
+ # Generate discriminator metadata class (frozen for hashability in Annotated)
95
+ writer.write_line("@dataclass(frozen=True)")
96
+ writer.write_line(f"class {alias_name}Discriminator:")
97
+ writer.write_line(f' """Discriminator metadata for {alias_name} union."""')
98
+ writer.write_line("")
99
+ writer.write_line(f' property_name: str = "{discriminator.property_name}"')
100
+ writer.write_line(f' """The discriminator property name"""')
101
+ writer.write_line("")
102
+
103
+ # Store mapping as immutable tuple for frozen dataclass, convert to dict at runtime
104
+ if discriminator.mapping:
105
+ # Store discriminator mapping as tuple of tuples (immutable for frozen dataclass)
106
+ writer.write_line(" # Mapping stored as tuple for frozen dataclass compatibility")
107
+ writer.write_line(" _mapping_data: tuple[tuple[str, str], ...] = (")
108
+ for disc_value, schema_ref in discriminator.mapping.items():
109
+ schema_name = schema_ref.split("/")[-1]
110
+ writer.write_line(f' ("{disc_value}", "{schema_name}"),')
111
+ writer.write_line(" )")
112
+ writer.write_line("")
113
+ writer.write_line(" def get_mapping(self) -> dict[str, type]:")
114
+ writer.write_line(' """Get discriminator mapping with actual type references."""')
115
+ # Import types locally
116
+ for disc_value, schema_ref in discriminator.mapping.items():
117
+ schema_name = schema_ref.split("/")[-1]
118
+ module_name = self._to_module_name(schema_name)
119
+ writer.write_line(f" from .{module_name} import {schema_name}")
120
+ writer.write_line(" return {")
121
+ for disc_value, schema_ref in discriminator.mapping.items():
122
+ schema_name = schema_ref.split("/")[-1]
123
+ writer.write_line(f' "{disc_value}": {schema_name},')
124
+ writer.write_line(" }")
125
+ else:
126
+ writer.write_line(" _mapping_data: tuple[tuple[str, str], ...] | None = None")
127
+ writer.write_line("")
128
+ writer.write_line(" def get_mapping(self) -> dict[str, type] | None:")
129
+ writer.write_line(' """Get discriminator mapping."""')
130
+ writer.write_line(" return None")
131
+
132
+ writer.write_line("")
133
+ writer.write_line("")
134
+
135
+ # Use Annotated to attach discriminator metadata
136
+ writer.write_line(f"{alias_name}: TypeAlias = Annotated[")
137
+ writer.write_line(f" {target_type},")
138
+ writer.write_line(f" {alias_name}Discriminator()")
139
+ writer.write_line("]")
140
+ else:
141
+ # No discriminator, use plain type alias
142
+ writer.write_line(f"{alias_name}: TypeAlias = {target_type}")
143
+
71
144
  if description:
72
145
  # Sanitize description for use within a triple-double-quoted string for the actual docstring
73
146
  safe_desc_content = description.replace("\\", "\\\\") # Escape backslashes first
@@ -278,15 +278,52 @@ class ModelsEmitter:
278
278
  # Update the main reference to use filtered schemas
279
279
  all_schemas_for_generation = filtered_schemas_for_generation
280
280
 
281
+ # Collect all nested schemas from oneOf/anyOf/allOf that need naming
282
+ def collect_nested_schemas(schema: IRSchema, collected: dict[int, IRSchema], visited_ids: Set[int]) -> None:
283
+ """Recursively collect all nested schemas that need generation_name."""
284
+ schema_id = id(schema)
285
+ if schema_id in visited_ids:
286
+ return # Avoid infinite loops
287
+
288
+ visited_ids.add(schema_id)
289
+ if schema.name: # Only collect schemas with names
290
+ collected[schema_id] = schema
291
+
292
+ # Process oneOf variants
293
+ if schema.one_of:
294
+ for variant in schema.one_of:
295
+ if variant.name and id(variant) not in visited_ids:
296
+ collect_nested_schemas(variant, collected, visited_ids)
297
+
298
+ # Process anyOf variants
299
+ if schema.any_of:
300
+ for variant in schema.any_of:
301
+ if variant.name and id(variant) not in visited_ids:
302
+ collect_nested_schemas(variant, collected, visited_ids)
303
+
304
+ # Process allOf components (though they typically merge, not union)
305
+ if schema.all_of:
306
+ for component in schema.all_of:
307
+ if component.name and id(component) not in visited_ids:
308
+ collect_nested_schemas(component, collected, visited_ids)
309
+
310
+ # Collect all schemas including nested ones
311
+ # Deduplicate schemas by ID first - same schema object may appear under multiple keys
312
+ unique_schemas_by_id = {id(s): s for s in all_schemas_for_generation.values()}
313
+ all_schemas_including_nested: dict[int, IRSchema] = {}
314
+ visited_schema_ids: Set[int] = set()
315
+ for schema in unique_schemas_by_id.values():
316
+ collect_nested_schemas(schema, all_schemas_including_nested, visited_schema_ids)
317
+
281
318
  schemas_to_name_decollision = sorted(
282
- [s for s in all_schemas_for_generation.values()],
283
- key=lambda s: s.name, # type: ignore
319
+ list(all_schemas_including_nested.values()),
320
+ key=lambda s: s.name if s.name else "",
284
321
  )
285
322
 
286
- # logger.debug(
287
- # f"ModelsEmitter: Schemas to actually de-collide (post-filter by s.name): "
288
- # f"{[s.name for s in schemas_to_name_decollision]}"
289
- # )
323
+ logger.debug(
324
+ f"ModelsEmitter: Schemas to actually de-collide (post-filter by s.name): "
325
+ f"{[(s.name, id(s)) for s in schemas_to_name_decollision]}"
326
+ )
290
327
 
291
328
  for schema_for_naming in schemas_to_name_decollision: # Use the comprehensive list
292
329
  original_schema_name = schema_for_naming.name
pyopenapi_gen/ir.py CHANGED
@@ -14,6 +14,27 @@ from .http_types import HTTPMethod
14
14
  # pass
15
15
 
16
16
 
17
+ @dataclass
18
+ class IRDiscriminator:
19
+ """
20
+ Discriminator metadata for oneOf/anyOf unions.
21
+
22
+ OpenAPI discriminator enables efficient runtime type resolution by specifying
23
+ which property identifies the variant and the mapping from property values to schema types.
24
+ """
25
+
26
+ property_name: str
27
+ """The name of the property used to discriminate between variants (e.g., 'type')"""
28
+
29
+ mapping: dict[str, str] | None = None
30
+ """Optional mapping from discriminator values to schema references.
31
+
32
+ Example: {'agent': '#/components/schemas/WorkflowAgentNode', 'end': '#/components/schemas/WorkflowEndNode'}
33
+
34
+ If None, the discriminator property value directly identifies the schema name.
35
+ """
36
+
37
+
17
38
  @dataclass
18
39
  class IRSchema:
19
40
  name: str | None = None
@@ -31,6 +52,7 @@ class IRSchema:
31
52
  any_of: List[IRSchema] | None = None
32
53
  one_of: List[IRSchema] | None = None
33
54
  all_of: List[IRSchema] | None = None # Store the list of IRSchema objects from allOf
55
+ discriminator: IRDiscriminator | None = None # Discriminator for oneOf/anyOf unions
34
56
  title: str | None = None # Added title
35
57
  is_data_wrapper: bool = False # True if schema is a simple {{ "data": OtherSchema }} wrapper
36
58
 
@@ -94,6 +116,10 @@ class IRSchema:
94
116
  if isinstance(self.additional_properties, dict):
95
117
  self.additional_properties = IRSchema(**self.additional_properties)
96
118
 
119
+ # Ensure discriminator is an IRDiscriminator instance
120
+ if isinstance(self.discriminator, dict):
121
+ self.discriminator = IRDiscriminator(**self.discriminator)
122
+
97
123
  for comp_list_attr in ["any_of", "one_of", "all_of"]:
98
124
  comp_list = getattr(self, comp_list_attr)
99
125
  if isinstance(comp_list, list):
@@ -71,11 +71,15 @@ class AliasGenerator:
71
71
 
72
72
  # logger.debug(f"AliasGenerator: Rendering alias '{alias_name}' for target type '{target_type}'.")
73
73
 
74
+ # Pass discriminator metadata if this is a union type
75
+ discriminator = schema.discriminator if hasattr(schema, "discriminator") else None
76
+
74
77
  rendered_code = self.renderer.render_alias(
75
78
  alias_name=alias_name,
76
79
  target_type=target_type,
77
80
  description=schema.description,
78
81
  context=context,
82
+ discriminator=discriminator,
79
83
  )
80
84
 
81
85
  # Post-condition
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyopenapi-gen
3
- Version: 2.7.4
3
+ Version: 3.0.1
4
4
  Summary: Modern, async-first Python client generator for OpenAPI specifications with advanced cycle detection and unified type resolution
5
5
  Project-URL: Homepage, https://github.com/your-org/pyopenapi-gen
6
6
  Project-URL: Documentation, https://github.com/your-org/pyopenapi-gen/blob/main/README.md
@@ -1,8 +1,8 @@
1
- pyopenapi_gen/__init__.py,sha256=zuPT808r7WFgX96LDB9lbpItlNt_2LTfDrwNuStMMiE,7632
1
+ pyopenapi_gen/__init__.py,sha256=Df2VrSmifyuu_8NbGBjzN9JRvXFoLv9FlqKk4x-_qnE,7632
2
2
  pyopenapi_gen/__main__.py,sha256=4-SCaCNhBd7rtyRK58uoDbdl93J0KhUeajP_b0CPpLE,110
3
3
  pyopenapi_gen/cli.py,sha256=TA3bpe8kmUuyA4lZqAHHx5YkWQvrNq81WuEDAtPO6JQ,2147
4
4
  pyopenapi_gen/http_types.py,sha256=EMMYZBt8PNVZKPFu77TQija-JI-nOKyXvpiQP9-VSWE,467
5
- pyopenapi_gen/ir.py,sha256=lGrn5VzU5gXHc_Tkjfte2AeMMsTyzAci4dy0bi5aG1E,7243
5
+ pyopenapi_gen/ir.py,sha256=VDA7ZFCm5-EU5r_k4H0zkKKDNp-cljg44xPhywN0wBw,8229
6
6
  pyopenapi_gen/py.typed,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
7
7
  pyopenapi_gen/context/CLAUDE.md,sha256=eUPvSY2ADQK21i52bWfzyBcDPVvvepErMiQrq6ndwlU,9004
8
8
  pyopenapi_gen/context/file_manager.py,sha256=vpbRByO5SH6COdjb6C-pXkdSIRu7QFqrXxi69VLKBnM,1691
@@ -10,7 +10,7 @@ pyopenapi_gen/context/import_collector.py,sha256=5WzQ5fXxARHJFZmYZ_GjPG5YjjzRLZT
10
10
  pyopenapi_gen/context/render_context.py,sha256=fAq0u3rDMZ0_OEHegrRUd98ySxs390XJbdypqQX8bwI,34894
11
11
  pyopenapi_gen/core/CLAUDE.md,sha256=bz48K-PSrhxCq5ScmiLiU9kfpVVzSWRKOA9RdKk_pbg,6482
12
12
  pyopenapi_gen/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- pyopenapi_gen/core/cattrs_converter.py,sha256=qaQz08_TTaM9xLfjZMcTJFO5ghSLp0yuThmGW-Xkdns,27796
13
+ pyopenapi_gen/core/cattrs_converter.py,sha256=k-2rcUUKc6AnSbnTNhqvEQYMIeZRl89czlr52bPceBo,31460
14
14
  pyopenapi_gen/core/exceptions.py,sha256=HYFiYdmzsZUl46vB8M3B6Vpp6m8iqjUcKDWdL4yEKHo,498
15
15
  pyopenapi_gen/core/http_status_codes.py,sha256=nn8QdXmkv5BQA9C-HTn9wmAVWyEyB9bAHpHve6EZX4M,6249
16
16
  pyopenapi_gen/core/http_transport.py,sha256=JWTka-p8VUKS9vL7NciZUaiZqpjchBYxzx_MrdCyVYo,9836
@@ -19,7 +19,7 @@ pyopenapi_gen/core/postprocess_manager.py,sha256=Ia4H47lInhVxkHnDB46t4U-6BLpLXzh
19
19
  pyopenapi_gen/core/spec_fetcher.py,sha256=kAEj5XgYvV-IA0UHKn8_bglWxtscQwvJXZncC4hzmTM,4601
20
20
  pyopenapi_gen/core/streaming_helpers.py,sha256=5fkzH9xsgzZWOTKFrZpmje07S7n7CcOpjteb5dig7ds,2664
21
21
  pyopenapi_gen/core/telemetry.py,sha256=LNMrlrUNVcp591w9cX4uvzlFrPB6ZZoGRIuCOHlDCqA,2296
22
- pyopenapi_gen/core/utils.py,sha256=4733xWshc_pnj1mLrCWTTj0Pdv-H42fGbh5bcmawQBc,16409
22
+ pyopenapi_gen/core/utils.py,sha256=oeZ3GIZGuODEQS5EIQ8uOZs88fDVOV-4mjo5HXxqCp0,17638
23
23
  pyopenapi_gen/core/warning_collector.py,sha256=DYl9D7eZYs04mDU84KeonS-5-d0aM7hNqraXTex31ss,2799
24
24
  pyopenapi_gen/core/auth/base.py,sha256=E2KUerA_mYv9D7xulUm-lenIxqZHqanjA4oRKpof2ZE,792
25
25
  pyopenapi_gen/core/auth/plugins.py,sha256=u4GZTykoxGwGaWAQyFeTdPKi-pSK7Dp0DCeg5RsrSw4,3446
@@ -34,12 +34,12 @@ pyopenapi_gen/core/loader/parameters/parser.py,sha256=TROPMq6iyL9Somh7K1ADjqyDGX
34
34
  pyopenapi_gen/core/loader/responses/__init__.py,sha256=6APWoH3IdNkgVmI0KsgZoZ6knDaG-S-pnUCa6gkzT8E,216
35
35
  pyopenapi_gen/core/loader/responses/parser.py,sha256=6dZ86WvyBxPq68UQjF_F8F2FtmGtMm5ZB8UjKXZTu9Y,4063
36
36
  pyopenapi_gen/core/loader/schemas/__init__.py,sha256=rlhujYfw_IzWgzhVhYMJ3eIhE6C5Vi1Ylba-BHEVqOg,296
37
- pyopenapi_gen/core/loader/schemas/extractor.py,sha256=VJtEM2QPuNSyrIdll43qD00StYIdmuUMcg49oTiyPqQ,13224
37
+ pyopenapi_gen/core/loader/schemas/extractor.py,sha256=bx7EG0f4fDOT8H9E0tiZAjs-jtfJmY9cwHmdHgZrAfY,15249
38
38
  pyopenapi_gen/core/parsing/__init__.py,sha256=RJsIR6cHaNoI4tBcpMlAa0JsY64vsHb9sPxPg6rd8FQ,486
39
39
  pyopenapi_gen/core/parsing/context.py,sha256=oahabtftjYiyeKNHwc3fWjSGrIoCeNnKN-KhWofiOmk,7968
40
40
  pyopenapi_gen/core/parsing/cycle_helpers.py,sha256=HyVQBWU78PbypyppHq34yxr6BG4W5Bt4ev5kkGUyvkg,5969
41
41
  pyopenapi_gen/core/parsing/schema_finalizer.py,sha256=BMx7nlg5tECbKDfv6XEwumNNPkzkL5qRPdYrRI8qrxo,6856
42
- pyopenapi_gen/core/parsing/schema_parser.py,sha256=DlC5dyLMCB8O-EBU5oxPgh4AQKU8wj_nD8C5xhoQqWc,41912
42
+ pyopenapi_gen/core/parsing/schema_parser.py,sha256=3XlzkDOdv-DVJhHy_34bJt4SLkrxb7LaSbwipQny08M,48838
43
43
  pyopenapi_gen/core/parsing/unified_cycle_detection.py,sha256=MXghh1Ip6P1etfjuzPiRiDHBdFdQUb9nFUQqhexcvfc,10836
44
44
  pyopenapi_gen/core/parsing/common/__init__.py,sha256=U3sHMO-l6S3Cm04CVOYmBCpqLEZvCylUI7yQfcTwxYU,27
45
45
  pyopenapi_gen/core/parsing/common/type_parser.py,sha256=h9pg0KWFezjNYvo-A1Dx_ADA7dp4THeMb_JKmZRJk1Q,2542
@@ -65,7 +65,7 @@ pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py,sha256=d9DZ1CV
65
65
  pyopenapi_gen/core/writers/code_writer.py,sha256=5rWMPRdj5dWu3NRER6do4NEJEIa9qL68Hc7zhJ8jSdg,4763
66
66
  pyopenapi_gen/core/writers/documentation_writer.py,sha256=ZnXJsRc9y9bUMsQR7g0qoBJZbBo2MCxHH_5KgxrPtyA,8645
67
67
  pyopenapi_gen/core/writers/line_writer.py,sha256=-K2FaMtQa6hdzGZLcjQrT2ItEmfE-kquTCrn6R3I_QA,7743
68
- pyopenapi_gen/core/writers/python_construct_renderer.py,sha256=IDAlk2yQkDntjLk3Uc3kKIONpaiWzggcNItVP8dbfk8,12356
68
+ pyopenapi_gen/core/writers/python_construct_renderer.py,sha256=ILR7wdCgjk8VWZn_WYCzMolSJhTrwyqByfvOjq3cZ48,16406
69
69
  pyopenapi_gen/core_package_template/README.md,sha256=8YP-MS0KxphRbCGBf7kV3dYIFLU9piOJ3IMm3K_0hcI,1488
70
70
  pyopenapi_gen/emit/models_emitter.py,sha256=MBRq71UjtlZHrpf9QhDN0wXk_X-oGeGd6TAq3RKG7ko,7180
71
71
  pyopenapi_gen/emitters/CLAUDE.md,sha256=iZYEZq1a1h033rxuh97cMpsKUElv72ysvTm3-QQUvrs,9323
@@ -75,7 +75,7 @@ pyopenapi_gen/emitters/docs_emitter.py,sha256=aouKqhRdtVvYfGVsye_uqM80nONRy0SqN0
75
75
  pyopenapi_gen/emitters/endpoints_emitter.py,sha256=m4Kts3-hxflNyCXcKtakyeHLELKLqVjcKLl4_F_1rhQ,10716
76
76
  pyopenapi_gen/emitters/exceptions_emitter.py,sha256=PfbDQX7dfgg2htvxEh40t7FR7b3BrK8jeRd5INu_kjk,7547
77
77
  pyopenapi_gen/emitters/mocks_emitter.py,sha256=D8fjD8KbgG-yFiqCEEAe2rKE2B439pW6bRLvoqQ6SdI,7431
78
- pyopenapi_gen/emitters/models_emitter.py,sha256=wJwtvCGEmhy5yhojfoUW2CXNOQytGlN4J8-GcwoYIMY,22221
78
+ pyopenapi_gen/emitters/models_emitter.py,sha256=OU8UNg18QdYop1lUZ2aAi4Co-hm_71ZRT6PKnG4oEYw,24107
79
79
  pyopenapi_gen/generator/CLAUDE.md,sha256=BS9KkmLvk2WD-Io-_apoWjGNeMU4q4LKy4UOxYF9WxM,10870
80
80
  pyopenapi_gen/generator/client_generator.py,sha256=ZtMTNXBbsLkqsVdFPepMhB2jsmV4poNKAR7pM-Wyvpo,29229
81
81
  pyopenapi_gen/generator/exceptions.py,sha256=6mgO_3pk1U61AVyRpv00UTPY5UYMUzY3MmfgmMv-4mM,168
@@ -126,12 +126,12 @@ pyopenapi_gen/visit/endpoint/processors/__init__.py,sha256=_6RqpOdDuDheArqDBi3yk
126
126
  pyopenapi_gen/visit/endpoint/processors/import_analyzer.py,sha256=ou5pl3S6PXvHrhKeBQraRfK9MTOQE1WUGLeieAVUXRM,3364
127
127
  pyopenapi_gen/visit/endpoint/processors/parameter_processor.py,sha256=E0VoygkhU8iCsvH0U-tA6ZZg2Nm3rfr4e17vxqQLH7c,7666
128
128
  pyopenapi_gen/visit/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
129
- pyopenapi_gen/visit/model/alias_generator.py,sha256=wEMHipPA1_CFxvQ6CS9j4qgXK93seI1bI_tFJvIrb70,3563
129
+ pyopenapi_gen/visit/model/alias_generator.py,sha256=NPFEMWzhrSHFCOdYpT_G-9QJTuyq7bxvIfT3a-fVggg,3758
130
130
  pyopenapi_gen/visit/model/dataclass_generator.py,sha256=NVQY3PNptVJn31zJv3pysPT8skLLoPObcQ906NvJ6gs,21869
131
131
  pyopenapi_gen/visit/model/enum_generator.py,sha256=AXqKUFuWUUjUF_6_HqBKY8vB5GYu35Pb2C2WPFrOw1k,10061
132
132
  pyopenapi_gen/visit/model/model_visitor.py,sha256=Xrlk0Re5PN7XhaO3R3q2G4Qf0p8kkZeWlBkyCuaLS98,9857
133
- pyopenapi_gen-2.7.4.dist-info/METADATA,sha256=q7XtojZUBPDVkB5bQ1iN2T60pbxcT1YgHe58AdWHxDw,39057
134
- pyopenapi_gen-2.7.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
135
- pyopenapi_gen-2.7.4.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
136
- pyopenapi_gen-2.7.4.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
137
- pyopenapi_gen-2.7.4.dist-info/RECORD,,
133
+ pyopenapi_gen-3.0.1.dist-info/METADATA,sha256=pNnWkFqEHuNgW5PYiKRgEDKkAsEA4wamleh4wsJ8pYM,39057
134
+ pyopenapi_gen-3.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
135
+ pyopenapi_gen-3.0.1.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
136
+ pyopenapi_gen-3.0.1.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
137
+ pyopenapi_gen-3.0.1.dist-info/RECORD,,