Skip to content

Commit ecd0d3a

Browse files
committed
Implemented some basic endpoints with better tortoise integration and tests.
-Using tortoise's pydantic model creation system for generating outgoing pydantic models. -Cleaned up test suite packaging by moving fixtures to a separate folder and leaving the root conftest for more session-specific configuration and setup.
1 parent 2cca97d commit ecd0d3a

Some content is hidden

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

54 files changed

+798
-232
lines changed
File renamed without changes.

app/api/http/endpoints/broadcast.py

-11
This file was deleted.

app/api/http/endpoints/features.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from fastapi import HTTPException
2+
from fastapi.routing import APIRouter
3+
4+
from app.crud.features import crud_feature
5+
from app.schemas.features import FeatureCreate, FeatureOut
6+
7+
router = APIRouter()
8+
9+
10+
@router.post("/features", response_model=FeatureOut)
11+
async def create_feature(feature_in: FeatureCreate):
12+
feature = await crud_feature.create(obj_in=feature_in)
13+
if feature:
14+
await feature.fetch_related("guests")
15+
else:
16+
raise HTTPException(status_code=404, detail="Could not create item")
17+
return feature
18+
19+
20+
@router.get("/features/{feature_id}", response_model=FeatureOut)
21+
async def get_feature(feature_id: str):
22+
feature = await crud_feature.get(id=feature_id)
23+
if feature:
24+
await feature.fetch_related("guests")
25+
else:
26+
raise HTTPException(status_code=404, detail="Could not create item")
27+
return feature

app/api/http/endpoints/guests.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from fastapi import APIRouter, HTTPException, Request
2+
3+
from app.crud.guests import crud_guest
4+
from app.schemas.guests import GuestCreate, GuestOut
5+
6+
router = APIRouter()
7+
8+
9+
@router.post("/features/{feature_id}/guest", response_model=GuestOut)
10+
async def create_current_guest(request: Request, guest_in: GuestCreate):
11+
guest_id = request.session.get("guest_id")
12+
if guest_id:
13+
raise HTTPException(status_code=400, detail="Guest already exists")
14+
else:
15+
guest = await crud_guest.create(obj_in=guest_in)
16+
request.session["guest_id"] = str(guest.id)
17+
return guest
18+
19+
20+
@router.get("/guest", response_model=GuestOut)
21+
async def get_current_guest(request: Request, guest_in: GuestCreate):
22+
guest_id = request.session.get("guest_id")
23+
if guest_id:
24+
raise HTTPException(status_code=400, detail="Guest already exists")
25+
else:
26+
guest = await crud_guest.create(obj_in=guest_in)
27+
request.session["guest_id"] = str(guest.id)
28+
return guest
29+
30+
31+
@router.get("/guests/{guest_id}", response_model=GuestOut)
32+
async def get_feature(guest_id: str):
33+
guest = await crud_guest.get(id=guest_id)
34+
return guest

app/api/http/routes.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi.routing import APIRouter
22

3-
from .endpoints import broadcast
3+
from .endpoints import features, guests
44

55
http_router = APIRouter()
6-
http_router.include_router(broadcast.router)
6+
http_router.include_router(guests.router, tags=["guests"])
7+
http_router.include_router(features.router, tags=["features"])

app/api/routes.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
from fastapi.routing import APIRouter
22

3-
from app.api.http.routes import http_router
4-
from app.api.ws.routes import ws_router
3+
from .http.routes import http_router
4+
from .ws.routes import ws_router
55

6-
api_router = APIRouter()
7-
8-
9-
api_router.include_router(ws_router, prefix="/api/ws")
10-
api_router.include_router(http_router, prefix="/api")
6+
router = APIRouter()
7+
router.include_router(http_router, prefix="/api")
8+
router.include_router(ws_router, prefix="/api/ws")

app/api/ws/__init__.py

Whitespace-only changes.

app/api/ws/endpoints/__init__.py

Whitespace-only changes.

app/api/ws/endpoints/guest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from starlette.concurrency import run_until_first_complete
88
from starlette.endpoints import WebSocketEndpoint
99

10-
from app.services.broadcast import broadcast
10+
from app.services.broadcasting import broadcast
1111

