Skip to content

Commit 403344d

Browse files
committed
create_tables_on_startd created, docs added
1 parent 78465c9 commit 403344d

28 files changed

+233
-458
lines changed

README.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@
9494
10. [ARQ Job Queues](#510-arq-job-queues)
9595
11. [Rate Limiting](#511-rate-limiting)
9696
12. [JWT Authentication](#512-jwt-authentication)
97-
13. [Running](#512-running)
97+
13. [Running](#513-running)
98+
14. [Create Application](#514-create-application)
9899
6. [Running in Production](#6-running-in-production)
99100
1. [Uvicorn Workers with Gunicorn](#61-uvicorn-workers-with-gunicorn)
100101
2. [Running With NGINX](#62-running-with-nginx)
@@ -1393,6 +1394,26 @@ CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker
13931394
> [!CAUTION]
13941395
> Do not forget to set the `ENVIRONMENT` in `.env` to `production` unless you want the API docs to be public.
13951396
1397+
### 5.14 Create Application
1398+
If you want to stop tables from being created every time you run the api, you should disable this here:
1399+
```python
1400+
# app/main.py
1401+
1402+
from .api import router
1403+
from .core.config import settings
1404+
from .core.setup import create_application
1405+
1406+
# create_tables_on_start defaults to True
1407+
app = create_application(router=router, settings=settings, create_tables_on_start=False)
1408+
```
1409+
1410+
This `create_application` function is defined in `app/core/setup.py`, and it's a flexible way to configure the behavior of your application.
1411+
1412+
A few examples:
1413+
- Deactivate or password protect /docs
1414+
- Add client-side cache middleware
1415+
- Add Startup and Shutdown event handlers for cache, queue and rate limit
1416+
13961417
### 6.2 Running with NGINX
13971418
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.
13981419

src/app/api/paginated.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

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

7+
78
class ListResponse(BaseModel, Generic[SchemaType]):
89
data: List[SchemaType]
910

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

1718

18-
def paginated_response(
19-
crud_data: dict,
20-
page: int,
21-
items_per_page: int
22-
) -> Dict[str, Any]:
19+
def paginated_response(crud_data: dict, page: int, items_per_page: int) -> Dict[str, Any]:
2320
"""
2421
Create a paginated response based on the provided data and pagination parameters.
2522
@@ -46,9 +43,10 @@ def paginated_response(
4643
"total_count": crud_data["total_count"],
4744
"has_more": (page * items_per_page) < crud_data["total_count"],
4845
"page": page,
49-
"items_per_page": items_per_page
46+
"items_per_page": items_per_page,
5047
}
5148

49+
5250
def compute_offset(page: int, items_per_page: int) -> int:
5351
"""
5452
Calculate the offset for pagination based on the given page number and items per page.

src/app/api/v1/login.py

+9-25
Original file line numberDiff line numberDiff line change
@@ -20,48 +20,32 @@
2020

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

23+
2324
@router.post("/login", response_model=Token)
2425
async def login_for_access_token(
2526
response: Response,
2627
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
27-
db: Annotated[AsyncSession, Depends(async_get_db)]
28+
db: Annotated[AsyncSession, Depends(async_get_db)],
2829
) -> Dict[str, str]:
29-
user = await authenticate_user(
30-
username_or_email=form_data.username,
31-
password=form_data.password,
32-
db=db
33-
)
30+
user = await authenticate_user(username_or_email=form_data.username, password=form_data.password, db=db)
3431
if not user:
3532
raise UnauthorizedException("Wrong username, email or password.")
36-
33+
3734
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
38-
access_token = await create_access_token(
39-
data={"sub": user["username"]}, expires_delta=access_token_expires
40-
)
35+
access_token = await create_access_token(data={"sub": user["username"]}, expires_delta=access_token_expires)
4136

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

4540
response.set_cookie(
46-
key="refresh_token",
47-
value=refresh_token,
48-
httponly=True,
49-
secure=True,
50-
samesite='Lax',
51-
max_age=max_age
41+
key="refresh_token", value=refresh_token, httponly=True, secure=True, samesite="Lax", max_age=max_age
5242
)
53-
54-
return {
55-
"access_token": access_token,
56-
"token_type": "bearer"
57-
}
43+
44+
return {"access_token": access_token, "token_type": "bearer"}
5845

5946

6047
@router.post("/refresh")
61-
async def refresh_access_token(
62-
request: Request,
63-
db: AsyncSession = Depends(async_get_db)
64-
) -> Dict[str, str]:
48+
async def refresh_access_token(request: Request, db: AsyncSession = Depends(async_get_db)) -> Dict[str, str]:
6549
refresh_token = request.cookies.get("refresh_token")
6650
if not refresh_token:
6751
raise UnauthorizedException("Refresh token missing.")

src/app/api/v1/logout.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,16 @@
1010

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

13+
1314
@router.post("/logout")
1415
async def logout(
15-
response: Response,
16-
access_token: str = Depends(oauth2_scheme),
17-
db: AsyncSession = Depends(async_get_db)
16+
response: Response, access_token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(async_get_db)
1817
) -> Dict[str, str]:
1918
try:
2019
await blacklist_token(token=access_token, db=db)
2120
response.delete_cookie(key="refresh_token")
2221

2322
return {"message": "Logged out successfully"}
24-
23+
2524
except JWTError:
2625
raise UnauthorizedException("Invalid token.")

src/app/api/v1/rate_limits.py

+18-49
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@
1414

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

17+
1718
@router.post("/tier/{tier_name}/rate_limit", dependencies=[Depends(get_current_superuser)], status_code=201)
1819
async def write_rate_limit(
19-
request: Request,
20-
tier_name: str,
21-
rate_limit: RateLimitCreate,
22-
db: Annotated[AsyncSession, Depends(async_get_db)]
20+
request: Request, tier_name: str, rate_limit: RateLimitCreate, db: Annotated[AsyncSession, Depends(async_get_db)]
2321
) -> RateLimitRead:
2422
db_tier = await crud_tiers.get(db=db, name=tier_name)
2523
if not db_tier:
@@ -31,7 +29,7 @@ async def write_rate_limit(
3129
db_rate_limit = await crud_rate_limits.exists(db=db, name=rate_limit_internal_dict["name"])
3230
if db_rate_limit:
3331
raise DuplicateValueException("Rate Limit Name not available")
34-
32+
3533
rate_limit_internal = RateLimitCreateInternal(**rate_limit_internal_dict)
3634
return await crud_rate_limits.create(db=db, object=rate_limit_internal)
3735

@@ -42,7 +40,7 @@ async def read_rate_limits(
4240
tier_name: str,
4341
db: Annotated[AsyncSession, Depends(async_get_db)],
4442
page: int = 1,
45-
items_per_page: int = 10
43+
items_per_page: int = 10,
4644
) -> dict:
4745
db_tier = await crud_tiers.get(db=db, name=tier_name)
4846
if not db_tier:
@@ -53,33 +51,21 @@ async def read_rate_limits(
5351
offset=compute_offset(page, items_per_page),
5452
limit=items_per_page,
5553
schema_to_select=RateLimitRead,
56-
tier_id=db_tier["id"]
54+
tier_id=db_tier["id"],
5755
)
5856

59-
return paginated_response(
60-
crud_data=rate_limits_data,
61-
page=page,
62-
items_per_page=items_per_page
63-
)
57+
return paginated_response(crud_data=rate_limits_data, page=page, items_per_page=items_per_page)
6458

6559

6660
@router.get("/tier/{tier_name}/rate_limit/{id}", response_model=RateLimitRead)
6761
async def read_rate_limit(
68-
request: Request,
69-
tier_name: str,
70-
id: int,
71-
db: Annotated[AsyncSession, Depends(async_get_db)]
62+
request: Request, tier_name: str, id: int, db: Annotated[AsyncSession, Depends(async_get_db)]
7263
) -> dict:
7364
db_tier = await crud_tiers.get(db=db, name=tier_name)
7465
if not db_tier:
7566
raise NotFoundException("Tier not found")
76-
77-
db_rate_limit = await crud_rate_limits.get(
78-
db=db,
79-
schema_to_select=RateLimitRead,
80-
tier_id=db_tier["id"],
81-
id=id
82-
)
67+
68+
db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, tier_id=db_tier["id"], id=id)
8369
if db_rate_limit is None:
8470
raise NotFoundException("Rate Limit not found")
8571

@@ -92,26 +78,17 @@ async def patch_rate_limit(
9278
tier_name: str,
9379
id: int,
9480
values: RateLimitUpdate,
95-
db: Annotated[AsyncSession, Depends(async_get_db)]
81+
db: Annotated[AsyncSession, Depends(async_get_db)],
9682
) -> Dict[str, str]:
9783
db_tier = await crud_tiers.get(db=db, name=tier_name)
9884
if db_tier is None:
9985
raise NotFoundException("Tier not found")
100-
101-
db_rate_limit = await crud_rate_limits.get(
102-
db=db,
103-
schema_to_select=RateLimitRead,
104-
tier_id=db_tier["id"],
105-
id=id
106-
)
86+
87+
db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, tier_id=db_tier["id"], id=id)
10788
if db_rate_limit is None:
10889
raise NotFoundException("Rate Limit not found")
109-
110-
db_rate_limit_path = await crud_rate_limits.exists(
111-
db=db,
112-
tier_id=db_tier["id"],
113-
path=values.path
114-
)
90+
91+
db_rate_limit_path = await crud_rate_limits.exists(db=db, tier_id=db_tier["id"], path=values.path)
11592
if db_rate_limit_path is not None:
11693
raise DuplicateValueException("There is already a rate limit for this path")
11794

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

126103
@router.delete("/tier/{tier_name}/rate_limit/{id}", dependencies=[Depends(get_current_superuser)])
127104
async def erase_rate_limit(
128-
request: Request,
129-
tier_name: str,
130-
id: int,
131-
db: Annotated[AsyncSession, Depends(async_get_db)]
105+
request: Request, tier_name: str, id: int, db: Annotated[AsyncSession, Depends(async_get_db)]
132106
) -> Dict[str, str]:
133107
db_tier = await crud_tiers.get(db=db, name=tier_name)
134108
if not db_tier:
135109
raise NotFoundException("Tier not found")
136-
137-
db_rate_limit = await crud_rate_limits.get(
138-
db=db,
139-
schema_to_select=RateLimitRead,
140-
tier_id=db_tier["id"],
141-
id=id
142-
)
110+
111+
db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, tier_id=db_tier["id"], id=id)
143112
if db_rate_limit is None:
144113
raise RateLimitException("Rate Limit not found")
145-
114+
146115
await crud_rate_limits.delete(db=db, db_row=db_rate_limit, id=db_rate_limit["id"])
147116
return {"message": "Rate Limit deleted"}

src/app/api/v1/tasks.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

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

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

3031

src/app/api/v1/tiers.py

+11-33
Original file line numberDiff line numberDiff line change
@@ -13,48 +13,33 @@
1313

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

16+
1617
@router.post("/tier", dependencies=[Depends(get_current_superuser)], status_code=201)
1718
async def write_tier(
18-
request: Request,
19-
tier: TierCreate,
20-
db: Annotated[AsyncSession, Depends(async_get_db)]
19+
request: Request, tier: TierCreate, db: Annotated[AsyncSession, Depends(async_get_db)]
2120
) -> TierRead:
2221
tier_internal_dict = tier.model_dump()
2322
db_tier = await crud_tiers.exists(db=db, name=tier_internal_dict["name"])
2423
if db_tier:
2524
raise DuplicateValueException("Tier Name not available")
26-
25+
2726
tier_internal = TierCreateInternal(**tier_internal_dict)
2827
return await crud_tiers.create(db=db, object=tier_internal)
2928

3029

3130
@router.get("/tiers", response_model=PaginatedListResponse[TierRead])
3231
async def read_tiers(
33-
request: Request,
34-
db: Annotated[AsyncSession, Depends(async_get_db)],
35-
page: int = 1,
36-
items_per_page: int = 10
32+
request: Request, db: Annotated[AsyncSession, Depends(async_get_db)], page: int = 1, items_per_page: int = 10
3733
) -> dict:
3834
tiers_data = await crud_tiers.get_multi(
39-
db=db,
40-
offset=compute_offset(page, items_per_page),
41-
limit=items_per_page,
42-
schema_to_select=TierRead
35+
db=db, offset=compute_offset(page, items_per_page), limit=items_per_page, schema_to_select=TierRead
4336
)
4437

45-
return paginated_response(
46-
crud_data=tiers_data,
47-
page=page,
48-
items_per_page=items_per_page
49-
)
38+
return paginated_response(crud_data=tiers_data, page=page, items_per_page=items_per_page)
5039

5140

5241
@router.get("/tier/{name}", response_model=TierRead)
53-
async def read_tier(
54-
request: Request,
55-
name: str,
56-
db: Annotated[AsyncSession, Depends(async_get_db)]
57-
) -> dict:
42+
async def read_tier(request: Request, name: str, db: Annotated[AsyncSession, Depends(async_get_db)]) -> dict:
5843
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name)
5944
if db_tier is None:
6045
raise NotFoundException("Tier not found")
@@ -64,28 +49,21 @@ async def read_tier(
6449

6550
@router.patch("/tier/{name}", dependencies=[Depends(get_current_superuser)])
6651
async def patch_tier(
67-
request: Request,
68-
values: TierUpdate,
69-
name: str,
70-
db: Annotated[AsyncSession, Depends(async_get_db)]
52+
request: Request, values: TierUpdate, name: str, db: Annotated[AsyncSession, Depends(async_get_db)]
7153
) -> Dict[str, str]:
7254
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name)
7355
if db_tier is None:
7456
raise NotFoundException("Tier not found")
75-
57+
7658
await crud_tiers.update(db=db, object=values, name=name)
7759
return {"message": "Tier updated"}
7860

7961

8062
@router.delete("/tier/{name}", dependencies=[Depends(get_current_superuser)])
81-
async def erase_tier(
82-
request: Request,
83-
name: str,
84-
db: Annotated[AsyncSession, Depends(async_get_db)]
85-
) -> Dict[str, str]:
63+
async def erase_tier(request: Request, name: str, db: Annotated[AsyncSession, Depends(async_get_db)]) -> Dict[str, str]:
8664
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name)
8765
if db_tier is None:
8866
raise NotFoundException("Tier not found")
89-
67+
9068
await crud_tiers.delete(db=db, db_row=db_tier, name=name)
9169
return {"message": "Tier deleted"}

0 commit comments

Comments
 (0)