- 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)
1177 lines
40 KiB
Python
1177 lines
40 KiB
Python
import datetime
|
|
import os.path
|
|
import base64
|
|
|
|
import pandas as pd
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response, Query
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from html2image import Html2Image
|
|
from typing import Optional, List, Literal
|
|
import logging
|
|
import pydantic
|
|
from pandas import DataFrame
|
|
from playwright.async_api import async_playwright
|
|
|
|
from ..card_creation import get_batter_card_data, get_pitcher_card_data
|
|
from ..db_engine import (
|
|
db,
|
|
Player,
|
|
model_to_dict,
|
|
fn,
|
|
chunked,
|
|
Paperdex,
|
|
Cardset,
|
|
Rarity,
|
|
BattingCard,
|
|
BattingCardRatings,
|
|
PitchingCard,
|
|
PitchingCardRatings,
|
|
CardPosition,
|
|
MlbPlayer,
|
|
)
|
|
from ..db_helpers import upsert_players
|
|
from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
|
|
|
|
# Franchise normalization: Convert city+team names to city-agnostic team names
|
|
# This enables cross-era player matching (e.g., 'Oakland Athletics' -> 'Athletics')
|
|
FRANCHISE_NORMALIZE = {
|
|
"Arizona Diamondbacks": "Diamondbacks",
|
|
"Atlanta Braves": "Braves",
|
|
"Baltimore Orioles": "Orioles",
|
|
"Boston Red Sox": "Red Sox",
|
|
"Chicago Cubs": "Cubs",
|
|
"Chicago White Sox": "White Sox",
|
|
"Cincinnati Reds": "Reds",
|
|
"Cleveland Guardians": "Guardians",
|
|
"Colorado Rockies": "Rockies",
|
|
"Detroit Tigers": "Tigers",
|
|
"Houston Astros": "Astros",
|
|
"Kansas City Royals": "Royals",
|
|
"Los Angeles Angels": "Angels",
|
|
"Los Angeles Dodgers": "Dodgers",
|
|
"Miami Marlins": "Marlins",
|
|
"Milwaukee Brewers": "Brewers",
|
|
"Minnesota Twins": "Twins",
|
|
"New York Mets": "Mets",
|
|
"New York Yankees": "Yankees",
|
|
"Oakland Athletics": "Athletics",
|
|
"Philadelphia Phillies": "Phillies",
|
|
"Pittsburgh Pirates": "Pirates",
|
|
"San Diego Padres": "Padres",
|
|
"San Francisco Giants": "Giants",
|
|
"Seattle Mariners": "Mariners",
|
|
"St Louis Cardinals": "Cardinals",
|
|
"St. Louis Cardinals": "Cardinals",
|
|
"Tampa Bay Rays": "Rays",
|
|
"Texas Rangers": "Rangers",
|
|
"Toronto Blue Jays": "Blue Jays",
|
|
"Washington Nationals": "Nationals",
|
|
}
|
|
|
|
|
|
def normalize_franchise(franchise: str) -> str:
|
|
"""Convert city+team name to team-only (e.g., 'Oakland Athletics' -> 'Athletics')"""
|
|
titled = franchise.title()
|
|
return FRANCHISE_NORMALIZE.get(titled, titled)
|
|
|
|
|
|
logging.basicConfig(
|
|
filename=LOG_DATA["filename"],
|
|
format=LOG_DATA["format"],
|
|
level=LOG_DATA["log_level"],
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/v2/players", tags=["players"])
|
|
|
|
|
|
templates = Jinja2Templates(directory="storage/templates")
|
|
|
|
|
|
class PlayerPydantic(pydantic.BaseModel):
|
|
player_id: int = None
|
|
p_name: str
|
|
cost: int
|
|
image: str
|
|
image2: Optional[str] = None
|
|
mlbclub: str
|
|
franchise: str
|
|
cardset_id: int
|
|
set_num: int
|
|
rarity_id: int
|
|
pos_1: str
|
|
pos_2: Optional[str] = None
|
|
pos_3: Optional[str] = None
|
|
pos_4: Optional[str] = None
|
|
pos_5: Optional[str] = None
|
|
pos_6: Optional[str] = None
|
|
pos_7: Optional[str] = None
|
|
pos_8: Optional[str] = None
|
|
headshot: Optional[str] = None
|
|
vanity_card: Optional[str] = None
|
|
strat_code: Optional[str] = None
|
|
bbref_id: Optional[str] = None
|
|
fangr_id: Optional[str] = None
|
|
description: str
|
|
quantity: Optional[int] = 999
|
|
mlbplayer_id: Optional[int] = None
|
|
|
|
|
|
class PlayerModel(pydantic.BaseModel):
|
|
players: List[PlayerPydantic]
|
|
|
|
|
|
@router.get("")
|
|
async def get_players(
|
|
name: Optional[str] = None,
|
|
value: Optional[int] = None,
|
|
min_cost: Optional[int] = None,
|
|
max_cost: Optional[int] = None,
|
|
has_image2: Optional[bool] = None,
|
|
mlbclub: Optional[str] = None,
|
|
franchise: Optional[str] = None,
|
|
cardset_id: list = Query(default=None),
|
|
rarity_id: list = Query(default=None),
|
|
pos_include: list = Query(default=None),
|
|
pos_exclude: list = Query(default=None),
|
|
has_headshot: Optional[bool] = None,
|
|
has_vanity_card: Optional[bool] = None,
|
|
strat_code: Optional[str] = None,
|
|
bbref_id: Optional[str] = None,
|
|
fangr_id: Optional[str] = None,
|
|
inc_dex: Optional[bool] = True,
|
|
in_desc: Optional[str] = None,
|
|
flat: Optional[bool] = False,
|
|
sort_by: Optional[str] = False,
|
|
cardset_id_exclude: list = Query(default=None),
|
|
limit: Optional[int] = None,
|
|
csv: Optional[bool] = None,
|
|
short_output: Optional[bool] = False,
|
|
mlbplayer_id: Optional[int] = None,
|
|
inc_keys: Optional[bool] = False,
|
|
):
|
|
all_players = Player.select()
|
|
if all_players.count() == 0:
|
|
db.close()
|
|
raise HTTPException(status_code=404, detail=f"There are no players to filter")
|
|
|
|
if name is not None:
|
|
all_players = all_players.where(fn.Lower(Player.p_name) == name.lower())
|
|
if value is not None:
|
|
all_players = all_players.where(Player.cost == value)
|
|
if min_cost is not None:
|
|
all_players = all_players.where(Player.cost >= min_cost)
|
|
if max_cost is not None:
|
|
all_players = all_players.where(Player.cost <= max_cost)
|
|
if has_image2 is not None:
|
|
all_players = all_players.where(Player.image2.is_null(not has_image2))
|
|
if mlbclub is not None:
|
|
all_players = all_players.where(fn.Lower(Player.mlbclub) == mlbclub.lower())
|
|
if franchise is not None:
|
|
all_players = all_players.where(fn.Lower(Player.franchise) == franchise.lower())
|
|
if cardset_id is not None:
|
|
all_players = all_players.where(Player.cardset_id << cardset_id)
|
|
if cardset_id_exclude is not None:
|
|
all_players = all_players.where(Player.cardset_id.not_in(cardset_id_exclude))
|
|
if rarity_id is not None:
|
|
all_players = all_players.where(Player.rarity_id << rarity_id)
|
|
if pos_include is not None:
|
|
p_list = [x.upper() for x in pos_include]
|
|
all_players = all_players.where(
|
|
(Player.pos_1 << p_list)
|
|
| (Player.pos_2 << p_list)
|
|
| (Player.pos_3 << p_list)
|
|
| (Player.pos_4 << p_list)
|
|
| (Player.pos_5 << p_list)
|
|
| (Player.pos_6 << p_list)
|
|
| (Player.pos_7 << p_list)
|
|
| (Player.pos_8 << p_list)
|
|
)
|
|
if has_headshot is not None:
|
|
all_players = all_players.where(Player.headshot.is_null(not has_headshot))
|
|
if has_vanity_card is not None:
|
|
all_players = all_players.where(Player.vanity_card.is_null(not has_vanity_card))
|
|
if strat_code is not None:
|
|
all_players = all_players.where(Player.strat_code == strat_code)
|
|
if bbref_id is not None:
|
|
all_players = all_players.where(Player.bbref_id == bbref_id)
|
|
if fangr_id is not None:
|
|
all_players = all_players.where(Player.fangr_id == fangr_id)
|
|
if mlbplayer_id is not None:
|
|
all_players = all_players.where(Player.mlbplayer_id == mlbplayer_id)
|
|
if in_desc is not None:
|
|
all_players = all_players.where(
|
|
fn.Lower(Player.description).contains(in_desc.lower())
|
|
)
|
|
|
|
if sort_by is not None:
|
|
if sort_by == "cost-desc":
|
|
all_players = all_players.order_by(-Player.cost)
|
|
elif sort_by == "cost-asc":
|
|
all_players = all_players.order_by(Player.cost)
|
|
elif sort_by == "name-asc":
|
|
all_players = all_players.order_by(Player.p_name)
|
|
elif sort_by == "name-desc":
|
|
all_players = all_players.order_by(-Player.p_name)
|
|
elif sort_by == "rarity-desc":
|
|
all_players = all_players.order_by(Player.rarity)
|
|
elif sort_by == "rarity-asc":
|
|
all_players = all_players.order_by(-Player.rarity)
|
|
|
|
final_players = []
|
|
# logging.info(f'pos_exclude: {type(pos_exclude)} - {pos_exclude} - is None: {pos_exclude is None}')
|
|
for x in all_players:
|
|
if pos_exclude is not None and set(
|
|
[x.upper() for x in pos_exclude]
|
|
).intersection(x.get_all_pos()):
|
|
pass
|
|
else:
|
|
final_players.append(x)
|
|
|
|
if limit is not None and len(final_players) >= limit:
|
|
break
|
|
|
|
# if len(final_players) == 0:
|
|
# db.close()
|
|
# raise HTTPException(status_code=404, detail=f'No players found')
|
|
|
|
if csv:
|
|
card_vals = [model_to_dict(x) for x in all_players]
|
|
db.close()
|
|
|
|
for x in card_vals:
|
|
x["player_name"] = x["p_name"]
|
|
x["cardset_name"] = x["cardset"]["name"]
|
|
x["rarity"] = x["rarity"]["name"]
|
|
x["for_purchase"] = x["cardset"]["for_purchase"]
|
|
x["ranked_legal"] = x["cardset"]["ranked_legal"]
|
|
if x["player_name"] not in x["description"]:
|
|
x["description"] = f"{x['description']} {x['player_name']}"
|
|
|
|
card_df = pd.DataFrame(card_vals)
|
|
output = card_df[
|
|
[
|
|
"player_id",
|
|
"player_name",
|
|
"cost",
|
|
"image",
|
|
"image2",
|
|
"mlbclub",
|
|
"franchise",
|
|
"cardset_name",
|
|
"rarity",
|
|
"pos_1",
|
|
"pos_2",
|
|
"pos_3",
|
|
"pos_4",
|
|
"pos_5",
|
|
"pos_6",
|
|
"pos_7",
|
|
"pos_8",
|
|
"headshot",
|
|
"vanity_card",
|
|
"fangr_id",
|
|
"bbref_id",
|
|
"description",
|
|
"for_purchase",
|
|
"ranked_legal",
|
|
]
|
|
]
|
|
return Response(
|
|
content=pd.DataFrame(output).to_csv(index=False), media_type="text/csv"
|
|
)
|
|
|
|
# all_players.order_by(-Player.rarity.value, Player.p_name)
|
|
# data_list = [['id', 'name', 'value', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1',
|
|
# 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card',
|
|
# 'strat_code', 'bbref_id', 'description', 'for_purchase', 'ranked_legal']]
|
|
# for line in final_players:
|
|
# data_list.append(
|
|
# [
|
|
# line.player_id, line.p_name, line.cost, line.image, line.image2, line.mlbclub, line.franchise,
|
|
# line.cardset, line.rarity, line.pos_1, line.pos_2, line.pos_3, line.pos_4, line.pos_5, line.pos_6,
|
|
# line.pos_7, line.pos_8, line.headshot, line.vanity_card, line.strat_code, line.bbref_id,
|
|
# line.description, line.cardset.for_purchase, line.cardset.ranked_legal
|
|
# # line.description, line.cardset.in_packs, line.quantity
|
|
# ]
|
|
# )
|
|
# return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
|
#
|
|
# db.close()
|
|
# return Response(content=return_val, media_type='text/csv')
|
|
|
|
else:
|
|
return_val = {"count": len(final_players), "players": []}
|
|
for x in final_players:
|
|
this_record = model_to_dict(x, recurse=not (flat or short_output))
|
|
|
|
if inc_dex:
|
|
this_dex = Paperdex.select().where(Paperdex.player == x)
|
|
this_record["paperdex"] = {"count": this_dex.count(), "paperdex": []}
|
|
for y in this_dex:
|
|
this_record["paperdex"]["paperdex"].append(
|
|
model_to_dict(y, recurse=False)
|
|
)
|
|
|
|
if inc_keys and (flat or short_output):
|
|
if this_record["mlbplayer"] is not None:
|
|
this_mlb = MlbPlayer.get_by_id(this_record["mlbplayer"])
|
|
this_record["key_mlbam"] = this_mlb.key_mlbam
|
|
this_record["key_fangraphs"] = this_mlb.key_fangraphs
|
|
this_record["key_bbref"] = this_mlb.key_bbref
|
|
this_record["key_retro"] = this_mlb.key_retro
|
|
this_record["offense_col"] = this_mlb.offense_col
|
|
|
|
return_val["players"].append(this_record)
|
|
|
|
# return_val['players'].append(model_to_dict(x, recurse=not flat))
|
|
|
|
db.close()
|
|
return return_val
|
|
|
|
|
|
@router.get("/random")
|
|
async def get_random_player(
|
|
min_cost: Optional[int] = None,
|
|
max_cost: Optional[int] = None,
|
|
in_packs: Optional[bool] = None,
|
|
min_rarity: Optional[int] = None,
|
|
max_rarity: Optional[int] = None,
|
|
limit: Optional[int] = None,
|
|
pos_include: Optional[str] = None,
|
|
pos_exclude: Optional[str] = None,
|
|
franchise: Optional[str] = None,
|
|
mlbclub: Optional[str] = None,
|
|
cardset_id: list = Query(default=None),
|
|
pos_inc: list = Query(default=None),
|
|
pos_exc: list = Query(default=None),
|
|
csv: Optional[bool] = None,
|
|
):
|
|
all_players = (
|
|
Player.select().join(Cardset).switch(Player).join(Rarity).order_by(fn.Random())
|
|
)
|
|
|
|
if min_cost is not None:
|
|
all_players = all_players.where(Player.cost >= min_cost)
|
|
if max_cost is not None:
|
|
all_players = all_players.where(Player.cost <= max_cost)
|
|
if in_packs is not None:
|
|
if in_packs:
|
|
all_players = all_players.where(Player.cardset.in_packs)
|
|
if min_rarity is not None:
|
|
all_players = all_players.where(Player.rarity.value >= min_rarity)
|
|
if max_rarity is not None:
|
|
all_players = all_players.where(Player.rarity.value <= max_rarity)
|
|
if pos_include is not None:
|
|
all_players = all_players.where(
|
|
(fn.lower(Player.pos_1) == pos_include.lower())
|
|
| (fn.lower(Player.pos_2) == pos_include.lower())
|
|
| (fn.lower(Player.pos_3) == pos_include.lower())
|
|
| (fn.lower(Player.pos_4) == pos_include.lower())
|
|
| (fn.lower(Player.pos_5) == pos_include.lower())
|
|
| (fn.lower(Player.pos_6) == pos_include.lower())
|
|
| (fn.lower(Player.pos_7) == pos_include.lower())
|
|
| (fn.lower(Player.pos_8) == pos_include.lower())
|
|
)
|
|
if franchise is not None:
|
|
all_players = all_players.where(fn.Lower(Player.franchise) == franchise.lower())
|
|
if mlbclub is not None:
|
|
all_players = all_players.where(fn.Lower(Player.mlbclub) == mlbclub.lower())
|
|
if cardset_id is not None:
|
|
all_players = all_players.where(Player.cardset_id << cardset_id)
|
|
if pos_inc is not None:
|
|
p_list = [x.upper() for x in pos_inc]
|
|
all_players = all_players.where(
|
|
(Player.pos_1 << p_list)
|
|
| (Player.pos_2 << p_list)
|
|
| (Player.pos_3 << p_list)
|
|
| (Player.pos_4 << p_list)
|
|
| (Player.pos_5 << p_list)
|
|
| (Player.pos_6 << p_list)
|
|
| (Player.pos_7 << p_list)
|
|
| (Player.pos_8 << p_list)
|
|
)
|
|
# if pos_exc is not None:
|
|
# p_list = [x.upper() for x in pos_exc]
|
|
# logging.info(f'starting query: {all_players}\n\np_list: {p_list}\n\n')
|
|
# all_players = all_players.where(
|
|
# Player.pos_1.not_in(p_list) & Player.pos_2.not_in(p_list) & Player.pos_3.not_in(p_list) &
|
|
# Player.pos_4.not_in(p_list) & Player.pos_5.not_in(p_list) & Player.pos_6.not_in(p_list) &
|
|
# Player.pos_7.not_in(p_list) & Player.pos_8.not_in(p_list)
|
|
# )
|
|
# logging.info(f'post pos query: {all_players}')
|
|
|
|
if pos_exclude is not None and pos_exc is None:
|
|
final_players = [x for x in all_players if pos_exclude not in x.get_all_pos()]
|
|
elif pos_exc is not None and pos_exclude is None:
|
|
final_players = []
|
|
p_list = [x.upper() for x in pos_exc]
|
|
for x in all_players:
|
|
if limit is not None and len(final_players) >= limit:
|
|
break
|
|
if not set(p_list).intersection(x.get_all_pos()):
|
|
final_players.append(x)
|
|
else:
|
|
final_players = all_players
|
|
|
|
if limit is not None:
|
|
final_players = final_players[:limit]
|
|
|
|
# if len(final_players) == 0:
|
|
# db.close()
|
|
# raise HTTPException(status_code=404, detail=f'No players found')
|
|
|
|
if csv:
|
|
data_list = [
|
|
[
|
|
"id",
|
|
"name",
|
|
"cost",
|
|
"image",
|
|
"image2",
|
|
"mlbclub",
|
|
"franchise",
|
|
"cardset",
|
|
"rarity",
|
|
"pos_1",
|
|
"pos_2",
|
|
"pos_3",
|
|
"pos_4",
|
|
"pos_5",
|
|
"pos_6",
|
|
"pos_7",
|
|
"pos_8",
|
|
"headshot",
|
|
"vanity_card",
|
|
"strat_code",
|
|
"bbref_id",
|
|
"description",
|
|
]
|
|
]
|
|
for line in final_players:
|
|
data_list.append(
|
|
[
|
|
line.id,
|
|
line.p_name,
|
|
line.cost,
|
|
line.image,
|
|
line.image2,
|
|
line.mlbclub,
|
|
line.franchise,
|
|
line.cardset.name,
|
|
line.rarity.name,
|
|
line.pos_1,
|
|
line.pos_2,
|
|
line.pos_3,
|
|
line.pos_4,
|
|
line.pos_5,
|
|
line.pos_6,
|
|
line.pos_7,
|
|
line.pos_8,
|
|
line.headshot,
|
|
line.vanity_card,
|
|
line.strat_code,
|
|
line.bbref_id,
|
|
line.description,
|
|
]
|
|
)
|
|
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
|
|
|
db.close()
|
|
return Response(content=return_val, media_type="text/csv")
|
|
|
|
else:
|
|
return_val = {"count": len(final_players), "players": []}
|
|
for x in final_players:
|
|
this_record = model_to_dict(x)
|
|
|
|
this_dex = Paperdex.select().where(Paperdex.player == x)
|
|
this_record["paperdex"] = {"count": this_dex.count(), "paperdex": []}
|
|
for y in this_dex:
|
|
this_record["paperdex"]["paperdex"].append(
|
|
model_to_dict(y, recurse=False)
|
|
)
|
|
|
|
return_val["players"].append(this_record)
|
|
# return_val['players'].append(model_to_dict(x))
|
|
|
|
db.close()
|
|
return return_val
|
|
|
|
|
|
@router.get("/search")
|
|
async def search_players(
|
|
q: str = Query(..., description="Search query for player name"),
|
|
cardset_id: list = Query(default=None),
|
|
rarity_id: list = Query(default=None),
|
|
limit: int = Query(
|
|
default=25, ge=1, le=100, description="Maximum number of results to return"
|
|
),
|
|
unique_names: bool = Query(
|
|
default=False, description="Return only unique player names (highest player_id)"
|
|
),
|
|
short_output: bool = False,
|
|
):
|
|
"""
|
|
Real-time fuzzy search for players by name.
|
|
|
|
Returns players matching the query with exact matches prioritized over partial matches.
|
|
When unique_names=True, only returns one player per unique name (the one with highest player_id).
|
|
"""
|
|
# Start with all players
|
|
all_players = Player.select()
|
|
|
|
# Apply name filter (partial match)
|
|
all_players = all_players.where(fn.Lower(Player.p_name).contains(q.lower()))
|
|
|
|
# Apply optional filters
|
|
if cardset_id is not None:
|
|
all_players = all_players.where(Player.cardset_id << cardset_id)
|
|
|
|
if rarity_id is not None:
|
|
all_players = all_players.where(Player.rarity_id << rarity_id)
|
|
|
|
# Convert to list for sorting
|
|
players_list = list(all_players)
|
|
|
|
# Sort by relevance (exact matches first, then name starts, then partial)
|
|
query_lower = q.lower()
|
|
exact_matches = []
|
|
name_start_matches = []
|
|
partial_matches = []
|
|
|
|
for player in players_list:
|
|
name_lower = player.p_name.lower()
|
|
if name_lower == query_lower:
|
|
exact_matches.append(player)
|
|
else:
|
|
# Check if query matches the start of first or last name
|
|
name_parts = name_lower.split()
|
|
starts_with_match = any(part.startswith(query_lower) for part in name_parts)
|
|
|
|
if starts_with_match:
|
|
name_start_matches.append(player)
|
|
elif query_lower in name_lower:
|
|
partial_matches.append(player)
|
|
|
|
# Combine results (exact, then name starts, then partial)
|
|
results = exact_matches + name_start_matches + partial_matches
|
|
|
|
# Deduplicate by name if requested (keeping highest player_id)
|
|
if unique_names:
|
|
seen_names = {}
|
|
for player in results:
|
|
name_lower = player.p_name.lower()
|
|
if (
|
|
name_lower not in seen_names
|
|
or player.player_id > seen_names[name_lower].player_id
|
|
):
|
|
seen_names[name_lower] = player
|
|
results = list(seen_names.values())
|
|
|
|
total_matches = len(results)
|
|
limited_results = results[:limit]
|
|
|
|
# Build response
|
|
return_val = {
|
|
"count": len(limited_results),
|
|
"total_matches": total_matches,
|
|
"players": [],
|
|
}
|
|
|
|
for x in limited_results:
|
|
this_record = model_to_dict(x, recurse=not short_output)
|
|
|
|
# this_dex = Paperdex.select().where(Paperdex.player == x)
|
|
# this_record['paperdex'] = {'count': this_dex.count(), 'paperdex': []}
|
|
# for y in this_dex:
|
|
# this_record['paperdex']['paperdex'].append(model_to_dict(y, recurse=False))
|
|
|
|
return_val["players"].append(this_record)
|
|
|
|
db.close()
|
|
return return_val
|
|
|
|
|
|
@router.get("/{player_id}")
|
|
async def get_one_player(player_id, csv: Optional[bool] = False):
|
|
try:
|
|
this_player = Player.get_by_id(player_id)
|
|
except Exception:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"No player found with id {player_id}"
|
|
)
|
|
|
|
if csv:
|
|
data_list = [
|
|
[
|
|
"id",
|
|
"name",
|
|
"cost",
|
|
"image",
|
|
"image2",
|
|
"mlbclub",
|
|
"franchise",
|
|
"cardset",
|
|
"rarity",
|
|
"pos_1",
|
|
"pos_2",
|
|
"pos_3",
|
|
"pos_4",
|
|
"pos_5",
|
|
"pos_6",
|
|
"pos_7",
|
|
"pos_8",
|
|
"headshot",
|
|
"vanity_card",
|
|
"strat_code",
|
|
"bbref_id",
|
|
"description",
|
|
]
|
|
]
|
|
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
|
data_list.append(
|
|
[
|
|
this_player.id,
|
|
this_player.p_name,
|
|
this_player.cost,
|
|
this_player.image,
|
|
this_player.image2,
|
|
this_player.mlbclub,
|
|
this_player.franchise,
|
|
this_player.cardset.name,
|
|
this_player.rarity.name,
|
|
this_player.pos_1,
|
|
this_player.pos_2,
|
|
this_player.pos_3,
|
|
this_player.pos_4,
|
|
this_player.pos_5,
|
|
this_player.pos_6,
|
|
this_player.pos_7,
|
|
this_player.pos_8,
|
|
this_player.headshot,
|
|
this_player.vanity_card,
|
|
this_player.strat_code,
|
|
this_player.bbref_id,
|
|
this_player.description,
|
|
]
|
|
)
|
|
|
|
db.close()
|
|
return Response(content=return_val, media_type="text/csv")
|
|
else:
|
|
return_val = model_to_dict(this_player)
|
|
this_dex = Paperdex.select().where(Paperdex.player == this_player)
|
|
return_val["paperdex"] = {"count": this_dex.count(), "paperdex": []}
|
|
for x in this_dex:
|
|
return_val["paperdex"]["paperdex"].append(model_to_dict(x, recurse=False))
|
|
db.close()
|
|
return return_val
|
|
|
|
|
|
@router.get("/{player_id}/{card_type}card")
|
|
@router.get("/{player_id}/{card_type}card/{d}")
|
|
@router.get("/{player_id}/{card_type}card/{d}/{variant}")
|
|
async def get_batter_card(
|
|
request: Request,
|
|
player_id: int,
|
|
card_type: Literal["batting", "pitching"],
|
|
variant: int = 0,
|
|
d: str = None,
|
|
html: Optional[bool] = False,
|
|
):
|
|
try:
|
|
this_player = Player.get_by_id(player_id)
|
|
except Exception:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"No player found with id {player_id}"
|
|
)
|
|
|
|
headers = {"Cache-Control": "public, max-age=86400"}
|
|
filename = (
|
|
f"{this_player.description} {this_player.p_name} {card_type} {d}-v{variant}"
|
|
)
|
|
if (
|
|
os.path.isfile(
|
|
f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png"
|
|
)
|
|
and html is False
|
|
):
|
|
db.close()
|
|
return FileResponse(
|
|
path=f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png",
|
|
media_type="image/png",
|
|
headers=headers,
|
|
)
|
|
|
|
all_pos = (
|
|
CardPosition.select()
|
|
.where(CardPosition.player == this_player)
|
|
.order_by(CardPosition.innings.desc())
|
|
)
|
|
|
|
if card_type == "batting":
|
|
this_bc = BattingCard.get_or_none(
|
|
BattingCard.player == this_player, BattingCard.variant == variant
|
|
)
|
|
if this_bc is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Batting card not found for id {player_id}, variant {variant}",
|
|
)
|
|
|
|
rating_vl = BattingCardRatings.get_or_none(
|
|
BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == "L"
|
|
)
|
|
rating_vr = BattingCardRatings.get_or_none(
|
|
BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == "R"
|
|
)
|
|
if None in [rating_vr, rating_vl]:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Ratings not found for batting card {this_bc.id}",
|
|
)
|
|
|
|
card_data = get_batter_card_data(
|
|
this_player, this_bc, rating_vl, rating_vr, all_pos
|
|
)
|
|
# Include Pokemon cardsets here to remove "Pokemon" from cardset name on card
|
|
if (
|
|
this_player.description in this_player.cardset.name
|
|
and this_player.cardset.id not in [23]
|
|
):
|
|
card_data["cardset_name"] = this_player.cardset.name
|
|
else:
|
|
card_data["cardset_name"] = this_player.description
|
|
card_data["request"] = request
|
|
html_response = templates.TemplateResponse("player_card.html", card_data)
|
|
|
|
else:
|
|
this_pc = PitchingCard.get_or_none(
|
|
PitchingCard.player == this_player, PitchingCard.variant == variant
|
|
)
|
|
if this_pc is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Pitching card not found for id {player_id}, variant {variant}",
|
|
)
|
|
|
|
rating_vl = PitchingCardRatings.get_or_none(
|
|
PitchingCardRatings.pitchingcard == this_pc,
|
|
PitchingCardRatings.vs_hand == "L",
|
|
)
|
|
rating_vr = PitchingCardRatings.get_or_none(
|
|
PitchingCardRatings.pitchingcard == this_pc,
|
|
PitchingCardRatings.vs_hand == "R",
|
|
)
|
|
if None in [rating_vr, rating_vl]:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Ratings not found for pitching card {this_pc.id}",
|
|
)
|
|
|
|
card_data = get_pitcher_card_data(
|
|
this_player, this_pc, rating_vl, rating_vr, all_pos
|
|
)
|
|
if (
|
|
this_player.description in this_player.cardset.name
|
|
and this_player.cardset.id not in [23]
|
|
):
|
|
card_data["cardset_name"] = this_player.cardset.name
|
|
else:
|
|
card_data["cardset_name"] = this_player.description
|
|
card_data["request"] = request
|
|
html_response = templates.TemplateResponse("player_card.html", card_data)
|
|
|
|
if html:
|
|
db.close()
|
|
return html_response
|
|
|
|
updates = 0
|
|
if card_type == "batting":
|
|
updates += (
|
|
BattingCardRatings.update(card_data["new_ratings_vl"].dict())
|
|
.where((BattingCardRatings.id == rating_vl.id))
|
|
.execute()
|
|
)
|
|
updates += (
|
|
BattingCardRatings.update(card_data["new_ratings_vr"].dict())
|
|
.where((BattingCardRatings.id == rating_vr.id))
|
|
.execute()
|
|
)
|
|
else:
|
|
updates += (
|
|
PitchingCardRatings.update(card_data["new_ratings_vl"].dict())
|
|
.where((PitchingCardRatings.id == rating_vl.id))
|
|
.execute()
|
|
)
|
|
updates += (
|
|
PitchingCardRatings.update(card_data["new_ratings_vr"].dict())
|
|
.where((PitchingCardRatings.id == rating_vr.id))
|
|
.execute()
|
|
)
|
|
|
|
logging.debug(f"Rating updates: {updates}")
|
|
logging.debug(f"body:\n{html_response.body.decode('UTF-8')}")
|
|
|
|
file_path = f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png"
|
|
async with async_playwright() as p:
|
|
browser = await p.chromium.launch()
|
|
page = await browser.new_page()
|
|
await page.set_content(html_response.body.decode("UTF-8"))
|
|
await page.screenshot(
|
|
path=file_path,
|
|
type="png",
|
|
clip={"x": 0.0, "y": 0, "width": 1200, "height": 600},
|
|
)
|
|
await browser.close()
|
|
|
|
# hti = Html2Image(
|
|
# browser='chrome',
|
|
# size=(1200, 600),
|
|
# output_path=f'storage/cards/cardset-{this_player.cardset.id}/{card_type}/',
|
|
# custom_flags=['--no-sandbox', '--disable-remote-debugging', '--headless', '--disable-gpu',
|
|
# '--disable-software-rasterizer', '--disable-dev-shm-usage']
|
|
# )
|
|
|
|
# x = hti.screenshot(
|
|
# html_str=str(html_response.body.decode("UTF-8")),
|
|
# save_as=f'{player_id}-{d}-v{variant}.png'
|
|
# )
|
|
|
|
db.close()
|
|
return FileResponse(path=file_path, media_type="image/png", headers=headers)
|
|
|
|
|
|
# @router.get('/{player_id}/pitchingcard')
|
|
# async def get_pitcher_card(
|
|
# request: Request, player_id: int, variant: int = 0, d: str = None, html: Optional[bool] = False)
|
|
|
|
|
|
@router.patch("/{player_id}")
|
|
async def v1_players_patch(
|
|
player_id,
|
|
name: Optional[str] = None,
|
|
image: Optional[str] = None,
|
|
image2: Optional[str] = None,
|
|
mlbclub: Optional[str] = None,
|
|
franchise: Optional[str] = None,
|
|
cardset_id: Optional[int] = None,
|
|
rarity_id: Optional[int] = None,
|
|
pos_1: Optional[str] = None,
|
|
pos_2: Optional[str] = None,
|
|
pos_3: Optional[str] = None,
|
|
pos_4: Optional[str] = None,
|
|
pos_5: Optional[str] = None,
|
|
mlbplayer_id: Optional[int] = None,
|
|
pos_6: Optional[str] = None,
|
|
pos_7: Optional[str] = None,
|
|
pos_8: Optional[str] = None,
|
|
headshot: Optional[str] = None,
|
|
vanity_card: Optional[str] = None,
|
|
strat_code: Optional[str] = None,
|
|
bbref_id: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
cost: Optional[int] = None,
|
|
fangr_id: 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 players. This event has been logged.",
|
|
)
|
|
|
|
try:
|
|
this_player = Player.get_by_id(player_id)
|
|
except Exception:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"No player found with id {player_id}"
|
|
)
|
|
|
|
if cost is not None:
|
|
this_player.cost = cost
|
|
if name is not None:
|
|
this_player.p_name = name
|
|
if image is not None:
|
|
this_player.image = image
|
|
if image2 is not None:
|
|
if image2.lower() == "false":
|
|
this_player.image2 = None
|
|
else:
|
|
this_player.image2 = image2
|
|
if mlbclub is not None:
|
|
this_player.mlbclub = mlbclub
|
|
if franchise is not None:
|
|
this_player.franchise = franchise
|
|
if cardset_id is not None:
|
|
try:
|
|
this_cardset = Cardset.get_by_id(cardset_id)
|
|
except Exception:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"No cardset found with id {cardset_id}"
|
|
)
|
|
this_player.cardset = this_cardset
|
|
if rarity_id is not None:
|
|
try:
|
|
this_rarity = Rarity.get_by_id(rarity_id)
|
|
except Exception:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"No rarity found with id {rarity_id}"
|
|
)
|
|
this_player.rarity = this_rarity
|
|
if pos_1 is not None:
|
|
if pos_1 in ["None", "False", ""]:
|
|
this_player.pos_1 = None
|
|
else:
|
|
this_player.pos_1 = pos_1
|
|
if pos_2 is not None:
|
|
if pos_2 in ["None", "False", ""]:
|
|
this_player.pos_2 = None
|
|
else:
|
|
this_player.pos_2 = pos_2
|
|
if pos_3 is not None:
|
|
if pos_3 in ["None", "False", ""]:
|
|
this_player.pos_3 = None
|
|
else:
|
|
this_player.pos_3 = pos_3
|
|
if pos_4 is not None:
|
|
if pos_4 in ["None", "False", ""]:
|
|
this_player.pos_4 = None
|
|
else:
|
|
this_player.pos_4 = pos_4
|
|
if pos_5 is not None:
|
|
if pos_5 in ["None", "False", ""]:
|
|
this_player.pos_5 = None
|
|
else:
|
|
this_player.pos_5 = pos_5
|
|
if pos_6 is not None:
|
|
if pos_6 in ["None", "False", ""]:
|
|
this_player.pos_6 = None
|
|
else:
|
|
this_player.pos_6 = pos_6
|
|
if pos_7 is not None:
|
|
if pos_7 in ["None", "False", ""]:
|
|
this_player.pos_7 = None
|
|
else:
|
|
this_player.pos_7 = pos_7
|
|
if pos_8 is not None:
|
|
if pos_8 in ["None", "False", ""]:
|
|
this_player.pos_8 = None
|
|
else:
|
|
this_player.pos_8 = pos_8
|
|
if headshot is not None:
|
|
this_player.headshot = headshot
|
|
if vanity_card is not None:
|
|
this_player.vanity_card = vanity_card
|
|
if strat_code is not None:
|
|
this_player.strat_code = strat_code
|
|
if bbref_id is not None:
|
|
this_player.bbref_id = bbref_id
|
|
if fangr_id is not None:
|
|
this_player.fangr_id = fangr_id
|
|
if description is not None:
|
|
this_player.description = description
|
|
if mlbplayer_id is not None:
|
|
this_player.mlbplayer_id = mlbplayer_id
|
|
|
|
if this_player.save() == 1:
|
|
return_val = model_to_dict(this_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 rarity",
|
|
)
|
|
|
|
|
|
@router.put("")
|
|
async def put_players(players: 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 players. This event has been logged.",
|
|
)
|
|
|
|
new_players = []
|
|
for x in players.players:
|
|
# this_player = Player(
|
|
# player_id=x.player_id,
|
|
# p_name=x.p_name,
|
|
# cost=x.cost,
|
|
# image=x.image,
|
|
# image2=x.image2,
|
|
# mlbclub=x.mlbclub,
|
|
# franchise=x.franchise,
|
|
# cardset_id=x.cardset_id,
|
|
# rarity_id=x.rarity_id,
|
|
# set_num=x.set_num,
|
|
# pos_1=x.pos_1,
|
|
# pos_2=x.pos_2,
|
|
# pos_3=x.pos_3,
|
|
# pos_4=x.pos_4,
|
|
# pos_5=x.pos_5,
|
|
# pos_6=x.pos_6,
|
|
# pos_7=x.pos_7,
|
|
# pos_8=x.pos_8,
|
|
# headshot=x.headshot,
|
|
# vanity_card=x.vanity_card,
|
|
# strat_code=x.strat_code,
|
|
# fangr_id=x.fangr_id,
|
|
# bbref_id=x.bbref_id,
|
|
# description=x.description
|
|
# )
|
|
# new_players.append(this_player)
|
|
new_players.append(
|
|
{
|
|
"player_id": x.player_id,
|
|
"p_name": x.p_name,
|
|
"cost": x.cost,
|
|
"image": x.image,
|
|
"image2": x.image2,
|
|
"mlbclub": x.mlbclub.title(),
|
|
"franchise": normalize_franchise(x.franchise),
|
|
"cardset_id": x.cardset_id,
|
|
"rarity_id": x.rarity_id,
|
|
"set_num": x.set_num,
|
|
"pos_1": x.pos_1,
|
|
"pos_2": x.pos_2,
|
|
"pos_3": x.pos_3,
|
|
"pos_4": x.pos_4,
|
|
"pos_5": x.pos_5,
|
|
"pos_6": x.pos_6,
|
|
"pos_7": x.pos_7,
|
|
"pos_8": x.pos_8,
|
|
"headshot": x.headshot,
|
|
"vanity_card": x.vanity_card,
|
|
"strat_code": x.strat_code,
|
|
"fangr_id": x.fangr_id,
|
|
"bbref_id": x.bbref_id,
|
|
"description": x.description,
|
|
}
|
|
)
|
|
|
|
logging.debug(f"new_players: {new_players}")
|
|
|
|
with db.atomic():
|
|
# Use PostgreSQL-compatible upsert helper (preserves SQLite compatibility)
|
|
upsert_players(new_players, batch_size=15)
|
|
db.close()
|
|
|
|
# sheets.update_all_players(SHEETS_AUTH)
|
|
raise HTTPException(
|
|
status_code=200, detail=f"{len(new_players)} players have been added"
|
|
)
|
|
|
|
|
|
@router.post("")
|
|
async def post_players(new_player: PlayerPydantic, 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 players. This event has been logged.",
|
|
)
|
|
|
|
dupe_query = Player.select().where(
|
|
(Player.bbref_id == new_player.bbref_id)
|
|
& (Player.cardset_id == new_player.cardset_id)
|
|
)
|
|
if dupe_query.count() != 0:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"This appears to be a duplicate with player {dupe_query[0].player_id}",
|
|
)
|
|
|
|
p_query = Player.select(Player.player_id).order_by(-Player.player_id).limit(1)
|
|
new_id = p_query[0].player_id + 1
|
|
|
|
new_player.player_id = new_id
|
|
p_id = Player.insert(new_player.dict()).execute()
|
|
|
|
return_val = model_to_dict(Player.get_by_id(p_id))
|
|
db.close()
|
|
return return_val
|
|
|
|
|
|
@router.post("/{player_id}/image-reset")
|
|
async def post_image_reset(
|
|
player_id: int, dev: bool = False, 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 modify players. This event has been logged.",
|
|
)
|
|
|
|
this_player = Player.get_or_none(Player.player_id == player_id)
|
|
if this_player is None:
|
|
db.close()
|
|
raise HTTPException(status_code=404, detail=f"Player ID {player_id} not found")
|
|
|
|
now = datetime.datetime.now()
|
|
today_url = (
|
|
f"https://pd{'dev' if dev else ''}.manticorum.com/api/v2/players/{player_id}/"
|
|
f"{'pitch' if 'pitch' in this_player.image else 'batt'}ingcard?d={now.year}-{now.month}-{now.day}"
|
|
)
|
|
logging.debug(f"image1 url: {today_url}")
|
|
this_player.image = today_url
|
|
|
|
if this_player.image2 is not None:
|
|
today_url = (
|
|
f"https://pd{'dev' if dev else ''}.manticorum.com/api/v2/players/{player_id}/"
|
|
f"{'pitch' if 'pitch' in this_player.image2 else 'batt'}ingcard?d={now.year}-{now.month}-{now.day}"
|
|
)
|
|
logging.debug(f"image2 url: {today_url}")
|
|
this_player.image2 = today_url
|
|
|
|
this_player.save()
|
|
r_player = model_to_dict(this_player)
|
|
db.close()
|
|
return r_player
|
|
|
|
|
|
@router.delete("/{player_id}")
|
|
async def delete_player(player_id, 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 players. This event has been logged.",
|
|
)
|
|
|
|
try:
|
|
this_player = Player.get_by_id(player_id)
|
|
except Exception:
|
|
db.close()
|
|
raise HTTPException(
|
|
status_code=404, detail=f"No player found with id {player_id}"
|
|
)
|
|
|
|
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"
|
|
)
|