Remove conversation context/recall system from ClaudeTools

Completely removed the database context recall system while preserving
database tables for safety. This major cleanup removes 80+ files and
16,831 lines of code.

What was removed:
- API layer: 4 routers (conversation-contexts, context-snippets,
  project-states, decision-logs) with 35+ endpoints
- Database models: 5 models (ConversationContext, ContextSnippet,
  DecisionLog, ProjectState, ContextTag)
- Services: 4 service layers with business logic
- Schemas: 4 Pydantic schema files
- Claude Code hooks: 13 hook files (user-prompt-submit, task-complete,
  sync-contexts, periodic saves)
- Scripts: 15+ scripts (import, migration, testing, tombstone checking)
- Tests: 5 test files (context recall, compression, diagnostics)
- Documentation: 30+ markdown files (guides, architecture, quick starts)
- Utilities: context compression, conversation parsing

Files modified:
- api/main.py: Removed router registrations
- api/models/__init__.py: Removed model imports
- api/schemas/__init__.py: Removed schema imports
- api/services/__init__.py: Removed service imports
- .claude/claude.md: Completely rewritten without context references

Database tables preserved:
- conversation_contexts, context_snippets, context_tags,
  project_states, decision_logs (5 orphaned tables remain for safety)
- Migration created but NOT applied: 20260118_172743_remove_context_system.py
- Tables can be dropped later when confirmed not needed

New files added:
- CONTEXT_SYSTEM_REMOVAL_SUMMARY.md: Detailed removal report
- CONTEXT_SYSTEM_REMOVAL_COMPLETE.md: Final status
- CONTEXT_EXPORT_RESULTS.md: Export attempt results
- scripts/export-tombstoned-contexts.py: Export tool for future use
- migrations/versions/20260118_172743_remove_context_system.py

Impact:
- Reduced from 130 to 95 API endpoints
- Reduced from 43 to 38 active database tables
- Removed 16,831 lines of code
- System fully operational without context recall

Reason for removal:
- System was not actively used (no tombstoned contexts found)
- Reduces codebase complexity
- Focuses on core MSP work tracking functionality
- Database preserved for safety (can rollback if needed)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-18 19:10:41 -07:00
parent 8bbc7737a0
commit 89e5118306
89 changed files with 7905 additions and 16831 deletions

View File

