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.
- pyopenapi_gen/__init__.py +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -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/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -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 +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -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 +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -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 +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -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 +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -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 +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -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 +28 -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 +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- 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,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"]
|