Skip to content

add built in fastapi support #2

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

Merged
merged 1 commit into from
May 25, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,6 @@ node_modules/
# OS specific
# Task files
tasks.json
tasks/
tasks/

instructions.txt
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,52 @@ pip install tadata-sdk
Deploy a Model Context Protocol (MCP) server with your OpenAPI specification:

```python
from tadata_sdk import deploy, OpenAPISpec
import tadata_sdk

# Deploy from a dictionary
result = deploy(
result = tadata_sdk.deploy(
openapi_spec={
"openapi": "3.0.0",
"info": {"title": "My API", "version": "1.0.0"},
"paths": {"/hello": {"get": {"responses": {"200": {"description": "OK"}}}}},
},
api_key="your-tadata-api-key",
api_key="TADATA_API_KEY", # Required
name="My MCP Deployment", # Optional
base_url="https://api.myservice.com", # Optional
base_url="https://api.myservice.com", # Required if no valid and absolute base url is found in the openapi spec
)

print(f"Deployed MCP server: {result.id}")
print(f"Created at: {result.created_at}")
```

## FastAPI Support

You can deploy FastAPI applications directly without manually extracting the OpenAPI specification:

```python
import tadata_sdk
from fastapi import FastAPI

# Create your FastAPI app
app = FastAPI(title="My API", version="1.0.0")

@app.get("/hello")
def hello():
return {"message": "Hello World"}

# Deploy the FastAPI app directly
result = tadata_sdk.deploy(
fastapi_app=app,
api_key="TADATA_API_KEY",
base_url="https://api.myservice.com",
name="My FastAPI Deployment"
)

print(f"Deployed FastAPI app: {result.id}")
```

**Note:** FastAPI is not a required dependency. If you want to use FastAPI support, install it separately:

```bash
pip install fastapi
```
44 changes: 44 additions & 0 deletions examples/02_fastapi_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import logging
import os

import tadata_sdk

# Configure logging to display SDK logs
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")

try:
from fastapi import FastAPI

# Create a simple FastAPI app
app = FastAPI(title="My API", version="1.0.0", description="A simple API example for Tadata deployment")

