pyopenapi-gen 2.7.2__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 (137) hide show
  1. pyopenapi_gen/__init__.py +224 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +62 -0
  4. pyopenapi_gen/context/CLAUDE.md +284 -0
  5. pyopenapi_gen/context/file_manager.py +52 -0
  6. pyopenapi_gen/context/import_collector.py +382 -0
  7. pyopenapi_gen/context/render_context.py +726 -0
  8. pyopenapi_gen/core/CLAUDE.md +224 -0
  9. pyopenapi_gen/core/__init__.py +0 -0
  10. pyopenapi_gen/core/auth/base.py +22 -0
  11. pyopenapi_gen/core/auth/plugins.py +89 -0
  12. pyopenapi_gen/core/cattrs_converter.py +810 -0
  13. pyopenapi_gen/core/exceptions.py +20 -0
  14. pyopenapi_gen/core/http_status_codes.py +218 -0
  15. pyopenapi_gen/core/http_transport.py +222 -0
  16. pyopenapi_gen/core/loader/__init__.py +12 -0
  17. pyopenapi_gen/core/loader/loader.py +174 -0
  18. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  19. pyopenapi_gen/core/loader/operations/parser.py +161 -0
  20. pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
  21. pyopenapi_gen/core/loader/operations/request_body.py +90 -0
  22. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  23. pyopenapi_gen/core/loader/parameters/parser.py +186 -0
  24. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  25. pyopenapi_gen/core/loader/responses/parser.py +111 -0
  26. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  27. pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
  28. pyopenapi_gen/core/pagination.py +64 -0
  29. pyopenapi_gen/core/parsing/__init__.py +13 -0
  30. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  37. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  38. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  39. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  40. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  41. pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
  42. pyopenapi_gen/core/parsing/context.py +187 -0
  43. pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
  44. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  45. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
  46. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
  47. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
  48. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
  49. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  50. pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
  51. pyopenapi_gen/core/parsing/schema_parser.py +804 -0
  52. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  53. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  54. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
  55. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  56. pyopenapi_gen/core/postprocess_manager.py +260 -0
  57. pyopenapi_gen/core/spec_fetcher.py +148 -0
  58. pyopenapi_gen/core/streaming_helpers.py +84 -0
  59. pyopenapi_gen/core/telemetry.py +69 -0
  60. pyopenapi_gen/core/utils.py +456 -0
  61. pyopenapi_gen/core/warning_collector.py +83 -0
  62. pyopenapi_gen/core/writers/code_writer.py +135 -0
  63. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  64. pyopenapi_gen/core/writers/line_writer.py +217 -0
  65. pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
  66. pyopenapi_gen/core_package_template/README.md +21 -0
  67. pyopenapi_gen/emit/models_emitter.py +143 -0
  68. pyopenapi_gen/emitters/CLAUDE.md +286 -0
  69. pyopenapi_gen/emitters/client_emitter.py +51 -0
  70. pyopenapi_gen/emitters/core_emitter.py +181 -0
  71. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  72. pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
  73. pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
  74. pyopenapi_gen/emitters/mocks_emitter.py +185 -0
  75. pyopenapi_gen/emitters/models_emitter.py +426 -0
  76. pyopenapi_gen/generator/CLAUDE.md +352 -0
  77. pyopenapi_gen/generator/client_generator.py +567 -0
  78. pyopenapi_gen/generator/exceptions.py +7 -0
  79. pyopenapi_gen/helpers/CLAUDE.md +325 -0
  80. pyopenapi_gen/helpers/__init__.py +1 -0
  81. pyopenapi_gen/helpers/endpoint_utils.py +532 -0
  82. pyopenapi_gen/helpers/type_cleaner.py +334 -0
  83. pyopenapi_gen/helpers/type_helper.py +112 -0
  84. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  85. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  86. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  87. pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
  88. pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
  89. pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
  90. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
  91. pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
  92. pyopenapi_gen/helpers/url_utils.py +14 -0
  93. pyopenapi_gen/http_types.py +20 -0
  94. pyopenapi_gen/ir.py +165 -0
  95. pyopenapi_gen/py.typed +1 -0
  96. pyopenapi_gen/types/CLAUDE.md +140 -0
  97. pyopenapi_gen/types/__init__.py +11 -0
  98. pyopenapi_gen/types/contracts/__init__.py +13 -0
  99. pyopenapi_gen/types/contracts/protocols.py +106 -0
  100. pyopenapi_gen/types/contracts/types.py +28 -0
  101. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  102. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  103. pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
  104. pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
  105. pyopenapi_gen/types/services/__init__.py +5 -0
  106. pyopenapi_gen/types/services/type_service.py +165 -0
  107. pyopenapi_gen/types/strategies/__init__.py +5 -0
  108. pyopenapi_gen/types/strategies/response_strategy.py +310 -0
  109. pyopenapi_gen/visit/CLAUDE.md +272 -0
  110. pyopenapi_gen/visit/client_visitor.py +477 -0
  111. pyopenapi_gen/visit/docs_visitor.py +38 -0
  112. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  113. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
  114. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  115. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
  116. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
  117. pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
  118. pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
  119. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  120. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
  121. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
  122. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
  123. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  124. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
  125. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  126. pyopenapi_gen/visit/exception_visitor.py +90 -0
  127. pyopenapi_gen/visit/model/__init__.py +0 -0
  128. pyopenapi_gen/visit/model/alias_generator.py +93 -0
  129. pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
  130. pyopenapi_gen/visit/model/enum_generator.py +212 -0
  131. pyopenapi_gen/visit/model/model_visitor.py +198 -0
  132. pyopenapi_gen/visit/visitor.py +97 -0
  133. pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
  134. pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
  135. pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
  136. pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
  137. pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,161 @@
