Merge branch 'feature/projects-api-pagination-search' into 'main'

Add pagination and search to projects API

See merge request esv/bsf/bsf-integration/orchard/orchard-mvp!1
This commit is contained in:
Mondo Diaz
2025-12-11 15:03:42 -06:00
3 changed files with 64 additions and 8 deletions

8
.gitignore vendored
View File

@@ -1,3 +1,11 @@
# Python
__pycache__/
*.py[cod]
*.pyo
.Python
*.egg-info/
.eggs/
# Binaries # Binaries
/bin/ /bin/
*.exe *.exe

View File

@@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import or_ from sqlalchemy import or_, func
from typing import List, Optional from typing import List, Optional
import math
import re import re
from .database import get_db from .database import get_db
@@ -16,6 +17,7 @@ from .schemas import (
UploadResponse, UploadResponse,
ConsumerResponse, ConsumerResponse,
HealthResponse, HealthResponse,
PaginatedResponse, PaginationMeta,
) )
router = APIRouter() router = APIRouter()
@@ -39,13 +41,44 @@ def health_check():
# Project routes # Project routes
@router.get("/api/v1/projects", response_model=List[ProjectResponse]) @router.get("/api/v1/projects", response_model=PaginatedResponse[ProjectResponse])
def list_projects(request: Request, db: Session = Depends(get_db)): def list_projects(
request: Request,
page: int = Query(default=1, ge=1, description="Page number"),
limit: int = Query(default=20, ge=1, le=100, description="Items per page"),
search: Optional[str] = Query(default=None, description="Search by project name"),
db: Session = Depends(get_db),
):
user_id = get_user_id(request) user_id = get_user_id(request)
projects = db.query(Project).filter(
# Base query - filter by access
query = db.query(Project).filter(
or_(Project.is_public == True, Project.created_by == user_id) or_(Project.is_public == True, Project.created_by == user_id)
).order_by(Project.name).all() )
return projects
# Apply search filter (case-insensitive)
if search:
query = query.filter(func.lower(Project.name).contains(search.lower()))
# Get total count before pagination
total = query.count()
# Apply pagination
offset = (page - 1) * limit
projects = query.order_by(Project.name).offset(offset).limit(limit).all()
# Calculate total pages
total_pages = math.ceil(total / limit) if total > 0 else 1
return PaginatedResponse(
items=projects,
pagination=PaginationMeta(
page=page,
limit=limit,
total=total,
total_pages=total_pages,
),
)
@router.post("/api/v1/projects", response_model=ProjectResponse) @router.post("/api/v1/projects", response_model=ProjectResponse)

View File

@@ -1,8 +1,23 @@
from datetime import datetime from datetime import datetime
from typing import Optional, List from typing import Optional, List, Generic, TypeVar
from pydantic import BaseModel from pydantic import BaseModel
from uuid import UUID from uuid import UUID
T = TypeVar("T")
# Pagination schemas
class PaginationMeta(BaseModel):
page: int
limit: int
total: int
total_pages: int
class PaginatedResponse(BaseModel, Generic[T]):
items: List[T]
pagination: PaginationMeta
# Project schemas # Project schemas
class ProjectCreate(BaseModel): class ProjectCreate(BaseModel):