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

296 lines
7.6 KiB
Python

"""
Site service layer for business logic and database operations.
This module handles all database operations for sites, 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.site import Site
from api.models.client import Client
from api.schemas.site import SiteCreate, SiteUpdate
def get_sites(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[Site], int]:
"""
Retrieve a paginated list of sites.
Args:
db: Database session
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
tuple: (list of sites, total count)
Example:
```python
sites, total = get_sites(db, skip=0, limit=50)
print(f"Retrieved {len(sites)} of {total} sites")
```
"""
# Get total count
total = db.query(Site).count()
# Get paginated results, ordered by created_at descending (newest first)
sites = (
db.query(Site)
.order_by(Site.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return sites, total
def get_site_by_id(db: Session, site_id: UUID) -> Site:
"""
Retrieve a single site by its ID.
Args:
db: Database session
site_id: UUID of the site to retrieve
Returns:
Site: The site object
Raises:
HTTPException: 404 if site not found
Example:
```python
site = get_site_by_id(db, site_id)
print(f"Found site: {site.name}")
```
"""
site = db.query(Site).filter(Site.id == str(site_id)).first()
if not site:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Site with ID {site_id} not found"
)
return site
def get_sites_by_client(db: Session, client_id: UUID, skip: int = 0, limit: int = 100) -> tuple[list[Site], int]:
"""
Retrieve sites belonging to a specific client.
Args:
db: Database session
client_id: UUID of the client
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
tuple: (list of sites, total count for this client)
Raises:
HTTPException: 404 if client not found
Example:
```python
sites, total = get_sites_by_client(db, client_id, skip=0, limit=50)
print(f"Retrieved {len(sites)} of {total} sites for client")
```
"""
# Verify client exists
client = db.query(Client).filter(Client.id == str(client_id)).first()
if not client:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Client with ID {client_id} not found"
)
# Get total count for this client
total = db.query(Site).filter(Site.client_id == str(client_id)).count()
# Get paginated results
sites = (
db.query(Site)
.filter(Site.client_id == str(client_id))
.order_by(Site.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return sites, total
def create_site(db: Session, site_data: SiteCreate) -> Site:
"""
Create a new site.
Args:
db: Database session
site_data: Site creation data
Returns:
Site: The created site object
Raises:
HTTPException: 404 if client not found
HTTPException: 500 if database error occurs
Example:
```python
site_data = SiteCreate(
client_id="123e4567-e89b-12d3-a456-426614174000",
name="Main Office",
network_subnet="172.16.9.0/24"
)
site = create_site(db, site_data)
print(f"Created site: {site.id}")
```
"""
# Verify client exists
client = db.query(Client).filter(Client.id == str(site_data.client_id)).first()
if not client:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Client with ID {site_data.client_id} not found"
)
try:
# Create new site instance
db_site = Site(**site_data.model_dump())
# Add to database
db.add(db_site)
db.commit()
db.refresh(db_site)
return db_site
except IntegrityError as e:
db.rollback()
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 site: {str(e)}"
)
def update_site(db: Session, site_id: UUID, site_data: SiteUpdate) -> Site:
"""
Update an existing site.
Args:
db: Database session
site_id: UUID of the site to update
site_data: Site update data (only provided fields will be updated)
Returns:
Site: The updated site object
Raises:
HTTPException: 404 if site or client not found
HTTPException: 500 if database error occurs
Example:
```python
update_data = SiteUpdate(
name="Main Office - Renovated",
vpn_required=True
)
site = update_site(db, site_id, update_data)
print(f"Updated site: {site.name}")
```
"""
# Get existing site
site = get_site_by_id(db, site_id)
try:
# Update only provided fields
update_data = site_data.model_dump(exclude_unset=True)
# If updating client_id, verify new client exists
if "client_id" in update_data:
client = db.query(Client).filter(Client.id == str(update_data["client_id"])).first()
if not client:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Client with ID {update_data['client_id']} not found"
)
# Apply updates
for field, value in update_data.items():
setattr(site, field, value)
db.commit()
db.refresh(site)
return site
except HTTPException:
db.rollback()
raise
except IntegrityError as e:
db.rollback()
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 site: {str(e)}"
)
def delete_site(db: Session, site_id: UUID) -> dict:
"""
Delete a site by its ID.
Args:
db: Database session
site_id: UUID of the site to delete
Returns:
dict: Success message
Raises:
HTTPException: 404 if site not found
HTTPException: 500 if database error occurs
Example:
```python
result = delete_site(db, site_id)
print(result["message"]) # "Site deleted successfully"
```
"""
# Get existing site (raises 404 if not found)
site = get_site_by_id(db, site_id)
try:
db.delete(site)
db.commit()
return {
"message": "Site deleted successfully",
"site_id": str(site_id)
}
except Exception as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete site: {str(e)}"
)