Skip to content

Commit 1ea1725

Browse files
committed
Added benchmark for AioHttpRouter
1 parent c82a356 commit 1ea1725

File tree

9 files changed

+374
-1
lines changed

9 files changed

+374
-1
lines changed

benchmarks/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Each implementation runs in a separate instance, and the benchmark measures resp
88

99
### 📈 Rough results
1010
- You can check rough results here:
11+
- [AioHttp](aiohttp/AIOHTTP.md)
1112
- [Falcon](falcon/FALCON.md)
1213
- [Flask](flask/FLASK.md)
1314
- [Quart](quart/QUART.md)

benchmarks/aiohttp/AIOHTTP.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# AioHttp Benchmark
2+
3+
---
4+
5+
## Testing Original Implementation
6+
```
7+
Original - Running 10000 iterations per endpoint
8+
--------------------------------------------------
9+
GET all records: 8.5742 sec total, 0.86 ms per request
10+
GET one record: 8.7001 sec total, 0.87 ms per request
11+
POST new record: 9.0483 sec total, 0.90 ms per request
12+
PUT record: 9.1385 sec total, 0.91 ms per request
13+
PATCH record: 9.1465 sec total, 0.91 ms per request
14+
DELETE record: 17.7022 sec total, 1.77 ms per request
15+
```
16+
---
17+
18+
## Testing FastOpenAPI Implementation
19+
20+
```
21+
FastOpenAPI - Running 10000 iterations per endpoint
22+
--------------------------------------------------
23+
GET all records: 8.7114 sec total, 0.87 ms per request
24+
GET one record: 9.2981 sec total, 0.93 ms per request
25+
POST new record: 9.4486 sec total, 0.94 ms per request
26+
PUT record: 9.4418 sec total, 0.94 ms per request
27+
PATCH record: 9.4253 sec total, 0.94 ms per request
28+
DELETE record: 18.2327 sec total, 1.82 ms per request
29+
```
30+
31+
---
32+
33+
## Performance Comparison (10000 iterations)
34+
35+
| Endpoint | Original | FastOpenAPI | Difference |
36+
|------------------|----------|-------------|-----------------|
37+
| GET all records | 0.86 ms | 0.87 ms | +0.01 ms (+1.6%) |
38+
| GET one record | 0.87 ms | 0.93 ms | +0.06 ms (+6.9%) |
39+
| POST new record | 0.90 ms | 0.94 ms | +0.04 ms (+4.4%) |
40+
| PUT record | 0.91 ms | 0.94 ms | +0.03 ms (+3.3%) |
41+
| PATCH record | 0.91 ms | 0.94 ms | +0.03 ms (+3.0%) |
42+
| DELETE record | 1.77 ms | 1.82 ms | +0.05 ms (+3.0%) |
43+
44+
45+
46+
---
47+
48+
[<< Back](../README.md)
49+
50+
---
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from aiohttp import web
2+
3+
from benchmarks.aiohttp.with_fastopenapi.schemas import (
4+
RecordCreate,
5+
RecordResponse,
6+
RecordUpdate,
7+
)
8+
from benchmarks.aiohttp.with_fastopenapi.storage import RecordStore
9+
from fastopenapi.routers import AioHttpRouter
10+
11+
# Initialize AioHttp app and router
12+
app = web.Application()
13+
router = AioHttpRouter(
14+
app=app,
15+
title="Record API",
16+
description="A simple Record API built with FastOpenAPI and AioHttp",
17+
version="1.0.0",
18+
docs_url="/docs",
19+
redoc_url="/redoc",
20+
openapi_url="/openapi.json",
21+
)
22+
23+
# Initialize the storage
24+
store = RecordStore()
25+
26+
27+
# Define routes using the AioHttpRouter decorators
28+
@router.get("/records", tags=["records"], response_model=list[RecordResponse])
29+
async def get_records():
30+
"""
31+
Get all records
32+
"""
33+
return store.get_all()
34+
35+
36+
@router.get("/records/{record_id}", tags=["records"], response_model=RecordResponse)
37+
async def get_record(record_id: str):
38+
"""
39+
Get a specific record by ID
40+
"""
41+
record = store.get_by_id(record_id)
42+
if not record:
43+
raise web.HTTPNotFound(reason="Not Found")
44+
return record
45+
46+
47+
@router.post(
48+
"/records", tags=["records"], status_code=201, response_model=RecordResponse
49+
)
50+
async def create_record(record: RecordCreate):
51+
"""
52+
Create a new record
53+
"""
54+
return store.create(record)
55+
56+
57+
@router.put("/records/{record_id}", tags=["records"], response_model=RecordResponse)
58+
async def update_record_full(record_id: str, record: RecordCreate):
59+
"""
60+
Update a record completely (all fields required)
61+
"""
62+
existing_record = store.get_by_id(record_id)
63+
if not existing_record:
64+
raise web.HTTPNotFound(reason="Not Found")
65+
66+
# Delete and recreate with the same ID
67+
store.delete(record_id)
68+
new_record = {"id": record_id, **record.model_dump()}
69+
store.records[record_id] = new_record
70+
return RecordResponse(**new_record)
71+
72+
73+
@router.patch("/records/{record_id}", tags=["records"], response_model=RecordResponse)
74+
async def update_record_partial(record_id: str, record: RecordUpdate):
75+
"""
76+
Update a record partially (only specified fields)
77+
"""
78+
updated_record = store.update(record_id, record)
79+
if not updated_record:
80+
raise web.HTTPNotFound(reason="Not Found")
81+
return updated_record
82+
83+
84+
@router.delete("/records/{record_id}", tags=["records"], status_code=204)
85+
async def delete_record(record_id: str):
86+
"""
87+
Delete a record
88+
"""
89+
if not store.delete(record_id):
90+
raise web.HTTPNotFound(reason="Not Found")
91+
return None
92+
93+
94+
if __name__ == "__main__":
95+
web.run_app(app, host="127.0.0.1", port=8001)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class RecordCreate(BaseModel):
5+
title: str = Field(..., min_length=1, max_length=100)
6+
description: str | None = Field(None, max_length=500)
7+
is_completed: bool = Field(False)
8+
9+
10+
class RecordUpdate(BaseModel):
11+
title: str | None = Field(None, min_length=1, max_length=100)
12+
description: str | None = Field(None, max_length=500)
13+
is_completed: bool | None = None
14+
15+
16+
class RecordResponse(BaseModel):
17+
id: str
18+
title: str
19+
description: str | None = None
20+
is_completed: bool
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import uuid
2+
3+
from .schemas import RecordCreate, RecordResponse, RecordUpdate
4+
5+
6+
class RecordStore:
7+
def __init__(self):
8+
self.records: dict[str, dict] = {}
9+
10+
def create(self, record_data: RecordCreate) -> RecordResponse:
11+
record_id = str(uuid.uuid4())
12+
record = {"id": record_id, **record_data.model_dump()}
13+
self.records[record_id] = record
14+
return RecordResponse(**record)
15+
16+
def get_all(self) -> list[RecordResponse]:
17+
return [RecordResponse(**record) for record in self.records.values()]
18+
19+
def get_by_id(self, record_id: str) -> RecordResponse | None:
20+
record = self.records.get(record_id)
21+
if record:
22+
return RecordResponse(**record)
23+
return None
24+
25+
def update(
26+
self, record_id: str, record_data: RecordUpdate
27+
) -> RecordResponse | None:
28+
if record_id not in self.records:
29+
return None
30+
31+
update_data = record_data.model_dump(exclude_unset=True)
32+
if not update_data:
33+
return RecordResponse(**self.records[record_id])
34+
35+
for key, value in update_data.items():
36+
if value is not None:
37+
self.records[record_id][key] = value
38+
39+
return RecordResponse(**self.records[record_id])
40+
41+
def delete(self, record_id: str) -> bool:
42+
if record_id in self.records:
43+
del self.records[record_id]
44+
return True
45+
return False
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from aiohttp import web
2+
3+
from benchmarks.aiohttp.without_fastopenapi.schemas import (
4+
RecordCreate,
5+
RecordResponse,
6+
RecordUpdate,
7+
)
8+
from benchmarks.aiohttp.without_fastopenapi.storage import RecordStore
9+
10+
store = RecordStore()
11+
12+
13+
async def get_all_records(request: web.Request):
14+
records = store.get_all()
15+
return web.json_response([record.model_dump() for record in records], status=200)
16+
17+
18+
async def get_one_record(request: web.Request):
19+
record_id = request.match_info.get("record_id")
20+
record = store.get_by_id(record_id)
21+
if record:
22+
return web.json_response(record.model_dump(), status=200)
23+
else:
24+
return web.json_response({"error": "Record not found"}, status=404)
25+
26+
27+
async def create_record(request: web.Request):
28+
try:
29+
data = await request.json()
30+
record_data = RecordCreate(**data)
31+
new_record = store.create(record_data)
32+
return web.json_response(new_record.model_dump(), status=201)
33+
except ValueError as e:
34+
return web.json_response({"error": str(e)}, status=400)
35+
36+
37+
async def replace_record(request: web.Request):
38+
record_id = request.match_info.get("record_id")
39+
if not store.get_by_id(record_id):
40+
return web.json_response({"error": "Record not found"}, status=404)
41+
try:
42+
data = await request.json()
43+
record_data = RecordCreate(**data)
44+
store.delete(record_id)
45+
record = {"id": record_id, **record_data.model_dump()}
46+
store.records[record_id] = record
47+
return web.json_response(RecordResponse(**record).model_dump(), status=200)
48+
except ValueError as e:
49+
return web.json_response({"error": str(e)}, status=400)
50+
51+
52+
async def update_record(request: web.Request):
53+
record_id = request.match_info.get("record_id")
54+
try:
55+
data = await request.json()
56+
record_data = RecordUpdate(**data)
57+
updated_record = store.update(record_id, record_data)
58+
if updated_record:
59+
return web.json_response(updated_record.model_dump(), status=200)
60+
else:
61+
return web.json_response({"error": "Record not found"}, status=404)
62+
except ValueError as e:
63+
return web.json_response({"error": str(e)}, status=400)
64+
65+
66+
async def delete_record(request: web.Request):
67+
record_id = request.match_info.get("record_id")
68+
if store.delete(record_id):
69+
return web.Response(status=204)
70+
else:
71+
return web.json_response({"error": "Record not found"}, status=404)
72+
73+
74+
app = web.Application()
75+
app.router.add_get("/records", get_all_records)
76+
app.router.add_get("/records/{record_id}", get_one_record)
77+
app.router.add_post("/records", create_record)
78+
app.router.add_put("/records/{record_id}", replace_record)
79+
app.router.add_patch("/records/{record_id}", update_record)
80+
app.router.add_delete("/records/{record_id}", delete_record)
81+
82+
if __name__ == "__main__":
83+
web.run_app(app, host="127.0.0.1", port=8000)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from pydantic import BaseModel, Field, field_validator
2+
3+
4+
class RecordCreate(BaseModel):
5+
title: str = Field(..., min_length=1, max_length=100)
6+
description: str | None = Field(None, max_length=500)
7+
is_completed: bool = Field(False)
8+
9+
@field_validator("title")
10+
@classmethod
11+
def title_cannot_be_empty(cls, v: str) -> str:
12+
if not v.strip():
13+
raise ValueError("Title cannot be empty")
14+
return v
15+
16+
17+
class RecordUpdate(BaseModel):
18+
title: str | None = Field(None, min_length=1, max_length=100)
19+
description: str | None = Field(None, max_length=500)
20+
is_completed: bool | None = None
21+
22+
@field_validator("title")
23+
@classmethod
24+
def title_cannot_be_empty(cls, v: str | None) -> str | None:
25+
if v is not None and not v.strip():
26+
raise ValueError("Title cannot be empty")
27+
return v
28+
29+
30+
class RecordResponse(BaseModel):
31+
id: str
32+
title: str
33+
description: str | None = None
34+
is_completed: bool
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import uuid
2+
3+
from .schemas import RecordCreate, RecordResponse, RecordUpdate
4+
5+
6+
class RecordStore:
7+
def __init__(self):
8+
self.records: dict[str, dict] = {}
9+
10+
def create(self, record_data: RecordCreate) -> RecordResponse:
11+
record_id = str(uuid.uuid4())
12+
record = {"id": record_id, **record_data.model_dump()}
13+
self.records[record_id] = record
14+
return RecordResponse(**record)
15+
16+
def get_all(self) -> list[RecordResponse]:
17+
return [RecordResponse(**record) for record in self.records.values()]
18+
19+
def get_by_id(self, record_id: str) -> RecordResponse | None:
20+
record = self.records.get(record_id)
21+
if record:
22+
return RecordResponse(**record)
23+
return None
24+
25+
def update(
26+
self, record_id: str, record_data: RecordUpdate
27+
) -> RecordResponse | None:
28+
if record_id not in self.records:
29+
return None
30+
31+
update_data = record_data.model_dump(exclude_unset=True)
32+
if not update_data:
33+
return RecordResponse(**self.records[record_id])
34+
35+
for key, value in update_data.items():
36+
if value is not None:
37+
self.records[record_id][key] = value
38+
39+
return RecordResponse(**self.records[record_id])
40+
41+
def delete(self, record_id: str) -> bool:
42+
if record_id in self.records:
43+
del self.records[record_id]
44+
return True
45+
return False

fastopenapi/base_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class BaseRouter:
2424
This class is extended by specific framework routers to integrate with
2525
web frameworks.
2626
27-
**Parameters**:
27+
Parameters:
2828
- `app`: The web framework application instance (e.g., Flask, Falcon, etc.).
2929
If provided, documentation and schema routes are automatically added to the app.
3030
- `docs_url`: URL path prefix where the Swagger documentation UI will be served

0 commit comments

Comments
 (0)