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

284 lines
7.6 KiB
Python

"""
Client service layer for business logic and database operations.
This module handles all database operations for clients, 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.client import Client
from api.schemas.client import ClientCreate, ClientUpdate
def get_clients(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[Client], int]:
"""
Retrieve a paginated list of clients.
Args:
db: Database session
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
tuple: (list of clients, total count)
Example:
```python
clients, total = get_clients(db, skip=0, limit=50)
print(f"Retrieved {len(clients)} of {total} clients")
```
"""
# Get total count
total = db.query(Client).count()
# Get paginated results, ordered by created_at descending (newest first)
clients = (
db.query(Client)
.order_by(Client.created_at.desc())
.offset(skip)
.limit(limit)
.all()
)
return clients, total
def get_client_by_id(db: Session, client_id: UUID) -> Client:
"""
Retrieve a single client by its ID.
Args:
db: Database session
client_id: UUID of the client to retrieve
Returns:
Client: The client object
Raises:
HTTPException: 404 if client not found
Example:
```python
client = get_client_by_id(db, client_id)
print(f"Found client: {client.name}")
```
"""
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"
)
return client
def get_client_by_name(db: Session, name: str) -> Optional[Client]:
"""
Retrieve a client by its name.
Args:
db: Database session
name: Client name to search for
Returns:
Optional[Client]: The client if found, None otherwise
Example:
```python
client = get_client_by_name(db, "Acme Corporation")
if client:
print(f"Found client: {client.type}")
```
"""
return db.query(Client).filter(Client.name == name).first()
def create_client(db: Session, client_data: ClientCreate) -> Client:
"""
Create a new client.
Args:
db: Database session
client_data: Client creation data
Returns:
Client: The created client object
Raises:
HTTPException: 409 if client with name already exists
HTTPException: 500 if database error occurs
Example:
```python
client_data = ClientCreate(
name="Acme Corporation",
type="msp_client",
primary_contact="John Doe"
)
client = create_client(db, client_data)
print(f"Created client: {client.id}")
```
"""
# Check if client with name already exists
existing_client = get_client_by_name(db, client_data.name)
if existing_client:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Client with name '{client_data.name}' already exists"
)
try:
# Create new client instance
db_client = Client(**client_data.model_dump())
# Add to database
db.add(db_client)
db.commit()
db.refresh(db_client)
return db_client
except IntegrityError as e:
db.rollback()
# Handle unique constraint violations
if "name" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Client with name '{client_data.name}' already exists"
)
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 client: {str(e)}"
)
def update_client(db: Session, client_id: UUID, client_data: ClientUpdate) -> Client:
"""
Update an existing client.
Args:
db: Database session
client_id: UUID of the client to update
client_data: Client update data (only provided fields will be updated)
Returns:
Client: The updated client object
Raises:
HTTPException: 404 if client not found
HTTPException: 409 if update would violate unique constraints
HTTPException: 500 if database error occurs
Example:
```python
update_data = ClientUpdate(
primary_contact="Jane Smith",
is_active=False
)
client = update_client(db, client_id, update_data)
print(f"Updated client: {client.name}")
```
"""
# Get existing client
client = get_client_by_id(db, client_id)
try:
# Update only provided fields
update_data = client_data.model_dump(exclude_unset=True)
# If updating name, check if new name is already taken
if "name" in update_data and update_data["name"] != client.name:
existing = get_client_by_name(db, update_data["name"])
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Client with name '{update_data['name']}' already exists"
)
# Apply updates
for field, value in update_data.items():
setattr(client, field, value)
db.commit()
db.refresh(client)
return client
except HTTPException:
db.rollback()
raise
except IntegrityError as e:
db.rollback()
if "name" in str(e.orig):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Client with this name already exists"
)
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 client: {str(e)}"
)
def delete_client(db: Session, client_id: UUID) -> dict:
"""
Delete a client by its ID.
Args:
db: Database session
client_id: UUID of the client to delete
Returns:
dict: Success message
Raises:
HTTPException: 404 if client not found
HTTPException: 500 if database error occurs
Example:
```python
result = delete_client(db, client_id)
print(result["message"]) # "Client deleted successfully"
```
"""
# Get existing client (raises 404 if not found)
client = get_client_by_id(db, client_id)
try:
db.delete(client)
db.commit()
return {
"message": "Client deleted successfully",
"client_id": str(client_id)
}
except Exception as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete client: {str(e)}"
)