Skip to content

Commit 98c3e4b

Browse files
authored
Merge pull request #38 from PythonFloripa/feature/#29
Feature/#29
2 parents 6f8507c + 20b456d commit 98c3e4b

File tree

10 files changed

+237
-38
lines changed

10 files changed

+237
-38
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,4 @@ marimo/_lsp/
207207
__marimo__/
208208

209209
# SQLiteDB
210-
pynewsdb.db
210+
pynewsdb.db

app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ async def lifespan(app: FastAPI):
2727
)
2828

2929

30-
app.include_router(setup_router_v2(), prefix="/api")
30+
app.include_router(setup_router_v2(), prefix="/api")
3131

3232
logger.info("PyNews Server Starter")

app/routers/authentication.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ async def authenticate_community(
3131
return None
3232
return found_community
3333

34+
# Teste
3435
async def get_current_community(
3536
request: Request,
3637
token: Annotated[str, Depends(oauth2_scheme)],

app/routers/libraries/routes.py

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
from fastapi import APIRouter, Request, status
1+
from fastapi import APIRouter, HTTPException, Request, status
22
from pydantic import BaseModel
33

44
from app.schemas import Library as LibrarySchema
5-
from app.services.database.models.libraries import Library
6-
from app.services.database.orm.library import insert_library
5+
from app.schemas import Subscription as SubscriptionSchema
6+
from app.services.database.models import Library, Subscription
7+
from app.services.database.orm.library import (
8+
get_library_ids_by_multiple_names,
9+
insert_library,
10+
)
11+
from app.services.database.orm.subscription import upsert_multiple_subscription
712

813

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

1217

18+
class SubscribeLibraryResponse(BaseModel):
19+
status: str = "Subscribed in libraries successfully"
20+
21+
1322
def setup():
1423
router = APIRouter(prefix="/libraries", tags=["libraries"])
1524

@@ -24,16 +33,51 @@ async def create_library(
2433
request: Request,
2534
body: LibrarySchema,
2635
):
27-
await insert_library(
28-
Library(
29-
library_name=body.library_name,
30-
user_email="",
31-
releases_url=body.releases_url.encoded_string(),
32-
logo=body.logo.encoded_string(),
33-
),
34-
request.app.db_session_factory,
36+
library = Library(
37+
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(),
3541
)
42+
try:
43+
await insert_library(library, request.app.db_session_factory)
44+
return LibraryResponse()
45+
except Exception as e:
46+
raise HTTPException(
47+
status_code=500, detail=f"Failed to create library: {e}"
48+
)
49+
50+
@router.post(
51+
"/subscribe",
52+
response_model=SubscribeLibraryResponse,
53+
status_code=status.HTTP_200_OK,
54+
summary="Subscribe to receive library updates",
55+
description=(
56+
"Subscribe to multiple libs and tags to receive libs updates"
57+
),
58+
)
59+
async def subscribe_libraries(
60+
request: Request,
61+
body: SubscriptionSchema,
62+
):
63+
try:
64+
library_ids = await get_library_ids_by_multiple_names(
65+
body.libraries_list, request.app.db_session_factory
66+
)
67+
68+
subscriptions = [
69+
Subscription(email=body.email, tags=body.tags, library_id=id)
70+
for id in library_ids
71+
]
72+
73+
await upsert_multiple_subscription(
74+
subscriptions, request.app.db_session_factory
75+
)
3676

37-
return LibraryResponse()
77+
return SubscribeLibraryResponse()
78+
except Exception as e:
79+
raise HTTPException(
80+
status_code=500, detail=f"Subscription failed: {e}"
81+
)
3882

3983
return router

app/schemas.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ class TokenPayload(BaseModel):
3232

3333

3434
# Subscription Class
35-
class TagEnum(str, Enum):
36-
bug_fix = "bug_fix"
37-
update = "update"
38-
deprecate = "deprecate"
39-
new_feature = "new_feature"
40-
security_fix = "security_fix"
35+
class SubscriptionTagEnum(str, Enum):
36+
UPDATE = "update"
37+
BUG_FIX = "bug_fix"
38+
NEW_FEATURE = "new_feature"
39+
SECURITY_FIX = "security_fix"
4140

4241

4342
class Subscription(BaseModel):
44-
tags: List[TagEnum]
43+
email: str
44+
tags: List[SubscriptionTagEnum]
4545
libraries_list: List[str]
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
from typing import Optional
1+
from typing import List, Optional
22

3-
from sqlmodel import SQLModel, Field
3+
from schemas import SubscriptionTagEnum
4+
from sqlalchemy import JSON, Column
5+
from sqlmodel import Field, SQLModel
46

57

68
class Subscription(SQLModel, table=True):
7-
__tablename__ = 'subscriptions'
9+
__tablename__ = "subscriptions" # type: ignore
810

911
id: Optional[int] = Field(default=None, primary_key=True)
1012
email: str
11-
tags: str
12-
community_id: Optional[int] = Field(default=None, foreign_key="communities.id")
13+
tags: List[SubscriptionTagEnum] = Field(sa_column=Column(JSON))
14+
community_id: Optional[int] = Field(
15+
default=None, foreign_key="communities.id"
16+
)
17+
library_id: Optional[int] = Field(default=None, foreign_key="libraries.id")
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
1+
from typing import List
2+
3+
from sqlmodel import func, select
14
from sqlmodel.ext.asyncio.session import AsyncSession
25

3-
from app.services.database.models.libraries import Library
6+
from app.services.database.models import Library
47

58

69
async def insert_library(
710
library: Library,
811
session: AsyncSession,
9-
):
12+
) -> Library:
1013
session.add(library)
1114
await session.commit()
1215
await session.refresh(library)
1316
return library
17+
18+
19+
async def get_library_ids_by_multiple_names(
20+
names: List[str],
21+
session: AsyncSession,
22+
) -> List[int]:
23+
lower_case_names = [name.lower() for name in names]
24+
statement = select(Library.id).where(
25+
func.lower(Library.library_name).in_(lower_case_names)
26+
)
27+
result = await session.exec(statement)
28+
return [id for id in result.all() if id is not None]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import Dict, List, Tuple
2+
3+
from sqlalchemy import tuple_
4+
from sqlmodel import select
5+
from sqlmodel.ext.asyncio.session import AsyncSession
6+
7+
from app.services.database.models.subscriptions import Subscription
8+
9+
10+
async def upsert_multiple_subscription(
11+
subscriptions: List[Subscription],
12+
session: AsyncSession,
13+
) -> List[Subscription]:
14+
if not subscriptions:
15+
return []
16+
17+
incoming_map: Dict[Tuple[str, int], Subscription] = {
18+
(sub.email, sub.library_id): sub for sub in subscriptions
19+
}
20+
21+
keys_to_check = incoming_map.keys()
22+
stmt = select(Subscription).where(
23+
tuple_(Subscription.email, Subscription.library_id).in_(keys_to_check)
24+
)
25+
result = await session.exec(stmt)
26+
existing_subscriptions = result.all()
27+
existing_map: Dict[Tuple[str, int], Subscription] = {
28+
(sub.email, sub.library_id): sub for sub in existing_subscriptions
29+
}
30+
31+
new_subscriptions: List[Subscription] = []
32+
for key, sub_to_upsert in incoming_map.items():
33+
if existing_sub := existing_map.get(key):
34+
existing_sub.tags = sub_to_upsert.tags
35+
else:
36+
new_subscriptions.append(sub_to_upsert)
37+
38+
session.add_all(new_subscriptions)
39+
await session.commit()
40+
41+
all_subs = list(existing_subscriptions) + new_subscriptions
42+
43+
for sub in all_subs:
44+
await session.refresh(sub)
45+
46+
return all_subs

tests/test_libraries.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
import pytest_asyncio
3+
from httpx import AsyncClient
34
from services.database.models import Community, Library
45
from sqlmodel import select
56
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -17,22 +18,52 @@ async def community(session: AsyncSession):
1718
@pytest.mark.asyncio
1819
async def test_insert_libraries(session: AsyncSession, community: Community):
1920
library = Library(
20-
library_name="DevOps",
21-
user_email="[email protected]",
22-
releases_url="http://teste.com",
23-
logo="logo",
24-
community_id=community.id,
25-
)
21+
library_name="Python",
22+
user_email="[email protected]",
23+
releases_url="http://teste.com",
24+
logo="logo",
25+
community_id=community.id,
26+
)
2627
session.add(library)
2728
await session.commit()
2829

29-
statement = select(Library).where(Library.library_name == "DevOps")
30+
statement = select(Library).where(Library.library_name == "Python")
3031
result = await session.exec(statement)
3132
found = result.first()
3233

3334
assert found is not None
34-
assert found.library_name == "DevOps"
35+
assert found.library_name == "Python"
3536
assert found.user_email == "[email protected]"
3637
assert found.releases_url == "http://teste.com"
3738
assert found.logo == "logo"
3839
assert found.community_id == community.id
40+
41+
42+
@pytest.mark.asyncio
43+
async def test_post_libraries_endpoint(
44+
async_client: AsyncClient, session: AsyncSession
45+
):
46+
body = {
47+
"library_name": "Python from API",
48+
"releases_url": "http://teste.com/",
49+
"logo": "http://teste.com/",
50+
}
51+
52+
response = await async_client.post(
53+
"/api/libraries",
54+
json=body,
55+
headers={"Content-Type": "application/json"},
56+
)
57+
58+
assert response.status_code == 200
59+
assert response.json()["status"] == "Library created successfully"
60+
61+
statement = select(Library).where(
62+
Library.library_name == body["library_name"]
63+
)
64+
result = await session.exec(statement)
65+
created_library = result.first()
66+
67+
assert created_library is not None
68+
assert created_library.releases_url == body["releases_url"]
69+
assert created_library.logo == body["logo"]

tests/test_subscriptions.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pytest
22
import pytest_asyncio
3+
from httpx import AsyncClient
4+
from schemas import SubscriptionTagEnum
35
from services.database.models import Community, Subscription
46
from sqlmodel import select
57
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -18,12 +20,15 @@ async def community(session: AsyncSession):
1820
async def test_insert_subscription(session: AsyncSession, community: Community):
1921
subscription = Subscription(
2022
21-
tags="teste,Python",
23+
tags=[SubscriptionTagEnum.BUG_FIX, SubscriptionTagEnum.UPDATE],
2224
community_id=community.id,
2325
)
2426
session.add(subscription)
2527
await session.commit()
2628

29+
statement = select(Subscription).where(
30+
Subscription.email == "[email protected]"
31+
)
2732
statement = select(Subscription).where(
2833
Subscription.email == "[email protected]"
2934
)
@@ -32,5 +37,57 @@ async def test_insert_subscription(session: AsyncSession, community: Community):
3237

3338
assert found is not None
3439
assert found.email == "[email protected]"
35-
assert found.tags == "teste,Python"
40+
assert found.tags == [
41+
SubscriptionTagEnum.BUG_FIX,
42+
SubscriptionTagEnum.UPDATE,
43+
]
3644
assert found.community_id == community.id
45+
46+
47+
@pytest_asyncio.fixture
48+
async def preset_library(async_client: AsyncClient):
49+
body1 = {
50+
"library_name": "Python",
51+
"releases_url": "http://teste.com/",
52+
"logo": "http://teste.com/",
53+
}
54+
55+
response1 = await async_client.post(
56+
"/api/libraries",
57+
json=body1,
58+
headers={"Content-Type": "application/json"},
59+
)
60+
61+
assert response1.status_code == 200
62+
63+
body2 = {
64+
"library_name": "Django",
65+
"releases_url": "http://teste.com/",
66+
"logo": "http://teste.com/",
67+
}
68+
69+
response2 = await async_client.post(
70+
"/api/libraries",
71+
json=body2,
72+
headers={"Content-Type": "application/json"},
73+
)
74+
75+
assert response2.status_code == 200
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_post_subscribe_endpoint(async_client: AsyncClient):
80+
body = {
81+
"email": "[email protected]",
82+
"tags": ["bug_fix", "update"],
83+
"libraries_list": ["Python", "Django"],
84+
}
85+
86+
response = await async_client.post(
87+
"/api/libraries/subscribe",
88+
json=body,
89+
headers={"Content-Type": "application/json"},
90+
)
91+
92+
assert response.status_code == 200
93+
assert response.json()["status"] == "Subscribed in libraries successfully"

0 commit comments

Comments
 (0)