Skip to content

Feature/#29 #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,6 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/

# SQLiteDB
pynewsdb.db
2 changes: 1 addition & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ async def lifespan(app: FastAPI):
)


app.include_router(setup_router_v2(), prefix="/api")
app.include_router(setup_router_v2(), prefix="/api")

logger.info("PyNews Server Starter")
60 changes: 37 additions & 23 deletions app/routers/authentication.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,71 @@
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from services.database.orm.community import get_community_by_username
from sqlmodel.ext.asyncio.session import AsyncSession
import jwt
from jwt.exceptions import InvalidTokenError

from app.schemas import Token, TokenPayload
from app.services import auth
from app.schemas import Token, TokenPayload, Community
from app.services.database.models import Community as DBCommunity
from services.database.orm.community import get_community_by_username

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


def setup():
router = APIRouter(prefix='/authentication', tags=['authentication'])
async def authenticate_community(request: Request , username: str, password: str):
router = APIRouter(prefix="/authentication", tags=["authentication"])

async def authenticate_community(
request: Request, username: str, password: str
):
# Valida se o usuário existe e se a senha está correta
found_community = await get_community_by_username(
username=username,
session=request.app.db_session_factory
)
if not found_community or not auth.verify_password(password, found_community.password):
username=username, session=request.app.db_session_factory
)
if not found_community or not auth.verify_password(
password, found_community.password
):
return None
return found_community


#### Teste
# Teste

@router.post("/create_commumity")
async def create_community(request: Request ):
async def create_community(request: Request):
password = "123Asd!@#"
hashed_password=auth.hash_password(password)
community = DBCommunity(username="username", email="[email protected]", password=hashed_password)
hashed_password = auth.hash_password(password)
community = DBCommunity(
username="username",
email="[email protected]",
password=hashed_password,
)
session: AsyncSession = request.app.db_session_factory
session.add(community)
await session.commit()
await session.refresh(community)
return {'msg':'succes? '}
#### Teste
return {"msg": "succes? "}

# Teste

@router.post("/token", response_model=Token)
async def login_for_access_token(request: Request , form_data: OAuth2PasswordRequestForm = Depends() ) :
async def login_for_access_token(
request: Request, form_data: OAuth2PasswordRequestForm = Depends()
):
# Rota de login: valida credenciais e retorna token JWT
community = await authenticate_community(form_data.username, form_data.password, request.app.db_session_factory)
community = await authenticate_community(
form_data.username,
form_data.password,
request.app.db_session_factory,
)
if not community:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciais inválidas"
detail="Credenciais inválidas",
)
payload = TokenPayload(username=community.username)
token, expires_in = auth.create_access_token(data=payload)
return {
"access_token": token,
"token_type": "Bearer",
"expires_in": expires_in
"expires_in": expires_in,
}
return router # Retorna o router configurado com as rotas de autenticação

return router # Retorna o router configurado com as rotas de autenticação
File renamed without changes.
83 changes: 83 additions & 0 deletions app/routers/libraries/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from fastapi import APIRouter, HTTPException, Request, status
from pydantic import BaseModel

from app.schemas import Library as LibrarySchema
from app.schemas import Subscription as SubscriptionSchema
from app.services.database.models import Library, Subscription
from app.services.database.orm.library import (
get_library_ids_by_multiple_names,
insert_library,
)
from app.services.database.orm.subscription import upsert_multiple_subscription


class LibraryResponse(BaseModel):
status: str = "Library created successfully"


class SubscribeLibraryResponse(BaseModel):
status: str = "Subscribed in libraries successfully"


def setup():
router = APIRouter(prefix="/libraries", tags=["libraries"])

@router.post(
"",
response_model=LibraryResponse,
status_code=status.HTTP_200_OK,
summary="Create a library",
description="Create a new library to follow",
)
async def create_library(
request: Request,
body: LibrarySchema,
):
library = Library(
library_name=body.library_name,
user_email="", # TODO: Considerar obter o email do usuário autenticado
releases_url=body.releases_url.encoded_string(),
logo=body.logo.encoded_string(),
)
try:
await insert_library(library, request.app.db_session_factory)
return LibraryResponse()
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Failed to create library: {e}"
)

@router.post(
"/subscribe",
response_model=SubscribeLibraryResponse,
status_code=status.HTTP_200_OK,
summary="Subscribe to receive library updates",
description=(
"Subscribe to multiple libs and tags to receive libs updates"
),
)
async def subscribe_libraries(
request: Request,
body: SubscriptionSchema,
):
try:
library_ids = await get_library_ids_by_multiple_names(
body.libraries_list, request.app.db_session_factory
)

subscriptions = [
Subscription(email=body.email, tags=body.tags, library_id=id)
for id in library_ids
]

await upsert_multiple_subscription(
subscriptions, request.app.db_session_factory
)

