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.
Files changed (109) hide show
  1. universal_mcp/applications/__init__.py +51 -7
  2. universal_mcp/cli.py +109 -17
  3. universal_mcp/integrations/__init__.py +1 -1
  4. universal_mcp/integrations/integration.py +79 -0
  5. universal_mcp/servers/README.md +79 -0
  6. universal_mcp/servers/server.py +17 -29
  7. universal_mcp/stores/README.md +74 -0
  8. universal_mcp/stores/store.py +0 -2
  9. universal_mcp/templates/README.md.j2 +93 -0
  10. universal_mcp/templates/api_client.py.j2 +27 -0
  11. universal_mcp/tools/README.md +86 -0
  12. universal_mcp/tools/tools.py +1 -1
  13. universal_mcp/utils/agentr.py +90 -0
  14. universal_mcp/utils/api_generator.py +166 -208
  15. universal_mcp/utils/openapi.py +221 -321
  16. universal_mcp/utils/singleton.py +23 -0
  17. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/METADATA +16 -41
  18. universal_mcp-0.1.13rc2.dist-info/RECORD +38 -0
  19. universal_mcp/applications/ahrefs/README.md +0 -76
  20. universal_mcp/applications/ahrefs/__init__.py +0 -0
  21. universal_mcp/applications/ahrefs/app.py +0 -2291
  22. universal_mcp/applications/cal_com_v2/README.md +0 -175
  23. universal_mcp/applications/cal_com_v2/__init__.py +0 -0
  24. universal_mcp/applications/cal_com_v2/app.py +0 -5390
  25. universal_mcp/applications/calendly/README.md +0 -78
  26. universal_mcp/applications/calendly/__init__.py +0 -0
  27. universal_mcp/applications/calendly/app.py +0 -1195
  28. universal_mcp/applications/clickup/README.md +0 -160
  29. universal_mcp/applications/clickup/__init__.py +0 -0
  30. universal_mcp/applications/clickup/app.py +0 -5009
  31. universal_mcp/applications/coda/README.md +0 -133
  32. universal_mcp/applications/coda/__init__.py +0 -0
  33. universal_mcp/applications/coda/app.py +0 -3671
  34. universal_mcp/applications/e2b/README.md +0 -37
  35. universal_mcp/applications/e2b/app.py +0 -65
  36. universal_mcp/applications/elevenlabs/README.md +0 -84
  37. universal_mcp/applications/elevenlabs/__init__.py +0 -0
  38. universal_mcp/applications/elevenlabs/app.py +0 -1402
  39. universal_mcp/applications/falai/README.md +0 -42
  40. universal_mcp/applications/falai/__init__.py +0 -0
  41. universal_mcp/applications/falai/app.py +0 -332
  42. universal_mcp/applications/figma/README.md +0 -74
  43. universal_mcp/applications/figma/__init__.py +0 -0
  44. universal_mcp/applications/figma/app.py +0 -1261
  45. universal_mcp/applications/firecrawl/README.md +0 -45
  46. universal_mcp/applications/firecrawl/app.py +0 -268
  47. universal_mcp/applications/github/README.md +0 -47
  48. universal_mcp/applications/github/app.py +0 -429
  49. universal_mcp/applications/gong/README.md +0 -88
  50. universal_mcp/applications/gong/__init__.py +0 -0
  51. universal_mcp/applications/gong/app.py +0 -2297
  52. universal_mcp/applications/google_calendar/app.py +0 -442
  53. universal_mcp/applications/google_docs/README.md +0 -40
  54. universal_mcp/applications/google_docs/app.py +0 -88
  55. universal_mcp/applications/google_drive/README.md +0 -44
  56. universal_mcp/applications/google_drive/app.py +0 -286
  57. universal_mcp/applications/google_mail/README.md +0 -47
  58. universal_mcp/applications/google_mail/app.py +0 -664
  59. universal_mcp/applications/google_sheet/README.md +0 -42
  60. universal_mcp/applications/google_sheet/app.py +0 -150
  61. universal_mcp/applications/hashnode/app.py +0 -81
  62. universal_mcp/applications/hashnode/prompt.md +0 -23
  63. universal_mcp/applications/heygen/README.md +0 -69
  64. universal_mcp/applications/heygen/__init__.py +0 -0
  65. universal_mcp/applications/heygen/app.py +0 -956
  66. universal_mcp/applications/mailchimp/README.md +0 -306
  67. universal_mcp/applications/mailchimp/__init__.py +0 -0
  68. universal_mcp/applications/mailchimp/app.py +0 -10937
  69. universal_mcp/applications/markitdown/app.py +0 -44
  70. universal_mcp/applications/notion/README.md +0 -55
  71. universal_mcp/applications/notion/__init__.py +0 -0
  72. universal_mcp/applications/notion/app.py +0 -527
  73. universal_mcp/applications/perplexity/README.md +0 -37
  74. universal_mcp/applications/perplexity/app.py +0 -65
  75. universal_mcp/applications/reddit/README.md +0 -45
  76. universal_mcp/applications/reddit/app.py +0 -379
  77. universal_mcp/applications/replicate/README.md +0 -65
  78. universal_mcp/applications/replicate/__init__.py +0 -0
  79. universal_mcp/applications/replicate/app.py +0 -980
  80. universal_mcp/applications/resend/README.md +0 -38
  81. universal_mcp/applications/resend/app.py +0 -37
  82. universal_mcp/applications/retell_ai/README.md +0 -46
  83. universal_mcp/applications/retell_ai/__init__.py +0 -0
  84. universal_mcp/applications/retell_ai/app.py +0 -333
  85. universal_mcp/applications/rocketlane/README.md +0 -42
  86. universal_mcp/applications/rocketlane/__init__.py +0 -0
  87. universal_mcp/applications/rocketlane/app.py +0 -194
  88. universal_mcp/applications/serpapi/README.md +0 -37
  89. universal_mcp/applications/serpapi/app.py +0 -73
  90. universal_mcp/applications/spotify/README.md +0 -116
  91. universal_mcp/applications/spotify/__init__.py +0 -0
  92. universal_mcp/applications/spotify/app.py +0 -2526
  93. universal_mcp/applications/supabase/README.md +0 -112
  94. universal_mcp/applications/supabase/__init__.py +0 -0
  95. universal_mcp/applications/supabase/app.py +0 -2970
  96. universal_mcp/applications/tavily/README.md +0 -38
  97. universal_mcp/applications/tavily/app.py +0 -51
  98. universal_mcp/applications/wrike/README.md +0 -71
  99. universal_mcp/applications/wrike/__init__.py +0 -0
  100. universal_mcp/applications/wrike/app.py +0 -1372
  101. universal_mcp/applications/youtube/README.md +0 -82
  102. universal_mcp/applications/youtube/__init__.py +0 -0
  103. universal_mcp/applications/youtube/app.py +0 -1428
  104. universal_mcp/applications/zenquotes/README.md +0 -37
  105. universal_mcp/applications/zenquotes/app.py +0 -31
  106. universal_mcp/integrations/agentr.py +0 -112
  107. universal_mcp-0.1.12.dist-info/RECORD +0 -119
  108. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/WHEEL +0 -0
  109. {universal_mcp-0.1.12.dist-info → universal_mcp-0.1.13rc2.dist-info}/entry_points.txt +0 -0
