Files
claudetools/api/services/work_item_service.py
Mike Swanson 390b10b32c Complete Phase 6: MSP Work Tracking with Context Recall System
Implements production-ready MSP platform with cross-machine persistent memory for Claude.

API Implementation:
- 130 REST API endpoints across 21 entities
- JWT authentication on all endpoints
- AES-256-GCM encryption for credentials
- Automatic audit logging
- Complete OpenAPI documentation

Database:
- 43 tables in MariaDB (172.16.3.20:3306)
- 42 SQLAlchemy models with modern 2.0 syntax
- Full Alembic migration system
- 99.1% CRUD test pass rate

Context Recall System (Phase 6):
- Cross-machine persistent memory via database
- Automatic context injection via Claude Code hooks
- Automatic context saving after task completion
- 90-95% token reduction with compression utilities
- Relevance scoring with time decay
- Tag-based semantic search
- One-command setup script

Security Features:
- JWT tokens with Argon2 password hashing
- AES-256-GCM encryption for all sensitive data
- Comprehensive audit trail for credentials
- HMAC tamper detection
- Secure configuration management

Test Results:
- Phase 3: 38/38 CRUD tests passing (100%)
- Phase 4: 34/35 core API tests passing (97.1%)
- Phase 5: 62/62 extended API tests passing (100%)
- Phase 6: 10/10 compression tests passing (100%)
- Overall: 144/145 tests passing (99.3%)

Documentation:
- Comprehensive architecture guides
- Setup automation scripts
- API documentation at /api/docs
- Complete test reports
- Troubleshooting guides

Project Status: 95% Complete (Production-Ready)
Phase 7 (optional work context APIs) remains for future enhancement.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 06:00:26 -07:00

456 lines
13 KiB
Python

