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.
- pyopenapi_gen/core/loader/parameters/parser.py +9 -0
- pyopenapi_gen/core/parsing/schema_parser.py +50 -5
- pyopenapi_gen/types/resolvers/schema_resolver.py +67 -10
- {pyopenapi_gen-0.12.0.dist-info → pyopenapi_gen-0.13.0.dist-info}/METADATA +1 -1
- {pyopenapi_gen-0.12.0.dist-info → pyopenapi_gen-0.13.0.dist-info}/RECORD +8 -8
- {pyopenapi_gen-0.12.0.dist-info → pyopenapi_gen-0.13.0.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.12.0.dist-info → pyopenapi_gen-0.13.0.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.12.0.dist-info → pyopenapi_gen-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
344
|
-
|
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.
|
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=
|
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=
|
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=
|
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.
|
128
|
-
pyopenapi_gen-0.
|
129
|
-
pyopenapi_gen-0.
|
130
|
-
pyopenapi_gen-0.
|
131
|
-
pyopenapi_gen-0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|