@@ -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
- # Iterate over paths and their operations
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
- method_code, func_name = generate_method_code(
160
- path, method, operation, schema, tool_name
161
- )
162
- methods.append(method_code)
163
- method_names.append(func_name)
164
-
165
- # Generate list_tools method with all the function names
166
- tools_list = ",\n ".join([f"self.{name}" for name in method_names])
167
- list_tools_method = f""" def list_tools(self):
168
- return [
169
- {tools_list}
170
- ]"""
171
-
172
- # Generate class imports
173
- imports = [
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
- def generate_method_code(path, method, operation, full_schema, tool_name=None):
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 the code for a single API method.
180
+ Generate a Function object for a single API method.
197
181
 
198
182
  Args:
199
- path (str): The API path (e.g., '/users/{user_id}').
200
- method (str): The HTTP method (e.g., 'get').
201
- operation (dict): The operation details from the schema.
202
- full_schema (dict): The complete OpenAPI schema, used for reference resolution.
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
- tuple: (method_code, func_name) - The Python code for the method and its name.
189
+ A Function object with metadata: name, args, return_type, description, tags.
207
190
  """
208
- # Extract path parameters from the URL path
209
- path_params_in_url = re.findall(r"{([^}]+)}", path)
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" in operation:
213
- raw_name = operation["operationId"]
214
- cleaned_name = raw_name.replace(".", "_").replace("-", "_")
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
- # Generate name from path and method
218
- path_parts = path.strip("/").split("/")
219
- name_parts = [method]
220
- for part in path_parts:
221
- if part.startswith("{") and part.endswith("}"):
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
- name_parts.append(part)
225
- func_name = "_".join(name_parts).replace("-", "_").lower()
226
-
227
- # Only fix isolated 'a' and 'an' as articles, not when they're part of words
228
- func_name = re.sub(
229
- r"_a([^_a-z])", r"_a_\1", func_name
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" in param:
246
- # Resolve reference to actual parameter object
247
- ref_param = resolve_schema_reference(param["$ref"], full_schema)
248
- if ref_param:
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
- print(
252
- f"Warning: Could not resolve parameter reference: {param['$ref']}"
253
- )
232
+ logger.warning(f"Unresolved parameter reference: {ref}")
254
233
  else:
255
- resolved_parameters.append(param)
234
+ resolved_params.append(param)
256
235
 
257
- # Filter out header parameters from the resolved parameters
258
- parameters = [param for param in resolved_parameters if param.get("in") != "header"]
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", False)
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
- array_items_schema = None
299
-
300
- if has_body:
301
- for content_type, content in (
302
- operation["requestBody"].get("content", {}).items()
303
- ):
304
- if content_type.startswith("application/json") and "schema" in content:
305
- schema = content["schema"]
306
-
307
- # Resolve schema reference if present
308
- if "$ref" in schema:
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
- optional_args.append(f"{param_name}=None")
363
-
364
- # Handle array type request body differently
365
- request_body_params = []
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
- # For array request bodies, add a single parameter for the entire array
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
- required_args.append(array_param_name)
282
+ arg_defs["items"] = f"Annotated[list[Any], {desc!r}]"
379
283
  else:
380
- optional_args.append(f"{array_param_name}=None")
381
-
382
- # Remember this is an array param
383
- request_body_params = [array_param_name]
384
- elif request_body_properties:
385
- # For object request bodies, add individual properties as parameters
386
- for prop_name in request_body_properties:
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
- request_body_params.append(prop_name)
393
- if f"{prop_name}=None" not in optional_args:
394
- optional_args.append(f"{prop_name}=None")
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
- # Combine required and optional arguments
401
- args = required_args + optional_args
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
- signature = f" def {func_name}(self, {', '.join(args)}) -> {return_type}:"
407
-
408
- # Build method body
409
- body_lines = []
410
-
411
- # Validate required parameters including path parameters
412
- for param_name in required_args:
413
- body_lines.append(f" if {param_name} is None:")
414
- body_lines.append(
415
- f" raise ValueError(\"Missing required parameter '{param_name}'\")"
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 (handle array and object types differently)
338
+ # Build request body
419
339
  if has_body:
420
340
  if is_array_body:
421
- # For array request bodies, use the array parameter directly
422
- body_lines.append(" # Use items array directly as request body")
423
- body_lines.append(f" request_body = {request_body_params[0]}")
424
- elif request_body_properties:
425
- # For object request bodies, build the request body from individual parameters
426
-
427
- body_lines.append(" request_body = {")
428
-
429
- for prop_name in request_body_params:
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
- # Format URL directly with path parameters
440
- url_line = f' url = f"{{self.base_url}}{path}"'
441
- body_lines.append(url_line)
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
- # Query parameters
444
- query_params = [p for p in parameters if p["in"] == "query"]
357
+ # Build query parameters
445
358
  if query_params:
446
- query_params_items = ", ".join(
447
- [f"('{p['name']}', {p['name']})" for p in query_params]
448
- )
449
- body_lines.append(
450
- f" query_params = {{k: v for k, v in [{query_params_items}] if v is not None}}"
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
- body_lines.append(" query_params = {}")
366
+ implementation_lines.append("query_params = {}")
454
367
 
455
- # Make HTTP request using the proper method
456
- method_lower = method.lower()
457
-
458
- # Determine what to use as the request body argument
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
- body_lines.append(
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
- body_lines.append(" response.raise_for_status()")
489
- body_lines.append(" return response.json()")
490
-
491
- method_code = signature + "\n" + "\n".join(body_lines)
492
- return method_code, func_name
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]