Skip to content

Commit b721d92

Browse files
committed
rate limit and tier models, schemas, routes; create_first_tier script and docker-compose; cache fix to work with paginated
1 parent 94f64fa commit b721d92

File tree

18 files changed

+409
-140
lines changed

18 files changed

+409
-140
lines changed

docker-compose.yml

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,18 @@ services:
4646
- redis-data:/data
4747

4848
# #-------- uncomment to create first superuser --------
49-
# create_superuser:
50-
# build:
51-
# context: .
52-
# dockerfile: Dockerfile
53-
# env_file:
54-
# - ./src/.env
55-
# depends_on:
56-
# - db
57-
# command: python -m src.scripts.create_first_superuser
58-
# volumes:
59-
# - ./src:/code/src
49+
create_superuser:
50+
build:
51+
context: .
52+
dockerfile: Dockerfile
53+
env_file:
54+
- ./src/.env
55+
depends_on:
56+
- db
57+
- web
58+
command: python -m src.scripts.create_first_superuser
59+
volumes:
60+
- ./src:/code/src
6061

6162
# #-------- uncomment to run tests --------
6263
# pytest:
@@ -73,6 +74,21 @@ services:
7374
# volumes:
7475
# - ./src:/code/src
7576

77+
# #-------- uncomment to create first tier --------
78+
create_tier:
79+
build:
80+
context: .
81+
dockerfile: Dockerfile
82+
env_file:
83+
- ./src/.env
84+
depends_on:
85+
- db
86+
- web
87+
command: python -m src.scripts.create_first_tier
88+
volumes:
89+
- ./src:/code/src
90+
7691
volumes:
7792
postgres-data:
7893
redis-data:
94+

src/app/api/dependencies.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@
88
from fastapi import (
99
Depends,
1010
HTTPException,
11-
Request,
11+
Request,
1212
status
1313
)
1414

1515
from app.core.database import async_get_db
1616
from app.core.models import TokenData
17+
# from app.core.rate_limit import is_rate_limited
1718
from app.models.user import User
18-
from app.crud.crud_users import crud_users
1919
from app.api.exceptions import credentials_exception, privileges_exception
20+
from app.crud.crud_users import crud_users
21+
from app.crud.crud_tier import crud_tiers
2022

2123
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[AsyncSession, Depends(async_get_db)]) -> User:
2224
try:

src/app/api/v1/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
from app.api.v1.users import router as users_router
55
from app.api.v1.posts import router as posts_router
66
from app.api.v1.tasks import router as tasks_router
7+
from app.api.v1.tiers import router as tiers_router
8+
from app.api.v1.rate_limits import router as rate_limits_router
79

810
router = APIRouter(prefix="/v1")
911
router.include_router(login_router)
1012
router.include_router(users_router)
1113
router.include_router(posts_router)
1214
router.include_router(tasks_router)
15+
router.include_router(tiers_router)
16+
router.include_router(rate_limits_router)

