Backend Development Guidelines
January 10, 2026 Β· View on GitHub
π Python Backend Development Standards
This document details the development standards and best practices for the LifeTrace project backend (Python + FastAPI).
Tech Stack
- Framework: FastAPI + Uvicorn (async web framework)
- Language: Python 3.12
- ORM: SQLAlchemy 2.x + SQLModel
- Database Migration: Alembic
- Data Validation: Pydantic 2.x
- Configuration Management: Dynaconf (supports YAML hot reload)
- Logging: Loguru
- Scheduler: APScheduler (background task scheduling)
- OCR: RapidOCR (local OCR recognition)
- Vector Database: ChromaDB (optional, for semantic search)
- Text Embeddings: sentence-transformers (optional)
- LLM: OpenAI-compatible API
- Package Manager: uv (recommended)
- Code Quality: Ruff (lint/format/check)
π Table of Contents
- Code Style
- Project Architecture
- Project Structure
- Naming Conventions
- Type Annotations
- Docstrings
- Error Handling
- API Design
- Layered Architecture
- Database Operations
- Configuration Management
- LLM Services
- Background Tasks
- Testing
- Logging
- Performance
- Security
- API and Frontend Interaction
- Dependency Management
π¨ Code Style
PEP 8 Standard
We follow the PEP 8 Python code style guide.
Using Ruff
The project uses Ruff as the linter and formatter.
# Check code
uv run ruff check .
# Auto-fix issues
uv run ruff check --fix .
# Format code
uv run ruff format .
Basic Rules
Indentation
# β
Correct: Use 4 spaces
def my_function():
if condition:
do_something()
# β Wrong: Use tabs
def my_function():
if condition:
do_something()
Line Length
# β
Correct: Maximum 100 characters per line
def calculate_result(
param1: int, param2: str, param3: float
) -> dict[str, Any]:
return {"result": param1}
# β Wrong: Line too long
def calculate_result(param1: int, param2: str, param3: float, param4: dict, param5: list) -> dict[str, Any]:
return {"result": param1}
File Length Limits
To maintain code maintainability and readability, we provide the following guidelines for Python file length:
Code Length Guidelines:
- Recommended Standard: Keep single files under 500 lines
- Warning Threshold: Consider refactoring when exceeding 700 lines
- Review Required: Files over 1000 lines must include justification and refactoring plan in PR
Refactoring Guidelines:
- Single function should not exceed 50 statements
- Single class should not exceed 400 lines
- Cyclomatic complexity must not exceed 15
- Prioritize functional cohesion over strict line count limits
# β
Correct: Moderate file length (~450 lines)
# task_service.py
class TaskService:
"""Task service containing complete task business logic."""
def create_task(self, data: dict) -> Task:
"""Create a task."""
pass
def update_task(self, task_id: int, data: dict) -> Task:
"""Update a task."""
pass
# ... other related methods
# β οΈ Needs Review: Files over 1000 lines
# complex_processor.py (1200 lines)
# PR must explain:
# 1. Why is this file so long?
# 2. Can it be split? How?
# 3. If not, what's the refactoring plan?
# β Wrong: Function too complex (over 50 statements)
def process_data(data):
# ... 100 lines of code
pass
When to Split Files:
-
Multiple Responsibilities: A file handles multiple unrelated responsibilities
# Before: user_operations.py (800 lines) # Contains: user management, permissions, data export, email notifications # After: users/ βββ manager.py # User management βββ permissions.py # Permission validation βββ export_service.py # Data export βββ notifications.py # Email notifications -
Large Classes: A single class exceeds 400 lines
# Before: task_handler.py (600 lines) class TaskHandler: # Contains: CRUD, validation, statistics, reporting # After: tasks/ βββ manager.py # TaskManager - CRUD operations βββ validator.py # TaskValidator - Data validation βββ stats.py # TaskStats - Statistics βββ reporter.py # TaskReporter - Report generation -
Long Functions: A single function exceeds 50 statements, extract sub-functions
# β Wrong: Function too long def process_order(order_data): # Validate data (20 lines) # Calculate price (30 lines) # Create order (25 lines) # Send notification (15 lines) pass # Total 90 lines # β Correct: Split into multiple functions def process_order(order_data): validated_data = validate_order_data(order_data) price = calculate_order_price(validated_data) order = create_order(validated_data, price) send_order_notification(order) return order
Imports
# β
Correct: Import order and grouping
# 1. Standard library imports
import os
import sys
from datetime import datetime
from typing import Any, Optional
# 2. Third-party library imports
import numpy as np
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
# 3. Local application/library imports
from lifetrace.storage.database import get_db
from lifetrace.schemas.task import TaskCreate, TaskResponse
# β Wrong: Mixed import order
from lifetrace.storage.database import get_db
import os
from fastapi import APIRouter
Quotes
# β
Correct: Use double quotes
message = "Hello, World!"
query = "SELECT * FROM users"
# β
Correct: Triple quotes for docstrings
description = """
This is a multi-line string.
"""
ποΈ Project Architecture
Layered Architecture
The project follows a layered architecture pattern:
Router Layer (routers/) β HTTP request handling, parameter validation
β
Service Layer (services/) β Business logic, orchestration of multiple Repository operations
β
Repository Layer (repositories/) β Data access abstraction, database query encapsulation
β
Storage Layer (storage/) β SQLAlchemy ORM model definitions
Layer Responsibilities:
- Router Layer: Handle HTTP requests, parameter validation, call Service layer
- Service Layer: Business logic, orchestrate multiple Repository operations
- Repository Layer: Data access abstraction, encapsulate database queries
- Schema Layer: Request/response Pydantic models
- Storage Layer: SQLAlchemy ORM model definitions
ποΈ Project Structure
Directory Organization
lifetrace/
βββ server.py # FastAPI application entry
βββ config/ # Configuration files
β βββ config.yaml # User configuration
β βββ default_config.yaml # Default configuration
β βββ prompt.yaml # LLM Prompt templates
βββ routers/ # API routes (Router layer)
βββ services/ # Business services (Service layer)
βββ repositories/ # Data access (Repository layer)
βββ schemas/ # Pydantic data models
βββ storage/ # Data storage layer
β βββ models.py # SQLAlchemy models (database tables)
β βββ *_manager.py # Data managers
βββ llm/ # LLM and AI services
βββ jobs/ # Background tasks
βββ core/ # Core dependencies and lazy-loaded services
βββ util/ # Utility functions
π Naming Conventions
Variables and Functions
# β
Correct: snake_case
user_name = "Alice"
user_age = 25
def get_user_profile(user_id: int):
pass
# β Wrong: camelCase
userName = "Alice"
def getUserProfile(userId: int):
pass
Classes
# β
Correct: PascalCase
class UserManager:
pass
class TaskScheduler:
pass
# β Wrong: snake_case
class user_manager:
pass
Constants
# β
Correct: UPPER_CASE
MAX_RETRY_COUNT = 3
DEFAULT_TIMEOUT = 30
API_BASE_URL = "https://api.example.com"
# β Wrong: lowercase
max_retry_count = 3
π€ Type Annotations
Basic Type Annotations
# β
Correct: Add type annotations
def greet(name: str) -> str:
return f"Hello, {name}!"
def add_numbers(a: int, b: int) -> int:
return a + b
def get_user(user_id: int) -> dict | None:
return None
# β Wrong: No type annotations
def greet(name):
return f"Hello, {name}!"
Collection Types
# Python 3.9+: Use built-in types
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# Type aliases
from typing import TypeAlias
UserID: TypeAlias = int
UserData: TypeAlias = dict[str, Any]
def get_user_data(user_id: UserID) -> UserData:
return {"id": user_id, "name": "Alice"}
Pydantic Models
from pydantic import BaseModel, Field
class User(BaseModel):
"""User model."""
id: int
name: str = Field(..., min_length=1, max_length=100)
email: str
age: Optional[int] = Field(None, ge=0, le=150)
is_active: bool = True
class Config:
from_attributes = True
π Docstrings
Function Docstrings
def create_task(
title: str,
description: str | None = None,
project_id: int | None = None
) -> Task:
"""
Create a new task.
Args:
title: Task title, required and non-empty
description: Task description, optional
project_id: Associated project ID, optional
Returns:
Task: Created task object
Raises:
ValueError: If title is empty
DatabaseError: If database operation fails
Example:
>>> task = create_task("Complete docs", "Write API docs", 1)
>>> print(task.title)
Complete docs
"""
if not title:
raise ValueError("Task title cannot be empty")
# Implementation...
return task
Class Docstrings
class TaskManager:
"""
Task manager.
Provides CRUD operations and advanced query functionality for tasks.
Attributes:
db: Database session object
logger: Logger instance
Example:
>>> manager = TaskManager(db_session)
>>> task = await manager.create_task(task_data)
"""
def __init__(self, db: AsyncSession):
"""
Initialize task manager.
Args:
db: Async database session
"""
self.db = db
π¨ Error Handling
Exception Handling
from fastapi import HTTPException
# β
Correct: Catch specific exceptions
async def get_task(task_id: int) -> Task:
try:
task = await task_manager.get_task(task_id)
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
return task
except DatabaseError as e:
logger.error(f"Database error: {e}")
raise HTTPException(status_code=500, detail="Database operation failed")
# β Wrong: Catch all exceptions
async def get_task(task_id: int) -> Task:
try:
task = await task_manager.get_task(task_id)
return task
except Exception as e: # Too broad
raise HTTPException(status_code=500, detail="Error occurred")
π API Design
RESTful API Standards
from fastapi import APIRouter, Depends, Query, Path
from lifetrace.repositories.task_repository import TaskRepository
from lifetrace.services.task_service import TaskService
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
# β
Correct: RESTful route design
@router.get("/", response_model=list[TaskResponse])
async def list_tasks(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
status: Optional[str] = Query(None),
task_service: TaskService = Depends(get_task_service)
):
"""List tasks."""
return await task_service.list_tasks(skip=skip, limit=limit, status=status)
@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(
task_id: int = Path(..., gt=0),
task_service: TaskService = Depends(get_task_service)
):
"""Get specific task."""
return await task_service.get_task(task_id)
@router.post("/", response_model=TaskResponse, status_code=201)
async def create_task(
task: TaskCreate,
task_service: TaskService = Depends(get_task_service)
):
"""Create new task."""
return await task_service.create_task(task)
@router.put("/{task_id}", response_model=TaskResponse)
async def update_task(
task_id: int = Path(..., gt=0),
task: TaskUpdate = None,
task_service: TaskService = Depends(get_task_service)
):
"""Update task."""
return await task_service.update_task(task_id, task)
@router.delete("/{task_id}", status_code=204)
async def delete_task(
task_id: int = Path(..., gt=0),
task_service: TaskService = Depends(get_task_service)
):
"""Delete task."""
await task_service.delete_task(task_id)
Registering Routes
Import and register new routes in server.py:
from lifetrace.routers import tasks
app.include_router(tasks.router)
ποΈ Layered Architecture
Router Layer
Handle HTTP requests, parameter validation, call Service layer:
# routers/tasks.py
from fastapi import APIRouter, Depends
from lifetrace.services.task_service import TaskService
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
@router.get("/", response_model=list[TaskResponse])
async def list_tasks(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
task_service: TaskService = Depends(get_task_service)
):
"""List tasks."""
return await task_service.list_tasks(skip=skip, limit=limit)
Service Layer
Implement complex business logic, orchestrate multiple Repository operations:
# services/task_service.py
from lifetrace.repositories.task_repository import TaskRepository
from lifetrace.schemas.task import TaskCreate, TaskUpdate
class TaskService:
"""Task service."""
def __init__(self, task_repository: TaskRepository):
self.task_repository = task_repository
async def create_task(self, task_data: TaskCreate) -> Task:
"""Create task with business logic."""
# Business validation
if len(task_data.title) > 200:
raise ValueError("Task title too long")
# Orchestrate repository operations
return await self.task_repository.create(task_data)
Repository Layer
Data access abstraction, encapsulate database queries:
# repositories/task_repository.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from lifetrace.storage.models import Task
class TaskRepository:
"""Task repository."""
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, task_id: int) -> Task | None:
"""Get task by ID."""
result = await self.db.execute(
select(Task).where(Task.id == task_id)
)
return result.scalar_one_or_none()
async def create(self, task_data: TaskCreate) -> Task:
"""Create task."""
task = Task(**task_data.model_dump())
self.db.add(task)
await self.db.commit()
await self.db.refresh(task)
return task
βοΈ Configuration Management
Configuration File Structure
config/default_config.yaml- Default configuration (do not modify)config/config.yaml- User configuration (overrides default values)- Uses Dynaconf for configuration hot reload
Accessing Configuration
Access via settings object in util/settings.py:
from lifetrace.util.settings import settings
# Access nested configuration
port = settings.server.port
# Access with default value
timeout = settings.get("timeout", default=30)
Configuration Hot Reload
The following configurations support hot reload (no restart required):
- LLM configuration
- Recorder configuration
- OCR configuration
πΎ Database Operations
SQLAlchemy Models
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text
from sqlalchemy.orm import relationship
class Task(Base):
"""Task model."""
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False, index=True)
description = Column(Text, nullable=True)
status = Column(String(50), nullable=False, default="pending", index=True)
priority = Column(Integer, nullable=False, default=0)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
project = relationship("Project", back_populates="tasks")
Database Migrations
The project uses Alembic for database migrations:
- Config file:
alembic.ini - Migration scripts:
migrations/versions/
Common commands:
alembic revision --autogenerate -m "description"- Generate migration scriptalembic upgrade head- Apply all migrationsalembic downgrade -1- Rollback one versionalembic history- View migration history
Database Queries
Use Repository layer for database queries (see Repository Layer above).
π§ͺ Testing
Unit Tests
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from lifetrace.schemas.task import TaskCreate
from lifetrace.storage.task_manager import TaskManager
@pytest.mark.asyncio
async def test_create_task(db_session: AsyncSession):
"""Test task creation."""
manager = TaskManager(db_session)
task_data = TaskCreate(title="Test Task")
task = await manager.create_task(task_data)
assert task.id is not None
assert task.title == "Test Task"
assert task.status == "pending"
π€ LLM Services
LLM Client Usage
The project uses OpenAI-compatible API, wrapped in llm/llm_client.py:
- Supports Alibaba Cloud Tongyi Qianwen, OpenAI, Claude, etc.
- Configuration managed in
config/config.yamlunderllmsection - Supports streaming responses (SSE)
RAG Service
llm/rag_service.py provides Retrieval-Augmented Generation:
- Intelligent time parsing (e.g., "last week", "yesterday")
- Hybrid retrieval strategy (vector retrieval + full-text search)
- Context compression and ranking
Prompt Management
Prompt templates are stored in config/prompt.yaml:
- Use YAML format for easy maintenance
- Support variable interpolation
- Organized by feature modules
β° Background Tasks
Task Scheduling
Use APScheduler to manage background tasks:
- Tasks defined in
lifetrace/jobs/directory - Managed through
job_manager.py - Supports scheduled tasks and interval tasks
Task Types
- recorder: Screen recorder, scheduled screenshots
- ocr: OCR processor, processes screenshots waiting for recognition
π Logging
Use Loguru for logging, import logger from util/logging_config.py:
from lifetrace.util.logging_config import logger
class TaskService:
"""Task service."""
async def create_task(self, task_data: TaskCreate) -> Task:
"""Create task."""
logger.info(f"Creating task: {task_data.title}")
try:
task = Task(**task_data.model_dump())
self.db.add(task)
await self.db.commit()
await self.db.refresh(task)
logger.info(f"Task created successfully: ID={task.id}")
return task
except Exception as e:
logger.error(f"Failed to create task: {e}")
await self.db.rollback()
raise
Logging Guidelines
- Must log critical operations
- Must log complete stack trace for exceptions
- Must sanitize sensitive information (API Keys, etc.)
- Use structured logging for easy analysis
β‘ Performance
Query Optimization
# β
Correct: Use eager loading
from sqlalchemy.orm import selectinload
async def get_tasks_with_projects(self) -> list[Task]:
"""Get tasks with their associated projects."""
result = await self.db.execute(
select(Task).options(selectinload(Task.project))
)
return list(result.scalars().all())
# β
Correct: Batch insert
async def create_tasks_batch(self, tasks_data: list[TaskCreate]) -> list[Task]:
"""Create multiple tasks."""
tasks = [Task(**data.model_dump()) for data in tasks_data]
self.db.add_all(tasks)
await self.db.commit()
return tasks
π Security
Input Validation
# β
Correct: Use Pydantic for validation
from pydantic import BaseModel, Field, field_validator
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=2000)
@field_validator("title")
@classmethod
def validate_title(cls, v: str) -> str:
if "<script>" in v.lower():
raise ValueError("Title contains illegal characters")
return v
SQL Injection Prevention
# β
Correct: Use parameterized queries (SQLAlchemy handles this)
task = await self.db.execute(
select(Task).where(Task.id == task_id)
)
# β Wrong: String concatenation (vulnerable to SQL injection)
query = f"SELECT * FROM tasks WHERE id = {task_id}"
π‘ API and Frontend Interaction
Naming Style Conversion
Backend uses snake_case, frontend uses camelCase:
- Frontend fetcher automatically performs conversion
- Backend Schema uniformly uses
snake_case - OpenAPI Schema automatically generated by FastAPI
Frontend Code Generation
Frontend uses Orval to auto-generate API code from OpenAPI Schema:
- After backend API changes, frontend runs
pnpm orvalto regenerate - Ensure OpenAPI Schema is complete and accurate
π¦ Dependency Management
Using uv
The project uses uv as the package manager:
uv sync- Sync dependenciesuv add <package>- Add dependencyuv remove <package>- Remove dependencyuv run <command>- Run command in virtual environment
Dependency Groups
- Main dependencies:
dependenciesinpyproject.toml - Development dependencies:
dependency-groups.dev - Optional dependencies:
dependency-groups.vector(vector search functionality)
β Code Review Checklist
Before submitting code, ensure:
- Code follows PEP 8 style guide
-
uv run ruff check .passes with no errors -
uv run ruff format .applied - All functions have type annotations
- Public functions have docstrings
- Proper error handling added
- Parameterized queries used (no SQL injection)
- Appropriate logging added
- Unit tests written
- Tests pass
- Documentation updated
- API changes reflected in OpenAPI Schema
- Follows layered architecture (Router β Service β Repository)
- Configuration supports hot reload (if applicable)
- Background tasks properly scheduled
Happy Coding! π