Skip to content

Commit d8d19f3

Browse files
authored
Merge pull request #7 from mr-fatalyst/0.5.0-dev
0.5.0 dev
2 parents e9cc3aa + 45ce9e1 commit d8d19f3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2002
-234
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to FastOpenAPI are documented in this file.
44

55
FastOpenAPI follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format.
66

7+
## [0.5.0] - Unreleased
8+
9+
### Added
10+
- `AioHttpRouter` for integration with the `AioHttp` framework
11+
- Class-level cache for model schemas
12+
- `response_errors` for routers
13+
- `error_handler` for standard error responses
14+
715
## [0.4.0] - 2025-03-20
816

917
### Added

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@ pip install fastopenapi
3030

3131
#### Install FastOpenAPI with a specific framework:
3232
```bash
33+
pip install fastopenapi[aiohttp]
34+
```
35+
```bash
3336
pip install fastopenapi[falcon]
3437
```
3538
```bash
3639
pip install fastopenapi[flask]
3740
```
3841
```bash
42+
pip install fastopenapi[quart]
43+
```
44+
```bash
3945
pip install fastopenapi[sanic]
4046
```
4147
```bash
@@ -57,6 +63,35 @@ pip install fastopenapi[tornado]
5763

5864
#### Examples:
5965

66+
- ![AIOHTTP](https://img.shields.io/badge/AioHttp-0078D7?style=flat&logo=python&logoColor=white)
67+
<details>
68+
<summary>Click to expand the Falcon Example</summary>
69+
70+
```python
71+
from aiohttp import web
72+
from pydantic import BaseModel
73+
74+
from fastopenapi.routers import AioHttpRouter
75+
76+
app = web.Application()
77+
router = AioHttpRouter(app=app)
78+
79+
80+
class HelloResponse(BaseModel):
81+
message: str
82+
83+
84+
@router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
85+
async def hello(name: str):
86+
"""Say hello from aiohttp"""
87+
return HelloResponse(message=f"Hello, {name}! It's aiohttp!")
88+
89+
90+
if __name__ == "__main__":
91+
web.run_app(app, host="127.0.0.1", port=8000)
92+
```
93+
</details>
94+
6095
- ![Falcon](https://img.shields.io/badge/Falcon-45b8d8?style=flat&logo=falcon&logoColor=white)
6196
<details>
6297
<summary>Click to expand the Falcon Example</summary>
@@ -264,7 +299,7 @@ http://127.0.0.1:8000/redoc
264299
## ⚙️ Features
265300
- **Generate OpenAPI schemas** with Pydantic v2.
266301
- **Data validation** using Pydantic models.
267-
- **Supports multiple frameworks:** Falcon, Flask, Quart, Sanic, Starlette, Tornado.
302+
- **Supports multiple frameworks:** AIOHTTP, Falcon, Flask, Quart, Sanic, Starlette, Tornado.
268303
- **Proxy routing provides FastAPI-style routing**
269304

270305
---

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)

0 commit comments

Comments
 (0)