Skip to content
Merged
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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@
10. [ARQ Job Queues](#510-arq-job-queues)
11. [Rate Limiting](#511-rate-limiting)
12. [JWT Authentication](#512-jwt-authentication)
13. [Running](#512-running)
13. [Running](#513-running)
14. [Create Application](#514-create-application)
6. [Running in Production](#6-running-in-production)
1. [Uvicorn Workers with Gunicorn](#61-uvicorn-workers-with-gunicorn)
2. [Running With NGINX](#62-running-with-nginx)
Expand Down Expand Up @@ -1393,6 +1394,26 @@ CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker
> [!CAUTION]
> Do not forget to set the `ENVIRONMENT` in `.env` to `production` unless you want the API docs to be public.

### 5.14 Create Application
If you want to stop tables from being created every time you run the api, you should disable this here:
```python
# app/main.py

from .api import router
from .core.config import settings
from .core.setup import create_application

# create_tables_on_start defaults to True
app = create_application(router=router, settings=settings, create_tables_on_start=False)
```

This `create_application` function is defined in `app/core/setup.py`, and it's a flexible way to configure the behavior of your application.

A few examples:
- Deactivate or password protect /docs
- Add client-side cache middleware
- Add Startup and Shutdown event handlers for cache, queue and rate limit

### 6.2 Running with NGINX
NGINX is a high-performance web server, known for its stability, rich feature set, simple configuration, and low resource consumption. NGINX acts as a reverse proxy, that is, it receives client requests, forwards them to the FastAPI server (running via Uvicorn or Gunicorn), and then passes the responses back to the clients.

Expand Down
10 changes: 4 additions & 6 deletions src/app/api/paginated.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

SchemaType = TypeVar("SchemaType", bound=BaseModel)


class ListResponse(BaseModel, Generic[SchemaType]):
data: List[SchemaType]

Expand All @@ -15,11 +16,7 @@ class PaginatedListResponse(ListResponse[SchemaType]):
items_per_page: int | None = None


def paginated_response(
crud_data: dict,
page: int,
items_per_page: int
) -> Dict[str, Any]:
def paginated_response(crud_data: dict, page: int, items_per_page: int) -> Dict[str, Any]:
"""
Create a paginated response based on the provided data and pagination parameters.

Expand All @@ -46,9 +43,10 @@ def paginated_response(
"total_count": crud_data["total_count"],
"has_more": (page * items_per_page) < crud_data["total_count"],
"page": page,
"items_per_page": items_per_page
"items_per_page": items_per_page,
}


def compute_offset(page: int, items_per_page: int) -> int:
"""
Calculate the offset for pagination based on the given page number and items per page.
Expand Down
34 changes: 9 additions & 25 deletions src/app/api/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,32 @@

router = fastapi.APIRouter(tags=["login"])


@router.post("/login", response_model=Token)
async def login_for_access_token(
response: Response,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Annotated[AsyncSession, Depends(async_get_db)]
db: Annotated[AsyncSession, Depends(async_get_db)],
) -> Dict[str, str]:
user = await authenticate_user(
username_or_email=form_data.username,
password=form_data.password,
db=db
)
user = await authenticate_user(username_or_email=form_data.username, password=form_data.password, db=db)
if not user:
raise UnauthorizedException("Wrong username, email or password.")

access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = await create_access_token(
data={"sub": user["username"]}, expires_delta=access_token_expires
)
access_token = await create_access_token(data={"sub": user["username"]}, expires_delta=access_token_expires)

refresh_token = await create_refresh_token(data={"sub": user["username"]})
max_age = settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60

response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=True,
samesite='Lax',
max_age=max_age
key="refresh_token", value=refresh_token, httponly=True, secure=True, samesite="Lax", max_age=max_age
)

return {
"access_token": access_token,
"token_type": "bearer"
}

return {"access_token": access_token, "token_type": "bearer"}


@router.post("/refresh")
async def refresh_access_token(
request: Request,
db: AsyncSession = Depends(async_get_db)
) -> Dict[str, str]:
async def refresh_access_token(request: Request, db: AsyncSession = Depends(async_get_db)) -> Dict[str, str]:
refresh_token = request.cookies.get("refresh_token")
if not refresh_token:
raise UnauthorizedException("Refresh token missing.")
Expand Down
7 changes: 3 additions & 4 deletions src/app/api/v1/logout.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,16 @@

router = APIRouter(tags=["login"])


@router.post("/logout")
async def logout(
response: Response,
access_token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(async_get_db)
response: Response, access_token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(async_get_db)
) -> Dict[str, str]:
try:
await blacklist_token(token=access_token, db=db)
response.delete_cookie(key="refresh_token")

return {"message": "Logged out successfully"}

except JWTError:
raise UnauthorizedException("Invalid token.")
67 changes: 18 additions & 49 deletions src/app/api/v1/rate_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@

router = fastapi.APIRouter(tags=["rate_limits"])


@router.post("/tier/{tier_name}/rate_limit", dependencies=[Depends(get_current_superuser)], status_code=201)
async def write_rate_limit(
request: Request,
tier_name: str,
rate_limit: RateLimitCreate,
db: Annotated[AsyncSession, Depends(async_get_db)]
request: Request, tier_name: str, rate_limit: RateLimitCreate, db: Annotated[AsyncSession, Depends(async_get_db)]
) -> RateLimitRead:
db_tier = await crud_tiers.get(db=db, name=tier_name)
if not db_tier:
Expand All @@ -31,7 +29,7 @@ async def write_rate_limit(
db_rate_limit = await crud_rate_limits.exists(db=db, name=rate_limit_internal_dict["name"])
if db_rate_limit:
raise DuplicateValueException("Rate Limit Name not available")

rate_limit_internal = RateLimitCreateInternal(**rate_limit_internal_dict)
return await crud_rate_limits.create(db=db, object=rate_limit_internal)

Expand All @@ -42,7 +40,7 @@ async def read_rate_limits(
tier_name: str,
db: Annotated[AsyncSession, Depends(async_get_db)],
page: int = 1,
items_per_page: int = 10
items_per_page: int = 10,
) -> dict:
db_tier = await crud_tiers.get(db=db, name=tier_name)
if not db_tier:
Expand All @@ -53,33 +51,21 @@ async def read_rate_limits(
offset=compute_offset(page, items_per_page),
limit=items_per_page,
schema_to_select=RateLimitRead,
tier_id=db_tier["id"]
tier_id=db_tier["id"],
)

return paginated_response(
crud_data=rate_limits_data,
page=page,
items_per_page=items_per_page
)
return paginated_response(crud_data=rate_limits_data, page=page, items_per_page=items_per_page)


@router.get("/tier/{tier_name}/rate_limit/{id}", response_model=RateLimitRead)
async def read_rate_limit(
request: Request,
tier_name: str,
id: int,
db: Annotated[AsyncSession, Depends(async_get_db)]
request: Request, tier_name: str, id: int, db: Annotated[AsyncSession, Depends(async_get_db)]
) -> dict:
db_tier = await crud_tiers.get(db=db, name=tier_name)
if not db_tier:
raise NotFoundException("Tier not found")

db_rate_limit = await crud_rate_limits.get(
db=db,
schema_to_select=RateLimitRead,
tier_id=db_tier["id"],
id=id
)

db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, tier_id=db_tier["id"], id=id)
if db_rate_limit is None:
raise NotFoundException("Rate Limit not found")

Expand All @@ -92,26 +78,17 @@ async def patch_rate_limit(
tier_name: str,
id: int,
values: RateLimitUpdate,
db: Annotated[AsyncSession, Depends(async_get_db)]
db: Annotated[AsyncSession, Depends(async_get_db)],
) -> Dict[str, str]:
db_tier = await crud_tiers.get(db=db, name=tier_name)
if db_tier is None:
raise NotFoundException("Tier not found")

db_rate_limit = await crud_rate_limits.get(
db=db,
schema_to_select=RateLimitRead,
tier_id=db_tier["id"],
id=id
)

db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, tier_id=db_tier["id"], id=id)
if db_rate_limit is None:
raise NotFoundException("Rate Limit not found")

db_rate_limit_path = await crud_rate_limits.exists(
db=db,
tier_id=db_tier["id"],
path=values.path
)

db_rate_limit_path = await crud_rate_limits.exists(db=db, tier_id=db_tier["id"], path=values.path)
if db_rate_limit_path is not None:
raise DuplicateValueException("There is already a rate limit for this path")

Expand All @@ -125,23 +102,15 @@ async def patch_rate_limit(

@router.delete("/tier/{tier_name}/rate_limit/{id}", dependencies=[Depends(get_current_superuser)])
async def erase_rate_limit(
request: Request,
tier_name: str,
id: int,
db: Annotated[AsyncSession, Depends(async_get_db)]
request: Request, tier_name: str, id: int, db: Annotated[AsyncSession, Depends(async_get_db)]
) -> Dict[str, str]:
db_tier = await crud_tiers.get(db=db, name=tier_name)
if not db_tier:
raise NotFoundException("Tier not found")

db_rate_limit = await crud_rate_limits.get(
db=db,
schema_to_select=RateLimitRead,
tier_id=db_tier["id"],
id=id
)

db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, tier_id=db_tier["id"], id=id)
if db_rate_limit is None:
raise RateLimitException("Rate Limit not found")

await crud_rate_limits.delete(db=db, db_row=db_rate_limit, id=db_rate_limit["id"])
return {"message": "Rate Limit deleted"}
3 changes: 2 additions & 1 deletion src/app/api/v1/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

router = APIRouter(prefix="/tasks", tags=["tasks"])


@router.post("/task", response_model=Job, status_code=201, dependencies=[Depends(rate_limiter)])
async def create_task(message: str) -> Dict[str, str]:
"""
Expand All @@ -24,7 +25,7 @@ async def create_task(message: str) -> Dict[str, str]:
Dict[str, str]
A dictionary containing the ID of the created task.
"""
job = await queue.pool.enqueue_job("sample_background_task", message) # type: ignore
job = await queue.pool.enqueue_job("sample_background_task", message) # type: ignore
return {"id": job.job_id}


Expand Down
44 changes: 11 additions & 33 deletions src/app/api/v1/tiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,33 @@

router = fastapi.APIRouter(tags=["tiers"])


@router.post("/tier", dependencies=[Depends(get_current_superuser)], status_code=201)
async def write_tier(
request: Request,
tier: TierCreate,
db: Annotated[AsyncSession, Depends(async_get_db)]
request: Request, tier: TierCreate, db: Annotated[AsyncSession, Depends(async_get_db)]
) -> TierRead:
tier_internal_dict = tier.model_dump()
db_tier = await crud_tiers.exists(db=db, name=tier_internal_dict["name"])
if db_tier:
raise DuplicateValueException("Tier Name not available")

tier_internal = TierCreateInternal(**tier_internal_dict)
return await crud_tiers.create(db=db, object=tier_internal)


@router.get("/tiers", response_model=PaginatedListResponse[TierRead])
async def read_tiers(
request: Request,
db: Annotated[AsyncSession, Depends(async_get_db)],
page: int = 1,
items_per_page: int = 10
request: Request, db: Annotated[AsyncSession, Depends(async_get_db)], page: int = 1, items_per_page: int = 10
) -> dict:
tiers_data = await crud_tiers.get_multi(
db=db,
offset=compute_offset(page, items_per_page),
limit=items_per_page,
schema_to_select=TierRead
db=db, offset=compute_offset(page, items_per_page), limit=items_per_page, schema_to_select=TierRead
)

return paginated_response(
crud_data=tiers_data,
page=page,
items_per_page=items_per_page
)
return paginated_response(crud_data=tiers_data, page=page, items_per_page=items_per_page)


@router.get("/tier/{name}", response_model=TierRead)
async def read_tier(
request: Request,
name: str,
db: Annotated[AsyncSession, Depends(async_get_db)]
) -> dict:
async def read_tier(request: Request, name: str, db: Annotated[AsyncSession, Depends(async_get_db)]) -> dict:
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name)
if db_tier is None:
raise NotFoundException("Tier not found")
Expand All @@ -64,28 +49,21 @@ async def read_tier(

@router.patch("/tier/{name}", dependencies=[Depends(get_current_superuser)])
async def patch_tier(
request: Request,
values: TierUpdate,
name: str,
db: Annotated[AsyncSession, Depends(async_get_db)]
request: Request, values: TierUpdate, name: str, db: Annotated[AsyncSession, Depends(async_get_db)]
) -> Dict[str, str]:
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name)
if db_tier is None:
raise NotFoundException("Tier not found")

await crud_tiers.update(db=db, object=values, name=name)
return {"message": "Tier updated"}


@router.delete("/tier/{name}", dependencies=[Depends(get_current_superuser)])
async def erase_tier(
request: Request,
name: str,
db: Annotated[AsyncSession, Depends(async_get_db)]
) -> Dict[str, str]:
async def erase_tier(request: Request, name: str, db: Annotated[AsyncSession, Depends(async_get_db)]) -> Dict[str, str]:
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name)
if db_tier is None:
raise NotFoundException("Tier not found")

await crud_tiers.delete(db=db, db_row=db_tier, name=name)
return {"message": "Tier deleted"}
Loading