Skip to content

Commit 4e6491e

Browse files
authored
Merge pull request #3 from tadata-org/feature/django-drf-support
add native support for django (only DRF and drf-spectacular)
2 parents 1307e4c + 82b1950 commit 4e6491e

File tree

9 files changed

+944
-5
lines changed

9 files changed

+944
-5
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,43 @@ print(f"Deployed FastAPI app: {result.id}")
6666
```bash
6767
pip install fastapi
6868
```
69+
70+
## Django Support
71+
72+
You can deploy Django REST Framework applications directly using drf-spectacular:
73+
74+
```python
75+
import os
76+
import tadata_sdk
77+
78+
# Set up Django environment
79+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
80+
import django
81+
django.setup()
82+
83+
# Your Django settings should include:
84+
# INSTALLED_APPS = [
85+
# # ... your apps
86+
# 'rest_framework',
87+
# 'drf_spectacular',
88+
# ]
89+
# REST_FRAMEWORK = {
90+
# 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
91+
# }
92+
93+
# Deploy using Django schema extraction
94+
result = tadata_sdk.deploy(
95+
use_django=True, # Extract schema from configured Django application
96+
api_key="TADATA_API_KEY",
97+
base_url="https://api.myservice.com",
98+
name="My Django Deployment"
99+
)
100+
101+
print(f"Deployed Django app: {result.id}")
102+
```
103+
104+
**Note:** Django, Django REST Framework, and drf-spectacular are not required dependencies. If you want to use Django support, install them separately:
105+
106+
```bash
107+
pip install django djangorestframework drf-spectacular
108+
```

