# 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# All tests
pytest
# With coverage
pytest --cov=. --cov-report=html
# Specific test file
pytest tests/test_auth.py -v
# Watch mode (requires pytest-watch)
ptwrunrush/
├── 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
# 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)# 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()# 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()# 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 NoneRunRush 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.
GET /api/training-load — login required, rate-limited (20 per hour). Returns JSON for the current user only.
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:
- Daily load per run:
time_min(duration in minutes). Iftime_minis missing butdistance_kmexists, estimate load asdistance_km × median_pace(median min/km across the user's runs that have both distance and duration). Runs with neither are skipped. - Aggregate to one load per calendar day (days with no run = 0; multiple runs on the same day are summed).
- Acute load = sum of loads over the last 7 days (including today).
- Chronic load = sum of loads over the last 28 days ÷ 4 (average weekly load).
- ACWR = acute ÷ chronic. Rounded to 2 decimals.
Guardrails:
- If
chronic == 0or fewer than 28 days exist between the user's first run andtoday, status is"insufficient_data"— never a number, never a divide-by-zero. todayis passed in (copied fromstreak_service.py'sdate.today()approach); the function never callsdatetime.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 |
- Reads from the existing
runstable (date,distance_km,time_mincolumns). 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()— forsqlite3.Rowcompatibility.
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."
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_requireddecorator for protected routes - Log security-relevant actions
- Hash sensitive data (passwords, tokens)
- Use HTTPS in production
- Set secure cookie flags
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)# 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()# 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/ddef 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# 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# config.py
class DevelopmentConfig(Config):
DEBUG = Trueimport 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")# Add breakpoint
import pdb; pdb.set_trace()
# Or use ipdb (better)
import ipdb; ipdb.set_trace()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)# Initialize database
flask init-db
# Migrate PINs to hashed format
flask migrate-pins
# Create admin user
flask create-admin# SQLite
sqlite3 runs.db
# PostgreSQL
psql $DATABASE_URL-- 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;# 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# 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@app.route('/health')
def health():
"""Health check endpoint."""
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat()
})- Flask: https://flask.palletsprojects.com/
- Pytest: https://docs.pytest.org/
- Bcrypt: https://github.com/pyca/bcrypt/
- Flask-WTF: https://flask-wtf.readthedocs.io/
- Flask-Limiter: https://flask-limiter.readthedocs.io/
- Black (formatter):
black . - Flake8 (linter):
flake8 . - MyPy (type checker):
mypy . - Coverage:
pytest --cov=.
- Create feature branch:
git checkout -b feature/my-feature - Write code + tests
- Run tests:
pytest - Format code:
black . - Commit:
git commit -m "Add my feature" - Push:
git push origin feature/my-feature - Create Pull Request
# conftest.py
@pytest.fixture(scope='session')
def app():
"""Create app once per test session."""
app = create_app('testing')
yield appdef 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# Development
export FLASK_ENV=development
# Testing
export FLASK_ENV=testing
# Production
export FLASK_ENV=production# Ensure virtual environment is activated
source .venv/bin/activate
# Reinstall dependencies
pip install -r requirements-dev.txt<!-- Add to all POST forms -->
<form method="POST">
{{ csrf_token() }}
<!-- form fields -->
</form># Increase limit in blueprint
@limiter.limit("100 per hour") # Instead of default
def my_route():
pass# Clear pytest cache
pytest --cache-clear
# Run with verbose output
pytest -vv
# Run single test
pytest tests/test_auth.py::TestAuth::test_login -vvHappy Coding! 🎉