12 KiB
12 KiB
Service Adapters Clean Code Implementation
This document outlines the clean code principles and best practices implemented in the LabFusion Service Adapters service (Python FastAPI).
🏗️ Architecture Overview
The Service Adapters follow a modular architecture with clear separation of concerns:
service-adapters/
├── main.py # FastAPI application entry point
├── models/ # Pydantic schemas (Domain Layer)
│ ├── __init__.py
│ └── schemas.py
├── routes/ # API endpoints (Presentation Layer)
│ ├── __init__.py
│ ├── general.py # General endpoints
│ ├── home_assistant.py # Home Assistant integration
│ ├── frigate.py # Frigate integration
│ ├── immich.py # Immich integration
│ └── events.py # Event management
├── services/ # Business logic (Service Layer)
│ ├── __init__.py
│ ├── config.py # Configuration management
│ └── redis_client.py # Redis connection
├── requirements.txt # Dependencies
└── README.md # Service documentation
🧹 Clean Code Principles Applied
1. Single Responsibility Principle (SRP)
Route Modules
- general.py: Only handles general endpoints (health, services, root)
- home_assistant.py: Only manages Home Assistant integration
- frigate.py: Only handles Frigate camera system integration
- immich.py: Only manages Immich photo management integration
- events.py: Only handles event publishing and retrieval
Service Modules
- config.py: Only manages service configurations
- redis_client.py: Only handles Redis connections and operations
Model Modules
- schemas.py: Only contains Pydantic data models
2. Open/Closed Principle (OCP)
Extensible Route Design
# Easy to add new service integrations
from fastapi import APIRouter
router = APIRouter(prefix="/home-assistant", tags=["Home Assistant"])
# New integrations can be added without modifying existing code
# Just create new route files and include them in main.py
Configurable Services
# services/config.py
SERVICES = {
"home_assistant": {
"name": "Home Assistant",
"url": os.getenv("HOME_ASSISTANT_URL", "http://homeassistant.local:8123"),
"token": os.getenv("HOME_ASSISTANT_TOKEN"),
"enabled": True
}
# Easy to add new services
}
3. Dependency Inversion Principle (DIP)
Dependency Injection
# main.py
from services.redis_client import get_redis_client
app.include_router(general_router, dependencies=[Depends(get_redis_client)])
app.include_router(home_assistant_router, dependencies=[Depends(get_redis_client)])
Interface-Based Design
# services/redis_client.py
class RedisClient:
def __init__(self, redis_url: str):
self.redis_url = redis_url
self.client = None
async def connect(self):
# Implementation details hidden
pass
4. Interface Segregation Principle (ISP)
Focused Route Groups
- Each route module only exposes endpoints relevant to its service
- Clear API boundaries
- Minimal dependencies between modules
📝 Code Quality Improvements
1. Naming Conventions
Clear, Descriptive Names
# Good: Clear purpose
class ServiceStatus(BaseModel):
name: str
status: str
response_time: Optional[str] = None
# Good: Descriptive function names
async def get_home_assistant_entities()
async def get_frigate_events()
async def publish_event(event_data: EventData)
Consistent Naming
- Classes: PascalCase (e.g.,
ServiceStatus,EventData) - Functions: snake_case (e.g.,
get_home_assistant_entities) - Variables: snake_case (e.g.,
service_config) - Constants: UPPER_SNAKE_CASE (e.g.,
SERVICES)
2. Function Design
Small, Focused Functions
@router.get("/entities")
async def get_home_assistant_entities():
"""Get all Home Assistant entities."""
try:
entities = await fetch_ha_entities()
return {"entities": entities}
except Exception as e:
logger.error(f"Error fetching HA entities: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch entities")
Single Level of Abstraction
- Route functions handle HTTP concerns only
- Business logic delegated to service functions
- Data validation handled by Pydantic models
3. Error Handling
Consistent Error Responses
try:
result = await some_operation()
return result
except ConnectionError as e:
logger.error(f"Connection error: {e}")
raise HTTPException(status_code=503, detail="Service unavailable")
except Exception as e:
logger.error(f"Unexpected error: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
Proper HTTP Status Codes
- 200: Success
- 404: Not found
- 503: Service unavailable
- 500: Internal server error
4. Data Validation
Pydantic Models
class EventData(BaseModel):
event_type: str
service: str
timestamp: datetime
data: Dict[str, Any]
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
Input Validation
@router.post("/publish-event")
async def publish_event(event_data: EventData):
# Pydantic automatically validates input
await redis_client.publish_event(event_data)
return {"message": "Event published successfully"}
🔧 FastAPI Best Practices
1. Router Organization
Modular Route Structure
# routes/home_assistant.py
from fastapi import APIRouter, HTTPException, Depends
from models.schemas import HAAttributes, HAEntity
from services.redis_client import RedisClient
router = APIRouter(prefix="/home-assistant", tags=["Home Assistant"])
@router.get("/entities")
async def get_entities(redis: RedisClient = Depends(get_redis_client)):
# Implementation
Clear API Documentation
@router.get(
"/entities",
response_model=Dict[str, List[HAEntity]],
summary="Get Home Assistant entities",
description="Retrieve all entities from Home Assistant"
)
async def get_home_assistant_entities():
# Implementation
2. Dependency Injection
Redis Client Injection
# services/redis_client.py
async def get_redis_client() -> RedisClient:
redis = RedisClient(REDIS_URL)
await redis.connect()
return redis
Service Configuration
# services/config.py
def get_service_config(service_name: str) -> Dict[str, Any]:
return SERVICES.get(service_name, {})
3. OpenAPI Documentation
Comprehensive API Documentation
app = FastAPI(
title="LabFusion Service Adapters",
description="Integration adapters for homelab services",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
Detailed Endpoint Documentation
@router.get(
"/events",
response_model=Dict[str, List[FrigateEvent]],
summary="Get Frigate events",
description="Retrieve recent events from Frigate camera system",
responses={
200: {"description": "Successfully retrieved events"},
503: {"description": "Frigate service unavailable"}
}
)
📊 Data Layer Best Practices
1. Pydantic Model Design
Clean Model Structure
class ServiceStatus(BaseModel):
name: str
status: str
response_time: Optional[str] = None
last_check: Optional[datetime] = None
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
Proper Type Hints
from typing import List, Dict, Optional, Any
from datetime import datetime
class EventData(BaseModel):
event_type: str
service: str
timestamp: datetime
data: Dict[str, Any]
2. Configuration Management
Environment-Based Configuration
# services/config.py
import os
SERVICES = {
"home_assistant": {
"name": "Home Assistant",
"url": os.getenv("HOME_ASSISTANT_URL", "http://homeassistant.local:8123"),
"token": os.getenv("HOME_ASSISTANT_TOKEN"),
"enabled": os.getenv("HOME_ASSISTANT_ENABLED", "true").lower() == "true"
}
}
Centralized Configuration
- All service configurations in one place
- Environment variable support
- Default values for development
🚀 Performance Optimizations
1. Async/Await Usage
async def fetch_ha_entities() -> List[HAEntity]:
async with httpx.AsyncClient() as client:
response = await client.get(f"{HA_URL}/api/states")
return response.json()
2. Connection Pooling
# services/redis_client.py
class RedisClient:
def __init__(self, redis_url: str):
self.redis_url = redis_url
self.client = None
async def connect(self):
self.client = await aioredis.from_url(self.redis_url)
3. Error Handling
async def safe_api_call(url: str, headers: Dict[str, str]) -> Optional[Dict]:
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers, timeout=5.0)
response.raise_for_status()
return response.json()
except httpx.TimeoutException:
logger.warning(f"Timeout calling {url}")
return None
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error calling {url}: {e}")
return None
🧪 Testing Strategy
1. Unit Testing
import pytest
from unittest.mock import AsyncMock, patch
from services.config import SERVICES
@pytest.mark.asyncio
async def test_get_services():
with patch('services.redis_client.get_redis_client') as mock_redis:
# Test implementation
pass
2. Integration Testing
@pytest.mark.asyncio
async def test_home_assistant_integration():
# Test actual HA API calls
pass
3. Test Structure
# tests/test_home_assistant.py
import pytest
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_get_ha_entities():
response = client.get("/home-assistant/entities")
assert response.status_code == 200
📋 Code Review Checklist
Route Layer
- Single responsibility per route module
- Proper HTTP status codes
- Input validation with Pydantic
- Error handling
- OpenAPI documentation
Service Layer
- Business logic only
- No direct HTTP calls in business logic
- Proper exception handling
- Async/await usage
- Clear function names
Model Layer
- Proper Pydantic models
- Type hints
- Validation constraints
- JSON serialization
Configuration
- Environment variable support
- Default values
- Centralized configuration
- Service-specific settings
🎯 Benefits Achieved
1. Maintainability
- Clear separation of concerns
- Easy to modify and extend
- Consistent patterns throughout
- Self-documenting code
2. Testability
- Isolated modules
- Mockable dependencies
- Clear interfaces
- Testable business logic
3. Performance
- Async/await for non-blocking operations
- Connection pooling
- Efficient error handling
- Resource management
4. Scalability
- Modular architecture
- Easy to add new service integrations
- Horizontal scaling support
- Configuration-driven services
🔮 Future Improvements
Potential Enhancements
- Circuit Breaker Pattern: Fault tolerance
- Retry Logic: Automatic retry with backoff
- Caching: Redis caching for frequently accessed data
- Metrics: Prometheus metrics integration
- Health Checks: Comprehensive health monitoring
Monitoring & Observability
- Logging: Structured logging with correlation IDs
- Tracing: Distributed tracing
- Metrics: Service performance metrics
- Alerts: Service availability alerts
This clean code implementation makes the Service Adapters more maintainable, testable, and scalable while following FastAPI and Python best practices.