src/app/api/v1/posts.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ async def write_post(
3939

4040

4141
@router.get("/{username}/posts", response_model=PaginatedListResponse[PostRead])
42-
@cache(key_prefix="{username}_posts", resource_id_name="username")
42+
@cache(
43+
key_prefix="{username}_posts:page_{page}:items_per_page:{items_per_page}",
44+
resource_id_name="username",
45+
expiration=60
46+
)
4347
async def read_posts(
4448
request: Request,
4549
username: str,
@@ -164,5 +168,5 @@ async def erase_db_post(
164168
if db_post is None:
165169
raise HTTPException(status_code=404, detail="Post not found")
166170

167-
await crud_posts.db_delete(db=db, db_object=db_post, id=id)
171+
await crud_posts.db_delete(db=db, id=id)
168172
return {"message": "Post deleted from the database"}

src/app/api/v1/rate_limits.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from typing import Annotated
2+
3+
from fastapi import Request, Depends, HTTPException
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
import fastapi
6+
7+
from app.schemas.rate_limit import (
8+
RateLimitRead,
9+
RateLimitCreate,
10+
RateLimitCreateInternal,
11+
RateLimitUpdate
12+
)
13+
from app.api.dependencies import get_current_superuser
14+
from app.core.database import async_get_db
15+
from app.crud.crud_rate_limit import crud_rate_limits
16+
from app.crud.crud_tier import crud_tiers
17+
from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset
18+
19+
router = fastapi.APIRouter(tags=["rate_limits"])
20+
21+
@router.post("/tier/{tier_id}/rate_limit", dependencies=[Depends(get_current_superuser)], status_code=201)
22+
async def write_rate_limit(
23+
request: Request,
24+
tier_id: int,
25+
rate_limit: RateLimitCreate,
26+
db: Annotated[AsyncSession, Depends(async_get_db)]
27+
):
28+
db_tier = await crud_tiers.exists(db=db, id=tier_id)
29+
if not db_tier:
30+
raise HTTPException(status_code=404, detail="Tier not found")
31+
32+
rate_limit_internal_dict = rate_limit.model_dump()
33+
rate_limit_internal_dict["tier_id"] = tier_id
34+
35+
db_rate_limit = await crud_rate_limits.exists(db=db, name=rate_limit_internal_dict["name"])
36+
if db_rate_limit:
37+
raise HTTPException(status_code=400, detail="Rate Limit Name not available")
38+
39+
rate_limit_internal = RateLimitCreateInternal(**rate_limit_internal_dict)
40+
return await crud_rate_limits.create(db=db, object=rate_limit_internal)
41+
42+
43+
@router.get("/tier/{tier_id}/rate_limits", response_model=PaginatedListResponse[RateLimitRead])
44+
async def read_rate_limits(
45+
request: Request,
46+
tier_id: int,
47+
db: Annotated[AsyncSession, Depends(async_get_db)],
48+
page: int = 1,
49+
items_per_page: int = 10
50+
):
51+
db_tier = await crud_tiers.exists(db=db, id=tier_id)
52+
if not db_tier:
53+
raise HTTPException(status_code=404, detail="Tier not found")
54+
55+
rate_limits_data = await crud_rate_limits.get_multi(
56+
db=db,
57+
offset=compute_offset(page, items_per_page),
58+
limit=items_per_page,
59+
schema_to_select=RateLimitRead,
60+
tier_id=tier_id
61+
)
62+
63+
return paginated_response(
64+
crud_data=rate_limits_data,
65+
page=page,
66+
items_per_page=items_per_page
67+
)
68+
69+
70+
@router.get("/tier/{tier_id}/rate_limit/{id}", response_model=RateLimitRead)
71+
async def read_rate_limit(
72+
request: Request,
73+
tier_id: int,
74+
id: int,
75+
db: Annotated[AsyncSession, Depends(async_get_db)]
76+
):
77+
db_tier = await crud_tiers.exists(db=db, id=tier_id)
78+
if not db_tier:
79+
raise HTTPException(status_code=404, detail="Tier not found")
80+
81+
db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, id=id)
82+
if db_rate_limit is None:
83+
raise HTTPException(status_code=404, detail="Rate Limit not found")
84+
85+
return db_rate_limit
86+
87+
88+
@router.patch("/tier/{tier_id}/rate_limit/{id}", dependencies=[Depends(get_current_superuser)])
89+
async def patch_rate_limit(
90+
request: Request,
91+
tier_id: int,
92+
id: int,
93+
values: RateLimitUpdate,
94+
db: Annotated[AsyncSession, Depends(async_get_db)]
95+
):
96+
db_tier = await crud_tiers.exists(db=db, id=tier_id)
97+
if not db_tier:
98+
raise HTTPException(status_code=404, detail="Tier not found")
99+
100+
db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, id=id)
101+
if db_rate_limit is None:
102+
raise HTTPException(status_code=404, detail="Rate Limit not found")
103+
104+
await crud_rate_limits.update(db=db, object=values, id=id)
105+
return {"message": "Rate Limit updated"}
106+
107+
108+
@router.delete("/tier/{tier_id}/rate_limit/{id}", dependencies=[Depends(get_current_superuser)])
109+
async def erase_rate_limit(
110+
request: Request,
111+
tier_id: int,
112+
id: int,
113+
db: Annotated[AsyncSession, Depends(async_get_db)]
114+
):
115+
db_tier = await crud_tiers.exists(db=db, id=tier_id)
116+
if not db_tier:
117+
raise HTTPException(status_code=404, detail="Tier not found")
118+
119+
db_rate_limit = await crud_rate_limits.get(db=db, schema_to_select=RateLimitRead, id=id)
120+
if db_rate_limit is None:
121+
raise HTTPException(status_code=404, detail="Rate Limit not found")
122+
123+
await crud_rate_limits.delete(db=db, db_row=db_rate_limit, id=id)
124+
return {"message": "Rate Limit deleted"}

src/app/api/v1/tasks.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
from app.core import queue
55
from app.schemas.job import Job
66

7-
router = APIRouter(prefix="/tasks", tags=["Tasks"])
8-
7+
router = APIRouter(prefix="/tasks", tags=["tasks"])
98

109
@router.post("/task", response_model=Job, status_code=201)
1110
async def create_task(message: str):

src/app/api/v1/tiers.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from typing import List, Annotated
2+
3+
from fastapi import Request, Depends, HTTPException
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
import fastapi
6+
7+
from app.schemas.tier import (
8+
TierRead,
9+
TierCreate,
10+
TierCreateInternal,
11+
TierUpdate
12+
)
13+
from app.api.dependencies import get_current_superuser
14+
from app.core.database import async_get_db
15+
from app.crud.crud_tier import crud_tiers
16+
from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset
17+
18+
router = fastapi.APIRouter(tags=["tiers"])
19+
20+
@router.post("/tier", dependencies=[Depends(get_current_superuser)], status_code=201)
21+
async def write_tier(
22+
request: Request,
23+
tier: TierCreate,
24+
db: Annotated[AsyncSession, Depends(async_get_db)]
25+
):
26+
tier_internal_dict = tier.model_dump()
27+
db_tier = await crud_tiers.exists(db=db, name=tier_internal_dict["name"])
28+
if db_tier:
29+
raise HTTPException(status_code=400, detail="Tier Name not available")
30+
31+
tier_internal = TierCreateInternal(**tier_internal_dict)
32+
return await crud_tiers.create(db=db, object=tier_internal)
33+
34+
35+
@router.get("/tiers", response_model=PaginatedListResponse[TierRead])
36+
async def read_tiers(
37+
request: Request,
38+
db: Annotated[AsyncSession, Depends(async_get_db)],
39+
page: int = 1,
40+
items_per_page: int = 10
41+
):
42+
tiers_data = await crud_tiers.get_multi(
43+
db=db,
44+
offset=compute_offset(page, items_per_page),
45+
limit=items_per_page,
46+
schema_to_select=TierRead
47+
)
48+
49+
return paginated_response(
50+
crud_data=tiers_data,
51+
page=page,
52+
items_per_page=items_per_page
53+
)
54+
55+
56+
@router.get("/tier/{id}", response_model=TierRead)
57+
async def read_tier(
58+
request: Request,
59+
id: int,
60+
db: Annotated[AsyncSession, Depends(async_get_db)]
61+
):
62+
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, id=id)
63+
if db_tier is None:
64+
raise HTTPException(status_code=404, detail="Tier not found")
65+
66+
return db_tier
67+
68+
69+
@router.patch("/tier/{id}", dependencies=[Depends(get_current_superuser)])
70+
async def patch_tier(
71+
request: Request,
72+
values: TierUpdate,
73+
id: int,
74+
db: Annotated[AsyncSession, Depends(async_get_db)]
75+
):
76+
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, id=id)
77+
if db_tier is None:
78+
raise HTTPException(status_code=404, detail="Tier not found")
79+
80+
await crud_tiers.update(db=db, object=values, id=id)
81+
return {"message": "Tier updated"}
82+
83+
84+
@router.delete("/tier/{id}", dependencies=[Depends(get_current_superuser)])
85+
async def erase_tier(
86+
request: Request,
87+
id: int,
88+
db: Annotated[AsyncSession, Depends(async_get_db)]
89+
):
90+
db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, id=id)
91+
if db_tier is None:
92+
raise HTTPException(status_code=404, detail="Tier not found")
93+
94+
await crud_tiers.delete(db=db, db_row=db_tier, id=id)
95+
return {"message": "Tier deleted"}

src/app/core/cache.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ async def inner(request: Request, *args, **kwargs) -> Response:
220220

221221
cached_data = await client.get(cache_key)
222222
if cached_data:
223-
print("cache hit")
224223
return json.loads(cached_data.decode())
225224

226225
result = await func(request, *args, **kwargs)

src/app/core/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from enum import Enum
22

3-
#from decouple import config
43
from starlette.config import Config
54
from pydantic_settings import BaseSettings
65

src/app/core/rate_limit.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from app.core.logger import logging
99
from app.schemas.user import UserRead
1010
from app.schemas.tier import TierRead
11+
from app.crud.crud_users import crud_users
12+
1113

1214
logger = logging.getLogger(__name__)
1315

0 commit comments

Comments
 (0)