- Add explicit ORDER BY id to all queries for consistent results across SQLite and PostgreSQL - PostgreSQL does not guarantee row order without ORDER BY, unlike SQLite - Skip table creation when DATABASE_TYPE=postgresql (production tables already exist) - Fix datetime handling in notifications (PostgreSQL native datetime vs SQLite timestamp) - Fix grouped query count() calls that don't work in PostgreSQL - Update .gitignore to include storage/templates/ directory This completes the PostgreSQL migration compatibility layer while maintaining backwards compatibility with SQLite for local development. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1179 lines
40 KiB
Python
1179 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)
|
|
else:
|
|
all_players = all_players.order_by(Player.player_id)
|
|
|
|
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"
|
|
)
|