Files
Gondulf/docs/standards/coding.md
Phil Skentelbery 6d21442705 chore: initialize gondulf project structure
Set up Python project with uv environment management and FastAPI stack.

Project structure:
- src/gondulf/ - Main application package
- tests/ - Test suite directory
- pyproject.toml - Project configuration with dependencies
- README.md - Project documentation
- uv.lock - Dependency lock file

Dependencies configured:
- FastAPI + Uvicorn for web framework
- SQLAlchemy for database ORM
- pytest + coverage for testing
- ruff, black, mypy, flake8 for code quality
- Development environment using uv direct execution model

All project standards reviewed and implemented per:
- /docs/standards/coding.md
- /docs/standards/testing.md
- /docs/standards/git.md
- /docs/standards/development-environment.md
- /docs/standards/versioning.md
2025-11-20 10:42:10 -07:00

9.7 KiB

Python Coding Standard

Overview

This document defines coding standards for the IndieAuth server implementation in Python. The primary goal is maintainability and clarity over cleverness.

Python Version

  • Target: Python 3.10+ (for modern type hints and async support)
  • Use only stable language features
  • Avoid deprecated patterns

Code Style

Formatting

  • Use Black for automatic code formatting (line length: 88)
  • Use isort for import sorting
  • No manual formatting - let tools handle it

Linting

  • Use flake8 with the following configuration:
# .flake8
[flake8]
max-line-length = 88
extend-ignore = E203, W503
exclude = .git,__pycache__,docs,build,dist
  • Use mypy for static type checking:
# mypy.ini
[mypy]
python_version = 3.10
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True

Project Structure

indieauth/
├── __init__.py
├── main.py                 # Application entry point
├── config.py               # Configuration management
├── models/                 # Data models
│   ├── __init__.py
│   ├── client.py
│   ├── token.py
│   └── user.py
├── endpoints/              # HTTP endpoint handlers
│   ├── __init__.py
│   ├── authorization.py
│   ├── token.py
│   └── registration.py
├── services/               # Business logic
│   ├── __init__.py
│   ├── auth_service.py
│   ├── token_service.py
│   └── client_service.py
├── storage/                # Data persistence
│   ├── __init__.py
│   ├── base.py
│   └── sqlite.py
├── utils/                  # Utility functions
│   ├── __init__.py
│   ├── crypto.py
│   └── validation.py
└── exceptions.py           # Custom exceptions

Naming Conventions

General Rules

  • Use descriptive names - clarity over brevity
  • Avoid abbreviations except well-known ones (url, id, db)
  • Use American English spelling

Specific Conventions

  • Modules: lowercase_with_underscores.py
  • Classes: PascalCase
  • Functions/Methods: lowercase_with_underscores()
  • Constants: UPPERCASE_WITH_UNDERSCORES
  • Private: Prefix with single underscore _private_method()
  • Internal: Prefix with double underscore __internal_var

Examples

# Good
class ClientRegistration:
    MAX_REDIRECT_URIS = 10

    def validate_redirect_uri(self, uri: str) -> bool:
        pass

# Bad
class client_reg:  # Wrong case
    maxURIs = 10    # Wrong case, abbreviation

    def checkURI(self, u):  # Unclear naming, missing types
        pass

Type Hints

Type hints are mandatory for all functions and methods:

from typing import Optional, List, Dict, Union
from datetime import datetime

def generate_token(
    client_id: str,
    scope: Optional[str] = None,
    expires_in: int = 3600
) -> Dict[str, Union[str, int, datetime]]:
    """Generate an access token for the client."""
    pass

Docstrings

Use Google-style docstrings for all public modules, classes, and functions:

def exchange_code(
    code: str,
    client_id: str,
    code_verifier: Optional[str] = None
) -> Token:
    """
    Exchange authorization code for access token.

    Args:
        code: The authorization code received from auth endpoint
        client_id: The client identifier
        code_verifier: PKCE code verifier if PKCE was used

    Returns:
        Access token with associated metadata

    Raises:
        InvalidCodeError: If code is invalid or expired
        InvalidClientError: If client_id doesn't match code
        PKCERequiredError: If PKCE is required but not provided
    """

Error Handling

Custom Exceptions

Define specific exceptions in exceptions.py:

class IndieAuthError(Exception):
    """Base exception for IndieAuth errors."""
    pass

class InvalidClientError(IndieAuthError):
    """Raised when client authentication fails."""
    pass

class InvalidTokenError(IndieAuthError):
    """Raised when token validation fails."""
    pass

Error Handling Pattern

# Good - Specific exception handling
try:
    token = validate_token(bearer_token)
except InvalidTokenError as e:
    logger.warning(f"Token validation failed: {e}")
    return error_response(401, "invalid_token")
except Exception as e:
    logger.error(f"Unexpected error during validation: {e}")
    return error_response(500, "internal_error")

# Bad - Catching all exceptions
try:
    token = validate_token(bearer_token)
