Skip to content

Latest commit

 

History

History
606 lines (444 loc) · 12.8 KB

developer-guide.md

File metadata and controls

606 lines (444 loc) · 12.8 KB

RunRush - Developer Quick Reference

🚀 Quick Start

Setup Development Environment

# Clone repository
git clone https://github.com/your-username/runrush.git
cd runrush

# Create virtual environment
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install dependencies
pip install -r requirements-dev.txt

# Set environment variables
cp .env.example .env
# Edit .env with your settings

# Initialize database
flask init-db

# Run development server
python app.py

Run Tests

# All tests
pytest

# With coverage
pytest --cov=. --cov-report=html

# Specific test file
pytest tests/test_auth.py -v

# Watch mode (requires pytest-watch)
ptw

📁 Project Structure

runrush/
├── app.py                 # Flask application and routes
├── config.py              # Environment-based configuration
├── db.py                  # SQLite/PostgreSQL database access
├── extensions.py          # CSRF, rate limiter, and PIN hashing
├── ml_predictor.py        # Per-user run prediction
├── requirements.txt       # Runtime dependencies
├── requirements-dev.txt   # Development and test dependencies
├── blueprints/            # Route modules
├── models/                # Data models
├── services/              # Application services
├── utils/                 # Shared utilities
├── scripts/               # Maintenance and migration scripts
├── migrations/            # Versioned SQL migrations
├── templates/             # Jinja templates
├── static/                # CSS, JavaScript, and PWA assets
├── tests/                 # Automated test suite
└── docs/                  # Project and deployment documentation

🔧 Common Tasks

Add a New Route

# 1. Create/edit blueprint file
# blueprints/my_feature.py

from flask import Blueprint, render_template
from utils.decorators import login_required

my_feature_bp = Blueprint('my_feature', __name__)

@my_feature_bp.route('/my-route')
@login_required
def my_route():
    return render_template('my_template.html')

# 2. Register blueprint in app.py
from blueprints.my_feature import my_feature_bp
app.register_blueprint(my_feature_bp)

Add a New Service Function

# services/my_service.py

from db import get_db

def my_business_logic(user_id, data):
    """
    Business logic description.
    
    Args:
        user_id: User ID
        data: Input data
        
    Returns:
        tuple: (success: bool, result: any, error: str or None)
    """
    conn = get_db()
    try:
        # Your logic here
        result = conn.execute("SELECT ...").fetchone()
        conn.commit()
        return True, result, None
    except Exception as e:
        return False, None, str(e)
    finally:
        conn.close()

Add Input Validation

# utils/validators.py

def validate_my_input(value):
    """
    Validate my input.
    
    Args:
        value: Value to validate
        
    Returns:
        validated_value: Validated and sanitized value
        
    Raises:
        ValidationError: If validation fails
    """
    if not value:
        raise ValidationError("Value is required")
    
    # Validation logic
    if not isinstance(value, str):
        raise ValidationError("Value must be a string")
    
    return value.strip()

Add a Test

# tests/test_my_feature.py

import pytest

class TestMyFeature:
    """Test my feature."""
    
    def test_my_function(self, app):
        """Test my function."""
        with app.app_context():
            from services.my_service import my_business_logic
            
            success, result, error = my_business_logic(1, {'key': 'value'})
            
            assert success is True
            assert result is not None
            assert error is None

🏋️ Training Load (ACWR)

RunRush includes an Acute:Chronic Workload Ratio (ACWR) feature in the Analytics tab. It compares a runner's recent load (last 7 days) to their longer-term average load (4-week rolling average) to surface spikes or detraining risk.

Endpoint

GET /api/training-load — login required, rate-limited (20 per hour). Returns JSON for the current user only.

Calculation (services/training_load.py)

The module is pure (no Flask or DB imports) and takes runs (list of dicts) and today (a date object) as parameters, so it is fully deterministic and testable.