1212
log = logging.getLogger(__name__)
1313
router = APIRouter()

app/core/__init__.py

Whitespace-only changes.

app/core/db.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from app.core import settings
2+
from tortoise import Tortoise
3+
from tortoise.contrib.fastapi import register_tortoise
4+
5+
6+
def register_db(app):
7+
register_tortoise(
8+
app,
9+
db_url=settings.DATABASE_URL,
10+
modules={"models": ["app.models.features", "app.models.guests"]},
11+
generate_schemas=True,
12+
add_exception_handlers=settings.DEBUG,
13+
)
14+
15+
16+
def init_models():
17+
Tortoise.init_models(["__main__"], "models")

app/core/events.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Callable
2+
3+
from ..services.broadcasting import connect_broadcaster, disconnect_broadcaster
4+
5+
6+
def create_startup_event_handler() -> Callable:
7+
async def on_startup() -> None:
8+
await connect_broadcaster()
9+
10+
return on_startup
11+
12+
13+
def create_shutdown_event_handler() -> Callable:
14+
async def on_shutdown() -> None:
15+
await disconnect_broadcaster()
16+
17+
return on_shutdown

app/core/middlewares.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
2+
from starlette.middleware.sessions import SessionMiddleware
3+
from starlette.middleware.trustedhost import TrustedHostMiddleware
4+
5+
from . import settings
6+
7+
8+
def add_middlewares(app):
9+
app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.ALLOWED_HOSTS)
10+
app.add_middleware(SessionMiddleware, secret_key=settings.SECRET_KEY),
11+
if settings.HTTPS_REDIRECT is True:
12+
app.add_middleware(HTTPSRedirectMiddleware)

app/core/settings.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import os
22

3-
# import databases
4-
from starlette.applications import Starlette
53
from starlette.config import Config
64
from starlette.datastructures import CommaSeparatedStrings, Secret
75

@@ -18,12 +16,10 @@
1816
# Security
1917
SECRET_KEY = config("SECRET_KEY", cast=Secret)
2018
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=CommaSeparatedStrings)
19+
HTTPS_REDIRECT = config("HTTPS_REDIRECT", default=True)
2120

2221
# Database
23-
# DATABASE_URL = config("DATABASE_URL", cast=databases.DatabaseURL)
2422
DATABASE_URL = config("DATABASE_URL")
2523

2624
# Caching
2725
REDIS_URL = config("REDIS_URL")
28-
29-
app = Starlette(debug=DEBUG)

app/crud/__init__.py

Whitespace-only changes.

app/crud/base.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Generic, Optional, Type, TypeVar
2+
3+
from fastapi.encoders import jsonable_encoder
4+
5+
from app.models.base import CustomTortoiseBase
6+
from app.schemas.base import CustomPydanticBase
7+
8+
ModelType = TypeVar("ModelType", bound=CustomTortoiseBase)
9+
CreateSchemaType = TypeVar("CreateSchemaType", bound=CustomPydanticBase)
10+
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=CustomPydanticBase)
11+
12+
13+
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
14+
"""CRUD object with default methods to Create, Read, Update, Delete (CRUD)."""
15+
16+
def __init__(self, model: Type[ModelType]):
17+
self.model = model
18+
19+
async def get(self, id: str) -> Optional[ModelType]:
20+
return await self.model.filter(id=id).first()
21+
22+
# async def get_multi(self, *, skip=0, limit=100) -> List[ModelType]:
23+
# return await self.model.offset(skip).limit(limit).all()
24+
25+
async def create(self, *, obj_in: CreateSchemaType) -> ModelType:
26+
obj_in_data = jsonable_encoder(obj_in)
27+
return await self.model.create(**obj_in_data)
28+
29+
async def update(self, *, db_obj: ModelType, obj_in: UpdateSchemaType) -> ModelType:
30+
obj_data = jsonable_encoder(db_obj)
31+
update_data = obj_in.dict(skip_defaults=True)
32+
for field in obj_data:
33+
if field in update_data:
34+
setattr(db_obj, field, update_data[field])
35+
db_obj = await self.model.create(**obj_data)
36+
return db_obj
37+
38+
# async def remove(self, *, id: int) -> ModelType:
39+
# obj = await self.model.get(id)
40+
# await self.model.delete(obj)
41+
# return obj

