universal-mcp 0.1.8rc1__py3-none-any.whl → 0.1.8rc3__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/__init__.py +0 -2
- universal_mcp/analytics.py +75 -0
- universal_mcp/applications/application.py +28 -5
- universal_mcp/applications/calendly/README.md +78 -0
- universal_mcp/applications/calendly/app.py +1207 -0
- universal_mcp/applications/coda/README.md +133 -0
- universal_mcp/applications/coda/__init__.py +0 -0
- universal_mcp/applications/coda/app.py +3704 -0
- universal_mcp/applications/e2b/app.py +12 -7
- universal_mcp/applications/firecrawl/app.py +27 -0
- universal_mcp/applications/github/app.py +127 -85
- universal_mcp/applications/google_calendar/app.py +62 -127
- universal_mcp/applications/google_docs/app.py +48 -35
- universal_mcp/applications/google_drive/app.py +119 -96
- universal_mcp/applications/google_mail/app.py +124 -34
- universal_mcp/applications/google_sheet/app.py +90 -74
- universal_mcp/applications/markitdown/app.py +9 -8
- universal_mcp/applications/notion/app.py +254 -134
- universal_mcp/applications/perplexity/app.py +16 -14
- universal_mcp/applications/reddit/app.py +94 -85
- universal_mcp/applications/resend/app.py +12 -5
- universal_mcp/applications/serpapi/app.py +11 -4
- universal_mcp/applications/tavily/app.py +11 -8
- universal_mcp/applications/wrike/README.md +71 -0
- universal_mcp/applications/wrike/__init__.py +0 -0
- universal_mcp/applications/wrike/app.py +1384 -0
- universal_mcp/applications/youtube/README.md +82 -0
- universal_mcp/applications/youtube/__init__.py +0 -0
- universal_mcp/applications/youtube/app.py +1446 -0
- universal_mcp/applications/zenquotes/app.py +12 -2
- universal_mcp/exceptions.py +9 -2
- universal_mcp/integrations/__init__.py +24 -1
- universal_mcp/integrations/integration.py +133 -28
- universal_mcp/logger.py +3 -56
- universal_mcp/servers/__init__.py +6 -14
- universal_mcp/servers/server.py +205 -150
- universal_mcp/stores/__init__.py +7 -2
- universal_mcp/stores/store.py +103 -40
- universal_mcp/tools/__init__.py +3 -0
- universal_mcp/tools/adapters.py +43 -0
- universal_mcp/tools/func_metadata.py +213 -0
- universal_mcp/tools/tools.py +342 -0
- universal_mcp/utils/docgen.py +325 -119
- universal_mcp/utils/docstring_parser.py +179 -0
- universal_mcp/utils/dump_app_tools.py +33 -23
- universal_mcp/utils/openapi.py +229 -46
- {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc3.dist-info}/METADATA +8 -4
- universal_mcp-0.1.8rc3.dist-info/RECORD +75 -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.8rc3.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.8rc1.dist-info → universal_mcp-0.1.8rc3.dist-info}/entry_points.txt +0 -0
universal_mcp/stores/store.py
CHANGED
@@ -1,18 +1,31 @@
|
|
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
|
+
|
12
|
+
pass
|
13
|
+
|
14
|
+
|
15
|
+
class KeyNotFoundError(StoreError):
|
16
|
+
"""Exception raised when a key is not found in the store."""
|
17
|
+
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
class BaseStore(ABC):
|
9
22
|
"""
|
10
23
|
Abstract base class defining the interface for credential stores.
|
11
24
|
All credential stores must implement get, set and delete methods.
|
12
25
|
"""
|
13
26
|
|
14
27
|
@abstractmethod
|
15
|
-
def get(self, key: str):
|
28
|
+
def get(self, key: str) -> Any:
|
16
29
|
"""
|
17
30
|
Retrieve a value from the store by key.
|
18
31
|
|
@@ -20,44 +33,54 @@ class Store(ABC):
|
|
20
33
|
key (str): The key to look up
|
21
34
|
|
22
35
|
Returns:
|
23
|
-
The stored value
|
36
|
+
Any: The stored value
|
37
|
+
|
38
|
+
Raises:
|
39
|
+
KeyNotFoundError: If the key is not found in the store
|
40
|
+
StoreError: If there is an error accessing the store
|
24
41
|
"""
|
25
42
|
pass
|
26
43
|
|
27
44
|
@abstractmethod
|
28
|
-
def set(self, key: str, value: str):
|
45
|
+
def set(self, key: str, value: str) -> None:
|
29
46
|
"""
|
30
47
|
Store a value in the store with the given key.
|
31
48
|
|
32
49
|
Args:
|
33
50
|
key (str): The key to store the value under
|
34
51
|
value (str): The value to store
|
52
|
+
|
53
|
+
Raises:
|
54
|
+
StoreError: If there is an error storing the value
|
35
55
|
"""
|
36
56
|
pass
|
37
57
|
|
38
58
|
@abstractmethod
|
39
|
-
def delete(self, key: str):
|
59
|
+
def delete(self, key: str) -> None:
|
40
60
|
"""
|
41
61
|
Delete a value from the store by key.
|
42
62
|
|
43
63
|
Args:
|
44
64
|
key (str): The key to delete
|
65
|
+
|
66
|
+
Raises:
|
67
|
+
KeyNotFoundError: If the key is not found in the store
|
68
|
+
StoreError: If there is an error deleting the value
|
45
69
|
"""
|
46
70
|
pass
|
47
71
|
|
48
72
|
|
49
|
-
class MemoryStore:
|
73
|
+
class MemoryStore(BaseStore):
|
50
74
|
"""
|
51
|
-
|
52
|
-
|
53
|
-
Ideally should be a key value store that keeps data in memory.
|
75
|
+
In-memory credential store implementation.
|
76
|
+
Stores credentials in a dictionary that persists only for the duration of the program execution.
|
54
77
|
"""
|
55
78
|
|
56
79
|
def __init__(self):
|
57
80
|
"""Initialize an empty dictionary to store the data."""
|
58
|
-
self.data = {}
|
81
|
+
self.data: dict[str, str] = {}
|
59
82
|
|
60
|
-
def get(self, key: str):
|
83
|
+
def get(self, key: str) -> Any:
|
61
84
|
"""
|
62
85
|
Retrieve a value from the in-memory store by key.
|
63
86
|
|
@@ -65,11 +88,16 @@ class MemoryStore:
|
|
65
88
|
key (str): The key to look up
|
66
89
|
|
67
90
|
Returns:
|
68
|
-
The stored value
|
91
|
+
Any: The stored value
|
92
|
+
|
93
|
+
Raises:
|
94
|
+
KeyNotFoundError: If the key is not found in the store
|
69
95
|
"""
|
70
|
-
|
96
|
+
if key not in self.data:
|
97
|
+
raise KeyNotFoundError(f"Key '{key}' not found in memory store")
|
98
|
+
return self.data[key]
|
71
99
|
|
72
|
-
def set(self, key: str, value: str):
|
100
|
+
def set(self, key: str, value: str) -> None:
|
73
101
|
"""
|
74
102
|
Store a value in the in-memory store with the given key.
|
75
103
|
|
@@ -79,27 +107,28 @@ class MemoryStore:
|
|
79
107
|
"""
|
80
108
|
self.data[key] = value
|
81
109
|
|
82
|
-
def delete(self, key: str):
|
110
|
+
def delete(self, key: str) -> None:
|
83
111
|
"""
|
84
112
|
Delete a value from the in-memory store by key.
|
85
113
|
|
86
114
|
Args:
|
87
115
|
key (str): The key to delete
|
116
|
+
|
117
|
+
Raises:
|
118
|
+
KeyNotFoundError: If the key is not found in the store
|
88
119
|
"""
|
120
|
+
if key not in self.data:
|
121
|
+
raise KeyNotFoundError(f"Key '{key}' not found in memory store")
|
89
122
|
del self.data[key]
|
90
123
|
|
91
124
|
|
92
|
-
class EnvironmentStore(
|
125
|
+
class EnvironmentStore(BaseStore):
|
93
126
|
"""
|
94
|
-
|
95
|
-
|
127
|
+
Environment variable-based credential store implementation.
|
128
|
+
Uses OS environment variables to store and retrieve credentials.
|
96
129
|
"""
|
97
130
|
|
98
|
-
def
|
99
|
-
"""Initialize the environment store."""
|
100
|
-
pass
|
101
|
-
|
102
|
-
def get(self, key: str):
|
131
|
+
def get(self, key: str) -> Any:
|
103
132
|
"""
|
104
133
|
Retrieve a value from environment variables by key.
|
105
134
|
|
@@ -107,11 +136,17 @@ class EnvironmentStore(Store):
|
|
107
136
|
key (str): The environment variable name to look up
|
108
137
|
|
109
138
|
Returns:
|
110
|
-
|
139
|
+
Any: The stored value
|
140
|
+
|
141
|
+
Raises:
|
142
|
+
KeyNotFoundError: If the environment variable is not found
|
111
143
|
"""
|
112
|
-
|
144
|
+
value = os.getenv(key)
|
145
|
+
if value is None:
|
146
|
+
raise KeyNotFoundError(f"Environment variable '{key}' not found")
|
147
|
+
return value
|
113
148
|
|
114
|
-
def set(self, key: str, value: str):
|
149
|
+
def set(self, key: str, value: str) -> None:
|
115
150
|
"""
|
116
151
|
Set an environment variable.
|
117
152
|
|
@@ -121,20 +156,25 @@ class EnvironmentStore(Store):
|
|
121
156
|
"""
|
122
157
|
os.environ[key] = value
|
123
158
|
|
124
|
-
def delete(self, key: str):
|
159
|
+
def delete(self, key: str) -> None:
|
125
160
|
"""
|
126
161
|
Delete an environment variable.
|
127
162
|
|
128
163
|
Args:
|
129
164
|
key (str): The environment variable name to delete
|
165
|
+
|
166
|
+
Raises:
|
167
|
+
KeyNotFoundError: If the environment variable is not found
|
130
168
|
"""
|
169
|
+
if key not in os.environ:
|
170
|
+
raise KeyNotFoundError(f"Environment variable '{key}' not found")
|
131
171
|
del os.environ[key]
|
132
172
|
|
133
173
|
|
134
|
-
class KeyringStore(
|
174
|
+
class KeyringStore(BaseStore):
|
135
175
|
"""
|
136
|
-
|
137
|
-
|
176
|
+
System keyring-based credential store implementation.
|
177
|
+
Uses the system's secure credential storage facility via the keyring library.
|
138
178
|
"""
|
139
179
|
|
140
180
|
def __init__(self, app_name: str = "universal_mcp"):
|
@@ -146,7 +186,7 @@ class KeyringStore(Store):
|
|
146
186
|
"""
|
147
187
|
self.app_name = app_name
|
148
188
|
|
149
|
-
def get(self, key: str):
|
189
|
+
def get(self, key: str) -> Any:
|
150
190
|
"""
|
151
191
|
Retrieve a password from the system keyring.
|
152
192
|
|
@@ -154,28 +194,51 @@ class KeyringStore(Store):
|
|
154
194
|
key (str): The key to look up
|
155
195
|
|
156
196
|
Returns:
|
157
|
-
The stored
|
197
|
+
Any: The stored value
|
198
|
+
|
199
|
+
Raises:
|
200
|
+
KeyNotFoundError: If the key is not found in the keyring
|
201
|
+
StoreError: If there is an error accessing the keyring
|
158
202
|
"""
|
159
|
-
|
160
|
-
|
203
|
+
try:
|
204
|
+
logger.info(f"Getting password for {key} from keyring")
|
205
|
+
value = keyring.get_password(self.app_name, key)
|
206
|
+
if value is None:
|
207
|
+
raise KeyNotFoundError(f"Key '{key}' not found in keyring")
|
208
|
+
return value
|
209
|
+
except Exception as e:
|
210
|
+
raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
|
161
211
|
|
162
|
-
def set(self, key: str, value: str):
|
212
|
+
def set(self, key: str, value: str) -> None:
|
163
213
|
"""
|
164
214
|
Store a password in the system keyring.
|
165
215
|
|
166
216
|
Args:
|
167
217
|
key (str): The key to store the password under
|
168
218
|
value (str): The password to store
|
219
|
+
|
220
|
+
Raises:
|
221
|
+
StoreError: If there is an error storing in the keyring
|
169
222
|
"""
|
170
|
-
|
171
|
-
|
223
|
+
try:
|
224
|
+
logger.info(f"Setting password for {key} in keyring")
|
225
|
+
keyring.set_password(self.app_name, key, value)
|
226
|
+
except Exception as e:
|
227
|
+
raise StoreError(f"Error storing in keyring: {str(e)}") from e
|
172
228
|
|
173
|
-
def delete(self, key: str):
|
229
|
+
def delete(self, key: str) -> None:
|
174
230
|
"""
|
175
231
|
Delete a password from the system keyring.
|
176
232
|
|
177
233
|
Args:
|
178
234
|
key (str): The key to delete
|
235
|
+
|
236
|
+
Raises:
|
237
|
+
KeyNotFoundError: If the key is not found in the keyring
|
238
|
+
StoreError: If there is an error deleting from the keyring
|
179
239
|
"""
|
180
|
-
|
181
|
-
|
240
|
+
try:
|
241
|
+
logger.info(f"Deleting password for {key} from keyring")
|
242
|
+
keyring.delete_password(self.app_name, key)
|
243
|
+
except Exception as e:
|
244
|
+
raise KeyNotFoundError(f"Key '{key}' not found in keyring") from e
|
@@ -0,0 +1,43 @@
|
|
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
|
+
|
9
|
+
return MCPTool(
|
10
|
+
name=tool.name,
|
11
|
+
description=tool.description or "",
|
12
|
+
inputSchema=tool.parameters,
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
def convert_tool_to_langchain_tool(
|
17
|
+
tool: Tool,
|
18
|
+
):
|
19
|
+
from langchain_core.tools import StructuredTool
|
20
|
+
|
21
|
+
"""Convert an tool to a LangChain tool.
|
22
|
+
|
23
|
+
NOTE: this tool can be executed only in a context of an active MCP client session.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
tool: Tool to convert
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
a LangChain tool
|
30
|
+
"""
|
31
|
+
|
32
|
+
async def call_tool(
|
33
|
+
**arguments: dict[str, any],
|
34
|
+
):
|
35
|
+
call_tool_result = await tool.run(arguments)
|
36
|
+
return call_tool_result
|
37
|
+
|
38
|
+
return StructuredTool(
|
39
|
+
name=tool.name,
|
40
|
+
description=tool.description or "",
|
41
|
+
coroutine=call_tool,
|
42
|
+
response_format="content",
|
43
|
+
)
|
@@ -0,0 +1,213 @@
|
|
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
|
+
@classmethod
|
139
|
+
def func_metadata(
|
140
|
+
cls, func: Callable[..., Any], skip_names: Sequence[str] = ()
|
141
|
+
) -> "FuncMetadata":
|
142
|
+
"""Given a function, return metadata including a pydantic model representing its
|
143
|
+
signature.
|
144
|
+
|
145
|
+
The use case for this is
|
146
|
+
```
|
147
|
+
meta = func_to_pyd(func)
|
148
|
+
validated_args = meta.arg_model.model_validate(some_raw_data_dict)
|
149
|
+
return func(**validated_args.model_dump_one_level())
|
150
|
+
```
|
151
|
+
|
152
|
+
**critically** it also provides pre-parse helper to attempt to parse things from
|
153
|
+
JSON.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
func: The function to convert to a pydantic model
|
157
|
+
skip_names: A list of parameter names to skip. These will not be included in
|
158
|
+
the model.
|
159
|
+
Returns:
|
160
|
+
A pydantic model representing the function's signature.
|
161
|
+
"""
|
162
|
+
sig = _get_typed_signature(func)
|
163
|
+
params = sig.parameters
|
164
|
+
dynamic_pydantic_model_params: dict[str, Any] = {}
|
165
|
+
globalns = getattr(func, "__globals__", {})
|
166
|
+
for param in params.values():
|
167
|
+
if param.name.startswith("_"):
|
168
|
+
raise InvalidSignature(
|
169
|
+
f"Parameter {param.name} of {func.__name__} cannot start with '_'"
|
170
|
+
)
|
171
|
+
if param.name in skip_names:
|
172
|
+
continue
|
173
|
+
annotation = param.annotation
|
174
|
+
|
175
|
+
# `x: None` / `x: None = None`
|
176
|
+
if annotation is None:
|
177
|
+
annotation = Annotated[
|
178
|
+
None,
|
179
|
+
Field(
|
180
|
+
default=param.default
|
181
|
+
if param.default is not inspect.Parameter.empty
|
182
|
+
else PydanticUndefined
|
183
|
+
),
|
184
|
+
]
|
185
|
+
|
186
|
+
# Untyped field
|
187
|
+
if annotation is inspect.Parameter.empty:
|
188
|
+
annotation = Annotated[
|
189
|
+
Any,
|
190
|
+
Field(),
|
191
|
+
# 🤷
|
192
|
+
WithJsonSchema({"title": param.name, "type": "string"}),
|
193
|
+
]
|
194
|
+
|
195
|
+
field_info = FieldInfo.from_annotated_attribute(
|
196
|
+
_get_typed_annotation(annotation, globalns),
|
197
|
+
param.default
|
198
|
+
if param.default is not inspect.Parameter.empty
|
199
|
+
else PydanticUndefined,
|
200
|
+
)
|
201
|
+
dynamic_pydantic_model_params[param.name] = (
|
202
|
+
field_info.annotation,
|
203
|
+
field_info,
|
204
|
+
)
|
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
|