Skip to content

Commit e4ea178

Browse files
committed
Merge branch 'main' into feature/#33
2 parents 9d2169e + d40db98 commit e4ea178

29 files changed

Lines changed: 1223 additions & 218 deletions

.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,10 @@
99
},
1010
"python-envs.defaultEnvManager": "ms-python.python:poetry",
1111
"python-envs.defaultPackageManager": "ms-python.python:poetry",
12-
"python-envs.pythonProjects": []
12+
"python-envs.pythonProjects": [],
13+
"python.testing.pytestArgs": [
14+
"tests"
15+
],
16+
"python.testing.unittestEnabled": false,
17+
"python.testing.pytestEnabled": true
1318
}

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
API_CONTAINER_NAME=pynews-server
12
.PHONY: help build up down logs test lint format clean dev prod restart health
23

34
# Colors for terminal output
@@ -73,3 +74,7 @@ shell: ## Entra no shell do container
7374
setup: install build up ## Setup completo do projeto
7475
@echo "$(GREEN)Setup completo realizado!$(NC)"
7576
@echo "$(GREEN)Acesse: http://localhost:8000/docs$(NC)"
77+
78+
79+
docker/test:
80+
docker exec -e PYTHONPATH=/app $(API_CONTAINER_NAME) pytest -s --cov-report=term-missing --cov-report html --cov-report=xml --cov=app tests/

app/enums.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from enum import Enum
2+
3+
4+
class LibraryTagUpdatesEnum(str, Enum):
5+
UPDATE = "updates"
6+
BUG_FIX = "bug_fix"
7+
NEW_FEATURE = "new_feature"
8+
SECURITY_FIX = "security_fix"

app/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from contextlib import asynccontextmanager
33

44
from fastapi import FastAPI
5+
from slowapi import _rate_limit_exceeded_handler
56

67
from app.routers.router import setup_router as setup_router_v2
78
from app.services.database.database import AsyncSessionLocal, init_db
9+
from app.services.limiter import limiter
810

911
logger = logging.getLogger(__name__)
1012

@@ -27,6 +29,9 @@ async def lifespan(app: FastAPI):
2729
)
2830

2931

32+
app.state.limiter = limiter
33+
app.add_exception_handler(429, _rate_limit_exceeded_handler)
34+
3035
app.include_router(setup_router_v2(), prefix="/api")
3136

3237
logger.info("PyNews Server Starter")

app/routers/authentication.py

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,49 @@
1010
from app.services import auth
1111
from app.services.database.models import Community as DBCommunity
1212
from app.services.database.orm.community import get_community_by_username
13+
from app.services.limiter import limiter
1314

1415
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/authentication/token")
1516

1617

18+
async def get_current_community(
19+
request: Request,
20+
token: Annotated[str, Depends(oauth2_scheme)],
21+
) -> DBCommunity:
22+
credentials_exception = HTTPException(
23+
status_code=status.HTTP_401_UNAUTHORIZED,
24+
detail="Could not validate credentials",
25+
headers={"WWW-Authenticate": "Bearer"},
26+
)
27+
28+
try:
29+
payload = jwt.decode(
30+
token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]
31+
)
32+
username = payload.get("sub")
33+
if username is None:
34+
raise credentials_exception
35+
token_data = TokenPayload(username=username)
36+
except InvalidTokenError:
37+
raise credentials_exception
38+
session: AsyncSession = request.app.db_session_factory
39+
community = await get_community_by_username(
40+
session=session, username=token_data.username
41+
)
42+
if community is None:
43+
raise credentials_exception
44+
45+
return community
46+
47+
48+
async def get_current_active_community(
49+
current_user: Annotated[DBCommunity, Depends(get_current_community)],
50+
) -> DBCommunity:
51+
# A função simplesmente retorna o usuário.
52+
# Pode ser estendido futuramente para verificar um status "ativo".
53+
return current_user
54+
55+
1756
def setup():
1857
router = APIRouter(prefix="/authentication", tags=["authentication"])
1958

@@ -31,43 +70,6 @@ async def authenticate_community(
3170
return None
3271
return found_community
3372

34-
# Teste
35-
async def get_current_community(
36-
request: Request,
37-
token: Annotated[str, Depends(oauth2_scheme)],
38-
) -> DBCommunity:
39-
credentials_exception = HTTPException(
40-
status_code=status.HTTP_401_UNAUTHORIZED,
41-
detail="Could not validate credentials",
42-
headers={"WWW-Authenticate": "Bearer"},
43-
)
44-
45-
try:
46-
payload = jwt.decode(
47-
token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]
48-
)
49-
username = payload.get("sub")
50-
if username is None:
51-
raise credentials_exception
52-
token_data = TokenPayload(username=username)
53-
except InvalidTokenError:
54-
raise credentials_exception
55-
session: AsyncSession = request.app.db_session_factory
56-
community = await get_community_by_username(
57-
session=session, username=token_data.username
58-
)
59-
if community is None:
60-
raise credentials_exception
61-
62-
return community
63-
64-
async def get_current_active_community(
65-
current_user: Annotated[DBCommunity, Depends(get_current_community)],
66-
) -> DBCommunity:
67-
# A função simplesmente retorna o usuário.
68-
# Pode ser estendido futuramente para verificar um status "ativo".
69-
return current_user
70-
7173
# Teste
7274

7375
@router.post("/create_commumity")
@@ -88,6 +90,7 @@ async def create_community(request: Request):
8890
# Teste
8991

9092
@router.post("/token", response_model=Token)
93+
@limiter.limit("60/minute")
9194
async def login_for_access_token(
9295
request: Request, form_data: OAuth2PasswordRequestForm = Depends()
9396
):
@@ -109,7 +112,9 @@ async def login_for_access_token(
109112
}
110113

