pyopenapi-gen 0.12.0__py3-none-any.whl → 0.13.0__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.
@@ -135,7 +135,16 @@ def parse_parameter(
135
135
  type="string",
136
136
  enum=sch["items"]["enum"],
137
137
  generation_name=enum_name, # Mark it as promoted
138
+ final_module_stem=NameSanitizer.sanitize_module_name(enum_name), # Set module stem for imports
138
139
  )
140
+
141
+ # Register this inline enum schema so it gets generated as a model file
142
+ if isinstance(context, ParsingContext) and enum_name not in context.parsed_schemas:
143
+ context.parsed_schemas[enum_name] = items_schema
144
+ logger.debug(
145
+ f"Registered enum schema '{enum_name}' for array parameter '{param_name}' with values {sch['items']['enum'][:3]}..."
146
+ )
147
+
139
148
  logger.debug(
140
149
  f"Created enum schema '{enum_name}' for array parameter '{param_name}' with values {sch['items']['enum'][:3]}..."
141
150
  )
@@ -17,6 +17,8 @@ from .keywords.any_of_parser import _parse_any_of_schemas
17
17
  from .keywords.one_of_parser import _parse_one_of_schemas
18
18
  from .unified_cycle_detection import CycleAction
19
19
 
20
+ logger = logging.getLogger(__name__)
21
+
20
22
  # Environment variables for configurable limits, with defaults
21
23
  try:
22
24
  MAX_CYCLES = int(os.environ.get("PYOPENAPI_MAX_CYCLES", "0")) # Default 0 means no explicit cycle count limit
@@ -27,8 +29,6 @@ try:
27
29
  except ValueError:
28
30
  ENV_MAX_DEPTH = 150 # Fallback to 150 if env var is invalid
29
31
 
30
- logger = logging.getLogger(__name__)
31
-
32
32
 
