Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

typing updates #17

Merged
merged 5 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Run Pre-commits

env:
# enable colored output
# https://github.com/pytest-dev/pytest/issues/7443
PY_COLORS: 1

on:
push:
branches: ["main"]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
static_analysis:
timeout-minutes: 1

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run pre-commit
uses: pre-commit/action@v3.0.1
30 changes: 12 additions & 18 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,22 @@ on:
workflow_dispatch:

jobs:
publish-pypi-release:
pypi-publish:
name: Upload to PyPI
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
id-token: write
id-token: write # For PyPI's trusted publishing
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: "**/pyproject.toml"
- name: Install dependencies
run: |
pip install setuptools wheel build
fetch-depth: 0

- name: "Install uv"
uses: astral-sh/setup-uv@v3

- name: Build
run: |
python -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
run: uv build

- name: Publish to PyPi
run: uv publish -v dist/*
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ wheels/
.DS_Store


src/fastmcp/_version.py
src/fastmcp/_version.py

# editors
.cursorrules
.vscode/
20 changes: 20 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
fail_fast: true

repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.23
hooks:
- id: validate-pyproject

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
types_or: [yaml, json5]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff-format
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
1 change: 0 additions & 1 deletion examples/echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from fastmcp import FastMCP


# Create server
mcp = FastMCP("Echo Server")

Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
write_to = "src/fastmcp/_version.py"

[dependency-groups]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dev was not being recognized as a valid group

[project.optional-dependencies]
dev = [
"copychat>=0.5.2",
"ipython>=8.12.3",
"pdbpp>=0.10.3",
"pre-commit",
"pytest-xdist>=3.6.1",
"pytest>=8.3.3",
"pytest-asyncio>=0.23.5",
"ruff",
]

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
44 changes: 32 additions & 12 deletions src/fastmcp/resources/base.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
"""Base classes and interfaces for FastMCP resources."""

import abc
from typing import Union
from typing import Annotated, Union

from pydantic import BaseModel, Field, field_validator
from pydantic.networks import _BaseUrl
from pydantic import (
AnyUrl,
BaseModel,
BeforeValidator,
ConfigDict,
Field,
FileUrl,
ValidationInfo,
field_validator,
)


def maybe_cast_str_to_any_url(x) -> AnyUrl:
if isinstance(x, FileUrl):
return x
elif isinstance(x, AnyUrl):
return x
elif isinstance(x, str):
if x.startswith("file://"):
return FileUrl(x)
return AnyUrl(x)
raise ValueError(f"Expected str or AnyUrl, got {type(x)}")


LaxAnyUrl = Annotated[AnyUrl, BeforeValidator(maybe_cast_str_to_any_url)]


class Resource(BaseModel, abc.ABC):
"""Base class for all resources."""

uri: _BaseUrl = Field(description="URI of the resource")
name: str = Field(description="Name of the resource", default=None)
model_config = ConfigDict(validate_default=True)

uri: LaxAnyUrl = Field(description="URI of the resource")
name: str | None = Field(description="Name of the resource", default=None)
description: str | None = Field(
description="Description of the resource", default=None
)
Expand All @@ -23,13 +48,12 @@ class Resource(BaseModel, abc.ABC):

@field_validator("name", mode="before")
@classmethod
def set_default_name(cls, name: str | None, info) -> str:
def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
"""Set default name from URI if not provided."""
if name:
return name
# Extract everything after the protocol (e.g., "desktop" from "resource://desktop")
uri = info.data.get("uri")
if uri:
if uri := info.data.get("uri"):
uri_str = str(uri)
if "://" in uri_str:
name = uri_str.split("://", 1)[1]
Expand All @@ -41,7 +65,3 @@ def set_default_name(cls, name: str | None, info) -> str:
async def read(self) -> Union[str, bytes]:
"""Read the resource content."""
pass

model_config = {
"validate_default": True,
}
4 changes: 2 additions & 2 deletions src/fastmcp/resources/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Callable, Dict, Optional, Union

from pydantic.networks import _BaseUrl
from pydantic import AnyUrl

from fastmcp.resources.base import Resource
from fastmcp.resources.templates import ResourceTemplate
Expand Down Expand Up @@ -64,7 +64,7 @@ def add_template(
self._templates[template.uri_template] = template
return template

async def get_resource(self, uri: Union[_BaseUrl, str]) -> Optional[Resource]:
async def get_resource(self, uri: Union[AnyUrl, str]) -> Optional[Resource]:
"""Get resource by URI, checking concrete resources first, then templates."""
uri_str = str(uri)
logger.debug("Getting resource", extra={"uri": uri_str})
Expand Down
2 changes: 1 addition & 1 deletion src/fastmcp/resources/types.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Concrete resource implementations."""

import pydantic_core
import asyncio
import json
from pathlib import Path
from typing import Any, Callable, Union

import httpx
import pydantic.json
import pydantic_core
from pydantic import Field

from fastmcp.resources.base import Resource
Expand Down
Loading