Skip to content

feat(integrations): added django integration #10

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ FastOpenAPI follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

## [0.7.0] - Unreleased

### Added
- `DjangoRouter` for integration with the `Django` framework.

### Fixed
- Fixed issue with parsing repeated query parameters in URL.

Expand Down
3 changes: 2 additions & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ url: 'https://fastopenapi.fatalyst.dev/'
abstract: >-
FastOpenAPI is a library for generating and integrating OpenAPI schemas
using Pydantic v2 and various frameworks
(AioHttp, Falcon, Flask, Quart, Sanic, Starlette, Tornado).
(AioHttp, Falcon, Flask, Quart, Sanic, Starlette, Tornado, Django).
keywords:
- aiohttp
- falcon
Expand All @@ -23,4 +23,5 @@ keywords:
- tornado
- pydantic
- openapi
- django
license: MIT
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pip install fastopenapi[starlette]
```bash
pip install fastopenapi[tornado]
```
```bash
pip install fastopenapi[django]
```

---

Expand Down Expand Up @@ -275,6 +278,43 @@ pip install fastopenapi[tornado]
```
</details>

- ![Django](https://img.shields.io/badge/-Django-2980B9?style=flat&logo=python&logoColor=white)
<details>
<summary>Click to expand the Django Example</summary>

```python
from django.conf import settings
from django.core.management import call_command
from django.core.wsgi import get_wsgi_application
from django.urls import path
from pydantic import BaseModel

from fastopenapi.routers import DjangoRouter

settings.configure(DEBUG=True, SECRET_KEY="__CHANGEME__", ROOT_URLCONF=__name__)
application = get_wsgi_application()

router = DjangoRouter(app=True)


class HelloResponse(BaseModel):
message: str


@router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
def hello(name: str):
"""Say hello from django"""
return HelloResponse(message=f"Hello, {name}! It's Django!")


urlpatterns = [path("", router.urls)]

if __name__ == "__main__":
call_command("runserver")

