- Add db_helpers.py with cross-database upsert functions for SQLite/PostgreSQL - Replace 12 on_conflict_replace() calls with PostgreSQL-compatible upserts - Add unique indexes: StratPlay(game, play_num), Decision(game, pitcher) - Add max_length to Team model fields (abbrev, sname, lname) - Fix boolean comparison in teams.py (== 0/1 to == False/True) - Create migrate_to_postgres.py with ID-preserving migration logic - Create audit_sqlite.py for pre-migration data integrity checks - Add PROJECT_PLAN.json for migration tracking - Add .secrets/ to .gitignore for credentials Audit results: 658,963 records across 29 tables, 2,390 orphaned stats (expected) Based on Major Domo migration lessons learned (33 issues resolved there)
382 lines
12 KiB
Python
382 lines
12 KiB
Python
import datetime
|
|
import random
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Response, Query
|
|
from typing import Optional, List
|
|
import logging
|
|
import pydantic
|
|
from pandas import DataFrame
|
|
|
|
from ..db_engine import (
|
|
db,
|
|
MlbPlayer,
|
|
Player,
|
|
BattingCard,
|
|
PitchingCard,
|
|
model_to_dict,
|
|
fn,
|
|
chunked,
|
|
query_to_csv,
|
|
)
|
|
from ..db_helpers import upsert_mlb_players
|
|
from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
|
|
|
|
logging.basicConfig(
|
|
filename=LOG_DATA["filename"],
|
|
format=LOG_DATA["format"],
|
|
level=LOG_DATA["log_level"],
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/v2/mlbplayers", tags=["mlbplayers"])
|
|
|
|
|
|
class PlayerModel(pydantic.BaseModel):
|
|
first_name: str
|
|
last_name: str
|
|
key_mlbam: int
|
|
key_fangraphs: int = None
|
|
key_bbref: str = None
|
|
key_retro: str = None
|
|
offense_col: int = random.randint(1, 3)
|
|
|
|
|
|
class PlayerList(pydantic.BaseModel):
|
|
players: List[PlayerModel]
|
|
|
|
|
|
def update_card_urls(mlbplayer: MlbPlayer):
|
|
logging.info(
|
|
f"Updating cards for mlbplayer: {mlbplayer.first_name} {mlbplayer.last_name} ({mlbplayer.key_bbref})"
|
|
)
|
|
now = datetime.datetime.now()
|
|
c1_update = Player.update(
|
|
{
|
|
Player.image: Player.image.name.split("?d=")[0]
|
|
+ f"?d={now.year}-{now.month}-{now.day}"
|
|
}
|
|
).where(Player.mlbplayer_id == mlbplayer.id)
|
|
count = c1_update.execute()
|
|
logging.info(f"Updated {count} image1s")
|
|
|
|
c2_update = Player.update(
|
|
{
|
|
Player.image2: Player.image2.name.split("?d=")[0]
|
|
+ f"{now.year}-{now.month}-{now.day}"
|
|
}
|
|
).where((Player.mlbplayer_id == mlbplayer.id) & (Player.image2.is_null(False)))
|
|
count2 = c2_update.execute()
|
|
logging.info(f"Updated {count2} image2s")
|
|
|
|
return count + count2
|
|
|
|
|
|
@router.get("")
|
|
async def get_players(
|
|
full_name: list = Query(default=None),
|
|
first_name: list = Query(default=None),
|
|
last_name: list = Query(default=None),
|
|
key_fangraphs: list = Query(default=None),
|
|
key_bbref: list = Query(default=None),
|
|
key_retro: list = Query(default=None),
|
|
key_mlbam: list = Query(default=None),
|
|
offense_col: list = Query(default=None),
|
|
csv: Optional[bool] = False,
|
|
):
|
|
all_players = MlbPlayer.select()
|
|
|
|
if full_name is not None:
|
|
name_list = [x.lower() for x in full_name]
|
|
all_players = all_players.where(
|
|
fn.lower(MlbPlayer.first_name) + " " + fn.lower(MlbPlayer.last_name)
|
|
<< name_list
|
|
)
|
|
if first_name is not None:
|
|
name_list = [x.lower() for x in first_name]
|
|
all_players = all_players.where(fn.lower(MlbPlayer.first_name) << name_list)
|
|
if last_name is not None:
|
|
name_list = [x.lower() for x in last_name]
|
|
all_players = all_players.where(fn.lower(MlbPlayer.last_name) << name_list)
|
|
if key_fangraphs is not None:
|
|
all_players = all_players.where(MlbPlayer.key_fangraphs << key_fangraphs)
|
|
if key_bbref is not None:
|
|
name_list = [x.lower() for x in key_bbref]
|
|
all_players = all_players.where(fn.lower(MlbPlayer.key_bbref) << name_list)
|
|
if key_retro is not None:
|
|
name_list = [x.lower() for x in key_retro]
|
|
all_players = all_players.where(fn.lower(MlbPlayer.key_retro) << name_list)
|
|
if key_mlbam is not None:
|
|
all_players = all_players.where(MlbPlayer.key_mlbam << key_mlbam)
|
|
if offense_col is not None:
|
|
all_players = all_players.where(MlbPlayer.offense_col << offense_col)
|
|
|
|
if csv:
|
|
return_val = query_to_csv(all_players)
|
|
db.close()
|
|
return Response(content=return_val, media_type="text/csv")
|
|
|
|
return_val = {
|
|
"count": all_players.count(),
|
|
"players": [model_to_dict(x) for x in all_players],
|
|
}
|
|
db.close()
|
|
return return_val
|
|
|
|
|
|
@router.get("/{player_id}")
|
|
async def get_one_player(player_id: int):
|
|
this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id)
|
|
if this_player is None:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"MlbPlayer id {player_id} not found"
|
|
)
|
|
|
|
r_data = model_to_dict(this_player)
|
|
db.close()
|
|
return r_data
|
|
|
|
|
|
@router.patch("/{player_id}")
|
|
async def patch_player(
|
|
player_id: int,
|
|
first_name: Optional[str] = None,
|
|
last_name: Optional[str] = None,
|
|
key_fangraphs: Optional[str] = None,
|
|
key_bbref: Optional[str] = None,
|
|
key_retro: Optional[str] = None,
|
|
key_mlbam: Optional[str] = None,
|
|
offense_col: Optional[str] = None,
|
|
token: str = Depends(oauth2_scheme),
|
|
):
|
|
if not valid_token(token):
|
|
logging.warning(f"Bad Token: {token}")
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="You are not authorized to patch mlb players. This event has been logged.",
|
|
)
|
|
|
|
this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id)
|
|
if this_player is None:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"MlbPlayer id {player_id} not found"
|
|
)
|
|
|
|
if first_name is not None:
|
|
this_player.first_name = first_name
|
|
if last_name is not None:
|
|
this_player.last_name = last_name
|
|
if key_fangraphs is not None:
|
|
this_player.key_fangraphs = key_fangraphs
|
|
if key_bbref is not None:
|
|
this_player.key_bbref = key_bbref
|
|
if key_retro is not None:
|
|
this_player.key_retro = key_retro
|
|
if key_mlbam is not None:
|
|
this_player.key_mlbam = key_mlbam
|
|
if offense_col is not None:
|
|
this_player.offense_col = offense_col
|
|
|
|
if this_player.save() == 1:
|
|
return_val = model_to_dict(this_player)
|
|
db.close()
|
|
return return_val
|
|
else:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=418,
|
|
detail="Well slap my ass and call me a teapot; I could not save that player",
|
|
)
|
|
|
|
|
|
@router.post("")
|
|
async def post_players(players: PlayerList, token: str = Depends(oauth2_scheme)):
|
|
if not valid_token(token):
|
|
logging.warning(f"Bad Token: {token}")
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="You are not authorized to post mlb players. This event has been logged.",
|
|
)
|
|
|
|
new_players = []
|
|
for x in players.players:
|
|
dupes = MlbPlayer.select().where(
|
|
(MlbPlayer.key_fangraphs == x.key_fangraphs)
|
|
| (MlbPlayer.key_mlbam == x.key_mlbam)
|
|
| (MlbPlayer.key_retro == x.key_retro)
|
|
| (MlbPlayer.key_bbref == x.key_bbref)
|
|
)
|
|
if dupes.count() > 0:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"{x.first_name} {x.last_name} has a key already in the database",
|
|
)
|
|
|
|
new_players.append(x.dict())
|
|
|
|
with db.atomic():
|
|
# Use PostgreSQL-compatible upsert helper
|
|
# Note: Duplicate check is already done above, so this is effectively just insert
|
|
upsert_mlb_players(new_players, batch_size=15)
|
|
db.close()
|
|
|
|
return f"Inserted {len(new_players)} new MLB players"
|
|
|
|
|
|
@router.post("/one")
|
|
async def post_one_player(player: PlayerModel, token: str = Depends(oauth2_scheme)):
|
|
if not valid_token(token):
|
|
logging.warning(f"Bad Token: {token}")
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="You are not authorized to post mlb players. This event has been logged.",
|
|
)
|
|
|
|
dupes = MlbPlayer.select().where(
|
|
(MlbPlayer.key_fangraphs == player.key_fangraphs)
|
|
| (MlbPlayer.key_mlbam == player.key_mlbam)
|
|
| (MlbPlayer.key_bbref == player.key_bbref)
|
|
)
|
|
if dupes.count() > 0:
|
|
logging.info(f"POST /mlbplayers/one - dupes found:")
|
|
for x in dupes:
|
|
logging.info(f"{x}")
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"{player.first_name} {player.last_name} has a key already in the database",
|
|
)
|
|
|
|
new_player = MlbPlayer(**player.dict())
|
|
saved = new_player.save()
|
|
if saved == 1:
|
|
return_val = model_to_dict(new_player)
|
|
db.close()
|
|
return return_val
|
|
else:
|
|
raise HTTPException(
|
|
status_code=418,
|
|
detail="Well slap my ass and call me a teapot; I could not save that player",
|
|
)
|
|
|
|
|
|
@router.delete("/{player_id}")
|
|
async def delete_player(player_id: int, token: str = Depends(oauth2_scheme)):
|
|
if not valid_token(token):
|
|
logging.warning(f"Bad Token: {token}")
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="You are not authorized to delete mlb players. This event has been logged.",
|
|
)
|
|
|
|
this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id)
|
|
if this_player is None:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"MlbPlayer id {player_id} not found"
|
|
)
|
|
|
|
count = this_player.delete_instance()
|
|
db.close()
|
|
|
|
if count == 1:
|
|
raise HTTPException(
|
|
status_code=200, detail=f"Player {player_id} has been deleted"
|
|
)
|
|
else:
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Player {player_id} was not deleted"
|
|
)
|
|
|
|
|
|
@router.post("/update-cols")
|
|
@router.post("/update-cols/{mlbplayer_id}")
|
|
async def update_columns(
|
|
mlbplayer_id: Optional[int] = None, token: str = Depends(oauth2_scheme)
|
|
):
|
|
if not valid_token(token):
|
|
logging.warning(f"Bad Token: {token}")
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="You are not authorized to update mlb players. This event has been logged.",
|
|
)
|
|
|
|
p_query = MlbPlayer.select()
|
|
if mlbplayer_id is not None:
|
|
p_query = p_query.where(MlbPlayer.id == mlbplayer_id)
|
|
|
|
total_count = 0
|
|
for x in p_query:
|
|
all_players = Player.select().where(Player.mlbplayer == x)
|
|
bc_update = BattingCard.update(
|
|
{
|
|
BattingCard.offense_col: x.offense_col,
|
|
}
|
|
).where(
|
|
(BattingCard.player << all_players)
|
|
& (BattingCard.offense_col != x.offense_col)
|
|
)
|
|
count = bc_update.execute()
|
|
total_count += count
|
|
logging.info(f"Updated {count} batting cards for {x.first_name} {x.last_name}")
|
|
update_card_urls(x)
|
|
|
|
db.close()
|
|
return f"Updated {total_count} batting cards"
|
|
|
|
|
|
@router.post("/update-names")
|
|
@router.post("/update-names/{mlbplayer_id}")
|
|
async def update_names(
|
|
mlbplayer_id: Optional[int] = None, token: str = Depends(oauth2_scheme)
|
|
):
|
|
if not valid_token(token):
|
|
logging.warning(f"Bad Token: {token}")
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="You are not authorized to update mlb players. This event has been logged.",
|
|
)
|
|
|
|
p_query = MlbPlayer.select()
|
|
if mlbplayer_id is not None:
|
|
p_query = p_query.where(MlbPlayer.id == mlbplayer_id)
|
|
|
|
total_count = 0
|
|
for x in p_query:
|
|
p_update = Player.update(
|
|
{Player.p_name: f"{x.first_name} {x.last_name}"}
|
|
).where(
|
|
(Player.mlbplayer == x) & (Player.p_name != f"{x.first_name} {x.last_name}")
|
|
)
|
|
count = p_update.execute()
|
|
total_count += count
|
|
logging.info(f"Update {count} player records for {x.first_name} {x.last_name}")
|
|
update_card_urls(x)
|
|
|
|
db.close()
|
|
return f"Updated {total_count} names"
|
|
|
|
|
|
# @router.post('/link-players')
|
|
# async def post_players(token: str = Depends(oauth2_scheme)):
|
|
# if not valid_token(token):
|
|
# logging.warning(f'Bad Token: {token}')
|
|
# db.close()
|
|
# raise HTTPException(
|
|
# status_code=401,
|
|
# detail='You are not authorized to link mlb players. This event has been logged.'
|
|
# )
|
|
#
|
|
# for x in Player.select().where(Player.mlbplayer.is_null()):
|
|
# p_query = MlbPlayer.get_or_none(MlbPlayer.key_bbref == x.key_bbref)
|
|
# if p_query is not None:
|
|
# x.mlbplayer = p_query
|
|
# x.save()
|