111114
@router.get("/me", response_model=Community)
115+
@limiter.limit("60/minute")
112116
async def read_community_me(
117+
request: Request,
113118
current_community: Annotated[
114119
DBCommunity, Depends(get_current_active_community)
115120
],

app/routers/libraries/routes.py

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
from fastapi import APIRouter, HTTPException, Request, status
1+
from typing import Annotated, List
2+
3+
from fastapi import APIRouter, Header, HTTPException, Request, status
24
from pydantic import BaseModel
35

46
from app.schemas import Library as LibrarySchema
7+
from app.schemas import LibraryNews
8+
from app.schemas import LibraryRequest as LibraryRequestSchema
59
from app.schemas import Subscription as SubscriptionSchema
610
from app.services.database.models import Library, Subscription
11+
from app.services.database.models.libraries_request import LibraryRequest
712
from app.services.database.orm.library import (
13+
get_libraries_by_language,
814
get_library_ids_by_multiple_names,
915
insert_library,
1016
)
17+
from app.services.database.orm.library_request import insert_library_request
1118
from app.services.database.orm.subscription import upsert_multiple_subscription
1219

1320

@@ -22,6 +29,41 @@ class SubscribeLibraryResponse(BaseModel):
2229
def setup():
2330
router = APIRouter(prefix="/libraries", tags=["libraries"])
2431

32+
@router.get(
33+
"",
34+
response_model=List[LibrarySchema],
35+
status_code=status.HTTP_200_OK,
36+
summary="Get libraries by language",
37+
description="Get libraries by language",
38+
)
39+
async def get_by_language(request: Request, language: str):
40+
try:
41+
libraryList = await get_libraries_by_language(
42+
language=language, session=request.app.db_session_factory
43+
)
44+
return [
45+
LibrarySchema(
46+
library_name=libraryDb.library_name,
47+
news=[
48+
LibraryNews(
49+
tag=news["tag"], description=news["description"]
50+
)
51+
for news in libraryDb.news
52+
],
53+
logo=libraryDb.logo,
54+
version=libraryDb.version,
55+
release_date=libraryDb.release_date,
56+
releases_doc_url=libraryDb.releases_doc_url,
57+
fixed_release_url=libraryDb.fixed_release_url,
58+
language=libraryDb.language,
59+
)
60+
for libraryDb in libraryList
61+
]
62+
except HTTPException as e:
63+
raise e
64+
except Exception as e:
65+
HTTPException(status_code=500, detail=f"Unexpected error: {e}")
66+
2567
@router.post(
2668
"",
2769
response_model=LibraryResponse,
@@ -35,16 +77,22 @@ async def create_library(
3577
):
3678
library = Library(
3779
library_name=body.library_name,
38-
user_email="", # TODO: Considerar obter o email do usuário autenticado
39-
releases_url=body.releases_url.encoded_string(),
40-
logo=body.logo.encoded_string(),
80+
news=[news.model_dump() for news in body.news],
81+
logo=body.logo,
82+
version=body.version,
83+
release_date=body.release_date,
84+
releases_doc_url=body.releases_doc_url,
85+
fixed_release_url=body.fixed_release_url,
86+
language=body.language,
4187
)
4288
try:
4389
await insert_library(library, request.app.db_session_factory)
4490
return LibraryResponse()
91+
except HTTPException as e:
92+
raise e
4593
except Exception as e:
4694
raise HTTPException(
47-
status_code=500, detail=f"Failed to create library: {e}"
95+
status_code=500, detail=f"Unexpected error: {e}"
4896
)
4997

5098
@router.post(
@@ -59,14 +107,22 @@ async def create_library(
59107
async def subscribe_libraries(
60108
request: Request,
61109
body: SubscriptionSchema,
110+
user_email: Annotated[str, Header(alias="user-email")],
62111
):
63112
try:
64113
library_ids = await get_library_ids_by_multiple_names(
65114
body.libraries_list, request.app.db_session_factory
66115
)
67116

117+
if (library_ids is None) or (len(library_ids) == 0):
118+
raise HTTPException(
119+
status_code=404, detail="Libraries not found"
120+
)
121+
68122
subscriptions = [
69-
Subscription(email=body.email, tags=body.tags, library_id=id)
123+
Subscription(
124+
user_email=user_email, tags=body.tags, library_id=id
125+
)
70126
for id in library_ids
71127
]
72128

@@ -75,9 +131,42 @@ async def subscribe_libraries(
75131
)
76132

77133
return SubscribeLibraryResponse()
134+
except HTTPException as e:
135+
raise e
136+
except Exception as e:
137+
raise HTTPException(
138+
status_code=500, detail=f"Unexpected error: {e}"
139+
)
140+
141+
@router.post(
142+
"/request",
143+
response_model=LibraryResponse,
144+
status_code=status.HTTP_200_OK,
145+
summary="Request a library",
146+
description="Request a library to follow",
147+
)
148+
async def request_library(
149+
request: Request,
150+
body: LibraryRequestSchema,
151+
user_email: Annotated[str, Header(alias="user-email")],
152+
):
153+
try:
154+
library_request = LibraryRequest(
155+
user_email=user_email,
156+
library_name=body.library_name,
157+
library_home_page=body.library_home_page,
158+
)
159+
160+
await insert_library_request(
161+
library_request, request.app.db_session_factory
162+
)
163+
164+
return LibraryResponse()
165+
except HTTPException as e:
166+
raise e
78167
except Exception as e:
79168
raise HTTPException(
80-
status_code=500, detail=f"Subscription failed: {e}"
169+
status_code=500, detail=f"Unexpected error: {e}"
81170
)
82171

83172
return router

0 commit comments

Comments
 (0)