```
</details>

### Step 2. Run the server

Launch the application:
Expand All @@ -299,7 +339,7 @@ http://127.0.0.1:8000/redoc
## ⚙️ Features
- **Generate OpenAPI schemas** with Pydantic v2.
- **Data validation** using Pydantic models.
- **Supports multiple frameworks:** AIOHTTP, Falcon, Flask, Quart, Sanic, Starlette, Tornado.
- **Supports multiple frameworks:** AIOHTTP, Falcon, Flask, Quart, Sanic, Starlette, Tornado, Django.
- **Proxy routing provides FastAPI-style routing**

---
Expand Down
1 change: 1 addition & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Each implementation runs in a separate instance, and the benchmark measures resp
- [Sanic](sanic/SANIC.md)
- [Starlette](starlette/STARLETTE.md)
- [Tornado](tornado/TORNADO.md)
- [Django](django/DJANGO.md)

### 📖 How It Works
- The script runs **10,000 requests per endpoint**. You can set your own value.
Expand Down
48 changes: 48 additions & 0 deletions benchmarks/django/DJANGO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Django Benchmark

---

## Testing Original Implementation
```
Original - Running 10000 iterations per endpoint
--------------------------------------------------
GET all records: 16.6954 sec total, 1.67 ms per request
GET one record: 17.9311 sec total, 1.79 ms per request
POST new record: 17.9401 sec total, 1.79 ms per request
PUT record: 17.6793 sec total, 1.77 ms per request
PATCH record: 17.6725 sec total, 1.77 ms per request
DELETE record: 38.5721 sec total, 3.86 ms per request
```
---

## Testing FastOpenAPI Implementation

```
FastOpenAPI - Running 10000 iterations per endpoint
--------------------------------------------------
GET all records: 21.4920 sec total, 2.15 ms per request
GET one record: 20.1516 sec total, 2.02 ms per request
POST new record: 20.0375 sec total, 2.00 ms per request
PUT record: 22.2360 sec total, 2.22 ms per request
PATCH record: 21.9553 sec total, 2.20 ms per request
DELETE record: 40.2411 sec total, 4.02 ms per request
```

---

## Performance Comparison (10000 iterations)

| Endpoint | Original | FastOpenAPI | Difference |
|-------------------------|----------|-------------|------------------|
| GET all records | 1.67 ms | 2.15 ms | 0.48 ms (+28.7%) |
| GET one record | 1.79 ms | 2.02 ms | 0.22 ms (+12.4%) |
| POST new record | 1.79 ms | 2.00 ms | 0.21 ms (+11.7%) |
| PUT record | 1.77 ms | 2.22 ms | 0.46 ms (+25.8%) |
| PATCH record | 1.77 ms | 2.20 ms | 0.43 ms (+24.2%) |
| DELETE record | 3.86 ms | 4.02 ms | 0.17 ms (+4.3%) |

---

[<< Back](../README.md)

---
107 changes: 107 additions & 0 deletions benchmarks/django/with_fastopenapi/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import uvicorn
from django.conf import settings
from django.core.asgi import get_asgi_application
from django.http import Http404
from django.urls import path

from benchmarks.django.with_fastopenapi.schemas import (
RecordCreate,
RecordResponse,
RecordUpdate,
)
from benchmarks.django.with_fastopenapi.storage import RecordStore
from fastopenapi.routers import DjangoRouter

# Initialize Django app and router
settings.configure(
DEBUG=False,
ALLOWED_HOSTS=["127.0.0.1"],
SECRET_KEY="__CHANGEME__",
ROOT_URLCONF=__name__,
)
application = get_asgi_application()
router = DjangoRouter(
app=True,
title="Record API",
description="A simple Record API built with FastOpenAPI and Django",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)

# Initialize the storage
store = RecordStore()


# Define routes using the DjangoRouter decorators
@router.get("/records", tags=["records"], response_model=list[RecordResponse])
def get_records():
"""
Get all records
"""
return store.get_all()


@router.get("/records/{record_id}", tags=["records"], response_model=RecordResponse)
def get_record(record_id: str):
"""
Get a specific record by ID
"""
record = store.get_by_id(record_id)
if not record:
raise Http404("Record not found")
return record


@router.post(
"/records", tags=["records"], status_code=201, response_model=RecordResponse
)
def create_record(record: RecordCreate):
"""
Create a new record
"""
return store.create(record)


@router.put("/records/{record_id}", tags=["records"], response_model=RecordResponse)
def update_record_full(record_id: str, record: RecordCreate):
"""
Update a record completely (all fields required)
"""
existing_record = store.get_by_id(record_id)
if not existing_record:
raise Http404("Record not found")

# Delete and recreate with the same ID
store.delete(record_id)
new_record = {"id": record_id, **record.model_dump()}
store.records[record_id] = new_record
return RecordResponse(**new_record)


@router.patch("/records/{record_id}", tags=["records"], response_model=RecordResponse)
def update_record_partial(record_id: str, record: RecordUpdate):
"""
Update a record partially (only specified fields)
"""
updated_record = store.update(record_id, record)
if not updated_record:
raise Http404("Record not found")
return updated_record


@router.delete("/records/{record_id}", tags=["records"], status_code=204)
def delete_record(record_id: str):
"""
Delete a record
"""
if not store.delete(record_id):
raise Http404("Record not found")
return None


urlpatterns = [path("", router.urls)]

if __name__ == "__main__":
uvicorn.run(application, host="127.0.0.1", port=8001)
20 changes: 20 additions & 0 deletions benchmarks/django/with_fastopenapi/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic import BaseModel, Field


class RecordCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=100)
description: str | None = Field(None, max_length=500)
is_completed: bool = Field(False)


class RecordUpdate(BaseModel):
title: str | None = Field(None, min_length=1, max_length=100)
description: str | None = Field(None, max_length=500)
is_completed: bool | None = None


class RecordResponse(BaseModel):
id: str
title: str
description: str | None = None
is_completed: bool
45 changes: 45 additions & 0 deletions benchmarks/django/with_fastopenapi/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import uuid

from .schemas import RecordCreate, RecordResponse, RecordUpdate


class RecordStore:
def __init__(self):
self.records: dict[str, dict] = {}

def create(self, record_data: RecordCreate) -> RecordResponse:
record_id = str(uuid.uuid4())
record = {"id": record_id, **record_data.model_dump()}
self.records[record_id] = record
return RecordResponse(**record)

def get_all(self) -> list[RecordResponse]:
return [RecordResponse(**record) for record in self.records.values()]

def get_by_id(self, record_id: str) -> RecordResponse | None:
record = self.records.get(record_id)
if record:
return RecordResponse(**record)
return None

def update(
self, record_id: str, record_data: RecordUpdate
) -> RecordResponse | None:
if record_id not in self.records:
return None

update_data = record_data.model_dump(exclude_unset=True)
if not update_data:
return RecordResponse(**self.records[record_id])

for key, value in update_data.items():
if value is not None:
self.records[record_id][key] = value

return RecordResponse(**self.records[record_id])

def delete(self, record_id: str) -> bool:
if record_id in self.records:
del self.records[record_id]
return True
return False
Loading