""" TickTick API router for ClaudeTools. This module defines REST API endpoints for managing TickTick projects and tasks, proxying requests through the TickTickService with automatic token management. """ import logging from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from api.middleware.auth import get_current_user from api.services.ticktick_service import TickTickResult, get_ticktick_service logger = logging.getLogger(__name__) router = APIRouter() # -------------------------------------------------------------------------- # Pydantic request/response schemas # -------------------------------------------------------------------------- class ProjectCreate(BaseModel): """Schema for creating a new TickTick project.""" name: str = Field(..., min_length=1, max_length=200, description="Project name") color: Optional[str] = Field( None, description="Hex color string (e.g., '#FF6347')" ) view_mode: Optional[str] = Field( None, description="View mode: 'list', 'kanban', or 'timeline'" ) kind: Optional[str] = Field( None, description="Project kind: 'TASK' or 'NOTE'" ) class ProjectUpdate(BaseModel): """Schema for updating an existing TickTick project.""" name: Optional[str] = Field( None, min_length=1, max_length=200, description="New project name" ) color: Optional[str] = Field(None, description="New hex color string") view_mode: Optional[str] = Field(None, description="New view mode") class TaskCreate(BaseModel): """Schema for creating a new task in a TickTick project.""" title: str = Field(..., min_length=1, max_length=500, description="Task title") content: Optional[str] = Field(None, description="Task description/content") priority: Optional[int] = Field( None, ge=0, le=5, description="Priority: 0=none, 1=low, 3=medium, 5=high", ) due_date: Optional[str] = Field( None, description="Due date in ISO 8601 format" ) tags: Optional[list[str]] = Field(None, description="List of tag strings") class TaskUpdate(BaseModel): """Schema for updating an existing task.""" title: Optional[str] = Field( None, min_length=1, max_length=500, description="New task title" ) content: Optional[str] = Field(None, description="New task content") priority: Optional[int] = Field( None, ge=0, le=5, description="New priority level" ) due_date: Optional[str] = Field(None, description="New due date in ISO 8601 format") tags: Optional[list[str]] = Field(None, description="New list of tags") class TickTickResponse(BaseModel): """Standard response wrapper for all TickTick endpoints.""" success: bool data: Optional[dict] = None error: Optional[str] = None # -------------------------------------------------------------------------- # Helpers # -------------------------------------------------------------------------- def _to_response(result: TickTickResult, status_code_on_error: int = 500) -> dict: """ Convert a TickTickResult to a JSON-serializable response dict. Raises an HTTPException when the result indicates failure. Args: result: The service result to convert. status_code_on_error: HTTP status code for error responses. Returns: Dict matching the TickTickResponse schema. """ if not result.success: raise HTTPException( status_code=status_code_on_error, detail={ "success": False, "data": None, "error": result.error or "Unknown error", }, ) return {"success": True, "data": result.data, "error": None} # -------------------------------------------------------------------------- # Project endpoints # -------------------------------------------------------------------------- @router.get( "", response_model=TickTickResponse, summary="List all TickTick projects", status_code=status.HTTP_200_OK, ) async def list_projects(current_user: dict = Depends(get_current_user)): """ Retrieve all projects (lists) from the authenticated TickTick account. **Example Request:** ``` GET /api/ticktick ``` """ service = get_ticktick_service() result = await service.list_projects() return _to_response(result) @router.get( "/{project_id}", response_model=TickTickResponse, summary="Get a TickTick project with tasks", status_code=status.HTTP_200_OK, ) async def get_project(project_id: str, current_user: dict = Depends(get_current_user)): """ Retrieve a single project and its associated task data. **Path Parameters:** - **project_id**: The TickTick project ID. """ service = get_ticktick_service() result = await service.get_project(project_id) return _to_response(result, status_code_on_error=404) @router.post( "", response_model=TickTickResponse, summary="Create a new TickTick project", status_code=status.HTTP_201_CREATED, ) async def create_project(body: ProjectCreate, current_user: dict = Depends(get_current_user)): """ Create a new project (list) in TickTick. **Request Body:** - **name** (required): Project name. - **color**: Hex color string. - **view_mode**: View mode ('list', 'kanban', 'timeline'). - **kind**: Project kind ('TASK' or 'NOTE'). """ service = get_ticktick_service() result = await service.create_project( name=body.name, color=body.color, view_mode=body.view_mode, kind=body.kind, ) return _to_response(result, status_code_on_error=400) @router.put( "/{project_id}", response_model=TickTickResponse, summary="Update a TickTick project", status_code=status.HTTP_200_OK, ) async def update_project(project_id: str, body: ProjectUpdate, current_user: dict = Depends(get_current_user)): """ Update an existing project's properties. **Path Parameters:** - **project_id**: The TickTick project ID to update. **Request Body:** At least one field must be provided. """ service = get_ticktick_service() result = await service.update_project( project_id=project_id, name=body.name, color=body.color, view_mode=body.view_mode, ) return _to_response(result, status_code_on_error=400) @router.delete( "/{project_id}", response_model=TickTickResponse, summary="Delete a TickTick project", status_code=status.HTTP_200_OK, ) async def delete_project(project_id: str, current_user: dict = Depends(get_current_user)): """ Delete a project from TickTick. **Path Parameters:** - **project_id**: The TickTick project ID to delete. """ service = get_ticktick_service() result = await service.delete_project(project_id) return _to_response(result, status_code_on_error=404) # -------------------------------------------------------------------------- # Task endpoints # -------------------------------------------------------------------------- @router.post( "/{project_id}/tasks", response_model=TickTickResponse, summary="Create a task in a TickTick project", status_code=status.HTTP_201_CREATED, ) async def create_task(project_id: str, body: TaskCreate, current_user: dict = Depends(get_current_user)): """ Create a new task within the specified project. **Path Parameters:** - **project_id**: The TickTick project ID. **Request Body:** - **title** (required): Task title. - **content**: Task description. - **priority**: 0=none, 1=low, 3=medium, 5=high. - **due_date**: ISO 8601 date string. - **tags**: List of tag strings. """ service = get_ticktick_service() result = await service.create_task( title=body.title, project_id=project_id, content=body.content, priority=body.priority, due_date=body.due_date, tags=body.tags, ) return _to_response(result, status_code_on_error=400) @router.put( "/{project_id}/tasks/{task_id}", response_model=TickTickResponse, summary="Update a task in a TickTick project", status_code=status.HTTP_200_OK, ) async def update_task(project_id: str, task_id: str, body: TaskUpdate, current_user: dict = Depends(get_current_user)): """ Update an existing task's properties. **Path Parameters:** - **project_id**: The TickTick project ID. - **task_id**: The task ID to update. """ service = get_ticktick_service() result = await service.update_task( task_id=task_id, project_id=project_id, title=body.title, content=body.content, priority=body.priority, due_date=body.due_date, tags=body.tags, ) return _to_response(result, status_code_on_error=400) @router.post( "/{project_id}/tasks/{task_id}/complete", response_model=TickTickResponse, summary="Complete a task", status_code=status.HTTP_200_OK, ) async def complete_task(project_id: str, task_id: str, current_user: dict = Depends(get_current_user)): """ Mark a task as complete in TickTick. **Path Parameters:** - **project_id**: The TickTick project ID. - **task_id**: The task ID to mark complete. """ service = get_ticktick_service() result = await service.complete_task(task_id=task_id, project_id=project_id) return _to_response(result, status_code_on_error=400) @router.delete( "/{project_id}/tasks/{task_id}", response_model=TickTickResponse, summary="Delete a task", status_code=status.HTTP_200_OK, ) async def delete_task(project_id: str, task_id: str, current_user: dict = Depends(get_current_user)): """ Delete a task from a TickTick project. **Path Parameters:** - **project_id**: The TickTick project ID. - **task_id**: The task ID to delete. """ service = get_ticktick_service() result = await service.delete_task(task_id=task_id, project_id=project_id) return _to_response(result, status_code_on_error=404)