From d820d9145bc8b6a9e37017057cd5faf3c3611d86 Mon Sep 17 00:00:00 2001 From: xds Date: Tue, 17 Feb 2026 15:54:01 +0300 Subject: [PATCH] feat: introduce post resource with full CRUD operations and generation linking. --- aiws.py | 2 + api/__pycache__/dependency.cpython-313.pyc | Bin 3002 -> 3246 bytes api/dependency.py | 7 +- api/endpoints/post_router.py | 99 +++++++++++++++++++++ api/models/PostRequest.py | 19 ++++ api/service/post_service.py | 79 ++++++++++++++++ models/Post.py | 23 +++++ repos/__pycache__/dao.cpython-313.pyc | Bin 1576 -> 1686 bytes repos/dao.py | 2 + repos/post_repo.py | 97 ++++++++++++++++++++ 10 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 api/endpoints/post_router.py create mode 100644 api/models/PostRequest.py create mode 100644 api/service/post_service.py create mode 100644 models/Post.py create mode 100644 repos/post_repo.py diff --git a/aiws.py b/aiws.py index f755f37..2cc7414 100644 --- a/aiws.py +++ b/aiws.py @@ -44,6 +44,7 @@ from api.endpoints.admin import router as api_admin_router from api.endpoints.album_router import router as api_album_router from api.endpoints.project_router import router as project_api_router from api.endpoints.idea_router import router as idea_api_router +from api.endpoints.post_router import router as post_api_router load_dotenv() logger = logging.getLogger(__name__) @@ -219,6 +220,7 @@ app.include_router(api_gen_router) app.include_router(api_album_router) app.include_router(project_api_router) app.include_router(idea_api_router) +app.include_router(post_api_router) # Prometheus Metrics (Instrument after all routers are added) Instrumentator( diff --git a/api/__pycache__/dependency.cpython-313.pyc b/api/__pycache__/dependency.cpython-313.pyc index 94bb23c5dc69317f62275bbbb0926d8b4d706f7b..64e776c3ee3542ede8fd8173002874af9ed4eb95 100644 GIT binary patch delta 312 zcmdlbzD|<&GcPX}0}uq*Ov!vUkynyQjA^6BGe(nOhF}G61uw-SMFoZ!CMAX#aZ^T+ zItB(4#$e$Xu3+vMMH7}_MJa|XmMq31rF1DxrOiT2b66QaPhQVn!Dux31B=LH8xC$p z)5#tj9gOCaFLJ1ItYo~!9gtsKGTDyZ-r53aSdlJ}C|4ymB}GN1ASrVYS0b?>QxB?D55*iy zkQ$I-MJ6BuWDvwk4x8Nkl+v73yCM&u7|1Qf-J9LHG8x&wGB7gAer22N%PqjD5m8%j|C delta 146 zcmZ1{xl5e)GcPX}0}x!Tnw%Lrkyn!G8RJHcXN)ZAQkn{zotWmZG8Rw1&tAc3I9Y*J zWO5D%H>1hq3XTp&v&oE{s!TvND>x+>&GmsM++r>+DJn7nvWm<=gfWPa0ud5G;ueQZ hZhlH>PO4pz6OhXY#Kqa0rMWX1xxO(lGRlHg0s!4=A~65} diff --git a/api/dependency.py b/api/dependency.py index 42fba46..1e8e8e9 100644 --- a/api/dependency.py +++ b/api/dependency.py @@ -57,4 +57,9 @@ async def get_project_id(x_project_id: Optional[str] = Header(None, alias="X-Pro return x_project_id async def get_album_service(dao: DAO = Depends(get_dao)) -> AlbumService: - return AlbumService(dao) \ No newline at end of file + return AlbumService(dao) + +from api.service.post_service import PostService + +def get_post_service(dao: DAO = Depends(get_dao)) -> PostService: + return PostService(dao) \ No newline at end of file diff --git a/api/endpoints/post_router.py b/api/endpoints/post_router.py new file mode 100644 index 0000000..a61dd51 --- /dev/null +++ b/api/endpoints/post_router.py @@ -0,0 +1,99 @@ +from typing import List, Optional +from datetime import datetime +from fastapi import APIRouter, Depends, HTTPException + +from api.dependency import get_post_service, get_project_id +from api.endpoints.auth import get_current_user +from api.service.post_service import PostService +from api.models.PostRequest import PostCreateRequest, PostUpdateRequest, AddGenerationsRequest +from models.Post import Post + +router = APIRouter(prefix="/api/posts", tags=["posts"]) + + +@router.post("", response_model=Post) +async def create_post( + request: PostCreateRequest, + project_id: Optional[str] = Depends(get_project_id), + current_user: dict = Depends(get_current_user), + post_service: PostService = Depends(get_post_service), +): + pid = project_id or request.project_id + return await post_service.create_post( + date=request.date, + topic=request.topic, + generation_ids=request.generation_ids, + project_id=pid, + user_id=str(current_user["_id"]), + ) + + +@router.get("", response_model=List[Post]) +async def get_posts( + project_id: Optional[str] = Depends(get_project_id), + limit: int = 200, + offset: int = 0, + date_from: Optional[datetime] = None, + date_to: Optional[datetime] = None, + current_user: dict = Depends(get_current_user), + post_service: PostService = Depends(get_post_service), +): + return await post_service.get_posts(project_id, str(current_user["_id"]), limit, offset, date_from, date_to) + + +@router.get("/{post_id}", response_model=Post) +async def get_post( + post_id: str, + post_service: PostService = Depends(get_post_service), +): + post = await post_service.get_post(post_id) + if not post: + raise HTTPException(status_code=404, detail="Post not found") + return post + + +@router.put("/{post_id}", response_model=Post) +async def update_post( + post_id: str, + request: PostUpdateRequest, + post_service: PostService = Depends(get_post_service), +): + post = await post_service.update_post(post_id, date=request.date, topic=request.topic) + if not post: + raise HTTPException(status_code=404, detail="Post not found") + return post + + +@router.delete("/{post_id}") +async def delete_post( + post_id: str, + post_service: PostService = Depends(get_post_service), +): + success = await post_service.delete_post(post_id) + if not success: + raise HTTPException(status_code=404, detail="Post not found or could not be deleted") + return {"status": "success"} + + +@router.post("/{post_id}/generations") +async def add_generations( + post_id: str, + request: AddGenerationsRequest, + post_service: PostService = Depends(get_post_service), +): + success = await post_service.add_generations(post_id, request.generation_ids) + if not success: + raise HTTPException(status_code=404, detail="Post not found") + return {"status": "success"} + + +@router.delete("/{post_id}/generations/{generation_id}") +async def remove_generation( + post_id: str, + generation_id: str, + post_service: PostService = Depends(get_post_service), +): + success = await post_service.remove_generation(post_id, generation_id) + if not success: + raise HTTPException(status_code=404, detail="Post not found or generation not linked") + return {"status": "success"} diff --git a/api/models/PostRequest.py b/api/models/PostRequest.py new file mode 100644 index 0000000..18be225 --- /dev/null +++ b/api/models/PostRequest.py @@ -0,0 +1,19 @@ +from datetime import datetime +from typing import Optional, List +from pydantic import BaseModel + + +class PostCreateRequest(BaseModel): + date: datetime + topic: str + generation_ids: List[str] = [] + project_id: Optional[str] = None + + +class PostUpdateRequest(BaseModel): + date: Optional[datetime] = None + topic: Optional[str] = None + + +class AddGenerationsRequest(BaseModel): + generation_ids: List[str] diff --git a/api/service/post_service.py b/api/service/post_service.py new file mode 100644 index 0000000..8c09a43 --- /dev/null +++ b/api/service/post_service.py @@ -0,0 +1,79 @@ +from typing import List, Optional +from datetime import datetime, UTC + +from repos.dao import DAO +from models.Post import Post + + +class PostService: + def __init__(self, dao: DAO): + self.dao = dao + + async def create_post( + self, + date: datetime, + topic: str, + generation_ids: List[str], + project_id: Optional[str], + user_id: str, + ) -> Post: + post = Post( + date=date, + topic=topic, + generation_ids=generation_ids, + project_id=project_id, + created_by=user_id, + ) + post_id = await self.dao.posts.create_post(post) + post.id = post_id + return post + + async def get_post(self, post_id: str) -> Optional[Post]: + return await self.dao.posts.get_post(post_id) + + async def get_posts( + self, + project_id: Optional[str], + user_id: str, + limit: int = 20, + offset: int = 0, + date_from: Optional[datetime] = None, + date_to: Optional[datetime] = None, + ) -> List[Post]: + return await self.dao.posts.get_posts(project_id, user_id, limit, offset, date_from, date_to) + + async def update_post( + self, + post_id: str, + date: Optional[datetime] = None, + topic: Optional[str] = None, + ) -> Optional[Post]: + post = await self.dao.posts.get_post(post_id) + if not post: + return None + + updates: dict = {"updated_at": datetime.now(UTC)} + if date is not None: + updates["date"] = date + if topic is not None: + updates["topic"] = topic + + await self.dao.posts.update_post(post_id, updates) + + # Return refreshed post + return await self.dao.posts.get_post(post_id) + + async def delete_post(self, post_id: str) -> bool: + return await self.dao.posts.delete_post(post_id) + + async def add_generations(self, post_id: str, generation_ids: List[str]) -> bool: + post = await self.dao.posts.get_post(post_id) + if not post: + return False + return await self.dao.posts.add_generations(post_id, generation_ids) + + async def remove_generation(self, post_id: str, generation_id: str) -> bool: + post = await self.dao.posts.get_post(post_id) + if not post: + return False + return await self.dao.posts.remove_generation(post_id, generation_id) diff --git a/models/Post.py b/models/Post.py new file mode 100644 index 0000000..4ab5837 --- /dev/null +++ b/models/Post.py @@ -0,0 +1,23 @@ +from datetime import datetime, timezone, UTC +from typing import Optional, List +from pydantic import BaseModel, Field, model_validator + + +class Post(BaseModel): + id: Optional[str] = None + date: datetime + topic: str + generation_ids: List[str] = Field(default_factory=list) + project_id: Optional[str] = None + created_by: str + is_deleted: bool = False + created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + + @model_validator(mode="after") + def ensure_tz_aware(self): + for field in ("date", "created_at", "updated_at"): + val = getattr(self, field) + if val is not None and val.tzinfo is None: + setattr(self, field, val.replace(tzinfo=timezone.utc)) + return self diff --git a/repos/__pycache__/dao.cpython-313.pyc b/repos/__pycache__/dao.cpython-313.pyc index 007507d4457fd67ace6b4f5866d484864518e2b6..73a23a16f9f7ca428c4898a8bcd618716892f68f 100644 GIT binary patch delta 429 zcmZ3%GmV$`GcPX}0}%MwOv!vOkyny&&P4TEm0*ToZf_AU(IQa=1_=fwhF~5khG1S( zCXh-7h9a?aK283Kd)in6@{3C*TQTM{3QS(bsK>;{IQbUiGp1f57!-0 z*%@hOw;5j5(lXj1rZ`3LJUMmfe2|3Aps&JC#$e{bMt_hydXk;autiL zry__c2O>ZY0}}#3MzI2rXk+-u#KEe6LEi8K1CZWPaf0)reb9Fh8!S)+)(q0;rzt-9 z0m~tI{-RW%AM`-}hzBtt{$eYsEXd4DFXEZJnpIL4Y&qC64WLel86XGT;;_lhPbtkw pwJS0Kav6cR*l6-cRt2fM3@TsQ7#L;lGH8Bh;bheR!~i6~8Uf;6UXlO+ delta 362 zcmbQnyMl-JGcPX}0}!OkPR{I^$ScV>VWN7igam^ULol}#Lokmi6G)bUp-3d1SCenz ziMGj6jOmR0lMgZKF+F3L{E6`ydpd(Aqn{?<0y zm@*)O4@U3<8N~`fqK)Ar6FaN^1$n~{3_yBE#R<-f_Ceo4Y_LEPSTjhUpQh+!Q`STN zY$cTinR)3&AX{z;C#ED8l%y6F>lGWvL)j3k!B(jO)j&)DS$~VeCO1E&G$+-r$Pmb7 g1ma@-$uVpS;$PVq7-jA str: + res = await self.collection.insert_one(post.model_dump()) + return str(res.inserted_id) + + async def get_post(self, post_id: str) -> Optional[Post]: + if not ObjectId.is_valid(post_id): + return None + res = await self.collection.find_one({"_id": ObjectId(post_id), "is_deleted": False}) + if res: + res["id"] = str(res.pop("_id")) + return Post(**res) + return None + + async def get_posts( + self, + project_id: Optional[str], + user_id: str, + limit: int = 20, + offset: int = 0, + date_from: Optional[datetime] = None, + date_to: Optional[datetime] = None, + ) -> List[Post]: + if project_id: + match = {"project_id": project_id, "is_deleted": False} + else: + match = {"created_by": user_id, "project_id": None, "is_deleted": False} + + if date_from or date_to: + date_filter = {} + if date_from: + date_filter["$gte"] = date_from + if date_to: + date_filter["$lte"] = date_to + match["date"] = date_filter + + cursor = ( + self.collection.find(match) + .sort("date", -1) + .skip(offset) + .limit(limit) + ) + posts = [] + async for doc in cursor: + doc["id"] = str(doc.pop("_id")) + posts.append(Post(**doc)) + return posts + + async def update_post(self, post_id: str, data: dict) -> bool: + if not ObjectId.is_valid(post_id): + return False + res = await self.collection.update_one( + {"_id": ObjectId(post_id)}, + {"$set": data}, + ) + return res.modified_count > 0 + + async def delete_post(self, post_id: str) -> bool: + if not ObjectId.is_valid(post_id): + return False + res = await self.collection.update_one( + {"_id": ObjectId(post_id)}, + {"$set": {"is_deleted": True}}, + ) + return res.modified_count > 0 + + async def add_generations(self, post_id: str, generation_ids: List[str]) -> bool: + if not ObjectId.is_valid(post_id): + return False + res = await self.collection.update_one( + {"_id": ObjectId(post_id)}, + {"$addToSet": {"generation_ids": {"$each": generation_ids}}}, + ) + return res.modified_count > 0 + + async def remove_generation(self, post_id: str, generation_id: str) -> bool: + if not ObjectId.is_valid(post_id): + return False + res = await self.collection.update_one( + {"_id": ObjectId(post_id)}, + {"$pull": {"generation_ids": generation_id}}, + ) + return res.modified_count > 0 -- 2.49.1