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

385 lines
11 KiB
Python

"""
Service service layer for business logic and database operations.
This module handles all database operations for services, 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.service import Service
from api.models.infrastructure import Infrastructure
from api.schemas.service import ServiceCreate, ServiceUpdate
def get_services(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[Service], int]:
"""
Retrieve a paginated list of services.
Args:
db: Database session
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
tuple: (list of services, total count)
Example:
```python
services, total = get_services(db, skip=0, limit=50)
print(f"Retrieved {len(services)} of {total} services")
```
"""
# Get total count
total = db.query(Service).count()
# Get paginated results, ordered by created_at descending (newest first)
services = (
db.query(Service)
.order_by(Service.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return services, total
def get_service_by_id(db: Session, service_id: UUID) -> Service:
"""
Retrieve a single service by its ID.
Args:
db: Database session
service_id: UUID of the service to retrieve
Returns:
Service: The service object
Raises:
HTTPException: 404 if service not found
Example:
```python
service = get_service_by_id(db, service_id)
print(f"Found service: {service.service_name}")
```
"""
service = db.query(Service).filter(Service.id == str(service_id)).first()
if not service:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Service with ID {service_id} not found"
)
return service
def get_services_by_client(db: Session, client_id: str, skip: int = 0, limit: int = 100) -> tuple[list[Service], int]:
"""
Retrieve services for a specific client (via infrastructure).
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 services, total count)
Example:
```python
services, total = get_services_by_client(db, client_id)
print(f"Client has {total} services")
```
"""
# Join with Infrastructure to filter by client_id
query = (
db.query(Service)
.join(Infrastructure, Service.infrastructure_id == Infrastructure.id)
.filter(Infrastructure.client_id == str(client_id))
)
total = query.count()
services = (
query
.order_by(Service.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return services, total
def get_services_by_type(db: Session, service_type: str, skip: int = 0, limit: int = 100) -> tuple[list[Service], int]:
"""
Retrieve services by type.
Args:
db: Database session
service_type: Service type to filter by (e.g., 'git_hosting', 'database')
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
tuple: (list of services, total count)
Example:
```python
services, total = get_services_by_type(db, "database")
print(f"Found {total} database services")
```
"""
total = db.query(Service).filter(Service.service_type == service_type).count()
services = (
db.query(Service)
.filter(Service.service_type == service_type)
.order_by(Service.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return services, total
def get_services_by_status(db: Session, status_filter: str, skip: int = 0, limit: int = 100) -> tuple[list[Service], int]:
"""
Retrieve services by status.
Args:
db: Database session
status_filter: Status to filter by (running, stopped, error, maintenance)
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
tuple: (list of services, total count)
Example:
```python
services, total = get_services_by_status(db, "running")
print(f"Found {total} running services")
```
"""
total = db.query(Service).filter(Service.status == status_filter).count()
services = (
db.query(Service)
.filter(Service.status == status_filter)
.order_by(Service.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return services, total
def validate_infrastructure_exists(db: Session, infrastructure_id: str) -> None:
"""
Validate that infrastructure exists.
Args:
db: Database session
infrastructure_id: Infrastructure UUID to validate
Raises:
HTTPException: 404 if infrastructure not found
Example:
```python
validate_infrastructure_exists(db, infrastructure_id)
# Continues if infrastructure exists, raises HTTPException if not
```
"""
infrastructure = db.query(Infrastructure).filter(Infrastructure.id == str(infrastructure_id)).first()
if not infrastructure:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Infrastructure with ID {infrastructure_id} not found"
)
def create_service(db: Session, service_data: ServiceCreate) -> Service:
"""
Create a new service.
Args:
db: Database session
service_data: Service creation data
Returns:
Service: The created service object
Raises:
HTTPException: 404 if infrastructure not found
HTTPException: 500 if database error occurs
Example:
```python
service_data = ServiceCreate(
infrastructure_id="123e4567-e89b-12d3-a456-426614174000",
service_name="Gitea",
service_type="git_hosting",
status="running"
)
service = create_service(db, service_data)
print(f"Created service: {service.id}")
```
"""
# Validate infrastructure exists if provided
if service_data.infrastructure_id:
validate_infrastructure_exists(db, service_data.infrastructure_id)
try:
# Create new service instance
db_service = Service(**service_data.model_dump())
# Add to database
db.add(db_service)
db.commit()
db.refresh(db_service)
return db_service
except IntegrityError as e:
db.rollback()
# Handle foreign key constraint violations
if "infrastructure_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Infrastructure with ID {service_data.infrastructure_id} not found"
)
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 service: {str(e)}"
)
def update_service(db: Session, service_id: UUID, service_data: ServiceUpdate) -> Service:
"""
Update an existing service.
Args:
db: Database session
service_id: UUID of the service to update
service_data: Service update data (only provided fields will be updated)
Returns:
Service: The updated service object
Raises:
HTTPException: 404 if service or infrastructure not found
HTTPException: 500 if database error occurs
Example:
```python
update_data = ServiceUpdate(
status="stopped",
notes="Service temporarily stopped for maintenance"
)
service = update_service(db, service_id, update_data)
print(f"Updated service: {service.service_name}")
```
"""
# Get existing service
service = get_service_by_id(db, service_id)
try:
# Update only provided fields
update_data = service_data.model_dump(exclude_unset=True)
# If updating infrastructure_id, validate infrastructure exists
if "infrastructure_id" in update_data and update_data["infrastructure_id"] and update_data["infrastructure_id"] != service.infrastructure_id:
validate_infrastructure_exists(db, update_data["infrastructure_id"])
# Apply updates
for field, value in update_data.items():
setattr(service, field, value)
db.commit()
db.refresh(service)
return service
except HTTPException:
db.rollback()
raise
except IntegrityError as e:
db.rollback()
if "infrastructure_id" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Infrastructure not found"
)
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 service: {str(e)}"
)
def delete_service(db: Session, service_id: UUID) -> dict:
"""
Delete a service by its ID.
Args:
db: Database session
service_id: UUID of the service to delete
Returns:
dict: Success message
Raises:
HTTPException: 404 if service not found
HTTPException: 500 if database error occurs
Example:
```python
result = delete_service(db, service_id)
print(result["message"]) # "Service deleted successfully"
```
"""
# Get existing service (raises 404 if not found)
service = get_service_by_id(db, service_id)
try:
db.delete(service)
db.commit()
return {
"message": "Service deleted successfully",
"service_id": str(service_id)
}
except Exception as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete service: {str(e)}"
)