universal-mcp 0.1.14__py3-none-any.whl → 0.1.15__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 (34) hide show
  1. universal_mcp/analytics.py +7 -1
  2. universal_mcp/applications/README.md +122 -0
  3. universal_mcp/applications/__init__.py +48 -46
  4. universal_mcp/applications/application.py +249 -40
  5. universal_mcp/cli.py +49 -49
  6. universal_mcp/config.py +95 -22
  7. universal_mcp/exceptions.py +8 -0
  8. universal_mcp/integrations/integration.py +18 -2
  9. universal_mcp/logger.py +59 -8
  10. universal_mcp/servers/__init__.py +2 -2
  11. universal_mcp/stores/store.py +2 -12
  12. universal_mcp/tools/__init__.py +14 -2
  13. universal_mcp/tools/adapters.py +25 -0
  14. universal_mcp/tools/func_metadata.py +12 -2
  15. universal_mcp/tools/manager.py +236 -0
  16. universal_mcp/tools/tools.py +5 -249
  17. universal_mcp/utils/common.py +33 -0
  18. universal_mcp/utils/openapi/__inti__.py +0 -0
  19. universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +1 -1
  20. universal_mcp/utils/openapi/openapi.py +930 -0
  21. universal_mcp/utils/openapi/preprocessor.py +1223 -0
  22. universal_mcp/utils/{readme.py → openapi/readme.py} +21 -31
  23. universal_mcp/utils/templates/README.md.j2 +17 -0
  24. {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15.dist-info}/METADATA +6 -3
  25. universal_mcp-0.1.15.dist-info/RECORD +44 -0
  26. universal_mcp-0.1.15.dist-info/licenses/LICENSE +21 -0
  27. universal_mcp/templates/README.md.j2 +0 -93
  28. universal_mcp/utils/dump_app_tools.py +0 -78
  29. universal_mcp/utils/openapi.py +0 -697
  30. universal_mcp-0.1.14.dist-info/RECORD +0 -39
  31. /universal_mcp/utils/{docgen.py → openapi/docgen.py} +0 -0
  32. /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
  33. {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15.dist-info}/WHEEL +0 -0
  34. {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15.dist-info}/entry_points.txt +0 -0
