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:
9
src/models/__init__.py
Normal file
9
src/models/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""SQLAlchemy models for Sneaky Klaus.
|
||||
|
||||
This package contains all database models used by the application.
|
||||
"""
|
||||
|
||||
from src.models.admin import Admin
|
||||
from src.models.exchange import Exchange
|
||||
|
||||
__all__ = ["Admin", "Exchange"]
|
||||
48
src/models/admin.py
Normal file
48
src/models/admin.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Admin model for Sneaky Klaus.
|
||||
|
||||
The Admin model represents the single administrator account
|
||||
for the entire installation.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src.app import db
|
||||
|
||||
|
||||
class Admin(db.Model): # type: ignore[name-defined]
|
||||
"""Administrator user model.
|
||||
|
||||
Represents the single admin account for the entire Sneaky Klaus installation.
|
||||
Only one admin should exist per deployment.
|
||||
|
||||
Attributes:
|
||||
id: Auto-increment primary key.
|
||||
email: Admin email address (unique, indexed).
|
||||
password_hash: bcrypt password hash.
|
||||
created_at: Account creation timestamp.
|
||||
updated_at: Last update timestamp.
|
||||
"""
|
||||
|
||||
__tablename__ = "admin"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
email: Mapped[str] = mapped_column(
|
||||
String(255), unique=True, nullable=False, index=True
|
||||
)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, default=datetime.utcnow
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of Admin instance."""
|
||||
return f"<Admin {self.email}>"
|
||||
98
src/models/exchange.py
Normal file
98
src/models/exchange.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Exchange model for Sneaky Klaus.
|
||||
|
||||
The Exchange model represents a single Secret Santa exchange event.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import CheckConstraint, DateTime, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src.app import db
|
||||
|
||||
|
||||
class Exchange(db.Model): # type: ignore[name-defined]
|
||||
"""Exchange (Secret Santa event) model.
|
||||
|
||||
Represents a single Secret Santa exchange with all configuration
|
||||
and state management.
|
||||
|
||||
Attributes:
|
||||
id: Auto-increment primary key.
|
||||
slug: URL-safe unique identifier (12 characters).
|
||||
name: Exchange name/title.
|
||||
description: Optional description.
|
||||
budget: Gift budget (freeform text, e.g., "$20-30").
|
||||
max_participants: Maximum participant limit (minimum 3).
|
||||
registration_close_date: When registration ends.
|
||||
exchange_date: When gifts are exchanged.
|
||||
timezone: IANA timezone name (e.g., "America/New_York").
|
||||
state: Current state (draft, registration_open, etc.).
|
||||
created_at: Exchange creation timestamp.
|
||||
updated_at: Last update timestamp.
|
||||
completed_at: When exchange was marked complete.
|
||||
"""
|
||||
|
||||
__tablename__ = "exchange"
|
||||
|
||||
# Valid state transitions
|
||||
STATE_DRAFT = "draft"
|
||||
STATE_REGISTRATION_OPEN = "registration_open"
|
||||
STATE_REGISTRATION_CLOSED = "registration_closed"
|
||||
STATE_MATCHED = "matched"
|
||||
STATE_COMPLETED = "completed"
|
||||
|
||||
VALID_STATES = [
|
||||
STATE_DRAFT,
|
||||
STATE_REGISTRATION_OPEN,
|
||||
STATE_REGISTRATION_CLOSED,
|
||||
STATE_MATCHED,
|
||||
STATE_COMPLETED,
|
||||
]
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
slug: Mapped[str] = mapped_column(
|
||||
String(12), unique=True, nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
budget: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
max_participants: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
registration_close_date: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
exchange_date: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
timezone: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
state: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default=STATE_DRAFT, index=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, nullable=False, default=datetime.utcnow
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, nullable=True, index=True
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("max_participants >= 3", name="min_participants_check"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generate_slug() -> str:
|
||||
"""Generate a unique 12-character URL-safe slug.
|
||||
|
||||
Returns:
|
||||
Random 12-character alphanumeric string.
|
||||
"""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(12))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of Exchange instance."""
|
||||
return f"<Exchange {self.name} ({self.slug})>"
|
||||
Reference in New Issue
Block a user