1
+ """Operation parsers for OpenAPI IR transformation.
2
+
3
+ Provides the main parse_operations function to transform OpenAPI paths into IR operations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import warnings
10
+ from typing import Any, List, Mapping, cast
11
+
12
+ from pyopenapi_gen import HTTPMethod, IROperation, IRParameter, IRRequestBody, IRResponse
13
+ from pyopenapi_gen.core.loader.operations.post_processor import post_process_operation
14
+ from pyopenapi_gen.core.loader.operations.request_body import parse_request_body
15
+ from pyopenapi_gen.core.loader.parameters import parse_parameter, resolve_parameter_node_if_ref
16
+ from pyopenapi_gen.core.loader.responses import parse_response
17
+ from pyopenapi_gen.core.parsing.context import ParsingContext
18
+ from pyopenapi_gen.core.utils import NameSanitizer
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def parse_operations(
24
+ paths: Mapping[str, Any],
25
+ raw_parameters: Mapping[str, Any],
26
+ raw_responses: Mapping[str, Any],
27
+ raw_request_bodies: Mapping[str, Any],
28
+ context: ParsingContext,
29
+ ) -> List[IROperation]:
30
+ """Iterate paths to build IROperation list.
31
+
32
+ Contracts:
33
+ Preconditions:
34
+ - paths is a valid paths object from OpenAPI spec
35
+ - raw_parameters, raw_responses, raw_request_bodies are component mappings
36
+ - context is properly initialized with schemas
37
+ Postconditions:
38
+ - Returns a list of IROperation objects
39
+ - All operations have correct path, method, parameters, responses, etc.
40
+ - All referenced schemas are properly stored in context
41
+ """
42
+ if not isinstance(paths, Mapping):
43
+ raise TypeError("paths must be a Mapping")
44
+ if not isinstance(raw_parameters, Mapping):
45
+ raise TypeError("raw_parameters must be a Mapping")
46
+ if not isinstance(raw_responses, Mapping):
47
+ raise TypeError("raw_responses must be a Mapping")
48
+ if not isinstance(raw_request_bodies, Mapping):
49
+ raise TypeError("raw_request_bodies must be a Mapping")
50
+ if not isinstance(context, ParsingContext):
51
+ raise TypeError("context must be a ParsingContext")
52
+
53
+ ops: List[IROperation] = []
54
+
55
+ for path, item in paths.items():
56
+ if not isinstance(item, Mapping):
57
+ continue
58
+ entry = cast(Mapping[str, Any], item)
59
+
60
+ base_params_nodes = cast(List[Mapping[str, Any]], entry.get("parameters", []))
61
+
62
+ for method, on in entry.items():
63
+ try:
64
+ if method in {
65
+ "parameters",
66
+ "summary",
67
+ "description",
68
+ "servers",
69
+ "$ref",
70
+ }:
71
+ continue
72
+ mu = method.upper()
73
+ if mu not in HTTPMethod.__members__:
74
+ continue
75
+
76
+ node_op = cast(Mapping[str, Any], on)
77
+
78
+ # Get operation_id for this specific operation
79
+ if "operationId" in node_op:
80
+ operation_id = node_op["operationId"]
81
+ else:
82
+ operation_id = NameSanitizer.sanitize_method_name(f"{mu}_{path}".strip("/"))
83
+
84
+ # Parse base parameters (path-level) with operation_id context
85
+ base_params: List[IRParameter] = []
86
+ for p_node_data_raw in base_params_nodes:
87
+ resolved_p_node_data = resolve_parameter_node_if_ref(p_node_data_raw, context)
88
+ base_params.append(
89
+ parse_parameter(resolved_p_node_data, context, operation_id_for_promo=operation_id)
90
+ )
91
+
92
+ # Parse operation-specific parameters
93
+ params: List[IRParameter] = list(base_params) # Start with copies of path-level params
94
+ for p_param_node_raw in cast(List[Mapping[str, Any]], node_op.get("parameters", [])):
95
+ resolved_p_param_node = resolve_parameter_node_if_ref(p_param_node_raw, context)
96
+ params.append(parse_parameter(resolved_p_param_node, context, operation_id_for_promo=operation_id))
97
+
98
+ # Parse request body
99
+ rb: IRRequestBody | None = None
100
+ if "requestBody" in node_op:
101
+ rb = parse_request_body(
102
+ cast(Mapping[str, Any], node_op["requestBody"]),
103
+ raw_request_bodies,
104
+ context,
105
+ operation_id,
106
+ )
107
+
108
+ # Parse responses
109
+ resps: List[IRResponse] = []
110
+ for sc, rn_node in cast(Mapping[str, Any], node_op.get("responses", {})).items():
111
+ if (
112
+ isinstance(rn_node, Mapping)
113
+ and "$ref" in rn_node
114
+ and isinstance(rn_node.get("$ref"), str)
115
+ and rn_node["$ref"].startswith("#/components/responses/")
116
+ ):
117
+ ref_name = rn_node["$ref"].split("/")[-1]
118
+ resp_node_resolved = raw_responses.get(ref_name, {}) or rn_node
119
+ elif (
120
+ isinstance(rn_node, Mapping)
121
+ and "$ref" in rn_node
122
+ and isinstance(rn_node.get("$ref"), str)
123
+ and rn_node["$ref"].startswith("#/components/schemas/")
124
+ ):
125
+ # Handle direct schema references in responses
126
+ # Convert schema reference to a response with content
127
+ resp_node_resolved = {
128
+ "description": f"Response with {rn_node['$ref'].split('/')[-1]} schema",
129
+ "content": {"application/json": {"schema": {"$ref": rn_node["$ref"]}}},
130
+ }
131
+ else:
132
+ resp_node_resolved = rn_node
133
+ resps.append(parse_response(sc, resp_node_resolved, context, operation_id_for_promo=operation_id))
134
+
135
+ op = IROperation(
136
+ operation_id=operation_id,
137
+ method=HTTPMethod[mu],
138
+ path=path,
139
+ summary=node_op.get("summary"),
140
+ description=node_op.get("description"),
141
+ parameters=params,
142
+ request_body=rb,
143
+ responses=resps,
144
+ tags=list(node_op.get("tags", [])),
145
+ )
146
+ except Exception as e:
147
+ warnings.warn(
148
+ f"Skipping operation parsing for {method.upper()} {path}: {e}",
149
+ UserWarning,
150
+ )
151
+ continue
152
+ else:
153
+ # Post-process the parsed operation to fill in schema names
154
+ post_process_operation(op, context)
155
+ ops.append(op)
156
+
157
+ # Post-condition check
158
+ if not all(isinstance(op, IROperation) for op in ops):
159
+ raise TypeError("All items must be IROperation objects")
160
+
161
+ return ops
@@ -0,0 +1,62 @@
1
+ """Operation post-processing utilities.
2
+
3
+ Provides functions to finalize and enhance parsed operations.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+
10
+ from pyopenapi_gen import IROperation
11
+ from pyopenapi_gen.core.parsing.context import ParsingContext
12
+ from pyopenapi_gen.core.utils import NameSanitizer
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def post_process_operation(op: IROperation, context: ParsingContext) -> None:
18
+ """Post-process an operation to finalize schema names and register them.
19
+
20
+ Contracts:
21
+ Preconditions:
22
+ - op is a valid IROperation
23
+ - context is properly initialized
24
+ Postconditions:
25
+ - All request body and response schemas are properly named and registered
26
+ """
27
+ if not isinstance(op, IROperation):
28
+ raise TypeError("op must be an IROperation")
29
+ if not isinstance(context, ParsingContext):
30
+ raise TypeError("context must be a ParsingContext")
31
+
32
+ # Handle request body schemas
33
+ if op.request_body:
34
+ for _, sch_val in op.request_body.content.items():
35
+ if not sch_val.name:
36
+ generated_rb_name = NameSanitizer.sanitize_class_name(op.operation_id + "Request")
37
+ sch_val.name = generated_rb_name
38
+ context.parsed_schemas[generated_rb_name] = sch_val
39
+ elif sch_val.name not in context.parsed_schemas:
40
+ context.parsed_schemas[sch_val.name] = sch_val
41
+
42
+ # Handle response schemas
43
+ for resp_val in op.responses:
44
+ for _, sch_resp_val in resp_val.content.items():
45
+ if sch_resp_val.name is None:
46
+ if getattr(sch_resp_val, "_from_unresolved_ref", False):
47
+ continue
48
+ is_streaming = getattr(resp_val, "stream", False)
49
+ if is_streaming:
50
+ continue
51
+
52
+ should_synthesize_name = False
53
+ if sch_resp_val.type == "object" and (sch_resp_val.properties or sch_resp_val.additional_properties):
54
+ should_synthesize_name = True
55
+
56
+ if should_synthesize_name:
57
+ generated_name = NameSanitizer.sanitize_class_name(op.operation_id + "Response")
58
+ sch_resp_val.name = generated_name
59
+ context.parsed_schemas[generated_name] = sch_resp_val
60
+
61
+ elif sch_resp_val.name and sch_resp_val.name not in context.parsed_schemas:
62
+ context.parsed_schemas[sch_resp_val.name] = sch_resp_val
@@ -0,0 +1,90 @@
1
+ """Request body parsers for OpenAPI IR transformation.
2
+
3
+ Provides functions to parse and transform OpenAPI request bodies into IR format.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any, Mapping
10
+
11
+ from pyopenapi_gen import IRRequestBody, IRSchema
12
+ from pyopenapi_gen.core.parsing.context import ParsingContext
13
+ from pyopenapi_gen.core.parsing.schema_parser import _parse_schema
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def parse_request_body(
19
+ rb_node: Mapping[str, Any],
20
+ raw_request_bodies: Mapping[str, Any],
21
+ context: ParsingContext,
22
+ operation_id: str,
23
+ ) -> IRRequestBody | None:
24
+ """Parse a request body node into an IRRequestBody.
25
+
26
+ Contracts:
27
+ Preconditions:
28
+ - rb_node is a valid request body node
29
+ - raw_request_bodies contains component request bodies
30
+ - context is properly initialized
31
+ - operation_id is provided for naming
32
+ Postconditions:
33
+ - Returns a properly populated IRRequestBody or None if invalid
34
+ - All content media types are properly mapped to schemas
35
+ """
36
+ if not isinstance(rb_node, Mapping):
37
+ raise TypeError("rb_node must be a Mapping")
38
+ if not isinstance(raw_request_bodies, Mapping):
39
+ raise TypeError("raw_request_bodies must be a Mapping")
40
+ if not isinstance(context, ParsingContext):
41
+ raise TypeError("context must be a ParsingContext")
42
+ if not operation_id:
43
+ raise ValueError("operation_id must be provided")
44
+
45
+ # Handle $ref in request body
46
+ if (
47
+ "$ref" in rb_node
48
+ and isinstance(rb_node.get("$ref"), str)
49
+ and rb_node["$ref"].startswith("#/components/requestBodies/")
50
+ ):
51
+ ref_name = rb_node["$ref"].split("/")[-1]
52
+ resolved_rb_node = raw_request_bodies.get(ref_name, {}) or rb_node
53
+ else:
54
+ resolved_rb_node = rb_node
55
+
56
+ required_flag = bool(resolved_rb_node.get("required", False))
57
+ desc = resolved_rb_node.get("description")
58
+ content_map: dict[str, IRSchema] = {}
59
+
60
+ parent_promo_name_for_req_body = f"{operation_id}RequestBody"
61
+
62
+ for mt, media in resolved_rb_node.get("content", {}).items():
63
+ media_schema_node = media.get("schema")
64
+ if (
65
+ isinstance(media_schema_node, Mapping)
66
+ and "$ref" not in media_schema_node
67
+ and (
68
+ media_schema_node.get("type") == "object"
69
+ or "properties" in media_schema_node
70
+ or "allOf" in media_schema_node
71
+ or "anyOf" in media_schema_node
72
+ or "oneOf" in media_schema_node
73
+ )
74
+ ):
75
+ content_map[mt] = _parse_schema(
76
+ parent_promo_name_for_req_body, media_schema_node, context, allow_self_reference=False
77
+ )
78
+ else:
79
+ content_map[mt] = _parse_schema(None, media_schema_node, context, allow_self_reference=False)
80
+
81
+ if not content_map:
82
+ return None
83
+
84
+ request_body = IRRequestBody(required=required_flag, content=content_map, description=desc)
85
+
86
+ # Post-condition check
87
+ if request_body.content != content_map:
88
+ raise RuntimeError("Request body content mismatch")
89
+
90
+ return request_body
@@ -0,0 +1,10 @@
1
+ """Parameter parsing utilities.
2
+
3
+ Functions to extract and transform parameters from raw OpenAPI specifications.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .parser import parse_parameter, resolve_parameter_node_if_ref
9
+
10
+ __all__ = ["parse_parameter", "resolve_parameter_node_if_ref"]
@@ -0,0 +1,186 @@
1
+ """Parameter parsers for OpenAPI IR transformation.
2
+
3
+ Provides functions to parse and transform OpenAPI parameters into IR format.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any, Mapping, cast
10
+
11
+ from pyopenapi_gen import IRParameter, IRSchema
12
+ from pyopenapi_gen.core.parsing.context import ParsingContext
13
+ from pyopenapi_gen.core.parsing.schema_parser import _parse_schema
14
+ from pyopenapi_gen.core.utils import NameSanitizer
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def resolve_parameter_node_if_ref(param_node_data: Mapping[str, Any], context: ParsingContext) -> Mapping[str, Any]:
20
+ """Resolve a parameter node if it's a reference.
21
+
22
+ Contracts:
23
+ Preconditions:
24
+ - param_node_data is a valid parameter node mapping
25
+ - context contains the required components information
26
+ Postconditions:
27
+ - Returns the resolved parameter node or the original if not a ref
28
+ - If a reference, the parameter is looked up in components
29
+ """
30
+ if not isinstance(param_node_data, Mapping):
31
+ raise TypeError("param_node_data must be a Mapping")
32
+ if not isinstance(context, ParsingContext):
33
+ raise TypeError("context must be a ParsingContext")
34
+
35
+ if "$ref" in param_node_data and isinstance(param_node_data.get("$ref"), str):
36
+ ref_path = param_node_data["$ref"]
37
+ if ref_path.startswith("#/components/parameters/"):
38
+ param_name = ref_path.split("/")[-1]
39
+ # Access raw_spec_components from the context
40
+ resolved_node = context.raw_spec_components.get("parameters", {}).get(param_name)
41
+ if resolved_node:
42
+ logger.debug(f"Resolved parameter $ref '{ref_path}' to '{param_name}'")
43
+ return cast(Mapping[str, Any], resolved_node)
44
+ else:
45
+ logger.warning(f"Could not resolve parameter $ref '{ref_path}'")
46
+ return param_node_data # Return original ref node if resolution fails
47
+
48
+ return param_node_data # Not a ref or not a component parameter ref
49
+
50
+
51
+ def parse_parameter(
52
+ node: Mapping[str, Any],
53
+ context: ParsingContext,
54
+ operation_id_for_promo: str | None = None,
55
+ ) -> IRParameter:
56
+ """Convert an OpenAPI parameter node into IRParameter.
57
+
58
+ Contracts:
59
+ Preconditions:
60
+ - node is a valid parameter node with required fields
61
+ - context is properly initialized
62
+ - If node has a schema, it is a valid schema definition
63
+ Postconditions:
64
+ - Returns a properly populated IRParameter
65
+ - Complex parameter schemas are given appropriate names
66
+ """
67
+ if not isinstance(node, Mapping):
68
+ raise TypeError("node must be a Mapping")
69
+ if "name" not in node:
70
+ raise ValueError("Parameter node must have a name")
71
+ if not isinstance(context, ParsingContext):
72
+ raise TypeError("context must be a ParsingContext")
73
+
74
+ sch = node.get("schema")
75
+ param_name = node["name"]
76
+
77
+ name_for_inline_param_schema: str | None = None
78
+ if (
79
+ sch
80
+ and isinstance(sch, Mapping)
81
+ and "$ref" not in sch
82
+ and (sch.get("type") == "object" or "properties" in sch or "allOf" in sch or "anyOf" in sch or "oneOf" in sch)
83
+ ):
84
+ base_param_promo_name = f"{operation_id_for_promo}Param" if operation_id_for_promo else ""
85
+ name_for_inline_param_schema = f"{base_param_promo_name}{NameSanitizer.sanitize_class_name(param_name)}"
86
+
87
+ # General rule: if a parameter is defined inline but a components parameter exists with the
88
+ # same name and location, prefer the components schema (often richer: arrays/enums/refs).
89
+ try:
90
+ if isinstance(context, ParsingContext):
91
+ components_params = context.raw_spec_components.get("parameters", {})
92
+ if isinstance(components_params, Mapping):
93
+ for comp_key, comp_param in components_params.items():
94
+ if not isinstance(comp_param, Mapping):
95
+ continue
96
+ if comp_param.get("name") == param_name and comp_param.get("in") == node.get("in"):
97
+ comp_schema = comp_param.get("schema")
98
+ if isinstance(comp_schema, Mapping):
99
+ # Prefer component schema if inline is missing or clearly less specific
100
+ inline_is_specific = isinstance(sch, Mapping) and (
101
+ sch.get("type") in {"array", "object"} or "$ref" in sch or "enum" in sch
102
+ )
103
+ if not inline_is_specific:
104
+ sch = comp_schema
105
+ break
106
+ except Exception as e:
107
+ # Log unexpected structure but continue with inline schema
108
+ logger.debug(f"Could not check component parameter for '{param_name}': {e}. Using inline schema.")
109
+
110
+ # For parameters, we want to avoid creating complex schemas for simple enum arrays
111
+ # Check if this is a simple enum array and handle it specially
112
+ if (
113
+ sch
114
+ and isinstance(sch, Mapping)
115
+ and sch.get("type") == "array"
116
+ and "items" in sch
117
+ and isinstance(sch["items"], Mapping)
118
+ and sch["items"].get("type") == "string"
119
+ and "enum" in sch["items"]
120
+ and "$ref" not in sch["items"]
121
+ ):
122
+ # This is an array of string enums - create a proper enum schema for the items
123
+ # Give it a name based on the parameter and operation
124
+ enum_name = None
125
+ if operation_id_for_promo and param_name:
126
+ # Create a name for this inline enum when we have operation context
127
+ enum_name = f"{operation_id_for_promo}Param{NameSanitizer.sanitize_class_name(param_name)}Item"
128
+ elif param_name:
129
+ # For component parameters without operation context, use just the parameter name
130
+ enum_name = f"{NameSanitizer.sanitize_class_name(param_name)}Item"
131
+
132
+ if enum_name:
133
+ items_schema = IRSchema(
134
+ name=enum_name,
135
+ type="string",
136
+ enum=sch["items"]["enum"],
137
+ generation_name=enum_name, # Mark it as promoted
138
+ final_module_stem=NameSanitizer.sanitize_module_name(enum_name), # Set module stem for imports
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
+
148
+ logger.debug(
149
+ f"Created enum schema '{enum_name}' for array parameter '{param_name}' with values {sch['items']['enum'][:3]}..."
150
+ )
151
+ else:
152
+ # Fallback if we don't have enough info to create a good name
153
+ items_schema = IRSchema(name=None, type="string", enum=sch["items"]["enum"])
154
+ logger.warning(
155
+ f"Could not create proper enum name for parameter array items with values {sch['items']['enum'][:3]}... "
156
+ f"This will generate a warning during type resolution."
157
+ )
158
+
159
+ schema_ir = IRSchema(
160
+ name=None,
161
+ type="array",
162
+ items=items_schema,
163
+ description=sch.get("description"),
164
+ )
165
+ else:
166
+ schema_ir = (
167
+ _parse_schema(name_for_inline_param_schema, sch, context, allow_self_reference=False)
168
+ if sch
169
+ else IRSchema(name=None)
170
+ )
171
+
172
+ param = IRParameter(
173
+ name=node["name"],
174
+ param_in=node.get("in", "query"),
175
+ required=bool(node.get("required", False)),
176
+ schema=schema_ir,
177
+ description=node.get("description"),
178
+ )
179
+
180
+ # Post-condition check
181
+ if param.name != node["name"]:
182
+ raise RuntimeError("Parameter name mismatch")
183
+ if param.schema is None:
184
+ raise RuntimeError("Parameter schema must be created")
185
+
186
+ return param
@@ -0,0 +1,10 @@
1
+ """Response parsing utilities.
2
+
3
+ Functions to extract and transform responses from raw OpenAPI specifications.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .parser import parse_response
9
+
10
+ __all__ = ["parse_response"]
@@ -0,0 +1,111 @@
1
+ """Response parsers for OpenAPI IR transformation.
2
+
3
+ Provides functions to parse and transform OpenAPI responses into IR format.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from typing import Any, Mapping
10
+
11
+ from pyopenapi_gen import IRResponse, IRSchema
12
+ from pyopenapi_gen.core.parsing.context import ParsingContext
13
+ from pyopenapi_gen.core.parsing.schema_parser import _parse_schema
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def parse_response(
19
+ code: str,
20
+ node: Mapping[str, Any],
21
+ context: ParsingContext,
22
+ operation_id_for_promo: str,
23
+ ) -> IRResponse:
24
+ """Convert an OpenAPI response node into IRResponse.
25
+
26
+ Contracts:
27
+ Preconditions:
28
+ - code is a valid HTTP status code as string
29
+ - node is a valid response node
30
+ - context is properly initialized
31
+ - operation_id_for_promo is provided for naming inline schemas
32
+ Postconditions:
33
+ - Returns a properly populated IRResponse
34
+ - All content media types are properly mapped to schemas
35
+ - Stream flags are correctly set based on media types
36
+ """
37
+ if not isinstance(code, str):
38
+ raise TypeError("code must be a string")
39
+ if not isinstance(node, Mapping):
40
+ raise TypeError("node must be a Mapping")
41
+ if not isinstance(context, ParsingContext):
42
+ raise TypeError("context must be a ParsingContext")
43
+ if not operation_id_for_promo:
44
+ raise ValueError("operation_id_for_promo must be provided")
45
+
46
+ content: dict[str, IRSchema] = {}
47
+ STREAM_FORMATS = {
48
+ "application/octet-stream": "octet-stream",
49
+ "text/event-stream": "event-stream",
50
+ "application/x-ndjson": "ndjson",
51
+ "application/json-seq": "json-seq",
52
+ "multipart/mixed": "multipart-mixed",
53
+ }
54
+ stream_flag = False
55
+ stream_format = None
56
+
57
+ # Construct a base name for promoting inline schemas within this response
58
+ parent_promo_name_for_resp_body = f"{operation_id_for_promo}{code}Response"
59
+
60
+ for mt, mn in node.get("content", {}).items():
61
+ if isinstance(mn, Mapping) and "$ref" in mn and mn["$ref"].startswith("#/components/schemas/"):
62
+ content[mt] = _parse_schema(None, mn, context, allow_self_reference=False)
63
+ elif isinstance(mn, Mapping) and "schema" in mn:
64
+ media_schema_node = mn["schema"]
65
+ if (
66
+ isinstance(media_schema_node, Mapping)
67
+ and "$ref" not in media_schema_node
68
+ and (
69
+ media_schema_node.get("type") == "object"
70
+ or "properties" in media_schema_node
71
+ or "allOf" in media_schema_node
72
+ or "anyOf" in media_schema_node
73
+ or "oneOf" in media_schema_node
74
+ )
75
+ ):
76
+ content[mt] = _parse_schema(
77
+ parent_promo_name_for_resp_body, media_schema_node, context, allow_self_reference=False
78
+ )
79
+ else:
80
+ content[mt] = _parse_schema(None, media_schema_node, context, allow_self_reference=False)
81
+ else:
82
+ content[mt] = IRSchema(name=None, _from_unresolved_ref=True)
83
+
84
+ fmt = STREAM_FORMATS.get(mt.lower())
85
+ if fmt:
86
+ stream_flag = True
87
+ stream_format = fmt
88
+
89
+ if not stream_flag:
90
+ for mt_val, schema_val in content.items():
91
+ if getattr(schema_val, "format", None) == "binary":
92
+ stream_flag = True
93
+ stream_format = "octet-stream"
94
+
95
+ response = IRResponse(
96
+ status_code=code,
97
+ description=node.get("description"),
98
+ content=content,
99
+ stream=stream_flag,
100
+ stream_format=stream_format,
101
+ )
102
+
103
+ # Post-condition checks
104
+ if response.status_code != code:
105
+ raise RuntimeError("Response status code mismatch")
106
+ if response.content != content:
107
+ raise RuntimeError("Response content mismatch")
108
+ if response.stream != stream_flag:
109
+ raise RuntimeError("Response stream flag mismatch")
110
+
111
+ return response
@@ -0,0 +1,11 @@
1
+ """Schema parsing and transformation utilities.
2
+
3
+ Functions to extract schemas from raw OpenAPI specifications and convert them
4
+ into IR format.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .extractor import build_schemas, extract_inline_enums
10
+
11
+ __all__ = ["extract_inline_enums", "build_schemas"]