examples/03_django_example.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""
2+
Example: Deploying a Django REST Framework application as an MCP server.
3+
4+
This example shows how to deploy a Django REST Framework application
5+
using the Tadata SDK. This is a complete working example that sets up
6+
a minimal Django configuration with drf-spectacular.
7+
"""
8+
9+
import os
10+
import django
11+
from django.conf import settings
12+
from tadata_sdk import deploy
13+
14+
15+
def setup_django():
16+
"""Set up a minimal Django configuration for demonstration."""
17+
if not settings.configured:
18+
settings.configure(
19+
DEBUG=True,
20+
SECRET_KEY="demo-secret-key-not-for-production",
21+
INSTALLED_APPS=[
22+
"django.contrib.contenttypes",
23+
"django.contrib.auth",
24+
"rest_framework",
25+
"drf_spectacular",
26+
],
27+
REST_FRAMEWORK={
28+
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
29+
},
30+
SPECTACULAR_SETTINGS={
31+
"TITLE": "Demo Django API",
32+
"DESCRIPTION": "A demonstration Django REST Framework API",
33+
"VERSION": "1.0.0",
34+
},
35+
ROOT_URLCONF=__name__, # Use this module as the URL config
36+
USE_TZ=True,
37+
)
38+
django.setup()
39+
40+
41+
def create_demo_api():
42+
"""Create a simple Django REST API for demonstration."""
43+
from django.urls import path, include
44+
from rest_framework import serializers, viewsets, routers
45+
from rest_framework.decorators import api_view
46+
from rest_framework.response import Response
47+
48+
# Simple serializer
49+
class ItemSerializer(serializers.Serializer):
50+
id = serializers.IntegerField()
51+
name = serializers.CharField(max_length=100)
52+
description = serializers.CharField(max_length=500, required=False)
53+
54+
# Simple viewset
55+
class ItemViewSet(viewsets.ViewSet):
56+
"""A simple ViewSet for managing items."""
57+
58+
def list(self, request):
59+
"""List all items."""
60+
items = [
61+
{"id": 1, "name": "Item 1", "description": "First item"},
62+
{"id": 2, "name": "Item 2", "description": "Second item"},
63+
]
64+
serializer = ItemSerializer(items, many=True)
65+
return Response(serializer.data)
66+
67+
def retrieve(self, request, pk=None):
68+
"""Retrieve a specific item."""
69+
item = {"id": int(pk), "name": f"Item {pk}", "description": f"Item number {pk}"}
70+
serializer = ItemSerializer(item)
71+
return Response(serializer.data)
72+
73+
# Simple API view
74+
@api_view(["GET"])
75+
def hello_world(request):
76+
"""A simple hello world endpoint."""
77+
return Response({"message": "Hello from Django!"})
78+
79+
# Setup URL routing
80+
router = routers.DefaultRouter()
81+
router.register(r"items", ItemViewSet, basename="item")
82+
83+
# URL patterns (this module serves as ROOT_URLCONF)
84+
global urlpatterns
85+
urlpatterns = [
86+
path("api/", include(router.urls)),
87+
path("hello/", hello_world, name="hello"),
88+
]
89+
90+
91+
def main():
92+
"""Deploy Django REST Framework application."""
93+
print("Setting up Django configuration...")
94+
setup_django()
95+
96+
print("Creating demo API...")
97+
create_demo_api()
98+
99+
print("Django setup complete! Now deploying to Tadata...")
100+
101+
# Get API key (you would set this in your environment)
102+
api_key = os.getenv("TADATA_API_KEY")
103+
if not api_key:
104+
print("⚠️ TADATA_API_KEY environment variable not set.")
105+
print(" For a real deployment, you would need to set this.")
106+
print(" For this demo, we'll show what the call would look like:")
107+
print()
108+
print(" result = deploy(")
109+
print(" use_django=True,")
110+
print(" api_key='your-api-key-here',")
111+
print(" name='my-django-api',")
112+
print(" base_url='https://api.example.com'")
113+
print(" )")
114+
print()
115+
print("Let's test the Django schema extraction instead...")
116+
117+
# Test the schema extraction without actually deploying
118+
from tadata_sdk.openapi.source import OpenAPISpec
119+
120+
try:
121+
spec = OpenAPISpec.from_django()
122+
print("✅ Django schema extraction successful!")
123+
print(f" API Title: {spec.info.title}")
124+
print(f" API Version: {spec.info.version}")
125+
print(f" Available paths: {list(spec.paths.keys())}")
126+
print()
127+
print("This OpenAPI specification would be deployed to Tadata as an MCP server.")
128+
except Exception as e:
129+
print(f"❌ Schema extraction failed: {e}")
130+
return
131+
132+
# Deploy using Django schema extraction
133+
try:
134+
result = deploy(
135+
use_django=True, # Extract schema from configured Django application
136+
api_key=api_key,
137+
base_url="https://api.example.com", # Your Django API base URL
138+
)
139+
140+
print("✅ Deployment successful!")
141+
print(f" MCP Server ID: {result.id}")
142+
print(f" Created at: {result.created_at}")
143+
if result.updated:
144+
print(" Status: New deployment created")
145+
else:
146+
print(" Status: No changes detected, deployment skipped")
147+
148+
except Exception as e:
149+
print(f"❌ Deployment failed: {e}")
150+
151+
152+
if __name__ == "__main__":
153+
main()

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ dev = [
3939
"types-pyyaml>=6.0.12.20250516",
4040
"pip>=25.1.1",
4141
"fastapi>=0.115.12",
42+
"django>=5.2.1",
43+
"drf-spectacular>=0.28.0",
44+
"djangorestframework>=3.16.0",
45+
"django-stubs>=5.2.0",
46+
"djangorestframework-stubs>=3.16.0",
4247
]
4348

4449
[project.urls]

tadata_sdk/core/sdk.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ def deploy(
9898
) -> DeploymentResult: ...
9999

100100

101+
@overload
102+
def deploy(
103+
*,
104+
use_django: bool,
105+
api_key: str,
106+
base_url: Optional[str] = None,
107+
name: Optional[str] = None,
108+
auth_config: Optional[AuthConfig] = None,
109+
api_version: Literal["05-2025", "latest"] = "latest",
110+
timeout: int = 30,
111+
) -> DeploymentResult: ...
112+
113+
101114
def deploy(
102115
*,
103116
openapi_spec_path: Annotated[Optional[str], Doc("Path to an OpenAPI specification file (JSON or YAML)")] = None,
@@ -106,6 +119,7 @@ def deploy(
106119
Optional[Union[Dict[str, Any], OpenAPISpec]], Doc("OpenAPI specification as a dictionary or OpenAPISpec object")
107120
] = None,
108121
fastapi_app: Annotated[Optional[FastAPI], Doc("FastAPI application instance")] = None,
122+
use_django: Annotated[Optional[bool], Doc("Extract OpenAPI spec from Django application")] = None,
109123
base_url: Annotated[
110124
Optional[str],
111125
Doc("Base URL of the API to proxy requests to. If not provided, will try to extract from the OpenAPI spec"),
@@ -120,7 +134,7 @@ def deploy(
120134
) -> DeploymentResult:
121135
"""Deploy a Model Context Protocol (MCP) server from an OpenAPI specification.
122136
123-
You must provide exactly one of: openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app.
137+
You must provide exactly one of: openapi_spec_path, openapi_spec_url, openapi_spec, fastapi_app, or use_django=True.
124138
125139
Returns:
126140
A DeploymentResult object containing details of the deployment.
@@ -135,12 +149,16 @@ def deploy(
135149
logger.info("Deploying MCP server from OpenAPI spec")
136150

137151
# Validate input - must have exactly one openapi_spec source
138-
source_count = sum(1 for x in [openapi_spec_path, openapi_spec_url, openapi_spec, fastapi_app] if x is not None)
152+
source_count = sum(
153+
1 for x in [openapi_spec_path, openapi_spec_url, openapi_spec, fastapi_app, use_django] if x is not None
154+
)
139155
if source_count == 0:
140-
raise ValueError("One of openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app must be provided")
156+
raise ValueError(
157+
"One of openapi_spec_path, openapi_spec_url, openapi_spec, fastapi_app, or use_django=True must be provided"
158+
)
141159
if source_count > 1:
142160
raise ValueError(
143-
"Only one of openapi_spec_path, openapi_spec_url, openapi_spec, or fastapi_app should be provided"
161+
"Only one of openapi_spec_path, openapi_spec_url, openapi_spec, fastapi_app, or use_django=True should be provided"
144162
)
145163

146164
# Process OpenAPI spec from the provided source
@@ -182,6 +200,9 @@ def deploy(
182200
elif fastapi_app is not None:
183201
logger.info("Loading OpenAPI spec from FastAPI app")
184202
spec = OpenAPISpec.from_fastapi(fastapi_app)
203+
elif use_django:
204+
logger.info("Loading OpenAPI spec from Django app")
205+
spec = OpenAPISpec.from_django()
185206
elif isinstance(openapi_spec, dict):
186207
logger.info("Using provided OpenAPI spec dictionary")
187208
spec = OpenAPISpec.from_dict(openapi_spec)

tadata_sdk/openapi/source.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,89 @@ def from_fastapi(cls, app: "FastAPI") -> "OpenAPISpec":
186186
details={"app_title": getattr(app, "title", "Unknown")},
187187
cause=e,
188188
)
189+
190+
@classmethod
191+
def from_django(cls) -> "OpenAPISpec":
192+
"""Create an OpenAPISpec instance from a Django application.
193+
194+
This method requires Django REST Framework and drf-spectacular to be installed
195+
and properly configured in the Django application.
196+
197+
Returns:
198+
An OpenAPISpec instance.
199+
200+
Raises:
201+
SpecInvalidError: If Django, DRF, or drf-spectacular are not installed,
202+
or the OpenAPI specification cannot be extracted.
203+
"""
204+
try:
205+
import django
206+
from django.conf import settings
207+
except ImportError as e:
208+
raise SpecInvalidError(
209+
"Django is not installed. Please install it with: pip install django",
210+
details={"missing_package": "django"},
211+
cause=e,
212+
)
213+
214+
try:
215+
# Check if Django REST Framework is installed
216+
import rest_framework # noqa: F401
217+
except ImportError as e:
218+
raise SpecInvalidError(
219+
"Django REST Framework is not installed. Please install it with: pip install djangorestframework",
220+
details={"missing_package": "djangorestframework"},
221+
cause=e,
222+
)
223+
224+
try:
225+
# Check if drf-spectacular is installed
226+
import drf_spectacular # noqa: F401
227+
from drf_spectacular.openapi import AutoSchema # noqa: F401
228+
except ImportError as e:
229+
raise SpecInvalidError(
230+
"drf-spectacular is not installed. Please install it with: pip install drf-spectacular",
231+
details={"missing_package": "drf-spectacular"},
232+
cause=e,
233+
)
234+
235+
try:
236+
# Ensure Django is configured
237+
if not settings.configured:
238+
raise SpecInvalidError(
239+
"Django settings are not configured. Please ensure Django is properly set up.",
240+
details={"django_configured": False},
241+
)
242+
243+
# Check if drf-spectacular is in INSTALLED_APPS
244+
if "drf_spectacular" not in settings.INSTALLED_APPS:
245+
raise SpecInvalidError(
246+
"drf-spectacular is not in INSTALLED_APPS. Please add 'drf_spectacular' to your INSTALLED_APPS setting.",
247+
details={"missing_app": "drf_spectacular"},
248+
)
249+
250+
# Check if the schema class is configured
251+
rest_framework_settings = getattr(settings, "REST_FRAMEWORK", {})
252+
schema_class = rest_framework_settings.get("DEFAULT_SCHEMA_CLASS")
253+
254+
if schema_class != "drf_spectacular.openapi.AutoSchema":
255+
raise SpecInvalidError(
256+
"drf-spectacular AutoSchema is not configured. Please set REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS'] = 'drf_spectacular.openapi.AutoSchema'",
257+
details={"current_schema_class": schema_class},
258+
)
259+
260+
# Generate the OpenAPI schema using drf-spectacular
261+
# Use the generator directly instead of SpectacularAPIView
262+
from drf_spectacular.generators import SchemaGenerator
263+
264+
generator = SchemaGenerator()
265+
schema_dict = generator.get_schema(request=None, public=True)
266+
267+
return cls.from_dict(schema_dict)
268+
269+
except Exception as e:
270+
raise SpecInvalidError(
271+
f"Failed to extract OpenAPI specification from Django app: {str(e)}",
272+
details={"django_version": getattr(django, "VERSION", "Unknown")},
273+
cause=e,
274+
)

0 commit comments

Comments
 (0)