Formula:

  1. Daily load per run: time_min (duration in minutes). If time_min is missing but distance_km exists, estimate load as distance_km × median_pace (median min/km across the user's runs that have both distance and duration). Runs with neither are skipped.
  2. Aggregate to one load per calendar day (days with no run = 0; multiple runs on the same day are summed).
  3. Acute load = sum of loads over the last 7 days (including today).
  4. Chronic load = sum of loads over the last 28 days ÷ 4 (average weekly load).
  5. ACWR = acute ÷ chronic. Rounded to 2 decimals.

Guardrails:

  • If chronic == 0 or fewer than 28 days exist between the user's first run and today, status is "insufficient_data" — never a number, never a divide-by-zero.
  • today is passed in (copied from streak_service.py's date.today() approach); the function never calls datetime.now() internally.

Zones:

ACWR Zone Guidance
< 0.8 Detraining risk Below usual volume — consider adding an easy run
0.8 – 1.3 Sweet spot Steady load — keep it up
1.3 – 1.5 Caution Slightly elevated — monitor how you feel
> 1.5 High spike Spiked above 4-week average — consider an easier day

Database / route

  • Reads from the existing runs table (date, distance_km, time_min columns). No schema change needed.
  • Query uses the project's param = "%s" if USE_PG else "?" placeholder convention so it works on SQLite and PostgreSQL.
  • Row access uses key access (r["distance_km"]) — never .get() — for sqlite3.Row compatibility.

Limitation

ACWR is a heuristic, not medical advice. It is intended to help runners notice abrupt changes in training load. The UI includes the disclaimer: "Guidance only — not medical advice."


🔐 Security Checklist

When adding new features:

  • Validate all user inputs
  • Sanitize HTML content
  • Use parameterized queries (no string concatenation)
  • Add CSRF token to forms
  • Add rate limiting to sensitive endpoints
  • Use @login_required decorator for protected routes
  • Log security-relevant actions
  • Hash sensitive data (passwords, tokens)
  • Use HTTPS in production
  • Set secure cookie flags

🧪 Testing Guidelines

Test Structure

class TestFeatureName:
    """Test feature description."""
    
    def setup_method(self):
        """Setup before each test."""
        pass
    
    def teardown_method(self):
        """Cleanup after each test."""
        pass
    
    def test_success_case(self, fixture):
        """Test successful operation."""
        # Arrange
        input_data = {'key': 'value'}
        
        # Act
        result = function_to_test(input_data)
        
        # Assert
        assert result == expected_value
    
    def test_error_case(self, fixture):
        """Test error handling."""
        with pytest.raises(ExpectedException):
            function_to_test(invalid_input)

Fixtures

# tests/conftest.py

@pytest.fixture
def my_fixture(app):
    """Create test data."""
    with app.app_context():
        # Setup
        data = create_test_data()
        yield data
        # Teardown
        cleanup_test_data()

🎨 Code Style

Python (PEP 8)

# Good
def calculate_pace(distance_km, time_min):
    """Calculate pace in min/km."""
    if distance_km <= 0:
        raise ValueError("Distance must be positive")
    return time_min / distance_km

# Bad
def calc(d,t):
    return t/d

Docstrings

def function_name(arg1, arg2):
    """
    Short description.
    
    Longer description if needed.
    
    Args:
        arg1: Description of arg1
        arg2: Description of arg2
        
    Returns:
        Description of return value
        
    Raises:
        ExceptionType: When this exception is raised
    """
    pass

Imports

# Standard library
import os
import sys
from datetime import datetime

# Third-party
from flask import Flask, request
import bcrypt

# Local
from models.user import User
from services.auth_service import authenticate_user

🐛 Debugging

Enable Debug Mode

# config.py
class DevelopmentConfig(Config):
    DEBUG = True

Print Debugging

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

logger.debug(f"Variable value: {variable}")
logger.info("Info message")
logger.warning("Warning message")
logger.error("Error message")

Interactive Debugging

# Add breakpoint
import pdb; pdb.set_trace()

# Or use ipdb (better)
import ipdb; ipdb.set_trace()

Flask Shell

flask shell

>>> from services.auth_service import get_current_user
>>> from db import get_db
>>> conn = get_db()
>>> users = conn.execute("SELECT * FROM users").fetchall()
>>> print(users)

📊 Database

Run Migrations

# Initialize database
flask init-db

# Migrate PINs to hashed format
flask migrate-pins

# Create admin user
flask create-admin

Database Console

# SQLite
sqlite3 runs.db

# PostgreSQL
psql $DATABASE_URL

Common Queries

-- List all users
SELECT id, username, role, status FROM users;

-- Count runs per user
SELECT user_id, COUNT(*) as run_count 
FROM runs 
GROUP BY user_id 
ORDER BY run_count DESC;

-- Find users with no runs
SELECT u.id, u.username 
FROM users u 
LEFT JOIN runs r ON u.id = r.user_id 
WHERE r.id IS NULL;

-- Activity in last 7 days
SELECT date, COUNT(*) as runs, SUM(distance_km) as total_km
FROM runs
WHERE date >= date('now', '-7 days')
GROUP BY date
ORDER BY date DESC;

🚀 Deployment

Local Production Test

# Set production config
export FLASK_ENV=production
export SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")

# Run with Gunicorn
gunicorn app:app --bind 0.0.0.0:5000 --workers 4

Render Deployment

# Build command
pip install -r requirements.txt

# Start command
gunicorn app:app

# Environment variables (set in Render dashboard)
SECRET_KEY=...
DATABASE_URL=...  # Auto-set by Render
FLASK_ENV=production
REDIS_URL=...  # If using Redis

Health Check Endpoint

@app.route('/health')
def health():
    """Health check endpoint."""
    return jsonify({
        'status': 'healthy',
        'timestamp': datetime.now().isoformat()
    })

📚 Resources

Documentation

Tools

  • Black (formatter): black .
  • Flake8 (linter): flake8 .
  • MyPy (type checker): mypy .
  • Coverage: pytest --cov=.

🤝 Contributing

  1. Create feature branch: git checkout -b feature/my-feature
  2. Write code + tests
  3. Run tests: pytest
  4. Format code: black .
  5. Commit: git commit -m "Add my feature"
  6. Push: git push origin feature/my-feature
  7. Create Pull Request

💡 Tips & Tricks

Speed Up Tests

# conftest.py
@pytest.fixture(scope='session')
def app():
    """Create app once per test session."""
    app = create_app('testing')
    yield app

Mock External Services

def test_email_sending(mocker):
    """Test email without actually sending."""
    mock_send = mocker.patch('services.email_service.send_email')
    mock_send.return_value = True
    
    result = trigger_email()
    
    assert mock_send.called
    assert result is True

Environment-Specific Config

# Development
export FLASK_ENV=development

# Testing
export FLASK_ENV=testing

# Production
export FLASK_ENV=production

🆘 Troubleshooting

"ModuleNotFoundError"

# Ensure virtual environment is activated
source .venv/bin/activate

# Reinstall dependencies
pip install -r requirements-dev.txt

"CSRF token missing"

<!-- Add to all POST forms -->
<form method="POST">
    {{ csrf_token() }}
    <!-- form fields -->
</form>

"Rate limit exceeded"

# Increase limit in blueprint
@limiter.limit("100 per hour")  # Instead of default
def my_route():
    pass

Tests failing

# Clear pytest cache
pytest --cache-clear

# Run with verbose output
pytest -vv

# Run single test
pytest tests/test_auth.py::TestAuth::test_login -vv

Happy Coding! 🎉