universal-mcp 0.1.12__py3-none-any.whl → 0.1.13rc2__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 +51 -7
- universal_mcp/cli.py +109 -17
- universal_mcp/integrations/__init__.py +1 -1
- universal_mcp/integrations/integration.py +79 -0
- universal_mcp/servers/README.md +79 -0
- universal_mcp/servers/server.py +17 -29
- universal_mcp/stores/README.md +74 -0
- universal_mcp/stores/store.py +0 -2
- universal_mcp/templates/README.md.j2 +93 -0
- universal_mcp/templates/api_client.py.j2 +27 -0
- universal_mcp/tools/README.md +86 -0
- universal_mcp/tools/tools.py +1 -1
- universal_mcp/utils/agentr.py +90 -0
- universal_mcp/utils/api_generator.py +166 -208
- universal_mcp/utils/openapi.py +221 -321
- universal_mcp/utils/singleton.py +23 -0
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/METADATA +16 -41
- universal_mcp-0.1.13rc2.dist-info/RECORD +38 -0
- universal_mcp/applications/ahrefs/README.md +0 -76
- universal_mcp/applications/ahrefs/__init__.py +0 -0
- universal_mcp/applications/ahrefs/app.py +0 -2291
- universal_mcp/applications/cal_com_v2/README.md +0 -175
- universal_mcp/applications/cal_com_v2/__init__.py +0 -0
- universal_mcp/applications/cal_com_v2/app.py +0 -5390
- universal_mcp/applications/calendly/README.md +0 -78
- universal_mcp/applications/calendly/__init__.py +0 -0
- universal_mcp/applications/calendly/app.py +0 -1195
- universal_mcp/applications/clickup/README.md +0 -160
- universal_mcp/applications/clickup/__init__.py +0 -0
- universal_mcp/applications/clickup/app.py +0 -5009
- universal_mcp/applications/coda/README.md +0 -133
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +0 -3671
- universal_mcp/applications/e2b/README.md +0 -37
- universal_mcp/applications/e2b/app.py +0 -65
- universal_mcp/applications/elevenlabs/README.md +0 -84
- universal_mcp/applications/elevenlabs/__init__.py +0 -0
- universal_mcp/applications/elevenlabs/app.py +0 -1402
- universal_mcp/applications/falai/README.md +0 -42
- universal_mcp/applications/falai/__init__.py +0 -0
- universal_mcp/applications/falai/app.py +0 -332
- universal_mcp/applications/figma/README.md +0 -74
- universal_mcp/applications/figma/__init__.py +0 -0
- universal_mcp/applications/figma/app.py +0 -1261
- universal_mcp/applications/firecrawl/README.md +0 -45
- universal_mcp/applications/firecrawl/app.py +0 -268
- universal_mcp/applications/github/README.md +0 -47
- universal_mcp/applications/github/app.py +0 -429
- universal_mcp/applications/gong/README.md +0 -88
- universal_mcp/applications/gong/__init__.py +0 -0
- universal_mcp/applications/gong/app.py +0 -2297
- universal_mcp/applications/google_calendar/app.py +0 -442
- universal_mcp/applications/google_docs/README.md +0 -40
- universal_mcp/applications/google_docs/app.py +0 -88
- universal_mcp/applications/google_drive/README.md +0 -44
- universal_mcp/applications/google_drive/app.py +0 -286
- universal_mcp/applications/google_mail/README.md +0 -47
- universal_mcp/applications/google_mail/app.py +0 -664
- universal_mcp/applications/google_sheet/README.md +0 -42
- universal_mcp/applications/google_sheet/app.py +0 -150
- universal_mcp/applications/hashnode/app.py +0 -81
- universal_mcp/applications/hashnode/prompt.md +0 -23
- universal_mcp/applications/heygen/README.md +0 -69
- universal_mcp/applications/heygen/__init__.py +0 -0
- universal_mcp/applications/heygen/app.py +0 -956
- universal_mcp/applications/mailchimp/README.md +0 -306
- universal_mcp/applications/mailchimp/__init__.py +0 -0
- universal_mcp/applications/mailchimp/app.py +0 -10937
- universal_mcp/applications/markitdown/app.py +0 -44
- universal_mcp/applications/notion/README.md +0 -55
- universal_mcp/applications/notion/__init__.py +0 -0
- universal_mcp/applications/notion/app.py +0 -527
- universal_mcp/applications/perplexity/README.md +0 -37
- universal_mcp/applications/perplexity/app.py +0 -65
- universal_mcp/applications/reddit/README.md +0 -45
- universal_mcp/applications/reddit/app.py +0 -379
- universal_mcp/applications/replicate/README.md +0 -65
- universal_mcp/applications/replicate/__init__.py +0 -0
- universal_mcp/applications/replicate/app.py +0 -980
- universal_mcp/applications/resend/README.md +0 -38
- universal_mcp/applications/resend/app.py +0 -37
- universal_mcp/applications/retell_ai/README.md +0 -46
- universal_mcp/applications/retell_ai/__init__.py +0 -0
- universal_mcp/applications/retell_ai/app.py +0 -333
- universal_mcp/applications/rocketlane/README.md +0 -42
- universal_mcp/applications/rocketlane/__init__.py +0 -0
- universal_mcp/applications/rocketlane/app.py +0 -194
- universal_mcp/applications/serpapi/README.md +0 -37
- universal_mcp/applications/serpapi/app.py +0 -73
- universal_mcp/applications/spotify/README.md +0 -116
- universal_mcp/applications/spotify/__init__.py +0 -0
- universal_mcp/applications/spotify/app.py +0 -2526
- universal_mcp/applications/supabase/README.md +0 -112
- universal_mcp/applications/supabase/__init__.py +0 -0
- universal_mcp/applications/supabase/app.py +0 -2970
- universal_mcp/applications/tavily/README.md +0 -38
- universal_mcp/applications/tavily/app.py +0 -51
- universal_mcp/applications/wrike/README.md +0 -71
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +0 -1372
- universal_mcp/applications/youtube/README.md +0 -82
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +0 -1428
- universal_mcp/applications/zenquotes/README.md +0 -37
- universal_mcp/applications/zenquotes/app.py +0 -31
- universal_mcp/integrations/agentr.py +0 -112
- universal_mcp-0.1.12.dist-info/RECORD +0 -119
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/entry_points.txt +0 -0
universal_mcp/utils/openapi.py
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
import json
|
2
2
|
import re
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import Any
|
4
|
+
from typing import Any, Dict, List, Literal
|
5
|
+
from loguru import logger
|
5
6
|
|
6
7
|
import yaml
|
7
|
-
|
8
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
9
|
+
from dataclasses import dataclass
|
8
10
|
|
9
11
|
def convert_to_snake_case(identifier: str) -> str:
|
10
|
-
"""
|
12
|
+
"""
|
11
13
|
Convert a camelCase or PascalCase identifier to snake_case.
|
12
14
|
|
13
15
|
Args:
|
@@ -107,9 +109,6 @@ def generate_api_client(schema):
|
|
107
109
|
Returns:
|
108
110
|
str: A string containing the Python code for the API client class.
|
109
111
|
"""
|
110
|
-
methods = []
|
111
|
-
method_names = []
|
112
|
-
|
113
112
|
# Extract API info for naming and base URL
|
114
113
|
info = schema.get("info", {})
|
115
114
|
api_title = info.get("title", "API")
|
@@ -126,371 +125,272 @@ def generate_api_client(schema):
|
|
126
125
|
base_name = "".join(word.capitalize() for word in api_title.split())
|
127
126
|
clean_name = "".join(c for c in base_name if c.isalnum())
|
128
127
|
class_name = f"{clean_name}App"
|
129
|
-
|
130
|
-
# Extract tool name - remove spaces and convert to lowercase
|
131
|
-
tool_name = api_title.lower()
|
132
|
-
|
133
|
-
# Remove version numbers (like 3.0, v1, etc.)
|
134
|
-
tool_name = re.sub(r"\s*v?\d+(\.\d+)*", "", tool_name)
|
135
|
-
|
136
|
-
# Remove common words that aren't needed
|
137
|
-
common_words = ["api", "openapi", "open", "swagger", "spec", "specification"]
|
138
|
-
for word in common_words:
|
139
|
-
tool_name = tool_name.replace(word, "")
|
140
|
-
|
141
|
-
# Remove spaces, hyphens, underscores
|
142
|
-
tool_name = tool_name.replace(" ", "").replace("-", "").replace("_", "")
|
143
|
-
|
144
|
-
# Remove any non-alphanumeric characters
|
145
|
-
tool_name = "".join(c for c in tool_name if c.isalnum())
|
146
|
-
|
147
|
-
# If empty (after cleaning), use generic name
|
148
|
-
if not tool_name:
|
149
|
-
tool_name = "api"
|
150
128
|
else:
|
151
129
|
class_name = "APIClient"
|
152
|
-
tool_name = "api"
|
153
130
|
|
154
|
-
#
|
131
|
+
# Collect all methods
|
132
|
+
methods = []
|
155
133
|
for path, path_info in schema.get("paths", {}).items():
|
156
134
|
for method in path_info:
|
157
135
|
if method in ["get", "post", "put", "delete", "patch", "options", "head"]:
|
158
136
|
operation = path_info[method]
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
"from typing import Any",
|
175
|
-
"from universal_mcp.applications import APIApplication",
|
176
|
-
"from universal_mcp.integrations import Integration",
|
177
|
-
]
|
178
|
-
|
179
|
-
# Construct the class code
|
180
|
-
class_code = (
|
181
|
-
"\n".join(imports) + "\n\n"
|
182
|
-
f"class {class_name}(APIApplication):\n"
|
183
|
-
f" def __init__(self, integration: Integration = None, **kwargs) -> None:\n"
|
184
|
-
f" super().__init__(name='{class_name.lower()}', integration=integration, **kwargs)\n"
|
185
|
-
f' self.base_url = "{base_url}"\n\n'
|
186
|
-
+ "\n\n".join(methods)
|
187
|
-
+ "\n\n"
|
188
|
-
+ list_tools_method
|
189
|
-
+ "\n"
|
137
|
+
function = generate_method_code(path, method, operation, schema)
|
138
|
+
methods.append(function)
|
139
|
+
|
140
|
+
# Set up Jinja2 environment
|
141
|
+
env = Environment(
|
142
|
+
loader=FileSystemLoader(Path(__file__).parent.parent / "templates"),
|
143
|
+
autoescape=select_autoescape()
|
144
|
+
)
|
145
|
+
template = env.get_template("api_client.py.j2")
|
146
|
+
|
147
|
+
# Render the template
|
148
|
+
class_code = template.render(
|
149
|
+
class_name=class_name,
|
150
|
+
base_url=base_url,
|
151
|
+
methods=methods
|
190
152
|
)
|
153
|
+
|
191
154
|
return class_code
|
192
155
|
|
193
156
|
|
194
|
-
|
157
|
+
@dataclass
|
158
|
+
class Function:
|
159
|
+
name: str
|
160
|
+
type: Literal["get", "post", "put", "delete", "patch", "options", "head"]
|
161
|
+
args: Dict[str, str]
|
162
|
+
return_type: str
|
163
|
+
description: str
|
164
|
+
tags: List[str]
|
165
|
+
implementation: str
|
166
|
+
|
167
|
+
@property
|
168
|
+
def args_str(self) -> str:
|
169
|
+
"""Convert args dictionary to a string representation."""
|
170
|
+
return ", ".join(f"{arg}: {typ}" for arg, typ in self.args.items())
|
171
|
+
|
172
|
+
|
173
|
+
def generate_method_code(
|
174
|
+
path: str,
|
175
|
+
method: str,
|
176
|
+
operation: dict[str, Any],
|
177
|
+
full_schema: dict[str, Any]
|
178
|
+
) -> Function:
|
195
179
|
"""
|
196
|
-
Generate
|
180
|
+
Generate a Function object for a single API method.
|
197
181
|
|
198
182
|
Args:
|
199
|
-
path
|
200
|
-
method
|
201
|
-
operation
|
202
|
-
full_schema
|
203
|
-
tool_name (str, optional): The name of the tool/app to prefix the function name with.
|
183
|
+
path: The API path (e.g., '/users/{user_id}').
|
184
|
+
method: The HTTP method (e.g., 'get').
|
185
|
+
operation: The operation details from the schema.
|
186
|
+
full_schema: The complete OpenAPI schema, used for reference resolution.
|
204
187
|
|
205
188
|
Returns:
|
206
|
-
|
189
|
+
A Function object with metadata: name, args, return_type, description, tags.
|
207
190
|
"""
|
208
|
-
|
209
|
-
|
191
|
+
logger.debug(f"Generating function for {method.upper()} {path}")
|
192
|
+
|
193
|
+
# Helper to map JSON schema types to Python types
|
194
|
+
def map_type(sch: dict[str, Any]) -> str:
|
195
|
+
t = sch.get("type")
|
196
|
+
if t == "integer":
|
197
|
+
return "int"
|
198
|
+
if t == "number":
|
199
|
+
return "float"
|
200
|
+
if t == "boolean":
|
201
|
+
return "bool"
|
202
|
+
if t == "array":
|
203
|
+
return "list[Any]"
|
204
|
+
if t == "object":
|
205
|
+
return "dict[str, Any]"
|
206
|
+
return "Any"
|
207
|
+
|
210
208
|
|
211
209
|
# Determine function name
|
212
|
-
if "operationId"
|
213
|
-
|
214
|
-
|
215
|
-
func_name = convert_to_snake_case(cleaned_name)
|
210
|
+
if op_id := operation.get("operationId"):
|
211
|
+
cleaned_id = op_id.replace(".", "_").replace("-", "_")
|
212
|
+
method_name = convert_to_snake_case(cleaned_id)
|
216
213
|
else:
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
name_parts.append("by_" + part[1:-1])
|
214
|
+
segments = [seg.replace("-", "_") for seg in path.strip("/").split("/")]
|
215
|
+
parts = [method.lower()]
|
216
|
+
for seg in segments:
|
217
|
+
if seg.startswith("{") and seg.endswith("}"):
|
218
|
+
parts.append(f"by_{seg[1:-1]}")
|
223
219
|
else:
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
) # Fix for patterns like retrieve_ablock -> retrieve_a_block
|
231
|
-
func_name = re.sub(
|
232
|
-
r"_a$", r"_a", func_name
|
233
|
-
) # Don't change if 'a' is at the end of the name
|
234
|
-
func_name = re.sub(
|
235
|
-
r"_an([^_a-z])", r"_an_\1", func_name
|
236
|
-
) # Fix for patterns like create_anitem -> create_an_item
|
237
|
-
func_name = re.sub(
|
238
|
-
r"_an$", r"_an", func_name
|
239
|
-
) # Don't change if 'an' is at the end of the name
|
240
|
-
|
241
|
-
# Get parameters and request body
|
242
|
-
# Resolve parameter references before processing
|
243
|
-
resolved_parameters = []
|
220
|
+
parts.append(seg)
|
221
|
+
method_name = "_".join(parts)
|
222
|
+
logger.debug(f"Resolved function name={method_name}")
|
223
|
+
|
224
|
+
# Resolve references and filter out header params
|
225
|
+
resolved_params = []
|
244
226
|
for param in operation.get("parameters", []):
|
245
|
-
if "$ref"
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
resolved_parameters.append(ref_param)
|
227
|
+
if ref := param.get("$ref"):
|
228
|
+
target = resolve_schema_reference(ref, full_schema)
|
229
|
+
if target:
|
230
|
+
resolved_params.append(target)
|
250
231
|
else:
|
251
|
-
|
252
|
-
f"Warning: Could not resolve parameter reference: {param['$ref']}"
|
253
|
-
)
|
232
|
+
logger.warning(f"Unresolved parameter reference: {ref}")
|
254
233
|
else:
|
255
|
-
|
234
|
+
resolved_params.append(param)
|
256
235
|
|
257
|
-
#
|
258
|
-
|
236
|
+
# Separate params by location
|
237
|
+
path_params = [p for p in resolved_params if p.get("in") == "path"]
|
238
|
+
query_params = [p for p in resolved_params if p.get("in") == "query"]
|
259
239
|
|
240
|
+
# Analyze requestBody
|
260
241
|
has_body = "requestBody" in operation
|
261
|
-
body_required = has_body and operation["requestBody"].get("required"
|
262
|
-
|
263
|
-
# Check if the requestBody has actual content or is empty
|
264
|
-
has_empty_body = False
|
265
|
-
if has_body:
|
266
|
-
request_body_content = operation["requestBody"].get("content", {})
|
267
|
-
if not request_body_content or all(
|
268
|
-
not content for content_type, content in request_body_content.items()
|
269
|
-
):
|
270
|
-
has_empty_body = True
|
271
|
-
else:
|
272
|
-
# Handle empty properties with additionalProperties:true
|
273
|
-
for content_type, content in request_body_content.items():
|
274
|
-
if content_type.startswith("application/json") and "schema" in content:
|
275
|
-
schema = content["schema"]
|
276
|
-
|
277
|
-
# Resolve schema reference if present
|
278
|
-
if "$ref" in schema:
|
279
|
-
ref_schema = resolve_schema_reference(
|
280
|
-
schema["$ref"], full_schema
|
281
|
-
)
|
282
|
-
if ref_schema:
|
283
|
-
schema = ref_schema
|
284
|
-
|
285
|
-
# Check if properties is empty and additionalProperties is true
|
286
|
-
if (
|
287
|
-
schema.get("type") == "object"
|
288
|
-
and schema.get("additionalProperties", False) is True
|
289
|
-
):
|
290
|
-
properties = schema.get("properties", {})
|
291
|
-
if not properties or len(properties) == 0:
|
292
|
-
has_empty_body = True
|
293
|
-
|
294
|
-
# Extract request body schema properties and required fields
|
295
|
-
required_fields = []
|
296
|
-
request_body_properties = {}
|
242
|
+
body_required = bool(has_body and operation["requestBody"].get("required"))
|
243
|
+
content = (operation.get("requestBody", {}) or {}).get("content", {}) if has_body else {}
|
297
244
|
is_array_body = False
|
298
|
-
|
299
|
-
|
300
|
-
if has_body:
|
301
|
-
for
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
ref_schema = resolve_schema_reference(schema["$ref"], full_schema)
|
310
|
-
if ref_schema:
|
311
|
-
schema = ref_schema
|
312
|
-
|
313
|
-
# Check if the schema is an array type
|
314
|
-
if schema.get("type") == "array":
|
315
|
-
is_array_body = True
|
316
|
-
array_items_schema = schema.get("items", {})
|
317
|
-
# Try to resolve any reference in items
|
318
|
-
if "$ref" in array_items_schema:
|
319
|
-
array_items_schema = resolve_schema_reference(
|
320
|
-
array_items_schema["$ref"], full_schema
|
321
|
-
)
|
322
|
-
else:
|
323
|
-
# Extract required fields from schema
|
324
|
-
if "required" in schema:
|
325
|
-
required_fields = schema["required"]
|
326
|
-
# Extract properties from schema
|
327
|
-
if "properties" in schema:
|
328
|
-
request_body_properties = schema["properties"]
|
329
|
-
|
330
|
-
# Check for nested references in properties
|
331
|
-
for prop_name, prop_schema in request_body_properties.items():
|
332
|
-
if "$ref" in prop_schema:
|
333
|
-
ref_prop_schema = resolve_schema_reference(
|
334
|
-
prop_schema["$ref"], full_schema
|
335
|
-
)
|
336
|
-
if ref_prop_schema:
|
337
|
-
request_body_properties[prop_name] = ref_prop_schema
|
338
|
-
|
339
|
-
# Handle schemas with empty properties but additionalProperties: true
|
340
|
-
# by treating them similar to empty bodies
|
341
|
-
if (
|
342
|
-
not request_body_properties or len(request_body_properties) == 0
|
343
|
-
) and schema.get("additionalProperties") is True:
|
344
|
-
has_empty_body = True
|
345
|
-
|
346
|
-
# Build function arguments
|
347
|
-
required_args = []
|
348
|
-
optional_args = []
|
349
|
-
|
350
|
-
# Add path parameters
|
351
|
-
for param_name in path_params_in_url:
|
352
|
-
if param_name not in required_args:
|
353
|
-
required_args.append(param_name)
|
354
|
-
|
355
|
-
# Add query parameters
|
356
|
-
for param in parameters:
|
357
|
-
param_name = param["name"]
|
358
|
-
if param_name not in required_args:
|
359
|
-
if param.get("required", False):
|
360
|
-
required_args.append(param_name)
|
245
|
+
request_props: Dict[str, Any] = {}
|
246
|
+
required_fields: List[str] = []
|
247
|
+
if has_body and content:
|
248
|
+
for mime, info in content.items():
|
249
|
+
if not mime.startswith("application/json") or "schema" not in info:
|
250
|
+
continue
|
251
|
+
schema = info["schema"]
|
252
|
+
if ref := schema.get("$ref"):
|
253
|
+
schema = resolve_schema_reference(ref, full_schema) or schema
|
254
|
+
if schema.get("type") == "array":
|
255
|
+
is_array_body = True
|
361
256
|
else:
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
257
|
+
required_fields = schema.get("required", []) or []
|
258
|
+
request_props = schema.get("properties", {}) or {}
|
259
|
+
for name, prop_schema in list(request_props.items()):
|
260
|
+
if pre := prop_schema.get("$ref"):
|
261
|
+
request_props[name] = resolve_schema_reference(pre, full_schema) or prop_schema
|
262
|
+
|
263
|
+
# Build function arguments with Annotated[type, description]
|
264
|
+
arg_defs: Dict[str, str] = {}
|
265
|
+
for p in path_params:
|
266
|
+
name = p["name"]
|
267
|
+
ty = map_type(p.get("schema", {}))
|
268
|
+
desc = p.get("description", "")
|
269
|
+
arg_defs[name] = f"Annotated[{ty}, {desc!r}]"
|
270
|
+
for p in query_params:
|
271
|
+
name = p["name"]
|
272
|
+
ty = map_type(p.get("schema", {}))
|
273
|
+
desc = p.get("description", "")
|
274
|
+
if p.get("required"):
|
275
|
+
arg_defs[name] = f"Annotated[{ty}, {desc!r}]"
|
276
|
+
else:
|
277
|
+
arg_defs[name] = f"Annotated[{ty}, {desc!r}] = None"
|
366
278
|
if has_body:
|
367
279
|
if is_array_body:
|
368
|
-
|
369
|
-
array_param_name = "items"
|
370
|
-
# Try to get a better name from the operation or path
|
371
|
-
if func_name.endswith("_list_input"):
|
372
|
-
array_param_name = func_name.replace("_list_input", "")
|
373
|
-
elif "List" in func_name:
|
374
|
-
array_param_name = func_name.split("List")[0].lower() + "_list"
|
375
|
-
|
376
|
-
# Make the array parameter required if the request body is required
|
280
|
+
desc = operation["requestBody"].get("description", "")
|
377
281
|
if body_required:
|
378
|
-
|
282
|
+
arg_defs["items"] = f"Annotated[list[Any], {desc!r}]"
|
379
283
|
else:
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
if prop_name in required_fields:
|
388
|
-
request_body_params.append(prop_name)
|
389
|
-
if prop_name not in required_args:
|
390
|
-
required_args.append(prop_name)
|
284
|
+
arg_defs["items"] = f"Annotated[list[Any], {desc!r}] = None"
|
285
|
+
elif request_props:
|
286
|
+
for prop, schema in request_props.items():
|
287
|
+
ty = map_type(schema)
|
288
|
+
desc = schema.get("description", "")
|
289
|
+
if prop in required_fields:
|
290
|
+
arg_defs[prop] = f"Annotated[{ty}, {desc!r}]"
|
391
291
|
else:
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
# If request body is present but empty (content: {}), add a generic request_body parameter
|
397
|
-
if has_empty_body and "request_body=None" not in optional_args:
|
398
|
-
optional_args.append("request_body=None")
|
292
|
+
arg_defs[prop] = f"Annotated[{ty}, {desc!r}] = None"
|
293
|
+
else:
|
294
|
+
desc = operation["requestBody"].get("description", "")
|
295
|
+
arg_defs["request_body"] = f"Annotated[Any, {desc!r}] = None"
|
399
296
|
|
400
|
-
#
|
401
|
-
|
297
|
+
# Sort and order arguments
|
298
|
+
required_keys = sorted(k for k, v in arg_defs.items() if "=" not in v)
|
299
|
+
optional_keys = sorted(k for k, v in arg_defs.items() if "=" in v)
|
300
|
+
ordered_args = {k: arg_defs[k] for k in required_keys + optional_keys}
|
402
301
|
|
403
|
-
# Determine return type
|
404
302
|
return_type = determine_return_type(operation)
|
405
303
|
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
304
|
+
# Assemble description
|
305
|
+
summary = operation.get("summary", "")
|
306
|
+
operation_desc = operation.get("description", "")
|
307
|
+
desc_parts: List[str] = []
|
308
|
+
if summary:
|
309
|
+
desc_parts.append(summary)
|
310
|
+
if operation_desc:
|
311
|
+
desc_parts.append(operation_desc)
|
312
|
+
description_text = ". ".join(desc_parts)
|
313
|
+
|
314
|
+
tags = operation.get("tags", []) or []
|
315
|
+
|
316
|
+
# Generate implementation code
|
317
|
+
implementation_lines = []
|
318
|
+
|
319
|
+
# Add parameter validation for required fields
|
320
|
+
for param in path_params + query_params:
|
321
|
+
if param.get("required"):
|
322
|
+
name = param["name"]
|
323
|
+
implementation_lines.append(f"if {name} is None:")
|
324
|
+
implementation_lines.append(f" raise ValueError(\"Missing required parameter '{name}'\")")
|
325
|
+
|
326
|
+
if has_body and body_required:
|
327
|
+
if is_array_body:
|
328
|
+
implementation_lines.append("if items is None:")
|
329
|
+
implementation_lines.append(" raise ValueError(\"Missing required parameter 'items'\")")
|
330
|
+
elif request_props:
|
331
|
+
for prop in required_fields:
|
332
|
+
implementation_lines.append(f"if {prop} is None:")
|
333
|
+
implementation_lines.append(f" raise ValueError(\"Missing required parameter '{prop}'\")")
|
334
|
+
else:
|
335
|
+
implementation_lines.append("if request_body is None:")
|
336
|
+
implementation_lines.append(" raise ValueError(\"Missing required parameter 'request_body'\")")
|
417
337
|
|
418
|
-
# Build request body
|
338
|
+
# Build request body
|
419
339
|
if has_body:
|
420
340
|
if is_array_body:
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
# Only include non-None values in the request body
|
431
|
-
body_lines.append(f" '{prop_name}': {prop_name},")
|
432
|
-
|
433
|
-
body_lines.append(" }")
|
434
|
-
|
435
|
-
body_lines.append(
|
436
|
-
" request_body = {k: v for k, v in request_body.items() if v is not None}"
|
437
|
-
)
|
341
|
+
implementation_lines.append("request_body = items")
|
342
|
+
elif request_props:
|
343
|
+
implementation_lines.append("request_body = {")
|
344
|
+
for prop in request_props:
|
345
|
+
implementation_lines.append(f" \"{prop}\": {prop},")
|
346
|
+
implementation_lines.append("}")
|
347
|
+
implementation_lines.append("request_body = {k: v for k, v in request_body.items() if v is not None}")
|
348
|
+
else:
|
349
|
+
implementation_lines.append("request_body = request_body")
|
438
350
|
|
439
|
-
#
|
440
|
-
|
441
|
-
|
351
|
+
# Build URL with path parameters
|
352
|
+
path = "/".join([path_params["name"] for path_params in path_params]) or '\"\"'
|
353
|
+
url = '\"{self.base_url}{path}\"'
|
354
|
+
implementation_lines.append(f'path = {path}')
|
355
|
+
implementation_lines.append(f'url = f{url}')
|
442
356
|
|
443
|
-
#
|
444
|
-
query_params = [p for p in parameters if p["in"] == "query"]
|
357
|
+
# Build query parameters
|
445
358
|
if query_params:
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
)
|
359
|
+
implementation_lines.append("query_params = {")
|
360
|
+
for param in query_params:
|
361
|
+
name = param["name"]
|
362
|
+
implementation_lines.append(f" \"{name}\": {name},")
|
363
|
+
implementation_lines.append(" }")
|
364
|
+
implementation_lines.append("query_params = {k: v for k, v in query_params.items() if v is not None}")
|
452
365
|
else:
|
453
|
-
|
366
|
+
implementation_lines.append("query_params = {}")
|
454
367
|
|
455
|
-
# Make
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
if has_empty_body:
|
460
|
-
request_body_arg = "request_body"
|
461
|
-
elif not has_body:
|
462
|
-
request_body_arg = "{}"
|
463
|
-
else:
|
464
|
-
request_body_arg = "request_body"
|
465
|
-
|
466
|
-
if method_lower == "get":
|
467
|
-
body_lines.append(" response = self._get(url, params=query_params)")
|
468
|
-
elif method_lower == "post":
|
469
|
-
body_lines.append(
|
470
|
-
f" response = self._post(url, data={request_body_arg}, params=query_params)"
|
471
|
-
)
|
472
|
-
elif method_lower == "put":
|
473
|
-
body_lines.append(
|
474
|
-
f" response = self._put(url, data={request_body_arg}, params=query_params)"
|
475
|
-
)
|
476
|
-
elif method_lower == "patch":
|
477
|
-
body_lines.append(
|
478
|
-
f" response = self._patch(url, data={request_body_arg}, params=query_params)"
|
479
|
-
)
|
480
|
-
elif method_lower == "delete":
|
481
|
-
body_lines.append(" response = self._delete(url, params=query_params)")
|
368
|
+
# Make the request using the appropriate method
|
369
|
+
http_method = method.lower()
|
370
|
+
if has_body:
|
371
|
+
implementation_lines.append(f"response = self._{http_method}(url, data=request_body, params=query_params)")
|
482
372
|
else:
|
483
|
-
|
484
|
-
f" response = self._{method_lower}(url, data={request_body_arg}, params=query_params)"
|
485
|
-
)
|
373
|
+
implementation_lines.append(f"response = self._{http_method}(url, params=query_params)")
|
486
374
|
|
487
375
|
# Handle response
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
376
|
+
implementation_lines.append("response.raise_for_status()")
|
377
|
+
implementation_lines.append("return response.json()")
|
378
|
+
|
379
|
+
implementation = "\n".join(implementation_lines)
|
380
|
+
|
381
|
+
# Build Function object
|
382
|
+
function = Function(
|
383
|
+
name=method_name,
|
384
|
+
type=http_method,
|
385
|
+
args=ordered_args,
|
386
|
+
return_type=return_type,
|
387
|
+
description=description_text,
|
388
|
+
tags=tags,
|
389
|
+
implementation=implementation
|
390
|
+
)
|
493
391
|
|
392
|
+
logger.debug(f"Generated function: {function}")
|
393
|
+
return function
|
494
394
|
|
495
395
|
# Example usage
|
496
396
|
if __name__ == "__main__":
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Singleton(type):
|
2
|
+
"""Metaclass that ensures only one instance of a class exists.
|
3
|
+
|
4
|
+
This metaclass implements the singleton pattern by maintaining a dictionary
|
5
|
+
of instances for each class that uses it. When a class with this metaclass
|
6
|
+
is instantiated, it checks if an instance already exists and returns that
|
7
|
+
instance instead of creating a new one.
|
8
|
+
|
9
|
+
Example:
|
10
|
+
class MyClass(metaclass=Singleton):
|
11
|
+
pass
|
12
|
+
|
13
|
+
a = MyClass()
|
14
|
+
b = MyClass()
|
15
|
+
assert a is b # True
|
16
|
+
"""
|
17
|
+
|
18
|
+
_instances = {}
|
19
|
+
|
20
|
+
def __call__(cls, *args, **kwargs):
|
21
|
+
if cls not in cls._instances:
|
22
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
23
|
+
return cls._instances[cls]
|