diff --git a/aiws.py b/aiws.py index 5214dba..e08bdda 100644 --- a/aiws.py +++ b/aiws.py @@ -130,7 +130,7 @@ async def start_scheduler(service: GenerationService): break except Exception as e: logger.error(f"Scheduler error: {e}") - await asyncio.sleep(600) # Check every 10 minutes + await asyncio.sleep(60) # Check every 10 minutes # --- LIFESPAN (Запуск FastAPI + Bot) --- @asynccontextmanager diff --git a/api/__pycache__/dependency.cpython-313.pyc b/api/__pycache__/dependency.cpython-313.pyc index 6e62158..21db54a 100644 Binary files a/api/__pycache__/dependency.cpython-313.pyc and b/api/__pycache__/dependency.cpython-313.pyc differ diff --git a/api/endpoints/idea_router.py b/api/endpoints/idea_router.py index f6ceb89..a9b612d 100644 --- a/api/endpoints/idea_router.py +++ b/api/endpoints/idea_router.py @@ -6,34 +6,30 @@ from api.service.idea_service import IdeaService from api.service.generation_service import GenerationService from models.Idea import Idea from api.models.GenerationRequest import GenerationResponse, GenerationsResponse -from api.models.IdeaRequest import IdeaCreateRequest, IdeaUpdateRequest +from api.models.IdeaRequest import IdeaCreateRequest, IdeaUpdateRequest, IdeaResponse router = APIRouter(prefix="/api/ideas", tags=["ideas"]) @router.post("", response_model=Idea) async def create_idea( request: IdeaCreateRequest, - project_id: str = Depends(get_project_id), + project_id: Optional[str] = Depends(get_project_id), current_user: dict = Depends(get_current_user), idea_service: IdeaService = Depends(get_idea_service) ): - if not project_id and not request.project_id: - raise HTTPException(status_code=400, detail="Project ID header is required") - pid = project_id or request.project_id return await idea_service.create_idea(request.name, request.description, pid, str(current_user["_id"])) -@router.get("", response_model=List[Idea]) +@router.get("", response_model=List[IdeaResponse]) async def get_ideas( - project_id: str = Depends(get_project_id), + project_id: Optional[str] = Depends(get_project_id), limit: int = 20, offset: int = 0, + current_user: dict = Depends(get_current_user), idea_service: IdeaService = Depends(get_idea_service) ): - if not project_id: - raise HTTPException(status_code=400, detail="Project ID header is required") - return await idea_service.get_ideas(project_id, limit, offset) + return await idea_service.get_ideas(project_id, str(current_user["_id"]), limit, offset) @router.get("/{idea_id}", response_model=Idea) async def get_idea( diff --git a/api/models/IdeaRequest.py b/api/models/IdeaRequest.py index 773f38c..82138f7 100644 --- a/api/models/IdeaRequest.py +++ b/api/models/IdeaRequest.py @@ -1,5 +1,7 @@ from typing import Optional from pydantic import BaseModel +from models.Idea import Idea +from api.models.GenerationRequest import GenerationResponse class IdeaCreateRequest(BaseModel): name: str @@ -9,3 +11,6 @@ class IdeaCreateRequest(BaseModel): class IdeaUpdateRequest(BaseModel): name: Optional[str] = None description: Optional[str] = None + +class IdeaResponse(Idea): + last_generation: Optional[GenerationResponse] = None diff --git a/api/models/__pycache__/GenerationRequest.cpython-313.pyc b/api/models/__pycache__/GenerationRequest.cpython-313.pyc index c74e802..66faad5 100644 Binary files a/api/models/__pycache__/GenerationRequest.cpython-313.pyc and b/api/models/__pycache__/GenerationRequest.cpython-313.pyc differ diff --git a/api/service/__pycache__/generation_service.cpython-313.pyc b/api/service/__pycache__/generation_service.cpython-313.pyc index 1d49568..431481c 100644 Binary files a/api/service/__pycache__/generation_service.cpython-313.pyc and b/api/service/__pycache__/generation_service.cpython-313.pyc differ diff --git a/api/service/idea_service.py b/api/service/idea_service.py index 89c430f..e154564 100644 --- a/api/service/idea_service.py +++ b/api/service/idea_service.py @@ -7,14 +7,14 @@ class IdeaService: def __init__(self, dao: DAO): self.dao = dao - async def create_idea(self, name: str, description: Optional[str], project_id: str, user_id: str) -> Idea: + async def create_idea(self, name: str, description: Optional[str], project_id: Optional[str], user_id: str) -> Idea: idea = Idea(name=name, description=description, project_id=project_id, created_by=user_id) idea_id = await self.dao.ideas.create_idea(idea) idea.id = idea_id return idea - async def get_ideas(self, project_id: str, limit: int = 20, offset: int = 0) -> List[Idea]: - return await self.dao.ideas.get_ideas(project_id, limit, offset) + async def get_ideas(self, project_id: Optional[str], user_id: str, limit: int = 20, offset: int = 0) -> List[dict]: + return await self.dao.ideas.get_ideas(project_id, user_id, limit, offset) async def get_idea(self, idea_id: str) -> Optional[Idea]: return await self.dao.ideas.get_idea(idea_id) diff --git a/models/Idea.py b/models/Idea.py index 305ecc2..4a2aeab 100644 --- a/models/Idea.py +++ b/models/Idea.py @@ -6,7 +6,7 @@ class Idea(BaseModel): id: Optional[str] = None name: str = "New Idea" description: Optional[str] = None - project_id: str + project_id: Optional[str] = None created_by: str # User ID is_deleted: bool = False created_at: datetime = Field(default_factory=datetime.now) diff --git a/models/__pycache__/Generation.cpython-313.pyc b/models/__pycache__/Generation.cpython-313.pyc index 189b987..0e828bb 100644 Binary files a/models/__pycache__/Generation.cpython-313.pyc and b/models/__pycache__/Generation.cpython-313.pyc differ diff --git a/repos/__pycache__/assets_repo.cpython-313.pyc b/repos/__pycache__/assets_repo.cpython-313.pyc index 60c530c..b1af289 100644 Binary files a/repos/__pycache__/assets_repo.cpython-313.pyc and b/repos/__pycache__/assets_repo.cpython-313.pyc differ diff --git a/repos/__pycache__/dao.cpython-313.pyc b/repos/__pycache__/dao.cpython-313.pyc index 925e4e8..b245d66 100644 Binary files a/repos/__pycache__/dao.cpython-313.pyc and b/repos/__pycache__/dao.cpython-313.pyc differ diff --git a/repos/__pycache__/generation_repo.cpython-313.pyc b/repos/__pycache__/generation_repo.cpython-313.pyc index 235953d..767e94e 100644 Binary files a/repos/__pycache__/generation_repo.cpython-313.pyc and b/repos/__pycache__/generation_repo.cpython-313.pyc differ diff --git a/repos/assets_repo.py b/repos/assets_repo.py index deaeac1..651b8cb 100644 --- a/repos/assets_repo.py +++ b/repos/assets_repo.py @@ -1,6 +1,7 @@ from typing import List, Optional import logging from bson import ObjectId +from uuid import uuid4 from motor.motor_asyncio import AsyncIOMotorClient from models.Asset import Asset @@ -19,7 +20,8 @@ class AssetsRepo: # Main data if asset.data: ts = int(asset.created_at.timestamp()) - object_name = f"{asset.type.value}/{ts}_{asset.name}" + uid = uuid4().hex[:8] + object_name = f"{asset.type.value}/{ts}_{uid}_{asset.name}" uploaded = await self.s3.upload_file(object_name, asset.data) if uploaded: @@ -32,7 +34,8 @@ class AssetsRepo: # Thumbnail if asset.thumbnail: ts = int(asset.created_at.timestamp()) - thumb_name = f"{asset.type.value}/thumbs/{ts}_{asset.name}_thumb.jpg" + uid = uuid4().hex[:8] + thumb_name = f"{asset.type.value}/thumbs/{ts}_{uid}_{asset.name}_thumb.jpg" uploaded_thumb = await self.s3.upload_file(thumb_name, asset.thumbnail) if uploaded_thumb: @@ -134,7 +137,8 @@ class AssetsRepo: if self.s3: if asset.data: ts = int(asset.created_at.timestamp()) - object_name = f"{asset.type.value}/{ts}_{asset.name}" + uid = uuid4().hex[:8] + object_name = f"{asset.type.value}/{ts}_{uid}_{asset.name}" if await self.s3.upload_file(object_name, asset.data): asset.minio_object_name = object_name asset.minio_bucket = self.s3.bucket_name @@ -142,7 +146,8 @@ class AssetsRepo: if asset.thumbnail: ts = int(asset.created_at.timestamp()) - thumb_name = f"{asset.type.value}/thumbs/{ts}_{asset.name}_thumb.jpg" + uid = uuid4().hex[:8] + thumb_name = f"{asset.type.value}/thumbs/{ts}_{uid}_{asset.name}_thumb.jpg" if await self.s3.upload_file(thumb_name, asset.thumbnail): asset.minio_thumbnail_object_name = thumb_name asset.thumbnail = None @@ -216,7 +221,8 @@ class AssetsRepo: created_at = doc.get("created_at") ts = int(created_at.timestamp()) if created_at else 0 - object_name = f"{type_}/{ts}_{asset_id}_{name}" + uid = uuid4().hex[:8] + object_name = f"{type_}/{ts}_{uid}_{asset_id}_{name}" if await self.s3.upload_file(object_name, data): await self.collection.update_one( {"_id": asset_id}, @@ -243,7 +249,8 @@ class AssetsRepo: created_at = doc.get("created_at") ts = int(created_at.timestamp()) if created_at else 0 - thumb_name = f"{type_}/thumbs/{ts}_{asset_id}_{name}_thumb.jpg" + uid = uuid4().hex[:8] + thumb_name = f"{type_}/thumbs/{ts}_{uid}_{asset_id}_{name}_thumb.jpg" if await self.s3.upload_file(thumb_name, thumb): await self.collection.update_one( {"_id": asset_id}, diff --git a/repos/generation_repo.py b/repos/generation_repo.py index ab1ab3d..f77ceee 100644 --- a/repos/generation_repo.py +++ b/repos/generation_repo.py @@ -98,7 +98,7 @@ class GenerationRepo: generations.append(Generation(**generation)) return generations - async def cancel_stale_generations(self, timeout_minutes: int = 60) -> int: + async def cancel_stale_generations(self, timeout_minutes: int = 5) -> int: cutoff_time = datetime.now(UTC) - timedelta(minutes=timeout_minutes) res = await self.collection.update_many( { diff --git a/repos/idea_repo.py b/repos/idea_repo.py index bb65e0e..4271018 100644 --- a/repos/idea_repo.py +++ b/repos/idea_repo.py @@ -20,14 +20,43 @@ class IdeaRepo: return Idea(**res) return None - async def get_ideas(self, project_id: str, limit: int = 20, offset: int = 0) -> List[Idea]: - filter = {"project_id": project_id, "is_deleted": False} - res = await self.collection.find(filter).sort("updated_at", -1).skip(offset).limit(limit).to_list(None) - ideas = [] - for doc in res: - doc["id"] = str(doc.pop("_id")) - ideas.append(Idea(**doc)) - return ideas + async def get_ideas(self, project_id: Optional[str], user_id: str, limit: int = 20, offset: int = 0) -> List[dict]: + if project_id: + match_stage = {"project_id": project_id, "is_deleted": False} + else: + match_stage = {"created_by": user_id, "project_id": None, "is_deleted": False} + + pipeline = [ + {"$match": match_stage}, + {"$sort": {"updated_at": -1}}, + {"$skip": offset}, + {"$limit": limit}, + # Add string id field for lookup + {"$addFields": {"str_id": {"$toString": "$_id"}}}, + # Lookup generations + { + "$lookup": { + "from": "generations", + "let": {"idea_id": "$str_id"}, + "pipeline": [ + {"$match": {"$expr": {"$eq": ["$idea_id", "$$idea_id"]}}}, + {"$sort": {"created_at": -1}}, # Ensure we get the latest + {"$limit": 1} + ], + "as": "generations" + } + }, + # Unwind generations array (preserve ideas without generations) + {"$unwind": {"path": "$generations", "preserveNullAndEmptyArrays": True}}, + # Rename for clarity + {"$addFields": { + "last_generation": "$generations", + "id": "$str_id" + }}, + {"$project": {"generations": 0, "str_id": 0, "_id": 0}} + ] + + return await self.collection.aggregate(pipeline).to_list(None) async def delete_idea(self, idea_id: str) -> bool: if not ObjectId.is_valid(idea_id):