44
55import inspect
66import re
7+ import urllib .parse
78from collections .abc import Callable
89from typing import Any
910
@@ -16,7 +17,7 @@ class ResourceTemplate(BaseModel):
1617 """A template for dynamically creating resources."""
1718
1819 uri_template : str = Field (
19- description = "URI template with parameters (e.g. weather://{city}/current)"
20+ description = "URI template with parameters (e.g. weather://{city}/current{?units,format} )"
2021 )
2122 name : str = Field (description = "Name of the resource" )
2223 description : str | None = Field (description = "Description of what the resource does" )
@@ -27,6 +28,14 @@ class ResourceTemplate(BaseModel):
2728 parameters : dict [str , Any ] = Field (
2829 description = "JSON schema for function parameters"
2930 )
31+ required_params : set [str ] = Field (
32+ default_factory = set ,
33+ description = "Set of required parameters from the path component" ,
34+ )
35+ optional_params : set [str ] = Field (
36+ default_factory = set ,
37+ description = "Set of optional parameters specified in the query component" ,
38+ )
3039
3140 @classmethod
3241 def from_function (
@@ -48,29 +57,113 @@ def from_function(
4857 # ensure the arguments are properly cast
4958 fn = validate_call (fn )
5059
60+ # Extract required and optional parameters from function signature
61+ required_params , optional_params = cls ._analyze_function_params (fn )
62+
63+ # Extract path parameters from URI template
64+ path_params : set [str ] = set (
65+ re .findall (r"{(\w+)}" , re .sub (r"{(\?.+?)}" , "" , uri_template ))
66+ )
67+
68+ # Extract query parameters from the URI template if present
69+ query_param_match = re .search (r"{(\?(?:\w+,)*\w+)}" , uri_template )
70+ query_params : set [str ] = set ()
71+ if query_param_match :
72+ # Extract query parameters from {?param1,param2,...} syntax
73+ query_str = query_param_match .group (1 )
74+ query_params = set (
75+ query_str [1 :].split ("," )
76+ ) # Remove the leading '?' and split
77+
78+ # Validate path parameters match required function parameters
79+ print (f"path_params: { path_params } " )
80+ print (f"required_params: { required_params } " )
81+ if path_params != required_params :
82+ raise ValueError (
83+ f"Mismatch between URI path parameters { path_params } "
84+ f"and required function parameters { required_params } "
85+ )
86+
87+ # Validate query parameters are a subset of optional function parameters
88+ if not query_params .issubset (optional_params ):
89+ invalid_params : set [str ] = query_params - optional_params
90+ raise ValueError (
91+ f"Query parameters { invalid_params } do not match optional "
92+ f"function parameters { optional_params } "
93+ )
94+
5195 return cls (
5296 uri_template = uri_template ,
5397 name = func_name ,
5498 description = description or fn .__doc__ or "" ,
5599 mime_type = mime_type or "text/plain" ,
56100 fn = fn ,
57101 parameters = parameters ,
102+ required_params = required_params ,
103+ optional_params = optional_params ,
58104 )
59105
106+ @staticmethod
107+ def _analyze_function_params (fn : Callable [..., Any ]) -> tuple [set [str ], set [str ]]:
108+ """Analyze function signature to extract required and optional parameters."""
109+ required_params : set [str ] = set ()
110+ optional_params : set [str ] = set ()
111+
112+ signature = inspect .signature (fn )
113+ for name , param in signature .parameters .items ():
114+ # Parameters with default values are optional
115+ if param .default is param .empty :
116+ required_params .add (name )
117+ else :
118+ optional_params .add (name )
119+
120+ return required_params , optional_params
121+
60122 def matches (self , uri : str ) -> dict [str , Any ] | None :
61123 """Check if URI matches template and extract parameters."""
62- # Convert template to regex pattern
63- pattern = self .uri_template .replace ("{" , "(?P<" ).replace ("}" , ">[^/]+)" )
64- match = re .match (f"^{ pattern } $" , uri )
65- if match :
66- return match .groupdict ()
67- return None
124+ # Split URI into path and query parts
125+ if "?" in uri :
126+ path , query = uri .split ("?" , 1 )
127+ else :
128+ path , query = uri , ""
129+
130+ # Remove the query parameter part from the template for matching
131+ path_template = re .sub (r"{(\?.+?)}" , "" , self .uri_template )
132+
133+ # Convert template to regex pattern for path part
134+ pattern = path_template .replace ("{" , "(?P<" ).replace ("}" , ">[^/]+)" )
135+ match = re .match (f"^{ pattern } $" , path )
136+
137+ if not match :
138+ return None
139+
140+ # Extract path parameters
141+ params = match .groupdict ()
142+
143+ # Parse and add query parameters if present
144+ if query :
145+ query_params = urllib .parse .parse_qs (query )
146+ for key , value in query_params .items ():
147+ if key in self .optional_params :
148+ # Use the first value if multiple are provided
149+ params [key ] = value [0 ] if value else None
150+
151+ return params
68152
69153 async def create_resource (self , uri : str , params : dict [str , Any ]) -> Resource :
70154 """Create a resource from the template with the given parameters."""
71155 try :
156+ # Prepare parameters for function call
157+ # For optional parameters not in URL, use their default values
158+ fn_params = {}
159+
160+ # First add extracted parameters
161+ for name , value in params .items ():
162+ if name in self .required_params or name in self .optional_params :
163+ fn_params [name ] = value
164+
72165 # Call function and check if result is a coroutine
73- result = self .fn (** params )
166+ result = self .fn (** fn_params )
74167 if inspect .iscoroutine (result ):
75168 result = await result
76169
0 commit comments