Skip to content

Commit 63edfd0

Browse files
committed
Merge branch 'feature/#41' into feature/#32
2 parents 1e2393e + bdb9ab9 commit 63edfd0

File tree

17 files changed

+522
-192
lines changed

17 files changed

+522
-192
lines changed

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/routers/authentication.py

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,44 @@
1414
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/authentication/token")
1515

1616

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

@@ -31,43 +69,6 @@ async def authenticate_community(
3169
return None
3270
return found_community
3371

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-
7172
# Teste
7273

7374
@router.post("/create_commumity")

app/routers/libraries/routes.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
from typing import List
2+
13
from fastapi import APIRouter, HTTPException, Request, status
24
from pydantic import BaseModel
35

46
from app.schemas import Library as LibrarySchema
7+
from app.schemas import LibraryNews
58
from app.schemas import Subscription as SubscriptionSchema
69
from app.services.database.models import Library, Subscription
710
from app.services.database.orm.library import (
11+
get_libraries_by_language,
812
get_library_ids_by_multiple_names,
913
insert_library,
1014
)
@@ -22,6 +26,36 @@ class SubscribeLibraryResponse(BaseModel):
2226
def setup():
2327
router = APIRouter(prefix="/libraries", tags=["libraries"])
2428

29+
@router.get(
30+
"",
31+
response_model=List[LibrarySchema],
32+
status_code=status.HTTP_200_OK,
33+
summary="Get libraries by language",
34+
description="Get libraries by language",
35+
)
36+
async def get_by_language(request: Request, language: str):
37+
libraryList = await get_libraries_by_language(
38+
language=language, session=request.app.db_session_factory
39+
)
40+
return [
41+
LibrarySchema(
42+
library_name=libraryDb.library_name,
43+
news=[
44+
LibraryNews(
45+
tag=news["tag"], description=news["description"]
46+
)
47+
for news in libraryDb.news
48+
],
49+
logo=libraryDb.logo,
50+
version=libraryDb.version,
51+
release_date=libraryDb.release_date,
52+
releases_doc_url=libraryDb.releases_doc_url,
53+
fixed_release_url=libraryDb.fixed_release_url,
54+
language=libraryDb.language,
55+
)
56+
for libraryDb in libraryList
57+
]
58+
2559
@router.post(
2660
"",
2761
response_model=LibraryResponse,
@@ -35,12 +69,13 @@ async def create_library(
3569
):
3670
library = Library(
3771
library_name=body.library_name,
38-
user_email="", # TODO: Considerar obter o email do usuário autenticado
39-
logo=body.logo.encoded_string(),
72+
news=[news.model_dump() for news in body.news],
73+
logo=body.logo,
4074
version=body.version,
4175
release_date=body.release_date,
42-
releases_doc_url=body.releases_doc_url.encoded_string(),
43-
fixed_release_url=body.fixed_release_url.encoded_string(),
76+
releases_doc_url=body.releases_doc_url,
77+
fixed_release_url=body.fixed_release_url,
78+
language=body.language,
4479
)
4580
try:
4681
await insert_library(library, request.app.db_session_factory)

app/routers/news/routes.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
from fastapi import APIRouter, Request, status
1+
from typing import Annotated
2+
3+
from fastapi import APIRouter, Depends, Request, status
4+
from fastapi.params import Header
25
from pydantic import BaseModel
3-
from services.database.orm.news import get_news_by_query_params
6+
7+
from app.routers.authentication import get_current_active_community
8+
from app.schemas import News
9+
from app.services.database.models import Community as DBCommunity
10+
from app.services.database.orm.news import create_news, get_news_by_query_params
411

512

613
class NewsPostResponse(BaseModel):
@@ -22,10 +29,22 @@ def setup():
2229
summary="News endpoint",
2330
description="Creates news and returns a confirmation message",
2431
)
25-
async def post_news():
32+
async def post_news(
33+
request: Request,
34+
current_community: Annotated[
35+
DBCommunity, Depends(get_current_active_community)
36+
],
37+
news: News,
38+
user_email: str = Header(..., alias="user-email"),
39+
):
2640
"""
2741
News endpoint that creates news and returns a confirmation message.
2842
"""
43+
news_dict = news.__dict__
44+
news_dict["user_email"] = user_email
45+
await create_news(
46+
session=request.app.db_session_factory, news=news_dict
47+
)
2948
return NewsPostResponse()
3049

3150
@router.get(
@@ -35,16 +54,25 @@ async def post_news():
3554
summary="Get News",
3655
description="Retrieves news filtered by user and query params",
3756
)
38-
async def get_news(request: Request):
57+
async def get_news(
58+
request: Request,
59+
current_community: Annotated[
60+
DBCommunity, Depends(get_current_active_community)
61+
],
62+
id: str | None = None,
63+
user_email: str = Header(..., alias="user-email"),
64+
category: str | None = None,
65+
tags: str | None = None,
66+
):
3967
"""
4068
Get News endpoint that retrieves news filtered by user and query params.
4169
"""
4270
news_list = await get_news_by_query_params(
4371
session=request.app.db_session_factory,
44-
id=request.query_params.get("id"),
45-
user_email=request.headers.get("user-email"),
46-
category=request.query_params.get("category"),
47-
tags=request.query_params.get("tags"),
72+
id=id,
73+
email=user_email,
74+
category=category,
75+
tags=tags,
4876
)
4977
return NewsGetResponse(news_list=news_list)
5078

app/schemas.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
11
from datetime import date
2-
from enum import Enum
32
from typing import List
43

5-
from pydantic import BaseModel, HttpUrl
4+
from pydantic import BaseModel
65

7-
8-
class LibraryTagEnum(str, Enum):
9-
UPDATES = "updates"
10-
BUG_FIX = "bug_fix"
11-
NEW_FEATURE = "new_feature"
12-
SECURITY_FIX = "security_fix"
13-
DEPRECATION = "deprecation"
6+
from app.enums import LibraryTagUpdatesEnum
147

158

169
class LibraryNews(BaseModel):
17-
tag: LibraryTagEnum
10+
tag: LibraryTagUpdatesEnum
1811
description: str
1912

2013

2114
class Library(BaseModel):
15+
id: int | None = None
2216
library_name: str
2317
news: List[LibraryNews]
24-
logo: HttpUrl
18+
logo: str
2519
version: str
2620
release_date: date
27-
releases_doc_url: HttpUrl
28-
fixed_release_url: HttpUrl
21+
releases_doc_url: str
22+
fixed_release_url: str
23+
language: str
2924

3025

3126
# Community / User Class
@@ -39,6 +34,16 @@ class CommunityInDB(Community):
3934
password: str
4035

4136

37+
class News(BaseModel):
38+
title: str
39+
content: str
40+
category: str
41+
tags: str | None = None
42+
source_url: str
43+
social_media_url: str | None = None
44+
likes: int = 0
45+
46+
4247
class Token(BaseModel):
4348
access_token: str
4449
token_type: str
@@ -51,5 +56,5 @@ class TokenPayload(BaseModel):
5156

5257
class Subscription(BaseModel):
5358
email: str
54-
tags: List[LibraryTagEnum]
59+
tags: List[LibraryTagUpdatesEnum]
5560
libraries_list: List[str]
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import date
2-
from typing import Optional
2+
from typing import List, Optional
33

4+
from sqlalchemy import JSON, Column
45
from sqlmodel import Field, SQLModel
56

67

@@ -9,12 +10,13 @@ class Library(SQLModel, table=True):
910

1011
id: Optional[int] = Field(default=None, primary_key=True)
1112
library_name: str
12-
user_email: str
13+
news: List[dict] = Field(sa_column=Column(JSON))
1314
logo: str
1415
version: str
1516
release_date: date
1617
releases_doc_url: str
1718
fixed_release_url: str
19+
language: str
1820
community_id: Optional[int] = Field(
1921
default=None, foreign_key="communities.id"
2022
)

app/services/database/models/subscriptions.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
from typing import List, Optional
22

3-
from schemas import LibraryTagEnum
43
from sqlalchemy import JSON, Column
54
from sqlmodel import Field, SQLModel
65

6+
from app.enums import LibraryTagUpdatesEnum
7+
78

89
class Subscription(SQLModel, table=True):
910
__tablename__ = "subscriptions" # type: ignore
1011

1112
id: Optional[int] = Field(default=None, primary_key=True)
1213
email: str
13-
tags: List[LibraryTagEnum] = Field(sa_column=Column(JSON))
14+
tags: List[LibraryTagUpdatesEnum] = Field(sa_column=Column(JSON))
1415
community_id: Optional[int] = Field(
1516
default=None, foreign_key="communities.id"
1617
)

app/services/database/orm/library.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,14 @@ async def get_library_ids_by_multiple_names(
2626
)
2727
result = await session.exec(statement)
2828
return [id for id in result.all() if id is not None]
29+
30+
31+
async def get_libraries_by_language(
32+
language: str,
33+
session: AsyncSession,
34+
) -> List[Library]:
35+
statement = select(Library).where(
36+
func.lower(Library.language) == language.lower()
37+
)
38+
result = await session.exec(statement)
39+
return [library for library in result.all()]

0 commit comments

Comments
 (0)