return SubscribeLibraryResponse()
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Subscription failed: {e}"
)

return router
7 changes: 5 additions & 2 deletions app/routers/router.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from fastapi import APIRouter

from app.routers.authentication import setup as authentication_router_setup
from app.routers.healthcheck.routes import setup as healthcheck_router_setup
from app.routers.libraries.routes import setup as libraries_router_setup
from app.routers.news.routes import setup as news_router_setup
from app.routers.authentication import setup as authentication_router_setup


def setup_router() -> APIRouter:
router = APIRouter()
router.include_router(healthcheck_router_setup(), prefix="")
router.include_router(news_router_setup(), prefix="")
router.include_router(authentication_router_setup(), prefix='')
router.include_router(authentication_router_setup(), prefix="")
router.include_router(libraries_router_setup(), prefix="")
return router
42 changes: 21 additions & 21 deletions app/schemas.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
from pydantic import BaseModel, HttpUrl
from datetime import datetime
from typing import List
from enum import Enum
from typing import List

from pydantic import BaseModel, HttpUrl

## News
class News(BaseModel):
description: str
tag: str

class Library(BaseModel):
library_name: str
news: list[News]
releases_url: HttpUrl
logo: HttpUrl
version: str
release_date: datetime
release_doc_url: HttpUrl

## Community / User Class

# Community / User Class
class Community(BaseModel):
username: str
email: str
## Extends Community Class with hashed password


# Extends Community Class with hashed password
class CommunityInDB(Community):
password: str


class Token(BaseModel):
access_token: str
token_type: str
expires_in: int


class TokenPayload(BaseModel):
username: str

## Subscription Class
class TagEnum(str, Enum):
bug_fix = "bug_fix"
update = "update"
deprecate = "deprecate"
new_feature = "new_feature"
security_fix = "security_fix"

# Subscription Class
class SubscriptionTagEnum(str, Enum):
UPDATE = "update"
BUG_FIX = "bug_fix"
NEW_FEATURE = "new_feature"
SECURITY_FIX = "security_fix"


class Subscription(BaseModel):
tags: List[TagEnum]
email: str
tags: List[SubscriptionTagEnum]
libraries_list: List[str]
15 changes: 10 additions & 5 deletions app/services/database/models/subscriptions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from typing import Optional
from typing import List, Optional

from sqlmodel import SQLModel, Field
from schemas import SubscriptionTagEnum
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel


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

id: Optional[int] = Field(default=None, primary_key=True)
email: str
tags: str
community_id: Optional[int] = Field(default=None, foreign_key="communities.id")
tags: List[SubscriptionTagEnum] = Field(sa_column=Column(JSON))
community_id: Optional[int] = Field(
default=None, foreign_key="communities.id"
)
library_id: Optional[int] = Field(default=None, foreign_key="libraries.id")
28 changes: 28 additions & 0 deletions app/services/database/orm/library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import List

from sqlmodel import func, select
from sqlmodel.ext.asyncio.session import AsyncSession

from app.services.database.models import Library


async def insert_library(
library: Library,
session: AsyncSession,
) -> Library:
session.add(library)
await session.commit()
await session.refresh(library)
return library


async def get_library_ids_by_multiple_names(
names: List[str],
session: AsyncSession,
) -> List[int]:
lower_case_names = [name.lower() for name in names]
statement = select(Library.id).where(
func.lower(Library.library_name).in_(lower_case_names)
)
result = await session.exec(statement)
return [id for id in result.all() if id is not None]
46 changes: 46 additions & 0 deletions app/services/database/orm/subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Dict, List, Tuple

from sqlalchemy import tuple_
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

from app.services.database.models.subscriptions import Subscription


async def upsert_multiple_subscription(
subscriptions: List[Subscription],
session: AsyncSession,
) -> List[Subscription]:
if not subscriptions:
return []

incoming_map: Dict[Tuple[str, int], Subscription] = {
(sub.email, sub.library_id): sub for sub in subscriptions
}

keys_to_check = incoming_map.keys()
stmt = select(Subscription).where(
tuple_(Subscription.email, Subscription.library_id).in_(keys_to_check)
)
result = await session.exec(stmt)
existing_subscriptions = result.all()
existing_map: Dict[Tuple[str, int], Subscription] = {
(sub.email, sub.library_id): sub for sub in existing_subscriptions
}

new_subscriptions: List[Subscription] = []
for key, sub_to_upsert in incoming_map.items():
if existing_sub := existing_map.get(key):
existing_sub.tags = sub_to_upsert.tags
else:
new_subscriptions.append(sub_to_upsert)

session.add_all(new_subscriptions)
await session.commit()

all_subs = list(existing_subscriptions) + new_subscriptions

for sub in all_subs:
await session.refresh(sub)

return all_subs