This file contains rules and conventions for AI coding assistants working on this API server project.
All service classes must follow the standard singleton pattern using @lru_cache.
from functools import lru_cache
class YourService:
"""Service description."""
def __init__(self):
self.dependency = get_dependency_service()
@lru_cache
def get_your_service() -> YourService:
"""Get or create the singleton YourService instance.
Returns:
The YourService instance.
"""
return YourService()- Use
@lru_cachedecorator on the getter function - Function returns service instance (not the class)
- Standard docstring with "singleton"
- No global variables for service instances
- Initialize dependencies in
__init__using their getter functions
HealthCheckService-get_health_check_service()PatientService-get_patient_service()AddressService-get_address_service()
Use loguru's {} placeholder format instead of f-strings for all logging calls (e.g., logger.debug("Processing {}", value) not logger.debug(f"Processing {value}")).
This is loguru's native format and provides lazy evaluation — the string is only formatted if the message will actually be emitted. With f-strings, Python evaluates the string before the logging call, wasting CPU when the log level is disabled.
logger.info("User {} logged in from {}", username, ip_address)
logger.error("Failed to process request: {}", error)logger.info(f"User {username} logged in from {ip_address}")
logger.error(f"Failed to process request: {error}")Avoid catching broad Exception - use specific exception types.
try:
service.create_resource(session, input_data)
except (ResourceNotFoundError, VersionConflictError, ValueError, RuntimeError) as e:
logger.warning("Failed to create resource: {}", e)
return Falsetry:
service.create_resource(session, input_data)
except Exception as e: # Too broad
logger.warning(f"Failed to create resource: {e}")
return False- Application:
ResourceNotFoundError,VersionConflictError(fromexceptions.py) - Validation:
ValueError,TypeError - Runtime:
RuntimeError,AttributeError - Database:
SQLAlchemyError
Services should declare dependencies in __init__ and use getter functions.
class YourService:
def __init__(self):
self.health_service = get_health_check_service()
self.patient_service = get_patient_service()Core services must be registered in services/di.py using getter functions.
def register_core_services(registry: ServiceRegistry) -> None:
registry.register_factory(YourService, get_your_service)Use get_*_service() in services and checks:
class YourService:
def __init__(self):
self.patient_service = get_patient_service()Use registry.get() in FastAPI/GraphQL contexts:
# API endpoints, GraphQL resolvers
def some_endpoint(registry: ServiceRegistry = Depends(get_service_registry)):
service = registry.get(YourService)Always use arrow.py for date/time operations.
import arrow
# Use arrow for all datetime operations
current_time = arrow.utcnow().datetime
created_at = arrow.utcnow()
formatted = arrow.get(created_at).format('YYYY-MM-DD HH:mm:ss')- Use
arrow.utcnow().datetimefor all timestamp creation - Use arrow for date/time arithmetic (shifting, formatting, parsing)
- Always store UTC in database, convert to local time only for display
- Use ISO format for API responses when possible
from datetime import datetime
user.created_at = datetime.utcnow() # Deprecated since Python 3.12!Clean separation between database schema and API contracts using three model layers:
- Purpose: Shared fields and common functionality
- Usage: Inherited by database models and API models
- Contains: Audit fields, common constraints, shared validation
- Example:
PatientBasewithpatient_id,first_name,last_name
- Purpose: Database table definitions with SQLModel
- Usage: Database operations, migrations, ORM queries
- Contains: Primary keys, foreign keys, indexes, table-specific fields
- Example:
class Patient(PatientBase, table=True)withid: UUID = Field(primary_key=True)
- Purpose: Request/response validation and serialization
- Usage: FastAPI endpoints, API documentation, client contracts
- Contains: Input validation, response formatting, API-specific fields
- Examples:
PatientCreateInput- POST request bodyPatientResponse- GET response bodyPatientInput- PUT request body
- Separation of Concerns: Database schema != API contract
- Security: API models expose only necessary fields
- Flexibility: Can evolve API independently of database
- Validation: Different validation rules for different contexts
- Documentation: Auto-generated OpenAPI specs from API models
Follow the established patterns in these modules:
models/base_model.py- Base model definitionsmodels/db_model.py- Database table modelsmodels/api_model.py- API models usingcreate_modelutilityutils/model_builder.py-create_modelimplementation
Use create_model utility for API models to ensure consistency and maintain DRY principles.
The server provides two CLI entry points:
-
api-server- Main server with subcommands:api-server(default: starts server)api-server run(explicit: starts server)api-server check(readiness checks only)api-server cli db check(admin CLI as subgroup)
-
api-server-cli- Standalone admin CLI:api-server-cli db checkapi-server-cli db upgrade
- Create a new file in
cli/commands/with atyper.Typer()app - Register it in
cli/app.pywithapp.add_typer() - Export it in
cli/commands/__init__.py
CLI operations use their own pipeline builders (in cli/checks/pipeline_builders.py)
separate from the server's pipeline builders. This allows CLI-specific check combinations
(e.g., basic checks before migration, full checks after).
New readiness checks should extend ReadinessCheck:
from api_server.readiness_pipeline import ReadinessCheck, ReadinessCheckResult
class YourCheck(ReadinessCheck):
def __init__(self, name="your_check", is_critical=False, run_once=False):
super().__init__(name, is_critical, run_once)
def _execute(self) -> ReadinessCheckResult:
# Perform check
if success:
return self.success("Check passed", {"detail": "value"})
return self.failed("Check failed", {"error": "reason"})Use constants from constants.py for stage names:
from api_server.constants import STAGE_DATABASE