Skip to content

Commit 1307e4c

Browse files
authored
Merge pull request #2 from tadata-org/feature/fastapi-support
add built in fastapi support
2 parents ac0e49b + 7d2dc38 commit 1307e4c

File tree

9 files changed

+340
-11
lines changed

9 files changed

+340
-11
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,6 @@ node_modules/
178178
# OS specific
179179
# Task files
180180
tasks.json
181-
tasks/
181+
tasks/
182+
183+
instructions.txt

README.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,52 @@ pip install tadata-sdk
1717
Deploy a Model Context Protocol (MCP) server with your OpenAPI specification:
1818

1919
```python
20-
from tadata_sdk import deploy, OpenAPISpec
20+
import tadata_sdk
2121

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

3434
print(f"Deployed MCP server: {result.id}")
3535
print(f"Created at: {result.created_at}")
3636
```
37+
38+
## FastAPI Support
39+
40+
You can deploy FastAPI applications directly without manually extracting the OpenAPI specification:
41+
42+
```python
43+
import tadata_sdk
44+
from fastapi import FastAPI
45+
46+
# Create your FastAPI app
47+
app = FastAPI(title="My API", version="1.0.0")
48+
49+
@app.get("/hello")
50+
def hello():
51+
return {"message": "Hello World"}
52+
53+
# Deploy the FastAPI app directly
54+
result = tadata_sdk.deploy(
55+
fastapi_app=app,
56+
api_key="TADATA_API_KEY",
57+
base_url="https://api.myservice.com",
58+
name="My FastAPI Deployment"
59+
)
60+
61+
print(f"Deployed FastAPI app: {result.id}")
62+
```
63+
64+
**Note:** FastAPI is not a required dependency. If you want to use FastAPI support, install it separately:
65+
66+
```bash
67+
pip install fastapi
68+
```

examples/02_fastapi_example.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import logging
2+
import os
3+
4+
import tadata_sdk
5+
6+
# Configure logging to display SDK logs
7+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
8+
9+
try:
10+
from fastapi import FastAPI
11+
12+
# Create a simple FastAPI app
13+
app = FastAPI(title="My API", version="1.0.0", description="A simple API example for Tadata deployment")
14+
15+
@app.get("/")
16+
def read_root():
17+
"""Root endpoint that returns a greeting."""
18+
return {"message": "Hello World"}
19+
20+
@app.get("/items/{item_id}")
21+
def read_item(item_id: int, q: str | None = None):
22+
"""Get an item by ID with optional query parameter."""
23+
return {"item_id": item_id, "q": q}
24+
25+
@app.post("/items/")
26+
def create_item(item: dict):
27+
"""Create a new item."""
28+
return {"item": item, "status": "created"}
29+
30+
# Deploy the FastAPI app directly
31+
result = tadata_sdk.deploy(
32+
api_key=os.getenv("TADATA_API_KEY", ""),
33+
fastapi_app=app,
34+
base_url="https://my-api.example.com", # Your actual API base URL
35+
name="My FastAPI Deployment",
36+
)
37+
38+
print(f"Deployment successful! ID: {result.id}")
39+
print(f"Created at: {result.created_at}")
40+
print(f"Updated: {result.updated}")
41+
42+
except ImportError:
43+
print("FastAPI is not installed.")
44+
print("This example demonstrates how to deploy a FastAPI app using tadata-sdk.")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dev = [
3838
"pre-commit>=4.2.0",
3939
"types-pyyaml>=6.0.12.20250516",
4040
"pip>=25.1.1",
41+
"fastapi>=0.115.12",
4142
]
4243

4344
[project.urls]

tadata_sdk/core/sdk.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
import logging
44
import urllib.parse
55
from datetime import datetime
6-
from typing import Any, Dict, Literal, Optional, Union, overload
6+
from typing import Any, Dict, Literal, Optional, Union, overload, TYPE_CHECKING
77
from typing_extensions import Annotated, Doc
88

9+
if TYPE_CHECKING:
10+
from fastapi import FastAPI
11+
else:
12+
FastAPI = Any
13+
914
from ..errors.exceptions import SpecInvalidError
1015
from ..http.client import ApiClient
1116
from ..http.schemas import DeploymentResponse, AuthConfig, UpsertDeploymentRequest
@@ -80,13 +85,27 @@ def deploy(
8085
) -> DeploymentResult: ...
8186

8287

