Compare commits
10 Commits
issue/202-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cd8db3f98e | |||
|
|
6a986bf0f8 | ||
| 5e85e27cda | |||
|
|
7f7d9ffe1f | ||
| d83a4bdbb7 | |||
|
|
b29450e7d6 | ||
| 5ff11759f9 | |||
|
|
fd2cc6534a | ||
| 7701777273 | |||
|
|
4028a24ef9 |
@ -44,6 +44,10 @@ else:
|
|||||||
pragmas={"journal_mode": "wal", "cache_size": -1 * 64000, "synchronous": 0},
|
pragmas={"journal_mode": "wal", "cache_size": -1 * 64000, "synchronous": 0},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Refractor stat accumulation starts at this season — stats from earlier seasons
|
||||||
|
# are excluded from evaluation queries. Override via REFRACTOR_START_SEASON env var.
|
||||||
|
REFRACTOR_START_SEASON = int(os.environ.get("REFRACTOR_START_SEASON", "11"))
|
||||||
|
|
||||||
# 2025, 2005
|
# 2025, 2005
|
||||||
ranked_cardsets = [24, 25, 26, 27, 28, 29]
|
ranked_cardsets = [24, 25, 26, 27, 28, 29]
|
||||||
LIVE_CARDSET_ID = 27
|
LIVE_CARDSET_ID = 27
|
||||||
|
|||||||
@ -332,10 +332,13 @@ async def get_card_scouting(team_id: int, ts: str):
|
|||||||
logging.debug(f"Team: {this_team} / has_guide: {this_team.has_guide}")
|
logging.debug(f"Team: {this_team} / has_guide: {this_team.has_guide}")
|
||||||
if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
|
if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
|
||||||
logging.warning(f"Team_id {team_id} attempted to pull ratings")
|
logging.warning(f"Team_id {team_id} attempted to pull ratings")
|
||||||
return (
|
raise HTTPException(
|
||||||
"Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to "
|
status_code=403,
|
||||||
"make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): "
|
detail=(
|
||||||
"https://ko-fi.com/manticorum/shop"
|
"Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to "
|
||||||
|
"make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): "
|
||||||
|
"https://ko-fi.com/manticorum/shop"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.isfile("storage/batting-ratings.csv"):
|
if os.path.isfile("storage/batting-ratings.csv"):
|
||||||
|
|||||||
@ -10,10 +10,7 @@ from ..db_engine import db, Cardset, model_to_dict, Pack, Team, PackType, DoesNo
|
|||||||
from ..dependencies import oauth2_scheme, valid_token
|
from ..dependencies import oauth2_scheme, valid_token
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(prefix="/api/v2/packs", tags=["packs"])
|
||||||
prefix='/api/v2/packs',
|
|
||||||
tags=['packs']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PackPydantic(pydantic.BaseModel):
|
class PackPydantic(pydantic.BaseModel):
|
||||||
@ -28,46 +25,58 @@ class PackModel(pydantic.BaseModel):
|
|||||||
packs: List[PackPydantic]
|
packs: List[PackPydantic]
|
||||||
|
|
||||||
|
|
||||||
@router.get('')
|
@router.get("")
|
||||||
async def get_packs(
|
async def get_packs(
|
||||||
team_id: Optional[int] = None, pack_type_id: Optional[int] = None, opened: Optional[bool] = None,
|
team_id: Optional[int] = None,
|
||||||
limit: Optional[int] = None, new_to_old: Optional[bool] = None, pack_team_id: Optional[int] = None,
|
pack_type_id: Optional[int] = None,
|
||||||
pack_cardset_id: Optional[int] = None, exact_match: Optional[bool] = False, csv: Optional[bool] = None):
|
opened: Optional[bool] = None,
|
||||||
|
limit: Optional[int] = None,
|
||||||
|
new_to_old: Optional[bool] = None,
|
||||||
|
pack_team_id: Optional[int] = None,
|
||||||
|
pack_cardset_id: Optional[int] = None,
|
||||||
|
exact_match: Optional[bool] = False,
|
||||||
|
csv: Optional[bool] = None,
|
||||||
|
):
|
||||||
all_packs = Pack.select()
|
all_packs = Pack.select()
|
||||||
|
|
||||||
if all_packs.count() == 0:
|
|
||||||
raise HTTPException(status_code=404, detail=f'There are no packs to filter')
|
|
||||||
|
|
||||||
if team_id is not None:
|
if team_id is not None:
|
||||||
try:
|
try:
|
||||||
this_team = Team.get_by_id(team_id)
|
this_team = Team.get_by_id(team_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No team found with id {team_id}"
|
||||||
|
)
|
||||||
all_packs = all_packs.where(Pack.team == this_team)
|
all_packs = all_packs.where(Pack.team == this_team)
|
||||||
if pack_type_id is not None:
|
if pack_type_id is not None:
|
||||||
try:
|
try:
|
||||||
this_pack_type = PackType.get_by_id(pack_type_id)
|
this_pack_type = PackType.get_by_id(pack_type_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No pack type found with id {pack_type_id}')
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No pack type found with id {pack_type_id}"
|
||||||
|
)
|
||||||
all_packs = all_packs.where(Pack.pack_type == this_pack_type)
|
all_packs = all_packs.where(Pack.pack_type == this_pack_type)
|
||||||
|
|
||||||
if pack_team_id is not None:
|
if pack_team_id is not None:
|
||||||
try:
|
try:
|
||||||
this_pack_team = Team.get_by_id(pack_team_id)
|
this_pack_team = Team.get_by_id(pack_team_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No team found with id {pack_team_id}')
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No team found with id {pack_team_id}"
|
||||||
|
)
|
||||||
all_packs = all_packs.where(Pack.pack_team == this_pack_team)
|
all_packs = all_packs.where(Pack.pack_team == this_pack_team)
|
||||||
elif exact_match:
|
elif exact_match:
|
||||||
all_packs = all_packs.where(Pack.pack_team == None)
|
all_packs = all_packs.where(Pack.pack_team == None) # noqa: E711
|
||||||
|
|
||||||
if pack_cardset_id is not None:
|
if pack_cardset_id is not None:
|
||||||
try:
|
try:
|
||||||
this_pack_cardset = Cardset.get_by_id(pack_cardset_id)
|
this_pack_cardset = Cardset.get_by_id(pack_cardset_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No cardset found with id {pack_cardset_id}')
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No cardset found with id {pack_cardset_id}"
|
||||||
|
)
|
||||||
all_packs = all_packs.where(Pack.pack_cardset == this_pack_cardset)
|
all_packs = all_packs.where(Pack.pack_cardset == this_pack_cardset)
|
||||||
elif exact_match:
|
elif exact_match:
|
||||||
all_packs = all_packs.where(Pack.pack_cardset == None)
|
all_packs = all_packs.where(Pack.pack_cardset == None) # noqa: E711
|
||||||
|
|
||||||
if opened is not None:
|
if opened is not None:
|
||||||
all_packs = all_packs.where(Pack.open_time.is_null(not opened))
|
all_packs = all_packs.where(Pack.open_time.is_null(not opened))
|
||||||
@ -78,60 +87,62 @@ async def get_packs(
|
|||||||
else:
|
else:
|
||||||
all_packs = all_packs.order_by(Pack.id)
|
all_packs = all_packs.order_by(Pack.id)
|
||||||
|
|
||||||
# if all_packs.count() == 0:
|
|
||||||
# db.close()
|
|
||||||
# raise HTTPException(status_code=404, detail=f'No packs found')
|
|
||||||
|
|
||||||
if csv:
|
if csv:
|
||||||
data_list = [['id', 'team', 'pack_type', 'open_time']]
|
data_list = [["id", "team", "pack_type", "open_time"]]
|
||||||
for line in all_packs:
|
for line in all_packs:
|
||||||
data_list.append(
|
data_list.append(
|
||||||
[
|
[
|
||||||
line.id, line.team.abbrev, line.pack_type.name,
|
line.id,
|
||||||
line.open_time # Already datetime in PostgreSQL
|
line.team.abbrev,
|
||||||
|
line.pack_type.name,
|
||||||
|
line.open_time, # Already datetime in PostgreSQL
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||||
|
|
||||||
return Response(content=return_val, media_type='text/csv')
|
return Response(content=return_val, media_type="text/csv")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return_val = {'count': all_packs.count(), 'packs': []}
|
return_val = {"count": all_packs.count(), "packs": []}
|
||||||
for x in all_packs:
|
for x in all_packs:
|
||||||
return_val['packs'].append(model_to_dict(x))
|
return_val["packs"].append(model_to_dict(x))
|
||||||
|
|
||||||
return return_val
|
return return_val
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pack_id}')
|
@router.get("/{pack_id}")
|
||||||
async def get_one_pack(pack_id: int, csv: Optional[bool] = False):
|
async def get_one_pack(pack_id: int, csv: Optional[bool] = False):
|
||||||
try:
|
try:
|
||||||
this_pack = Pack.get_by_id(pack_id)
|
this_pack = Pack.get_by_id(pack_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}')
|
raise HTTPException(status_code=404, detail=f"No pack found with id {pack_id}")
|
||||||
|
|
||||||
if csv:
|
if csv:
|
||||||
data_list = [
|
data_list = [
|
||||||
['id', 'team', 'pack_type', 'open_time'],
|
["id", "team", "pack_type", "open_time"],
|
||||||
[this_pack.id, this_pack.team.abbrev, this_pack.pack_type.name,
|
[
|
||||||
this_pack.open_time] # Already datetime in PostgreSQL
|
this_pack.id,
|
||||||
|
this_pack.team.abbrev,
|
||||||
|
this_pack.pack_type.name,
|
||||||
|
this_pack.open_time,
|
||||||
|
], # Already datetime in PostgreSQL
|
||||||
]
|
]
|
||||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||||
|
|
||||||
return Response(content=return_val, media_type='text/csv')
|
return Response(content=return_val, media_type="text/csv")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return_val = model_to_dict(this_pack)
|
return_val = model_to_dict(this_pack)
|
||||||
return return_val
|
return return_val
|
||||||
|
|
||||||
|
|
||||||
@router.post('')
|
@router.post("")
|
||||||
async def post_pack(packs: PackModel, token: str = Depends(oauth2_scheme)):
|
async def post_pack(packs: PackModel, token: str = Depends(oauth2_scheme)):
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning('Bad Token: [REDACTED]')
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail='You are not authorized to post packs. This event has been logged.'
|
detail="You are not authorized to post packs. This event has been logged.",
|
||||||
)
|
)
|
||||||
|
|
||||||
new_packs = []
|
new_packs = []
|
||||||
@ -141,23 +152,27 @@ async def post_pack(packs: PackModel, token: str = Depends(oauth2_scheme)):
|
|||||||
pack_type_id=x.pack_type_id,
|
pack_type_id=x.pack_type_id,
|
||||||
pack_team_id=x.pack_team_id,
|
pack_team_id=x.pack_team_id,
|
||||||
pack_cardset_id=x.pack_cardset_id,
|
pack_cardset_id=x.pack_cardset_id,
|
||||||
open_time=datetime.fromtimestamp(x.open_time / 1000) if x.open_time else None
|
open_time=datetime.fromtimestamp(x.open_time / 1000)
|
||||||
|
if x.open_time
|
||||||
|
else None,
|
||||||
)
|
)
|
||||||
new_packs.append(this_player)
|
new_packs.append(this_player)
|
||||||
|
|
||||||
with db.atomic():
|
with db.atomic():
|
||||||
Pack.bulk_create(new_packs, batch_size=15)
|
Pack.bulk_create(new_packs, batch_size=15)
|
||||||
|
|
||||||
raise HTTPException(status_code=200, detail=f'{len(new_packs)} packs have been added')
|
raise HTTPException(
|
||||||
|
status_code=200, detail=f"{len(new_packs)} packs have been added"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/one')
|
@router.post("/one")
|
||||||
async def post_one_pack(pack: PackPydantic, token: str = Depends(oauth2_scheme)):
|
async def post_one_pack(pack: PackPydantic, token: str = Depends(oauth2_scheme)):
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning('Bad Token: [REDACTED]')
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail='You are not authorized to post packs. This event has been logged.'
|
detail="You are not authorized to post packs. This event has been logged.",
|
||||||
)
|
)
|
||||||
|
|
||||||
this_pack = Pack(
|
this_pack = Pack(
|
||||||
@ -165,7 +180,9 @@ async def post_one_pack(pack: PackPydantic, token: str = Depends(oauth2_scheme))
|
|||||||
pack_type_id=pack.pack_type_id,
|
pack_type_id=pack.pack_type_id,
|
||||||
pack_team_id=pack.pack_team_id,
|
pack_team_id=pack.pack_team_id,
|
||||||
pack_cardset_id=pack.pack_cardset_id,
|
pack_cardset_id=pack.pack_cardset_id,
|
||||||
open_time=datetime.fromtimestamp(pack.open_time / 1000) if pack.open_time else None
|
open_time=datetime.fromtimestamp(pack.open_time / 1000)
|
||||||
|
if pack.open_time
|
||||||
|
else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
saved = this_pack.save()
|
saved = this_pack.save()
|
||||||
@ -175,24 +192,30 @@ async def post_one_pack(pack: PackPydantic, token: str = Depends(oauth2_scheme))
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=418,
|
status_code=418,
|
||||||
detail='Well slap my ass and call me a teapot; I could not save that cardset'
|
detail="Well slap my ass and call me a teapot; I could not save that cardset",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.patch('/{pack_id}')
|
@router.patch("/{pack_id}")
|
||||||
async def patch_pack(
|
async def patch_pack(
|
||||||
pack_id, team_id: Optional[int] = None, pack_type_id: Optional[int] = None, open_time: Optional[int] = None,
|
pack_id,
|
||||||
pack_team_id: Optional[int] = None, pack_cardset_id: Optional[int] = None, token: str = Depends(oauth2_scheme)):
|
team_id: Optional[int] = None,
|
||||||
|
pack_type_id: Optional[int] = None,
|
||||||
|
open_time: Optional[int] = None,
|
||||||
|
pack_team_id: Optional[int] = None,
|
||||||
|
pack_cardset_id: Optional[int] = None,
|
||||||
|
token: str = Depends(oauth2_scheme),
|
||||||
|
):
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning('Bad Token: [REDACTED]')
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail='You are not authorized to patch packs. This event has been logged.'
|
detail="You are not authorized to patch packs. This event has been logged.",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
this_pack = Pack.get_by_id(pack_id)
|
this_pack = Pack.get_by_id(pack_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}')
|
raise HTTPException(status_code=404, detail=f"No pack found with id {pack_id}")
|
||||||
|
|
||||||
if team_id is not None:
|
if team_id is not None:
|
||||||
this_pack.team_id = team_id
|
this_pack.team_id = team_id
|
||||||
@ -220,26 +243,26 @@ async def patch_pack(
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=418,
|
status_code=418,
|
||||||
detail='Well slap my ass and call me a teapot; I could not save that rarity'
|
detail="Well slap my ass and call me a teapot; I could not save that rarity",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete('/{pack_id}')
|
@router.delete("/{pack_id}")
|
||||||
async def delete_pack(pack_id, token: str = Depends(oauth2_scheme)):
|
async def delete_pack(pack_id, token: str = Depends(oauth2_scheme)):
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning('Bad Token: [REDACTED]')
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail='You are not authorized to delete packs. This event has been logged.'
|
detail="You are not authorized to delete packs. This event has been logged.",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
this_pack = Pack.get_by_id(pack_id)
|
this_pack = Pack.get_by_id(pack_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No packs found with id {pack_id}')
|
raise HTTPException(status_code=404, detail=f"No packs found with id {pack_id}")
|
||||||
|
|
||||||
count = this_pack.delete_instance()
|
count = this_pack.delete_instance()
|
||||||
|
|
||||||
if count == 1:
|
if count == 1:
|
||||||
raise HTTPException(status_code=200, detail=f'Pack {pack_id} has been deleted')
|
raise HTTPException(status_code=200, detail=f"Pack {pack_id} has been deleted")
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail=f'Pack {pack_id} was not deleted')
|
raise HTTPException(status_code=500, detail=f"Pack {pack_id} was not deleted")
|
||||||
|
|||||||
@ -252,10 +252,13 @@ async def get_card_scouting(team_id: int, ts: str):
|
|||||||
logging.debug(f"Team: {this_team} / has_guide: {this_team.has_guide}")
|
logging.debug(f"Team: {this_team} / has_guide: {this_team.has_guide}")
|
||||||
if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
|
if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
|
||||||
logging.warning(f"Team_id {team_id} attempted to pull ratings")
|
logging.warning(f"Team_id {team_id} attempted to pull ratings")
|
||||||
return (
|
raise HTTPException(
|
||||||
"Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to "
|
status_code=403,
|
||||||
"make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): "
|
detail=(
|
||||||
"https://ko-fi.com/manticorum/shop"
|
"Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to "
|
||||||
|
"make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): "
|
||||||
|
"https://ko-fi.com/manticorum/shop"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.isfile(RATINGS_FILE):
|
if os.path.isfile(RATINGS_FILE):
|
||||||
|
|||||||
@ -40,7 +40,7 @@ from ..db_engine import (
|
|||||||
)
|
)
|
||||||
from ..db_helpers import upsert_players
|
from ..db_helpers import upsert_players
|
||||||
from ..dependencies import oauth2_scheme, valid_token
|
from ..dependencies import oauth2_scheme, valid_token
|
||||||
from ..services.card_storage import backfill_variant_image_url
|
from ..services.card_storage import backfill_variant_image_url, upload_variant_apng
|
||||||
from ..services.refractor_boost import compute_variant_hash
|
from ..services.refractor_boost import compute_variant_hash
|
||||||
from ..services.apng_generator import apng_cache_path, generate_animated_card
|
from ..services.apng_generator import apng_cache_path, generate_animated_card
|
||||||
|
|
||||||
@ -740,6 +740,7 @@ async def get_one_player(player_id: int, csv: Optional[bool] = False):
|
|||||||
@router.get("/{player_id}/{card_type}card/{d}/{variant}/animated")
|
@router.get("/{player_id}/{card_type}card/{d}/{variant}/animated")
|
||||||
async def get_animated_card(
|
async def get_animated_card(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
player_id: int,
|
player_id: int,
|
||||||
card_type: Literal["batting", "pitching"],
|
card_type: Literal["batting", "pitching"],
|
||||||
variant: int,
|
variant: int,
|
||||||
@ -860,6 +861,16 @@ async def get_animated_card(
|
|||||||
finally:
|
finally:
|
||||||
await page.close()
|
await page.close()
|
||||||
|
|
||||||
|
if tier is None:
|
||||||
|
background_tasks.add_task(
|
||||||
|
upload_variant_apng,
|
||||||
|
player_id=player_id,
|
||||||
|
variant=variant,
|
||||||
|
card_type=card_type,
|
||||||
|
cardset_id=this_player.cardset.id,
|
||||||
|
apng_path=cache_path,
|
||||||
|
)
|
||||||
|
|
||||||
return FileResponse(path=cache_path, media_type="image/apng", headers=headers)
|
return FileResponse(path=cache_path, media_type="image/apng", headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
import logging
|
import logging
|
||||||
@ -476,9 +477,15 @@ async def evaluate_game(game_id: int, token: str = Depends(oauth2_scheme)):
|
|||||||
|
|
||||||
# Non-breaking addition: include boost info when available.
|
# Non-breaking addition: include boost info when available.
|
||||||
if boost_result:
|
if boost_result:
|
||||||
tier_up_entry["variant_created"] = boost_result.get(
|
variant_num = boost_result.get("variant_created")
|
||||||
"variant_created"
|
tier_up_entry["variant_created"] = variant_num
|
||||||
)
|
if computed_tier >= 3 and variant_num and card_type:
|
||||||
|
d = date.today().strftime("%Y-%m-%d")
|
||||||
|
api_base = os.environ.get("API_BASE_URL", "").rstrip("/")
|
||||||
|
tier_up_entry["animated_url"] = (
|
||||||
|
f"{api_base}/api/v2/players/{player_id}/{card_type}card"
|
||||||
|
f"/{d}/{variant_num}/animated"
|
||||||
|
)
|
||||||
|
|
||||||
tier_ups.append(tier_up_entry)
|
tier_ups.append(tier_up_entry)
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,10 @@ get_s3_client()
|
|||||||
(environment variables or instance profile).
|
(environment variables or instance profile).
|
||||||
|
|
||||||
build_s3_key(cardset_id, player_id, variant, card_type)
|
build_s3_key(cardset_id, player_id, variant, card_type)
|
||||||
Construct the S3 object key for a variant card image.
|
Construct the S3 object key for a variant card PNG image.
|
||||||
|
|
||||||
|
build_apng_s3_key(cardset_id, player_id, variant, card_type)
|
||||||
|
Construct the S3 object key for a variant animated card APNG.
|
||||||
|
|
||||||
build_s3_url(s3_key, render_date)
|
build_s3_url(s3_key, render_date)
|
||||||
Return the full HTTPS S3 URL with a cache-busting date query param.
|
Return the full HTTPS S3 URL with a cache-busting date query param.
|
||||||
@ -16,11 +19,19 @@ build_s3_url(s3_key, render_date)
|
|||||||
upload_card_to_s3(s3_client, png_bytes, s3_key)
|
upload_card_to_s3(s3_client, png_bytes, s3_key)
|
||||||
Upload raw PNG bytes to S3 with correct ContentType and CacheControl headers.
|
Upload raw PNG bytes to S3 with correct ContentType and CacheControl headers.
|
||||||
|
|
||||||
|
upload_apng_to_s3(s3_client, apng_bytes, s3_key)
|
||||||
|
Upload raw APNG bytes to S3 with correct ContentType and CacheControl headers.
|
||||||
|
|
||||||
backfill_variant_image_url(player_id, variant, card_type, cardset_id, png_path)
|
backfill_variant_image_url(player_id, variant, card_type, cardset_id, png_path)
|
||||||
End-to-end: read PNG from disk, upload to S3, update BattingCard or
|
End-to-end: read PNG from disk, upload to S3, update BattingCard or
|
||||||
PitchingCard.image_url in the database. All exceptions are caught and
|
PitchingCard.image_url in the database. All exceptions are caught and
|
||||||
logged; this function never raises (safe to call as a background task).
|
logged; this function never raises (safe to call as a background task).
|
||||||
|
|
||||||
|
upload_variant_apng(player_id, variant, card_type, cardset_id, apng_path)
|
||||||
|
End-to-end: read APNG from disk and upload to S3. No DB update (no
|
||||||
|
animated_url column exists yet). All exceptions are caught and logged;
|
||||||
|
this function never raises (safe to call as a background task).
|
||||||
|
|
||||||
Design notes
|
Design notes
|
||||||
------------
|
------------
|
||||||
- S3 credentials are resolved from the environment by boto3 at call time;
|
- S3 credentials are resolved from the environment by boto3 at call time;
|
||||||
@ -97,6 +108,29 @@ def build_s3_url(s3_key: str, render_date: date) -> str:
|
|||||||
return f"{base_url}/{s3_key}?d={date_str}"
|
return f"{base_url}/{s3_key}?d={date_str}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_apng_s3_key(
|
||||||
|
cardset_id: int, player_id: int, variant: int, card_type: str
|
||||||
|
) -> str:
|
||||||
|
"""Construct the S3 object key for a variant animated card APNG.
|
||||||
|
|
||||||
|
Key format:
|
||||||
|
cards/cardset-{csid:03d}/player-{pid}/v{variant}/{card_type}card.apng
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cardset_id: Numeric cardset ID (zero-padded to 3 digits).
|
||||||
|
player_id: Player ID.
|
||||||
|
variant: Variant number (1-4 = refractor tiers).
|
||||||
|
card_type: Either "batting" or "pitching".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The S3 object key string.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
f"cards/cardset-{cardset_id:03d}/player-{player_id}"
|
||||||
|
f"/v{variant}/{card_type}card.apng"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def upload_card_to_s3(s3_client, png_bytes: bytes, s3_key: str) -> None:
|
def upload_card_to_s3(s3_client, png_bytes: bytes, s3_key: str) -> None:
|
||||||
"""Upload raw PNG bytes to S3 with the standard card image headers.
|
"""Upload raw PNG bytes to S3 with the standard card image headers.
|
||||||
|
|
||||||
@ -196,3 +230,81 @@ def backfill_variant_image_url(
|
|||||||
variant,
|
variant,
|
||||||
card_type,
|
card_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_apng_to_s3(s3_client, apng_bytes: bytes, s3_key: str) -> None:
|
||||||
|
"""Upload raw APNG bytes to S3 with the standard animated card headers.
|
||||||
|
|
||||||
|
Sets ContentType=image/apng and CacheControl=public, max-age=86400 (1 day)
|
||||||
|
matching the animated endpoint's own Cache-Control header.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
s3_client: A boto3 S3 client (from get_s3_client).
|
||||||
|
apng_bytes: Raw APNG image bytes.
|
||||||
|
s3_key: S3 object key (from build_apng_s3_key).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
s3_client.put_object(
|
||||||
|
Bucket=S3_BUCKET,
|
||||||
|
Key=s3_key,
|
||||||
|
Body=apng_bytes,
|
||||||
|
ContentType="image/apng",
|
||||||
|
CacheControl="public, max-age=86400",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_variant_apng(
|
||||||
|
player_id: int,
|
||||||
|
variant: int,
|
||||||
|
card_type: str,
|
||||||
|
cardset_id: int,
|
||||||
|
apng_path: str,
|
||||||
|
) -> None:
|
||||||
|
"""Read a rendered APNG from disk and upload it to S3.
|
||||||
|
|
||||||
|
Intended to be called as a background task after a new animated card is
|
||||||
|
rendered. No DB update is performed (no animated_url column exists yet).
|
||||||
|
|
||||||
|
All exceptions are caught and logged — this function is intended to be
|
||||||
|
called as a background task and must never propagate exceptions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
player_id: Player ID used for the S3 key.
|
||||||
|
variant: Variant number (matches the refractor tier variant).
|
||||||
|
card_type: "batting" or "pitching" — selects the S3 key.
|
||||||
|
cardset_id: Cardset ID used for the S3 key.
|
||||||
|
apng_path: Absolute path to the rendered APNG file on disk.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(apng_path, "rb") as f:
|
||||||
|
apng_bytes = f.read()
|
||||||
|
|
||||||
|
s3_key = build_apng_s3_key(
|
||||||
|
cardset_id=cardset_id,
|
||||||
|
player_id=player_id,
|
||||||
|
variant=variant,
|
||||||
|
card_type=card_type,
|
||||||
|
)
|
||||||
|
s3_client = get_s3_client()
|
||||||
|
upload_apng_to_s3(s3_client, apng_bytes, s3_key)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"upload_variant_apng: uploaded %s animated card player=%s variant=%s key=%s",
|
||||||
|
card_type,
|
||||||
|
player_id,
|
||||||
|
variant,
|
||||||
|
s3_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"upload_variant_apng: failed for player=%s variant=%s card_type=%s",
|
||||||
|
player_id,
|
||||||
|
variant,
|
||||||
|
card_type,
|
||||||
|
)
|
||||||
|
|||||||
@ -148,10 +148,11 @@ def evaluate_card(
|
|||||||
strikeouts=sum(r.strikeouts for r in rows),
|
strikeouts=sum(r.strikeouts for r in rows),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
from app.db_engine import (
|
from app.db_engine import ( # noqa: PLC0415
|
||||||
BattingSeasonStats,
|
BattingSeasonStats,
|
||||||
PitchingSeasonStats,
|
PitchingSeasonStats,
|
||||||
) # noqa: PLC0415
|
REFRACTOR_START_SEASON,
|
||||||
|
)
|
||||||
|
|
||||||
card_type = card_state.track.card_type
|
card_type = card_state.track.card_type
|
||||||
if card_type == "batter":
|
if card_type == "batter":
|
||||||
@ -159,6 +160,7 @@ def evaluate_card(
|
|||||||
BattingSeasonStats.select().where(
|
BattingSeasonStats.select().where(
|
||||||
(BattingSeasonStats.player == player_id)
|
(BattingSeasonStats.player == player_id)
|
||||||
& (BattingSeasonStats.team == team_id)
|
& (BattingSeasonStats.team == team_id)
|
||||||
|
& (BattingSeasonStats.season >= REFRACTOR_START_SEASON)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
totals = _CareerTotals(
|
totals = _CareerTotals(
|
||||||
@ -175,6 +177,7 @@ def evaluate_card(
|
|||||||
PitchingSeasonStats.select().where(
|
PitchingSeasonStats.select().where(
|
||||||
(PitchingSeasonStats.player == player_id)
|
(PitchingSeasonStats.player == player_id)
|
||||||
& (PitchingSeasonStats.team == team_id)
|
& (PitchingSeasonStats.team == team_id)
|
||||||
|
& (PitchingSeasonStats.season >= REFRACTOR_START_SEASON)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
totals = _CareerTotals(
|
totals = _CareerTotals(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user