"""API endpoints for Forgejo issue operations.""" from __future__ import annotations from typing import TYPE_CHECKING from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlmodel import select, func from app.api.deps import require_org_admin from app.core.auth import get_auth_context from app.db import crud from app.db.session import get_session from app.models.forgejo_issues import ForgejoIssue from app.schemas.forgejo_issues import ForgejoIssueListResponse, ForgejoIssueRead from app.services.organizations import OrganizationContext if TYPE_CHECKING: from sqlmodel.ext.asyncio.session import AsyncSession router = APIRouter(prefix="/forgejo/issues", tags=["forgejo-issues"]) SESSION_DEP = Depends(get_session) AUTH_DEP = Depends(get_auth_context) ORG_ADMIN_DEP = Depends(require_org_admin) @router.get("", response_model=ForgejoIssueListResponse) async def list_issues( session: AsyncSession = SESSION_DEP, ctx: OrganizationContext = ORG_ADMIN_DEP, repository_id: str | None = Query(None, description="Filter by repository ID"), state: str | None = Query(None, description="Filter by state (open, closed)"), label: str | None = Query(None, description="Filter by label name"), assignee: str | None = Query(None, description="Filter by assignee login"), search: str | None = Query(None, description="Search in title and body"), page: int = Query(1, ge=1, description="Page number"), limit: int = Query(30, ge=1, le=100, description="Items per page"), ) -> ForgejoIssueListResponse: """List cached issues with optional filters.""" # Build query with filters statement = select(ForgejoIssue).where(ForgejoIssue.organization_id == ctx.organization.id) if repository_id: try: repo_uuid = UUID(repository_id) except ValueError: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid repository_id format") statement = statement.where(ForgejoIssue.repository_id == repo_uuid) if state: statement = statement.where(ForgejoIssue.state == state) if search: statement = statement.where( (ForgejoIssue.title.ilike(f"%{search}%")) | (ForgejoIssue.body_preview.ilike(f"%{search}%")) ) # Count total total_statement = select(func.count()).select_from(ForgejoIssue).where(ForgejoIssue.organization_id == ctx.organization.id) if repository_id: try: repo_uuid = UUID(repository_id) total_statement = total_statement.where(ForgejoIssue.repository_id == repo_uuid) except ValueError: pass if state: total_statement = total_statement.where(ForgejoIssue.state == state) if search: total_statement = total_statement.where( (ForgejoIssue.title.ilike(f"%{search}%")) | (ForgejoIssue.body_preview.ilike(f"%{search}%")) ) total_result = await session.exec(total_statement) total = total_result.one() # Pagination offset = (page - 1) * limit statement = statement.offset(offset).limit(limit).order_by(ForgejoIssue.forgejo_issue_number.desc()) issues = (await session.exec(statement)).all() items = [ForgejoIssueRead.model_validate(issue) for issue in issues] return ForgejoIssueListResponse( items=items, total=total, page=page, limit=limit, ) @router.get("/{issue_id}", response_model=ForgejoIssueRead) async def get_issue( issue_id: str, session: AsyncSession = SESSION_DEP, ctx: OrganizationContext = ORG_ADMIN_DEP, ) -> ForgejoIssueRead: """Get one cached issue by ID.""" try: uuid = UUID(issue_id) except ValueError: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid issue_id format") issue = await crud.get_by_id(session, ForgejoIssue, uuid) if issue is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) if issue.organization_id != ctx.organization.id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return ForgejoIssueRead.model_validate(issue)