From 6da4c3e37972ded76e3c50b07e07172164f4ebac Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 24 Jun 2026 19:53:19 -0700 Subject: [PATCH] major file restrucutre --- app.py | 62 +--------- boker/__init__.py | 60 +++++++++ auth.py => boker/auth.py | 0 charts.py => boker/charts.py | 4 +- config.py => boker/config.py | 4 +- db.py => boker/db.py | 2 +- db_models.py => boker/db_models.py | 2 +- emails.py => boker/emails.py | 2 +- extensions.py => boker/extensions.py | 0 models.py => boker/models.py | 0 {routes => boker/repositories}/__init__.py | 0 .../repositories/leagues.py | 12 +- .../repositories/ledger.py | 6 +- boker/routes/__init__.py | 1 + {routes => boker/routes}/account.py | 31 ++--- {routes => boker/routes}/admin.py | 8 +- {routes => boker/routes}/leagues.py | 117 +++++++++--------- {routes => boker/routes}/public.py | 16 +-- services.py => boker/services.py | 6 +- {static => boker/static}/css/style.css | 0 {static => boker/static}/css/tokens.css | 0 {static => boker/static}/favicon.svg | 0 storage.py => boker/storage.py | 0 .../templates}/_league_subnav.html | 0 .../templates}/account_login.html | 0 .../templates}/account_register.html | 0 .../templates}/account_settings.html | 0 .../templates}/admin_dashboard.html | 0 .../templates}/admin_login.html | 0 {templates => boker/templates}/base.html | 0 {templates => boker/templates}/docs.html | 0 {templates => boker/templates}/explore.html | 0 .../templates}/forgot_password.html | 0 {templates => boker/templates}/landing.html | 0 .../templates}/leaderboard.html | 0 .../templates}/league_dashboard.html | 0 .../templates}/league_leaderboard.html | 0 .../templates}/league_ledger.html | 0 .../templates}/league_new.html | 0 .../templates}/league_player_detail.html | 0 .../templates}/league_players.html | 0 .../templates}/league_session_detail.html | 0 .../templates}/league_session_view.html | 0 .../templates}/league_sessions.html | 0 .../templates}/league_settings.html | 0 .../templates}/leagues_index.html | 0 .../templates}/player_detail.html | 0 .../templates}/reset_password.html | 0 .../templates}/session_detail.html | 0 {templates => boker/templates}/sessions.html | 0 utils.py => boker/utils.py | 2 +- tests/test_accounting.py | 28 +++-- tests/test_stats.py | 5 +- 53 files changed, 188 insertions(+), 180 deletions(-) create mode 100644 boker/__init__.py rename auth.py => boker/auth.py (100%) rename charts.py => boker/charts.py (97%) rename config.py => boker/config.py (96%) rename db.py => boker/db.py (90%) rename db_models.py => boker/db_models.py (99%) rename emails.py => boker/emails.py (97%) rename extensions.py => boker/extensions.py (100%) rename models.py => boker/models.py (100%) rename {routes => boker/repositories}/__init__.py (100%) rename league_repositories.py => boker/repositories/leagues.py (97%) rename ledger_repositories.py => boker/repositories/ledger.py (98%) create mode 100644 boker/routes/__init__.py rename {routes => boker/routes}/account.py (93%) rename {routes => boker/routes}/admin.py (99%) rename {routes => boker/routes}/leagues.py (92%) rename {routes => boker/routes}/public.py (93%) rename services.py => boker/services.py (98%) rename {static => boker/static}/css/style.css (100%) rename {static => boker/static}/css/tokens.css (100%) rename {static => boker/static}/favicon.svg (100%) rename storage.py => boker/storage.py (100%) rename {templates => boker/templates}/_league_subnav.html (100%) rename {templates => boker/templates}/account_login.html (100%) rename {templates => boker/templates}/account_register.html (100%) rename {templates => boker/templates}/account_settings.html (100%) rename {templates => boker/templates}/admin_dashboard.html (100%) rename {templates => boker/templates}/admin_login.html (100%) rename {templates => boker/templates}/base.html (100%) rename {templates => boker/templates}/docs.html (100%) rename {templates => boker/templates}/explore.html (100%) rename {templates => boker/templates}/forgot_password.html (100%) rename {templates => boker/templates}/landing.html (100%) rename {templates => boker/templates}/leaderboard.html (100%) rename {templates => boker/templates}/league_dashboard.html (100%) rename {templates => boker/templates}/league_leaderboard.html (100%) rename {templates => boker/templates}/league_ledger.html (100%) rename {templates => boker/templates}/league_new.html (100%) rename {templates => boker/templates}/league_player_detail.html (100%) rename {templates => boker/templates}/league_players.html (100%) rename {templates => boker/templates}/league_session_detail.html (100%) rename {templates => boker/templates}/league_session_view.html (100%) rename {templates => boker/templates}/league_sessions.html (100%) rename {templates => boker/templates}/league_settings.html (100%) rename {templates => boker/templates}/leagues_index.html (100%) rename {templates => boker/templates}/player_detail.html (100%) rename {templates => boker/templates}/reset_password.html (100%) rename {templates => boker/templates}/session_detail.html (100%) rename {templates => boker/templates}/sessions.html (100%) rename utils.py => boker/utils.py (98%) diff --git a/app.py b/app.py index c2c707b..d71a532 100644 --- a/app.py +++ b/app.py @@ -1,66 +1,8 @@ #!/usr/bin/env python3 -from __future__ import annotations - -import os - -import click -from flask import Flask - -from auth import current_user_id, is_logged_in -from config import Config, ProductionConfig -from db import database_extensions_available, db, init_database -from extensions import csrf, limiter, mail -from routes.account import account_bp -from routes.leagues import leagues_bp -from routes.public import public_bp -from storage import ensure_data_file -from utils import cents_to_dollars, safe_date_label - - -def create_app(config_overrides: dict | None = None) -> Flask: - app = Flask(__name__) - cfg = ProductionConfig if os.getenv("FLASK_ENV") == "production" else Config - app.config.from_object(cfg) - if config_overrides: - app.config.update(config_overrides) - - ensure_data_file(app.config["DATA_PATH"]) - init_database(app) - csrf.init_app(app) - limiter.init_app(app) - mail.init_app(app) - - app.jinja_env.filters["money"] = cents_to_dollars - app.jinja_env.filters["pretty_date"] = safe_date_label - - @app.context_processor - def inject_globals() -> dict: - return { - "app_version": app.config["APP_VERSION"], - "current_user_id": current_user_id(), - "is_logged_in": is_logged_in(), - } - - app.register_blueprint(public_bp) - app.register_blueprint(account_bp) - app.register_blueprint(leagues_bp) - - @app.cli.command("init-db") - def init_db_command() -> None: - if not database_extensions_available() or db is None: - raise click.ClickException( - "Database dependencies are not installed. Run pip install -r requirements.txt." - ) - - with app.app_context(): - db.create_all() - - click.echo("Initialized database tables.") - - return app - +from boker import create_app app = create_app() if __name__ == "__main__": + import os app.run(debug=os.getenv("FLASK_DEBUG", "0") == "1") diff --git a/boker/__init__.py b/boker/__init__.py new file mode 100644 index 0000000..a1cc031 --- /dev/null +++ b/boker/__init__.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os + +import click +from flask import Flask + +from .auth import current_user_id, is_logged_in +from .config import Config, ProductionConfig +from .db import database_extensions_available, db, init_database +from .extensions import csrf, limiter, mail +from .routes.account import account_bp +from .routes.leagues import leagues_bp +from .routes.public import public_bp +from .storage import ensure_data_file +from .utils import cents_to_dollars, safe_date_label + + +def create_app(config_overrides: dict | None = None) -> Flask: + app = Flask(__name__) + cfg = ProductionConfig if os.getenv("FLASK_ENV") == "production" else Config + app.config.from_object(cfg) + if config_overrides: + app.config.update(config_overrides) + + ensure_data_file(app.config["DATA_PATH"]) + init_database(app) + csrf.init_app(app) + limiter.init_app(app) + mail.init_app(app) + + app.jinja_env.filters["money"] = cents_to_dollars + app.jinja_env.filters["pretty_date"] = safe_date_label + + @app.context_processor + def inject_globals() -> dict: + return { + "app_version": app.config["APP_VERSION"], + "current_user_id": current_user_id(), + "is_logged_in": is_logged_in(), + } + + app.register_blueprint(public_bp) + app.register_blueprint(account_bp) + app.register_blueprint(leagues_bp) + + @app.cli.command("init-db") + def init_db_command() -> None: + if not database_extensions_available() or db is None: + raise click.ClickException( + "Database dependencies are not installed. Run pip install -r requirements.txt." + ) + + with app.app_context(): + db.create_all() + + click.echo("Initialized database tables.") + + return app diff --git a/auth.py b/boker/auth.py similarity index 100% rename from auth.py rename to boker/auth.py diff --git a/charts.py b/boker/charts.py similarity index 97% rename from charts.py rename to boker/charts.py index 0a8d074..de70f3b 100644 --- a/charts.py +++ b/boker/charts.py @@ -4,8 +4,8 @@ from __future__ import annotations from typing import Any -from models import SessionEntry, SessionSummary -from utils import net_result_bucket, session_chart_label, session_sort_key +from .models import SessionEntry, SessionSummary +from .utils import net_result_bucket, session_chart_label, session_sort_key PLAYER_PALETTE = [ "#9b8cf0", # --line-1 violet diff --git a/config.py b/boker/config.py similarity index 96% rename from config.py rename to boker/config.py index c5a83af..0730954 100644 --- a/config.py +++ b/boker/config.py @@ -4,12 +4,12 @@ from __future__ import annotations import os from pathlib import Path -BASE_DIR = Path(__file__).resolve().parent +BASE_DIR = Path(__file__).resolve().parent.parent DATA_PATH = BASE_DIR / "data" / "entries.csv" DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" ELIGIBLE_MIN_SESSIONS = 3 -APP_VERSION = "2.3.0" +APP_VERSION = "2.3.1" def load_local_env(env_path: Path) -> None: diff --git a/db.py b/boker/db.py similarity index 90% rename from db.py rename to boker/db.py index 050416c..430a768 100644 --- a/db.py +++ b/boker/db.py @@ -24,7 +24,7 @@ def init_database(app: Any) -> bool: app.config["DATABASE_EXTENSIONS_AVAILABLE"] = False return False - import db_models # noqa: F401 - registers SQLAlchemy models with metadata. + from . import db_models # noqa: F401 - registers SQLAlchemy models with metadata. db.init_app(app) migrate.init_app(app, db) diff --git a/db_models.py b/boker/db_models.py similarity index 99% rename from db_models.py rename to boker/db_models.py index 23b2915..229ebe9 100644 --- a/db_models.py +++ b/boker/db_models.py @@ -4,7 +4,7 @@ from __future__ import annotations import uuid from datetime import date, datetime, timezone -from db import db +from .db import db if db is None: # pragma: no cover - imported only when DB dependencies exist. raise RuntimeError("Database dependencies are not installed.") diff --git a/emails.py b/boker/emails.py similarity index 97% rename from emails.py rename to boker/emails.py index 9f15ce6..7b02ddf 100644 --- a/emails.py +++ b/boker/emails.py @@ -4,7 +4,7 @@ from __future__ import annotations from flask import current_app from flask_mail import Message -from extensions import mail +from .extensions import mail def send_password_reset(to_email: str, reset_url: str) -> None: diff --git a/extensions.py b/boker/extensions.py similarity index 100% rename from extensions.py rename to boker/extensions.py diff --git a/models.py b/boker/models.py similarity index 100% rename from models.py rename to boker/models.py diff --git a/routes/__init__.py b/boker/repositories/__init__.py similarity index 100% rename from routes/__init__.py rename to boker/repositories/__init__.py diff --git a/league_repositories.py b/boker/repositories/leagues.py similarity index 97% rename from league_repositories.py rename to boker/repositories/leagues.py index 3d4417a..b23dbc3 100644 --- a/league_repositories.py +++ b/boker/repositories/leagues.py @@ -5,13 +5,13 @@ import secrets import string from datetime import date -from auth import hash_password, normalize_email -from db import db +from ..auth import hash_password, normalize_email +from ..db import db if db is None: # pragma: no cover - imported only when DB dependencies exist. raise RuntimeError("Database dependencies are not installed.") -from db_models import ( # noqa: E402 +from ..db_models import ( # noqa: E402 LedgerEvent, League, LeagueMembership, @@ -24,7 +24,7 @@ from db_models import ( # noqa: E402 make_user, normalize_lookup, ) -from utils import slugify +from ..utils import slugify PUBLIC_KEY_ALPHABET = string.ascii_lowercase + string.digits @@ -223,7 +223,7 @@ def set_session_status(session: PokerSession, status: str) -> PokerSession: if status not in {"open", "closed"}: raise ValueError(f"Unsupported session status: {status}") - from db_models import utc_now + from ..db_models import utc_now session.status = status if status == "open": @@ -313,7 +313,7 @@ def add_league_member( def remove_league_member(league_id: str, user_id: str) -> None: - from db_models import utc_now + from ..db_models import utc_now membership = LeagueMembership.query.filter_by( league_id=league_id, diff --git a/ledger_repositories.py b/boker/repositories/ledger.py similarity index 98% rename from ledger_repositories.py rename to boker/repositories/ledger.py index 93cb1e2..748caaa 100644 --- a/ledger_repositories.py +++ b/boker/repositories/ledger.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 from __future__ import annotations -from db import db +from ..db import db if db is None: # pragma: no cover - imported only when DB dependencies exist. raise RuntimeError("Database dependencies are not installed.") -from db_models import ( # noqa: E402 +from ..db_models import ( # noqa: E402 CANONICAL_EVENT_TYPES, LedgerEvent, Player, @@ -14,7 +14,7 @@ from db_models import ( # noqa: E402 User, canonical_event_type, ) -from storage import EventRow # noqa: E402 +from ..storage import EventRow # noqa: E402 def session_event_ref(session: PokerSession) -> str: diff --git a/boker/routes/__init__.py b/boker/routes/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/boker/routes/__init__.py @@ -0,0 +1 @@ + diff --git a/routes/account.py b/boker/routes/account.py similarity index 93% rename from routes/account.py rename to boker/routes/account.py index ba0f5ec..4eee3b3 100644 --- a/routes/account.py +++ b/boker/routes/account.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse from flask import Blueprint, flash, redirect, render_template, request, url_for -from auth import ( +from ..auth import ( current_user_id, generate_invite_token, generate_reset_token, @@ -18,8 +18,8 @@ from auth import ( verify_password, verify_reset_token, ) -from db import database_extensions_available, db -from extensions import limiter +from ..db import database_extensions_available, db +from ..extensions import limiter account_bp = Blueprint("account", __name__, url_prefix="/account") @@ -49,7 +49,7 @@ def register(): } if request.method == "POST": - from league_repositories import create_user, find_user_by_email + from ..repositories.leagues import create_user, find_user_by_email email = normalize_email(form["email"]) password = request.form.get("password", "") @@ -88,7 +88,7 @@ def login(): } if request.method == "POST": - from league_repositories import find_user_by_email + from ..repositories.leagues import find_user_by_email user = find_user_by_email(form["email"]) password = request.form.get("password", "") @@ -122,7 +122,7 @@ def settings(): flash("Account database is not available.", "error") return redirect(url_for("leagues.index")) - from db_models import User + from ..db_models import User user = db.session.get(User, current_user_id()) if user is None: @@ -139,7 +139,7 @@ def update_email(): flash("Account database is not available.", "error") return redirect(url_for("account.settings")) - from db_models import User + from ..db_models import User user = db.session.get(User, current_user_id()) if user is None: @@ -154,7 +154,7 @@ def update_email(): elif not verify_password(user.password_hash, current_password): flash("Current password is incorrect.", "error") else: - from league_repositories import find_user_by_email + from ..repositories.leagues import find_user_by_email existing = find_user_by_email(new_email) if existing and existing.id != user.id: @@ -174,7 +174,7 @@ def update_password(): flash("Account database is not available.", "error") return redirect(url_for("account.settings")) - from db_models import User + from ..db_models import User user = db.session.get(User, current_user_id()) if user is None: @@ -206,7 +206,7 @@ def delete_account(): flash("Account database is not available.", "error") return redirect(url_for("account.settings")) - from db_models import League, User, utc_now + from ..db_models import League, User, utc_now user = db.session.get(User, current_user_id()) if user is None: @@ -246,9 +246,10 @@ def forgot_password(): flash("Account database is not available.", "error") return redirect(url_for("account.forgot_password")) - from emails import send_password_reset from flask import current_app - from league_repositories import find_user_by_email + + from ..emails import send_password_reset + from ..repositories.leagues import find_user_by_email email = normalize_email(request.form.get("email", "")) user = find_user_by_email(email) @@ -282,7 +283,7 @@ def reset_password(token): flash("Account database is not available.", "error") return redirect(url_for("account.login")) - from db_models import User + from ..db_models import User user = db.session.get(User, user_id) if user is None or user.disabled_at is not None: @@ -320,8 +321,8 @@ def accept_invite(token): flash("Account database is not available.", "error") return redirect(url_for("leagues.index")) - from db_models import User - from league_repositories import add_league_member, find_league_by_id, find_membership + from ..db_models import User + from ..repositories.leagues import add_league_member, find_league_by_id, find_membership user = db.session.get(User, current_user_id()) if user is None or user.email != data.get("email"): diff --git a/routes/admin.py b/boker/routes/admin.py similarity index 99% rename from routes/admin.py rename to boker/routes/admin.py index f959d89..625e236 100644 --- a/routes/admin.py +++ b/boker/routes/admin.py @@ -16,16 +16,16 @@ from flask import ( url_for, ) -from auth import is_admin -from services import ( +from ..auth import is_admin +from ..services import ( build_session_summaries, next_session_id, pending_payout_carry_items, prunable_empty_session_ids, unique_player_names, ) -from storage import CSV_HEADERS, append_event, load_events, write_events -from utils import cents_to_dollars, session_label +from ..storage import CSV_HEADERS, append_event, load_events, write_events +from ..utils import cents_to_dollars, session_label admin_bp = Blueprint("admin", __name__) diff --git a/routes/leagues.py b/boker/routes/leagues.py similarity index 92% rename from routes/leagues.py rename to boker/routes/leagues.py index 44bcb13..4d46c83 100644 --- a/routes/leagues.py +++ b/boker/routes/leagues.py @@ -9,12 +9,12 @@ from datetime import datetime, timezone from flask import Blueprint, abort, flash, make_response, redirect, render_template, request, url_for -from auth import current_user_id, login_required, normalize_email -from charts import cumulative_profit_series, player_session_series -from db import database_extensions_available, db -from models import SessionSummary -from services import apply_rank_changes, build_leaderboard, build_session_summaries, session_events -from utils import cents_to_dollars, session_label, session_sort_key +from ..auth import current_user_id, login_required, normalize_email +from ..charts import cumulative_profit_series, player_session_series +from ..db import database_extensions_available, db +from ..models import SessionSummary +from ..services import apply_rank_changes, build_leaderboard, build_session_summaries, session_events +from ..utils import cents_to_dollars, session_label, session_sort_key leagues_bp = Blueprint("leagues", __name__) @@ -48,7 +48,7 @@ def split_league_ref(league_ref: str) -> tuple[str, str]: def require_league(league_ref: str, allowed_roles: set[str]): - from league_repositories import find_league_by_public_key, user_has_league_role + from ..repositories.leagues import find_league_by_public_key, user_has_league_role _slug, public_key = split_league_ref(league_ref) league = find_league_by_public_key(public_key) @@ -64,7 +64,7 @@ def require_league(league_ref: str, allowed_roles: set[str]): def get_league_with_visibility_gate(league_ref: str): """Load league; if private, enforce login + membership. Returns (league, None) or (None, redirect).""" - from league_repositories import find_league_by_public_key, user_has_league_role + from ..repositories.leagues import find_league_by_public_key, user_has_league_role _slug, public_key = split_league_ref(league_ref) league = find_league_by_public_key(public_key) @@ -99,8 +99,8 @@ def empty_session_summary(session) -> SessionSummary: def session_ref_map(league_id: str) -> dict[str, str]: - from ledger_repositories import session_event_ref - from league_repositories import list_sessions_for_league + from ..repositories.ledger import session_event_ref + from ..repositories.leagues import list_sessions_for_league return {session_event_ref(session): session.id for session in list_sessions_for_league(league_id)} @@ -112,8 +112,8 @@ def index(): flash("League database is not available.", "error") return render_template("leagues_index.html", leagues=[]) - from league_repositories import list_leagues_for_user - from ledger_repositories import list_event_rows_for_league + from ..repositories.leagues import list_leagues_for_user + from ..repositories.ledger import list_event_rows_for_league memberships = list_leagues_for_user(current_user_id() or "") league_summaries = [] @@ -190,8 +190,8 @@ def new(): } if request.method == "POST": - from db_models import User - from league_repositories import create_league, unique_league_slug + from ..db_models import User + from ..repositories.leagues import create_league, unique_league_slug owner = db.session.get(User, current_user_id()) if owner is None: @@ -221,7 +221,7 @@ def dashboard(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from league_repositories import league_counts, user_has_league_role + from ..repositories.leagues import league_counts, user_has_league_role league, resp = get_league_with_visibility_gate(league_ref) if resp: @@ -242,7 +242,7 @@ def legacy_dashboard_redirect(league_id: str, league_slug: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from league_repositories import find_league_by_id, user_has_league_role + from ..repositories.leagues import find_league_by_id, user_has_league_role league = find_league_by_id(league_id) if league is None: @@ -259,9 +259,9 @@ def leaderboard(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from config import ELIGIBLE_MIN_SESSIONS - from ledger_repositories import list_event_rows_for_league - from league_repositories import list_players_for_league, user_has_league_role + from ..config import ELIGIBLE_MIN_SESSIONS + from ..repositories.ledger import list_event_rows_for_league + from ..repositories.leagues import list_players_for_league, user_has_league_role league, resp = get_league_with_visibility_gate(league_ref) if resp: @@ -366,12 +366,12 @@ def ledger(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from ledger_repositories import ( + from ..repositories.ledger import ( append_ledger_event, list_all_event_rows_for_league, list_event_rows_for_league, ) - from league_repositories import ( + from ..repositories.leagues import ( find_player_for_league, find_session_for_league, list_players_for_league, @@ -506,7 +506,7 @@ def void_event(league_ref: str, event_id: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from ledger_repositories import void_ledger_event + from ..repositories.ledger import void_ledger_event league = require_league(league_ref, {"owner"}) reason = request.form.get("void_reason", "").strip() @@ -529,8 +529,8 @@ def player_detail(league_ref: str, player_id: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from ledger_repositories import list_event_rows_for_league - from league_repositories import find_player_for_league, user_has_league_role + from ..repositories.ledger import list_event_rows_for_league + from ..repositories.leagues import find_player_for_league, user_has_league_role league, resp = get_league_with_visibility_gate(league_ref) if resp: @@ -588,7 +588,7 @@ def players(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from league_repositories import ( + from ..repositories.leagues import ( create_player, list_players_for_league, player_name_exists, @@ -627,7 +627,7 @@ def players(league_ref: str): flash("Player added.", "success") return redirect(url_for("leagues.players", **league_url_values(league))) - from ledger_repositories import list_event_rows_for_league + from ..repositories.ledger import list_event_rows_for_league all_sessions = build_session_summaries(list_event_rows_for_league(league.id)) board = build_leaderboard(all_sessions) @@ -661,7 +661,7 @@ def update_player_status(league_ref: str, player_id: str, status: str, message: flash("League database is not available.", "error") return redirect(url_for("public.home")) - from league_repositories import find_player_for_league, set_player_status + from ..repositories.leagues import find_player_for_league, set_player_status league = require_league(league_ref, {"owner", "manager"}) player = find_player_for_league(league.id, player_id) @@ -681,7 +681,7 @@ def edit_player(league_ref: str, player_id: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from league_repositories import find_player_for_league, player_name_taken, update_player + from ..repositories.leagues import find_player_for_league, player_name_taken, update_player league = require_league(league_ref, {"owner", "manager"}) player = find_player_for_league(league.id, player_id) @@ -709,8 +709,8 @@ def sessions(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from ledger_repositories import append_ledger_event - from league_repositories import ( + from ..repositories.ledger import append_ledger_event + from ..repositories.leagues import ( create_poker_session, list_sessions_for_league, user_has_league_role, @@ -747,7 +747,7 @@ def sessions(league_ref: str): session.label = form["label"] or None session.notes = form["notes"] or None if status == "open": - from league_repositories import set_session_status + from ..repositories.leagues import set_session_status set_session_status(session, "open") @@ -765,8 +765,8 @@ def sessions(league_ref: str): flash(f"Created {session.display_label}.", "success") return redirect(url_for("leagues.sessions", **league_url_values(league))) - from db_models import LedgerEvent - from ledger_repositories import list_event_rows_for_league + from ..db_models import LedgerEvent + from ..repositories.ledger import list_event_rows_for_league all_sessions = list_sessions_for_league(league.id) summaries = build_session_summaries(list_event_rows_for_league(league.id)) @@ -803,8 +803,8 @@ def prune_empty_sessions(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from db_models import LedgerEvent, PokerSession - from league_repositories import list_sessions_for_league + from ..db_models import LedgerEvent, PokerSession + from ..repositories.leagues import list_sessions_for_league league = require_league(league_ref, {"owner", "manager"}) @@ -839,7 +839,7 @@ def delete_session(league_ref: str, session_id: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from league_repositories import find_session_for_league + from ..repositories.leagues import find_session_for_league league = require_league(league_ref, {"owner"}) session = find_session_for_league(league.id, session_id) @@ -859,7 +859,7 @@ def edit_session(league_ref: str, session_id: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from league_repositories import find_session_for_league + from ..repositories.leagues import find_session_for_league league = require_league(league_ref, {"owner", "manager"}) session = find_session_for_league(league.id, session_id) @@ -877,7 +877,7 @@ def edit_session(league_ref: str, session_id: str): return redirect(url_for("leagues.session_detail", league_ref=league.url_ref, session_id=session_id)) if new_date != session.session_date: - from db_models import PokerSession as _PS + from ..db_models import PokerSession as _PS max_seq = db.session.query(db.func.max(_PS.sequence_on_date)).filter( _PS.league_id == league.id, _PS.session_date == new_date, @@ -900,8 +900,8 @@ def session_detail(league_ref: str, session_id: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from ledger_repositories import append_ledger_event, list_all_event_rows_for_session, list_event_rows_for_session - from league_repositories import ( + from ..repositories.ledger import append_ledger_event, list_all_event_rows_for_session, list_event_rows_for_session + from ..repositories.leagues import ( find_session_for_league, list_players_for_league, user_has_league_role, @@ -989,9 +989,9 @@ def session_public_view(league_ref: str, session_id: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from charts import session_breakdown_series - from ledger_repositories import list_event_rows_for_league, list_event_rows_for_session - from league_repositories import find_league_by_public_key, find_session_for_league, list_sessions_for_league, user_has_league_role + from ..charts import session_breakdown_series + from ..repositories.ledger import list_event_rows_for_league, list_event_rows_for_session + from ..repositories.leagues import find_league_by_public_key, find_session_for_league, list_sessions_for_league, user_has_league_role _slug, public_key = split_league_ref(league_ref) league = find_league_by_public_key(public_key) @@ -1062,8 +1062,8 @@ def update_session_status(league_ref: str, session_id: str, status: str, message flash("League database is not available.", "error") return redirect(url_for("public.home")) - from ledger_repositories import append_ledger_event - from league_repositories import find_session_for_league, set_session_status + from ..repositories.ledger import append_ledger_event + from ..repositories.leagues import find_session_for_league, set_session_status league = require_league(league_ref, {"owner", "manager"}) session = find_session_for_league(league.id, session_id) @@ -1114,7 +1114,7 @@ def league_settings(league_ref: str): elif form["visibility"] not in ("private", "public"): flash("Invalid visibility value.", "error") else: - from utils import slugify + from ..utils import slugify league.name = form["name"] league.slug = slugify(form["name"]) @@ -1124,7 +1124,7 @@ def league_settings(league_ref: str): flash("League settings saved.", "success") return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) - from league_repositories import list_members_for_league + from ..repositories.leagues import list_members_for_league members = list_members_for_league(league.id) return render_template("league_settings.html", league=league, form=form, is_owner=True, members=members) @@ -1137,10 +1137,11 @@ def invite_member(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from auth import generate_invite_token - from emails import send_league_invite from flask import current_app - from league_repositories import find_membership, find_user_by_email + + from ..auth import generate_invite_token + from ..emails import send_league_invite + from ..repositories.leagues import find_membership, find_user_by_email league = require_league(league_ref, {"owner"}) email = normalize_email(request.form.get("email", "")) @@ -1181,7 +1182,7 @@ def remove_member(league_ref: str, user_id: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from league_repositories import remove_league_member + from ..repositories.leagues import remove_league_member league = require_league(league_ref, {"owner"}) @@ -1202,7 +1203,7 @@ def archive_league(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from db_models import utc_now + from ..db_models import utc_now league = require_league(league_ref, {"owner"}) confirm_name = request.form.get("confirm_name", "").strip() @@ -1224,8 +1225,8 @@ def export_ledger_csv(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from ledger_repositories import list_event_rows_for_league - from storage import CSV_HEADERS + from ..repositories.ledger import list_event_rows_for_league + from ..storage import CSV_HEADERS league = require_league(league_ref, {"owner", "manager", "viewer"}) rows = list_event_rows_for_league(league.id) @@ -1250,14 +1251,14 @@ def import_ledger_csv(league_ref: str): flash("League database is not available.", "error") return redirect(url_for("public.home")) - from db_models import CANONICAL_EVENT_TYPES, canonical_event_type - from ledger_repositories import ( + from ..db_models import CANONICAL_EVENT_TYPES, canonical_event_type + from ..repositories.ledger import ( append_ledger_event, list_ledger_events_for_league, session_event_ref, ) - from league_repositories import list_players_for_league, list_sessions_for_league - from storage import CSV_HEADERS + from ..repositories.leagues import list_players_for_league, list_sessions_for_league + from ..storage import CSV_HEADERS league = require_league(league_ref, {"owner", "manager"}) diff --git a/routes/public.py b/boker/routes/public.py similarity index 93% rename from routes/public.py rename to boker/routes/public.py index c2371be..f88828f 100644 --- a/routes/public.py +++ b/boker/routes/public.py @@ -4,16 +4,16 @@ from __future__ import annotations from flask import Blueprint, flash, redirect, render_template, request, url_for from flask import current_app -from charts import cumulative_profit_series, player_session_series, session_breakdown_series -from config import ELIGIBLE_MIN_SESSIONS -from services import ( +from ..charts import cumulative_profit_series, player_session_series, session_breakdown_series +from ..config import ELIGIBLE_MIN_SESSIONS +from ..services import ( apply_rank_changes, build_leaderboard, build_session_summaries, session_events, ) -from storage import load_events -from utils import session_label, session_sort_key +from ..storage import load_events +from ..utils import session_label, session_sort_key public_bp = Blueprint("public", __name__) @@ -30,9 +30,9 @@ def help(): @public_bp.get("/explore") def explore(): - from auth import is_logged_in - from db import database_extensions_available - from league_repositories import league_counts, list_public_leagues + from ..auth import is_logged_in + from ..db import database_extensions_available + from ..repositories.leagues import league_counts, list_public_leagues q = request.args.get("q", "").strip() leagues = list_public_leagues(q) if database_extensions_available() else [] diff --git a/services.py b/boker/services.py similarity index 98% rename from services.py rename to boker/services.py index c364759..6d6123a 100644 --- a/services.py +++ b/boker/services.py @@ -4,9 +4,9 @@ from __future__ import annotations from collections import defaultdict -from models import PlayerStats, SessionEntry, SessionSummary -from storage import EventRow -from utils import entry_sort_key, net_result_bucket, session_sort_key +from .models import PlayerStats, SessionEntry, SessionSummary +from .storage import EventRow +from .utils import entry_sort_key, net_result_bucket, session_sort_key SESSION_MARKER_TYPES = {"session_open", "session_close"} diff --git a/static/css/style.css b/boker/static/css/style.css similarity index 100% rename from static/css/style.css rename to boker/static/css/style.css diff --git a/static/css/tokens.css b/boker/static/css/tokens.css similarity index 100% rename from static/css/tokens.css rename to boker/static/css/tokens.css diff --git a/static/favicon.svg b/boker/static/favicon.svg similarity index 100% rename from static/favicon.svg rename to boker/static/favicon.svg diff --git a/storage.py b/boker/storage.py similarity index 100% rename from storage.py rename to boker/storage.py diff --git a/templates/_league_subnav.html b/boker/templates/_league_subnav.html similarity index 100% rename from templates/_league_subnav.html rename to boker/templates/_league_subnav.html diff --git a/templates/account_login.html b/boker/templates/account_login.html similarity index 100% rename from templates/account_login.html rename to boker/templates/account_login.html diff --git a/templates/account_register.html b/boker/templates/account_register.html similarity index 100% rename from templates/account_register.html rename to boker/templates/account_register.html diff --git a/templates/account_settings.html b/boker/templates/account_settings.html similarity index 100% rename from templates/account_settings.html rename to boker/templates/account_settings.html diff --git a/templates/admin_dashboard.html b/boker/templates/admin_dashboard.html similarity index 100% rename from templates/admin_dashboard.html rename to boker/templates/admin_dashboard.html diff --git a/templates/admin_login.html b/boker/templates/admin_login.html similarity index 100% rename from templates/admin_login.html rename to boker/templates/admin_login.html diff --git a/templates/base.html b/boker/templates/base.html similarity index 100% rename from templates/base.html rename to boker/templates/base.html diff --git a/templates/docs.html b/boker/templates/docs.html similarity index 100% rename from templates/docs.html rename to boker/templates/docs.html diff --git a/templates/explore.html b/boker/templates/explore.html similarity index 100% rename from templates/explore.html rename to boker/templates/explore.html diff --git a/templates/forgot_password.html b/boker/templates/forgot_password.html similarity index 100% rename from templates/forgot_password.html rename to boker/templates/forgot_password.html diff --git a/templates/landing.html b/boker/templates/landing.html similarity index 100% rename from templates/landing.html rename to boker/templates/landing.html diff --git a/templates/leaderboard.html b/boker/templates/leaderboard.html similarity index 100% rename from templates/leaderboard.html rename to boker/templates/leaderboard.html diff --git a/templates/league_dashboard.html b/boker/templates/league_dashboard.html similarity index 100% rename from templates/league_dashboard.html rename to boker/templates/league_dashboard.html diff --git a/templates/league_leaderboard.html b/boker/templates/league_leaderboard.html similarity index 100% rename from templates/league_leaderboard.html rename to boker/templates/league_leaderboard.html diff --git a/templates/league_ledger.html b/boker/templates/league_ledger.html similarity index 100% rename from templates/league_ledger.html rename to boker/templates/league_ledger.html diff --git a/templates/league_new.html b/boker/templates/league_new.html similarity index 100% rename from templates/league_new.html rename to boker/templates/league_new.html diff --git a/templates/league_player_detail.html b/boker/templates/league_player_detail.html similarity index 100% rename from templates/league_player_detail.html rename to boker/templates/league_player_detail.html diff --git a/templates/league_players.html b/boker/templates/league_players.html similarity index 100% rename from templates/league_players.html rename to boker/templates/league_players.html diff --git a/templates/league_session_detail.html b/boker/templates/league_session_detail.html similarity index 100% rename from templates/league_session_detail.html rename to boker/templates/league_session_detail.html diff --git a/templates/league_session_view.html b/boker/templates/league_session_view.html similarity index 100% rename from templates/league_session_view.html rename to boker/templates/league_session_view.html diff --git a/templates/league_sessions.html b/boker/templates/league_sessions.html similarity index 100% rename from templates/league_sessions.html rename to boker/templates/league_sessions.html diff --git a/templates/league_settings.html b/boker/templates/league_settings.html similarity index 100% rename from templates/league_settings.html rename to boker/templates/league_settings.html diff --git a/templates/leagues_index.html b/boker/templates/leagues_index.html similarity index 100% rename from templates/leagues_index.html rename to boker/templates/leagues_index.html diff --git a/templates/player_detail.html b/boker/templates/player_detail.html similarity index 100% rename from templates/player_detail.html rename to boker/templates/player_detail.html diff --git a/templates/reset_password.html b/boker/templates/reset_password.html similarity index 100% rename from templates/reset_password.html rename to boker/templates/reset_password.html diff --git a/templates/session_detail.html b/boker/templates/session_detail.html similarity index 100% rename from templates/session_detail.html rename to boker/templates/session_detail.html diff --git a/templates/sessions.html b/boker/templates/sessions.html similarity index 100% rename from templates/sessions.html rename to boker/templates/sessions.html diff --git a/utils.py b/boker/utils.py similarity index 98% rename from utils.py rename to boker/utils.py index 2fa8e79..95d83dd 100644 --- a/utils.py +++ b/boker/utils.py @@ -7,7 +7,7 @@ import re from markupsafe import Markup -from models import SessionEntry, SessionSummary +from .models import SessionEntry, SessionSummary BREAK_EVEN_TOLERANCE_CENTS = 100 diff --git a/tests/test_accounting.py b/tests/test_accounting.py index f4e2c8c..799e3e8 100644 --- a/tests/test_accounting.py +++ b/tests/test_accounting.py @@ -2,15 +2,17 @@ from pathlib import Path import tempfile import unittest -import app as app_module -from charts import player_session_series -from services import ( +from boker import create_app +from boker.charts import player_session_series +from boker.services import ( build_leaderboard, build_session_summaries, pending_payout_carry_items, ) -from storage import load_events, write_events -from utils import session_sort_key +from boker.storage import load_events, write_events +from boker.utils import session_sort_key + +_app = create_app() def cents(amount: float) -> int: @@ -203,8 +205,8 @@ class AccountingTests(unittest.TestCase): with tempfile.TemporaryDirectory() as tmpdir: data_path = Path(tmpdir) / "entries.csv" write_events(data_path, rows) - original_path = app_module.app.config["DATA_PATH"] - app_module.app.config["DATA_PATH"] = data_path + original_path = _app.config["DATA_PATH"] + _app.config["DATA_PATH"] = data_path try: sessions = build_session_summaries(load_events(data_path)) @@ -213,7 +215,7 @@ class AccountingTests(unittest.TestCase): [{"player_name": "A", "amount_cents": cents(5)}], ) - with app_module.app.test_client() as client: + with _app.test_client() as client: with client.session_transaction() as flask_session: flask_session["is_admin"] = True @@ -222,7 +224,7 @@ class AccountingTests(unittest.TestCase): data={"session_id": "s2", "player_name": "A"}, ) finally: - app_module.app.config["DATA_PATH"] = original_path + _app.config["DATA_PATH"] = original_path self.assertEqual(response.status_code, 302) updated_sessions = build_session_summaries(load_events(data_path)) @@ -261,17 +263,17 @@ class AccountingTests(unittest.TestCase): with tempfile.TemporaryDirectory() as tmpdir: data_path = Path(tmpdir) / "entries.csv" write_events(data_path, rows) - original_path = app_module.app.config["DATA_PATH"] - app_module.app.config["DATA_PATH"] = data_path + original_path = _app.config["DATA_PATH"] + _app.config["DATA_PATH"] = data_path try: - with app_module.app.test_client() as client: + with _app.test_client() as client: with client.session_transaction() as flask_session: flask_session["is_admin"] = True response = client.post("/admin/prune-empty-sessions") finally: - app_module.app.config["DATA_PATH"] = original_path + _app.config["DATA_PATH"] = original_path self.assertEqual(response.status_code, 302) remaining = load_events(data_path) diff --git a/tests/test_stats.py b/tests/test_stats.py index 36ceaf3..5f3b1cd 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -1,6 +1,7 @@ import unittest -from stats import SessionEntry, SessionSummary, build_leaderboard +from boker.models import SessionEntry, SessionSummary +from boker.services import build_leaderboard class CashInAccountingTest(unittest.TestCase): @@ -78,7 +79,7 @@ class CashInAccountingTest(unittest.TestCase): self.assertEqual(player.total_buy_in_cents, 2500) self.assertEqual(player.total_front_collected_cents, 0) - self.assertEqual(player.total_cash_in_cents, 3000) + self.assertEqual(player.total_real_cash_in_cents, 2500) if __name__ == "__main__":