fastmcp 2.2.3__tar.gz → 2.2.4__tar.gz
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.
- {fastmcp-2.2.3 → fastmcp-2.2.4}/PKG-INFO +2 -2
- {fastmcp-2.2.3 → fastmcp-2.2.4}/README.md +1 -1
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/resources.mdx +8 -1
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/cli/cli.py +36 -3
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/template.py +1 -1
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/openapi.py +13 -59
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/func_metadata.py +16 -4
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_resource_template.py +77 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_openapi.py +137 -56
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_server.py +73 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/test_func_metadata.py +68 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.cursor/rules/core-mcp-objects.mdc +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/release.yml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/workflows/publish.yml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/workflows/run-static.yml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/workflows/run-tests.yml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.gitignore +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/.pre-commit-config.yaml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/LICENSE +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/Windows_Notes.md +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/assets/demo-inspector.png +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/clients/client.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/clients/transports.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/docs.json +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/getting-started/installation.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/getting-started/quickstart.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/getting-started/welcome.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/composition.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/contrib.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/decorating-methods.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/fastapi.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/openapi.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/proxy.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/context.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/fastmcp.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/prompts.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/tools.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/snippets/version-badge.mdx +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/style.css +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/complex_inputs.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/desktop.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/echo.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/memory.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/mount_example.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/readme-quickstart.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/sampling.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/screenshot.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/simple_echo.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/README.md +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/pyproject.toml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/__main__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/hub.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/lights/server.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/py.typed +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/settings.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/uv.lock +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/text_me.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/justfile +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/pyproject.toml +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/cli/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/cli/claude.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/base.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/client.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/roots.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/sampling.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/transports.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/README.md +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/exceptions.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/prompts/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/prompts/prompt.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/prompts/prompt_manager.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/py.typed +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/resource.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/resource_manager.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/types.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/context.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/proxy.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/server.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/settings.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/tools/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/tools/tool.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/tools/tool_manager.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/decorators.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/logging.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/openapi.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/types.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/cli/test_run.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/client/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/client/test_client.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/client/test_roots.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/client/test_sampling.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/conftest.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/contrib/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/contrib/test_bulk_tool_caller.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/contrib/test_mcp_mixin.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/prompts/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/prompts/test_base.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/prompts/test_prompt_manager.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_file_resources.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_function_resources.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_resource_manager.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_resources.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_file_server.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_import_server.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_lifespan.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_mount.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_proxy.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_run_server.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/test_servers/fastmcp_server.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/test_servers/sse.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/test_servers/stdio.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/tools/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/tools/test_tool_manager.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/__init__.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/conftest.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/test_openapi.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/test_openapi_fastapi.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/test_decorated_function.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/test_logging.py +0 -0
- {fastmcp-2.2.3 → fastmcp-2.2.4}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastmcp
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: The fast, Pythonic way to build MCP servers.
|
|
5
5
|
Project-URL: Homepage, https://gofastmcp.com
|
|
6
6
|
Project-URL: Repository, https://github.com/jlowin/fastmcp
|
|
@@ -141,7 +141,7 @@ FastMCP aims to be:
|
|
|
141
141
|
### Servers
|
|
142
142
|
- **Create** servers with minimal boilerplate using intuitive decorators
|
|
143
143
|
- **Proxy** existing servers to modify configuration or transport
|
|
144
|
-
- **Compose**
|
|
144
|
+
- **Compose** servers into complex applications
|
|
145
145
|
- **Generate** servers from OpenAPI specs or FastAPI objects
|
|
146
146
|
|
|
147
147
|
### Clients
|
|
@@ -112,7 +112,7 @@ FastMCP aims to be:
|
|
|
112
112
|
### Servers
|
|
113
113
|
- **Create** servers with minimal boilerplate using intuitive decorators
|
|
114
114
|
- **Proxy** existing servers to modify configuration or transport
|
|
115
|
-
- **Compose**
|
|
115
|
+
- **Compose** servers into complex applications
|
|
116
116
|
- **Generate** servers from OpenAPI specs or FastAPI objects
|
|
117
117
|
|
|
118
118
|
### Clients
|
|
@@ -251,13 +251,18 @@ With these two templates defined, clients can request a variety of resources:
|
|
|
251
251
|
|
|
252
252
|
<VersionBadge version="2.2.3" />
|
|
253
253
|
|
|
254
|
+
<Warning>
|
|
255
|
+
Please note: the Model Context Protocol URI standard follows RFC 6570, which does not include support for wildcard parameters. FastMCP extends the template syntax to support wildcards (`{param*}`), and because template matching happens entirely in the FastMCP server, it is not expected that these wildcards will cause compatibility issues with other MCP implementations. However, this can not be guaranteed.
|
|
256
|
+
</Warning>
|
|
257
|
+
|
|
254
258
|
Resource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross "/" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
|
|
255
259
|
|
|
256
|
-
```python
|
|
260
|
+
```python {15, 23}
|
|
257
261
|
from fastmcp import FastMCP
|
|
258
262
|
|
|
259
263
|
mcp = FastMCP(name="DataServer")
|
|
260
264
|
|
|
265
|
+
|
|
261
266
|
# Standard parameter only matches one segment
|
|
262
267
|
@mcp.resource("files://{filename}")
|
|
263
268
|
def get_file(filename: str) -> str:
|
|
@@ -265,6 +270,7 @@ def get_file(filename: str) -> str:
|
|
|
265
270
|
# Will only match files://<single-segment>
|
|
266
271
|
return f"File content for: {filename}"
|
|
267
272
|
|
|
273
|
+
|
|
268
274
|
# Wildcard parameter can match multiple segments
|
|
269
275
|
@mcp.resource("path://{filepath*}")
|
|
270
276
|
def get_path_content(filepath: str) -> str:
|
|
@@ -272,6 +278,7 @@ def get_path_content(filepath: str) -> str:
|
|
|
272
278
|
# Can match path://docs/server/resources.mdx
|
|
273
279
|
return f"Content at path: {filepath}"
|
|
274
280
|
|
|
281
|
+
|
|
275
282
|
# Mixing standard and wildcard parameters
|
|
276
283
|
@mcp.resource("repo://{owner}/{path*}/template.py")
|
|
277
284
|
def get_template_file(owner: str, path: str) -> dict:
|
|
@@ -223,6 +223,27 @@ def dev(
|
|
|
223
223
|
help="Additional packages to install",
|
|
224
224
|
),
|
|
225
225
|
] = [],
|
|
226
|
+
inspector_version: Annotated[
|
|
227
|
+
str | None,
|
|
228
|
+
typer.Option(
|
|
229
|
+
"--inspector-version",
|
|
230
|
+
help="Version of the MCP Inspector to use",
|
|
231
|
+
),
|
|
232
|
+
] = None,
|
|
233
|
+
ui_port: Annotated[
|
|
234
|
+
int | None,
|
|
235
|
+
typer.Option(
|
|
236
|
+
"--ui-port",
|
|
237
|
+
help="Port for the MCP Inspector UI",
|
|
238
|
+
),
|
|
239
|
+
] = None,
|
|
240
|
+
server_port: Annotated[
|
|
241
|
+
int | None,
|
|
242
|
+
typer.Option(
|
|
243
|
+
"--server-port",
|
|
244
|
+
help="Port for the MCP Inspector Proxy server",
|
|
245
|
+
),
|
|
246
|
+
] = None,
|
|
226
247
|
) -> None:
|
|
227
248
|
"""Run a MCP server with the MCP Inspector."""
|
|
228
249
|
file, server_object = _parse_file_path(file_spec)
|
|
@@ -234,6 +255,8 @@ def dev(
|
|
|
234
255
|
"server_object": server_object,
|
|
235
256
|
"with_editable": str(with_editable) if with_editable else None,
|
|
236
257
|
"with_packages": with_packages,
|
|
258
|
+
"ui_port": ui_port,
|
|
259
|
+
"server_port": server_port,
|
|
237
260
|
},
|
|
238
261
|
)
|
|
239
262
|
|
|
@@ -243,7 +266,11 @@ def dev(
|
|
|
243
266
|
if hasattr(server, "dependencies"):
|
|
244
267
|
with_packages = list(set(with_packages + server.dependencies))
|
|
245
268
|
|
|
246
|
-
|
|
269
|
+
env_vars = {}
|
|
270
|
+
if ui_port:
|
|
271
|
+
env_vars["CLIENT_PORT"] = str(ui_port)
|
|
272
|
+
if server_port:
|
|
273
|
+
env_vars["SERVER_PORT"] = str(server_port)
|
|
247
274
|
|
|
248
275
|
# Get the correct npx command
|
|
249
276
|
npx_cmd = _get_npx_command()
|
|
@@ -254,13 +281,19 @@ def dev(
|
|
|
254
281
|
)
|
|
255
282
|
sys.exit(1)
|
|
256
283
|
|
|
284
|
+
inspector_cmd = "@modelcontextprotocol/inspector"
|
|
285
|
+
if inspector_version:
|
|
286
|
+
inspector_cmd += f"@{inspector_version}"
|
|
287
|
+
|
|
288
|
+
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
|
289
|
+
|
|
257
290
|
# Run the MCP Inspector command with shell=True on Windows
|
|
258
291
|
shell = sys.platform == "win32"
|
|
259
292
|
process = subprocess.run(
|
|
260
|
-
[npx_cmd,
|
|
293
|
+
[npx_cmd, inspector_cmd] + uv_cmd,
|
|
261
294
|
check=True,
|
|
262
295
|
shell=shell,
|
|
263
|
-
env=dict(os.environ.items())
|
|
296
|
+
env=dict(os.environ.items()) | env_vars,
|
|
264
297
|
)
|
|
265
298
|
sys.exit(process.returncode)
|
|
266
299
|
except subprocess.CalledProcessError as e:
|
|
@@ -95,7 +95,7 @@ class ResourceTemplate(BaseModel):
|
|
|
95
95
|
raise ValueError("You must provide a name for lambda functions")
|
|
96
96
|
|
|
97
97
|
# Validate that URI params match function params
|
|
98
|
-
uri_params = set(re.findall(r"{(\w+)}", uri_template))
|
|
98
|
+
uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
|
|
99
99
|
if not uri_params:
|
|
100
100
|
raise ValueError("URI template must contain at least one parameter")
|
|
101
101
|
|
|
@@ -257,7 +257,7 @@ class OpenAPIResource(Resource):
|
|
|
257
257
|
self._client = client
|
|
258
258
|
self._route = route
|
|
259
259
|
|
|
260
|
-
async def read(self) -> str:
|
|
260
|
+
async def read(self) -> str | bytes:
|
|
261
261
|
"""Fetch the resource data by making an HTTP request."""
|
|
262
262
|
try:
|
|
263
263
|
# Extract path parameters from the URI if present
|
|
@@ -297,15 +297,16 @@ class OpenAPIResource(Resource):
|
|
|
297
297
|
# Raise for 4xx/5xx responses
|
|
298
298
|
response.raise_for_status()
|
|
299
299
|
|
|
300
|
-
#
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
else:
|
|
300
|
+
# Determine content type and return appropriate format
|
|
301
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
302
|
+
|
|
303
|
+
if "application/json" in content_type:
|
|
304
|
+
result = response.json()
|
|
305
|
+
return json.dumps(result)
|
|
306
|
+
elif any(ct in content_type for ct in ["text/", "application/xml"]):
|
|
308
307
|
return response.text
|
|
308
|
+
else:
|
|
309
|
+
return response.content
|
|
309
310
|
|
|
310
311
|
except httpx.HTTPStatusError as e:
|
|
311
312
|
# Handle HTTP errors (4xx, 5xx)
|
|
@@ -343,59 +344,13 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
343
344
|
uri_template=uri_template,
|
|
344
345
|
name=name,
|
|
345
346
|
description=description,
|
|
346
|
-
fn=
|
|
347
|
+
fn=lambda **kwargs: None,
|
|
347
348
|
parameters=parameters,
|
|
348
349
|
tags=tags,
|
|
349
350
|
)
|
|
350
351
|
self._client = client
|
|
351
352
|
self._route = route
|
|
352
353
|
|
|
353
|
-
async def _create_resource_fn(self, **kwargs):
|
|
354
|
-
"""Create a resource with parameters."""
|
|
355
|
-
# Prepare the path with parameters
|
|
356
|
-
path = self._route.path
|
|
357
|
-
for param_name, param_value in kwargs.items():
|
|
358
|
-
path = path.replace(f"{{{param_name}}}", str(param_value))
|
|
359
|
-
|
|
360
|
-
try:
|
|
361
|
-
response = await self._client.request(
|
|
362
|
-
method=self._route.method,
|
|
363
|
-
url=path,
|
|
364
|
-
timeout=30.0, # Default timeout
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
# Raise for 4xx/5xx responses
|
|
368
|
-
response.raise_for_status()
|
|
369
|
-
|
|
370
|
-
# Determine the mime type from the response
|
|
371
|
-
content_type = response.headers.get("content-type", "application/json")
|
|
372
|
-
mime_type = content_type.split(";")[0].strip()
|
|
373
|
-
|
|
374
|
-
# Return the appropriate data
|
|
375
|
-
if mime_type == "application/json":
|
|
376
|
-
try:
|
|
377
|
-
return response.json()
|
|
378
|
-
except (json.JSONDecodeError, ValueError):
|
|
379
|
-
return response.text
|
|
380
|
-
else:
|
|
381
|
-
return response.text
|
|
382
|
-
|
|
383
|
-
except httpx.HTTPStatusError as e:
|
|
384
|
-
error_message = (
|
|
385
|
-
f"HTTP error {e.response.status_code}: {e.response.reason_phrase}"
|
|
386
|
-
)
|
|
387
|
-
try:
|
|
388
|
-
error_data = e.response.json()
|
|
389
|
-
error_message += f" - {error_data}"
|
|
390
|
-
except (json.JSONDecodeError, ValueError):
|
|
391
|
-
if e.response.text:
|
|
392
|
-
error_message += f" - {e.response.text}"
|
|
393
|
-
|
|
394
|
-
raise ValueError(error_message)
|
|
395
|
-
|
|
396
|
-
except httpx.RequestError as e:
|
|
397
|
-
raise ValueError(f"Request error: {str(e)}")
|
|
398
|
-
|
|
399
354
|
async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
|
|
400
355
|
"""Create a resource with the given parameters."""
|
|
401
356
|
# Generate a URI for this resource instance
|
|
@@ -409,9 +364,8 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
|
409
364
|
route=self._route,
|
|
410
365
|
uri=uri,
|
|
411
366
|
name=f"{self.name}-{'-'.join(uri_parts)}",
|
|
412
|
-
description=self.description
|
|
413
|
-
|
|
414
|
-
mime_type="application/json", # Default, will be updated when read
|
|
367
|
+
description=self.description or f"Resource for {self._route.path}",
|
|
368
|
+
mime_type="application/json",
|
|
415
369
|
tags=set(self._route.tags or []),
|
|
416
370
|
)
|
|
417
371
|
|
|
@@ -7,7 +7,15 @@ from typing import (
|
|
|
7
7
|
ForwardRef,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
from pydantic import
|
|
10
|
+
from pydantic import (
|
|
11
|
+
BaseModel,
|
|
12
|
+
ConfigDict,
|
|
13
|
+
Field,
|
|
14
|
+
TypeAdapter,
|
|
15
|
+
ValidationError,
|
|
16
|
+
WithJsonSchema,
|
|
17
|
+
create_model,
|
|
18
|
+
)
|
|
11
19
|
from pydantic._internal._typing_extra import eval_type_backport
|
|
12
20
|
from pydantic.fields import FieldInfo
|
|
13
21
|
from pydantic_core import PydanticUndefined
|
|
@@ -80,14 +88,18 @@ class FuncMetadata(BaseModel):
|
|
|
80
88
|
dicts (JSON objects) as JSON strings, which can be pre-parsed here.
|
|
81
89
|
"""
|
|
82
90
|
new_data = data.copy() # Shallow copy
|
|
83
|
-
for field_name,
|
|
91
|
+
for field_name, field_info in self.arg_model.model_fields.items():
|
|
84
92
|
if field_name not in data.keys():
|
|
85
93
|
continue
|
|
86
94
|
if isinstance(data[field_name], str):
|
|
87
95
|
try:
|
|
88
96
|
pre_parsed = json.loads(data[field_name])
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
|
|
98
|
+
# Check if the pre_parsed value is valid for the field
|
|
99
|
+
validator = TypeAdapter(field_info.annotation)
|
|
100
|
+
validator.validate_python(pre_parsed)
|
|
101
|
+
except (json.JSONDecodeError, ValidationError):
|
|
102
|
+
continue # Not JSON or invalid for the field
|
|
91
103
|
if isinstance(pre_parsed, str | int | float):
|
|
92
104
|
# This is likely that the raw value is e.g. `"hello"` which we
|
|
93
105
|
# Should really be parsed as '"hello"' in Python - but if we parse
|
|
@@ -297,6 +297,66 @@ class TestResourceTemplate:
|
|
|
297
297
|
content = await resource.read()
|
|
298
298
|
assert content == "hello"
|
|
299
299
|
|
|
300
|
+
async def test_wildcard_param_can_create_resource(self):
|
|
301
|
+
"""Test that wildcard parameters are valid."""
|
|
302
|
+
|
|
303
|
+
def identity(path: str) -> str:
|
|
304
|
+
return path
|
|
305
|
+
|
|
306
|
+
template = ResourceTemplate.from_function(
|
|
307
|
+
fn=identity,
|
|
308
|
+
uri_template="test://{path*}.py",
|
|
309
|
+
name="test",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
assert await template.create_resource(
|
|
313
|
+
"test://path/to/test.py",
|
|
314
|
+
{"path": "path/to/test.py"},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
async def test_wildcard_param_matches(self):
|
|
318
|
+
def identify(path: str) -> str:
|
|
319
|
+
return path
|
|
320
|
+
|
|
321
|
+
template = ResourceTemplate.from_function(
|
|
322
|
+
fn=identify,
|
|
323
|
+
uri_template="test://src/{path*}.py",
|
|
324
|
+
name="test",
|
|
325
|
+
)
|
|
326
|
+
# Valid match
|
|
327
|
+
params = template.matches("test://src/path/to/test.py")
|
|
328
|
+
assert params == {"path": "path/to/test"}
|
|
329
|
+
|
|
330
|
+
async def test_multiple_wildcard_params(self):
|
|
331
|
+
"""Test that multiple wildcard parameters are valid."""
|
|
332
|
+
|
|
333
|
+
def identity(path: str, path2: str) -> str:
|
|
334
|
+
return f"{path}/{path2}"
|
|
335
|
+
|
|
336
|
+
template = ResourceTemplate.from_function(
|
|
337
|
+
fn=identity,
|
|
338
|
+
uri_template="test://{path*}/xyz/{path2*}",
|
|
339
|
+
name="test",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
params = template.matches("test://path/to/xyz/abc")
|
|
343
|
+
assert params == {"path": "path/to", "path2": "abc"}
|
|
344
|
+
|
|
345
|
+
async def test_wildcard_param_with_regular_param(self):
|
|
346
|
+
"""Test that a wildcard parameter can be used with a regular parameter."""
|
|
347
|
+
|
|
348
|
+
def identity(prefix: str, path: str) -> str:
|
|
349
|
+
return f"{prefix}/{path}"
|
|
350
|
+
|
|
351
|
+
template = ResourceTemplate.from_function(
|
|
352
|
+
fn=identity,
|
|
353
|
+
uri_template="test://{prefix}/{path*}",
|
|
354
|
+
name="test",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
params = template.matches("test://src/path/to/test.py")
|
|
358
|
+
assert params == {"prefix": "src", "path": "path/to/test.py"}
|
|
359
|
+
|
|
300
360
|
|
|
301
361
|
class TestMatchUriTemplate:
|
|
302
362
|
"""Test match_uri_template function."""
|
|
@@ -443,3 +503,20 @@ class TestMatchUriTemplate:
|
|
|
443
503
|
uri_template = "test://a/{x}{y}"
|
|
444
504
|
result = match_uri_template(uri=uri, uri_template=uri_template)
|
|
445
505
|
assert result is None
|
|
506
|
+
|
|
507
|
+
@pytest.mark.parametrize(
|
|
508
|
+
"uri, expected_params",
|
|
509
|
+
[
|
|
510
|
+
("file://abc/xyz.py", {"path": "xyz"}),
|
|
511
|
+
("file://abc/x/y/z.py", {"path": "x/y/z"}),
|
|
512
|
+
("file://abc/x/y/z/.py", {"path": "x/y/z/"}),
|
|
513
|
+
("file://abc/x/y/z.md", None),
|
|
514
|
+
("file://x/y/z.txt", None),
|
|
515
|
+
],
|
|
516
|
+
)
|
|
517
|
+
def test_match_uri_template_with_non_slash_suffix(
|
|
518
|
+
self, uri: str, expected_params: dict[str, str]
|
|
519
|
+
):
|
|
520
|
+
uri_template = "file://abc/{path*}.py"
|
|
521
|
+
result = match_uri_template(uri=uri, uri_template=uri_template)
|
|
522
|
+
assert result == expected_params
|