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.
Files changed (122) hide show
  1. pyopenapi_gen/__init__.py +114 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +86 -0
  4. pyopenapi_gen/context/file_manager.py +52 -0
  5. pyopenapi_gen/context/import_collector.py +382 -0
  6. pyopenapi_gen/context/render_context.py +630 -0
  7. pyopenapi_gen/core/__init__.py +0 -0
  8. pyopenapi_gen/core/auth/base.py +22 -0
  9. pyopenapi_gen/core/auth/plugins.py +89 -0
  10. pyopenapi_gen/core/exceptions.py +25 -0
  11. pyopenapi_gen/core/http_transport.py +219 -0
  12. pyopenapi_gen/core/loader/__init__.py +12 -0
  13. pyopenapi_gen/core/loader/loader.py +158 -0
  14. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  15. pyopenapi_gen/core/loader/operations/parser.py +155 -0
  16. pyopenapi_gen/core/loader/operations/post_processor.py +60 -0
  17. pyopenapi_gen/core/loader/operations/request_body.py +85 -0
  18. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  19. pyopenapi_gen/core/loader/parameters/parser.py +121 -0
  20. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  21. pyopenapi_gen/core/loader/responses/parser.py +104 -0
  22. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  23. pyopenapi_gen/core/loader/schemas/extractor.py +184 -0
  24. pyopenapi_gen/core/pagination.py +64 -0
  25. pyopenapi_gen/core/parsing/__init__.py +13 -0
  26. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  27. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  28. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  29. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  30. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  37. pyopenapi_gen/core/parsing/common/type_parser.py +74 -0
  38. pyopenapi_gen/core/parsing/context.py +184 -0
  39. pyopenapi_gen/core/parsing/cycle_helpers.py +123 -0
  40. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  41. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +77 -0
  42. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +79 -0
  43. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +69 -0
  44. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +72 -0
  45. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  46. pyopenapi_gen/core/parsing/schema_finalizer.py +166 -0
  47. pyopenapi_gen/core/parsing/schema_parser.py +610 -0
  48. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  49. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  50. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +117 -0
  51. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  52. pyopenapi_gen/core/postprocess_manager.py +161 -0
  53. pyopenapi_gen/core/schemas.py +40 -0
  54. pyopenapi_gen/core/streaming_helpers.py +86 -0
  55. pyopenapi_gen/core/telemetry.py +67 -0
  56. pyopenapi_gen/core/utils.py +409 -0
  57. pyopenapi_gen/core/warning_collector.py +83 -0
  58. pyopenapi_gen/core/writers/code_writer.py +135 -0
  59. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  60. pyopenapi_gen/core/writers/line_writer.py +217 -0
  61. pyopenapi_gen/core/writers/python_construct_renderer.py +274 -0
  62. pyopenapi_gen/core_package_template/README.md +21 -0
  63. pyopenapi_gen/emit/models_emitter.py +143 -0
  64. pyopenapi_gen/emitters/client_emitter.py +51 -0
  65. pyopenapi_gen/emitters/core_emitter.py +181 -0
  66. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  67. pyopenapi_gen/emitters/endpoints_emitter.py +223 -0
  68. pyopenapi_gen/emitters/exceptions_emitter.py +52 -0
  69. pyopenapi_gen/emitters/models_emitter.py +428 -0
  70. pyopenapi_gen/generator/client_generator.py +562 -0
  71. pyopenapi_gen/helpers/__init__.py +1 -0
  72. pyopenapi_gen/helpers/endpoint_utils.py +552 -0
  73. pyopenapi_gen/helpers/type_cleaner.py +341 -0
  74. pyopenapi_gen/helpers/type_helper.py +112 -0
  75. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  76. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  77. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  78. pyopenapi_gen/helpers/type_resolution/finalizer.py +89 -0
  79. pyopenapi_gen/helpers/type_resolution/named_resolver.py +174 -0
  80. pyopenapi_gen/helpers/type_resolution/object_resolver.py +212 -0
  81. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +57 -0
  82. pyopenapi_gen/helpers/type_resolution/resolver.py +48 -0
  83. pyopenapi_gen/helpers/url_utils.py +14 -0
  84. pyopenapi_gen/http_types.py +20 -0
  85. pyopenapi_gen/ir.py +167 -0
  86. pyopenapi_gen/py.typed +1 -0
  87. pyopenapi_gen/types/__init__.py +11 -0
  88. pyopenapi_gen/types/contracts/__init__.py +13 -0
  89. pyopenapi_gen/types/contracts/protocols.py +106 -0
  90. pyopenapi_gen/types/contracts/types.py +30 -0
  91. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  92. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  93. pyopenapi_gen/types/resolvers/response_resolver.py +203 -0
  94. pyopenapi_gen/types/resolvers/schema_resolver.py +367 -0
  95. pyopenapi_gen/types/services/__init__.py +5 -0
  96. pyopenapi_gen/types/services/type_service.py +133 -0
  97. pyopenapi_gen/visit/client_visitor.py +228 -0
  98. pyopenapi_gen/visit/docs_visitor.py +38 -0
  99. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  100. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +103 -0
  101. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  102. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +121 -0
  103. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +87 -0
  104. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  105. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +497 -0
  106. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +88 -0
  107. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +183 -0
  108. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  109. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +76 -0
  110. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  111. pyopenapi_gen/visit/exception_visitor.py +52 -0
  112. pyopenapi_gen/visit/model/__init__.py +0 -0
  113. pyopenapi_gen/visit/model/alias_generator.py +89 -0
  114. pyopenapi_gen/visit/model/dataclass_generator.py +197 -0
  115. pyopenapi_gen/visit/model/enum_generator.py +200 -0
  116. pyopenapi_gen/visit/model/model_visitor.py +197 -0
  117. pyopenapi_gen/visit/visitor.py +97 -0
  118. pyopenapi_gen-0.8.3.dist-info/METADATA +224 -0
  119. pyopenapi_gen-0.8.3.dist-info/RECORD +122 -0
  120. pyopenapi_gen-0.8.3.dist-info/WHEEL +4 -0
  121. pyopenapi_gen-0.8.3.dist-info/entry_points.txt +2 -0
  122. 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