pyopenapi-gen 0.8.3__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 +114 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +86 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +630 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/exceptions.py +25 -0
- pyopenapi_gen/core/http_transport.py +219 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +158 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +155 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
- pyopenapi_gen/core/loader/operations/request_body.py +85 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +121 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +104 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
- pyopenapi_gen/core/parsing/context.py +184 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
- pyopenapi_gen/core/parsing/schema_parser.py +610 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +161 -0
- pyopenapi_gen/core/schemas.py +40 -0
- pyopenapi_gen/core/streaming_helpers.py +86 -0
- pyopenapi_gen/core/telemetry.py +67 -0
- pyopenapi_gen/core/utils.py +409 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
- pyopenapi_gen/emitters/models_emitter.py +428 -0
- pyopenapi_gen/generator/client_generator.py +562 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +552 -0
- pyopenapi_gen/helpers/type_cleaner.py +341 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +167 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +30 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +133 -0
- pyopenapi_gen/visit/client_visitor.py +228 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +52 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +89 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
- pyopenapi_gen/visit/model/enum_generator.py +200 -0
- pyopenapi_gen/visit/model/model_visitor.py +197 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
- pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
- pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
- pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-0.8.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,184 @@
|
|
1
|
+
"""
|
2
|
+
Defines the ParsingContext dataclass used to manage state during OpenAPI schema parsing.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import os
|
9
|
+
from dataclasses import dataclass, field
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Tuple
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from pyopenapi_gen import IRSchema
|
14
|
+
|
15
|
+
# from pyopenapi_gen.core.utils import NameSanitizer # If needed later
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class ParsingContext:
|
22
|
+
"""Manages shared state and context during the schema parsing process."""
|
23
|
+
|
24
|
+
raw_spec_schemas: Dict[str, Mapping[str, Any]] = field(default_factory=dict)
|
25
|
+
raw_spec_components: Mapping[str, Any] = field(default_factory=dict)
|
26
|
+
parsed_schemas: Dict[str, IRSchema] = field(default_factory=dict)
|
27
|
+
visited_refs: Set[str] = field(default_factory=set)
|
28
|
+
global_schema_names: Set[str] = field(default_factory=set)
|
29
|
+
package_root_name: Optional[str] = None
|
30
|
+
# name_sanitizer: NameSanitizer = field(default_factory=NameSanitizer) # Decided to instantiate where needed for now
|
31
|
+
collected_warnings: List[str] = field(default_factory=list) # For collecting warnings from helpers
|
32
|
+
|
33
|
+
# Cycle detection
|
34
|
+
currently_parsing: List[str] = field(default_factory=list)
|
35
|
+
recursion_depth: int = 0
|
36
|
+
cycle_detected: bool = False
|
37
|
+
|
38
|
+
def __post_init__(self) -> None:
|
39
|
+
# Initialize logger for the context instance if needed, or rely on module logger
|
40
|
+
self.logger = logger # or logging.getLogger(f"{__name__}.ParsingContext")
|
41
|
+
|
42
|
+
# Initialize unified cycle detection context
|
43
|
+
# Import here to avoid circular imports
|
44
|
+
from .unified_cycle_detection import UnifiedCycleContext
|
45
|
+
|
46
|
+
# Get max depth from environment or default
|
47
|
+
max_depth = int(os.environ.get("PYOPENAPI_MAX_DEPTH", 150))
|
48
|
+
|
49
|
+
self.unified_cycle_context = UnifiedCycleContext(
|
50
|
+
parsed_schemas=self.parsed_schemas, max_depth=max_depth # Share the same parsed_schemas dict
|
51
|
+
)
|
52
|
+
|
53
|
+
def unified_enter_schema(self, schema_name: Optional[str]) -> Any:
|
54
|
+
"""Enter schema using unified cycle detection system."""
|
55
|
+
from .unified_cycle_detection import unified_enter_schema
|
56
|
+
|
57
|
+
result = unified_enter_schema(schema_name, self.unified_cycle_context)
|
58
|
+
|
59
|
+
# Update legacy fields for backward compatibility
|
60
|
+
self.recursion_depth = self.unified_cycle_context.recursion_depth
|
61
|
+
self.cycle_detected = self.unified_cycle_context.cycle_detected
|
62
|
+
self.currently_parsing = self.unified_cycle_context.schema_stack.copy()
|
63
|
+
|
64
|
+
return result
|
65
|
+
|
66
|
+
def unified_exit_schema(self, schema_name: Optional[str]) -> None:
|
67
|
+
"""Exit schema using unified cycle detection system."""
|
68
|
+
from .unified_cycle_detection import unified_exit_schema
|
69
|
+
|
70
|
+
unified_exit_schema(schema_name, self.unified_cycle_context)
|
71
|
+
|
72
|
+
# Update legacy fields for backward compatibility
|
73
|
+
self.recursion_depth = self.unified_cycle_context.recursion_depth
|
74
|
+
self.currently_parsing = self.unified_cycle_context.schema_stack.copy()
|
75
|
+
|
76
|
+
def clear_cycle_state(self) -> None:
|
77
|
+
"""Clear both legacy and unified cycle detection state."""
|
78
|
+
# Clear legacy state
|
79
|
+
self.currently_parsing.clear()
|
80
|
+
self.recursion_depth = 0
|
81
|
+
self.cycle_detected = False
|
82
|
+
|
83
|
+
# Clear unified context state
|
84
|
+
self.unified_cycle_context.schema_stack.clear()
|
85
|
+
self.unified_cycle_context.schema_states.clear()
|
86
|
+
self.unified_cycle_context.recursion_depth = 0
|
87
|
+
self.unified_cycle_context.detected_cycles.clear()
|
88
|
+
self.unified_cycle_context.depth_exceeded_schemas.clear()
|
89
|
+
self.unified_cycle_context.cycle_detected = False
|
90
|
+
|
91
|
+
def enter_schema(self, schema_name: Optional[str]) -> Tuple[bool, Optional[str]]:
|
92
|
+
self.recursion_depth += 1
|
93
|
+
|
94
|
+
if schema_name is None:
|
95
|
+
return False, None
|
96
|
+
|
97
|
+
# Named cycle detection using ordered list currently_parsing
|
98
|
+
if schema_name in self.currently_parsing:
|
99
|
+
self.cycle_detected = True
|
100
|
+
try:
|
101
|
+
start_index = self.currently_parsing.index(schema_name)
|
102
|
+
# Path is from the first occurrence of schema_name to the current end of stack
|
103
|
+
cycle_path_list = self.currently_parsing[start_index:]
|
104
|
+
except ValueError: # Should not happen
|
105
|
+
cycle_path_list = list(self.currently_parsing) # Fallback
|
106
|
+
|
107
|
+
cycle_path_list.append(schema_name) # Add the re-entrant schema_name to show the loop
|
108
|
+
cycle_path_str = " -> ".join(cycle_path_list)
|
109
|
+
|
110
|
+
return True, cycle_path_str
|
111
|
+
|
112
|
+
self.currently_parsing.append(schema_name)
|
113
|
+
return False, None
|
114
|
+
|
115
|
+
def exit_schema(self, schema_name: Optional[str]) -> None:
|
116
|
+
if self.recursion_depth == 0:
|
117
|
+
self.logger.error("Cannot exit schema: recursion depth would go below zero.")
|
118
|
+
return
|
119
|
+
|
120
|
+
self.recursion_depth -= 1
|
121
|
+
if schema_name is not None:
|
122
|
+
if self.currently_parsing and self.currently_parsing[-1] == schema_name:
|
123
|
+
self.currently_parsing.pop()
|
124
|
+
elif (
|
125
|
+
schema_name in self.currently_parsing
|
126
|
+
): # Not last on stack but present: indicates mismatched enter/exit or error
|
127
|
+
self.logger.error(
|
128
|
+
f"Exiting schema '{schema_name}' which is not at the top of the parsing stack. "
|
129
|
+
f"Stack: {self.currently_parsing}. This indicates an issue."
|
130
|
+
)
|
131
|
+
# Attempt to remove it to prevent it being stuck, though this is a recovery attempt.
|
132
|
+
try:
|
133
|
+
self.currently_parsing.remove(schema_name)
|
134
|
+
except ValueError:
|
135
|
+
pass # Should not happen if it was in the list.
|
136
|
+
# If schema_name is None, or (it's not None and not in currently_parsing), do nothing to currently_parsing.
|
137
|
+
# The latter case could be if exit_schema is called for a schema_name that wasn't pushed
|
138
|
+
# (e.g., after yielding a placeholder, where the original enter_schema
|
139
|
+
# didn't add it because it was already a cycle).
|
140
|
+
|
141
|
+
def reset_for_new_parse(self) -> None:
|
142
|
+
self.recursion_depth = 0
|
143
|
+
self.cycle_detected = False
|
144
|
+
self.currently_parsing.clear()
|
145
|
+
self.parsed_schemas.clear()
|
146
|
+
|
147
|
+
def get_current_path_for_logging(self) -> str:
|
148
|
+
"""Helper to get a string representation of the current parsing path for logs."""
|
149
|
+
return " -> ".join(self.currently_parsing)
|
150
|
+
|
151
|
+
def get_parsed_schemas_for_emitter(self) -> Dict[str, IRSchema]:
|
152
|
+
# ---- START RESTORE ----
|
153
|
+
return {
|
154
|
+
name: schema
|
155
|
+
for name, schema in self.parsed_schemas.items()
|
156
|
+
if not getattr(schema, "_is_circular_ref", False)
|
157
|
+
and not getattr(schema, "_from_unresolved_ref", False)
|
158
|
+
and not getattr(schema, "_max_depth_exceeded_marker", False)
|
159
|
+
}
|
160
|
+
# ---- END RESTORE ----
|
161
|
+
|
162
|
+
def is_schema_parsed(self, schema_name: str) -> bool:
|
163
|
+
"""Check if a schema with the given name has been parsed.
|
164
|
+
|
165
|
+
Contracts:
|
166
|
+
Preconditions:
|
167
|
+
- schema_name is a valid string
|
168
|
+
Postconditions:
|
169
|
+
- Returns True if the schema exists in parsed_schemas, False otherwise
|
170
|
+
"""
|
171
|
+
assert isinstance(schema_name, str), "schema_name must be a string"
|
172
|
+
return schema_name in self.parsed_schemas
|
173
|
+
|
174
|
+
def get_parsed_schema(self, schema_name: str) -> Optional["IRSchema"]:
|
175
|
+
"""Get a parsed schema by its name.
|
176
|
+
|
177
|
+
Contracts:
|
178
|
+
Preconditions:
|
179
|
+
- schema_name is a valid string
|
180
|
+
Postconditions:
|
181
|
+
- Returns the IRSchema if it exists, None otherwise
|
182
|
+
"""
|
183
|
+
assert isinstance(schema_name, str), "schema_name must be a string"
|
184
|
+
return self.parsed_schemas.get(schema_name)
|
@@ -0,0 +1,123 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from pyopenapi_gen import IRSchema
|
5
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
6
|
+
|
7
|
+
from .context import ParsingContext
|
8
|
+
|
9
|
+
# Define module-level logger
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
def _handle_cycle_detection(
|
14
|
+
original_name: str, cycle_path: str, context: ParsingContext, allow_self_reference: bool
|
15
|
+
) -> IRSchema:
|
16
|
+
"""Handle case where a cycle is detected in schema references.
|
17
|
+
|
18
|
+
Contracts:
|
19
|
+
Pre-conditions:
|
20
|
+
- original_name is not None
|
21
|
+
- context is a valid ParsingContext instance
|
22
|
+
- allow_self_reference indicates if direct self-references are permitted without being treated as errors.
|
23
|
+
Post-conditions:
|
24
|
+
- Returns an IRSchema instance.
|
25
|
+
- If not a permitted self-reference, it's marked as circular and registered.
|
26
|
+
- If a permitted self-reference, a placeholder is returned and not marked as an error cycle.
|
27
|
+
"""
|
28
|
+
schema_ir_name_attr = NameSanitizer.sanitize_class_name(original_name)
|
29
|
+
|
30
|
+
# Check for direct self-reference when allowed
|
31
|
+
path_parts = cycle_path.split(" -> ")
|
32
|
+
is_direct_self_ref = len(path_parts) == 2 and path_parts[0] == original_name and path_parts[1] == original_name
|
33
|
+
|
34
|
+
if allow_self_reference and is_direct_self_ref:
|
35
|
+
# Permitted direct self-reference, creating placeholder without marking as error cycle
|
36
|
+
if original_name not in context.parsed_schemas:
|
37
|
+
# Create a basic placeholder. It will be fully populated when its real definition is parsed.
|
38
|
+
# Key is NOT to mark _is_circular_ref = True here.
|
39
|
+
schema = IRSchema(
|
40
|
+
name=schema_ir_name_attr,
|
41
|
+
type="object", # Default type, might be refined if we parse its own definition later
|
42
|
+
description=f"[Self-referential placeholder for {original_name}]",
|
43
|
+
_from_unresolved_ref=False, # Not unresolved in the error sense
|
44
|
+
_is_self_referential_stub=True, # New flag to indicate this state
|
45
|
+
)
|
46
|
+
context.parsed_schemas[original_name] = schema
|
47
|
+
return schema
|
48
|
+
else:
|
49
|
+
# If it's already in parsed_schemas, it means we're re-entering it.
|
50
|
+
# This could happen if it was created as a placeholder by another ref first.
|
51
|
+
# Ensure it's marked as a self-referential stub if not already.
|
52
|
+
existing_schema = context.parsed_schemas[original_name]
|
53
|
+
if not getattr(existing_schema, "_is_self_referential_stub", False):
|
54
|
+
existing_schema._is_self_referential_stub = True # Mark it
|
55
|
+
return existing_schema
|
56
|
+
|
57
|
+
# If not a permitted direct self-reference, or if self-references are not allowed, proceed with error cycle handling
|
58
|
+
if original_name not in context.parsed_schemas:
|
59
|
+
schema = IRSchema(
|
60
|
+
name=schema_ir_name_attr,
|
61
|
+
type="object",
|
62
|
+
description=f"[Circular reference detected: {cycle_path}]",
|
63
|
+
_from_unresolved_ref=True,
|
64
|
+
_circular_ref_path=cycle_path,
|
65
|
+
_is_circular_ref=True,
|
66
|
+
)
|
67
|
+
context.parsed_schemas[original_name] = schema
|
68
|
+
else:
|
69
|
+
schema = context.parsed_schemas[original_name]
|
70
|
+
schema._is_circular_ref = True
|
71
|
+
schema._from_unresolved_ref = True
|
72
|
+
schema._circular_ref_path = cycle_path
|
73
|
+
if schema.name != schema_ir_name_attr:
|
74
|
+
schema.name = schema_ir_name_attr
|
75
|
+
|
76
|
+
context.cycle_detected = True
|
77
|
+
return schema
|
78
|
+
|
79
|
+
|
80
|
+
def _handle_max_depth_exceeded(original_name: Optional[str], context: ParsingContext, max_depth: int) -> IRSchema:
|
81
|
+
"""Handle case where maximum recursion depth is exceeded.
|
82
|
+
|
83
|
+
Contracts:
|
84
|
+
Pre-conditions:
|
85
|
+
- context is a valid ParsingContext instance
|
86
|
+
- max_depth >= 0
|
87
|
+
Post-conditions:
|
88
|
+
- Returns an IRSchema instance marked with _max_depth_exceeded_marker=True
|
89
|
+
- If original_name is provided, the schema is registered in context.parsed_schemas
|
90
|
+
"""
|
91
|
+
schema_ir_name_attr = NameSanitizer.sanitize_class_name(original_name) if original_name else None
|
92
|
+
|
93
|
+
# path_prefix = schema_ir_name_attr if schema_ir_name_attr else "<anonymous_schema>"
|
94
|
+
# cycle_path_for_desc = f"{path_prefix} -> MAX_DEPTH_EXCEEDED"
|
95
|
+
description = f"[Maximum recursion depth ({max_depth}) exceeded for '{original_name or 'anonymous'}']"
|
96
|
+
logger.warning(description)
|
97
|
+
|
98
|
+
placeholder_schema = IRSchema(
|
99
|
+
name=schema_ir_name_attr,
|
100
|
+
type="object", # Default type for a placeholder created due to depth
|
101
|
+
description=description,
|
102
|
+
_max_depth_exceeded_marker=True,
|
103
|
+
# Do NOT set _is_circular_ref or _from_unresolved_ref here just for depth limit
|
104
|
+
)
|
105
|
+
|
106
|
+
if original_name is not None:
|
107
|
+
if original_name not in context.parsed_schemas:
|
108
|
+
context.parsed_schemas[original_name] = placeholder_schema
|
109
|
+
else:
|
110
|
+
# If a schema with this name already exists (e.g. a forward ref stub),
|
111
|
+
# update it to mark that max depth was hit during its resolution attempt.
|
112
|
+
# This is tricky because we don't want to overwrite a fully parsed schema.
|
113
|
+
# For now, let's assume if we are here, the existing one is also some form of placeholder
|
114
|
+
# or its parsing was interrupted to get here.
|
115
|
+
existing_schema = context.parsed_schemas[original_name]
|
116
|
+
existing_schema.description = description # Update description
|
117
|
+
existing_schema._max_depth_exceeded_marker = True
|
118
|
+
# Avoid re-assigning to placeholder_schema directly to keep existing IR object if it was complex
|
119
|
+
# and just needs this flag + description update.
|
120
|
+
return existing_schema # Return the (now updated) existing schema
|
121
|
+
|
122
|
+
# context.cycle_detected = True # Max depth is not strictly a cycle in the schema definition itself
|
123
|
+
return placeholder_schema
|
@@ -0,0 +1 @@
|
|
1
|
+
# keyword-specific parsers
|
@@ -0,0 +1,77 @@
|
|
1
|
+
"""
|
2
|
+
Handles the 'allOf' keyword in an OpenAPI schema, merging properties and required fields.
|
3
|
+
Renamed from all_of_merger to all_of_parser for consistency.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import os
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Set, Tuple
|
10
|
+
|
11
|
+
from pyopenapi_gen import IRSchema
|
12
|
+
|
13
|
+
from ..context import ParsingContext
|
14
|
+
|
15
|
+
ENV_MAX_DEPTH = int(os.environ.get("PYOPENAPI_MAX_DEPTH", "100"))
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
def _process_all_of(
|
22
|
+
node: Mapping[str, Any],
|
23
|
+
current_schema_name: Optional[str],
|
24
|
+
context: ParsingContext,
|
25
|
+
_parse_schema_func: Callable[[Optional[str], Optional[Mapping[str, Any]], ParsingContext, Optional[int]], IRSchema],
|
26
|
+
max_depth: int = ENV_MAX_DEPTH,
|
27
|
+
) -> Tuple[Dict[str, IRSchema], Set[str], List[IRSchema]]:
|
28
|
+
"""Processes the 'allOf' keyword in a schema node.
|
29
|
+
|
30
|
+
Merges properties and required fields from all sub-schemas listed in 'allOf'
|
31
|
+
and also from any direct 'properties' defined at the same level as 'allOf'.
|
32
|
+
|
33
|
+
Contracts:
|
34
|
+
Pre-conditions:
|
35
|
+
- node is a non-empty mapping representing an OpenAPI schema node.
|
36
|
+
- context is a valid ParsingContext instance.
|
37
|
+
- _parse_schema_func is a callable function.
|
38
|
+
- max_depth is a non-negative integer.
|
39
|
+
Post-conditions:
|
40
|
+
- Returns a tuple containing:
|
41
|
+
- merged_properties: Dict of property names to IRSchema.
|
42
|
+
- merged_required: Set of required property names.
|
43
|
+
- parsed_all_of_components: List of IRSchema for each item in 'allOf' (empty if 'allOf' not present).
|
44
|
+
"""
|
45
|
+
# Pre-conditions
|
46
|
+
assert isinstance(node, Mapping) and node, "node must be a non-empty Mapping"
|
47
|
+
assert isinstance(context, ParsingContext), "context must be a ParsingContext instance"
|
48
|
+
assert callable(_parse_schema_func), "_parse_schema_func must be callable"
|
49
|
+
assert isinstance(max_depth, int) and max_depth >= 0, "max_depth must be a non-negative integer"
|
50
|
+
|
51
|
+
parsed_all_of_components: List[IRSchema] = []
|
52
|
+
merged_required: Set[str] = set(node.get("required", []))
|
53
|
+
merged_properties: Dict[str, IRSchema] = {}
|
54
|
+
|
55
|
+
if "allOf" not in node:
|
56
|
+
current_node_direct_properties = node.get("properties", {})
|
57
|
+
for prop_name, prop_data in current_node_direct_properties.items():
|
58
|
+
prop_schema_name_context = f"{current_schema_name}.{prop_name}" if current_schema_name else prop_name
|
59
|
+
merged_properties[prop_name] = _parse_schema_func(prop_schema_name_context, prop_data, context, max_depth)
|
60
|
+
return merged_properties, merged_required, parsed_all_of_components
|
61
|
+
|
62
|
+
for sub_node in node["allOf"]:
|
63
|
+
sub_schema_ir = _parse_schema_func(None, sub_node, context, max_depth)
|
64
|
+
parsed_all_of_components.append(sub_schema_ir)
|
65
|
+
if sub_schema_ir.properties:
|
66
|
+
for prop_name, prop_schema_val in sub_schema_ir.properties.items():
|
67
|
+
if prop_name not in merged_properties:
|
68
|
+
merged_properties[prop_name] = prop_schema_val
|
69
|
+
if sub_schema_ir.required:
|
70
|
+
merged_required.update(sub_schema_ir.required)
|
71
|
+
|
72
|
+
current_node_direct_properties = node.get("properties", {})
|
73
|
+
for prop_name, prop_data in current_node_direct_properties.items():
|
74
|
+
prop_schema_name_context = f"{current_schema_name}.{prop_name}" if current_schema_name else prop_name
|
75
|
+
merged_properties[prop_name] = _parse_schema_func(prop_schema_name_context, prop_data, context, max_depth)
|
76
|
+
|
77
|
+
return merged_properties, merged_required, parsed_all_of_components
|
@@ -0,0 +1,79 @@
|
|
1
|
+
"""
|
2
|
+
Parser for 'anyOf' keyword in OpenAPI schemas.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, List, Mapping, Optional
|
8
|
+
|
9
|
+
from pyopenapi_gen import IRSchema # Main IR model
|
10
|
+
|
11
|
+
from ..context import ParsingContext # Context object - MOVED
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
# from ..context import ParsingContext # No longer here
|
15
|
+
# No direct import of _parse_schema from schema_parser to avoid circularity
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
def _parse_any_of_schemas(
|
20
|
+
any_of_nodes: List[Mapping[str, Any]],
|
21
|
+
context: ParsingContext,
|
22
|
+
max_depth: int,
|
23
|
+
parse_fn: Callable[ # Accepts the main schema parsing function
|
24
|
+
[Optional[str], Optional[Mapping[str, Any]], ParsingContext, int], IRSchema
|
25
|
+
],
|
26
|
+
) -> tuple[Optional[List[IRSchema]], bool, Optional[str]]:
|
27
|
+
"""Parses 'anyOf' sub-schemas using a provided parsing function.
|
28
|
+
|
29
|
+
Contracts:
|
30
|
+
Pre-conditions:
|
31
|
+
- any_of_nodes is a list of schema node mappings.
|
32
|
+
- context is a valid ParsingContext instance.
|
33
|
+
- max_depth >= 0.
|
34
|
+
- parse_fn is a callable that can parse a schema node.
|
35
|
+
Post-conditions:
|
36
|
+
- Returns a tuple: (parsed_schemas, is_nullable, effective_schema_type)
|
37
|
+
- parsed_schemas: List of IRSchema for non-null sub-schemas, or None.
|
38
|
+
- is_nullable: True if a null type was present.
|
39
|
+
- effective_schema_type: Potential schema_type if list becomes empty/None (currently always None).
|
40
|
+
"""
|
41
|
+
assert isinstance(any_of_nodes, list), "any_of_nodes must be a list"
|
42
|
+
assert all(isinstance(n, Mapping) for n in any_of_nodes), "all items in any_of_nodes must be Mappings"
|
43
|
+
assert isinstance(context, ParsingContext), "context must be a ParsingContext instance"
|
44
|
+
assert max_depth >= 0, "max_depth must be non-negative"
|
45
|
+
assert callable(parse_fn), "parse_fn must be a callable"
|
46
|
+
|
47
|
+
parsed_schemas_list: List[IRSchema] = [] # Renamed to avoid confusion with module name
|
48
|
+
is_nullable_from_any_of = False
|
49
|
+
effective_schema_type: Optional[str] = None
|
50
|
+
|
51
|
+
for sub_node in any_of_nodes:
|
52
|
+
if isinstance(sub_node, dict) and sub_node.get("type") == "null":
|
53
|
+
is_nullable_from_any_of = True
|
54
|
+
continue
|
55
|
+
|
56
|
+
parsed_schemas_list.append(parse_fn(None, sub_node, context, max_depth))
|
57
|
+
|
58
|
+
filtered_schemas = [
|
59
|
+
s
|
60
|
+
for s in parsed_schemas_list
|
61
|
+
if not (
|
62
|
+
s.type is None
|
63
|
+
and not s.properties
|
64
|
+
and not s.items
|
65
|
+
and not s.enum
|
66
|
+
and not s.any_of
|
67
|
+
and not s.one_of
|
68
|
+
and not s.all_of
|
69
|
+
)
|
70
|
+
]
|
71
|
+
|
72
|
+
if not filtered_schemas:
|
73
|
+
effective_schema_type = None
|
74
|
+
return None, is_nullable_from_any_of, effective_schema_type
|
75
|
+
|
76
|
+
return filtered_schemas, is_nullable_from_any_of, effective_schema_type
|
77
|
+
|
78
|
+
|
79
|
+
# ... existing code ...
|
@@ -0,0 +1,69 @@
|
|
1
|
+
"""
|
2
|
+
Dedicated parser for handling 'items' within an array schema.
|
3
|
+
Renamed from array_parser.py for clarity.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional
|
9
|
+
|
10
|
+
from pyopenapi_gen import IRSchema
|
11
|
+
|
12
|
+
from ..context import ParsingContext
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
# from pyopenapi_gen import IRSchema # Already above or handled by context
|
16
|
+
# from ..context import ParsingContext # No longer here
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
def _parse_array_items_schema(
|
21
|
+
parent_schema_name: Optional[str],
|
22
|
+
items_node_data: Mapping[str, Any],
|
23
|
+
context: ParsingContext,
|
24
|
+
parse_fn: Callable[ # Accepts the main schema parsing function
|
25
|
+
[Optional[str], Optional[Mapping[str, Any]], ParsingContext, int], IRSchema
|
26
|
+
],
|
27
|
+
max_depth: int,
|
28
|
+
) -> Optional[IRSchema]:
|
29
|
+
"""Parses the 'items' sub-schema of an array.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
parent_schema_name: The name of the parent array schema (if any).
|
33
|
+
items_node_data: The raw dictionary of the 'items' schema.
|
34
|
+
context: The parsing context.
|
35
|
+
parse_fn: The main schema parsing function to call recursively (_parse_schema from schema_parser.py).
|
36
|
+
max_depth: Maximum recursion depth.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
The parsed IRSchema for the items, or None if items_node_data is not suitable.
|
40
|
+
|
41
|
+
Contracts:
|
42
|
+
Pre-conditions:
|
43
|
+
- items_node_data is a mapping representing the items schema.
|
44
|
+
- context is a valid ParsingContext instance.
|
45
|
+
- parse_fn is a callable function.
|
46
|
+
- max_depth is a non-negative integer.
|
47
|
+
Post-conditions:
|
48
|
+
- Returns an IRSchema if items_node_data is a valid schema mapping.
|
49
|
+
- Returns None if items_node_data is not a mapping.
|
50
|
+
- Calls parse_fn with an appropriate name for the item schema.
|
51
|
+
"""
|
52
|
+
# Pre-conditions
|
53
|
+
# items_node_data is checked later, as it can be non-Mapping to return None
|
54
|
+
assert isinstance(context, ParsingContext), "context must be a ParsingContext instance"
|
55
|
+
assert callable(parse_fn), "parse_fn must be callable"
|
56
|
+
assert isinstance(max_depth, int) and max_depth >= 0, "max_depth must be a non-negative integer"
|
57
|
+
|
58
|
+
item_name_for_parse = f"{parent_schema_name}Item" if parent_schema_name else None
|
59
|
+
if (
|
60
|
+
isinstance(items_node_data, dict)
|
61
|
+
and "$ref" in items_node_data
|
62
|
+
and items_node_data["$ref"].startswith("#/components/schemas/")
|
63
|
+
):
|
64
|
+
item_name_for_parse = items_node_data["$ref"].split("/")[-1]
|
65
|
+
|
66
|
+
if not isinstance(items_node_data, Mapping):
|
67
|
+
return None
|
68
|
+
|
69
|
+
return parse_fn(item_name_for_parse, items_node_data, context, max_depth)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
"""
|
2
|
+
Parser for 'oneOf' keyword in OpenAPI schemas.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, List, Mapping, Optional
|
8
|
+
|
9
|
+
from pyopenapi_gen import IRSchema
|
10
|
+
|
11
|
+
from ..context import ParsingContext
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
# from ..context import ParsingContext # No longer here
|
15
|
+
pass
|
16
|
+
|
17
|
+
|
18
|
+
def _parse_one_of_schemas(
|
19
|
+
one_of_nodes: List[Mapping[str, Any]],
|
20
|
+
context: ParsingContext,
|
21
|
+
max_depth: int,
|
22
|
+
parse_fn: Callable[[Optional[str], Optional[Mapping[str, Any]], ParsingContext, int], IRSchema],
|
23
|
+
) -> tuple[Optional[List[IRSchema]], bool, Optional[str]]:
|
24
|
+
"""Parses 'oneOf' sub-schemas using a provided parsing function.
|
25
|
+
|
26
|
+
Contracts:
|
27
|
+
Pre-conditions:
|
28
|
+
- one_of_nodes is a list of schema node mappings.
|
29
|
+
- context is a valid ParsingContext instance.
|
30
|
+
- max_depth >= 0.
|
31
|
+
- parse_fn is a callable that can parse a schema node.
|
32
|
+
Post-conditions:
|
33
|
+
- Returns a tuple: (parsed_schemas, is_nullable, effective_schema_type)
|
34
|
+
- parsed_schemas: List of IRSchema for non-null sub-schemas, or None.
|
35
|
+
- is_nullable: True if a null type was present.
|
36
|
+
- effective_schema_type: Potential schema_type if list becomes empty/None (currently always None).
|
37
|
+
"""
|
38
|
+
assert isinstance(one_of_nodes, list), "one_of_nodes must be a list"
|
39
|
+
assert all(isinstance(n, Mapping) for n in one_of_nodes), "all items in one_of_nodes must be Mappings"
|
40
|
+
assert isinstance(context, ParsingContext), "context must be a ParsingContext instance"
|
41
|
+
assert max_depth >= 0, "max_depth must be non-negative"
|
42
|
+
assert callable(parse_fn), "parse_fn must be a callable"
|
43
|
+
|
44
|
+
parsed_schemas_list: List[IRSchema] = []
|
45
|
+
is_nullable_from_one_of = False
|
46
|
+
effective_schema_type: Optional[str] = None
|
47
|
+
|
48
|
+
for sub_node in one_of_nodes:
|
49
|
+
if isinstance(sub_node, dict) and sub_node.get("type") == "null":
|
50
|
+
is_nullable_from_one_of = True
|
51
|
+
continue
|
52
|
+
parsed_schemas_list.append(parse_fn(None, sub_node, context, max_depth))
|
53
|
+
|
54
|
+
filtered_schemas = [
|
55
|
+
s
|
56
|
+
for s in parsed_schemas_list
|
57
|
+
if not (
|
58
|
+
s.type is None
|
59
|
+
and not s.properties
|
60
|
+
and not s.items
|
61
|
+
and not s.enum
|
62
|
+
and not s.any_of
|
63
|
+
and not s.one_of
|
64
|
+
and not s.all_of
|
65
|
+
)
|
66
|
+
]
|
67
|
+
|
68
|
+
if not filtered_schemas:
|
69
|
+
effective_schema_type = None
|
70
|
+
return None, is_nullable_from_one_of, effective_schema_type
|
71
|
+
|
72
|
+
return filtered_schemas, is_nullable_from_one_of, effective_schema_type
|