universal-mcp 0.1.8rc1__py3-none-any.whl → 0.1.8rc2__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/application.py +6 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/app.py +954 -0
- universal_mcp/applications/e2b/app.py +18 -12
- universal_mcp/applications/firecrawl/app.py +28 -1
- universal_mcp/applications/github/app.py +150 -107
- universal_mcp/applications/google_calendar/app.py +72 -137
- universal_mcp/applications/google_docs/app.py +35 -15
- universal_mcp/applications/google_drive/app.py +84 -55
- universal_mcp/applications/google_mail/app.py +143 -53
- universal_mcp/applications/google_sheet/app.py +61 -38
- universal_mcp/applications/markitdown/app.py +12 -11
- universal_mcp/applications/notion/app.py +199 -89
- universal_mcp/applications/perplexity/app.py +17 -15
- universal_mcp/applications/reddit/app.py +110 -101
- universal_mcp/applications/resend/app.py +14 -7
- universal_mcp/applications/serpapi/app.py +13 -6
- universal_mcp/applications/tavily/app.py +13 -10
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1044 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +986 -0
- universal_mcp/applications/zenquotes/app.py +13 -3
- universal_mcp/exceptions.py +8 -2
- universal_mcp/integrations/__init__.py +15 -1
- universal_mcp/integrations/integration.py +132 -27
- universal_mcp/servers/__init__.py +6 -15
- universal_mcp/servers/server.py +209 -153
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -42
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +40 -0
- universal_mcp/tools/func_metadata.py +214 -0
- universal_mcp/tools/tools.py +285 -0
- universal_mcp/utils/docgen.py +277 -123
- universal_mcp/utils/docstring_parser.py +156 -0
- universal_mcp/utils/openapi.py +149 -40
- {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc2.dist-info}/METADATA +7 -3
- universal_mcp-0.1.8rc2.dist-info/RECORD +71 -0
- universal_mcp-0.1.8rc1.dist-info/RECORD +0 -58
- /universal_mcp/{utils/bridge.py → applications/calendly/__init__.py} +0 -0
- {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc2.dist-info}/entry_points.txt +0 -0
universal_mcp/stores/store.py
CHANGED
@@ -1,63 +1,84 @@
|
|
1
1
|
import os
|
2
2
|
from abc import ABC, abstractmethod
|
3
|
+
from typing import Any
|
3
4
|
|
4
5
|
import keyring
|
5
6
|
from loguru import logger
|
6
7
|
|
7
8
|
|
8
|
-
class
|
9
|
+
class StoreError(Exception):
|
10
|
+
"""Base exception class for store-related errors."""
|
11
|
+
pass
|
12
|
+
|
13
|
+
|
14
|
+
class KeyNotFoundError(StoreError):
|
15
|
+
"""Exception raised when a key is not found in the store."""
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
class BaseStore(ABC):
|
9
20
|
"""
|
10
21
|
Abstract base class defining the interface for credential stores.
|
11
22
|
All credential stores must implement get, set and delete methods.
|
12
23
|
"""
|
13
24
|
|
14
25
|
@abstractmethod
|
15
|
-
def get(self, key: str):
|
26
|
+
def get(self, key: str) -> Any:
|
16
27
|
"""
|
17
28
|
Retrieve a value from the store by key.
|
18
29
|
|
19
30
|
Args:
|
20
31
|
key (str): The key to look up
|
21
|
-
|
32
|
+
|
22
33
|
Returns:
|
23
|
-
The stored value
|
34
|
+
Any: The stored value
|
35
|
+
|
36
|
+
Raises:
|
37
|
+
KeyNotFoundError: If the key is not found in the store
|
38
|
+
StoreError: If there is an error accessing the store
|
24
39
|
"""
|
25
40
|
pass
|
26
41
|
|
27
42
|
@abstractmethod
|
28
|
-
def set(self, key: str, value: str):
|
43
|
+
def set(self, key: str, value: str) -> None:
|
29
44
|
"""
|
30
45
|
Store a value in the store with the given key.
|
31
46
|
|
32
47
|
Args:
|
33
48
|
key (str): The key to store the value under
|
34
49
|
value (str): The value to store
|
50
|
+
|
51
|
+
Raises:
|
52
|
+
StoreError: If there is an error storing the value
|
35
53
|
"""
|
36
54
|
pass
|
37
55
|
|
38
56
|
@abstractmethod
|
39
|
-
def delete(self, key: str):
|
57
|
+
def delete(self, key: str) -> None:
|
40
58
|
"""
|
41
59
|
Delete a value from the store by key.
|
42
60
|
|
43
61
|
Args:
|
44
62
|
key (str): The key to delete
|
63
|
+
|
64
|
+
Raises:
|
65
|
+
KeyNotFoundError: If the key is not found in the store
|
66
|
+
StoreError: If there is an error deleting the value
|
45
67
|
"""
|
46
68
|
pass
|
47
69
|
|
48
70
|
|
49
|
-
class MemoryStore:
|
71
|
+
class MemoryStore(BaseStore):
|
50
72
|
"""
|
51
|
-
|
52
|
-
|
53
|
-
Ideally should be a key value store that keeps data in memory.
|
73
|
+
In-memory credential store implementation.
|
74
|
+
Stores credentials in a dictionary that persists only for the duration of the program execution.
|
54
75
|
"""
|
55
76
|
|
56
77
|
def __init__(self):
|
57
78
|
"""Initialize an empty dictionary to store the data."""
|
58
|
-
self.data = {}
|
79
|
+
self.data: dict[str, str] = {}
|
59
80
|
|
60
|
-
def get(self, key: str):
|
81
|
+
def get(self, key: str) -> Any:
|
61
82
|
"""
|
62
83
|
Retrieve a value from the in-memory store by key.
|
63
84
|
|
@@ -65,11 +86,16 @@ class MemoryStore:
|
|
65
86
|
key (str): The key to look up
|
66
87
|
|
67
88
|
Returns:
|
68
|
-
The stored value
|
89
|
+
Any: The stored value
|
90
|
+
|
91
|
+
Raises:
|
92
|
+
KeyNotFoundError: If the key is not found in the store
|
69
93
|
"""
|
70
|
-
|
94
|
+
if key not in self.data:
|
95
|
+
raise KeyNotFoundError(f"Key '{key}' not found in memory store")
|
96
|
+
return self.data[key]
|
71
97
|
|
72
|
-
def set(self, key: str, value: str):
|
98
|
+
def set(self, key: str, value: str) -> None:
|
73
99
|
"""
|
74
100
|
Store a value in the in-memory store with the given key.
|
75
101
|
|
@@ -79,27 +105,28 @@ class MemoryStore:
|
|
79
105
|
"""
|
80
106
|
self.data[key] = value
|
81
107
|
|
82
|
-
def delete(self, key: str):
|
108
|
+
def delete(self, key: str) -> None:
|
83
109
|
"""
|
84
110
|
Delete a value from the in-memory store by key.
|
85
111
|
|
86
112
|
Args:
|
87
113
|
key (str): The key to delete
|
114
|
+
|
115
|
+
Raises:
|
116
|
+
KeyNotFoundError: If the key is not found in the store
|
88
117
|
"""
|
118
|
+
if key not in self.data:
|
119
|
+
raise KeyNotFoundError(f"Key '{key}' not found in memory store")
|
89
120
|
del self.data[key]
|
90
121
|
|
91
122
|
|
92
|
-
class EnvironmentStore(
|
123
|
+
class EnvironmentStore(BaseStore):
|
93
124
|
"""
|
94
|
-
|
95
|
-
|
125
|
+
Environment variable-based credential store implementation.
|
126
|
+
Uses OS environment variables to store and retrieve credentials.
|
96
127
|
"""
|
97
128
|
|
98
|
-
def
|
99
|
-
"""Initialize the environment store."""
|
100
|
-
pass
|
101
|
-
|
102
|
-
def get(self, key: str):
|
129
|
+
def get(self, key: str) -> Any:
|
103
130
|
"""
|
104
131
|
Retrieve a value from environment variables by key.
|
105
132
|
|
@@ -107,11 +134,17 @@ class EnvironmentStore(Store):
|
|
107
134
|
key (str): The environment variable name to look up
|
108
135
|
|
109
136
|
Returns:
|
110
|
-
|
137
|
+
Any: The stored value
|
138
|
+
|
139
|
+
Raises:
|
140
|
+
KeyNotFoundError: If the environment variable is not found
|
111
141
|
"""
|
112
|
-
|
142
|
+
value = os.getenv(key)
|
143
|
+
if value is None:
|
144
|
+
raise KeyNotFoundError(f"Environment variable '{key}' not found")
|
145
|
+
return value
|
113
146
|
|
114
|
-
def set(self, key: str, value: str):
|
147
|
+
def set(self, key: str, value: str) -> None:
|
115
148
|
"""
|
116
149
|
Set an environment variable.
|
117
150
|
|
@@ -121,20 +154,25 @@ class EnvironmentStore(Store):
|
|
121
154
|
"""
|
122
155
|
os.environ[key] = value
|
123
156
|
|
124
|
-
def delete(self, key: str):
|
157
|
+
def delete(self, key: str) -> None:
|
125
158
|
"""
|
126
159
|
Delete an environment variable.
|
127
160
|
|
128
161
|
Args:
|
129
162
|
key (str): The environment variable name to delete
|
163
|
+
|
164
|
+
Raises:
|
165
|
+
KeyNotFoundError: If the environment variable is not found
|
130
166
|
"""
|
167
|
+
if key not in os.environ:
|
168
|
+
raise KeyNotFoundError(f"Environment variable '{key}' not found")
|
131
169
|
del os.environ[key]
|
132
170
|
|
133
171
|
|
134
|
-
class KeyringStore(
|
172
|
+
class KeyringStore(BaseStore):
|
135
173
|
"""
|
136
|
-
|
137
|
-
|
174
|
+
System keyring-based credential store implementation.
|
175
|
+
Uses the system's secure credential storage facility via the keyring library.
|
138
176
|
"""
|
139
177
|
|
140
178
|
def __init__(self, app_name: str = "universal_mcp"):
|
@@ -146,7 +184,7 @@ class KeyringStore(Store):
|
|
146
184
|
"""
|
147
185
|
self.app_name = app_name
|
148
186
|
|
149
|
-
def get(self, key: str):
|
187
|
+
def get(self, key: str) -> Any:
|
150
188
|
"""
|
151
189
|
Retrieve a password from the system keyring.
|
152
190
|
|
@@ -154,28 +192,51 @@ class KeyringStore(Store):
|
|
154
192
|
key (str): The key to look up
|
155
193
|
|
156
194
|
Returns:
|
157
|
-
The stored
|
195
|
+
Any: The stored value
|
196
|
+
|
197
|
+
Raises:
|
198
|
+
KeyNotFoundError: If the key is not found in the keyring
|
199
|
+
StoreError: If there is an error accessing the keyring
|
158
200
|
"""
|
159
|
-
|
160
|
-
|
201
|
+
try:
|
202
|
+
logger.info(f"Getting password for {key} from keyring")
|
203
|
+
value = keyring.get_password(self.app_name, key)
|
204
|
+
if value is None:
|
205
|
+
raise KeyNotFoundError(f"Key '{key}' not found in keyring")
|
206
|
+
return value
|
207
|
+
except Exception as e:
|
208
|
+
raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
|
161
209
|
|
162
|
-
def set(self, key: str, value: str):
|
210
|
+
def set(self, key: str, value: str) -> None:
|
163
211
|
"""
|
164
212
|
Store a password in the system keyring.
|
165
213
|
|
166
214
|
Args:
|
167
215
|
key (str): The key to store the password under
|
168
216
|
value (str): The password to store
|
169
|
-
"""
|
170
|
-
logger.info(f"Setting password for {key} in keyring")
|
171
|
-
keyring.set_password(self.app_name, key, value)
|
172
217
|
|
173
|
-
|
218
|
+
Raises:
|
219
|
+
StoreError: If there is an error storing in the keyring
|
220
|
+
"""
|
221
|
+
try:
|
222
|
+
logger.info(f"Setting password for {key} in keyring")
|
223
|
+
keyring.set_password(self.app_name, key, value)
|
224
|
+
except Exception as e:
|
225
|
+
raise StoreError(f"Error storing in keyring: {str(e)}") from e
|
226
|
+
|
227
|
+
def delete(self, key: str) -> None:
|
174
228
|
"""
|
175
229
|
Delete a password from the system keyring.
|
176
230
|
|
177
231
|
Args:
|
178
232
|
key (str): The key to delete
|
233
|
+
|
234
|
+
Raises:
|
235
|
+
KeyNotFoundError: If the key is not found in the keyring
|
236
|
+
StoreError: If there is an error deleting from the keyring
|
179
237
|
"""
|
180
|
-
|
181
|
-
|
238
|
+
try:
|
239
|
+
logger.info(f"Deleting password for {key} from keyring")
|
240
|
+
keyring.delete_password(self.app_name, key)
|
241
|
+
except Exception as e:
|
242
|
+
raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
|
@@ -0,0 +1,40 @@
|
|
1
|
+
from universal_mcp.tools.tools import Tool
|
2
|
+
|
3
|
+
|
4
|
+
def convert_tool_to_mcp_tool(
|
5
|
+
tool: Tool,
|
6
|
+
):
|
7
|
+
from mcp.server.fastmcp.server import MCPTool
|
8
|
+
return MCPTool(
|
9
|
+
name=tool.name,
|
10
|
+
description=tool.description or "",
|
11
|
+
inputSchema=tool.parameters,
|
12
|
+
)
|
13
|
+
|
14
|
+
def convert_tool_to_langchain_tool(
|
15
|
+
tool: Tool,
|
16
|
+
):
|
17
|
+
from langchain_core.tools import StructuredTool
|
18
|
+
"""Convert an tool to a LangChain tool.
|
19
|
+
|
20
|
+
NOTE: this tool can be executed only in a context of an active MCP client session.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
tool: Tool to convert
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
a LangChain tool
|
27
|
+
"""
|
28
|
+
|
29
|
+
async def call_tool(
|
30
|
+
**arguments: dict[str, any],
|
31
|
+
):
|
32
|
+
call_tool_result = await tool.run(arguments)
|
33
|
+
return call_tool_result
|
34
|
+
|
35
|
+
return StructuredTool(
|
36
|
+
name=tool.name,
|
37
|
+
description=tool.description or "",
|
38
|
+
coroutine=call_tool,
|
39
|
+
response_format="content",
|
40
|
+
)
|
@@ -0,0 +1,214 @@
|
|
1
|
+
import inspect
|
2
|
+
import json
|
3
|
+
from collections.abc import Awaitable, Callable, Sequence
|
4
|
+
from typing import (
|
5
|
+
Annotated,
|
6
|
+
Any,
|
7
|
+
ForwardRef,
|
8
|
+
)
|
9
|
+
|
10
|
+
from mcp.server.fastmcp.exceptions import InvalidSignature
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model
|
12
|
+
from pydantic._internal._typing_extra import eval_type_backport
|
13
|
+
from pydantic.fields import FieldInfo
|
14
|
+
from pydantic_core import PydanticUndefined
|
15
|
+
|
16
|
+
|
17
|
+
def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
|
18
|
+
def try_eval_type(
|
19
|
+
value: Any, globalns: dict[str, Any], localns: dict[str, Any]
|
20
|
+
) -> tuple[Any, bool]:
|
21
|
+
try:
|
22
|
+
return eval_type_backport(value, globalns, localns), True
|
23
|
+
except NameError:
|
24
|
+
return value, False
|
25
|
+
|
26
|
+
if isinstance(annotation, str):
|
27
|
+
annotation = ForwardRef(annotation)
|
28
|
+
annotation, status = try_eval_type(annotation, globalns, globalns)
|
29
|
+
|
30
|
+
# This check and raise could perhaps be skipped, and we (FastMCP) just call
|
31
|
+
# model_rebuild right before using it 🤷
|
32
|
+
if status is False:
|
33
|
+
raise InvalidSignature(f"Unable to evaluate type annotation {annotation}")
|
34
|
+
|
35
|
+
return annotation
|
36
|
+
|
37
|
+
|
38
|
+
def _get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
39
|
+
"""Get function signature while evaluating forward references"""
|
40
|
+
signature = inspect.signature(call)
|
41
|
+
globalns = getattr(call, "__globals__", {})
|
42
|
+
typed_params = [
|
43
|
+
inspect.Parameter(
|
44
|
+
name=param.name,
|
45
|
+
kind=param.kind,
|
46
|
+
default=param.default,
|
47
|
+
annotation=_get_typed_annotation(param.annotation, globalns),
|
48
|
+
)
|
49
|
+
for param in signature.parameters.values()
|
50
|
+
]
|
51
|
+
typed_signature = inspect.Signature(typed_params)
|
52
|
+
return typed_signature
|
53
|
+
|
54
|
+
|
55
|
+
class ArgModelBase(BaseModel):
|
56
|
+
"""A model representing the arguments to a function."""
|
57
|
+
|
58
|
+
def model_dump_one_level(self) -> dict[str, Any]:
|
59
|
+
"""Return a dict of the model's fields, one level deep.
|
60
|
+
|
61
|
+
That is, sub-models etc are not dumped - they are kept as pydantic models.
|
62
|
+
"""
|
63
|
+
kwargs: dict[str, Any] = {}
|
64
|
+
for field_name in self.model_fields:
|
65
|
+
kwargs[field_name] = getattr(self, field_name)
|
66
|
+
return kwargs
|
67
|
+
|
68
|
+
model_config = ConfigDict(
|
69
|
+
arbitrary_types_allowed=True,
|
70
|
+
)
|
71
|
+
|
72
|
+
|
73
|
+
class FuncMetadata(BaseModel):
|
74
|
+
arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)]
|
75
|
+
# We can add things in the future like
|
76
|
+
# - Maybe some args are excluded from attempting to parse from JSON
|
77
|
+
# - Maybe some args are special (like context) for dependency injection
|
78
|
+
|
79
|
+
async def call_fn_with_arg_validation(
|
80
|
+
self,
|
81
|
+
fn: Callable[..., Any] | Awaitable[Any],
|
82
|
+
fn_is_async: bool,
|
83
|
+
arguments_to_validate: dict[str, Any],
|
84
|
+
arguments_to_pass_directly: dict[str, Any] | None,
|
85
|
+
) -> Any:
|
86
|
+
"""Call the given function with arguments validated and injected.
|
87
|
+
|
88
|
+
Arguments are first attempted to be parsed from JSON, then validated against
|
89
|
+
the argument model, before being passed to the function.
|
90
|
+
"""
|
91
|
+
arguments_pre_parsed = self.pre_parse_json(arguments_to_validate)
|
92
|
+
arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed)
|
93
|
+
arguments_parsed_dict = arguments_parsed_model.model_dump_one_level()
|
94
|
+
|
95
|
+
arguments_parsed_dict |= arguments_to_pass_directly or {}
|
96
|
+
|
97
|
+
if fn_is_async:
|
98
|
+
if isinstance(fn, Awaitable):
|
99
|
+
return await fn
|
100
|
+
return await fn(**arguments_parsed_dict)
|
101
|
+
if isinstance(fn, Callable):
|
102
|
+
return fn(**arguments_parsed_dict)
|
103
|
+
raise TypeError("fn must be either Callable or Awaitable")
|
104
|
+
|
105
|
+
def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
|
106
|
+
"""Pre-parse data from JSON.
|
107
|
+
|
108
|
+
Return a dict with same keys as input but with values parsed from JSON
|
109
|
+
if appropriate.
|
110
|
+
|
111
|
+
This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside
|
112
|
+
a string rather than an actual list. Claude desktop is prone to this - in fact
|
113
|
+
it seems incapable of NOT doing this. For sub-models, it tends to pass
|
114
|
+
dicts (JSON objects) as JSON strings, which can be pre-parsed here.
|
115
|
+
"""
|
116
|
+
new_data = data.copy() # Shallow copy
|
117
|
+
for field_name, _field_info in self.arg_model.model_fields.items():
|
118
|
+
if field_name not in data:
|
119
|
+
continue
|
120
|
+
if isinstance(data[field_name], str):
|
121
|
+
try:
|
122
|
+
pre_parsed = json.loads(data[field_name])
|
123
|
+
except json.JSONDecodeError:
|
124
|
+
continue # Not JSON - skip
|
125
|
+
if isinstance(pre_parsed, str | int | float):
|
126
|
+
# This is likely that the raw value is e.g. `"hello"` which we
|
127
|
+
# Should really be parsed as '"hello"' in Python - but if we parse
|
128
|
+
# it as JSON it'll turn into just 'hello'. So we skip it.
|
129
|
+
continue
|
130
|
+
new_data[field_name] = pre_parsed
|
131
|
+
assert new_data.keys() == data.keys()
|
132
|
+
return new_data
|
133
|
+
|
134
|
+
model_config = ConfigDict(
|
135
|
+
arbitrary_types_allowed=True,
|
136
|
+
)
|
137
|
+
|
138
|
+
|
139
|
+
@classmethod
|
140
|
+
def func_metadata(
|
141
|
+
cls,
|
142
|
+
func: Callable[..., Any],
|
143
|
+
skip_names: Sequence[str] = ()
|
144
|
+
) -> "FuncMetadata":
|
145
|
+
"""Given a function, return metadata including a pydantic model representing its
|
146
|
+
signature.
|
147
|
+
|
148
|
+
The use case for this is
|
149
|
+
```
|
150
|
+
meta = func_to_pyd(func)
|
151
|
+
validated_args = meta.arg_model.model_validate(some_raw_data_dict)
|
152
|
+
return func(**validated_args.model_dump_one_level())
|
153
|
+
```
|
154
|
+
|
155
|
+
**critically** it also provides pre-parse helper to attempt to parse things from
|
156
|
+
JSON.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
func: The function to convert to a pydantic model
|
160
|
+
skip_names: A list of parameter names to skip. These will not be included in
|
161
|
+
the model.
|
162
|
+
Returns:
|
163
|
+
A pydantic model representing the function's signature.
|
164
|
+
"""
|
165
|
+
sig = _get_typed_signature(func)
|
166
|
+
params = sig.parameters
|
167
|
+
dynamic_pydantic_model_params: dict[str, Any] = {}
|
168
|
+
globalns = getattr(func, "__globals__", {})
|
169
|
+
for param in params.values():
|
170
|
+
if param.name.startswith("_"):
|
171
|
+
raise InvalidSignature(
|
172
|
+
f"Parameter {param.name} of {func.__name__} cannot start with '_'"
|
173
|
+
)
|
174
|
+
if param.name in skip_names:
|
175
|
+
continue
|
176
|
+
annotation = param.annotation
|
177
|
+
|
178
|
+
# `x: None` / `x: None = None`
|
179
|
+
if annotation is None:
|
180
|
+
annotation = Annotated[
|
181
|
+
None,
|
182
|
+
Field(
|
183
|
+
default=param.default
|
184
|
+
if param.default is not inspect.Parameter.empty
|
185
|
+
else PydanticUndefined
|
186
|
+
),
|
187
|
+
]
|
188
|
+
|
189
|
+
# Untyped field
|
190
|
+
if annotation is inspect.Parameter.empty:
|
191
|
+
annotation = Annotated[
|
192
|
+
Any,
|
193
|
+
Field(),
|
194
|
+
# 🤷
|
195
|
+
WithJsonSchema({"title": param.name, "type": "string"}),
|
196
|
+
]
|
197
|
+
|
198
|
+
field_info = FieldInfo.from_annotated_attribute(
|
199
|
+
_get_typed_annotation(annotation, globalns),
|
200
|
+
param.default
|
201
|
+
if param.default is not inspect.Parameter.empty
|
202
|
+
else PydanticUndefined,
|
203
|
+
)
|
204
|
+
dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
|
205
|
+
continue
|
206
|
+
|
207
|
+
arguments_model = create_model(
|
208
|
+
f"{func.__name__}Arguments",
|
209
|
+
**dynamic_pydantic_model_params,
|
210
|
+
__base__=ArgModelBase,
|
211
|
+
)
|
212
|
+
resp = FuncMetadata(arg_model=arguments_model)
|
213
|
+
return resp
|
214
|
+
|