chore: initial project setup

Initialize Sneaky Klaus project with:
- uv package management and pyproject.toml
- Flask application structure (app.py, config.py)
- SQLAlchemy models for Admin and Exchange
- Alembic database migrations
- Pre-commit hooks configuration
- Development tooling (pytest, ruff, mypy)

Initial structure follows design documents in docs/:
- src/app.py: Application factory with Flask extensions
- src/config.py: Environment-based configuration
- src/models/: Admin and Exchange models
- migrations/: Alembic migration setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-22 11:28:15 -07:00
commit b077112aba
32 changed files with 10931 additions and 0 deletions

143
src/app.py Normal file
View File

@@ -0,0 +1,143 @@
"""Flask application factory for Sneaky Klaus.
This module provides the application factory function that creates
and configures the Flask application instance.
"""
from pathlib import Path
from flask import Flask
from flask_bcrypt import Bcrypt
from flask_session import Session
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
# Initialize Flask extensions (without app instance)
db = SQLAlchemy()
bcrypt = Bcrypt()
csrf = CSRFProtect()
session = Session()
def create_app(config_name: str | None = None) -> Flask:
"""Create and configure the Flask application.
Args:
config_name: Configuration environment name (development, production, testing).
If None, uses FLASK_ENV environment variable.
Returns:
Configured Flask application instance.
"""
# Create Flask app instance
app = Flask(__name__)
# Load configuration
from src.config import ProductionConfig, get_config
config_class = get_config(config_name)
app.config.from_object(config_class)
# Validate production config if needed
if config_name == "production":
ProductionConfig.validate()
# Ensure data directory exists
data_dir = Path(app.config["DATA_DIR"])
data_dir.mkdir(parents=True, exist_ok=True)
# Initialize extensions
db.init_app(app)
bcrypt.init_app(app)
csrf.init_app(app)
# Initialize session with SQLAlchemy backend
app.config["SESSION_SQLALCHEMY"] = db
session.init_app(app)
# Register blueprints (will be created later)
# from src.routes import admin, auth, participant, public
# app.register_blueprint(admin.bp)
# app.register_blueprint(auth.bp)
# app.register_blueprint(participant.bp)
# app.register_blueprint(public.bp)
# Register error handlers
register_error_handlers(app)
# First-run setup check
register_setup_check(app)
# Import models to ensure they're registered with SQLAlchemy
with app.app_context():
from src.models import Admin, Exchange # noqa: F401
db.create_all()
return app
def register_error_handlers(app: Flask) -> None:
"""Register custom error handlers.
Args:
app: Flask application instance.
"""
from flask import render_template
@app.errorhandler(404)
def not_found_error(_error):
"""Handle 404 Not Found errors."""
return render_template("errors/404.html"), 404
@app.errorhandler(403)
def forbidden_error(_error):
"""Handle 403 Forbidden errors."""
return render_template("errors/403.html"), 403
@app.errorhandler(500)
def internal_error(error):
"""Handle 500 Internal Server Error."""
db.session.rollback() # Rollback any failed transactions
app.logger.error(f"Internal server error: {error}")
return render_template("errors/500.html"), 500
@app.errorhandler(429)
def rate_limit_error(_error):
"""Handle 429 Too Many Requests errors."""
from flask import flash, redirect, request
flash("Too many attempts. Please try again later.", "error")
return redirect(request.referrer or "/"), 429
def register_setup_check(app: Flask) -> None:
"""Register before_request handler for first-run setup detection.
Args:
app: Flask application instance.
"""
from flask import redirect, request, url_for
@app.before_request
def check_setup_required():
"""Redirect to setup page if no admin exists.
This runs before every request to ensure the application
has been set up with an admin account.
"""
# Skip check for certain endpoints
if request.endpoint in ["setup", "static", "health"]:
return
# Check if we've already determined setup is required
if not hasattr(app, "_setup_checked"):
from src.models.admin import Admin
admin_count = db.session.query(Admin).count()
app.config["REQUIRES_SETUP"] = admin_count == 0
app._setup_checked = True
# Redirect to setup if needed
if app.config.get("REQUIRES_SETUP") and request.endpoint != "setup":
return redirect(url_for("setup"))