"""
WorkItem service layer for business logic and database operations.
This module handles all database operations for work items, providing a clean
separation between the API routes and data access layer.
"""
from typing import Optional
from uuid import UUID
from fastapi import HTTPException, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from api.models.work_item import WorkItem
from api.models.session import Session as SessionModel
from api.schemas.work_item import WorkItemCreate, WorkItemUpdate
def get_work_items(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[WorkItem], int]:
"""
Retrieve a paginated list of work items.
Args:
db: Database session
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
tuple: (list of work items, total count)
Example:
```python
work_items, total = get_work_items(db, skip=0, limit=50)
print(f"Retrieved {len(work_items)} of {total} work items")
```
"""
# Get total count
total = db.query(WorkItem).count()
# Get paginated results, ordered by created_at descending (newest first)
work_items = (
db.query(WorkItem)
.order_by(WorkItem.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return work_items, total
def get_work_item_by_id(db: Session, work_item_id: UUID) -> WorkItem:
"""
Retrieve a single work item by its ID.
Args:
db: Database session
work_item_id: UUID of the work item to retrieve
Returns:
WorkItem: The work item object
Raises:
HTTPException: 404 if work item not found
Example:
```python
work_item = get_work_item_by_id(db, work_item_id)
print(f"Found work item: {work_item.title}")
```
"""
work_item = db.query(WorkItem).filter(WorkItem.id == str(work_item_id)).first()
if not work_item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Work item with ID {work_item_id} not found"
)
return work_item
def get_work_items_by_session(db: Session, session_id: str, skip: int = 0, limit: int = 100) -> tuple[list[WorkItem], int]:
"""
Retrieve work items for a specific session.
Args:
db: Database session
session_id: Session UUID
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
tuple: (list of work items, total count)
Example:
```python
work_items, total = get_work_items_by_session(db, session_id)
print(f"Session has {total} work items")
```
"""
total = db.query(WorkItem).filter(WorkItem.session_id == str(session_id)).count()
work_items = (
db.query(WorkItem)
.filter(WorkItem.session_id == str(session_id))
.order_by(WorkItem.item_order, WorkItem.created_at)
.offset(skip)
.limit(limit)
.all()
)
return work_items, total
def get_work_items_by_project(db: Session, project_id: str, skip: int = 0, limit: int = 100) -> tuple[list[WorkItem], int]:
"""
Retrieve work items for a specific project (through sessions).
Args:
db: Database session
project_id: Project UUID
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
tuple: (list of work items, total count)
Example:
```python
work_items, total = get_work_items_by_project(db, project_id)
print(f"Project has {total} work items")
```
"""
total = (
db.query(WorkItem)
.join(SessionModel, WorkItem.session_id == SessionModel.id)
.filter(SessionModel.project_id == str(project_id))
.count()
)
work_items = (
db.query(WorkItem)
.join(SessionModel, WorkItem.session_id == SessionModel.id)
.filter(SessionModel.project_id == str(project_id))
.order_by(WorkItem.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return work_items, total
def get_work_items_by_client(db: Session, client_id: str, skip: int = 0, limit: int = 100) -> tuple[list[WorkItem], int]:
"""
Retrieve work items for a specific client (through sessions).
Args:
db: Database session
client_id: Client UUID
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
tuple: (list of work items, total count)
Example:
```python
work_items, total = get_work_items_by_client(db, client_id)
print(f"Client has {total} work items")
```
"""
total = (
db.query(WorkItem)
.join(SessionModel, WorkItem.session_id == SessionModel.id)
.filter(SessionModel.client_id == str(client_id))
.count()
)
work_items = (
db.query(WorkItem)
.join(SessionModel, WorkItem.session_id == SessionModel.id)
.filter(SessionModel.client_id == str(client_id))
.order_by(WorkItem.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return work_items, total
def get_work_items_by_status(db: Session, status_filter: str, skip: int = 0, limit: int = 100) -> tuple[list[WorkItem], int]:
"""
Retrieve work items by status.
Args:
db: Database session
status_filter: Status to filter by (completed, in_progress, blocked, pending, deferred)
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
tuple: (list of work items, total count)
Example:
```python
work_items, total = get_work_items_by_status(db, "in_progress")
print(f"Found {total} in progress work items")
```
"""
total = db.query(WorkItem).filter(WorkItem.status == status_filter).count()
work_items = (
db.query(WorkItem)
.filter(WorkItem.status == status_filter)
.order_by(WorkItem.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return work_items, total
def validate_session_exists(db: Session, session_id: str) -> None:
"""
Validate that a session exists.
Args:
db: Database session
session_id: Session UUID to validate
Raises:
HTTPException: 404 if session not found
Example:
```python
validate_session_exists(db, session_id)
# Continues if session exists, raises HTTPException if not
```
"""
session = db.query(SessionModel).filter(SessionModel.id == str(session_id)).first()
if not session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session with ID {session_id} not found"
)
def create_work_item(db: Session, work_item_data: WorkItemCreate) -> WorkItem:
"""
Create a new work item.
Args:
db: Database session
work_item_data: Work item creation data
Returns:
WorkItem: The created work item object
Raises:
HTTPException: 404 if session not found
HTTPException: 422 if validation fails
HTTPException: 500 if database error occurs
Example:
```python
work_item_data = WorkItemCreate(
session_id="123e4567-e89b-12d3-a456-426614174000",
category="infrastructure",
title="Configure firewall rules",
description="Updated firewall rules for new server",
status="completed"
)
work_item = create_work_item(db, work_item_data)
print(f"Created work item: {work_item.id}")
```
"""
# Validate session exists
validate_session_exists(db, work_item_data.session_id)
try:
# Create new work item instance
db_work_item = WorkItem(**work_item_data.model_dump())
# Add to database
db.add(db_work_item)
db.commit()
db.refresh(db_work_item)
return db_work_item
except IntegrityError as e:
db.rollback()
# Handle foreign key constraint violations
if "session_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Session with ID {work_item_data.session_id} not found"
)
elif "category" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid category. Must be one of: infrastructure, troubleshooting, configuration, development, maintenance, security, documentation"
)
elif "status" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid status. Must be one of: completed, in_progress, blocked, pending, deferred"
)
elif "priority" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid priority. Must be one of: critical, high, medium, low"
)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Database error: {str(e)}"
)
except Exception as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create work item: {str(e)}"
)
def update_work_item(db: Session, work_item_id: UUID, work_item_data: WorkItemUpdate) -> WorkItem:
"""
Update an existing work item.
Args:
db: Database session
work_item_id: UUID of the work item to update
work_item_data: Work item update data (only provided fields will be updated)
Returns:
WorkItem: The updated work item object
Raises:
HTTPException: 404 if work item or session not found
HTTPException: 422 if validation fails
HTTPException: 500 if database error occurs
Example:
```python
update_data = WorkItemUpdate(
status="completed",
actual_minutes=45
)
work_item = update_work_item(db, work_item_id, update_data)
print(f"Updated work item: {work_item.title}")
```
"""
# Get existing work item
work_item = get_work_item_by_id(db, work_item_id)
try:
# Update only provided fields
update_data = work_item_data.model_dump(exclude_unset=True)
# If updating session_id, validate session exists
if "session_id" in update_data and update_data["session_id"] != work_item.session_id:
validate_session_exists(db, update_data["session_id"])
# Apply updates
for field, value in update_data.items():
setattr(work_item, field, value)
db.commit()
db.refresh(work_item)
return work_item
except HTTPException:
db.rollback()
raise
except IntegrityError as e:
db.rollback()
if "session_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Session not found"
)
elif "category" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid category. Must be one of: infrastructure, troubleshooting, configuration, development, maintenance, security, documentation"
)
elif "status" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid status. Must be one of: completed, in_progress, blocked, pending, deferred"
)
elif "priority" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid priority. Must be one of: critical, high, medium, low"
)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Database error: {str(e)}"
)
except Exception as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update work item: {str(e)}"
)
def delete_work_item(db: Session, work_item_id: UUID) -> dict:
"""
Delete a work item by its ID.
Args:
db: Database session
work_item_id: UUID of the work item to delete
Returns:
dict: Success message
Raises:
HTTPException: 404 if work item not found
HTTPException: 500 if database error occurs
Example:
```python
result = delete_work_item(db, work_item_id)
print(result["message"]) # "Work item deleted successfully"
```
"""
# Get existing work item (raises 404 if not found)
work_item = get_work_item_by_id(db, work_item_id)
try:
db.delete(work_item)
db.commit()
return {
"message": "Work item deleted successfully",
"work_item_id": str(work_item_id)
}
except Exception as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete work item: {str(e)}"
)