Files
claudetools/api/services/session_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

376 lines
11 KiB
Python

"""
Session service layer for business logic and database operations.
This module handles all database operations for sessions, 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.session import Session as SessionModel
from api.models.project import Project
from api.models.machine import Machine
from api.schemas.session import SessionCreate, SessionUpdate
def get_sessions(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[SessionModel], int]:
"""
Retrieve a paginated list of sessions.
Args:
db: Database session
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
tuple: (list of sessions, total count)
Example:
```python
sessions, total = get_sessions(db, skip=0, limit=50)
print(f"Retrieved {len(sessions)} of {total} sessions")
```
"""
# Get total count
total = db.query(SessionModel).count()
# Get paginated results, ordered by session_date descending (newest first)
sessions = (
db.query(SessionModel)
.order_by(SessionModel.session_date.desc())
.offset(skip)
.limit(limit)
.all()
)
return sessions, total
def get_session_by_id(db: Session, session_id: UUID) -> SessionModel:
"""
Retrieve a single session by its ID.
Args:
db: Database session
session_id: UUID of the session to retrieve
Returns:
SessionModel: The session object
Raises:
HTTPException: 404 if session not found
Example:
```python
session = get_session_by_id(db, session_id)
print(f"Found session: {session.session_title}")
```
"""
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"
)
return session
def get_sessions_by_project(db: Session, project_id: UUID, skip: int = 0, limit: int = 100) -> tuple[list[SessionModel], int]:
"""
Retrieve sessions for a specific project.
Args:
db: Database session
project_id: UUID of the project
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
tuple: (list of sessions, total count)
Example:
```python
sessions, total = get_sessions_by_project(db, project_id)
print(f"Found {total} sessions for project")
```
"""
# Get total count
total = db.query(SessionModel).filter(SessionModel.project_id == str(project_id)).count()
# Get paginated results
sessions = (
db.query(SessionModel)
.filter(SessionModel.project_id == str(project_id))
.order_by(SessionModel.session_date.desc())
.offset(skip)
.limit(limit)
.all()
)
return sessions, total
def get_sessions_by_machine(db: Session, machine_id: UUID, skip: int = 0, limit: int = 100) -> tuple[list[SessionModel], int]:
"""
Retrieve sessions for a specific machine.
Args:
db: Database session
machine_id: UUID of the machine
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
tuple: (list of sessions, total count)
Example:
```python
sessions, total = get_sessions_by_machine(db, machine_id)
print(f"Found {total} sessions on this machine")
```
"""
# Get total count
total = db.query(SessionModel).filter(SessionModel.machine_id == str(machine_id)).count()
# Get paginated results
sessions = (
db.query(SessionModel)
.filter(SessionModel.machine_id == str(machine_id))
.order_by(SessionModel.session_date.desc())
.offset(skip)
.limit(limit)
.all()
)
return sessions, total
def create_session(db: Session, session_data: SessionCreate) -> SessionModel:
"""
Create a new session.
Args:
db: Database session
session_data: Session creation data
Returns:
SessionModel: The created session object
Raises:
HTTPException: 404 if referenced project or machine not found
HTTPException: 422 if validation fails
HTTPException: 500 if database error occurs
Example:
```python
session_data = SessionCreate(
session_title="Database migration work",
session_date=date.today(),
project_id="123e4567-e89b-12d3-a456-426614174000"
)
session = create_session(db, session_data)
print(f"Created session: {session.id}")
```
"""
try:
# Validate foreign keys if provided
if session_data.project_id:
project = db.query(Project).filter(Project.id == str(session_data.project_id)).first()
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Project with ID {session_data.project_id} not found"
)
if session_data.machine_id:
machine = db.query(Machine).filter(Machine.id == str(session_data.machine_id)).first()
if not machine:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Machine with ID {session_data.machine_id} not found"
)
# Create new session instance
db_session = SessionModel(**session_data.model_dump())
# Add to database
db.add(db_session)
db.commit()
db.refresh(db_session)
return db_session
except HTTPException:
db.rollback()
raise
except IntegrityError as e:
db.rollback()
# Handle foreign key constraint violations
if "project_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid project_id: {session_data.project_id}"
)
elif "machine_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid machine_id: {session_data.machine_id}"
)
elif "client_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid client_id: {session_data.client_id}"
)
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 session: {str(e)}"
)
def update_session(db: Session, session_id: UUID, session_data: SessionUpdate) -> SessionModel:
"""
Update an existing session.
Args:
db: Database session
session_id: UUID of the session to update
session_data: Session update data (only provided fields will be updated)
Returns:
SessionModel: The updated session object
Raises:
HTTPException: 404 if session, project, or machine not found
HTTPException: 422 if validation fails
HTTPException: 500 if database error occurs
Example:
```python
update_data = SessionUpdate(
status="completed",
duration_minutes=120
)
session = update_session(db, session_id, update_data)
print(f"Updated session: {session.session_title}")
```
"""
# Get existing session
session = get_session_by_id(db, session_id)
try:
# Update only provided fields
update_data = session_data.model_dump(exclude_unset=True)
# Validate foreign keys if being updated
if "project_id" in update_data and update_data["project_id"]:
project = db.query(Project).filter(Project.id == str(update_data["project_id"])).first()
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Project with ID {update_data['project_id']} not found"
)
if "machine_id" in update_data and update_data["machine_id"]:
machine = db.query(Machine).filter(Machine.id == str(update_data["machine_id"])).first()
if not machine:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Machine with ID {update_data['machine_id']} not found"
)
# Apply updates
for field, value in update_data.items():
setattr(session, field, value)
db.commit()
db.refresh(session)
return session
except HTTPException:
db.rollback()
raise
except IntegrityError as e:
db.rollback()
if "project_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid project_id"
)
elif "machine_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid machine_id"
)
elif "client_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid client_id"
)
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 session: {str(e)}"
)
def delete_session(db: Session, session_id: UUID) -> dict:
"""
Delete a session by its ID.
Args:
db: Database session
session_id: UUID of the session to delete
Returns:
dict: Success message
Raises:
HTTPException: 404 if session not found
HTTPException: 500 if database error occurs
Example:
```python
result = delete_session(db, session_id)
print(result["message"]) # "Session deleted successfully"
```
"""
# Get existing session (raises 404 if not found)
session = get_session_by_id(db, session_id)
try:
db.delete(session)
db.commit()
return {
"message": "Session deleted successfully",
"session_id": str(session_id)
}
except Exception as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete session: {str(e)}"
)