88+
@overload
89+
def deploy(
90+
*,
91+
fastapi_app: FastAPI,
92+
api_key: str,
93+
base_url: Optional[str] = None,
94+
name: Optional[str] = None,
95+
auth_config: Optional[AuthConfig] = None,
96+
api_version: Literal["05-2025", "latest"] = "latest",
97+
timeout: int = 30,
98+
) -> DeploymentResult: ...
99+
100+
83101
def deploy(
84102
*,
85103
openapi_spec_path: Annotated[Optional[str], Doc("Path to an OpenAPI specification file (JSON or YAML)")] = None,
86104
openapi_spec_url: Annotated[Optional[str], Doc("URL to an OpenAPI specification")] = None,
87105
openapi_spec: Annotated[
88106
Optional[Union[Dict[str, Any], OpenAPISpec]], Doc("OpenAPI specification as a dictionary or OpenAPISpec object")
89107
] = None,
108+
fastapi_app: Annotated[Optional[FastAPI], Doc("FastAPI application instance")] = None,
90109
base_url: Annotated[
91110
Optional[str],
92111
Doc("Base URL of the API to proxy requests to. If not provided, will try to extract from the OpenAPI spec"),
@@ -101,7 +120,7 @@ def deploy(
101120
) -> DeploymentResult:
102121
"""Deploy a Model Context Protocol (MCP) server from an OpenAPI specification.
103122
104-
You must provide exactly one of: openapi_spec_path, openapi_spec_url, or openapi_spec.
123+
You must provide exactly one of: openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app.
105124
106125
Returns:
107126
A DeploymentResult object containing details of the deployment.
@@ -116,11 +135,13 @@ def deploy(
116135
logger.info("Deploying MCP server from OpenAPI spec")
117136

118137
# Validate input - must have exactly one openapi_spec source
119-
source_count = sum(1 for x in [openapi_spec_path, openapi_spec_url, openapi_spec] if x is not None)
138+
source_count = sum(1 for x in [openapi_spec_path, openapi_spec_url, openapi_spec, fastapi_app] if x is not None)
120139
if source_count == 0:
121-
raise ValueError("One of openapi_spec_path, openapi_spec_url, or openapi_spec must be provided")
140+
raise ValueError("One of openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app must be provided")
122141
if source_count > 1:
123-
raise ValueError("Only one of openapi_spec_path, openapi_spec_url, or openapi_spec should be provided")
142+
raise ValueError(
143+
"Only one of openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app should be provided"
144+
)
124145

125146
# Process OpenAPI spec from the provided source
126147
spec: Optional[OpenAPISpec] = None
@@ -158,6 +179,9 @@ def deploy(
158179
details={"url": openapi_spec_url},
159180
cause=e,
160181
)
182+
elif fastapi_app is not None:
183+
logger.info("Loading OpenAPI spec from FastAPI app")
184+
spec = OpenAPISpec.from_fastapi(fastapi_app)
161185
elif isinstance(openapi_spec, dict):
162186
logger.info("Using provided OpenAPI spec dictionary")
163187
spec = OpenAPISpec.from_dict(openapi_spec)

tadata_sdk/openapi/source.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import json
22
from pathlib import Path
3-
from typing import Any, Dict, Optional, Union
3+
from typing import Any, Dict, Optional, Union, TYPE_CHECKING
44

55
import yaml
66
from pydantic import BaseModel, ConfigDict, Field, field_validator
77

88
from ..errors.exceptions import SpecInvalidError
99

10+
if TYPE_CHECKING:
11+
from fastapi import FastAPI
12+
1013

1114
class OpenAPIInfo(BaseModel):
1215
"""OpenAPI info object."""
@@ -143,3 +146,43 @@ def from_file(cls, file_path: Union[str, Path]) -> "OpenAPISpec":
143146
details={"file_path": str(file_path)},
144147
cause=e,
145148
)
149+
150+
@classmethod
151+
def from_fastapi(cls, app: "FastAPI") -> "OpenAPISpec":
152+
"""Create an OpenAPISpec instance from a FastAPI application.
153+
154+
Args:
155+
app: A FastAPI application instance.
156+
157+
Returns:
158+
An OpenAPISpec instance.
159+
160+
Raises:
161+
SpecInvalidError: If FastAPI is not installed, the app is not a FastAPI instance,
162+
or the OpenAPI specification cannot be extracted.
163+
"""
164+
try:
165+
from fastapi import FastAPI
166+
except ImportError as e:
167+
raise SpecInvalidError(
168+
"FastAPI is not installed. Please install it with: pip install fastapi",
169+
details={"missing_package": "fastapi"},
170+
cause=e,
171+
)
172+
173+
if not isinstance(app, FastAPI):
174+
raise SpecInvalidError(
175+
f"Expected a FastAPI instance, got {type(app).__name__}",
176+
details={"app_type": type(app).__name__},
177+
)
178+
179+
try:
180+
# Get the OpenAPI schema from the FastAPI app
181+
openapi_schema = app.openapi()
182+
return cls.from_dict(openapi_schema)
183+
except Exception as e:
184+
raise SpecInvalidError(
185+
f"Failed to extract OpenAPI specification from FastAPI app: {str(e)}",
186+
details={"app_title": getattr(app, "title", "Unknown")},
187+
cause=e,
188+
)

tests/test_deploy.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ def test_deploy_invalid_input():
113113
assert "Only one of" in str(exc_info.value)
114114

115115

116+
def test_deploy_invalid_input_with_fastapi():
117+
"""Test that providing multiple inputs including FastAPI raises appropriate errors."""
118+
try:
119+
from fastapi import FastAPI
120+
121+
app = FastAPI()
122+
123+
# Multiple spec sources including FastAPI
124+
with pytest.raises(ValueError) as exc_info:
125+
deploy(
126+
openapi_spec={"openapi": "3.0.0", "info": {"title": "Test", "version": "1.0.0"}, "paths": {}},
127+
fastapi_app=app,
128+
api_key="test-api-key",
129+
)
130+
assert "Only one of" in str(exc_info.value)
131+
assert "fastapi_app" in str(exc_info.value)
132+
133+
except ImportError:
134+
pytest.skip("FastAPI not available for testing")
135+
136+
116137
@patch("tadata_sdk.core.sdk.OpenAPISpec.from_file")
117138
def test_deploy_from_file(mock_from_file, mock_api_client):
118139
"""Test deploying from a file."""
@@ -140,3 +161,70 @@ def test_deploy_from_file(mock_from_file, mock_api_client):
140161

141162
# Check the result
142163
assert result.id == "test-deployment-id"
164+
165+
166+
@patch("tadata_sdk.core.sdk.OpenAPISpec.from_fastapi")
167+
def test_deploy_from_fastapi(mock_from_fastapi, mock_api_client, valid_openapi_dict):
168+
"""Test deploying from a FastAPI app."""
169+
try:
170+
from fastapi import FastAPI
171+
172+
# Create a FastAPI app
173+
app = FastAPI(title="Test API", version="1.0.0")
174+
175+
# Set up the mock to return a valid spec
176+
mock_spec = OpenAPISpec.model_validate(valid_openapi_dict)
177+
mock_from_fastapi.return_value = mock_spec
178+
179+
# Call deploy with a FastAPI app
180+
result = deploy(
181+
fastapi_app=app,
182+
api_key="test-api-key",
183+
)
184+
185+
# Check that the FastAPI app was processed
186+
mock_from_fastapi.assert_called_once_with(app)
187+
188+
# Check that the client was used correctly
189+
mock_api_client.deploy_from_openapi.assert_called_once()
190+
191+
# Check the result
192+
assert result.id == "test-deployment-id"
193+
194+
except ImportError:
195+
pytest.skip("FastAPI not available for testing")
196+
197+
198+
def test_deploy_from_fastapi_real_app(mock_api_client, valid_openapi_dict):
199+
"""Test deploying from a real FastAPI app (integration test)."""
200+
try:
201+
from fastapi import FastAPI
202+
203+
# Create a real FastAPI app
204+
app = FastAPI(title="Test API", version="1.0.0")
205+
206+
@app.get("/test")
207+
def test_endpoint():
208+
return {"message": "test"}
209+
210+
# Call deploy with the real FastAPI app
211+
result = deploy(
212+
fastapi_app=app,
213+
api_key="test-api-key",
214+
)
215+
216+
# Check that the client was used correctly
217+
mock_api_client.deploy_from_openapi.assert_called_once()
218+
219+
# Check the result
220+
assert result.id == "test-deployment-id"
221+
222+
# Verify that the OpenAPI spec was extracted correctly
223+
call_args = mock_api_client.deploy_from_openapi.call_args
224+
request = call_args[0][0]
225+
assert request.open_api_spec.info.title == "Test API"
226+
assert request.open_api_spec.info.version == "1.0.0"
227+
assert "/test" in request.open_api_spec.paths
228+
229+
except ImportError:
230+
pytest.skip("FastAPI not available for testing")

0 commit comments

Comments
 (0)