app/crud/features.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from app.crud.base import CRUDBase
2+
from app.models.features import Feature
3+
from app.schemas.features import FeatureCreate, FeatureUpdate
4+
5+
6+
class CRUDFeature(CRUDBase[Feature, FeatureCreate, FeatureUpdate]):
7+
pass
8+
9+
10+
crud_feature = CRUDFeature(Feature)

app/crud/guests.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from app.crud.base import CRUDBase
2+
from app.models.guests import Guest
3+
from app.schemas.guests import GuestCreate, GuestUpdate
4+
5+
6+
class CRUDGuest(CRUDBase[Guest, GuestCreate, GuestUpdate]):
7+
pass
8+
9+
10+
crud_guest = CRUDGuest(Guest)

app/db/initialize.py

-11
This file was deleted.

app/db/models/feature.py

-22
This file was deleted.

app/db/models/guest.py

-17
This file was deleted.

app/main.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from fastapi import FastAPI
22

3-
from .api.routes import api_router
4-
from .db.initialize import init_db
5-
from .services.broadcast import connect_broadcaster, disconnect_broadcaster
3+
from .api.routes import router
4+
from .core.db import register_db
5+
from .core.events import create_shutdown_event_handler, create_startup_event_handler
6+
from .core.middlewares import add_middlewares
67

78

89
def get_app() -> FastAPI:
910
app = FastAPI()
10-
app.include_router(api_router)
11-
app.add_event_handler("startup", connect_broadcaster)
12-
app.add_event_handler("shutdown", disconnect_broadcaster)
13-
init_db(app)
11+
app.include_router(router)
12+
app.add_event_handler("startup", create_startup_event_handler())
13+
app.add_event_handler("shutdown", create_shutdown_event_handler())
14+
add_middlewares(app)
15+
register_db(app)
1416
return app
1517

1618

app/models/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
from tortoise import fields
1+
from tortoise import fields, models
22

33

44
class TimestampMixin:
55
created_at = fields.DatetimeField(null=True, auto_now_add=True)
66
modified_at = fields.DatetimeField(null=True, auto_now=True)
7+
8+
9+
class CustomTortoiseBase(models.Model):
10+
class Meta:
11+
abstract = True

app/models/features.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import annotations
2+
3+
from app.models.guests import Guest
4+
from tortoise import fields
5+
6+
from .base import CustomTortoiseBase, TimestampMixin
7+
8+
9+
class Feature(TimestampMixin, CustomTortoiseBase):
10+
id = fields.UUIDField(pk=True, read_only=True)
11+
title = fields.CharField(max_length=100, unique=True)
12+
slug = fields.CharField(index=True, required=True, unique=True, max_length=50)
13+
turn_duration = fields.IntField(required=True)
14+
guests = fields.ReverseRelation[Guest]
15+
16+
class Meta:
17+
table = "features"
18+
19+
def __repr__(self):
20+
return f"<{self.__class__.__name__}: {str(self.id)} name='{self.title}'>"
21+
22+
def __str__(self):
23+
return self.__repr__()
24+
25+
class PydanticMeta:
26+
exclude = (
27+
"created_at",
28+
"updated_at",
29+
)

app/models/guests.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import annotations
2+
3+
from tortoise import fields
4+
5+
from .base import CustomTortoiseBase, TimestampMixin
6+
7+
8+
class Guest(TimestampMixin, CustomTortoiseBase):
9+
id = fields.UUIDField(pk=True, read_only=True)
10+
name = fields.CharField(max_length=100)
11+
feature = fields.ForeignKeyField(
12+
"models.Feature", on_delete="CASCADE", related_name="guests",
13+
)
14+
15+
class Meta:
16+
table = "guests"
17+
18+
def __repr__(self):
19+
return f"<{self.__class__.__name__}: {str(self.id)} name='{self.name}'>"
20+
21+
def __str__(self):
22+
return self.__repr__()
23+
24+
class PydanticMeta:
25+
exclude = (
26+
"created_at",
27+
"updated_at",
28+
"feature",
29+
)

app/schemas/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)