except:  # Never use bare except
    return error_response(400, "error")

Logging

Use the standard logging module:

import logging

logger = logging.getLogger(__name__)

class TokenService:
    def create_token(self, client_id: str) -> str:
        logger.debug(f"Creating token for client: {client_id}")
        token = self._generate_token()
        logger.info(f"Token created for client: {client_id}")
        return token

Logging Levels

  • DEBUG: Detailed diagnostic information
  • INFO: General informational messages
  • WARNING: Warning messages for potentially harmful situations
  • ERROR: Error messages for failures
  • CRITICAL: Critical problems that require immediate attention

Sensitive Data

Never log sensitive data:

# Bad
logger.info(f"User logged in with password: {password}")
logger.debug(f"Generated token: {access_token}")

# Good
logger.info(f"User logged in: {user_id}")
logger.debug(f"Token generated for client: {client_id}")

Configuration Management

Use environment variables for configuration:

# config.py
import os
from typing import Optional

class Config:
    """Application configuration."""

    # Required settings
    SECRET_KEY: str = os.environ["INDIEAUTH_SECRET_KEY"]
    DATABASE_URL: str = os.environ["INDIEAUTH_DATABASE_URL"]

    # Optional settings with defaults
    TOKEN_EXPIRY: int = int(os.getenv("INDIEAUTH_TOKEN_EXPIRY", "3600"))
    RATE_LIMIT: int = int(os.getenv("INDIEAUTH_RATE_LIMIT", "100"))
    DEBUG: bool = os.getenv("INDIEAUTH_DEBUG", "false").lower() == "true"

    @classmethod
    def validate(cls) -> None:
        """Validate configuration on startup."""
        if not cls.SECRET_KEY:
            raise ValueError("INDIEAUTH_SECRET_KEY must be set")
        if len(cls.SECRET_KEY) < 32:
            raise ValueError("INDIEAUTH_SECRET_KEY must be at least 32 characters")

Dependency Management

Requirements Files

requirements.txt         # Production dependencies only
requirements-dev.txt     # Development dependencies (includes requirements.txt)
requirements-test.txt    # Test dependencies (includes requirements.txt)

Dependency Principles

  • Pin exact versions in requirements.txt
  • Minimize dependencies - prefer standard library
  • Audit dependencies for security vulnerabilities
  • Document why each dependency is needed

Security Practices

Input Validation

Always validate and sanitize input:

from urllib.parse import urlparse

def validate_redirect_uri(uri: str) -> bool:
    """Validate that redirect URI is safe."""
    parsed = urlparse(uri)

    # Must be absolute URI
    if not parsed.scheme or not parsed.netloc:
        return False

    # Must be HTTPS in production
    if not DEBUG and parsed.scheme != "https":
        return False

    # Prevent open redirects
    if parsed.netloc in BLACKLISTED_DOMAINS:
        return False

    return True

Secrets Management

import secrets

def generate_token() -> str:
    """Generate cryptographically secure token."""
    return secrets.token_urlsafe(32)

def constant_time_compare(a: str, b: str) -> bool:
    """Compare strings in constant time to prevent timing attacks."""
    return secrets.compare_digest(a, b)

Performance Considerations

Async/Await

Use async for I/O operations when beneficial:

async def verify_client(client_id: str) -> Optional[Client]:
    """Verify client exists and is valid."""
    client = await db.get_client(client_id)
    if client and not client.is_revoked:
        return client
    return None

Caching

Cache expensive operations appropriately:

from functools import lru_cache

@lru_cache(maxsize=128)
def get_client_metadata(client_id: str) -> dict:
    """Fetch and cache client metadata."""
    # Expensive operation
    return fetch_client_metadata(client_id)

Module Documentation

Each module should have a header docstring:

"""
Authorization endpoint implementation.

This module handles the OAuth 2.0 authorization endpoint as specified
in the IndieAuth specification. It processes authorization requests,
validates client information, and generates authorization codes.
"""

Comments

When to Comment

  • Complex algorithms or business logic
  • Workarounds or non-obvious solutions
  • TODO items with issue references
  • Security-critical code sections

Comment Style

# Good comments explain WHY, not WHAT

# Bad - Explains what the code does
counter = counter + 1  # Increment counter

# Good - Explains why
counter = counter + 1  # Track attempts for rate limiting

# Security-critical sections need extra attention
# SECURITY: Validate redirect_uri to prevent open redirect attacks
# See: https://owasp.org/www-project-web-security-testing-guide/
if not validate_redirect_uri(redirect_uri):
    raise SecurityError("Invalid redirect URI")

Code Organization Principles

  1. Single Responsibility: Each module/class/function does one thing
  2. Dependency Injection: Pass dependencies, don't hard-code them
  3. Composition over Inheritance: Prefer composition for code reuse
  4. Fail Fast: Validate input early and fail with clear errors
  5. Explicit over Implicit: Clear interfaces over magic behavior