fastmcp 2.1.0__py3-none-any.whl → 2.1.1__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.
- fastmcp/cli/cli.py +2 -0
- fastmcp/client/transports.py +22 -0
- fastmcp/exceptions.py +4 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/prompt.py +6 -16
- fastmcp/prompts/prompt_manager.py +2 -1
- fastmcp/resources/__init__.py +1 -1
- fastmcp/resources/resource_manager.py +89 -5
- fastmcp/server/context.py +1 -1
- fastmcp/server/proxy.py +4 -4
- fastmcp/server/server.py +156 -73
- fastmcp/tools/tool.py +4 -1
- fastmcp/utilities/decorators.py +101 -0
- fastmcp/utilities/func_metadata.py +4 -1
- fastmcp/utilities/openapi.py +671 -292
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/METADATA +72 -52
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/RECORD +20 -19
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.1.0.dist-info → fastmcp-2.1.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/utilities/openapi.py
CHANGED
|
@@ -14,6 +14,16 @@ from openapi_pydantic import (
|
|
|
14
14
|
Response,
|
|
15
15
|
Schema,
|
|
16
16
|
)
|
|
17
|
+
|
|
18
|
+
# Import OpenAPI 3.0 models as well
|
|
19
|
+
from openapi_pydantic.v3.v3_0 import OpenAPI as OpenAPI_30
|
|
20
|
+
from openapi_pydantic.v3.v3_0 import Operation as Operation_30
|
|
21
|
+
from openapi_pydantic.v3.v3_0 import Parameter as Parameter_30
|
|
22
|
+
from openapi_pydantic.v3.v3_0 import PathItem as PathItem_30
|
|
23
|
+
from openapi_pydantic.v3.v3_0 import Reference as Reference_30
|
|
24
|
+
from openapi_pydantic.v3.v3_0 import RequestBody as RequestBody_30
|
|
25
|
+
from openapi_pydantic.v3.v3_0 import Response as Response_30
|
|
26
|
+
from openapi_pydantic.v3.v3_0 import Schema as Schema_30
|
|
17
27
|
from pydantic import BaseModel, Field, ValidationError
|
|
18
28
|
|
|
19
29
|
from fastmcp.utilities import openapi
|
|
@@ -176,131 +186,6 @@ def _convert_to_parameter_location(param_in: str) -> ParameterLocation:
|
|
|
176
186
|
return "query"
|
|
177
187
|
|
|
178
188
|
|
|
179
|
-
def _extract_parameters(
|
|
180
|
-
operation_params: list[Parameter | Reference] | None,
|
|
181
|
-
path_item_params: list[Parameter | Reference] | None,
|
|
182
|
-
openapi: OpenAPI,
|
|
183
|
-
) -> list[ParameterInfo]:
|
|
184
|
-
"""Extracts and resolves parameters using corrected attribute names."""
|
|
185
|
-
extracted_params: list[ParameterInfo] = []
|
|
186
|
-
seen_params: dict[
|
|
187
|
-
tuple[str, str], bool
|
|
188
|
-
] = {} # Use string keys to avoid type issues
|
|
189
|
-
all_params_refs = (operation_params or []) + (path_item_params or [])
|
|
190
|
-
|
|
191
|
-
for param_or_ref in all_params_refs:
|
|
192
|
-
try:
|
|
193
|
-
parameter = cast(Parameter, _resolve_ref(param_or_ref, openapi))
|
|
194
|
-
if not isinstance(parameter, Parameter):
|
|
195
|
-
# ... (error logging remains the same)
|
|
196
|
-
continue
|
|
197
|
-
|
|
198
|
-
# --- *** CORRECTED ATTRIBUTE ACCESS HERE *** ---
|
|
199
|
-
param_in = parameter.param_in # CORRECTED: Use 'param_in'
|
|
200
|
-
param_location = _convert_to_parameter_location(param_in)
|
|
201
|
-
param_schema_obj = parameter.param_schema # CORRECTED: Use 'param_schema'
|
|
202
|
-
# --- *** ---
|
|
203
|
-
|
|
204
|
-
param_key = (parameter.name, param_in)
|
|
205
|
-
if param_key in seen_params:
|
|
206
|
-
continue
|
|
207
|
-
seen_params[param_key] = True
|
|
208
|
-
|
|
209
|
-
param_schema_dict = {}
|
|
210
|
-
if param_schema_obj: # Check if schema exists
|
|
211
|
-
param_schema_dict = _extract_schema_as_dict(param_schema_obj, openapi)
|
|
212
|
-
elif parameter.content:
|
|
213
|
-
# Handle complex parameters with 'content'
|
|
214
|
-
first_media_type = next(iter(parameter.content.values()), None)
|
|
215
|
-
if (
|
|
216
|
-
first_media_type and first_media_type.media_type_schema
|
|
217
|
-
): # CORRECTED: Use 'media_type_schema'
|
|
218
|
-
param_schema_dict = _extract_schema_as_dict(
|
|
219
|
-
first_media_type.media_type_schema, openapi
|
|
220
|
-
)
|
|
221
|
-
logger.debug(
|
|
222
|
-
f"Parameter '{parameter.name}' using schema from 'content' field."
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
# Manually create ParameterInfo instance using correct field names
|
|
226
|
-
param_info = ParameterInfo(
|
|
227
|
-
name=parameter.name,
|
|
228
|
-
location=param_location, # Use converted parameter location
|
|
229
|
-
required=parameter.required,
|
|
230
|
-
schema=param_schema_dict, # Populate 'schema' field in IR
|
|
231
|
-
description=parameter.description,
|
|
232
|
-
)
|
|
233
|
-
extracted_params.append(param_info)
|
|
234
|
-
|
|
235
|
-
except (
|
|
236
|
-
ValidationError,
|
|
237
|
-
ValueError,
|
|
238
|
-
AttributeError,
|
|
239
|
-
TypeError,
|
|
240
|
-
) as e: # Added TypeError
|
|
241
|
-
param_name = getattr(
|
|
242
|
-
param_or_ref, "name", getattr(param_or_ref, "ref", "unknown")
|
|
243
|
-
)
|
|
244
|
-
logger.error(
|
|
245
|
-
f"Failed to extract parameter '{param_name}': {e}", exc_info=False
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
return extracted_params
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def _extract_request_body(
|
|
252
|
-
request_body_or_ref: RequestBody | Reference | None, openapi: OpenAPI
|
|
253
|
-
) -> RequestBodyInfo | None:
|
|
254
|
-
"""Extracts and resolves the request body using corrected attribute names."""
|
|
255
|
-
if not request_body_or_ref:
|
|
256
|
-
return None
|
|
257
|
-
try:
|
|
258
|
-
request_body = cast(RequestBody, _resolve_ref(request_body_or_ref, openapi))
|
|
259
|
-
if not isinstance(request_body, RequestBody):
|
|
260
|
-
# ... (error logging remains the same)
|
|
261
|
-
return None
|
|
262
|
-
|
|
263
|
-
content_schemas: dict[str, JsonSchema] = {}
|
|
264
|
-
if request_body.content:
|
|
265
|
-
for media_type_str, media_type_obj in request_body.content.items():
|
|
266
|
-
# --- *** CORRECTED ATTRIBUTE ACCESS HERE *** ---
|
|
267
|
-
if (
|
|
268
|
-
isinstance(media_type_obj, MediaType)
|
|
269
|
-
and media_type_obj.media_type_schema
|
|
270
|
-
): # CORRECTED: Use 'media_type_schema'
|
|
271
|
-
# --- *** ---
|
|
272
|
-
try:
|
|
273
|
-
# Use the corrected attribute here as well
|
|
274
|
-
schema_dict = _extract_schema_as_dict(
|
|
275
|
-
media_type_obj.media_type_schema, openapi
|
|
276
|
-
)
|
|
277
|
-
content_schemas[media_type_str] = schema_dict
|
|
278
|
-
except ValueError as schema_err:
|
|
279
|
-
logger.error(
|
|
280
|
-
f"Failed to extract schema for media type '{media_type_str}' in request body: {schema_err}"
|
|
281
|
-
)
|
|
282
|
-
elif not isinstance(media_type_obj, MediaType):
|
|
283
|
-
logger.warning(
|
|
284
|
-
f"Skipping invalid media type object for '{media_type_str}' (type: {type(media_type_obj)}) in request body."
|
|
285
|
-
)
|
|
286
|
-
elif not media_type_obj.media_type_schema: # Corrected check
|
|
287
|
-
logger.warning(
|
|
288
|
-
f"Skipping media type '{media_type_str}' in request body because it lacks a schema."
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
return RequestBodyInfo(
|
|
292
|
-
required=request_body.required,
|
|
293
|
-
content_schema=content_schemas,
|
|
294
|
-
description=request_body.description,
|
|
295
|
-
)
|
|
296
|
-
except (ValidationError, ValueError, AttributeError) as e:
|
|
297
|
-
ref_name = getattr(request_body_or_ref, "ref", "unknown")
|
|
298
|
-
logger.error(
|
|
299
|
-
f"Failed to extract request body '{ref_name}': {e}", exc_info=False
|
|
300
|
-
)
|
|
301
|
-
return None
|
|
302
|
-
|
|
303
|
-
|
|
304
189
|
def _extract_responses(
|
|
305
190
|
operation_responses: dict[str, Response | Reference] | None,
|
|
306
191
|
openapi: OpenAPI,
|
|
@@ -358,194 +243,688 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
|
|
|
358
243
|
"""
|
|
359
244
|
Parses an OpenAPI schema dictionary into a list of HTTPRoute objects
|
|
360
245
|
using the openapi-pydantic library.
|
|
246
|
+
|
|
247
|
+
Supports both OpenAPI 3.0.x and 3.1.x versions.
|
|
361
248
|
"""
|
|
362
|
-
|
|
249
|
+
# Check OpenAPI version to use appropriate model
|
|
250
|
+
openapi_version = openapi_dict.get("openapi", "")
|
|
251
|
+
|
|
363
252
|
try:
|
|
364
|
-
|
|
365
|
-
|
|
253
|
+
if openapi_version.startswith("3.0"):
|
|
254
|
+
# Use OpenAPI 3.0 models
|
|
255
|
+
openapi_30 = OpenAPI_30.model_validate(openapi_dict)
|
|
256
|
+
logger.info(
|
|
257
|
+
f"Successfully parsed OpenAPI 3.0 schema version: {openapi_30.openapi}"
|
|
258
|
+
)
|
|
259
|
+
parser = OpenAPI30Parser(openapi_30)
|
|
260
|
+
return parser.parse()
|
|
261
|
+
else:
|
|
262
|
+
# Default to OpenAPI 3.1 models
|
|
263
|
+
openapi_31 = OpenAPI.model_validate(openapi_dict)
|
|
264
|
+
logger.info(
|
|
265
|
+
f"Successfully parsed OpenAPI 3.1 schema version: {openapi_31.openapi}"
|
|
266
|
+
)
|
|
267
|
+
parser = OpenAPI31Parser(openapi_31)
|
|
268
|
+
return parser.parse()
|
|
366
269
|
except ValidationError as e:
|
|
367
270
|
logger.error(f"OpenAPI schema validation failed: {e}")
|
|
368
271
|
error_details = e.errors()
|
|
369
272
|
logger.error(f"Validation errors: {error_details}")
|
|
370
273
|
raise ValueError(f"Invalid OpenAPI schema: {error_details}") from e
|
|
371
274
|
|
|
372
|
-
if not openapi.paths:
|
|
373
|
-
logger.warning("OpenAPI schema has no paths defined.")
|
|
374
|
-
return []
|
|
375
275
|
|
|
376
|
-
|
|
377
|
-
|
|
276
|
+
# Base parser class for shared functionality
|
|
277
|
+
class BaseOpenAPIParser:
|
|
278
|
+
"""Base class for OpenAPI parsers with common functionality."""
|
|
279
|
+
|
|
280
|
+
def _convert_to_parameter_location(self, param_in: str) -> ParameterLocation:
|
|
281
|
+
"""Convert string parameter location to our ParameterLocation type."""
|
|
282
|
+
if param_in == "path":
|
|
283
|
+
return "path"
|
|
284
|
+
elif param_in == "query":
|
|
285
|
+
return "query"
|
|
286
|
+
elif param_in == "header":
|
|
287
|
+
return "header"
|
|
288
|
+
elif param_in == "cookie":
|
|
289
|
+
return "cookie"
|
|
290
|
+
else:
|
|
378
291
|
logger.warning(
|
|
379
|
-
f"
|
|
292
|
+
f"Unknown parameter location: {param_in}, defaulting to 'query'"
|
|
380
293
|
)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
294
|
+
return "query"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class OpenAPI31Parser(BaseOpenAPIParser):
|
|
298
|
+
"""Parser for OpenAPI 3.1 schemas."""
|
|
299
|
+
|
|
300
|
+
def __init__(self, openapi: OpenAPI):
|
|
301
|
+
self.openapi = openapi
|
|
302
|
+
|
|
303
|
+
def parse(self) -> list[HTTPRoute]:
|
|
304
|
+
"""Parse an OpenAPI 3.1 schema into HTTP routes."""
|
|
305
|
+
routes: list[HTTPRoute] = []
|
|
306
|
+
|
|
307
|
+
if not self.openapi.paths:
|
|
308
|
+
logger.warning("OpenAPI schema has no paths defined.")
|
|
309
|
+
return []
|
|
310
|
+
|
|
311
|
+
for path_str, path_item_obj in self.openapi.paths.items():
|
|
312
|
+
if not isinstance(path_item_obj, PathItem):
|
|
313
|
+
logger.warning(
|
|
314
|
+
f"Skipping invalid path item object for path '{path_str}' (type: {type(path_item_obj)})"
|
|
315
|
+
)
|
|
398
316
|
continue
|
|
399
317
|
|
|
400
|
-
|
|
318
|
+
path_level_params = path_item_obj.parameters
|
|
319
|
+
|
|
320
|
+
# Iterate through possible HTTP methods defined in the PathItem model fields
|
|
321
|
+
# Use model_fields from the class, not the instance
|
|
322
|
+
for method_lower in PathItem.model_fields.keys():
|
|
323
|
+
if method_lower not in [
|
|
324
|
+
"get",
|
|
325
|
+
"put",
|
|
326
|
+
"post",
|
|
327
|
+
"delete",
|
|
328
|
+
"options",
|
|
329
|
+
"head",
|
|
330
|
+
"patch",
|
|
331
|
+
"trace",
|
|
332
|
+
]:
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
operation: Operation | None = getattr(path_item_obj, method_lower, None)
|
|
336
|
+
|
|
337
|
+
if operation and isinstance(operation, Operation):
|
|
338
|
+
method_upper = cast(HttpMethod, method_lower.upper())
|
|
339
|
+
logger.debug(f"Processing operation: {method_upper} {path_str}")
|
|
340
|
+
try:
|
|
341
|
+
parameters = self._extract_parameters(
|
|
342
|
+
operation.parameters, path_level_params
|
|
343
|
+
)
|
|
344
|
+
request_body_info = self._extract_request_body(
|
|
345
|
+
operation.requestBody
|
|
346
|
+
)
|
|
347
|
+
responses = self._extract_responses(operation.responses)
|
|
348
|
+
|
|
349
|
+
route = HTTPRoute(
|
|
350
|
+
path=path_str,
|
|
351
|
+
method=method_upper,
|
|
352
|
+
operation_id=operation.operationId,
|
|
353
|
+
summary=operation.summary,
|
|
354
|
+
description=operation.description,
|
|
355
|
+
tags=operation.tags or [],
|
|
356
|
+
parameters=parameters,
|
|
357
|
+
request_body=request_body_info,
|
|
358
|
+
responses=responses,
|
|
359
|
+
)
|
|
360
|
+
routes.append(route)
|
|
361
|
+
logger.info(
|
|
362
|
+
f"Successfully extracted route: {method_upper} {path_str}"
|
|
363
|
+
)
|
|
364
|
+
except Exception as op_error:
|
|
365
|
+
op_id = operation.operationId or "unknown"
|
|
366
|
+
logger.error(
|
|
367
|
+
f"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}",
|
|
368
|
+
exc_info=True,
|
|
369
|
+
)
|
|
401
370
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
371
|
+
logger.info(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
|
|
372
|
+
return routes
|
|
373
|
+
|
|
374
|
+
def _resolve_ref(
|
|
375
|
+
self, item: Reference | Schema | Parameter | RequestBody | Any
|
|
376
|
+
) -> Any:
|
|
377
|
+
"""Resolves a potential Reference object to its target definition."""
|
|
378
|
+
if isinstance(item, Reference):
|
|
379
|
+
ref_str = item.ref
|
|
380
|
+
try:
|
|
381
|
+
if not ref_str.startswith("#/"):
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"External or non-local reference not supported: {ref_str}"
|
|
408
384
|
)
|
|
409
|
-
|
|
410
|
-
|
|
385
|
+
parts = ref_str.strip("#/").split("/")
|
|
386
|
+
target = self.openapi
|
|
387
|
+
for part in parts:
|
|
388
|
+
if part.isdigit() and isinstance(target, list):
|
|
389
|
+
target = target[int(part)]
|
|
390
|
+
elif isinstance(target, BaseModel):
|
|
391
|
+
# Use model_extra for fields not explicitly defined (like components types)
|
|
392
|
+
# Check class fields first, then model_extra
|
|
393
|
+
if part in target.__class__.model_fields:
|
|
394
|
+
target = getattr(target, part, None)
|
|
395
|
+
elif target.model_extra and part in target.model_extra:
|
|
396
|
+
target = target.model_extra[part]
|
|
397
|
+
else:
|
|
398
|
+
# Special handling for components sub-types common structure
|
|
399
|
+
if part == "components" and hasattr(target, "components"):
|
|
400
|
+
target = getattr(target, "components")
|
|
401
|
+
elif hasattr(target, part): # Fallback check
|
|
402
|
+
target = getattr(target, part, None)
|
|
403
|
+
else:
|
|
404
|
+
target = None # Part not found
|
|
405
|
+
elif isinstance(target, dict):
|
|
406
|
+
target = target.get(part)
|
|
407
|
+
else:
|
|
408
|
+
raise ValueError(
|
|
409
|
+
f"Cannot traverse part '{part}' in reference '{ref_str}' from type {type(target)}"
|
|
410
|
+
)
|
|
411
|
+
if target is None:
|
|
412
|
+
raise ValueError(
|
|
413
|
+
f"Reference part '{part}' not found in path '{ref_str}'"
|
|
414
|
+
)
|
|
415
|
+
if isinstance(target, Reference):
|
|
416
|
+
return self._resolve_ref(target)
|
|
417
|
+
return target
|
|
418
|
+
except (AttributeError, KeyError, IndexError, TypeError, ValueError) as e:
|
|
419
|
+
raise ValueError(f"Failed to resolve reference '{ref_str}': {e}") from e
|
|
420
|
+
return item
|
|
421
|
+
|
|
422
|
+
def _extract_schema_as_dict(self, schema_obj: Schema | Reference) -> JsonSchema:
|
|
423
|
+
"""Resolves a schema/reference and returns it as a dictionary."""
|
|
424
|
+
resolved_schema = self._resolve_ref(schema_obj)
|
|
425
|
+
if isinstance(resolved_schema, Schema):
|
|
426
|
+
# Using exclude_none=True might be better than exclude_unset sometimes
|
|
427
|
+
return resolved_schema.model_dump(
|
|
428
|
+
mode="json", by_alias=True, exclude_none=True
|
|
429
|
+
)
|
|
430
|
+
elif isinstance(resolved_schema, dict):
|
|
431
|
+
logger.warning(
|
|
432
|
+
"Resolved schema reference resulted in a dict, not a Schema model."
|
|
433
|
+
)
|
|
434
|
+
return resolved_schema
|
|
435
|
+
else:
|
|
436
|
+
ref_str = getattr(schema_obj, "ref", "unknown")
|
|
437
|
+
logger.warning(
|
|
438
|
+
f"Expected Schema after resolving ref '{ref_str}', got {type(resolved_schema)}. Returning empty dict."
|
|
439
|
+
)
|
|
440
|
+
return {}
|
|
441
|
+
|
|
442
|
+
def _extract_parameters(
|
|
443
|
+
self,
|
|
444
|
+
operation_params: list[Parameter | Reference] | None,
|
|
445
|
+
path_item_params: list[Parameter | Reference] | None,
|
|
446
|
+
) -> list[ParameterInfo]:
|
|
447
|
+
"""Extracts and resolves parameters using corrected attribute names."""
|
|
448
|
+
extracted_params: list[ParameterInfo] = []
|
|
449
|
+
seen_params: dict[
|
|
450
|
+
tuple[str, str], bool
|
|
451
|
+
] = {} # Use string keys to avoid type issues
|
|
452
|
+
all_params_refs = (operation_params or []) + (path_item_params or [])
|
|
453
|
+
|
|
454
|
+
for param_or_ref in all_params_refs:
|
|
455
|
+
try:
|
|
456
|
+
parameter = cast(Parameter, self._resolve_ref(param_or_ref))
|
|
457
|
+
if not isinstance(parameter, Parameter):
|
|
458
|
+
# ... (error logging remains the same)
|
|
459
|
+
continue
|
|
460
|
+
|
|
461
|
+
# --- *** CORRECTED ATTRIBUTE ACCESS HERE *** ---
|
|
462
|
+
param_in = parameter.param_in # CORRECTED: Use 'param_in'
|
|
463
|
+
param_location = self._convert_to_parameter_location(param_in)
|
|
464
|
+
param_schema_obj = (
|
|
465
|
+
parameter.param_schema
|
|
466
|
+
) # CORRECTED: Use 'param_schema'
|
|
467
|
+
# --- *** ---
|
|
468
|
+
|
|
469
|
+
param_key = (parameter.name, param_in)
|
|
470
|
+
if param_key in seen_params:
|
|
471
|
+
continue
|
|
472
|
+
seen_params[param_key] = True
|
|
473
|
+
|
|
474
|
+
param_schema_dict = {}
|
|
475
|
+
if param_schema_obj: # Check if schema exists
|
|
476
|
+
param_schema_dict = self._extract_schema_as_dict(param_schema_obj)
|
|
477
|
+
elif parameter.content:
|
|
478
|
+
# Handle complex parameters with 'content'
|
|
479
|
+
first_media_type = next(iter(parameter.content.values()), None)
|
|
480
|
+
if (
|
|
481
|
+
first_media_type and first_media_type.media_type_schema
|
|
482
|
+
): # CORRECTED: Use 'media_type_schema'
|
|
483
|
+
param_schema_dict = self._extract_schema_as_dict(
|
|
484
|
+
first_media_type.media_type_schema
|
|
485
|
+
)
|
|
486
|
+
logger.debug(
|
|
487
|
+
f"Parameter '{parameter.name}' using schema from 'content' field."
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Manually create ParameterInfo instance using correct field names
|
|
491
|
+
param_info = ParameterInfo(
|
|
492
|
+
name=parameter.name,
|
|
493
|
+
location=param_location, # Use converted parameter location
|
|
494
|
+
required=parameter.required,
|
|
495
|
+
schema=param_schema_dict, # Populate 'schema' field in IR
|
|
496
|
+
description=parameter.description,
|
|
497
|
+
)
|
|
498
|
+
extracted_params.append(param_info)
|
|
499
|
+
|
|
500
|
+
except (
|
|
501
|
+
ValidationError,
|
|
502
|
+
ValueError,
|
|
503
|
+
AttributeError,
|
|
504
|
+
TypeError,
|
|
505
|
+
) as e: # Added TypeError
|
|
506
|
+
param_name = getattr(
|
|
507
|
+
param_or_ref, "name", getattr(param_or_ref, "ref", "unknown")
|
|
508
|
+
)
|
|
509
|
+
logger.error(
|
|
510
|
+
f"Failed to extract parameter '{param_name}': {e}", exc_info=False
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return extracted_params
|
|
514
|
+
|
|
515
|
+
def _extract_request_body(
|
|
516
|
+
self, request_body_or_ref: RequestBody | Reference | None
|
|
517
|
+
) -> RequestBodyInfo | None:
|
|
518
|
+
"""Extracts and resolves the request body using corrected attribute names."""
|
|
519
|
+
if not request_body_or_ref:
|
|
520
|
+
return None
|
|
521
|
+
try:
|
|
522
|
+
request_body = cast(RequestBody, self._resolve_ref(request_body_or_ref))
|
|
523
|
+
if not isinstance(request_body, RequestBody):
|
|
524
|
+
# ... (error logging remains the same)
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
content_schemas: dict[str, JsonSchema] = {}
|
|
528
|
+
if request_body.content:
|
|
529
|
+
for media_type_str, media_type_obj in request_body.content.items():
|
|
530
|
+
# --- *** CORRECTED ATTRIBUTE ACCESS HERE *** ---
|
|
531
|
+
if (
|
|
532
|
+
isinstance(media_type_obj, MediaType)
|
|
533
|
+
and media_type_obj.media_type_schema
|
|
534
|
+
): # CORRECTED: Use 'media_type_schema'
|
|
535
|
+
# --- *** ---
|
|
536
|
+
try:
|
|
537
|
+
# Use the corrected attribute here as well
|
|
538
|
+
schema_dict = self._extract_schema_as_dict(
|
|
539
|
+
media_type_obj.media_type_schema
|
|
540
|
+
)
|
|
541
|
+
content_schemas[media_type_str] = schema_dict
|
|
542
|
+
except ValueError as schema_err:
|
|
543
|
+
logger.error(
|
|
544
|
+
f"Failed to extract schema for media type '{media_type_str}' in request body: {schema_err}"
|
|
545
|
+
)
|
|
546
|
+
elif not isinstance(media_type_obj, MediaType):
|
|
547
|
+
logger.warning(
|
|
548
|
+
f"Skipping invalid media type object for '{media_type_str}' (type: {type(media_type_obj)}) in request body."
|
|
549
|
+
)
|
|
550
|
+
elif not media_type_obj.media_type_schema: # Corrected check
|
|
551
|
+
logger.warning(
|
|
552
|
+
f"Skipping media type '{media_type_str}' in request body because it lacks a schema."
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
return RequestBodyInfo(
|
|
556
|
+
required=request_body.required,
|
|
557
|
+
content_schema=content_schemas,
|
|
558
|
+
description=request_body.description,
|
|
559
|
+
)
|
|
560
|
+
except (ValidationError, ValueError, AttributeError) as e:
|
|
561
|
+
ref_name = getattr(request_body_or_ref, "ref", "unknown")
|
|
562
|
+
logger.error(
|
|
563
|
+
f"Failed to extract request body '{ref_name}': {e}", exc_info=False
|
|
564
|
+
)
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
def _extract_responses(
|
|
568
|
+
self,
|
|
569
|
+
operation_responses: dict[str, Response | Reference] | None,
|
|
570
|
+
) -> dict[str, ResponseInfo]:
|
|
571
|
+
"""Extracts and resolves response information for an operation."""
|
|
572
|
+
extracted_responses: dict[str, ResponseInfo] = {}
|
|
573
|
+
if not operation_responses:
|
|
574
|
+
return extracted_responses
|
|
575
|
+
|
|
576
|
+
for status_code, resp_or_ref in operation_responses.items():
|
|
577
|
+
try:
|
|
578
|
+
response = cast(Response, self._resolve_ref(resp_or_ref))
|
|
579
|
+
if not isinstance(response, Response):
|
|
580
|
+
ref_str = getattr(resp_or_ref, "ref", "unknown")
|
|
581
|
+
logger.warning(
|
|
582
|
+
f"Expected Response after resolving ref '{ref_str}' for status code {status_code}, got {type(response)}. Skipping."
|
|
411
583
|
)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
584
|
+
continue
|
|
585
|
+
|
|
586
|
+
content_schemas: dict[str, JsonSchema] = {}
|
|
587
|
+
if response.content:
|
|
588
|
+
for media_type_str, media_type_obj in response.content.items():
|
|
589
|
+
if (
|
|
590
|
+
isinstance(media_type_obj, MediaType)
|
|
591
|
+
and media_type_obj.media_type_schema
|
|
592
|
+
):
|
|
593
|
+
try:
|
|
594
|
+
schema_dict = self._extract_schema_as_dict(
|
|
595
|
+
media_type_obj.media_type_schema
|
|
596
|
+
)
|
|
597
|
+
content_schemas[media_type_str] = schema_dict
|
|
598
|
+
except ValueError as schema_err:
|
|
599
|
+
logger.error(
|
|
600
|
+
f"Failed to extract schema for media type '{media_type_str}' in response {status_code}: {schema_err}"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
resp_info = ResponseInfo(
|
|
604
|
+
description=response.description, content_schema=content_schemas
|
|
605
|
+
)
|
|
606
|
+
extracted_responses[str(status_code)] = resp_info
|
|
607
|
+
|
|
608
|
+
except (ValidationError, ValueError, AttributeError) as e:
|
|
609
|
+
ref_name = getattr(resp_or_ref, "ref", "unknown")
|
|
610
|
+
logger.error(
|
|
611
|
+
f"Failed to extract response for status code {status_code} "
|
|
612
|
+
f"from reference '{ref_name}': {e}",
|
|
613
|
+
exc_info=False,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
return extracted_responses
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class OpenAPI30Parser(BaseOpenAPIParser):
|
|
620
|
+
"""Parser for OpenAPI 3.0 schemas."""
|
|
621
|
+
|
|
622
|
+
def __init__(self, openapi: OpenAPI_30):
|
|
623
|
+
self.openapi = openapi
|
|
624
|
+
|
|
625
|
+
def parse(self) -> list[HTTPRoute]:
|
|
626
|
+
"""Parse an OpenAPI 3.0 schema into HTTP routes."""
|
|
627
|
+
routes: list[HTTPRoute] = []
|
|
628
|
+
|
|
629
|
+
if not self.openapi.paths:
|
|
630
|
+
logger.warning("OpenAPI schema has no paths defined.")
|
|
631
|
+
return []
|
|
632
|
+
|
|
633
|
+
for path_str, path_item_obj in self.openapi.paths.items():
|
|
634
|
+
if not isinstance(path_item_obj, PathItem_30):
|
|
635
|
+
logger.warning(
|
|
636
|
+
f"Skipping invalid path item object for path '{path_str}' (type: {type(path_item_obj)})"
|
|
637
|
+
)
|
|
638
|
+
continue
|
|
639
|
+
|
|
640
|
+
path_level_params = path_item_obj.parameters
|
|
641
|
+
|
|
642
|
+
# Iterate through possible HTTP methods defined in the PathItem model fields
|
|
643
|
+
# Use model_fields from the class, not the instance
|
|
644
|
+
for method_lower in PathItem_30.model_fields.keys():
|
|
645
|
+
if method_lower not in [
|
|
646
|
+
"get",
|
|
647
|
+
"put",
|
|
648
|
+
"post",
|
|
649
|
+
"delete",
|
|
650
|
+
"options",
|
|
651
|
+
"head",
|
|
652
|
+
"patch",
|
|
653
|
+
"trace",
|
|
654
|
+
]:
|
|
655
|
+
continue
|
|
656
|
+
|
|
657
|
+
operation: Operation_30 | None = getattr(
|
|
658
|
+
path_item_obj, method_lower, None
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
if operation and isinstance(operation, Operation_30):
|
|
662
|
+
method_upper = cast(HttpMethod, method_lower.upper())
|
|
663
|
+
logger.debug(f"Processing operation: {method_upper} {path_str}")
|
|
664
|
+
try:
|
|
665
|
+
parameters = self._extract_parameters(
|
|
666
|
+
operation.parameters, path_level_params
|
|
667
|
+
)
|
|
668
|
+
request_body_info = self._extract_request_body(
|
|
669
|
+
operation.requestBody
|
|
670
|
+
)
|
|
671
|
+
responses = self._extract_responses(operation.responses)
|
|
672
|
+
|
|
673
|
+
route = HTTPRoute(
|
|
674
|
+
path=path_str,
|
|
675
|
+
method=method_upper,
|
|
676
|
+
operation_id=operation.operationId,
|
|
677
|
+
summary=operation.summary,
|
|
678
|
+
description=operation.description,
|
|
679
|
+
tags=operation.tags or [],
|
|
680
|
+
parameters=parameters,
|
|
681
|
+
request_body=request_body_info,
|
|
682
|
+
responses=responses,
|
|
683
|
+
)
|
|
684
|
+
routes.append(route)
|
|
685
|
+
logger.info(
|
|
686
|
+
f"Successfully extracted route: {method_upper} {path_str}"
|
|
687
|
+
)
|
|
688
|
+
except Exception as op_error:
|
|
689
|
+
op_id = operation.operationId or "unknown"
|
|
690
|
+
logger.error(
|
|
691
|
+
f"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}",
|
|
692
|
+
exc_info=True,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
logger.info(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
|
|
696
|
+
return routes
|
|
697
|
+
|
|
698
|
+
def _resolve_ref(
|
|
699
|
+
self, item: Reference_30 | Schema_30 | Parameter_30 | RequestBody_30 | Any
|
|
700
|
+
) -> Any:
|
|
701
|
+
"""Resolves a potential Reference object to its target definition for OpenAPI 3.0."""
|
|
702
|
+
if isinstance(item, Reference_30):
|
|
703
|
+
ref_str = item.ref
|
|
704
|
+
try:
|
|
705
|
+
if not ref_str.startswith("#/"):
|
|
706
|
+
raise ValueError(
|
|
707
|
+
f"External or non-local reference not supported: {ref_str}"
|
|
424
708
|
)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
709
|
+
parts = ref_str.strip("#/").split("/")
|
|
710
|
+
target = self.openapi
|
|
711
|
+
for part in parts:
|
|
712
|
+
if part.isdigit() and isinstance(target, list):
|
|
713
|
+
target = target[int(part)]
|
|
714
|
+
elif isinstance(target, BaseModel):
|
|
715
|
+
# Use model_extra for fields not explicitly defined (like components types)
|
|
716
|
+
# Check class fields first, then model_extra
|
|
717
|
+
if part in target.__class__.model_fields:
|
|
718
|
+
target = getattr(target, part, None)
|
|
719
|
+
elif target.model_extra and part in target.model_extra:
|
|
720
|
+
target = target.model_extra[part]
|
|
721
|
+
else:
|
|
722
|
+
# Special handling for components sub-types common structure
|
|
723
|
+
if part == "components" and hasattr(target, "components"):
|
|
724
|
+
target = getattr(target, "components")
|
|
725
|
+
elif hasattr(target, part): # Fallback check
|
|
726
|
+
target = getattr(target, part, None)
|
|
727
|
+
else:
|
|
728
|
+
target = None # Part not found
|
|
729
|
+
elif isinstance(target, dict):
|
|
730
|
+
target = target.get(part)
|
|
731
|
+
else:
|
|
732
|
+
raise ValueError(
|
|
733
|
+
f"Cannot traverse part '{part}' in reference '{ref_str}' from type {type(target)}"
|
|
734
|
+
)
|
|
735
|
+
if target is None:
|
|
736
|
+
raise ValueError(
|
|
737
|
+
f"Reference part '{part}' not found in path '{ref_str}'"
|
|
738
|
+
)
|
|
739
|
+
if isinstance(target, Reference_30):
|
|
740
|
+
return self._resolve_ref(target)
|
|
741
|
+
return target
|
|
742
|
+
except (AttributeError, KeyError, IndexError, TypeError, ValueError) as e:
|
|
743
|
+
raise ValueError(f"Failed to resolve reference '{ref_str}': {e}") from e
|
|
744
|
+
return item
|
|
745
|
+
|
|
746
|
+
def _extract_schema_as_dict(
|
|
747
|
+
self, schema_obj: Schema_30 | Reference_30
|
|
748
|
+
) -> JsonSchema:
|
|
749
|
+
"""Resolves a schema/reference and returns it as a dictionary for OpenAPI 3.0."""
|
|
750
|
+
resolved_schema = self._resolve_ref(schema_obj)
|
|
751
|
+
if isinstance(resolved_schema, Schema_30):
|
|
752
|
+
# Using exclude_none=True might be better than exclude_unset sometimes
|
|
753
|
+
return resolved_schema.model_dump(
|
|
754
|
+
mode="json", by_alias=True, exclude_none=True
|
|
755
|
+
)
|
|
756
|
+
elif isinstance(resolved_schema, dict):
|
|
757
|
+
logger.warning(
|
|
758
|
+
"Resolved schema reference resulted in a dict, not a Schema model."
|
|
759
|
+
)
|
|
760
|
+
return resolved_schema
|
|
761
|
+
else:
|
|
762
|
+
ref_str = getattr(schema_obj, "ref", "unknown")
|
|
763
|
+
logger.warning(
|
|
764
|
+
f"Expected Schema after resolving ref '{ref_str}', got {type(resolved_schema)}. Returning empty dict."
|
|
765
|
+
)
|
|
766
|
+
return {}
|
|
767
|
+
|
|
768
|
+
def _extract_parameters(
|
|
769
|
+
self,
|
|
770
|
+
operation_params: list[Parameter_30 | Reference_30] | None,
|
|
771
|
+
path_item_params: list[Parameter_30 | Reference_30] | None,
|
|
772
|
+
) -> list[ParameterInfo]:
|
|
773
|
+
"""Extracts and resolves parameters for OpenAPI 3.0."""
|
|
774
|
+
extracted_params: list[ParameterInfo] = []
|
|
775
|
+
seen_params: dict[
|
|
776
|
+
tuple[str, str], bool
|
|
777
|
+
] = {} # Use string keys to avoid type issues
|
|
778
|
+
all_params_refs = (operation_params or []) + (path_item_params or [])
|
|
779
|
+
|
|
780
|
+
for param_or_ref in all_params_refs:
|
|
781
|
+
try:
|
|
782
|
+
parameter = cast(Parameter_30, self._resolve_ref(param_or_ref))
|
|
783
|
+
if not isinstance(parameter, Parameter_30):
|
|
784
|
+
logger.warning(
|
|
785
|
+
f"Expected Parameter after resolving reference, got {type(parameter)}. Skipping."
|
|
428
786
|
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
787
|
+
continue
|
|
788
|
+
|
|
789
|
+
# OpenAPI 3.0 uses 'in' field for parameter location
|
|
790
|
+
param_in = parameter.param_in
|
|
791
|
+
param_location = self._convert_to_parameter_location(param_in)
|
|
792
|
+
param_schema_obj = parameter.param_schema
|
|
793
|
+
|
|
794
|
+
param_key = (parameter.name, param_in)
|
|
795
|
+
if param_key in seen_params:
|
|
796
|
+
continue
|
|
797
|
+
seen_params[param_key] = True
|
|
798
|
+
|
|
799
|
+
param_schema_dict = {}
|
|
800
|
+
if param_schema_obj: # Check if schema exists
|
|
801
|
+
param_schema_dict = self._extract_schema_as_dict(param_schema_obj)
|
|
802
|
+
elif parameter.content:
|
|
803
|
+
# Handle complex parameters with 'content'
|
|
804
|
+
first_media_type = next(iter(parameter.content.values()), None)
|
|
805
|
+
if first_media_type and first_media_type.media_type_schema:
|
|
806
|
+
param_schema_dict = self._extract_schema_as_dict(
|
|
807
|
+
first_media_type.media_type_schema
|
|
808
|
+
)
|
|
809
|
+
logger.debug(
|
|
810
|
+
f"Parameter '{parameter.name}' using schema from 'content' field."
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
# Manually create ParameterInfo instance using correct field names
|
|
814
|
+
param_info = ParameterInfo(
|
|
815
|
+
name=parameter.name,
|
|
816
|
+
location=param_location, # Use converted parameter location
|
|
817
|
+
required=parameter.required,
|
|
818
|
+
schema=param_schema_dict, # Populate 'schema' field in IR
|
|
819
|
+
description=parameter.description,
|
|
820
|
+
)
|
|
821
|
+
extracted_params.append(param_info)
|
|
822
|
+
|
|
823
|
+
except (
|
|
824
|
+
ValidationError,
|
|
825
|
+
ValueError,
|
|
826
|
+
AttributeError,
|
|
827
|
+
TypeError,
|
|
828
|
+
) as e: # Added TypeError
|
|
829
|
+
param_name = getattr(
|
|
830
|
+
param_or_ref, "name", getattr(param_or_ref, "ref", "unknown")
|
|
831
|
+
)
|
|
832
|
+
logger.error(
|
|
833
|
+
f"Failed to extract parameter '{param_name}': {e}", exc_info=False
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
return extracted_params
|
|
837
|
+
|
|
838
|
+
def _extract_request_body(
|
|
839
|
+
self, request_body_or_ref: RequestBody_30 | Reference_30 | None
|
|
840
|
+
) -> RequestBodyInfo | None:
|
|
841
|
+
"""Extracts request body information for OpenAPI 3.0 using correct attribute names."""
|
|
842
|
+
if request_body_or_ref is None:
|
|
843
|
+
return None
|
|
844
|
+
|
|
845
|
+
try:
|
|
846
|
+
request_body = cast(RequestBody_30, self._resolve_ref(request_body_or_ref))
|
|
847
|
+
|
|
848
|
+
if not isinstance(request_body, RequestBody_30):
|
|
849
|
+
logger.warning(
|
|
850
|
+
f"Expected RequestBody after resolving reference, got {type(request_body)}. Returning None."
|
|
851
|
+
)
|
|
852
|
+
return None
|
|
853
|
+
|
|
854
|
+
request_body_info = RequestBodyInfo(
|
|
855
|
+
required=request_body.required,
|
|
856
|
+
description=request_body.description,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
# Process content field for request body schemas
|
|
860
|
+
if request_body.content:
|
|
861
|
+
for media_type_key, media_type_obj in request_body.content.items():
|
|
862
|
+
if (
|
|
863
|
+
media_type_obj and media_type_obj.media_type_schema
|
|
864
|
+
): # CORRECTED: Use 'media_type_schema'
|
|
865
|
+
schema_dict = self._extract_schema_as_dict(
|
|
866
|
+
media_type_obj.media_type_schema
|
|
867
|
+
)
|
|
868
|
+
request_body_info.content_schema[media_type_key] = schema_dict
|
|
869
|
+
|
|
870
|
+
return request_body_info
|
|
871
|
+
|
|
872
|
+
except (ValidationError, ValueError, AttributeError) as e:
|
|
873
|
+
ref_str = getattr(request_body_or_ref, "ref", "unknown")
|
|
874
|
+
logger.error(
|
|
875
|
+
f"Failed to extract request body info from reference '{ref_str}': {e}",
|
|
876
|
+
exc_info=False,
|
|
877
|
+
)
|
|
878
|
+
return None
|
|
879
|
+
|
|
880
|
+
def _extract_responses(
|
|
881
|
+
self,
|
|
882
|
+
operation_responses: dict[str, Response_30 | Reference_30] | None,
|
|
883
|
+
) -> dict[str, ResponseInfo]:
|
|
884
|
+
"""Extracts response information from an OpenAPI 3.0 operation's responses."""
|
|
885
|
+
extracted_responses: dict[str, ResponseInfo] = {}
|
|
886
|
+
if not operation_responses:
|
|
887
|
+
return extracted_responses
|
|
888
|
+
|
|
889
|
+
for status_code, response_or_ref in operation_responses.items():
|
|
890
|
+
try:
|
|
891
|
+
# Skip 'default' response for simplicity if needed
|
|
892
|
+
# if status_code == "default":
|
|
893
|
+
# continue
|
|
894
|
+
|
|
895
|
+
response = cast(Response_30, self._resolve_ref(response_or_ref))
|
|
896
|
+
|
|
897
|
+
if not isinstance(response, Response_30):
|
|
898
|
+
logger.warning(
|
|
899
|
+
f"Expected Response after resolving reference for status code {status_code}, "
|
|
900
|
+
f"got {type(response)}. Skipping."
|
|
434
901
|
)
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
response_info = ResponseInfo(description=response.description)
|
|
905
|
+
|
|
906
|
+
# Extract content schemas if present
|
|
907
|
+
if response.content:
|
|
908
|
+
for media_type_key, media_type_obj in response.content.items():
|
|
909
|
+
if (
|
|
910
|
+
media_type_obj and media_type_obj.media_type_schema
|
|
911
|
+
): # CORRECTED: Use 'media_type_schema'
|
|
912
|
+
schema_dict = self._extract_schema_as_dict(
|
|
913
|
+
media_type_obj.media_type_schema
|
|
914
|
+
)
|
|
915
|
+
response_info.content_schema[media_type_key] = schema_dict
|
|
435
916
|
|
|
436
|
-
|
|
437
|
-
return routes
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
# --- Example Usage (Optional) ---
|
|
441
|
-
if __name__ == "__main__":
|
|
442
|
-
import json
|
|
443
|
-
|
|
444
|
-
logging.basicConfig(
|
|
445
|
-
level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s"
|
|
446
|
-
) # Set to INFO
|
|
447
|
-
|
|
448
|
-
petstore_schema = {
|
|
449
|
-
"openapi": "3.1.0", # Keep corrected version
|
|
450
|
-
"info": {"title": "Simple Pet Store API", "version": "1.0.0"},
|
|
451
|
-
"paths": {
|
|
452
|
-
"/pets": {
|
|
453
|
-
"get": {
|
|
454
|
-
"summary": "list all pets",
|
|
455
|
-
"operationId": "listPets",
|
|
456
|
-
"tags": ["pets"],
|
|
457
|
-
"parameters": [
|
|
458
|
-
{
|
|
459
|
-
"name": "limit",
|
|
460
|
-
"in": "query",
|
|
461
|
-
"description": "How many items to return",
|
|
462
|
-
"required": False,
|
|
463
|
-
"schema": {"type": "integer", "format": "int32"},
|
|
464
|
-
}
|
|
465
|
-
],
|
|
466
|
-
"responses": {"200": {"description": "A paged array of pets"}},
|
|
467
|
-
},
|
|
468
|
-
"post": {
|
|
469
|
-
"summary": "Create a pet",
|
|
470
|
-
"operationId": "createPet",
|
|
471
|
-
"tags": ["pets"],
|
|
472
|
-
"requestBody": {"$ref": "#/components/requestBodies/PetBody"},
|
|
473
|
-
"responses": {"201": {"description": "Null response"}},
|
|
474
|
-
},
|
|
475
|
-
},
|
|
476
|
-
"/pets/{petId}": {
|
|
477
|
-
"get": {
|
|
478
|
-
"summary": "Info for a specific pet",
|
|
479
|
-
"operationId": "showPetById",
|
|
480
|
-
"tags": ["pets"],
|
|
481
|
-
"parameters": [
|
|
482
|
-
{
|
|
483
|
-
"name": "petId",
|
|
484
|
-
"in": "path",
|
|
485
|
-
"required": True,
|
|
486
|
-
"description": "The id of the pet",
|
|
487
|
-
"schema": {"type": "string"},
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
"name": "X-Request-ID",
|
|
491
|
-
"in": "header",
|
|
492
|
-
"required": False,
|
|
493
|
-
"schema": {"type": "string", "format": "uuid"},
|
|
494
|
-
},
|
|
495
|
-
],
|
|
496
|
-
"responses": {"200": {"description": "Information about the pet"}},
|
|
497
|
-
},
|
|
498
|
-
"parameters": [ # Path level parameter example
|
|
499
|
-
{
|
|
500
|
-
"name": "traceId",
|
|
501
|
-
"in": "header",
|
|
502
|
-
"description": "Common trace ID",
|
|
503
|
-
"required": False,
|
|
504
|
-
"schema": {"type": "string"},
|
|
505
|
-
}
|
|
506
|
-
],
|
|
507
|
-
},
|
|
508
|
-
},
|
|
509
|
-
"components": {
|
|
510
|
-
"schemas": {
|
|
511
|
-
"Pet": {
|
|
512
|
-
"type": "object",
|
|
513
|
-
"required": ["id", "name"],
|
|
514
|
-
"properties": {
|
|
515
|
-
"id": {"type": "integer", "format": "int64"},
|
|
516
|
-
"name": {"type": "string"},
|
|
517
|
-
"tag": {"type": "string"},
|
|
518
|
-
},
|
|
519
|
-
}
|
|
520
|
-
},
|
|
521
|
-
"requestBodies": {
|
|
522
|
-
"PetBody": {
|
|
523
|
-
"description": "Pet object",
|
|
524
|
-
"required": True,
|
|
525
|
-
"content": {
|
|
526
|
-
"application/json": {
|
|
527
|
-
"schema": {"$ref": "#/components/schemas/Pet"}
|
|
528
|
-
}
|
|
529
|
-
},
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
},
|
|
533
|
-
}
|
|
917
|
+
extracted_responses[status_code] = response_info
|
|
534
918
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
) # exclude_none is often cleaner
|
|
545
|
-
except ValueError as e:
|
|
546
|
-
print(f"\nError parsing schema: {e}")
|
|
547
|
-
except Exception as e:
|
|
548
|
-
print(f"\nAn unexpected error occurred: {e}")
|
|
919
|
+
except (ValidationError, ValueError, AttributeError) as e:
|
|
920
|
+
ref_str = getattr(response_or_ref, "ref", "unknown")
|
|
921
|
+
logger.error(
|
|
922
|
+
f"Failed to extract response info for status code {status_code} "
|
|
923
|
+
f"from reference '{ref_str}': {e}",
|
|
924
|
+
exc_info=False,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
return extracted_responses
|
|
549
928
|
|
|
550
929
|
|
|
551
930
|
def clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None:
|