33
33
  def _resolve_ref(
34
34
  ref_path_str: str,
@@ -417,7 +417,23 @@ def _parse_schema(
417
417
  raw_type_field = schema_node.get("type")
418
418
 
419
419
  if isinstance(raw_type_field, str):
420
- extracted_type = raw_type_field
420
+ # Handle non-standard type values that might appear
421
+ if raw_type_field in ["Any", "any"]:
422
+ # Convert 'Any' to None - will be handled as object later
423
+ extracted_type = None
424
+ logger.warning(
425
+ f"Schema{f' {schema_name}' if schema_name else ''} uses non-standard type 'Any'. "
426
+ "Converting to 'object'. Use standard OpenAPI types: string, number, integer, boolean, array, object."
427
+ )
428
+ elif raw_type_field == "None":
429
+ # Convert 'None' string to null handling
430
+ extracted_type = "null"
431
+ logger.warning(
432
+ f"Schema{f' {schema_name}' if schema_name else ''} uses type 'None'. "
433
+ 'Converting to nullable object. Use \'type: ["object", "null"]\' for nullable types.'
434
+ )
435
+ else:
436
+ extracted_type = raw_type_field
421
437
  elif isinstance(raw_type_field, list):
422
438
  if "null" in raw_type_field:
423
439
  is_nullable_from_type_field = True
@@ -446,10 +462,33 @@ def _parse_schema(
446
462
  if props_from_comp or "allOf" in schema_node or "properties" in schema_node:
447
463
  current_final_type = "object"
448
464
  elif any_of_irs or one_of_irs:
465
+ # Keep None for composition types - they'll be handled by resolver
449
466
  current_final_type = None
467
+ elif "enum" in schema_node:
468
+ # Enum without explicit type - infer from enum values
469
+ enum_values = schema_node.get("enum", [])
470
+ if enum_values:
471
+ first_val = enum_values[0]
472
+ if isinstance(first_val, str):
473
+ current_final_type = "string"
474
+ elif isinstance(first_val, (int, float)):
475
+ current_final_type = "number"
476
+ elif isinstance(first_val, bool):
477
+ current_final_type = "boolean"
478
+ else:
479
+ # Fallback to object for complex enum values
480
+ current_final_type = "object"
481
+ else:
482
+ current_final_type = "string" # Default for empty enums
483
+ else:
484
+ # No type specified and no clear indicators - default to object
485
+ # This is safer than 'Any' and matches OpenAPI spec defaults
486
+ current_final_type = "object"
450
487
 
451
488
  if current_final_type == "null":
452
- current_final_type = None
489
+ # Explicit null type - mark as nullable but use object type
490
+ is_nullable_overall = True
491
+ current_final_type = "object"
453
492
 
454
493
  if current_final_type == "object":
455
494
  # Properties from allOf have already been handled by _parse_composition_keywords
@@ -511,7 +550,13 @@ def _parse_schema(
511
550
  else:
512
551
  items_ir = actual_item_ir
513
552
  else:
514
- items_ir = IRSchema(type="Any")
553
+ # Array without items specification - use object as safer default
554
+ # Log warning to help developers fix their specs
555
+ logger.warning(
556
+ f"Array type without 'items' specification found{f' in {schema_name}' if schema_name else ''}. "
557
+ "Using 'object' as item type. Consider adding 'items' to your OpenAPI spec for better type safety."
558
+ )
559
+ items_ir = IRSchema(type="object")
515
560
 
516
561
  schema_ir_name_attr = NameSanitizer.sanitize_class_name(schema_name) if schema_name else None
517
562
 
@@ -53,11 +53,12 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
53
53
 
54
54
  # Handle composition types (any_of, all_of, one_of)
55
55
  # These are processed for inline compositions or when generating the alias for a named composition
56
- if hasattr(schema, "any_of") and schema.any_of:
56
+ # Check for the attribute existence, not just truthiness, to handle empty lists
57
+ if hasattr(schema, "any_of") and schema.any_of is not None:
57
58
  return self._resolve_any_of(schema, context, required, resolve_underlying)
58
- elif hasattr(schema, "all_of") and schema.all_of:
59
+ elif hasattr(schema, "all_of") and schema.all_of is not None:
59
60
  return self._resolve_all_of(schema, context, required, resolve_underlying)
60
- elif hasattr(schema, "one_of") and schema.one_of:
61
+ elif hasattr(schema, "one_of") and schema.one_of is not None:
61
62
  return self._resolve_one_of(schema, context, required, resolve_underlying)
62
63
 
63
64
  # Handle named schemas without generation_name (fallback for references)
@@ -96,7 +97,52 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
96
97
  elif schema_type == "null":
97
98
  return self._resolve_null(context, required)
98
99
  else:
99
- logger.warning(f"Unknown schema type: {schema_type}")
100
+ # Gather detailed information about the problematic schema
101
+ schema_details = {
102
+ "type": schema_type,
103
+ "name": getattr(schema, "name", None),
104
+ "ref": getattr(schema, "ref", None),
105
+ "properties": list(getattr(schema, "properties", {}).keys()) if hasattr(schema, "properties") else None,
106
+ "enum": getattr(schema, "enum", None),
107
+ "description": getattr(schema, "description", None),
108
+ "generation_name": getattr(schema, "generation_name", None),
109
+ "is_nullable": getattr(schema, "is_nullable", None),
110
+ "any_of": len(getattr(schema, "any_of", []) or []) if hasattr(schema, "any_of") else 0,
111
+ "all_of": len(getattr(schema, "all_of", []) or []) if hasattr(schema, "all_of") else 0,
112
+ "one_of": len(getattr(schema, "one_of", []) or []) if hasattr(schema, "one_of") else 0,
113
+ }
114
+
115
+ # Remove None values for cleaner output
116
+ schema_details = {k: v for k, v in schema_details.items() if v is not None}
117
+
118
+ # Create detailed error message
119
+ error_msg = f"Unknown schema type '{schema_type}' encountered."
120
+ if schema_details.get("name"):
121
+ error_msg += f" Schema name: '{schema_details['name']}'."
122
+ if schema_details.get("ref"):
123
+ error_msg += f" Reference: '{schema_details['ref']}'."
124
+
125
+ # Log full details with actionable advice
126
+ logger.warning(f"{error_msg} Full details: {schema_details}")
127
+
128
+ # Provide specific guidance based on the unknown type
129
+ if schema_type == "Any":
130
+ logger.info(
131
+ "Schema type 'Any' will be mapped to typing.Any. Consider using a more specific type in your OpenAPI spec."
132
+ )
133
+ elif schema_type == "None" or schema_type is None:
134
+ logger.info(
135
+ "Schema type 'None' detected - likely an optional field or null type. This will be mapped to Optional[Any]."
136
+ )
137
+ return self._resolve_null(context, required)
138
+ elif schema_type and isinstance(schema_type, str):
139
+ # Unknown string type - provide helpful suggestions
140
+ logger.info(f"Unknown type '{schema_type}' - common issues:")
141
+ logger.info(" 1. Typo in type name (should be: string, integer, number, boolean, array, object)")
142
+ logger.info(" 2. Using a schema name as type (should use $ref instead)")
143
+ logger.info(" 3. Custom type not supported by OpenAPI (consider using allOf/oneOf/anyOf)")
144
+ logger.info(f" Location: Check your OpenAPI spec for schemas with type='{schema_type}'")
145
+
100
146
  return self._resolve_any(context)
101
147
 
102
148
  def _resolve_reference(
@@ -115,7 +161,12 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
115
161
  module_stem = getattr(schema, "final_module_stem", None)
116
162
 
117
163
  if not module_stem:
118
- logger.warning(f"Named schema {schema.name} missing final_module_stem")
164
+ logger.warning(f"Named schema '{schema.name}' missing final_module_stem attribute.")
165
+ logger.info(f" This usually means the schema wasn't properly processed during parsing.")
166
+ logger.info(
167
+ f" Check if '{schema.name}' is defined in components/schemas or if it's an inline schema that should be promoted."
168
+ )
169
+ logger.info(f" The schema will be treated as 'Any' type for now.")
119
170
  return ResolvedType(python_type=class_name or "Any", is_optional=not required)
120
171
 
121
172
  # Check if we're trying to import from the same module (self-import)
@@ -237,6 +288,10 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
237
288
 
238
289
  def _resolve_null(self, context: TypeContext, required: bool) -> ResolvedType:
239
290
  """Resolve null type."""
291
+ # For null types in schemas, we need to import Any for the Optional[Any] pattern
292
+ # But the type itself is None for union composition
293
+ if not required:
294
+ context.add_import("typing", "Any")
240
295
  return ResolvedType(python_type="None", is_optional=not required)
241
296
 
242
297
  def _resolve_array(
@@ -245,7 +300,11 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
245
300
  """Resolve array type."""
246
301
  items_schema = getattr(schema, "items", None)
247
302
  if not items_schema:
248
- logger.warning("Array schema missing items")
303
+ schema_name = getattr(schema, "name", "unnamed")
304
+ logger.warning(f"Array schema '{schema_name}' missing 'items' definition.")
305
+ logger.info(" Arrays in OpenAPI must define the type of items they contain.")
306
+ logger.info(' Example: { "type": "array", "items": { "type": "string" } }')
307
+ logger.info(" This will be mapped to List[Any] - consider fixing the OpenAPI spec.")
249
308
  context.add_import("typing", "List")
250
309
  context.add_import("typing", "Any")
251
310
  return ResolvedType(python_type="List[Any]", is_optional=not required)
@@ -340,10 +399,8 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
340
399
  if hasattr(sub_schema, "type") and sub_schema.type:
341
400
  return self.resolve_schema(sub_schema, context, required, resolve_underlying)
342
401
 
343
- # Fallback to first schema
344
- if schema.all_of:
345
- return self.resolve_schema(schema.all_of[0], context, required, resolve_underlying)
346
-
402
+ # Fallback - if no schema has a concrete type, return Any
403
+ # Don't recurse into schemas with no type as that causes warnings
347
404
  return self._resolve_any(context)
348
405
 
349
406
  def _resolve_one_of(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyopenapi-gen
3
- Version: 0.12.0
3
+ Version: 0.13.0
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
@@ -28,7 +28,7 @@ pyopenapi_gen/core/loader/operations/parser.py,sha256=QHg3o8TRaReYofEEcBwuDuNxGD
28
28
  pyopenapi_gen/core/loader/operations/post_processor.py,sha256=Rzb3GSiLyJk-0hTBZ6s6iWAj4KqE4Rfo3w-q2wm_R7w,2487
29
29
  pyopenapi_gen/core/loader/operations/request_body.py,sha256=r-jscZEfOmqFA-E4i1Uj3S66rNEf1gDTlBiGUiA0P9k,3224
30
30
  pyopenapi_gen/core/loader/parameters/__init__.py,sha256=p13oSibCRC5RCfsP6w7yD9MYs5TXcdI4WwPv7oGUYKk,284
31
- pyopenapi_gen/core/loader/parameters/parser.py,sha256=rOZWbykHQy7MH1gTHI61460mFG1nPKdU9A4hOsYAiIc,7505
31
+ pyopenapi_gen/core/loader/parameters/parser.py,sha256=tMAAxcPnAQQtdVFP7-NY6XSfxvc2HkG5rPXd1wwyE8I,8049
32
32
  pyopenapi_gen/core/loader/responses/__init__.py,sha256=6APWoH3IdNkgVmI0KsgZoZ6knDaG-S-pnUCa6gkzT8E,216
33
33
  pyopenapi_gen/core/loader/responses/parser.py,sha256=F9otAn6ncQiY3C25Fq4mEJ5UFNgN83eujcX20Vd_nPU,4069
34
34
  pyopenapi_gen/core/loader/schemas/__init__.py,sha256=rlhujYfw_IzWgzhVhYMJ3eIhE6C5Vi1Ylba-BHEVqOg,296
@@ -37,7 +37,7 @@ pyopenapi_gen/core/parsing/__init__.py,sha256=RJsIR6cHaNoI4tBcpMlAa0JsY64vsHb9sP
37
37
  pyopenapi_gen/core/parsing/context.py,sha256=8cM8mPItvDvJr8ZiukvdHBumlQl9hK1gUZL4BDpHaBk,8005
38
38
  pyopenapi_gen/core/parsing/cycle_helpers.py,sha256=nG5ysNavL_6lpnHWFUZR9qraBxqOzuNfI6NgSEa8a5M,5939
39
39
  pyopenapi_gen/core/parsing/schema_finalizer.py,sha256=qRTHUoVBQTgGmdfLuBuWxtWdj_SG71STGC3rn-tJvnA,6914
40
- pyopenapi_gen/core/parsing/schema_parser.py,sha256=W0kCV0ucq6Wybr7x1wZl-nMSRXVWAqI4T57ASSabZsM,30557
40
+ pyopenapi_gen/core/parsing/schema_parser.py,sha256=2hOj1Xz8a2f2PF3oAAG-DwQse9WHt9DzvR6nhQ5dHJU,33148
41
41
  pyopenapi_gen/core/parsing/unified_cycle_detection.py,sha256=3nplaCVh2dFwBPbmDc2kiU0SzTPXXktdQ5Rc0Q9Uu9s,10873
42
42
  pyopenapi_gen/core/parsing/common/__init__.py,sha256=U3sHMO-l6S3Cm04CVOYmBCpqLEZvCylUI7yQfcTwxYU,27
43
43
  pyopenapi_gen/core/parsing/common/type_parser.py,sha256=cK7xtxhoD43K2WjLP9TGip3As3akYeYW7L2XztXCecg,2562
@@ -97,7 +97,7 @@ pyopenapi_gen/types/contracts/types.py,sha256=-Qvbx3N_14AaN-1BeyocrvsjiwXPn_eWQh
97
97
  pyopenapi_gen/types/resolvers/__init__.py,sha256=_5kA49RvyOTyXgt0GbbOfHJcdQw2zHxvU9af8GGyNWc,295
98
98
  pyopenapi_gen/types/resolvers/reference_resolver.py,sha256=qnaZeLmtyh4_NBMcKib58s6o5ycUJaattYt8F38_qIo,2053
99
99
  pyopenapi_gen/types/resolvers/response_resolver.py,sha256=Kb1a2803lyoukoZy06ztPBlUw-A1lHiZ6NlJmsixxA8,6500
100
- pyopenapi_gen/types/resolvers/schema_resolver.py,sha256=PsSF-DE8-GDXmvbAJz-tHlTCR-1UATGhT4dHf6kUDQQ,17988
100
+ pyopenapi_gen/types/resolvers/schema_resolver.py,sha256=n8pI9kUJo-vLjZAOPSvwpVmRoM7AzTa7cOMlidYJVXo,21779
101
101
  pyopenapi_gen/types/services/__init__.py,sha256=inSUKmY_Vnuym6tC-AhvjCTj16GbkfxCGLESRr_uQPE,123
102
102
  pyopenapi_gen/types/services/type_service.py,sha256=-LQj7oSx1mxb10Zi6DpawS8uyoUrUbnYhmUA0GuKZTc,4402
103
103
  pyopenapi_gen/types/strategies/__init__.py,sha256=bju8_KEPNIow1-woMO-zJCgK_E0M6JnFq0NFsK1R4Ss,171
@@ -124,8 +124,8 @@ pyopenapi_gen/visit/model/alias_generator.py,sha256=TGL3AMq_PkBWFWeeXbNnA8hgO9hv
124
124
  pyopenapi_gen/visit/model/dataclass_generator.py,sha256=nyTvBph6rtbJlCwTiDW_Y2UJmLLiA6D2QJUpA2xE0m8,10289
125
125
  pyopenapi_gen/visit/model/enum_generator.py,sha256=AXqKUFuWUUjUF_6_HqBKY8vB5GYu35Pb2C2WPFrOw1k,10061
126
126
  pyopenapi_gen/visit/model/model_visitor.py,sha256=4kAQSWsI4XumVYB3aAE7Ts_31hGfDlbytRalxyMFV3g,9510
127
- pyopenapi_gen-0.12.0.dist-info/METADATA,sha256=do9Qt8fZ_bOvewCqxXYsCYbNE0qs7BtvC5Xe-AhBnBQ,14025
128
- pyopenapi_gen-0.12.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
- pyopenapi_gen-0.12.0.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
130
- pyopenapi_gen-0.12.0.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
131
- pyopenapi_gen-0.12.0.dist-info/RECORD,,
127
+ pyopenapi_gen-0.13.0.dist-info/METADATA,sha256=mGXIXN6P2PxDBwf8glZdGr7Ak_Wx2f9HfpzZfl1GVa4,14025
128
+ pyopenapi_gen-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
129
+ pyopenapi_gen-0.13.0.dist-info/entry_points.txt,sha256=gxSlNiwom50T3OEZnlocA6qRjGdV0bn6hN_Xr-Ub5wA,56
130
+ pyopenapi_gen-0.13.0.dist-info/licenses/LICENSE,sha256=UFAyTWKa4w10-QerlJaHJeep7G2gcwpf-JmvI2dS2Gc,1088
131
+ pyopenapi_gen-0.13.0.dist-info/RECORD,,