@@ -1,697 +0,0 @@
1
- import json
2
- import re
3
- from functools import cache
4
- from pathlib import Path
5
- from typing import Any, Literal
6
-
7
- import yaml
8
- from pydantic import BaseModel
9
-
10
-
11
- class Parameters(BaseModel):
12
- name: str
13
- identifier: str
14
- description: str = ""
15
- type: str = "string"
16
- where: Literal["path", "query", "header", "body"]
17
- required: bool
18
- example: str | None = None
19
-
20
- def __str__(self):
21
- return f"{self.name}: ({self.type})"
22
-
23
-
24
- class Method(BaseModel):
25
- name: str
26
- summary: str
27
- tags: list[str]
28
- path: str
29
- method: str
30
- path_params: list[Parameters]
31
- query_params: list[Parameters]
32
- body_params: list[Parameters]
33
- return_type: str
34
-
35
- def deduplicate_params(self):
36
- """
37
- Deduplicate parameters by name.
38
- Sometimes the same parameter is defined in multiple places, we only want to include it once.
39
- """
40
- # TODO: Implement this
41
- pass
42
-
43
- def render(self, template_dir: str, template_name: str = "method.jinja2") -> str:
44
- """
45
- Render this Method instance into source code using a Jinja2 template.
46
-
47
- Args:
48
- template_dir (str): Directory where the Jinja2 templates are located.
49
- template_name (str): Filename of the method template.
50
-
51
- Returns:
52
- str: The rendered method source code.
53
- """
54
- from jinja2 import Environment, FileSystemLoader
55
-
56
- env = Environment(
57
- loader=FileSystemLoader(template_dir),
58
- trim_blocks=True,
59
- lstrip_blocks=True,
60
- )
61
- template = env.get_template(template_name)
62
- return template.render(method=self)
63
-
64
-
65
- def convert_to_snake_case(identifier: str) -> str:
66
- """
67
- Convert a camelCase or PascalCase identifier to snake_case.
68
-
69
- Args:
70
- identifier (str): The string to convert
71
-
72
- Returns:
73
- str: The converted snake_case string
74
- """
75
- if not identifier:
76
- return identifier
77
- # Add underscore between lowercase and uppercase letters
78
- result = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", identifier)
79
- # Convert to lowercase
80
- return result.lower()
81
-
82
-
83
- @cache
84
- def _resolve_schema_reference(reference, schema):
85
- """
86
- Resolve a JSON schema reference to its target schema.
87
-
88
- Args:
89
- reference (str): The reference string (e.g., '#/components/schemas/User')
90
- schema (dict): The complete OpenAPI schema that contains the reference
91
-
92
- Returns:
93
- dict: The resolved schema, or None if not found
94
- """
95
- if not reference.startswith("#/"):
96
- return None
97
-
98
- # Split the reference path and navigate through the schema
99
- parts = reference[2:].split("/")
100
- current = schema
101
-
102
- for part in parts:
103
- if part in current:
104
- current = current[part]
105
- else:
106
- return None
107
-
108
- return current
109
-
110
-
111
- def _resolve_references(schema: dict[str, Any]):
112
- """
113
- Recursively walk the OpenAPI schema and inline all JSON Schema $ref references.
114
- """
115
-
116
- def _resolve(node):
117
- if isinstance(node, dict):
118
- # If this dict is a reference, replace it with the resolved schema
119
- if "$ref" in node:
120
- ref = node["$ref"]
121
- resolved = _resolve_schema_reference(ref, schema)
122
- # If resolution fails, leave the ref dict as-is
123
- return _resolve(resolved) if resolved is not None else node
124
- # Otherwise, recurse into each key/value
125
- return {key: _resolve(value) for key, value in node.items()}
126
- elif isinstance(node, list):
127
- # Recurse into list elements
128
- return [_resolve(item) for item in node]
129
- # Primitive value, return as-is
130
- return node
131
-
132
- return _resolve(schema)
133
-
134
-
135
- def _load_and_resolve_references(path: Path):
136
- # Load the schema
137
- type = "yaml" if path.suffix == ".yaml" else "json"
138
- with open(path) as f:
139
- schema = yaml.safe_load(f) if type == "yaml" else json.load(f)
140
- # Resolve references
141
- return _resolve_references(schema)
142
-
143
-
144
- def _determine_return_type(operation: dict[str, Any]) -> str:
145
- """
146
- Determine the return type from the response schema.
147
-
148
- Args:
149
- operation (dict): The operation details from the schema.
150
-
151
- Returns:
152
- str: The appropriate return type annotation (list[Any], dict[str, Any], or Any)
153
- """
154
- responses = operation.get("responses", {})
155
- # Find successful response (2XX)
156
- success_response = None
157
- for code in responses:
158
- if code.startswith("2"):
159
- success_response = responses[code]
160
- break
161
-
162
- if not success_response:
163
- return "Any" # Default to Any if no success response
164
-
165
- # Check if there's content with schema
166
- if "content" in success_response:
167
- for content_type, content_info in success_response["content"].items():
168
- if content_type.startswith("application/json") and "schema" in content_info:
169
- schema = content_info["schema"]
170
-
171
- # Only determine if it's a list, dict, or unknown (Any)
172
- if schema.get("type") == "array":
173
- return "list[Any]"
174
- elif schema.get("type") == "object" or "$ref" in schema:
175
- return "dict[str, Any]"
176
-
177
- # Default to Any if unable to determine
178
- return "Any"
179
-
180
-
181
- def _determine_function_name(operation: dict[str, Any], path: str, method: str) -> str:
182
- """
183
- Determine the function name from the operation.
184
- """
185
- # Determine function name
186
- if "operationId" in operation:
187
- raw_name = operation["operationId"]
188
- cleaned_name = raw_name.replace(".", "_").replace("-", "_")
189
- func_name = convert_to_snake_case(cleaned_name)
190
- else:
191
- # Generate name from path and method
192
- path_parts = path.strip("/").split("/")
193
- name_parts = [method]
194
- for part in path_parts:
195
- if part.startswith("{") and part.endswith("}"):
196
- name_parts.append("by_" + part[1:-1])
197
- else:
198
- name_parts.append(part)
199
- func_name = "_".join(name_parts).replace("-", "_").lower()
200
-
201
- # Only fix isolated 'a' and 'an' as articles, not when they're part of words
202
- func_name = re.sub(
203
- r"_a([^_a-z])", r"_a_\1", func_name
204
- ) # Fix for patterns like retrieve_ablock -> retrieve_a_block
205
- func_name = re.sub(
206
- r"_a$", r"_a", func_name
207
- ) # Don't change if 'a' is at the end of the name
208
- func_name = re.sub(
209
- r"_an([^_a-z])", r"_an_\1", func_name
210
- ) # Fix for patterns like create_anitem -> create_an_item
211
- func_name = re.sub(
212
- r"_an$", r"_an", func_name
213
- ) # Don't change if 'an' is at the end of the name
214
- return func_name
215
-
216
-
217
- def _generate_path_params(path: str) -> list[Parameters]:
218
- path_params_in_url = re.findall(r"{([^}]+)}", path)
219
- parameters = []
220
- for param in path_params_in_url:
221
- try:
222
- parameters.append(
223
- Parameters(
224
- name=param.replace("-", "_"),
225
- identifier=param,
226
- description=param,
227
- type="string",
228
- where="path",
229
- required=True,
230
- )
231
- )
232
- except Exception as e:
233
- print(f"Error generating path parameters {param}: {e}")
234
- raise e
235
- return parameters
236
-
237
-
238
- def _generate_url(path: str, path_params: list[Parameters]):
239
- formatted_path = path
240
- for param in path_params:
241
- formatted_path = formatted_path.replace(
242
- f"{{{param.identifier}}}", f"{{{param.name}}}"
243
- )
244
- return formatted_path
245
-
246
-
247
- def _generate_query_params(operation: dict[str, Any]) -> list[Parameters]:
248
- query_params = []
249
- for param in operation.get("parameters", []):
250
- name = param.get("name")
251
- description = param.get("description", "")
252
- type = param.get("type")
253
- where = param.get("in")
254
- required = param.get("required")
255
- if where == "query":
256
- parameter = Parameters(
257
- name=name.replace("-", "_"),
258
- identifier=name,
259
- description=description,
260
- type=type,
261
- where=where,
262
- required=required,
263
- )
264
- query_params.append(parameter)
265
- return query_params
266
-
267
-
268
- def _generate_body_params(operation: dict[str, Any]) -> list[Parameters]:
269
- body_params = []
270
- request_body = operation.get("requestBody", {})
271
- required = request_body.get("required", False)
272
- content = request_body.get("content", {})
273
- json_content = content.get("application/json", {})
274
- schema = json_content.get("schema", {})
275
- properties = schema.get("properties", {})
276
- for param in properties:
277
- body_params.append(
278
- Parameters(
279
- name=param,
280
- identifier=param,
281
- description=param,
282
- type="string",
283
- where="body",
284
- required=required,
285
- )
286
- )
287
- return body_params
288
-
289
-
290
- def _generate_method_code(path, method, operation):
291
- """
292
- Generate the code for a single API method.
293
-
294
- Args:
295
- path (str): The API path (e.g., '/users/{user_id}').
296
- method (str): The HTTP method (e.g., 'get').
297
- operation (dict): The operation details from the schema.
298
- full_schema (dict): The complete OpenAPI schema, used for reference resolution.
299
- tool_name (str, optional): The name of the tool/app to prefix the function name with.
300
-
301
- Returns:
302
- tuple: (method_code, func_name) - The Python code for the method and its name.
303
- """
304
-
305
- func_name = _determine_function_name(operation, path, method)
306
- operation.get("summary", "")
307
- operation.get("tags", [])
308
- # Extract path parameters from the URL path
309
- path_params = _generate_path_params(path)
310
- query_params = _generate_query_params(operation)
311
- _generate_body_params(operation)
312
- return_type = _determine_return_type(operation)
313
- # gen_method = Method(name=func_name, summary=summary, tags=tags, path=path, method=method, path_params=path_params, query_params=query_params, body_params=body_params, return_type=return_type)
314
- # logger.info(f"Generated method: {gen_method.model_dump()}")
315
- # return method.render(template_dir="templates", template_name="method.jinja2")
316
-
317
- has_body = "requestBody" in operation
318
- body_required = has_body and operation["requestBody"].get("required", False)
319
-
320
- # Check if the requestBody has actual content or is empty
321
- has_empty_body = False
322
- if has_body:
323
- request_body_content = operation["requestBody"].get("content", {})
324
- if not request_body_content or all(
325
- not content for content_type, content in request_body_content.items()
326
- ):
327
- has_empty_body = True
328
- else:
329
- # Handle empty properties with additionalProperties:true
330
- for content_type, content in request_body_content.items():
331
- if content_type.startswith("application/json") and "schema" in content:
332
- schema = content["schema"]
333
-
334
- # Check if properties is empty and additionalProperties is true
335
- if (
336
- schema.get("type") == "object"
337
- and schema.get("additionalProperties", False) is True
338
- ):
339
- properties = schema.get("properties", {})
340
- if not properties or len(properties) == 0:
341
- has_empty_body = True
342
-
343
- # Extract request body schema properties and required fields
344
- required_fields = []
345
- request_body_properties = {}
346
- is_array_body = False
347
-
348
- if has_body:
349
- for content_type, content in (
350
- operation["requestBody"].get("content", {}).items()
351
- ):
352
- if content_type.startswith("application/json") and "schema" in content:
353
- schema = content["schema"]
354
-
355
- # Check if the schema is an array type
356
- if schema.get("type") == "array":
357
- is_array_body = True
358
- schema.get("items", {})
359
-
360
- else:
361
- # Extract required fields from schema
362
- if "required" in schema:
363
- required_fields = schema["required"]
364
- # Extract properties from schema
365
- if "properties" in schema:
366
- request_body_properties = schema["properties"]
367
-
368
- # Handle schemas with empty properties but additionalProperties: true
369
- # by treating them similar to empty bodies
370
- if (
371
- not request_body_properties or len(request_body_properties) == 0
372
- ) and schema.get("additionalProperties") is True:
373
- has_empty_body = True
374
-
375
- # Build function arguments
376
- required_args = []
377
- optional_args = []
378
-
379
- # Add path parameters
380
- for param in path_params:
381
- if param.name not in required_args:
382
- required_args.append(param.name)
383
-
384
- for param in query_params:
385
- param_name = param["name"]
386
- # Handle parameters with square brackets and hyphens by converting to valid Python identifiers
387
- param_identifier = (
388
- param_name.replace("[", "_").replace("]", "").replace("-", "_")
389
- )
390
- if param_identifier not in required_args and param_identifier not in [
391
- p.split("=")[0] for p in optional_args
392
- ]:
393
- if param.get("required", False):
394
- required_args.append(param_identifier)
395
- else:
396
- optional_args.append(f"{param_identifier}=None")
397
-
398
- # Handle array type request body differently
399
- request_body_params = []
400
- if has_body:
401
- if is_array_body:
402
- # For array request bodies, add a single parameter for the entire array
403
- array_param_name = "items"
404
- # Try to get a better name from the operation or path
405
- if func_name.endswith("_list_input"):
406
- array_param_name = func_name.replace("_list_input", "")
407
- elif "List" in func_name:
408
- array_param_name = func_name.split("List")[0].lower() + "_list"
409
-
410
- # Make the array parameter required if the request body is required
411
- if body_required:
412
- required_args.append(array_param_name)
413
- else:
414
- optional_args.append(f"{array_param_name}=None")
415
-
416
- # Remember this is an array param
417
- request_body_params = [array_param_name]
418
- elif request_body_properties:
419
- # For object request bodies, add individual properties as parameters
420
- for prop_name in request_body_properties:
421
- if prop_name in required_fields:
422
- request_body_params.append(prop_name)
423
- if prop_name not in required_args:
424
- required_args.append(prop_name)
425
- else:
426
- request_body_params.append(prop_name)
427
- if f"{prop_name}=None" not in optional_args:
428
- optional_args.append(f"{prop_name}=None")
429
-
430
- # If request body is present but empty (content: {}), add a generic request_body parameter
431
- if has_empty_body and "request_body=None" not in optional_args:
432
- optional_args.append("request_body=None")
433
-
434
- # Combine required and optional arguments
435
- args = required_args + optional_args
436
-
437
- # Determine return type
438
- return_type = _determine_return_type(operation)
439
- if args:
440
- signature = f" def {func_name}(self, {', '.join(args)}) -> {return_type}:"
441
- else:
442
- signature = f" def {func_name}(self) -> {return_type}:"
443
-
444
- # Build method body
445
- body_lines = []
446
-
447
- for param in path_params:
448
- body_lines.append(f" if {param.name} is None:")
449
- body_lines.append(
450
- f" raise ValueError(\"Missing required parameter '{param.identifier}'\")" # Use original name in error
451
- )
452
-
453
- # Build request body (handle array and object types differently)
454
- if has_body:
455
- if is_array_body:
456
- # For array request bodies, use the array parameter directly
457
- body_lines.append(" # Use items array directly as request body")
458
- body_lines.append(f" request_body = {request_body_params[0]}")
459
- elif request_body_properties:
460
- # For object request bodies, build the request body from individual parameters
461
-
462
- body_lines.append(" request_body = {")
463
-
464
- for prop_name in request_body_params:
465
- # Only include non-None values in the request body
466
- body_lines.append(f" '{prop_name}': {prop_name},")
467
-
468
- body_lines.append(" }")
469
-
470
- body_lines.append(
471
- " request_body = {k: v for k, v in request_body.items() if v is not None}"
472
- )
473
-
474
- # Format URL directly with path parameters
475
- url = _generate_url(path, path_params)
476
- url_line = f' url = f"{{self.base_url}}{url}"'
477
- body_lines.append(url_line)
478
-
479
- # Build query parameters, handling square brackets in parameter names
480
- if query_params:
481
- query_params_items = []
482
- for param in query_params:
483
- param_name = param.name
484
- param_identifier = (
485
- param_name.replace("[", "_").replace("]", "").replace("-", "_")
486
- )
487
- query_params_items.append(f"('{param_name}', {param_identifier})")
488
- body_lines.append(
489
- f" query_params = {{k: v for k, v in [{', '.join(query_params_items)}] if v is not None}}"
490
- )
491
- else:
492
- body_lines.append(" query_params = {}")
493
-
494
- # Make HTTP request using the proper method
495
- method_lower = method.lower()
496
-
497
- # Determine what to use as the request body argument
498
- if has_empty_body:
499
- request_body_arg = "request_body"
500
- elif not has_body:
501
- request_body_arg = "{}"
502
- else:
503
- request_body_arg = "request_body"
504
-
505
- if method_lower == "get":
506
- body_lines.append(" response = self._get(url, params=query_params)")
507
- elif method_lower == "post":
508
- body_lines.append(
509
- f" response = self._post(url, data={request_body_arg}, params=query_params)"
510
- )
511
- elif method_lower == "put":
512
- body_lines.append(
513
- f" response = self._put(url, data={request_body_arg}, params=query_params)"
514
- )
515
- elif method_lower == "patch":
516
- body_lines.append(
517
- f" response = self._patch(url, data={request_body_arg}, params=query_params)"
518
- )
519
- elif method_lower == "delete":
520
- body_lines.append(" response = self._delete(url, params=query_params)")
521
- else:
522
- body_lines.append(
523
- f" response = self._{method_lower}(url, data={request_body_arg}, params=query_params)"
524
- )
525
-
526
- # Handle response
527
- body_lines.append(" response.raise_for_status()")
528
- body_lines.append(" return response.json()")
529
-
530
- method_code = signature + "\n" + "\n".join(body_lines)
531
- return method_code, func_name
532
-
533
-
534
- def load_schema(path: Path):
535
- return _load_and_resolve_references(path)
536
-
537
-
538
- def generate_api_client(schema, class_name: str | None = None):
539
- """
540
- Generate a Python API client class from an OpenAPI schema.
541
-
542
- Args:
543
- schema (dict): The OpenAPI schema as a dictionary.
544
-
545
- Returns:
546
- str: A string containing the Python code for the API client class.
547
- """
548
- methods = []
549
- method_names = []
550
-
551
- # Extract API info for naming and base URL
552
- info = schema.get("info", {})
553
- api_title = info.get("title", "API")
554
-
555
- # Get base URL from servers array if available
556
- base_url = ""
557
- servers = schema.get("servers", [])
558
- if servers and isinstance(servers, list) and "url" in servers[0]:
559
- base_url = servers[0]["url"].rstrip("/")
560
-
561
- # Create a clean class name from API title
562
- if api_title:
563
- # Convert API title to a clean class name
564
- if class_name:
565
- clean_name = (
566
- class_name.capitalize()[:-3]
567
- if class_name.endswith("App")
568
- else class_name.capitalize()
569
- )
570
- else:
571
- base_name = "".join(word.capitalize() for word in api_title.split())
572
- clean_name = "".join(c for c in base_name if c.isalnum())
573
- class_name = f"{clean_name}App"
574
-
575
- # Extract tool name - remove spaces and convert to lowercase
576
- tool_name = api_title.lower()
577
-
578
- # Remove version numbers (like 3.0, v1, etc.)
579
- tool_name = re.sub(r"\s*v?\d+(\.\d+)*", "", tool_name)
580
-
581
- # Remove common words that aren't needed
582
- common_words = ["api", "openapi", "open", "swagger", "spec", "specification"]
583
- for word in common_words:
584
- tool_name = tool_name.replace(word, "")
585
-
586
- # Remove spaces, hyphens, underscores
587
- tool_name = tool_name.replace(" ", "").replace("-", "").replace("_", "")
588
-
589
- # Remove any non-alphanumeric characters
590
- tool_name = "".join(c for c in tool_name if c.isalnum())
591
-
592
- # If empty (after cleaning), use generic name
593
- if not tool_name:
594
- tool_name = "api"
595
- else:
596
- class_name = "APIClient"
597
- tool_name = "api"
598
-
599
- # Iterate over paths and their operations
600
- for path, path_info in schema.get("paths", {}).items():
601
- for method in path_info:
602
- if method in ["get", "post", "put", "delete", "patch", "options", "head"]:
603
- operation = path_info[method]
604
- method_code, func_name = _generate_method_code(path, method, operation)
605
- methods.append(method_code)
606
- method_names.append(func_name)
607
-
608
- # Generate list_tools method with all the function names
609
- tools_list = ",\n ".join([f"self.{name}" for name in method_names])
610
- list_tools_method = f""" def list_tools(self):
611
- return [
612
- {tools_list}
613
- ]"""
614
-
615
- # Generate class imports
616
- imports = [
617
- "from typing import Any",
618
- "from universal_mcp.applications import APIApplication",
619
- "from universal_mcp.integrations import Integration",
620
- ]
621
-
622
- # Construct the class code
623
- class_code = (
624
- "\n".join(imports) + "\n\n"
625
- f"class {class_name}(APIApplication):\n"
626
- f" def __init__(self, integration: Integration = None, **kwargs) -> None:\n"
627
- f" super().__init__(name='{class_name.lower()}', integration=integration, **kwargs)\n"
628
- f' self.base_url = "{base_url}"\n\n'
629
- + "\n\n".join(methods)
630
- + "\n\n"
631
- + list_tools_method
632
- + "\n"
633
- )
634
- return class_code
635
-
636
-
637
- # Example usage
638
- if __name__ == "__main__":
639
- # Sample OpenAPI schema
640
- schema = {
641
- "paths": {
642
- "/users": {
643
- "get": {
644
- "summary": "Get a list of users",
645
- "parameters": [
646
- {
647
- "name": "limit",
648
- "in": "query",
649
- "required": False,
650
- "schema": {"type": "integer"},
651
- }
652
- ],
653
- "responses": {
654
- "200": {
655
- "description": "A list of users",
656
- "content": {
657
- "application/json": {"schema": {"type": "array"}}
658
- },
659
- }
660
- },
661
- },
662
- "post": {
663
- "summary": "Create a user",
664
- "requestBody": {
665
- "required": True,
666
- "content": {
667
- "application/json": {
668
- "schema": {
669
- "type": "object",
670
- "properties": {"name": {"type": "string"}},
671
- }
672
- }
673
- },
674
- },
675
- "responses": {"201": {"description": "User created"}},
676
- },
677
- },
678
- "/users/{user_id}": {
679
- "get": {
680
- "summary": "Get a user by ID",
681
- "parameters": [
682
- {
683
- "name": "user_id",
684
- "in": "path",
685
- "required": True,
686
- "schema": {"type": "string"},
687
- }
688
- ],
689
- "responses": {"200": {"description": "User details"}},
690
- }
691
- },
692
- }
693
- }
694
-
695
- schema = load_schema("openapi.yaml")
696
- code = generate_api_client(schema)
697
- print(code)