@app.get("/")
def read_root():
"""Root endpoint that returns a greeting."""
return {"message": "Hello World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
"""Get an item by ID with optional query parameter."""
return {"item_id": item_id, "q": q}

@app.post("/items/")
def create_item(item: dict):
"""Create a new item."""
return {"item": item, "status": "created"}

# Deploy the FastAPI app directly
result = tadata_sdk.deploy(
api_key=os.getenv("TADATA_API_KEY", ""),
fastapi_app=app,
base_url="https://my-api.example.com", # Your actual API base URL
name="My FastAPI Deployment",
)

print(f"Deployment successful! ID: {result.id}")
print(f"Created at: {result.created_at}")
print(f"Updated: {result.updated}")

except ImportError:
print("FastAPI is not installed.")
print("This example demonstrates how to deploy a FastAPI app using tadata-sdk.")
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dev = [
"pre-commit>=4.2.0",
"types-pyyaml>=6.0.12.20250516",
"pip>=25.1.1",
"fastapi>=0.115.12",
]

[project.urls]
Expand Down
34 changes: 29 additions & 5 deletions tadata_sdk/core/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import logging
import urllib.parse
from datetime import datetime
from typing import Any, Dict, Literal, Optional, Union, overload
from typing import Any, Dict, Literal, Optional, Union, overload, TYPE_CHECKING
from typing_extensions import Annotated, Doc

if TYPE_CHECKING:
from fastapi import FastAPI
else:
FastAPI = Any

from ..errors.exceptions import SpecInvalidError
from ..http.client import ApiClient
from ..http.schemas import DeploymentResponse, AuthConfig, UpsertDeploymentRequest
Expand Down Expand Up @@ -80,13 +85,27 @@ def deploy(
) -> DeploymentResult: ...


@overload
def deploy(
*,
fastapi_app: FastAPI,
api_key: str,
base_url: Optional[str] = None,
name: Optional[str] = None,
auth_config: Optional[AuthConfig] = None,
api_version: Literal["05-2025", "latest"] = "latest",
timeout: int = 30,
) -> DeploymentResult: ...


def deploy(
*,
openapi_spec_path: Annotated[Optional[str], Doc("Path to an OpenAPI specification file (JSON or YAML)")] = None,
openapi_spec_url: Annotated[Optional[str], Doc("URL to an OpenAPI specification")] = None,
openapi_spec: Annotated[
Optional[Union[Dict[str, Any], OpenAPISpec]], Doc("OpenAPI specification as a dictionary or OpenAPISpec object")
] = None,
fastapi_app: Annotated[Optional[FastAPI], Doc("FastAPI application instance")] = None,
base_url: Annotated[
Optional[str],
Doc("Base URL of the API to proxy requests to. If not provided, will try to extract from the OpenAPI spec"),
Expand All @@ -101,7 +120,7 @@ def deploy(
) -> DeploymentResult:
"""Deploy a Model Context Protocol (MCP) server from an OpenAPI specification.

You must provide exactly one of: openapi_spec_path, openapi_spec_url, or openapi_spec.
You must provide exactly one of: openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app.

Returns:
A DeploymentResult object containing details of the deployment.
Expand All @@ -116,11 +135,13 @@ def deploy(
logger.info("Deploying MCP server from OpenAPI spec")

# Validate input - must have exactly one openapi_spec source
source_count = sum(1 for x in [openapi_spec_path, openapi_spec_url, openapi_spec] if x is not None)
source_count = sum(1 for x in [openapi_spec_path, openapi_spec_url, openapi_spec, fastapi_app] if x is not None)
if source_count == 0:
raise ValueError("One of openapi_spec_path, openapi_spec_url, or openapi_spec must be provided")
raise ValueError("One of openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app must be provided")
if source_count > 1:
raise ValueError("Only one of openapi_spec_path, openapi_spec_url, or openapi_spec should be provided")
raise ValueError(
"Only one of openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app should be provided"
)

# Process OpenAPI spec from the provided source
spec: Optional[OpenAPISpec] = None
Expand Down Expand Up @@ -158,6 +179,9 @@ def deploy(
details={"url": openapi_spec_url},
cause=e,
)
elif fastapi_app is not None:
logger.info("Loading OpenAPI spec from FastAPI app")
spec = OpenAPISpec.from_fastapi(fastapi_app)
elif isinstance(openapi_spec, dict):
logger.info("Using provided OpenAPI spec dictionary")
spec = OpenAPISpec.from_dict(openapi_spec)
Expand Down
45 changes: 44 additions & 1 deletion tadata_sdk/openapi/source.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import json
from pathlib import Path
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Optional, Union, TYPE_CHECKING

import yaml
from pydantic import BaseModel, ConfigDict, Field, field_validator

from ..errors.exceptions import SpecInvalidError

if TYPE_CHECKING:
from fastapi import FastAPI


class OpenAPIInfo(BaseModel):
"""OpenAPI info object."""
Expand Down Expand Up @@ -143,3 +146,43 @@ def from_file(cls, file_path: Union[str, Path]) -> "OpenAPISpec":
details={"file_path": str(file_path)},
cause=e,
)

@classmethod
def from_fastapi(cls, app: "FastAPI") -> "OpenAPISpec":
"""Create an OpenAPISpec instance from a FastAPI application.

Args:
app: A FastAPI application instance.

Returns:
An OpenAPISpec instance.

Raises:
SpecInvalidError: If FastAPI is not installed, the app is not a FastAPI instance,
or the OpenAPI specification cannot be extracted.
"""
try:
from fastapi import FastAPI
except ImportError as e:
raise SpecInvalidError(
"FastAPI is not installed. Please install it with: pip install fastapi",
details={"missing_package": "fastapi"},
cause=e,
)

if not isinstance(app, FastAPI):
raise SpecInvalidError(
f"Expected a FastAPI instance, got {type(app).__name__}",
details={"app_type": type(app).__name__},
)

try:
# Get the OpenAPI schema from the FastAPI app
openapi_schema = app.openapi()
return cls.from_dict(openapi_schema)
except Exception as e:
raise SpecInvalidError(
f"Failed to extract OpenAPI specification from FastAPI app: {str(e)}",
details={"app_title": getattr(app, "title", "Unknown")},
cause=e,
)
88 changes: 88 additions & 0 deletions tests/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ def test_deploy_invalid_input():
assert "Only one of" in str(exc_info.value)


def test_deploy_invalid_input_with_fastapi():
"""Test that providing multiple inputs including FastAPI raises appropriate errors."""
try:
from fastapi import FastAPI

app = FastAPI()

# Multiple spec sources including FastAPI
with pytest.raises(ValueError) as exc_info:
deploy(
openapi_spec={"openapi": "3.0.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": {}},
fastapi_app=app,
api_key="test-api-key",
)
assert "Only one of" in str(exc_info.value)
assert "fastapi_app" in str(exc_info.value)

except ImportError:
pytest.skip("FastAPI not available for testing")


@patch("tadata_sdk.core.sdk.OpenAPISpec.from_file")
def test_deploy_from_file(mock_from_file, mock_api_client):
"""Test deploying from a file."""
Expand Down Expand Up @@ -140,3 +161,70 @@ def test_deploy_from_file(mock_from_file, mock_api_client):

# Check the result
assert result.id == "test-deployment-id"


@patch("tadata_sdk.core.sdk.OpenAPISpec.from_fastapi")
def test_deploy_from_fastapi(mock_from_fastapi, mock_api_client, valid_openapi_dict):
"""Test deploying from a FastAPI app."""
try:
from fastapi import FastAPI

# Create a FastAPI app
app = FastAPI(title="Test API", version="1.0.0")

# Set up the mock to return a valid spec
mock_spec = OpenAPISpec.model_validate(valid_openapi_dict)
mock_from_fastapi.return_value = mock_spec

# Call deploy with a FastAPI app
result = deploy(
fastapi_app=app,
api_key="test-api-key",
)

# Check that the FastAPI app was processed
mock_from_fastapi.assert_called_once_with(app)

# Check that the client was used correctly
mock_api_client.deploy_from_openapi.assert_called_once()

# Check the result
assert result.id == "test-deployment-id"

except ImportError:
pytest.skip("FastAPI not available for testing")


def test_deploy_from_fastapi_real_app(mock_api_client, valid_openapi_dict):
"""Test deploying from a real FastAPI app (integration test)."""
try:
from fastapi import FastAPI

# Create a real FastAPI app
app = FastAPI(title="Test API", version="1.0.0")

@app.get("/test")
def test_endpoint():
return {"message": "test"}

# Call deploy with the real FastAPI app
result = deploy(
fastapi_app=app,
api_key="test-api-key",
)

# Check that the client was used correctly
mock_api_client.deploy_from_openapi.assert_called_once()

# Check the result
assert result.id == "test-deployment-id"

# Verify that the OpenAPI spec was extracted correctly
call_args = mock_api_client.deploy_from_openapi.call_args
request = call_args[0][0]
assert request.open_api_spec.info.title == "Test API"
assert request.open_api_spec.info.version == "1.0.0"
assert "/test" in request.open_api_spec.paths

except ImportError:
pytest.skip("FastAPI not available for testing")
Loading