# 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** ```python # 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** ```python # 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** ```python # 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** ```python # 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** ```python # 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** ```python @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** ```python 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** ```python 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** ```python @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** ```python # 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** ```python @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** ```python # services/redis_client.py async def get_redis_client() -> RedisClient: redis = RedisClient(REDIS_URL) await redis.connect() return redis ``` #### **Service Configuration** ```python # services/config.py def get_service_config(service_name: str) -> Dict[str, Any]: return SERVICES.get(service_name, {}) ``` ### **3. OpenAPI Documentation** #### **Comprehensive API Documentation** ```python 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** ```python @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** ```python 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** ```python 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** ```python # 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** ```python 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** ```python # 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** ```python 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** ```python 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** ```python @pytest.mark.asyncio async def test_home_assistant_integration(): # Test actual HA API calls pass ``` ### **3. Test Structure** ```python # 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** 1. **Circuit Breaker Pattern**: Fault tolerance 2. **Retry Logic**: Automatic retry with backoff 3. **Caching**: Redis caching for frequently accessed data 4. **Metrics**: Prometheus metrics integration 5. **Health Checks**: Comprehensive health monitoring ### **Monitoring & Observability** 1. **Logging**: Structured logging with correlation IDs 2. **Tracing**: Distributed tracing 3. **Metrics**: Service performance metrics 4. **Alerts**: Service availability alerts This clean code implementation makes the Service Adapters more maintainable, testable, and scalable while following FastAPI and Python best practices.