universal-mcp 0.1.21rc2__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 +15 -31
- universal_mcp/tools/adapters.py +39 -3
- 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 +137 -118
- universal_mcp/utils/openapi/preprocessor.py +272 -29
- universal_mcp/utils/testing.py +31 -0
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22.dist-info}/METADATA +2 -1
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22.dist-info}/RECORD +17 -16
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.21rc2.dist-info → universal_mcp-0.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -87,34 +87,34 @@ def _openapi_type_to_python_type(schema: dict, required: bool = True) -> str:
|
|
87
87
|
"""
|
88
88
|
openapi_type = schema.get("type")
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
py_type = "dict[str, Any]"
|
90
|
+
if "$ref" in schema and not openapi_type:
|
91
|
+
py_type = "dict[str, Any]"
|
93
92
|
elif openapi_type == "array":
|
94
93
|
items_schema = schema.get("items", {})
|
95
|
-
item_type = _openapi_type_to_python_type(items_schema, required=True)
|
94
|
+
item_type = _openapi_type_to_python_type(items_schema, required=True)
|
96
95
|
py_type = f"List[{item_type}]"
|
97
96
|
elif openapi_type == "object":
|
98
|
-
|
99
97
|
if schema.get("format") in ["binary", "byte"]:
|
100
|
-
|
98
|
+
py_type = "bytes"
|
101
99
|
else:
|
102
|
-
|
103
100
|
if "additionalProperties" in schema and isinstance(schema["additionalProperties"], dict):
|
104
101
|
additional_props_schema = schema["additionalProperties"]
|
105
|
-
|
102
|
+
|
106
103
|
value_type = _openapi_type_to_python_type(additional_props_schema, required=True)
|
107
104
|
py_type = f"dict[str, {value_type}]"
|
108
|
-
elif
|
109
|
-
|
110
|
-
|
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]"
|
111
112
|
else:
|
112
|
-
|
113
113
|
py_type = "dict[str, Any]"
|
114
114
|
elif openapi_type == "integer":
|
115
115
|
py_type = "int"
|
116
116
|
elif openapi_type == "number":
|
117
|
-
py_type = "float"
|
117
|
+
py_type = "float"
|
118
118
|
elif openapi_type == "boolean":
|
119
119
|
py_type = "bool"
|
120
120
|
elif openapi_type == "string":
|
@@ -124,9 +124,8 @@ def _openapi_type_to_python_type(schema: dict, required: bool = True) -> str:
|
|
124
124
|
py_type = "str"
|
125
125
|
else:
|
126
126
|
py_type = "str"
|
127
|
-
else:
|
128
|
-
|
129
|
-
py_type = "Any"
|
127
|
+
else:
|
128
|
+
py_type = "Any"
|
130
129
|
|
131
130
|
if not required:
|
132
131
|
if py_type.startswith("Optional[") and py_type.endswith("]"):
|
@@ -150,7 +149,9 @@ def _sanitize_identifier(name: str | None) -> str:
|
|
150
149
|
return ""
|
151
150
|
|
152
151
|
# Initial replacements for common non-alphanumeric characters
|
153
|
-
sanitized =
|
152
|
+
sanitized = (
|
153
|
+
name.replace("-", "_").replace(".", "_").replace("[", "_").replace("]", "").replace("$", "_").replace("/", "_")
|
154
|
+
)
|
154
155
|
|
155
156
|
# Remove leading underscores, but preserve a single underscore if the name (after initial replace)
|
156
157
|
# consisted only of underscores.
|
@@ -256,7 +257,7 @@ def _determine_function_name(operation: dict[str, Any], path: str, method: str)
|
|
256
257
|
if "operationId" in operation:
|
257
258
|
raw_name = operation["operationId"]
|
258
259
|
cleaned_name = raw_name.replace(".", "_").replace("-", "_")
|
259
|
-
cleaned_name_no_numbers = re.sub(r
|
260
|
+
cleaned_name_no_numbers = re.sub(r"\d+", "", cleaned_name)
|
260
261
|
func_name = convert_to_snake_case(cleaned_name_no_numbers)
|
261
262
|
else:
|
262
263
|
# Generate name from path and method
|
@@ -291,7 +292,7 @@ def _generate_path_params(path: str) -> list[Parameters]:
|
|
291
292
|
where="path",
|
292
293
|
required=True,
|
293
294
|
example=None,
|
294
|
-
schema={"type": "string"}
|
295
|
+
schema={"type": "string"},
|
295
296
|
)
|
296
297
|
)
|
297
298
|
except Exception as e:
|
@@ -340,7 +341,7 @@ def _generate_query_params(operation: dict[str, Any]) -> list[Parameters]:
|
|
340
341
|
where=where,
|
341
342
|
required=required,
|
342
343
|
example=str(example_value) if example_value is not None else None,
|
343
|
-
schema=param_schema if param_schema else {"type": type_value}
|
344
|
+
schema=param_schema if param_schema else {"type": type_value},
|
344
345
|
)
|
345
346
|
query_params.append(parameter)
|
346
347
|
return query_params
|
@@ -372,12 +373,12 @@ def _generate_body_params(schema_to_process: dict[str, Any] | None, overall_body
|
|
372
373
|
name=_sanitize_identifier(param_name),
|
373
374
|
identifier=param_name,
|
374
375
|
description=param_description,
|
375
|
-
type=effective_param_type,
|
376
|
+
type=effective_param_type,
|
376
377
|
where="body",
|
377
378
|
required=param_required,
|
378
379
|
example=str(param_example) if param_example is not None else None,
|
379
380
|
is_file=current_is_file,
|
380
|
-
schema=param_schema_details
|
381
|
+
schema=param_schema_details,
|
381
382
|
)
|
382
383
|
)
|
383
384
|
# print(f"[DEBUG] Final body_params list generated: {body_params}") # DEBUG
|
@@ -403,10 +404,10 @@ def _generate_method_code(path, method, operation):
|
|
403
404
|
|
404
405
|
# --- Determine Function Name and Basic Operation Details ---
|
405
406
|
func_name = _determine_function_name(operation, path, method)
|
406
|
-
method_lower = method.lower()
|
407
|
-
operation.get("summary", "")
|
408
|
-
operation.get("tags", [])
|
409
|
-
|
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
|
+
|
410
411
|
# --- Generate Path and Query Parameters (pre-aliasing) ---
|
411
412
|
path_params = _generate_path_params(path)
|
412
413
|
query_params = _generate_query_params(operation)
|
@@ -415,7 +416,7 @@ def _generate_method_code(path, method, operation):
|
|
415
416
|
# This section selects the primary content type and its schema to be used for the request body.
|
416
417
|
has_body = "requestBody" in operation
|
417
418
|
body_schema_to_use = None
|
418
|
-
selected_content_type = None
|
419
|
+
selected_content_type = None # This will hold the chosen content type string
|
419
420
|
|
420
421
|
if has_body:
|
421
422
|
request_body_spec = operation["requestBody"]
|
@@ -428,7 +429,7 @@ def _generate_method_code(path, method, operation):
|
|
428
429
|
"application/octet-stream",
|
429
430
|
"text/plain",
|
430
431
|
]
|
431
|
-
|
432
|
+
|
432
433
|
found_preferred = False
|
433
434
|
for ct in preferred_content_types:
|
434
435
|
if ct in request_body_content_map:
|
@@ -436,25 +437,25 @@ def _generate_method_code(path, method, operation):
|
|
436
437
|
body_schema_to_use = request_body_content_map[ct].get("schema")
|
437
438
|
found_preferred = True
|
438
439
|
break
|
439
|
-
|
440
|
-
if not found_preferred:
|
440
|
+
|
441
|
+
if not found_preferred: # Check for image/* if no direct match yet
|
441
442
|
for ct_key in request_body_content_map:
|
442
443
|
if ct_key.startswith("image/"):
|
443
444
|
selected_content_type = ct_key
|
444
445
|
body_schema_to_use = request_body_content_map[ct_key].get("schema")
|
445
446
|
found_preferred = True
|
446
447
|
break
|
447
|
-
|
448
|
-
if not found_preferred and request_body_content_map:
|
448
|
+
|
449
|
+
if not found_preferred and request_body_content_map: # Fallback to first listed
|
449
450
|
first_ct_key = next(iter(request_body_content_map))
|
450
451
|
selected_content_type = first_ct_key
|
451
452
|
body_schema_to_use = request_body_content_map[first_ct_key].get("schema")
|
452
453
|
|
453
454
|
# --- Generate Body Parameters (based on selected schema, pre-aliasing) ---
|
454
|
-
if body_schema_to_use:
|
455
|
+
if body_schema_to_use: # If a schema was actually found for the selected content type
|
455
456
|
body_params = _generate_body_params(
|
456
|
-
body_schema_to_use,
|
457
|
-
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
|
458
459
|
)
|
459
460
|
else:
|
460
461
|
body_params = []
|
@@ -526,29 +527,36 @@ def _generate_method_code(path, method, operation):
|
|
526
527
|
current_body_param_names.add(b_param.name)
|
527
528
|
# --- End Alias duplicate parameter names ---
|
528
529
|
|
529
|
-
|
530
530
|
# --- Determine Return Type and Body Characteristics ---
|
531
531
|
return_type = _determine_return_type(operation)
|
532
532
|
|
533
|
-
body_required = has_body and operation["requestBody"].get("required", False)
|
534
|
-
|
533
|
+
body_required = has_body and operation["requestBody"].get("required", False) # Remains useful
|
534
|
+
|
535
535
|
is_array_body = False
|
536
|
-
has_empty_body = False
|
536
|
+
has_empty_body = False
|
537
537
|
|
538
|
-
if has_body and body_schema_to_use:
|
538
|
+
if has_body and body_schema_to_use: # Use the determined body_schema_to_use
|
539
539
|
if body_schema_to_use.get("type") == "array":
|
540
540
|
is_array_body = True
|
541
|
-
|
541
|
+
|
542
542
|
# Check for cases that might lead to an "empty" body parameter (for JSON) in the signature,
|
543
543
|
# or indicate a raw body type where _generate_body_params wouldn't create named params.
|
544
|
-
if
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
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
|
552
560
|
|
553
561
|
# --- Build Function Arguments for Signature ---
|
554
562
|
# This section constructs the list of arguments (required and optional)
|
@@ -588,8 +596,8 @@ def _generate_method_code(path, method, operation):
|
|
588
596
|
# Process Body Parameters / Request Body
|
589
597
|
# This list tracks the *final* names of parameters in the signature that come from the request body,
|
590
598
|
final_request_body_arg_names_for_signature = []
|
591
|
-
final_empty_body_param_name = None
|
592
|
-
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/*
|
593
601
|
|
594
602
|
if has_body:
|
595
603
|
current_arg_names_set = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
@@ -624,11 +632,17 @@ def _generate_method_code(path, method, operation):
|
|
624
632
|
final_request_body_arg_names_for_signature.append(final_array_param_name)
|
625
633
|
|
626
634
|
# New: Handle raw body parameter (if body_params is empty but body is expected and not array/empty JSON)
|
627
|
-
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
|
+
):
|
628
642
|
# This branch is for raw content types like application/octet-stream, text/plain, image/*
|
629
643
|
# where _generate_body_params returned an empty list because the schema isn't an object with properties.
|
630
644
|
raw_body_param_name_base = "body_content"
|
631
|
-
|
645
|
+
|
632
646
|
temp_raw_body_name = raw_body_param_name_base
|
633
647
|
counter = 1
|
634
648
|
is_first_suffix_attempt = True
|
@@ -643,15 +657,15 @@ def _generate_method_code(path, method, operation):
|
|
643
657
|
|
644
658
|
# For signature with types
|
645
659
|
# Determine type based on selected_content_type for raw body
|
646
|
-
raw_body_schema_for_type = {"type": "string", "format": "binary"}
|
660
|
+
raw_body_schema_for_type = {"type": "string", "format": "binary"} # Default to bytes
|
647
661
|
if selected_content_type and "text" in selected_content_type:
|
648
662
|
raw_body_schema_for_type = {"type": "string"}
|
649
663
|
elif selected_content_type and selected_content_type.startswith("image/"):
|
650
|
-
raw_body_schema_for_type = {"type": "string", "format": "binary"}
|
651
|
-
|
664
|
+
raw_body_schema_for_type = {"type": "string", "format": "binary"} # image is bytes
|
665
|
+
|
652
666
|
raw_body_py_type = _openapi_type_to_python_type(raw_body_schema_for_type, required=body_required)
|
653
667
|
|
654
|
-
if body_required:
|
668
|
+
if body_required: # If the raw body itself is required
|
655
669
|
required_args.append(raw_body_param_name)
|
656
670
|
signature_required_args_typed.append(f"{raw_body_param_name}: {raw_body_py_type}")
|
657
671
|
else:
|
@@ -659,13 +673,13 @@ def _generate_method_code(path, method, operation):
|
|
659
673
|
signature_optional_args_typed.append(f"{raw_body_param_name}: {raw_body_py_type} = None")
|
660
674
|
final_request_body_arg_names_for_signature.append(raw_body_param_name)
|
661
675
|
|
662
|
-
elif body_params:
|
676
|
+
elif body_params: # Object body with discernible properties
|
663
677
|
for param in body_params: # Iterate ALIASED body_params
|
664
|
-
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")
|
665
679
|
|
666
|
-
# Defensive check against already added args
|
680
|
+
# Defensive check against already added args
|
667
681
|
current_arg_names_set_loop = set(required_args) | {arg.split("=")[0] for arg in optional_args}
|
668
|
-
|
682
|
+
|
669
683
|
# For signature with types
|
670
684
|
param_py_type = _openapi_type_to_python_type(param.schema, required=param.required)
|
671
685
|
|
@@ -679,11 +693,16 @@ def _generate_method_code(path, method, operation):
|
|
679
693
|
signature_optional_args_typed.append(f"{arg_name_for_sig}: {param_py_type} = None")
|
680
694
|
final_request_body_arg_names_for_signature.append(arg_name_for_sig)
|
681
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}
|
682
705
|
|
683
|
-
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:
|
684
|
-
empty_body_param_name_base = "request_body" # For empty JSON object
|
685
|
-
current_arg_names_set = set(required_args) | {arg.split('=')[0] for arg in optional_args}
|
686
|
-
|
687
706
|
final_empty_body_param_name = empty_body_param_name_base
|
688
707
|
counter = 1
|
689
708
|
is_first_suffix_attempt = True
|
@@ -710,19 +729,19 @@ def _generate_method_code(path, method, operation):
|
|
710
729
|
|
711
730
|
# Combine required and optional arguments FOR DOCSTRING (as before, without types)
|
712
731
|
args = required_args + optional_args
|
713
|
-
print(f"[DEBUG] Final combined args for DOCSTRING: {args}")
|
732
|
+
print(f"[DEBUG] Final combined args for DOCSTRING: {args}") # DEBUG
|
714
733
|
|
715
734
|
# Combine required and optional arguments FOR SIGNATURE (with types)
|
716
735
|
signature_args_combined_typed = signature_required_args_typed + signature_optional_args_typed
|
717
|
-
print(f"[DEBUG] Final combined args for SIGNATURE: {signature_args_combined_typed}")
|
736
|
+
print(f"[DEBUG] Final combined args for SIGNATURE: {signature_args_combined_typed}") # DEBUG
|
718
737
|
|
719
|
-
# ----- Build Docstring -----
|
738
|
+
# ----- Build Docstring -----
|
720
739
|
# This section constructs the entire docstring for the generated method,
|
721
740
|
# including summary, argument descriptions, return type, and tags.
|
722
741
|
docstring_parts = []
|
723
742
|
# NEW: Add OpenAPI path as the first line of the docstring
|
724
|
-
openapi_path_comment_for_docstring = f"# openapi_path: {path}"
|
725
|
-
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)
|
726
745
|
|
727
746
|
return_type = _determine_return_type(operation)
|
728
747
|
|
@@ -737,7 +756,7 @@ def _generate_method_code(path, method, operation):
|
|
737
756
|
# Args
|
738
757
|
args_doc_lines = []
|
739
758
|
param_details = {}
|
740
|
-
|
759
|
+
|
741
760
|
# Create a combined list of all parameter objects (path, query, body) to fetch details for docstring
|
742
761
|
all_parameter_objects_for_docstring = path_params + query_params + body_params
|
743
762
|
|
@@ -748,18 +767,18 @@ def _generate_method_code(path, method, operation):
|
|
748
767
|
param_details[param_obj.name] = param_obj
|
749
768
|
|
750
769
|
# Fetch request body example
|
751
|
-
example_data = None
|
770
|
+
example_data = None # Initialize example_data here for wider scope
|
752
771
|
|
753
772
|
if has_body:
|
754
773
|
try:
|
755
774
|
json_content = operation["requestBody"]["content"]["application/json"]
|
756
|
-
#From direct content definition
|
775
|
+
# From direct content definition
|
757
776
|
if "example" in json_content:
|
758
777
|
example_data = json_content["example"]
|
759
778
|
elif "examples" in json_content and json_content["examples"]:
|
760
779
|
first_example_key = list(json_content["examples"].keys())[0]
|
761
780
|
example_data = json_content["examples"][first_example_key].get("value")
|
762
|
-
#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)
|
763
782
|
if example_data is None and body_schema_to_use and "example" in body_schema_to_use:
|
764
783
|
example_data = body_schema_to_use["example"]
|
765
784
|
except KeyError:
|
@@ -777,18 +796,24 @@ def _generate_method_code(path, method, operation):
|
|
777
796
|
# Adjust type_hint for file parameters for the docstring
|
778
797
|
if detail.is_file:
|
779
798
|
type_hint = "file (e.g., open('path/to/file', 'rb'))"
|
780
|
-
|
799
|
+
|
781
800
|
arg_line = f" {arg_name} ({type_hint}): {desc}"
|
782
|
-
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
|
783
802
|
example_str = repr(detail.example)
|
784
803
|
arg_line += f" Example: {example_str}."
|
785
804
|
# Fallback for body parameters if no direct example was found
|
786
|
-
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
|
+
):
|
787
812
|
current_body_param_example = example_data[detail.identifier]
|
788
|
-
if current_body_param_example is not None:
|
813
|
+
if current_body_param_example is not None: # Ensure the extracted part is not None
|
789
814
|
try:
|
790
815
|
arg_line += f" Example: {repr(current_body_param_example)}."
|
791
|
-
except Exception:
|
816
|
+
except Exception: # Fallback if repr fails
|
792
817
|
arg_line += " Example: [Could not represent example]."
|
793
818
|
|
794
819
|
args_doc_lines.append(arg_line)
|
@@ -797,19 +822,17 @@ def _generate_method_code(path, method, operation):
|
|
797
822
|
args_doc_lines.append(
|
798
823
|
f" {arg_name} (dict | None): Optional dictionary for an empty JSON request body (e.g., {{}})."
|
799
824
|
)
|
800
|
-
elif arg_name == raw_body_param_name:
|
825
|
+
elif arg_name == raw_body_param_name:
|
801
826
|
raw_body_type_hint = "bytes"
|
802
827
|
raw_body_desc = "Raw binary content for the request body."
|
803
828
|
if selected_content_type and "text" in selected_content_type:
|
804
829
|
raw_body_type_hint = "str"
|
805
830
|
raw_body_desc = "Raw text content for the request body."
|
806
831
|
elif selected_content_type and selected_content_type.startswith("image/"):
|
807
|
-
|
808
|
-
|
809
|
-
args_doc_lines.append(
|
810
|
-
|
811
|
-
)
|
812
|
-
|
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
|
+
|
813
836
|
if args_doc_lines:
|
814
837
|
docstring_parts.append("\n".join(args_doc_lines))
|
815
838
|
|
@@ -825,7 +848,7 @@ def _generate_method_code(path, method, operation):
|
|
825
848
|
raises_section_lines = [
|
826
849
|
"Raises:",
|
827
850
|
" HTTPError: Raised when the API request fails (e.g., non-2XX status code).",
|
828
|
-
" JSONDecodeError: Raised if the response body cannot be parsed as JSON."
|
851
|
+
" JSONDecodeError: Raised if the response body cannot be parsed as JSON.",
|
829
852
|
]
|
830
853
|
docstring_parts.append("\n".join(raises_section_lines))
|
831
854
|
|
@@ -853,7 +876,7 @@ def _generate_method_code(path, method, operation):
|
|
853
876
|
else:
|
854
877
|
signature = f" def {func_name}(self) -> {return_type}:"
|
855
878
|
|
856
|
-
# --- Build Method Body ---
|
879
|
+
# --- Build Method Body ---
|
857
880
|
# This section constructs the executable lines of code within the generated method.
|
858
881
|
body_lines = []
|
859
882
|
|
@@ -861,10 +884,9 @@ def _generate_method_code(path, method, operation):
|
|
861
884
|
for param in path_params:
|
862
885
|
body_lines.append(f" if {param.name} is None:")
|
863
886
|
body_lines.append(
|
864
|
-
f
|
887
|
+
f" raise ValueError(\"Missing required parameter '{param.identifier}'.\")" # Use original name in error, ensure quotes are balanced
|
865
888
|
)
|
866
889
|
|
867
|
-
|
868
890
|
if method_lower not in ["get", "delete"]:
|
869
891
|
body_lines.append(" request_body_data = None")
|
870
892
|
|
@@ -874,7 +896,6 @@ def _generate_method_code(path, method, operation):
|
|
874
896
|
if method_lower in ["post", "put"] and selected_content_type == "multipart/form-data":
|
875
897
|
body_lines.append(" files_data = None")
|
876
898
|
|
877
|
-
|
878
899
|
# --- Build Request Payload (request_body_data and files_data) ---
|
879
900
|
# This section prepares the data to be sent in the request body,
|
880
901
|
# differentiating between files and other data for multipart forms,
|
@@ -883,27 +904,30 @@ def _generate_method_code(path, method, operation):
|
|
883
904
|
# This block will now overwrite the initial None values if a body is present.
|
884
905
|
if is_array_body:
|
885
906
|
# For array request bodies, use the array parameter directly
|
886
|
-
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
|
887
912
|
body_lines.append(f" # Using array parameter '{array_arg_name}' directly as request body")
|
888
|
-
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
|
889
914
|
# files_data remains None
|
890
915
|
|
891
916
|
elif selected_content_type == "multipart/form-data":
|
892
|
-
body_lines.append(" request_body_data = {}")
|
893
|
-
body_lines.append(" files_data = {}")
|
894
|
-
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
|
895
920
|
if b_param.is_file:
|
896
|
-
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
|
897
922
|
body_lines.append(f" files_data['{b_param.identifier}'] = {b_param.name}")
|
898
923
|
else:
|
899
|
-
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
|
900
925
|
body_lines.append(f" request_body_data['{b_param.identifier}'] = {b_param.name}")
|
901
926
|
body_lines.append(" files_data = {k: v for k, v in files_data.items() if v is not None}")
|
902
927
|
# Ensure files_data is None if it's empty after filtering, as httpx expects None, not {}
|
903
928
|
body_lines.append(" if not files_data: files_data = None")
|
904
929
|
|
905
|
-
|
906
|
-
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
|
907
931
|
body_lines.append(" request_body_data = {")
|
908
932
|
for b_param in body_params:
|
909
933
|
body_lines.append(f" '{b_param.identifier}': {b_param.name},")
|
@@ -911,13 +935,14 @@ def _generate_method_code(path, method, operation):
|
|
911
935
|
body_lines.append(
|
912
936
|
" request_body_data = {k: v for k, v in request_body_data.items() if v is not None}"
|
913
937
|
)
|
914
|
-
|
915
|
-
elif raw_body_param_name:
|
938
|
+
|
939
|
+
elif raw_body_param_name: # Raw content type (octet-stream, text, image)
|
916
940
|
body_lines.append(f" request_body_data = {raw_body_param_name}")
|
917
941
|
|
918
|
-
elif has_empty_body and selected_content_type == "application/json":
|
919
|
-
body_lines.append(
|
920
|
-
|
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
|
+
)
|
921
946
|
|
922
947
|
# --- Format URL and Query Parameters for Request ---
|
923
948
|
url = _generate_url(path, path_params)
|
@@ -937,21 +962,20 @@ def _generate_method_code(path, method, operation):
|
|
937
962
|
|
938
963
|
# --- Determine Final Content-Type for API Call (Obsolete Block, selected_content_type is used) ---
|
939
964
|
# The following block for request_body_content_type is largely superseded by selected_content_type,
|
940
|
-
|
965
|
+
|
941
966
|
# Use the selected_content_type determined by the new logic as the primary source of truth.
|
942
967
|
final_content_type_for_api_call = selected_content_type if selected_content_type else "application/json"
|
943
968
|
|
944
969
|
# --- Make HTTP Request ---
|
945
|
-
# This section generates the actual HTTP call
|
970
|
+
# This section generates the actual HTTP call
|
946
971
|
# using the prepared URL, query parameters, request body data, files, and content type.
|
947
972
|
|
948
|
-
|
949
973
|
if method_lower == "get":
|
950
974
|
body_lines.append(" response = self._get(url, params=query_params)")
|
951
975
|
elif method_lower == "post":
|
952
976
|
if selected_content_type == "multipart/form-data":
|
953
977
|
body_lines.append(
|
954
|
-
|
978
|
+
f" response = self._post(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
955
979
|
)
|
956
980
|
else:
|
957
981
|
body_lines.append(
|
@@ -960,23 +984,18 @@ def _generate_method_code(path, method, operation):
|
|
960
984
|
elif method_lower == "put":
|
961
985
|
if selected_content_type == "multipart/form-data":
|
962
986
|
body_lines.append(
|
963
|
-
|
987
|
+
f" response = self._put(url, data=request_body_data, files=files_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
964
988
|
)
|
965
989
|
else:
|
966
990
|
body_lines.append(
|
967
991
|
f" response = self._put(url, data=request_body_data, params=query_params, content_type='{final_content_type_for_api_call}')"
|
968
992
|
)
|
969
993
|
elif method_lower == "patch":
|
970
|
-
|
971
|
-
body_lines.append(
|
972
|
-
" response = self._patch(url, data=request_body_data, params=query_params)"
|
973
|
-
)
|
994
|
+
body_lines.append(" response = self._patch(url, data=request_body_data, params=query_params)")
|
974
995
|
elif method_lower == "delete":
|
975
996
|
body_lines.append(" response = self._delete(url, params=query_params)")
|
976
997
|
else:
|
977
|
-
body_lines.append(
|
978
|
-
f" response = self._{method_lower}(url, data=request_body_data, params=query_params)"
|
979
|
-
)
|
998
|
+
body_lines.append(f" response = self._{method_lower}(url, data=request_body_data, params=query_params)")
|
980
999
|
|
981
1000
|
# --- Handle Response ---
|
982
1001
|
body_lines.append(" response.raise_for_status()")
|
@@ -1145,4 +1164,4 @@ if __name__ == "__main__":
|
|
1145
1164
|
|
1146
1165
|
schema = load_schema("openapi.yaml")
|
1147
1166
|
code = generate_api_client(schema)
|
1148
|
-
print(code)
|
1167
|
+
print(code)
|