universal-mcp 0.1.20rc2__py3-none-any.whl → 0.1.22__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.
- universal_mcp/applications/__init__.py +5 -1
- universal_mcp/integrations/integration.py +4 -8
- universal_mcp/servers/server.py +16 -31
- universal_mcp/tools/adapters.py +39 -3
- universal_mcp/tools/func_metadata.py +4 -2
- universal_mcp/tools/manager.py +122 -37
- universal_mcp/tools/tools.py +1 -1
- universal_mcp/utils/agentr.py +27 -13
- universal_mcp/utils/docstring_parser.py +18 -64
- universal_mcp/utils/openapi/api_splitter.py +250 -132
- universal_mcp/utils/openapi/openapi.py +224 -99
- universal_mcp/utils/openapi/preprocessor.py +272 -29
- universal_mcp/utils/testing.py +31 -0
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22.dist-info}/METADATA +2 -1
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22.dist-info}/RECORD +18 -17
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.20rc2.dist-info → universal_mcp-0.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -19,6 +19,7 @@ class Parameters(BaseModel):
|
|
19
19
|
required: bool
|
20
20
|
example: str | None = None
|
21
21
|
is_file: bool = False
|
22
|
+
schema: dict = {}
|
22
23
|
|
23
24
|
def __str__(self):
|
24
25
|
return f"{self.name}: ({self.type})"
|
@@ -79,6 +80,60 @@ def convert_to_snake_case(identifier: str) -> str:
|
|
79
80
|
return result.strip("_").lower()
|
80
81
|
|
81
82
|
|
83
|
+
# Added new recursive type mapper
|
84
|
+
def _openapi_type_to_python_type(schema: dict, required: bool = True) -> str:
|
85
|
+
"""
|
86
|
+
Recursively map OpenAPI schema to Python type hints.
|
87
|
+
"""
|
88
|
+
openapi_type = schema.get("type")
|
89
|
+
|
90
|
+
if "$ref" in schema and not openapi_type:
|
91
|
+
py_type = "dict[str, Any]"
|
92
|
+
elif openapi_type == "array":
|
93
|
+
items_schema = schema.get("items", {})
|
94
|
+
item_type = _openapi_type_to_python_type(items_schema, required=True)
|
95
|
+
py_type = f"List[{item_type}]"
|
96
|
+
elif openapi_type == "object":
|
97
|
+
if schema.get("format") in ["binary", "byte"]:
|
98
|
+
py_type = "bytes"
|
99
|
+
else:
|
100
|
+
if "additionalProperties" in schema and isinstance(schema["additionalProperties"], dict):
|
101
|
+
additional_props_schema = schema["additionalProperties"]
|
102
|
+
|
103
|
+
value_type = _openapi_type_to_python_type(additional_props_schema, required=True)
|
104
|
+
py_type = f"dict[str, {value_type}]"
|
105
|
+
elif (
|
106
|
+
not schema.get("properties")
|
107
|
+
and not schema.get("allOf")
|
108
|
+
and not schema.get("oneOf")
|
109
|
+
and not schema.get("anyOf")
|
110
|
+
):
|
111
|
+
py_type = "dict[str, Any]"
|
112
|
+
else:
|
113
|
+
py_type = "dict[str, Any]"
|
114
|
+
elif openapi_type == "integer":
|
115
|
+
py_type = "int"
|
116
|
+
elif openapi_type == "number":
|
117
|
+
py_type = "float"
|
118
|
+
elif openapi_type == "boolean":
|
119
|
+
py_type = "bool"
|
120
|
+
elif openapi_type == "string":
|
121
|
+
if schema.get("format") in ["binary", "byte"]:
|
122
|
+
py_type = "bytes"
|
123
|
+
elif schema.get("format") == "date" or schema.get("format") == "date-time":
|
124
|
+
py_type = "str"
|
125
|
+
else:
|
126
|
+
py_type = "str"
|
127
|
+
else:
|
128
|
+
py_type = "Any"
|
129
|
+
|
130
|
+
if not required:
|
131
|
+
if py_type.startswith("Optional[") and py_type.endswith("]"):
|
132
|
+
return py_type
|
133
|
+
return f"Optional[{py_type}]"
|
134
|
+
return py_type
|
135
|
+
|
136
|
+
|
82
137
|
def _sanitize_identifier(name: str | None) -> str:
|
83
138
|
"""Cleans a string to be a valid Python identifier.
|
84
139
|
|
@@ -94,7 +149,9 @@ def _sanitize_identifier(name: str | None) -> str:
|
|
94
149
|
return ""
|
95
150
|
|
96
151
|
# Initial replacements for common non-alphanumeric characters
|
97
|
-
sanitized =
|
152
|
+
sanitized = (
|
153
|
+
name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_").replace("/", "_")
|
154
|
+
)
|
98
155
|
|
99
156
|
# Remove leading underscores, but preserve a single underscore if the name (after initial replace)
|
100
157
|
# consisted only of underscores.
|
@@ -200,7 +257,7 @@ def _determine_function_name(operation: dict[str, Any], path: str, method: str)
|
|
200
257
|
if "operationId" in operation:
|
201
258
|
raw_name = operation["operationId"]
|
202
259
|
cleaned_name = raw_name.replace(".", "_").replace("-", "_")
|
203
|
-
cleaned_name_no_numbers = re.sub(r
|
260
|
+
cleaned_name_no_numbers = re.sub(r"\d+", "", cleaned_name)
|
204
261
|
func_name = convert_to_snake_case(cleaned_name_no_numbers)
|
205
262
|
else:
|
206
263
|
# Generate name from path and method
|
@@ -235,6 +292,7 @@ def _generate_path_params(path: str) -> list[Parameters]:
|
|
235
292
|
where="path",
|
236
293
|
required=True,
|
237
294
|
example=None,
|
295
|
+
schema={"type": "string"},
|
238
296
|
)
|
239
297
|
)
|
240
298
|
except Exception as e:
|
@@ -283,6 +341,7 @@ def _generate_query_params(operation: dict[str, Any]) -> list[Parameters]:
|
|
283
341
|
where=where,
|
284
342
|
required=required,
|
285
343
|
example=str(example_value) if example_value is not None else None,
|
344
|
+
schema=param_schema if param_schema else {"type": type_value},
|
286
345
|
)
|
287
346
|
query_params.append(parameter)
|
288
347
|
return query_params
|
@@ -314,11 +373,12 @@ def _generate_body_params(schema_to_process: dict[str, Any] | None, overall_body
|
|
314
373
|
name=_sanitize_identifier(param_name),
|
315
374
|
identifier=param_name,
|
316
375
|
description=param_description,
|
317
|
-
type=effective_param_type,
|
376
|
+
type=effective_param_type,
|
318
377
|
where="body",
|
319
378
|
required=param_required,
|
320
379
|
example=str(param_example) if param_example is not None else None,
|
321
|
-
is_file=current_is_file
|
380
|
+
is_file=current_is_file,
|
381
|
+
schema=param_schema_details,
|
322
382
|
)
|
323
383
|
)
|
324
384
|
# print(f"[DEBUG] Final body_params list generated: {body_params}") # DEBUG
|
@@ -344,10 +404,10 @@ def _generate_method_code(path, method, operation):
|
|
344
404
|
|
345
405
|
# --- Determine Function Name and Basic Operation Details ---
|
346
406
|
func_name = _determine_function_name(operation, path, method)
|
347
|
-
method_lower = method.lower()
|
348
|
-
operation.get("summary", "")
|
349
|
-
operation.get("tags", [])
|
350
|
-
|
407
|
+
method_lower = method.lower() # Define method_lower earlier
|
408
|
+
operation.get("summary", "") # Ensure summary is accessed if needed elsewhere
|
409
|
+
operation.get("tags", []) # Ensure tags are accessed if needed elsewhere
|
410
|
+
|
351
411
|
# --- Generate Path and Query Parameters (pre-aliasing) ---
|
352
412
|
path_params = _generate_path_params(path)
|
353
413
|
query_params = _generate_query_params(operation)
|
@@ -356,7 +416,7 @@ def _generate_method_code(path, method, operation):
|
|
356
416
|
# This section selects the primary content type and its schema to be used for the request body.
|
357
417
|
has_body = "requestBody" in operation
|
358
418
|
body_schema_to_use = None
|
359
|
-
selected_content_type = None
|
419
|
+
selected_content_type = None # This will hold the chosen content type string
|
360
420
|
|
361
421
|
if has_body:
|
362
422
|
request_body_spec = operation["requestBody"]
|
@@ -369,7 +429,7 @@ def _generate_method_code(path, method, operation):
|
|
369
429
|
"application/octet-stream",
|
370
430
|
"text/plain",
|
371
431
|
]
|
372
|
-
|
432
|
+
|
373
433
|
found_preferred = False
|
374
434
|
for ct in preferred_content_types:
|
375
435
|
if ct in request_body_content_map:
|
@@ -377,25 +437,25 @@ def _generate_method_code(path, method, operation):
|
|
377
437
|
body_schema_to_use = request_body_content_map[ct].get("schema")
|
378
438
|
found_preferred = True
|
379
439
|
break
|
380
|
-
|
381
|
-
if not found_preferred:
|
440
|
+
|
441
|
+
if not found_preferred: # Check for image/* if no direct match yet
|
382
442
|
for ct_key in request_body_content_map:
|
383
443
|
if ct_key.startswith("image/"):
|
384
444
|
selected_content_type = ct_key
|
385
445
|
body_schema_to_use = request_body_content_map[ct_key].get("schema")
|
386
446
|
found_preferred = True
|
387
447
|
break
|
388
|
-
|
389
|
-
if not found_preferred and request_body_content_map:
|
448
|
+
|
449
|
+
if not found_preferred and request_body_content_map: # Fallback to first listed
|
390
450
|
first_ct_key = next(iter(request_body_content_map))
|
391
451
|
selected_content_type = first_ct_key
|
392
452
|
body_schema_to_use = request_body_content_map[first_ct_key].get("schema")
|
393
453
|
|
394
454
|
# --- Generate Body Parameters (based on selected schema, pre-aliasing) ---
|
395
|
-
if body_schema_to_use:
|
455
|
+
if body_schema_to_use: # If a schema was actually found for the selected content type
|
396
456
|
body_params = _generate_body_params(
|
397
|
-
body_schema_to_use,
|
398
|
-
operation.get("requestBody", {}).get("required", False)
|
457
|
+
body_schema_to_use, # Pass the specific schema
|
458
|
+
operation.get("requestBody", {}).get("required", False), # Pass the overall body requirement
|
399
459
|
)
|
400
460
|
else:
|
401
461
|
body_params = []
|
@@ -467,29 +527,36 @@ def _generate_method_code(path, method, operation):
|
|
467
527
|
current_body_param_names.add(b_param.name)
|
468
528
|
# --- End Alias duplicate parameter names ---
|
469
529
|
|
470
|
-
|
471
530
|
# --- Determine Return Type and Body Characteristics ---
|
472
531
|
return_type = _determine_return_type(operation)
|
473
532
|
|
474
|
-
body_required = has_body and operation["requestBody"].get("required", False)
|
475
|
-
|
533
|
+
body_required = has_body and operation["requestBody"].get("required", False) # Remains useful
|
534
|
+
|
476
535
|
is_array_body = False
|
477
|
-
has_empty_body = False
|
536
|
+
has_empty_body = False
|
478
537
|
|
479
|
-
if has_body and body_schema_to_use:
|
538
|
+
if has_body and body_schema_to_use: # Use the determined body_schema_to_use
|
480
539
|
if body_schema_to_use.get("type") == "array":
|
481
540
|
is_array_body = True
|
482
|
-
|
541
|
+
|
483
542
|
# Check for cases that might lead to an "empty" body parameter (for JSON) in the signature,
|
484
543
|
# or indicate a raw body type where _generate_body_params wouldn't create named params.
|
485
|
-
if
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
544
|
+
if (
|
545
|
+
not body_params
|
546
|
+
and not is_array_body
|
547
|
+
and selected_content_type == "application/json"
|
548
|
+
and (
|
549
|
+
body_schema_to_use == {}
|
550
|
+
or (
|
551
|
+
body_schema_to_use.get("type") == "object"
|
552
|
+
and not body_schema_to_use.get("properties")
|
553
|
+
and not body_schema_to_use.get("allOf")
|
554
|
+
and not body_schema_to_use.get("oneOf")
|
555
|
+
and not body_schema_to_use.get("anyOf")
|
556
|
+
)
|
557
|
+
)
|
558
|
+
):
|
559
|
+
has_empty_body = True # Indicates a generic 'request_body: dict = None' might be needed for empty JSON
|
493
560
|
|
494
561
|
# --- Build Function Arguments for Signature ---
|
495
562
|
# This section constructs the list of arguments (required and optional)
|
@@ -497,27 +564,40 @@ def _generate_method_code(path, method, operation):
|
|
497
564
|
required_args = []
|
498
565
|
optional_args = []
|
499
566
|
|
567
|
+
# Arguments for the function signature with type hints
|
568
|
+
signature_required_args_typed = []
|
569
|
+
signature_optional_args_typed = []
|
570
|
+
|
500
571
|
# Process Path Parameters (Highest Priority)
|
501
572
|
for param in path_params:
|
502
573
|
# Path param names are sanitized but not suffixed by aliasing.
|
503
574
|
if param.name not in required_args: # param.name is the sanitized name
|
504
575
|
required_args.append(param.name)
|
576
|
+
# For signature with types
|
577
|
+
param_py_type = _openapi_type_to_python_type(param.schema, required=True)
|
578
|
+
signature_required_args_typed.append(f"{param.name}: {param_py_type}")
|
505
579
|
|
506
580
|
# Process Query Parameters
|
507
581
|
for param in query_params: # param.name is the potentially aliased name (e.g., id_query)
|
508
582
|
arg_name_for_sig = param.name
|
509
583
|
current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
584
|
+
|
585
|
+
# For signature with types
|
586
|
+
param_py_type = _openapi_type_to_python_type(param.schema, required=param.required)
|
587
|
+
|
510
588
|
if arg_name_for_sig not in current_arg_names_set:
|
511
589
|
if param.required:
|
512
590
|
required_args.append(arg_name_for_sig)
|
591
|
+
signature_required_args_typed.append(f"{arg_name_for_sig}: {param_py_type}")
|
513
592
|
else:
|
514
593
|
optional_args.append(f"{arg_name_for_sig}=None")
|
594
|
+
signature_optional_args_typed.append(f"{arg_name_for_sig}: {param_py_type} = None")
|
515
595
|
|
516
596
|
# Process Body Parameters / Request Body
|
517
597
|
# This list tracks the *final* names of parameters in the signature that come from the request body,
|
518
598
|
final_request_body_arg_names_for_signature = []
|
519
|
-
final_empty_body_param_name = None
|
520
|
-
raw_body_param_name = None
|
599
|
+
final_empty_body_param_name = None # For the specific case of has_empty_body (empty JSON object)
|
600
|
+
raw_body_param_name = None # For raw content like octet-stream, text/plain, image/*
|
521
601
|
|
522
602
|
if has_body:
|
523
603
|
current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
@@ -539,18 +619,30 @@ def _generate_method_code(path, method, operation):
|
|
539
619
|
final_array_param_name = f"{array_param_name_base}_body_{counter}"
|
540
620
|
counter += 1
|
541
621
|
|
622
|
+
# For signature with types
|
623
|
+
# The schema for an array body is body_schema_to_use itself
|
624
|
+
array_body_py_type = _openapi_type_to_python_type(body_schema_to_use, required=body_required)
|
625
|
+
|
542
626
|
if body_required:
|
543
627
|
required_args.append(final_array_param_name)
|
628
|
+
signature_required_args_typed.append(f"{final_array_param_name}: {array_body_py_type}")
|
544
629
|
else:
|
545
630
|
optional_args.append(f"{final_array_param_name}=None")
|
631
|
+
signature_optional_args_typed.append(f"{final_array_param_name}: {array_body_py_type} = None")
|
546
632
|
final_request_body_arg_names_for_signature.append(final_array_param_name)
|
547
633
|
|
548
634
|
# New: Handle raw body parameter (if body_params is empty but body is expected and not array/empty JSON)
|
549
|
-
elif
|
635
|
+
elif (
|
636
|
+
not body_params
|
637
|
+
and not is_array_body
|
638
|
+
and selected_content_type
|
639
|
+
and selected_content_type
|
640
|
+
not in ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"]
|
641
|
+
):
|
550
642
|
# This branch is for raw content types like application/octet-stream, text/plain, image/*
|
551
643
|
# where _generate_body_params returned an empty list because the schema isn't an object with properties.
|
552
644
|
raw_body_param_name_base = "body_content"
|
553
|
-
|
645
|
+
|
554
646
|
temp_raw_body_name = raw_body_param_name_base
|
555
647
|
counter = 1
|
556
648
|
is_first_suffix_attempt = True
|
@@ -563,31 +655,54 @@ def _generate_method_code(path, method, operation):
|
|
563
655
|
counter += 1
|
564
656
|
raw_body_param_name = temp_raw_body_name
|
565
657
|
|
566
|
-
|
658
|
+
# For signature with types
|
659
|
+
# Determine type based on selected_content_type for raw body
|
660
|
+
raw_body_schema_for_type = {"type": "string", "format": "binary"} # Default to bytes
|
661
|
+
if selected_content_type and "text" in selected_content_type:
|
662
|
+
raw_body_schema_for_type = {"type": "string"}
|
663
|
+
elif selected_content_type and selected_content_type.startswith("image/"):
|
664
|
+
raw_body_schema_for_type = {"type": "string", "format": "binary"} # image is bytes
|
665
|
+
|
666
|
+
raw_body_py_type = _openapi_type_to_python_type(raw_body_schema_for_type, required=body_required)
|
667
|
+
|
668
|
+
if body_required: # If the raw body itself is required
|
567
669
|
required_args.append(raw_body_param_name)
|
670
|
+
signature_required_args_typed.append(f"{raw_body_param_name}: {raw_body_py_type}")
|
568
671
|
else:
|
569
672
|
optional_args.append(f"{raw_body_param_name}=None")
|
673
|
+
signature_optional_args_typed.append(f"{raw_body_param_name}: {raw_body_py_type} = None")
|
570
674
|
final_request_body_arg_names_for_signature.append(raw_body_param_name)
|
571
675
|
|
572
|
-
elif body_params:
|
676
|
+
elif body_params: # Object body with discernible properties
|
573
677
|
for param in body_params: # Iterate ALIASED body_params
|
574
|
-
arg_name_for_sig = param.name #final aliased name (e.g., "id_body")
|
678
|
+
arg_name_for_sig = param.name # final aliased name (e.g., "id_body")
|
575
679
|
|
576
|
-
# Defensive check against already added args
|
680
|
+
# Defensive check against already added args
|
577
681
|
current_arg_names_set_loop = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
682
|
+
|
683
|
+
# For signature with types
|
684
|
+
param_py_type = _openapi_type_to_python_type(param.schema, required=param.required)
|
685
|
+
|
578
686
|
if arg_name_for_sig not in current_arg_names_set_loop:
|
579
687
|
if param.required:
|
580
688
|
required_args.append(arg_name_for_sig)
|
689
|
+
signature_required_args_typed.append(f"{arg_name_for_sig}: {param_py_type}")
|
581
690
|
else:
|
582
691
|
# Parameters model does not store schema 'default'. Optional params default to None.
|
583
692
|
optional_args.append(f"{arg_name_for_sig}=None")
|
693
|
+
signature_optional_args_typed.append(f"{arg_name_for_sig}: {param_py_type} = None")
|
584
694
|
final_request_body_arg_names_for_signature.append(arg_name_for_sig)
|
585
695
|
|
696
|
+
if (
|
697
|
+
has_empty_body
|
698
|
+
and selected_content_type == "application/json"
|
699
|
+
and not body_params
|
700
|
+
and not is_array_body
|
701
|
+
and not raw_body_param_name
|
702
|
+
):
|
703
|
+
empty_body_param_name_base = "request_body" # For empty JSON object
|
704
|
+
current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
586
705
|
|
587
|
-
if has_empty_body and selected_content_type == "application/json" and not body_params and not is_array_body and not raw_body_param_name:
|
588
|
-
empty_body_param_name_base = "request_body" # For empty JSON object
|
589
|
-
current_arg_names_set = set(required_args) | {arg.split('=')[0] for arg in optional_args}
|
590
|
-
|
591
706
|
final_empty_body_param_name = empty_body_param_name_base
|
592
707
|
counter = 1
|
593
708
|
is_first_suffix_attempt = True
|
@@ -599,25 +714,34 @@ def _generate_method_code(path, method, operation):
|
|
599
714
|
final_empty_body_param_name = f"{empty_body_param_name_base}_body_{counter}"
|
600
715
|
counter += 1
|
601
716
|
|
602
|
-
#
|
717
|
+
# For signature with types
|
718
|
+
# Empty body usually implies an empty JSON object, so dict or Optional[dict]
|
719
|
+
empty_body_py_type = _openapi_type_to_python_type({"type": "object"}, required=False)
|
603
720
|
|
604
721
|
if final_empty_body_param_name not in (set(required_args) | {arg.split("=")[0] for arg in optional_args}):
|
605
722
|
optional_args.append(f"{final_empty_body_param_name}=None")
|
723
|
+
# Add to typed signature list
|
724
|
+
signature_optional_args_typed.append(f"{final_empty_body_param_name}: {empty_body_py_type} = None")
|
725
|
+
|
606
726
|
# Track for docstring, even if it's just 'request_body' or 'request_body_body'
|
607
727
|
if final_empty_body_param_name not in final_request_body_arg_names_for_signature:
|
608
728
|
final_request_body_arg_names_for_signature.append(final_empty_body_param_name)
|
609
729
|
|
610
|
-
# Combine required and optional arguments
|
730
|
+
# Combine required and optional arguments FOR DOCSTRING (as before, without types)
|
611
731
|
args = required_args + optional_args
|
612
|
-
print(f"[DEBUG] Final combined args for
|
732
|
+
print(f"[DEBUG] Final combined args for DOCSTRING: {args}") # DEBUG
|
613
733
|
|
614
|
-
#
|
734
|
+
# Combine required and optional arguments FOR SIGNATURE (with types)
|
735
|
+
signature_args_combined_typed = signature_required_args_typed + signature_optional_args_typed
|
736
|
+
print(f"[DEBUG] Final combined args for SIGNATURE: {signature_args_combined_typed}") # DEBUG
|
737
|
+
|
738
|
+
# ----- Build Docstring -----
|
615
739
|
# This section constructs the entire docstring for the generated method,
|
616
740
|
# including summary, argument descriptions, return type, and tags.
|
617
741
|
docstring_parts = []
|
618
742
|
# NEW: Add OpenAPI path as the first line of the docstring
|
619
|
-
openapi_path_comment_for_docstring = f"# openapi_path: {path}"
|
620
|
-
docstring_parts.append(openapi_path_comment_for_docstring)
|
743
|
+
# openapi_path_comment_for_docstring = f"# openapi_path: {path}"
|
744
|
+
# docstring_parts.append(openapi_path_comment_for_docstring)
|
621
745
|
|
622
746
|
return_type = _determine_return_type(operation)
|
623
747
|
|
@@ -632,7 +756,7 @@ def _generate_method_code(path, method, operation):
|
|
632
756
|
# Args
|
633
757
|
args_doc_lines = []
|
634
758
|
param_details = {}
|
635
|
-
|
759
|
+
|
636
760
|
# Create a combined list of all parameter objects (path, query, body) to fetch details for docstring
|
637
761
|
all_parameter_objects_for_docstring = path_params + query_params + body_params
|
638
762
|
|
@@ -643,18 +767,18 @@ def _generate_method_code(path, method, operation):
|
|
643
767
|
param_details[param_obj.name] = param_obj
|
644
768
|
|
645
769
|
# Fetch request body example
|
646
|
-
example_data = None
|
770
|
+
example_data = None # Initialize example_data here for wider scope
|
647
771
|
|
648
772
|
if has_body:
|
649
773
|
try:
|
650
774
|
json_content = operation["requestBody"]["content"]["application/json"]
|
651
|
-
#From direct content definition
|
775
|
+
# From direct content definition
|
652
776
|
if "example" in json_content:
|
653
777
|
example_data = json_content["example"]
|
654
778
|
elif "examples" in json_content and json_content["examples"]:
|
655
779
|
first_example_key = list(json_content["examples"].keys())[0]
|
656
780
|
example_data = json_content["examples"][first_example_key].get("value")
|
657
|
-
#If not found directly, try from resolved body schema (for nested/referenced examples)
|
781
|
+
# If not found directly, try from resolved body schema (for nested/referenced examples)
|
658
782
|
if example_data is None and body_schema_to_use and "example" in body_schema_to_use:
|
659
783
|
example_data = body_schema_to_use["example"]
|
660
784
|
except KeyError:
|
@@ -672,18 +796,24 @@ def _generate_method_code(path, method, operation):
|
|
672
796
|
# Adjust type_hint for file parameters for the docstring
|
673
797
|
if detail.is_file:
|
674
798
|
type_hint = "file (e.g., open('path/to/file', 'rb'))"
|
675
|
-
|
799
|
+
|
676
800
|
arg_line = f" {arg_name} ({type_hint}): {desc}"
|
677
|
-
if detail.example and not detail.is_file:
|
801
|
+
if detail.example and not detail.is_file: # Don't show schema example for file inputs
|
678
802
|
example_str = repr(detail.example)
|
679
803
|
arg_line += f" Example: {example_str}."
|
680
804
|
# Fallback for body parameters if no direct example was found
|
681
|
-
elif
|
805
|
+
elif (
|
806
|
+
not example_str
|
807
|
+
and detail.where == "body"
|
808
|
+
and example_data
|
809
|
+
and isinstance(example_data, dict)
|
810
|
+
and detail.identifier in example_data
|
811
|
+
):
|
682
812
|
current_body_param_example = example_data[detail.identifier]
|
683
|
-
if current_body_param_example is not None:
|
813
|
+
if current_body_param_example is not None: # Ensure the extracted part is not None
|
684
814
|
try:
|
685
815
|
arg_line += f" Example: {repr(current_body_param_example)}."
|
686
|
-
except Exception:
|
816
|
+
except Exception: # Fallback if repr fails
|
687
817
|
arg_line += " Example: [Could not represent example]."
|
688
818
|
|
689
819
|
args_doc_lines.append(arg_line)
|
@@ -692,19 +822,17 @@ def _generate_method_code(path, method, operation):
|
|
692
822
|
args_doc_lines.append(
|
693
823
|
f" {arg_name} (dict | None): Optional dictionary for an empty JSON request body (e.g., {{}})."
|
694
824
|
)
|
695
|
-
elif arg_name == raw_body_param_name:
|
825
|
+
elif arg_name == raw_body_param_name:
|
696
826
|
raw_body_type_hint = "bytes"
|
697
827
|
raw_body_desc = "Raw binary content for the request body."
|
698
828
|
if selected_content_type and "text" in selected_content_type:
|
699
829
|
raw_body_type_hint = "str"
|
700
830
|
raw_body_desc = "Raw text content for the request body."
|
701
831
|
elif selected_content_type and selected_content_type.startswith("image/"):
|
702
|
-
|
703
|
-
|
704
|
-
args_doc_lines.append(
|
705
|
-
|
706
|
-
)
|
707
|
-
|
832
|
+
raw_body_type_hint = "bytes (image data)"
|
833
|
+
raw_body_desc = f"Raw image content ({selected_content_type}) for the request body."
|
834
|
+
args_doc_lines.append(f" {arg_name} ({raw_body_type_hint} | None): {raw_body_desc}")
|
835
|
+
|
708
836
|
if args_doc_lines:
|
709
837
|
docstring_parts.append("\n".join(args_doc_lines))
|
710
838
|
|
@@ -720,7 +848,7 @@ def _generate_method_code(path, method, operation):
|
|
720
848
|
raises_section_lines = [
|
721
849
|
"Raises:",
|
722
850
|
" HTTPError: Raised when the API request fails (e.g., non-2XX status code).",
|
723
|
-
" JSONDecodeError: Raised if the response body cannot be parsed as JSON."
|
851
|
+
" JSONDecodeError: Raised if the response body cannot be parsed as JSON.",
|
724
852
|
]
|
725
853
|
docstring_parts.append("\n".join(raises_section_lines))
|
726
854
|
|
@@ -742,12 +870,13 @@ def _generate_method_code(path, method, operation):
|
|
742
870
|
# ----- End Build Docstring -----
|
743
871
|
|
744
872
|
# --- Construct Method Signature String ---
|
745
|
-
|
746
|
-
|
873
|
+
# Use signature_args_combined_typed for the signature
|
874
|
+
if signature_args_combined_typed:
|
875
|
+
signature = f" def {func_name}(self, {', '.join(signature_args_combined_typed)}) -> {return_type}:"
|
747
876
|
else:
|
748
877
|
signature = f" def {func_name}(self) -> {return_type}:"
|
749
878
|
|
750
|
-
# --- Build Method Body ---
|
879
|
+
# --- Build Method Body ---
|
751
880
|
# This section constructs the executable lines of code within the generated method.
|
752
881
|
body_lines = []
|
753
882
|
|
@@ -755,10 +884,9 @@ def _generate_method_code(path, method, operation):
|
|
755
884
|
for param in path_params:
|
756
885
|
body_lines.append(f" if {param.name} is None:")
|
757
886
|
body_lines.append(
|
758
|
-
f
|
887
|
+
f" raise ValueError(\"Missing required parameter '{param.identifier}'.\")" # Use original name in error, ensure quotes are balanced
|
759
888
|
)
|
760
889
|
|
761
|
-
|
762
890
|
if method_lower not in ["get", "delete"]:
|
763
891
|
body_lines.append(" request_body_data = None")
|
764
892
|
|
@@ -768,7 +896,6 @@ def _generate_method_code(path, method, operation):
|
|
768
896
|
if method_lower in ["post", "put"] and selected_content_type == "multipart/form-data":
|
769
897
|
body_lines.append(" files_data = None")
|
770
898
|
|
771
|
-
|
772
899
|
# --- Build Request Payload (request_body_data and files_data) ---
|
773
900
|
# This section prepares the data to be sent in the request body,
|
774
901
|
# differentiating between files and other data for multipart forms,
|
@@ -777,27 +904,30 @@ def _generate_method_code(path, method, operation):
|
|
777
904
|
# This block will now overwrite the initial None values if a body is present.
|
778
905
|
if is_array_body:
|
779
906
|
# For array request bodies, use the array parameter directly
|
780
|
-
array_arg_name =
|
907
|
+
array_arg_name = (
|
908
|
+
final_request_body_arg_names_for_signature[0]
|
909
|
+
if final_request_body_arg_names_for_signature
|
910
|
+
else "items_body"
|
911
|
+
) # Fallback
|
781
912
|
body_lines.append(f" # Using array parameter '{array_arg_name}' directly as request body")
|
782
|
-
body_lines.append(f" request_body_data = {array_arg_name}")
|
913
|
+
body_lines.append(f" request_body_data = {array_arg_name}") # Use a neutral temp name
|
783
914
|
# files_data remains None
|
784
915
|
|
785
916
|
elif selected_content_type == "multipart/form-data":
|
786
|
-
body_lines.append(" request_body_data = {}")
|
787
|
-
body_lines.append(" files_data = {}")
|
788
|
-
for b_param in body_params:
|
917
|
+
body_lines.append(" request_body_data = {}") # For non-file form fields
|
918
|
+
body_lines.append(" files_data = {}") # For file fields
|
919
|
+
for b_param in body_params: # Iterate through ALIASED body_params
|
789
920
|
if b_param.is_file:
|
790
|
-
body_lines.append(f" if {b_param.name} is not None:")
|
921
|
+
body_lines.append(f" if {b_param.name} is not None:") # Check if file param is provided
|
791
922
|
body_lines.append(f" files_data['{b_param.identifier}'] = {b_param.name}")
|
792
923
|
else:
|
793
|
-
body_lines.append(f" if {b_param.name} is not None:")
|
924
|
+
body_lines.append(f" if {b_param.name} is not None:") # Check if form field is provided
|
794
925
|
body_lines.append(f" request_body_data['{b_param.identifier}'] = {b_param.name}")
|
795
926
|
body_lines.append(" files_data = {k: v for k, v in files_data.items() if v is not None}")
|
796
927
|
# Ensure files_data is None if it's empty after filtering, as httpx expects None, not {}
|
797
928
|
body_lines.append(" if not files_data: files_data = None")
|
798
929
|
|
799
|
-
|
800
|
-
elif body_params: # Object request bodies (JSON, x-www-form-urlencoded) with specific parameters
|
930
|
+
elif body_params: # Object request bodies (JSON, x-www-form-urlencoded) with specific parameters
|
801
931
|
body_lines.append(" request_body_data = {")
|
802
932
|
for b_param in body_params:
|
803
933
|
body_lines.append(f" '{b_param.identifier}': {b_param.name},")
|
@@ -805,13 +935,14 @@ def _generate_method_code(path, method, operation):
|
|
805
935
|
body_lines.append(
|
806
936
|
" request_body_data = {k: v for k, v in request_body_data.items() if v is not None}"
|
807
937
|
)
|
808
|
-
|
809
|
-
elif raw_body_param_name:
|
938
|
+
|
939
|
+
elif raw_body_param_name: # Raw content type (octet-stream, text, image)
|
810
940
|
body_lines.append(f" request_body_data = {raw_body_param_name}")
|
811
941
|
|
812
|
-
elif has_empty_body and selected_content_type == "application/json":
|
813
|
-
body_lines.append(
|
814
|
-
|
942
|
+
elif has_empty_body and selected_content_type == "application/json": # Empty JSON object {}
|
943
|
+
body_lines.append(
|
944
|
+
f" request_body_data = {final_empty_body_param_name} if {final_empty_body_param_name} is not None else {{}}"
|
945
|
+
)
|
815
946
|
|
816
947
|
# --- Format URL and Query Parameters for Request ---
|
817
948
|
url = _generate_url(path, path_params)
|
@@ -831,21 +962,20 @@ def _generate_method_code(path, method, operation):
|
|
831
962
|
|
832
963
|
# --- Determine Final Content-Type for API Call (Obsolete Block, selected_content_type is used) ---
|
833
964
|
# The following block for request_body_content_type is largely superseded by selected_content_type,
|
834
|
-
|
965
|
+
|
835
966
|
# Use the selected_content_type determined by the new logic as the primary source of truth.
|
836
967
|
final_content_type_for_api_call = selected_content_type if selected_content_type else "application/json"
|
837
968
|
|
838
969
|
# --- Make HTTP Request ---
|
839
|
-
# This section generates the actual HTTP call
|
970
|
+
# This section generates the actual HTTP call
|
840
971
|
# using the prepared URL, query parameters, request body data, files, and content type.
|
841
972
|
|
842
|
-
|
843
973
|
if method_lower == "get":
|
844
974
|
body_lines.append(" response = self._get(url, params=query_params)")
|
845
975
|
elif method_lower == "post":
|
846
976
|
if selected_content_type == "multipart/form-data":
|
847
977
|
body_lines.append(
|
848
|
-
|
978
|
+
f" response = self._post(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
849
979
|
)
|
850
980
|
else:
|
851
981
|
body_lines.append(
|
@@ -854,23 +984,18 @@ def _generate_method_code(path, method, operation):
|
|
854
984
|
elif method_lower == "put":
|
855
985
|
if selected_content_type == "multipart/form-data":
|
856
986
|
body_lines.append(
|
857
|
-
|
987
|
+
f" response = self._put(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
858
988
|
)
|
859
989
|
else:
|
860
990
|
body_lines.append(
|
861
991
|
f" response = self._put(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
862
992
|
)
|
863
993
|
elif method_lower == "patch":
|
864
|
-
|
865
|
-
body_lines.append(
|
866
|
-
" response = self._patch(url, data=request_body_data, params=query_params)"
|
867
|
-
)
|
994
|
+
body_lines.append(" response = self._patch(url, data=request_body_data, params=query_params)")
|
868
995
|
elif method_lower == "delete":
|
869
996
|
body_lines.append(" response = self._delete(url, params=query_params)")
|
870
997
|
else:
|
871
|
-
body_lines.append(
|
872
|
-
f" response = self._{method_lower}(url, data=request_body_data, params=query_params)"
|
873
|
-
)
|
998
|
+
body_lines.append(f" response = self._{method_lower}(url, data=request_body_data, params=query_params)")
|
874
999
|
|
875
1000
|
# --- Handle Response ---
|
876
1001
|
body_lines.append(" response.raise_for_status()")
|
@@ -965,7 +1090,7 @@ def generate_api_client(schema, class_name: str | None = None):
|
|
965
1090
|
|
966
1091
|
# Generate class imports
|
967
1092
|
imports = [
|
968
|
-
"from typing import Any",
|
1093
|
+
"from typing import Any, Optional, List",
|
969
1094
|
"from universal_mcp.applications import APIApplication",
|
970
1095
|
"from universal_mcp.integrations import Integration",
|
971
1096
|
]
|