Compare commits
18 Commits
next-relea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f9e24eb4bc | |||
|
|
36b962e5d5 | ||
| 5d5df325bc | |||
|
|
682b990321 | ||
| 5b19bd486a | |||
|
|
718abc0096 | ||
| 52d88ae950 | |||
|
|
9165419ed0 | ||
| d23d6520c3 | |||
|
|
c23ca9a721 | ||
| 1db06576cc | |||
|
|
7a5327f490 | ||
| a2889751da | |||
|
|
eb886a4690 | ||
| 0ee7367bc0 | |||
|
|
6637f6e9eb | ||
| fa176c9b05 | |||
|
|
ece25ec22c |
205
app/db_engine.py
205
app/db_engine.py
@ -8,39 +8,30 @@ from typing import Literal, List, Optional
|
||||
from pandas import DataFrame
|
||||
from peewee import *
|
||||
from peewee import ModelSelect
|
||||
from playhouse.pool import PooledPostgresqlDatabase
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
# Database configuration - supports both SQLite and PostgreSQL
|
||||
DATABASE_TYPE = os.environ.get("DATABASE_TYPE", "sqlite")
|
||||
|
||||
if DATABASE_TYPE.lower() == "postgresql":
|
||||
from playhouse.pool import PooledPostgresqlDatabase
|
||||
|
||||
_postgres_password = os.environ.get("POSTGRES_PASSWORD")
|
||||
if _postgres_password is None:
|
||||
raise RuntimeError(
|
||||
"POSTGRES_PASSWORD environment variable is not set. "
|
||||
"This variable is required when DATABASE_TYPE=postgresql."
|
||||
)
|
||||
db = PooledPostgresqlDatabase(
|
||||
os.environ.get("POSTGRES_DB", "sba_master"),
|
||||
user=os.environ.get("POSTGRES_USER", "sba_admin"),
|
||||
password=_postgres_password,
|
||||
host=os.environ.get("POSTGRES_HOST", "sba_postgres"),
|
||||
port=int(os.environ.get("POSTGRES_PORT", "5432")),
|
||||
max_connections=20,
|
||||
stale_timeout=300, # 5 minutes
|
||||
timeout=0,
|
||||
autoconnect=True,
|
||||
autorollback=True, # Automatically rollback failed transactions
|
||||
)
|
||||
else:
|
||||
# Default SQLite configuration
|
||||
db = SqliteDatabase(
|
||||
"storage/sba_master.db",
|
||||
pragmas={"journal_mode": "wal", "cache_size": -1 * 64000, "synchronous": 0},
|
||||
_postgres_password = os.environ.get("POSTGRES_PASSWORD")
|
||||
if _postgres_password is None:
|
||||
raise RuntimeError(
|
||||
"POSTGRES_PASSWORD environment variable is not set. "
|
||||
"This variable is required when DATABASE_TYPE=postgresql."
|
||||
)
|
||||
|
||||
db = PooledPostgresqlDatabase(
|
||||
os.environ.get("POSTGRES_DB", "sba_master"),
|
||||
user=os.environ.get("POSTGRES_USER", "sba_admin"),
|
||||
password=_postgres_password,
|
||||
host=os.environ.get("POSTGRES_HOST", "sba_postgres"),
|
||||
port=int(os.environ.get("POSTGRES_PORT", "5432")),
|
||||
max_connections=20,
|
||||
stale_timeout=300, # 5 minutes
|
||||
timeout=5,
|
||||
autoconnect=False,
|
||||
autorollback=True, # Automatically rollback failed transactions
|
||||
)
|
||||
|
||||
|
||||
date = f"{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}"
|
||||
logger = logging.getLogger("discord_app")
|
||||
|
||||
@ -523,12 +514,12 @@ class Team(BaseModel):
|
||||
all_drops = Transaction.select_season(Current.latest().season).where(
|
||||
(Transaction.oldteam == self)
|
||||
& (Transaction.week == current.week + 1)
|
||||
& (Transaction.cancelled == 0)
|
||||
& (Transaction.cancelled == False)
|
||||
)
|
||||
all_adds = Transaction.select_season(Current.latest().season).where(
|
||||
(Transaction.newteam == self)
|
||||
& (Transaction.week == current.week + 1)
|
||||
& (Transaction.cancelled == 0)
|
||||
& (Transaction.cancelled == False)
|
||||
)
|
||||
|
||||
for move in all_drops:
|
||||
@ -606,12 +597,12 @@ class Team(BaseModel):
|
||||
all_drops = Transaction.select_season(Current.latest().season).where(
|
||||
(Transaction.oldteam == sil_team)
|
||||
& (Transaction.week == current.week + 1)
|
||||
& (Transaction.cancelled == 0)
|
||||
& (Transaction.cancelled == False)
|
||||
)
|
||||
all_adds = Transaction.select_season(Current.latest().season).where(
|
||||
(Transaction.newteam == sil_team)
|
||||
& (Transaction.week == current.week + 1)
|
||||
& (Transaction.cancelled == 0)
|
||||
& (Transaction.cancelled == False)
|
||||
)
|
||||
|
||||
for move in all_drops:
|
||||
@ -689,12 +680,12 @@ class Team(BaseModel):
|
||||
all_drops = Transaction.select_season(Current.latest().season).where(
|
||||
(Transaction.oldteam == lil_team)
|
||||
& (Transaction.week == current.week + 1)
|
||||
& (Transaction.cancelled == 0)
|
||||
& (Transaction.cancelled == False)
|
||||
)
|
||||
all_adds = Transaction.select_season(Current.latest().season).where(
|
||||
(Transaction.newteam == lil_team)
|
||||
& (Transaction.week == current.week + 1)
|
||||
& (Transaction.cancelled == 0)
|
||||
& (Transaction.cancelled == False)
|
||||
)
|
||||
|
||||
for move in all_drops:
|
||||
@ -1533,7 +1524,18 @@ class Standings(BaseModel):
|
||||
with db.atomic():
|
||||
Standings.bulk_create(create_teams)
|
||||
|
||||
# Iterate through each individual result
|
||||
# Pre-fetch all data needed for in-memory processing (avoids N+1 queries)
|
||||
standings_by_team_id = {
|
||||
s.team_id: s
|
||||
for s in Standings.select().where(Standings.team << s_teams)
|
||||
}
|
||||
teams_by_id = {t.id: t for t in Team.select().where(Team.season == season)}
|
||||
divisions_by_id = {
|
||||
d.id: d
|
||||
for d in Division.select().where(Division.season == season)
|
||||
}
|
||||
|
||||
# Iterate through each individual result, tallying wins/losses in memory
|
||||
# for game in Result.select_season(season).where(Result.week <= 22):
|
||||
for game in (
|
||||
StratGame.select()
|
||||
@ -1544,8 +1546,121 @@ class Standings(BaseModel):
|
||||
)
|
||||
.order_by(StratGame.week, StratGame.game_num)
|
||||
):
|
||||
# tally win and loss for each standings object
|
||||
game.update_standings()
|
||||
away_stan = standings_by_team_id.get(game.away_team_id)
|
||||
home_stan = standings_by_team_id.get(game.home_team_id)
|
||||
away_team_obj = teams_by_id.get(game.away_team_id)
|
||||
home_team_obj = teams_by_id.get(game.home_team_id)
|
||||
if None in (away_stan, home_stan, away_team_obj, home_team_obj):
|
||||
continue
|
||||
away_div = divisions_by_id.get(away_team_obj.division_id)
|
||||
home_div = divisions_by_id.get(home_team_obj.division_id)
|
||||
if away_div is None or home_div is None:
|
||||
continue
|
||||
|
||||
# Home Team Won
|
||||
if game.home_score > game.away_score:
|
||||
home_stan.wins += 1
|
||||
home_stan.home_wins += 1
|
||||
away_stan.losses += 1
|
||||
away_stan.away_losses += 1
|
||||
|
||||
if home_stan.streak_wl == 'w':
|
||||
home_stan.streak_num += 1
|
||||
else:
|
||||
home_stan.streak_wl = 'w'
|
||||
home_stan.streak_num = 1
|
||||
|
||||
if away_stan.streak_wl == 'l':
|
||||
away_stan.streak_num += 1
|
||||
else:
|
||||
away_stan.streak_wl = 'l'
|
||||
away_stan.streak_num = 1
|
||||
|
||||
if game.home_score == game.away_score + 1:
|
||||
home_stan.one_run_wins += 1
|
||||
away_stan.one_run_losses += 1
|
||||
|
||||
if away_div.division_abbrev == 'TC':
|
||||
home_stan.div1_wins += 1
|
||||
elif away_div.division_abbrev == 'ETSOS':
|
||||
home_stan.div2_wins += 1
|
||||
elif away_div.division_abbrev == 'APL':
|
||||
home_stan.div3_wins += 1
|
||||
elif away_div.division_abbrev == 'BBC':
|
||||
home_stan.div4_wins += 1
|
||||
|
||||
if home_div.division_abbrev == 'TC':
|
||||
away_stan.div1_losses += 1
|
||||
elif home_div.division_abbrev == 'ETSOS':
|
||||
away_stan.div2_losses += 1
|
||||
elif home_div.division_abbrev == 'APL':
|
||||
away_stan.div3_losses += 1
|
||||
elif home_div.division_abbrev == 'BBC':
|
||||
away_stan.div4_losses += 1
|
||||
|
||||
home_stan.run_diff += game.home_score - game.away_score
|
||||
away_stan.run_diff -= game.home_score - game.away_score
|
||||
# Away Team Won
|
||||
else:
|
||||
home_stan.losses += 1
|
||||
home_stan.home_losses += 1
|
||||
away_stan.wins += 1
|
||||
away_stan.away_wins += 1
|
||||
|
||||
if home_stan.streak_wl == 'l':
|
||||
home_stan.streak_num += 1
|
||||
else:
|
||||
home_stan.streak_wl = 'l'
|
||||
home_stan.streak_num = 1
|
||||
|
||||
if away_stan.streak_wl == 'w':
|
||||
away_stan.streak_num += 1
|
||||
else:
|
||||
away_stan.streak_wl = 'w'
|
||||
away_stan.streak_num = 1
|
||||
|
||||
if game.away_score == game.home_score + 1:
|
||||
home_stan.one_run_losses += 1
|
||||
away_stan.one_run_wins += 1
|
||||
|
||||
if away_div.division_abbrev == 'TC':
|
||||
home_stan.div1_losses += 1
|
||||
elif away_div.division_abbrev == 'ETSOS':
|
||||
home_stan.div2_losses += 1
|
||||
elif away_div.division_abbrev == 'APL':
|
||||
home_stan.div3_losses += 1
|
||||
elif away_div.division_abbrev == 'BBC':
|
||||
home_stan.div4_losses += 1
|
||||
|
||||
if home_div.division_abbrev == 'TC':
|
||||
away_stan.div1_wins += 1
|
||||
elif home_div.division_abbrev == 'ETSOS':
|
||||
away_stan.div2_wins += 1
|
||||
elif home_div.division_abbrev == 'APL':
|
||||
away_stan.div3_wins += 1
|
||||
elif home_div.division_abbrev == 'BBC':
|
||||
away_stan.div4_wins += 1
|
||||
|
||||
home_stan.run_diff -= game.away_score - game.home_score
|
||||
away_stan.run_diff += game.away_score - game.home_score
|
||||
|
||||
# Bulk save all modified standings
|
||||
with db.atomic():
|
||||
Standings.bulk_update(
|
||||
list(standings_by_team_id.values()),
|
||||
fields=[
|
||||
Standings.wins, Standings.losses,
|
||||
Standings.home_wins, Standings.home_losses,
|
||||
Standings.away_wins, Standings.away_losses,
|
||||
Standings.one_run_wins, Standings.one_run_losses,
|
||||
Standings.streak_wl, Standings.streak_num,
|
||||
Standings.run_diff,
|
||||
Standings.div1_wins, Standings.div1_losses,
|
||||
Standings.div2_wins, Standings.div2_losses,
|
||||
Standings.div3_wins, Standings.div3_losses,
|
||||
Standings.div4_wins, Standings.div4_losses,
|
||||
]
|
||||
)
|
||||
|
||||
# Set pythag record and iterate through last 8 games for last8 record
|
||||
for team in all_teams:
|
||||
@ -2367,6 +2482,12 @@ class StratGame(BaseModel):
|
||||
home_stan.save()
|
||||
away_stan.save()
|
||||
|
||||
class Meta:
|
||||
indexes = (
|
||||
(("season",), False),
|
||||
(("season", "week", "game_num"), False),
|
||||
)
|
||||
|
||||
|
||||
class StratPlay(BaseModel):
|
||||
game = ForeignKeyField(StratGame)
|
||||
@ -2441,6 +2562,14 @@ class StratPlay(BaseModel):
|
||||
re24_primary = FloatField(null=True)
|
||||
re24_running = FloatField(null=True)
|
||||
|
||||
class Meta:
|
||||
indexes = (
|
||||
(("game",), False),
|
||||
(("batter",), False),
|
||||
(("pitcher",), False),
|
||||
(("runner",), False),
|
||||
)
|
||||
|
||||
|
||||
class Decision(BaseModel):
|
||||
game = ForeignKeyField(StratGame)
|
||||
|
||||
13
app/main.py
13
app/main.py
@ -8,6 +8,8 @@ from fastapi import Depends, FastAPI, Request
|
||||
from fastapi.openapi.docs import get_swagger_ui_html
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
||||
from .db_engine import db
|
||||
|
||||
# from fastapi.openapi.docs import get_swagger_ui_html
|
||||
# from fastapi.openapi.utils import get_openapi
|
||||
|
||||
@ -68,6 +70,17 @@ app = FastAPI(
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def db_connection_middleware(request: Request, call_next):
|
||||
db.connect(reuse_if_open=True)
|
||||
try:
|
||||
response = await call_next(request)
|
||||
finally:
|
||||
if not db.is_closed():
|
||||
db.close()
|
||||
return response
|
||||
|
||||
|
||||
logger.info(f"Starting up now...")
|
||||
|
||||
|
||||
|
||||
@ -276,5 +276,4 @@ async def get_totalstats(
|
||||
}
|
||||
)
|
||||
|
||||
return_stats["count"] = len(return_stats["stats"])
|
||||
return return_stats
|
||||
|
||||
@ -4,7 +4,7 @@ Thin HTTP layer using PlayerService for business logic.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Query, Response, Depends
|
||||
from typing import Optional, List
|
||||
from typing import Literal, Optional, List
|
||||
|
||||
from ..dependencies import (
|
||||
oauth2_scheme,
|
||||
@ -27,8 +27,10 @@ async def get_players(
|
||||
pos: list = Query(default=None),
|
||||
strat_code: list = Query(default=None),
|
||||
is_injured: Optional[bool] = None,
|
||||
sort: Optional[str] = None,
|
||||
limit: Optional[int] = Query(default=None, ge=1),
|
||||
sort: Optional[Literal["cost-asc", "cost-desc", "name-asc", "name-desc"]] = None,
|
||||
limit: Optional[int] = Query(
|
||||
default=None, ge=1, description="Maximum number of results to return"
|
||||
),
|
||||
offset: Optional[int] = Query(
|
||||
default=None, ge=0, description="Number of results to skip for pagination"
|
||||
),
|
||||
|
||||
@ -17,7 +17,6 @@ from ...dependencies import (
|
||||
add_cache_headers,
|
||||
cache_result,
|
||||
handle_db_errors,
|
||||
MAX_LIMIT,
|
||||
DEFAULT_LIMIT,
|
||||
)
|
||||
from .common import build_season_games
|
||||
@ -58,7 +57,7 @@ async def get_batting_totals(
|
||||
risp: Optional[bool] = None,
|
||||
inning: list = Query(default=None),
|
||||
sort: Optional[str] = None,
|
||||
limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=MAX_LIMIT),
|
||||
limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=1000),
|
||||
short_output: Optional[bool] = False,
|
||||
page_num: Optional[int] = 1,
|
||||
week_start: Optional[int] = None,
|
||||
|
||||
@ -17,7 +17,6 @@ from ...dependencies import (
|
||||
handle_db_errors,
|
||||
add_cache_headers,
|
||||
cache_result,
|
||||
MAX_LIMIT,
|
||||
DEFAULT_LIMIT,
|
||||
)
|
||||
from .common import build_season_games
|
||||
@ -57,7 +56,7 @@ async def get_fielding_totals(
|
||||
team_id: list = Query(default=None),
|
||||
manager_id: list = Query(default=None),
|
||||
sort: Optional[str] = None,
|
||||
limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=MAX_LIMIT),
|
||||
limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=1000),
|
||||
short_output: Optional[bool] = False,
|
||||
page_num: Optional[int] = 1,
|
||||
):
|
||||
|
||||
@ -20,7 +20,6 @@ from ...dependencies import (
|
||||
handle_db_errors,
|
||||
add_cache_headers,
|
||||
cache_result,
|
||||
MAX_LIMIT,
|
||||
DEFAULT_LIMIT,
|
||||
)
|
||||
from .common import build_season_games
|
||||
@ -57,7 +56,7 @@ async def get_pitching_totals(
|
||||
risp: Optional[bool] = None,
|
||||
inning: list = Query(default=None),
|
||||
sort: Optional[str] = None,
|
||||
limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=MAX_LIMIT),
|
||||
limit: int = Query(default=DEFAULT_LIMIT, ge=1, le=1000),
|
||||
short_output: Optional[bool] = False,
|
||||
csv: Optional[bool] = False,
|
||||
page_num: Optional[int] = 1,
|
||||
|
||||
@ -78,14 +78,14 @@ async def get_transactions(
|
||||
transactions = transactions.where(Transaction.player << these_players)
|
||||
|
||||
if cancelled:
|
||||
transactions = transactions.where(Transaction.cancelled == 1)
|
||||
transactions = transactions.where(Transaction.cancelled == True)
|
||||
else:
|
||||
transactions = transactions.where(Transaction.cancelled == 0)
|
||||
transactions = transactions.where(Transaction.cancelled == False)
|
||||
|
||||
if frozen:
|
||||
transactions = transactions.where(Transaction.frozen == 1)
|
||||
transactions = transactions.where(Transaction.frozen == True)
|
||||
else:
|
||||
transactions = transactions.where(Transaction.frozen == 0)
|
||||
transactions = transactions.where(Transaction.frozen == False)
|
||||
|
||||
transactions = transactions.order_by(-Transaction.week, Transaction.moveid)
|
||||
|
||||
|
||||
24
migrations/2026-03-27_add_stratplay_stratgame_indexes.sql
Normal file
24
migrations/2026-03-27_add_stratplay_stratgame_indexes.sql
Normal file
@ -0,0 +1,24 @@
|
||||
-- Migration: Add missing indexes on foreign key columns in stratplay and stratgame
|
||||
-- Created: 2026-03-27
|
||||
--
|
||||
-- PostgreSQL does not auto-index foreign key columns. These tables are the
|
||||
-- highest-volume tables in the schema and are filtered/joined on these columns
|
||||
-- in batting, pitching, and running stats aggregation and standings recalculation.
|
||||
|
||||
-- stratplay: FK join column
|
||||
CREATE INDEX IF NOT EXISTS idx_stratplay_game_id ON stratplay(game_id);
|
||||
|
||||
-- stratplay: filtered in batting stats aggregation
|
||||
CREATE INDEX IF NOT EXISTS idx_stratplay_batter_id ON stratplay(batter_id);
|
||||
|
||||
-- stratplay: filtered in pitching stats aggregation
|
||||
CREATE INDEX IF NOT EXISTS idx_stratplay_pitcher_id ON stratplay(pitcher_id);
|
||||
|
||||
-- stratplay: filtered in running stats
|
||||
CREATE INDEX IF NOT EXISTS idx_stratplay_runner_id ON stratplay(runner_id);
|
||||
|
||||
-- stratgame: heavily filtered by season
|
||||
CREATE INDEX IF NOT EXISTS idx_stratgame_season ON stratgame(season);
|
||||
|
||||
-- stratgame: standings recalculation query ordering
|
||||
CREATE INDEX IF NOT EXISTS idx_stratgame_season_week_game_num ON stratgame(season, week, game_num);
|
||||
@ -569,7 +569,7 @@ class TestGroupBySbaPlayer:
|
||||
# Get per-season rows
|
||||
r_seasons = requests.get(
|
||||
f"{api}/api/v3/plays/batting",
|
||||
params={"group_by": "player", "sbaplayer_id": 1, "limit": 999},
|
||||
params={"group_by": "player", "sbaplayer_id": 1, "limit": 500},
|
||||
timeout=15,
|
||||
)
|
||||
assert r_seasons.status_code == 200
|
||||
|
||||
Loading…
Reference in New Issue
Block a user