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
377 lines
9.7 KiB
Markdown
377 lines
9.7 KiB
Markdown
# 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:
|
|
```ini
|
|
# .flake8
|
|
[flake8]
|
|
max-line-length = 88
|
|
extend-ignore = E203, W503
|
|
exclude = .git,__pycache__,docs,build,dist
|
|
```
|
|
|
|
- Use **mypy** for static type checking:
|
|
```ini
|
|
# 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
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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`:
|
|
|
|
```python
|
|
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
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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:
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:
|
|
```python
|
|
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
|
|
```python
|
|
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:
|
|
```python
|
|
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:
|
|
```python
|
|
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:
|
|
```python
|
|
"""
|
|
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
|
|
```python
|
|
# 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 |