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
This commit is contained in:
377
docs/standards/coding.md
Normal file
377
docs/standards/coding.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user