@@ -1,312 +0,0 @@
"""
ContextSnippet API router for ClaudeTools.
Defines all REST API endpoints for managing context snippets,
reusable pieces of knowledge for quick retrieval.
"""
from typing import List
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from api.database import get_db
from api.middleware.auth import get_current_user
from api.schemas.context_snippet import (
ContextSnippetCreate,
ContextSnippetResponse,
ContextSnippetUpdate,
)
from api.services import context_snippet_service
# Create router with prefix and tags
router = APIRouter()
@router.get(
"",
response_model=dict,
summary="List all context snippets",
description="Retrieve a paginated list of all context snippets with optional filtering",
status_code=status.HTTP_200_OK,
)
def list_context_snippets(
skip: int = Query(
default=0,
ge=0,
description="Number of records to skip for pagination"
),
limit: int = Query(
default=100,
ge=1,
le=1000,
description="Maximum number of records to return (max 1000)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
List all context snippets with pagination.
Returns snippets ordered by relevance score and usage count.
"""
try:
snippets, total = context_snippet_service.get_context_snippets(db, skip, limit)
return {
"total": total,
"skip": skip,
"limit": limit,
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve context snippets: {str(e)}"
)
@router.get(
"/by-tags",
response_model=dict,
summary="Get context snippets by tags",
description="Retrieve context snippets filtered by tags",
status_code=status.HTTP_200_OK,
)
def get_context_snippets_by_tags(
tags: List[str] = Query(..., description="Tags to filter by (OR logic - any match)"),
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get context snippets filtered by tags.
Uses OR logic - snippets matching any of the provided tags will be returned.
"""
try:
snippets, total = context_snippet_service.get_context_snippets_by_tags(
db, tags, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"tags": tags,
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve context snippets: {str(e)}"
)
@router.get(
"/top-relevant",
response_model=dict,
summary="Get top relevant context snippets",
description="Retrieve the most relevant context snippets by relevance score",
status_code=status.HTTP_200_OK,
)
def get_top_relevant_snippets(
limit: int = Query(
default=10,
ge=1,
le=50,
description="Maximum number of snippets to retrieve (max 50)"
),
min_relevance_score: float = Query(
default=7.0,
ge=0.0,
le=10.0,
description="Minimum relevance score threshold (0.0-10.0)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get the top most relevant context snippets.
Returns snippets ordered by relevance score (highest first).
"""
try:
snippets = context_snippet_service.get_top_relevant_snippets(
db, limit, min_relevance_score
)
return {
"total": len(snippets),
"limit": limit,
"min_relevance_score": min_relevance_score,
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve top relevant snippets: {str(e)}"
)
@router.get(
"/by-project/{project_id}",
response_model=dict,
summary="Get context snippets by project",
description="Retrieve all context snippets for a specific project",
status_code=status.HTTP_200_OK,
)
def get_context_snippets_by_project(
project_id: UUID,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get all context snippets for a specific project.
"""
try:
snippets, total = context_snippet_service.get_context_snippets_by_project(
db, project_id, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"project_id": str(project_id),
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve context snippets: {str(e)}"
)
@router.get(
"/by-client/{client_id}",
response_model=dict,
summary="Get context snippets by client",
description="Retrieve all context snippets for a specific client",
status_code=status.HTTP_200_OK,
)
def get_context_snippets_by_client(
client_id: UUID,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get all context snippets for a specific client.
"""
try:
snippets, total = context_snippet_service.get_context_snippets_by_client(
db, client_id, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"client_id": str(client_id),
"snippets": [ContextSnippetResponse.model_validate(snippet) for snippet in snippets]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve context snippets: {str(e)}"
)
@router.get(
"/{snippet_id}",
response_model=ContextSnippetResponse,
summary="Get context snippet by ID",
description="Retrieve a single context snippet by its unique identifier (increments usage_count)",
status_code=status.HTTP_200_OK,
)
def get_context_snippet(
snippet_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific context snippet by ID.
Note: This automatically increments the usage_count for tracking.
"""
snippet = context_snippet_service.get_context_snippet_by_id(db, snippet_id)
return ContextSnippetResponse.model_validate(snippet)
@router.post(
"",
response_model=ContextSnippetResponse,
summary="Create new context snippet",
description="Create a new context snippet with the provided details",
status_code=status.HTTP_201_CREATED,
)
def create_context_snippet(
snippet_data: ContextSnippetCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new context snippet.
Requires a valid JWT token with appropriate permissions.
"""
snippet = context_snippet_service.create_context_snippet(db, snippet_data)
return ContextSnippetResponse.model_validate(snippet)
@router.put(
"/{snippet_id}",
response_model=ContextSnippetResponse,
summary="Update context snippet",
description="Update an existing context snippet's details",
status_code=status.HTTP_200_OK,
)
def update_context_snippet(
snippet_id: UUID,
snippet_data: ContextSnippetUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Update an existing context snippet.
Only provided fields will be updated. All fields are optional.
"""
snippet = context_snippet_service.update_context_snippet(db, snippet_id, snippet_data)
return ContextSnippetResponse.model_validate(snippet)
@router.delete(
"/{snippet_id}",
response_model=dict,
summary="Delete context snippet",
description="Delete a context snippet by its ID",
status_code=status.HTTP_200_OK,
)
def delete_context_snippet(
snippet_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a context snippet.
This is a permanent operation and cannot be undone.
"""
return context_snippet_service.delete_context_snippet(db, snippet_id)

View File

@@ -1,312 +0,0 @@
"""
ConversationContext API router for ClaudeTools.
Defines all REST API endpoints for managing conversation contexts,
including context recall functionality for Claude's memory system.
"""
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from api.database import get_db
from api.middleware.auth import get_current_user
from api.schemas.conversation_context import (
ConversationContextCreate,
ConversationContextResponse,
ConversationContextUpdate,
)
from api.services import conversation_context_service
# Create router with prefix and tags
router = APIRouter()
@router.get(
"",
response_model=dict,
summary="List all conversation contexts",
description="Retrieve a paginated list of all conversation contexts with optional filtering",
status_code=status.HTTP_200_OK,
)
def list_conversation_contexts(
skip: int = Query(
default=0,
ge=0,
description="Number of records to skip for pagination"
),
limit: int = Query(
default=100,
ge=1,
le=1000,
description="Maximum number of records to return (max 1000)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
List all conversation contexts with pagination.
Returns contexts ordered by relevance score and recency.
"""
try:
contexts, total = conversation_context_service.get_conversation_contexts(db, skip, limit)
return {
"total": total,
"skip": skip,
"limit": limit,
"contexts": [ConversationContextResponse.model_validate(ctx) for ctx in contexts]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve conversation contexts: {str(e)}"
)
@router.get(
"/recall",
response_model=dict,
summary="Retrieve relevant contexts for injection",
description="Get token-efficient context formatted for Claude prompt injection",
status_code=status.HTTP_200_OK,
)
def recall_context(
search_term: Optional[str] = Query(
None,
max_length=200,
pattern=r'^[a-zA-Z0-9\s\-_.,!?()]+$',
description="Full-text search term (alphanumeric, spaces, and basic punctuation only)"
),
project_id: Optional[UUID] = Query(None, description="Filter by project ID"),
tags: Optional[List[str]] = Query(
None,
description="Filter by tags (OR logic)",
max_items=20
),
limit: int = Query(
default=10,
ge=1,
le=50,
description="Maximum number of contexts to retrieve (max 50)"
),
min_relevance_score: float = Query(
default=5.0,
ge=0.0,
le=10.0,
description="Minimum relevance score threshold (0.0-10.0)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Retrieve relevant contexts formatted for Claude prompt injection.
This endpoint returns contexts matching the search criteria with
properly formatted JSON response containing the contexts array.
Query Parameters:
- search_term: Full-text search across title and summary (uses FULLTEXT index)
- project_id: Filter contexts by project
- tags: Filter contexts by tags (any match)
- limit: Maximum number of contexts to retrieve
- min_relevance_score: Minimum relevance score threshold
Returns JSON with contexts array and metadata.
"""
# Validate tags to prevent SQL injection
if tags:
import re
tag_pattern = re.compile(r'^[a-zA-Z0-9\-_]+$')
for tag in tags:
if not tag_pattern.match(tag):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid tag format: '{tag}'. Tags must be alphanumeric with hyphens or underscores only."
)
try:
contexts, total = conversation_context_service.get_recall_context(
db=db,
search_term=search_term,
project_id=project_id,
tags=tags,
limit=limit,
min_relevance_score=min_relevance_score
)
return {
"total": total,
"limit": limit,
"search_term": search_term,
"project_id": str(project_id) if project_id else None,
"tags": tags,
"min_relevance_score": min_relevance_score,
"contexts": [ConversationContextResponse.model_validate(ctx) for ctx in contexts]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve recall context: {str(e)}"
)
@router.get(
"/by-project/{project_id}",
response_model=dict,
summary="Get conversation contexts by project",
description="Retrieve all conversation contexts for a specific project",
status_code=status.HTTP_200_OK,
)
def get_conversation_contexts_by_project(
project_id: UUID,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get all conversation contexts for a specific project.
"""
try:
contexts, total = conversation_context_service.get_conversation_contexts_by_project(
db, project_id, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"project_id": str(project_id),
"contexts": [ConversationContextResponse.model_validate(ctx) for ctx in contexts]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve conversation contexts: {str(e)}"
)
@router.get(
"/by-session/{session_id}",
response_model=dict,
summary="Get conversation contexts by session",
description="Retrieve all conversation contexts for a specific session",
status_code=status.HTTP_200_OK,
)
def get_conversation_contexts_by_session(
session_id: UUID,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get all conversation contexts for a specific session.
"""
try:
contexts, total = conversation_context_service.get_conversation_contexts_by_session(
db, session_id, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"session_id": str(session_id),
"contexts": [ConversationContextResponse.model_validate(ctx) for ctx in contexts]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve conversation contexts: {str(e)}"
)
@router.get(
"/{context_id}",
response_model=ConversationContextResponse,
summary="Get conversation context by ID",
description="Retrieve a single conversation context by its unique identifier",
status_code=status.HTTP_200_OK,
)
def get_conversation_context(
context_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific conversation context by ID.
"""
context = conversation_context_service.get_conversation_context_by_id(db, context_id)
return ConversationContextResponse.model_validate(context)
@router.post(
"",
response_model=ConversationContextResponse,
summary="Create new conversation context",
description="Create a new conversation context with the provided details",
status_code=status.HTTP_201_CREATED,
)
def create_conversation_context(
context_data: ConversationContextCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new conversation context.
Requires a valid JWT token with appropriate permissions.
"""
context = conversation_context_service.create_conversation_context(db, context_data)
return ConversationContextResponse.model_validate(context)
@router.put(
"/{context_id}",
response_model=ConversationContextResponse,
summary="Update conversation context",
description="Update an existing conversation context's details",
status_code=status.HTTP_200_OK,
)
def update_conversation_context(
context_id: UUID,
context_data: ConversationContextUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Update an existing conversation context.
Only provided fields will be updated. All fields are optional.
"""
context = conversation_context_service.update_conversation_context(db, context_id, context_data)
return ConversationContextResponse.model_validate(context)
@router.delete(
"/{context_id}",
response_model=dict,
summary="Delete conversation context",
description="Delete a conversation context by its ID",
status_code=status.HTTP_200_OK,
)
def delete_conversation_context(
context_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a conversation context.
This is a permanent operation and cannot be undone.
"""
return conversation_context_service.delete_conversation_context(db, context_id)

View File

@@ -1,264 +0,0 @@
"""
DecisionLog API router for ClaudeTools.
Defines all REST API endpoints for managing decision logs,
tracking important decisions made during work.
"""
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from api.database import get_db
from api.middleware.auth import get_current_user
from api.schemas.decision_log import (
DecisionLogCreate,
DecisionLogResponse,
DecisionLogUpdate,
)
from api.services import decision_log_service
# Create router with prefix and tags
router = APIRouter()
@router.get(
"",
response_model=dict,
summary="List all decision logs",
description="Retrieve a paginated list of all decision logs",
status_code=status.HTTP_200_OK,
)
def list_decision_logs(
skip: int = Query(
default=0,
ge=0,
description="Number of records to skip for pagination"
),
limit: int = Query(
default=100,
ge=1,
le=1000,
description="Maximum number of records to return (max 1000)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
List all decision logs with pagination.
Returns decision logs ordered by most recent first.
"""
try:
logs, total = decision_log_service.get_decision_logs(db, skip, limit)
return {
"total": total,
"skip": skip,
"limit": limit,
"logs": [DecisionLogResponse.model_validate(log) for log in logs]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve decision logs: {str(e)}"
)
@router.get(
"/by-impact/{impact}",
response_model=dict,
summary="Get decision logs by impact level",
description="Retrieve decision logs filtered by impact level",
status_code=status.HTTP_200_OK,
)
def get_decision_logs_by_impact(
impact: str,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get decision logs filtered by impact level.
Valid impact levels: low, medium, high, critical
"""
try:
logs, total = decision_log_service.get_decision_logs_by_impact(
db, impact, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"impact": impact,
"logs": [DecisionLogResponse.model_validate(log) for log in logs]
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve decision logs: {str(e)}"
)
@router.get(
"/by-project/{project_id}",
response_model=dict,
summary="Get decision logs by project",
description="Retrieve all decision logs for a specific project",
status_code=status.HTTP_200_OK,
)
def get_decision_logs_by_project(
project_id: UUID,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get all decision logs for a specific project.
"""
try:
logs, total = decision_log_service.get_decision_logs_by_project(
db, project_id, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"project_id": str(project_id),
"logs": [DecisionLogResponse.model_validate(log) for log in logs]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve decision logs: {str(e)}"
)
@router.get(
"/by-session/{session_id}",
response_model=dict,
summary="Get decision logs by session",
description="Retrieve all decision logs for a specific session",
status_code=status.HTTP_200_OK,
)
def get_decision_logs_by_session(
session_id: UUID,
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=1000),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get all decision logs for a specific session.
"""
try:
logs, total = decision_log_service.get_decision_logs_by_session(
db, session_id, skip, limit
)
return {
"total": total,
"skip": skip,
"limit": limit,
"session_id": str(session_id),
"logs": [DecisionLogResponse.model_validate(log) for log in logs]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve decision logs: {str(e)}"
)
@router.get(
"/{log_id}",
response_model=DecisionLogResponse,
summary="Get decision log by ID",
description="Retrieve a single decision log by its unique identifier",
status_code=status.HTTP_200_OK,
)
def get_decision_log(
log_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific decision log by ID.
"""
log = decision_log_service.get_decision_log_by_id(db, log_id)
return DecisionLogResponse.model_validate(log)
@router.post(
"",
response_model=DecisionLogResponse,
summary="Create new decision log",
description="Create a new decision log with the provided details",
status_code=status.HTTP_201_CREATED,
)
def create_decision_log(
log_data: DecisionLogCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new decision log.
Requires a valid JWT token with appropriate permissions.
"""
log = decision_log_service.create_decision_log(db, log_data)
return DecisionLogResponse.model_validate(log)
@router.put(
"/{log_id}",
response_model=DecisionLogResponse,
summary="Update decision log",
description="Update an existing decision log's details",
status_code=status.HTTP_200_OK,
)
def update_decision_log(
log_id: UUID,
log_data: DecisionLogUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Update an existing decision log.
Only provided fields will be updated. All fields are optional.
"""
log = decision_log_service.update_decision_log(db, log_id, log_data)
return DecisionLogResponse.model_validate(log)
@router.delete(
"/{log_id}",
response_model=dict,
summary="Delete decision log",
description="Delete a decision log by its ID",
status_code=status.HTTP_200_OK,
)
def delete_decision_log(
log_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a decision log.
This is a permanent operation and cannot be undone.
"""
return decision_log_service.delete_decision_log(db, log_id)

View File

@@ -1,202 +0,0 @@
"""
ProjectState API router for ClaudeTools.
Defines all REST API endpoints for managing project states,
tracking the current state of projects for context retrieval.
"""
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from api.database import get_db
from api.middleware.auth import get_current_user
from api.schemas.project_state import (
ProjectStateCreate,
ProjectStateResponse,
ProjectStateUpdate,
)
from api.services import project_state_service
# Create router with prefix and tags
router = APIRouter()
@router.get(
"",
response_model=dict,
summary="List all project states",
description="Retrieve a paginated list of all project states",
status_code=status.HTTP_200_OK,
)
def list_project_states(
skip: int = Query(
default=0,
ge=0,
description="Number of records to skip for pagination"
),
limit: int = Query(
default=100,
ge=1,
le=1000,
description="Maximum number of records to return (max 1000)"
),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
List all project states with pagination.
Returns project states ordered by most recently updated.
"""
try:
states, total = project_state_service.get_project_states(db, skip, limit)
return {
"total": total,
"skip": skip,
"limit": limit,
"states": [ProjectStateResponse.model_validate(state) for state in states]
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to retrieve project states: {str(e)}"
)
@router.get(
"/by-project/{project_id}",
response_model=ProjectStateResponse,
summary="Get project state by project ID",
description="Retrieve the project state for a specific project (unique per project)",
status_code=status.HTTP_200_OK,
)
def get_project_state_by_project(
project_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get the project state for a specific project.
Each project has exactly one project state.
"""
state = project_state_service.get_project_state_by_project(db, project_id)
if not state:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"ProjectState for project ID {project_id} not found"
)
return ProjectStateResponse.model_validate(state)
@router.get(
"/{state_id}",
response_model=ProjectStateResponse,
summary="Get project state by ID",
description="Retrieve a single project state by its unique identifier",
status_code=status.HTTP_200_OK,
)
def get_project_state(
state_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a specific project state by ID.
"""
state = project_state_service.get_project_state_by_id(db, state_id)
return ProjectStateResponse.model_validate(state)
@router.post(
"",
response_model=ProjectStateResponse,
summary="Create new project state",
description="Create a new project state with the provided details",
status_code=status.HTTP_201_CREATED,
)
def create_project_state(
state_data: ProjectStateCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Create a new project state.
Each project can only have one project state (enforced by unique constraint).
Requires a valid JWT token with appropriate permissions.
"""
state = project_state_service.create_project_state(db, state_data)
return ProjectStateResponse.model_validate(state)
@router.put(
"/{state_id}",
response_model=ProjectStateResponse,
summary="Update project state",
description="Update an existing project state's details",
status_code=status.HTTP_200_OK,
)
def update_project_state(
state_id: UUID,
state_data: ProjectStateUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Update an existing project state.
Only provided fields will be updated. All fields are optional.
Uses compression utilities when updating to maintain efficient storage.
"""
state = project_state_service.update_project_state(db, state_id, state_data)
return ProjectStateResponse.model_validate(state)
@router.put(
"/by-project/{project_id}",
response_model=ProjectStateResponse,
summary="Update project state by project ID",
description="Update project state by project ID (creates if doesn't exist)",
status_code=status.HTTP_200_OK,
)
def update_project_state_by_project(
project_id: UUID,
state_data: ProjectStateUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Update project state by project ID.
Convenience method that creates a new project state if it doesn't exist,
or updates the existing one if it does.
"""
state = project_state_service.update_project_state_by_project(db, project_id, state_data)
return ProjectStateResponse.model_validate(state)
@router.delete(
"/{state_id}",
response_model=dict,
summary="Delete project state",
description="Delete a project state by its ID",
status_code=status.HTTP_200_OK,
)
def delete_project_state(
state_id: UUID,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete a project state.
This is a permanent operation and cannot be undone.
"""
return project_state_service.delete_project_state(db, state_id)