From 2fb5a291e5f0c6e43eb4f8fcda14ee602ab48319 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Sat, 27 Jul 2024 19:58:34 -0700 Subject: [PATCH] Implement incremental updating When `seed/all_in_one` is now called, it will update with only that data that exists later than the most-recently-played* game, allowing the upload to be used repeatedly without having to clear the database. \* Actually, "highest-ID game", as we haven't implemented `list_games_by_date`, yet --- DEVELOPMENT.md | 6 ++ app/routers/games.py | 22 +++++ app/routers/seed.py | 90 +++++++++++++---- ...ne-updated-for-incremental-add-testing.csv | 96 +++++++++++++++++++ tests/test_fresh_db_tests.py | 30 ++++++ 5 files changed, 225 insertions(+), 19 deletions(-) create mode 100644 seed-data/all-in-one-updated-for-incremental-add-testing.csv diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 32be04f..6f2d62a 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -16,3 +16,9 @@ alter sequence games_id_seq restart with 1; alter sequence players_id_seq restart with 1; alter sequence elo_scores_id_seq restart with 1; ``` + +# To copy the database out to local + +``` +$ docker cp edh-elo-server-1:/app/database/ +``` diff --git a/app/routers/games.py b/app/routers/games.py index 1e2618a..62ac6ea 100644 --- a/app/routers/games.py +++ b/app/routers/games.py @@ -61,11 +61,30 @@ def create_game(game: schemas.GameCreate, db: Session = Depends(get_db)): return created_game +# TODO - when this is updated to support sorting, also update `app/routers/seed.py:all_in_one` to take advantage of it +# TODO - and `latest_game` @api_router.get("/list", response_model=list[schemas.Game]) def list_games(skip: int = 0, limit: int = 100, db=Depends(get_db)): return crud.get_games(db, skip=skip, limit=limit) +# This is helpful for at least the `all_in_one` data-seed path, but it could conceivably also be useful for a +# "Frontpage/dashboard" widget +@api_router.get("/latest_game", response_model=schemas.Game) +def latest_game(db=Depends(get_db)): + # Limit gives me a bit of time to allow natural growth before being forced into updating to basing on sorted-response + # Will error out if called on a database with no games + # TODO - logging does not seem to be coming up during testing + latest_game = db.query(models.Game).order_by(models.Game.date.desc()).first() + if latest_game is None: + # I.e. database is empty + raise HTTPException( + status_code=404, detail="Cannot read latest_game of an empty database" + ) + return latest_game + + +# Note that this must be after all the "static" routes, lest it take precedence @api_router.get("/{game_id}", response_model=schemas.Game) def read_game(game_id: int, db=Depends(get_db)): db_game = crud.get_game_by_id(db, game_id) @@ -79,6 +98,9 @@ def delete_game(game_id: str, db=Depends(get_db)): crud.delete_game_by_id(db, int(game_id)) +# Do not add more api routes under here! See the comment above `read_game` + + ######## # HTML Routes ######## diff --git a/app/routers/seed.py b/app/routers/seed.py index b8db1f3..3494607 100644 --- a/app/routers/seed.py +++ b/app/routers/seed.py @@ -5,11 +5,13 @@ import logging from collections import defaultdict -from fastapi import APIRouter, Depends, Request, UploadFile +from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session -from .games import create_game +from .decks import list_decks +from .games import create_game, latest_game, list_games +from .players import list_players from ..templates import jinja_templates from ..sql import crud, schemas from ..sql.database import get_db @@ -89,6 +91,29 @@ def all_in_one(file: UploadFile, db: Session = Depends(get_db)): file_contents = file.file.read().decode("utf-8").split("\n") reader = csv.DictReader(file_contents, delimiter=",") + # Fetch the currently-known-information so that we can avoid recreating existing data + current_player_ids_by_name = { + player.name: player.id for player in list_players(db=db) + } + current_deck_ids_by_name = {deck.name: deck.id for deck in list_decks(db=db)} + try: + latest_recorded_game = latest_game(db=db) + date_of_latest_game = latest_recorded_game.date + except HTTPException: + # No games have been returned from the db, thus no games should be skipped for downloading + date_of_latest_game = datetime.datetime(datetime.MINYEAR, 1, 1, 0, 0, 0, 0) + current_games = list_games(db=db) + print(f"{current_games}") + # Depends on being sorted by date - which is currently _coincidentally_ true of our source data as games have thus + # far only been added in date order, but is not necessarily the case. + # TODO - implement sorting (and pagination) of returned data, then update this to take advantage of it. + if current_games: + # I.e. if any games have been returned from the db + date_of_latest_game = current_games[-1].date + else: + # No games have been returned from the db, thus no games should be skipped for downloading + date_of_latest_game = datetime.datetime(datetime.MINYEAR, 1, 1, 0, 0, 0, 0) + # Mapping from name to set-of-owned-decks # (Set rather than list so that we can blindly `.add`) player_decks = defaultdict(set) @@ -116,23 +141,35 @@ def all_in_one(file: UploadFile, db: Session = Depends(get_db)): deck_id_lookup = {} for player_name, decks in player_decks.items(): - player = crud.create_player( - db=db, player=schemas.PlayerCreate(name=player_name) - ) - LOGGER.info(f"Seeded {player=}") - player_id = player.id - player_id_lookup[player_name] = player_id - for deck_name in decks: - deck = crud.create_deck( - db=db, - deck=schemas.DeckCreate( - name=deck_name, description="", owner_id=player_id - ), + if player_name in current_player_ids_by_name: + LOGGER.info(f"Looked up {player_name=} from existing database") + player_id = current_player_ids_by_name[player_name] + else: + player = crud.create_player( + db=db, player=schemas.PlayerCreate(name=player_name) ) - LOGGER.info(f"Seeded {deck=}") - # We need to look up deck id by `player_name:deck_name` because there could be multiple decks with the same - # name owned by different people :D - deck_id_lookup[f"{player_name}:{deck_name}"] = deck.id + LOGGER.info(f"Seeded {player=}") + player_id = player.id + + player_id_lookup[player_name] = player_id + + for deck_name in decks: + if deck_name in current_deck_ids_by_name: + LOGGER.info(f"Looked up {deck_name=} from existing database") + deck_id_lookup[f"{player_name}:{deck_name}"] = current_deck_ids_by_name[ + deck_name + ] + else: + deck = crud.create_deck( + db=db, + deck=schemas.DeckCreate( + name=deck_name, description="", owner_id=player_id + ), + ) + LOGGER.info(f"Seeded {deck=}") + # We need to look up deck id by `player_name:deck_name` because there could be multiple decks with the same + # name owned by different people :D + deck_id_lookup[f"{player_name}:{deck_name}"] = deck.id def parse_date(date_string) -> datetime.datetime: month, day, year = date_string.split("/") @@ -149,6 +186,21 @@ def all_in_one(file: UploadFile, db: Session = Depends(get_db)): # ((Yes, I know that's an abuse of Big-O notation, shut up - you knew what I meant :P )) reader = csv.DictReader(file_contents, delimiter=",") for row in reader: + # Skip any games created before the date of the latest current game + # (Note that this means that the `all_in_one` method cannot be used to backfill any previously-played games. If + # there arises a desire for that, instead will have to check each potentially-uploaded game against _every_ + # currently-uploaded one to check for pre-existence (or, make the "create" option idempotent...though that + # probably shouldn't be the case, as attempting to upload the same game twice is _probably_ an indication of an + # automated script or summarization going rogue, which should be flagged up _as_ an error rather than blindly + # continued. For the User-facing UI, just present a "whoops! You submitted a duplicate" screen)) + date_of_current_row = parse_date(row["Date"]) + if date_of_current_row <= date_of_latest_game: + message = f"Skipped a game on {date_of_current_row} because it is not later than {date_of_latest_game}" + LOGGER.info(message) + # TBD - logging does not seem to be showing up as-expected + print(message) + continue + # Note that we intentionally create via the API, not via direct `crud.create_game`, to trigger ELO calculation. index_of_winning_deck = [ @@ -157,7 +209,7 @@ def all_in_one(file: UploadFile, db: Session = Depends(get_db)): print(f"DEBUG - checking row {row}") created_game = create_game( schemas.GameCreate( - date=parse_date(row["Date"]), + date=date_of_current_row, **{ f"deck_id_{i+1}": deck_id_lookup[ row[f"Player {i+1}"] + ":" + row[f"Deck {i+1}"] diff --git a/seed-data/all-in-one-updated-for-incremental-add-testing.csv b/seed-data/all-in-one-updated-for-incremental-add-testing.csv new file mode 100644 index 0000000..fe611ec --- /dev/null +++ b/seed-data/all-in-one-updated-for-incremental-add-testing.csv @@ -0,0 +1,96 @@ +Date,Player 1,Deck 1,Player 2,Deck 2,Player 3,Deck 3,Player 4,Deck 4,Player 5,Deck 5,Player 6,Deck 6,Winning Player,Winning Deck,# turns,turn 1st player out,Type of win,Format,Notes +1/13/24,Evan,Kelsien the Plague,Terence,Ravos/Rebbec,Patrick,Duke Ulder Ravengard,Jeff,Mondrak,,,,,Terence,Ravos/Rebbec,15,12,combat damage,FFA,3-4 board wipes; Biotransference +1/13/24,Terence,Wernog/Cecily,Patrick,Duke Ulder Ravengard,Ryan,Don Andres,Jeff,Mondrak,Evan,Jasmine Boreal of the Seven,,,Patrick,Duke Ulder Ravengard,12,10,combat damage,FFA,Possessed Portal scary! +1/13/24,Jeff,Go-Shintai of Life's Origin,Evan,Kethis the Hidden Hand,Terence,Rograkh/Silas ninjas,Patrick,Tekuthal,Ryan,Gitrog,,,Patrick,Tekuthal,8,8,poison,FFA,Flux Channeler + Tekuthal +1/13/24,Patrick,Tekuthal,Jeff,Illuna,Evan,"Atraxa, Praetor's Voice",Terence,Muldrotha,,,,,Patrick,Tekuthal,20,5,combat damage,FFA,3 Hullbreaker Horrors simultaneously (Evan forfeited ~t5; game took ~2hrs) +1/17/24,Patrick,Duke Ulder Ravengard,Ryan,Grist,Terence,Ravos/Rebbec,,,,,,,Patrick,Duke Ulder Ravengard,12,12,combat damage,FFA,"Eternal Wanderer wipe, Angel Serenity + Sun Titan followup" +1/17/24,Terence,Ravos/Rebbec,Patrick,Duke Ulder Ravengard,Ryan,Grist,,,,,,,Terence,Ravos/Rebbec,13,11,combat damage,FFA,Verge Rangers + Conjurer's Mantle +1/23/24,Ryan,Goose Mother,Patrick,Maarika,Terence,Wilson/Cultist,,,,,,,Patrick,Maarika,8,7,21+ commander,FFA,Maarika + Runes of the Deus +1/23/24,Ryan,Goose Mother,Patrick,Maarika,Terence,Wilson/Cultist,,,,,,,Terence,Wilson/Cultist,10,8,combat damage,FFA,Scepter of Celebration brings an army +1/23/24,Terence,Abdel Adrian/Far Traveler,Ryan,Obeka,Patrick,Laelia,,,,,,,Patrick,Laelia,9,8,21+ commander,FFA,Nalfeshnee copying Storm's Wrath cleared the way; Ryan mana screwed +1/23/24,Terence,Jan Jansen,Ryan,Don Andres,Patrick,Laelia,,,,,,,Terence,Jan Jansen,10,10,aristocrats/burn,FFA,Mirkwood Bats + Thornbite Staff ftw; Ryan mana screwed +1/30/24,Patrick,Urza,Terence,"Me, the Immortal",Ryan,Gitrog,,,,,,,Ryan,Gitrog,8,7,combat damage,FFA,Mending of Dominaria + Lotus Cobra (10 lands) +1/30/24,Patrick,Kiora,Terence,Raffine,Ryan,Kozilek,,,,,,,Patrick,Kiora,11,9,combat damage,FFA,"Raffine does all the work, then Kederekt Leviathan cleans up" +2/4/24,Ajit,Jhoira of the Ghitu,Brandon,Anikthea,Patrick,Oops all Kayas,,,,,,,Brandon,Anikthea,12,11,combat damage,FFA,Unopposed enchantress with card draw and Nature's Will +2/4/24,Brandon,Silvar/Trynn,Patrick,Oops all Kayas,Ryan,"Purphoros, God of the Forge",Ajit,Jhoira of the Ghitu,,,,,Brandon,Silvar/Trynn,12,10,aristocrats/burn,FFA,Bastion of Remembrance overcame Emrakul + It That Betrays +2/4/24,Ajit,Brago,Brandon,Marneus Calgar,Patrick,Laelia,Ryan,Gitrog,,,,,Ryan,Gitrog,15,12,combat damage,FFA,Gitrog draws a zillion cards and closes w/ Multani +2/4/24,Terence,Slimefoot and Squee,Patrick,Ayara,Ryan,Omnath,,,,,,,Ryan,Omnath,8,8,combat damage,FFA,"Vorinclex, Voice of Hunger enables Crackle with Power (x=4) to overcome Army of the Damned" +2/20/24,Terence,Wilson/Cultist,Ryan,Gitrog,Patrick,Tekuthal,,,,,,,Terence,Wilson/Cultist,15,12,combat damage,FFA,Weatherlight and Ormendahl collect many tithes +2/20/24,Ryan,Myrel,Patrick,Tekuthal,Terence,Ravos/Rebbec,,,,,,,Patrick,Tekuthal,10,9,combat damage,FFA,Arcbound Crusher got chonky (w/ Sword of Truth & Justice) +2/20/24,Terence,Ravos/Rebbec,Ryan,Myrel,Patrick,Tekuthal,,,,,,,Terence,Ravos/Rebbec,10,9,combat damage,FFA,Ryan's Coat of Arms + Terence's Door of Destinies and Haunted One = math! +2/25/24,Patrick,Elmar storm,Jack J,Gale/Scion of Halaster,Terence,Kefnet the Mindful,Jeff,Mondrak,Ryan,Don Andres,,,Jeff,Mondrak,12,9,combat damage,FFA,Elesh Norn is a good finisher; Ryan achievement unlocked: control all other commanders at once +2/25/24,Terence,Rograkh/Silas ninjas,Jeff,Illuna,Ryan,Emiel,Patrick,Myrkul planeswalkers,Jack J,Syr Ginger,,,Terence,Rograkh/Silas ninjas,16,15,combat damage,FFA,Yuriko + ninja informant = overwhelming card advantage +2/25/24,Ryan,Omnath,Patrick,Rafiq,Terence,Yoshimaru/Reyhan,Jeff,Go-Shintai of Life's Origin,,,,,Jeff,Go-Shintai of Life's Origin,8,5,combat damage,FFA,Shrine of tapping stuff down bought just enough time +2/28/24,Terence,Dargo/Nadier,Ryan,Pantlaza,Patrick,Niv-Mizzet,,,,,,,Ryan,Pantlaza,10,9,combat damage,FFA,Hasty dinosaurs post-wrath with a Savage Order for Gishath chonks life totals +2/28/24,Ryan,Gitrog,Patrick,Niv-Mizzet,Terence,Dargo/Nadier,,,,,,,Patrick,Niv-Mizzet,9,9,combat damage,FFA,Faeburrow Elder enables quick acceleration; Case of the Shattered Pact is quick damage +2/28/24,Ryan,Gitrog,Patrick,Niv-Mizzet,Terence,Dargo/Nadier,,,,,,,Terence,Dargo/Nadier,8,5,combat damage,FFA,Dargo with Jeska 1-shot Ryan (and earned 27 impulse draws via Fire Giant's Fury - into Burnt Offering); Skull Storm precisely lethal +2/28/24,Ryan,"Liesa, Shroud of Dusk",Patrick,Duke Ulder Ravengard,Terence,Malcolm/Ich-Tekik,,,,,,,Patrick,Duke Ulder Ravengard,13,11,combat damage,FFA,Stealing Angel of Destiny and giving myriad 1-shot Ryan (while earning 60 life) +3/10/24,stranger,"Krenko, Tin Street Kingpin",Patrick,Tekuthal,stranger,Gishath,stranger,Ur-Dragon,,,,,stranger,Ur-Dragon,7,7,combat damage,FFA,"Master Warcraft with Atarka, Miirym, and 2 Ur-Dragons; poor threat analysis (2 friends didn't target each other)" +3/10/24,stranger,Gishath,stranger,Ur-Dragon,stranger,Korvold,Patrick,Dihada,,,,,Patrick,Dihada,10,8,combat damage,FFA,Heroes' Podium + Day of Destiny stacks quickly; Malik gets around Lightning Greaves (same 2 friends) +3/10/24,stranger,"Lazav, the Multifarious",stranger,"Mirri, Weatherlight Duelist",Patrick,Duke Ulder Ravengard,stranger,Anzrag,,,,,Patrick,Duke Ulder Ravengard,11,9,combat damage,FFA,Lazav cast an honest Eater of Days to block Anzrag buffed by Xenagos; Knight-Captain of Eos perma-fogged Anzrag to victory +3/10/24,stranger,"Brimaz, Blight of Oreskos",stranger,"The Master, Multiplied",stranger,"Lonis, Cryptozoologist",stranger,"Mirri, Weatherlight Duelist",Patrick,Tekuthal,,,stranger,"The Master, Multiplied",7,7,combat damage,FFA,unchecked Master Multiplied with ramp = 13 Masters attacking everyone; Brimaz + Tekuthal both mana screwed +3/10/24,stranger,"Brimaz, Blight of Oreskos",stranger,"The Master, Multiplied",stranger,"Lonis, Cryptozoologist",stranger,"Mirri, Weatherlight Duelist",Patrick,Tekuthal,,,stranger,"Brimaz, Blight of Oreskos",12,12,quality of life concede,FFA,Several board wipes + removal with life totals largely intact; dealt with Koma; Myojin of Seeing Winds drew 13 and could keep going but conceded +3/15/24,Terence,Atraxa win-cons,Ryan,Emiel,Patrick,Duke Ulder Ravengard,,,,,,,Patrick,Duke Ulder Ravengard,16,13,combat damage,FFA,Ghostway + wrath to slow down Emiel+Seedborn +3/15/24,Patrick,Niv-Mizzet,Terence,Yidris dredge,Ryan,Pantlaza,,,,,,,Terence,Yidris dredge,10,9,combat damage,FFA,"Living End claims another victim, even when scripted a turn ahead" +3/15/24,Ryan,Pantlaza,Patrick,Niv-Mizzet,Terence,Yidris dredge,,,,,,,Ryan,Pantlaza,12,12,combat damage,FFA,Ghalta discover trigger into Portal to Phyrexia (with Skullspore Nexus) +3/15/24,Ryan,Reyav,Patrick,Laelia,Terence,Yidris dredge,,,,,,,Terence,Yidris dredge,9,7,combat damage,FFA,Echoing Equation on Hogaak takes out Patrick; Narcomoeba attacking was the final damage to Ryan +3/30/24,Jack F,"Brenard, Ginger Sculptor",Terence,Ravos/Rebbec,Patrick,Duke Ulder Ravengard,Jeff,Mondrak,Brandon,The Wise Mothman,,,Jeff,Mondrak,11,8,combat damage,FFA,Anointed Procession + (destroyed: Cathar's Crusade + Starlight Spectacular) with multiple wraths overcame a LARGE Mothman and Brenard's army +3/30/24,Ajit,Brago,Patrick,Laelia,Jeff,Go-Shintai of Life's Origin,Brandon,"Liberty Prime, Recharged",Jack F,"Astor, Bearer of Blades",,,Ajit,Brago,14,9,combat damage,FFA,"Brago + Medomai turns ft. 21/21 Astor (RIP Patrick), 22/22 Danitha; 31/31 Laelia; Goblin Welder loops of Synth-Reflector Mages; 7+ shrines threatening w/ Zur" +4/10/24,Terence,Ishai/Tana,Ryan,Don Andres,Patrick,Tekuthal,,,,,,,Terence,Ishai/Tana,9,9,combat damage,FFA,Assemble the Legion + Felidar Retreat +4/10/24,Patrick,Tekuthal,Terence,Ishai/Tana,Ryan,Don Andres,,,,,,,Patrick,Tekuthal,10,10,poison,FFA,Don Andres cast Eternal Dominion +4/16/24,Ryan,Goose Mother,Terence,"Vorinclex, Monstrous Raider",Patrick,Maarika,,,,,,,Terence,"Vorinclex, Monstrous Raider",10,9,21+ commander,FFA,Vorinclex + Bone Saws backed up by hexproof +4/16/24,Terence,Kefnet the Mindful,Patrick,Maarika,Ryan,Goose Mother,,,,,,,Terence,Kefnet the Mindful,10,9,21+ commander,FFA,Kefnet wears a Robe of the Archmagi and survives Phasing of Zhalfir; Ryan Mana Drained a t4 Harmonize into a large Goose +4/16/24,Terence,Kefnet the Mindful,Patrick,Dihada,Ryan,Emiel,,,,,,,Terence,Kefnet the Mindful,13,12,21+ commander,FFA,Kefnet w/ Empyrial Plate +4/20/24,Jack J,Syr Ginger,Patrick,Kamahl/Prava,Terence,Atraxa win-cons,,,,,,,Patrick,Kamahl/Prava,9,8,combat damage,FFA,Inspiring Leader + both commanders +4/20/24,Terence,Atraxa win-cons,Jack J,Syr Ginger,Ryan,Emiel,Patrick,Kamahl/Prava,,,,,Jack J,Syr Ginger,15,12,combat damage,FFA,Ghalta/Mavren made 20 vampires (w/ Doubling Season); Millenium Calendar hit 50; Spine of Ish Sah recurred each turn +4/20/24,Ryan,Gitrog,Patrick,Laelia,Terence,Amber/Veteran Soldier,Jack J,Ghyrson Starn,,,,,Patrick,Laelia,10,9,21+ commander,FFA,"Throne of Eldraine into Etali snowballed, then Laelia cascaded for +50/+50. Slicer *nearly* took Laelia out. Terence Generous Gifted Ryan's only land" +4/20/24,Terence,Vial Smasher/Sidar Kondo,Ryan,"Liesa, Shroud of Dusk",Patrick,Dihada,,,,,,,Ryan,"Liesa, Shroud of Dusk",11,10,combat damage,FFA,Austere Command +4/20/24,Patrick,Dihada,Terence,Vial Smasher/Sidar Kondo,Ryan,"Liesa, Shroud of Dusk",,,,,,,Terence,Vial Smasher/Sidar Kondo,10,8,combat damage,FFA,Authority of the Consul +4/23/24,Patrick,Ashad,Terence,Phabine,Ryan,Zaxara,,,,,,,Terence,Phabine,8,7,combat damage,FFA,"Adeline, Port Razer, Halana and Alena into Phabine" +4/23/24,Ryan,Zaxara,Patrick,Ashad,Terence,(Borrowed) Ryan's Jon Irenicus,,,,,,,Ryan,Zaxara,9,7,combat damage,FFA,Genesis Wave for 7; Exponential Growth for 3; Villainous Wealth for 19; Biomass Mutation for 32 (477 trample damage) +4/23/24,Ryan,Reyav,Patrick,Gwenna,Terence,Yidris dredge,,,,,,,Patrick,Gwenna,11,7,combat damage,FFA,Living Death wiped out Reyav early; Ram Through post Zopandrel for lethal +4/23/24,Ryan,Gitrog,Patrick,Rafiq,Terence,Jan Jansen,,,,,,,Terence,Jan Jansen,9,8,aristocrats/burn,FFA,Mirkwood Bats + Thornbite Staff + Mayhem Devil + Jan Jansen ftw; Rafiq flooded after a terrifying start +5/1/24,Brandon,"Gavi, Nest Warden",Ryan,Ziatora,Patrick,Ashad,Terence,"Izzet (Jori En, Ruin Diver)",,,,,Terence,"Izzet (Jori En, Ruin Diver)",9,8,combat damage,FFA,"Arcane Bombardment, Stormkiln Artist, Haughty Djinn should've been stopped sooner. Djinn Illuminatus replicated Lightning Bolt x8 to take out Ashad" +5/1/24,Patrick,Ashad,Terence,"Izzet (Bilbo, Retired Burglar)",Brandon,"Dogmeat, Ever Loyal",Ryan,Ziatora,,,,,Patrick,Ashad,11,6,combat damage,FFA,Ziatora+GE-Rhonas+Zopandrel KO'ed Brandon on t6; Ashad and 8 Mishra's Self-Replicators went wide enough +5/7/24,Terence,Tevesh Szat/Kraum,Ryan,Ur-Dragon,Patrick,Kamahl/Prava,,,,,,,Ryan,Ur-Dragon,11,10,combat damage,FFA,"Elminster's Simulacrum (copied) created 2 dragons and a Kamahl, but didn't live to untap" +5/7/24,Terence,Wernog/Cecily,Ryan,Ur-Dragon,Patrick,Kamahl/Prava,,,,,,,Terence,Wernog/Cecily,11,11,alt win-con,FFA,Hellkite Tyrant win-con enabled by Storm the Vault and Wernog flickers (equalled 32 life!) +5/7/24,Terence,Ravos/Rebbec,Ryan,Zaxara,Patrick,Kamahl/Prava,,,,,,,Terence,Ravos/Rebbec,11,10,combat damage,FFA,"Rammas Echor, Door of Destinies, Metallic Mimic, Hero of Bladehold took out Ryan 2 turns before Simic Ascendancy, then Anduril, Narsil Reforged + Biotransference closed it out after a mana-screwed Patrick dropped 8 lands (Harvest Season) w/ Felidar Retreat and Doubling Season" +5/15/24,Ryan,"Tinybones, the Pickpocket",Patrick,"Smeagol, Helpful Guide",Terence,Atraxa win-cons,,,,,,,Terence,Atraxa win-cons,10,10,alt win-con,FFA,Accidental Millennium Calendar win; Smeagol milled both opponents out but forgot Calendar would win on upkeep (and had artifact removal in hand) +5/15/24,Patrick,"Smeagol, Helpful Guide",Terence,Atraxa win-cons,Ryan,"Tinybones, the Pickpocket",,,,,,,Patrick,"Smeagol, Helpful Guide",11,10,combat damage,FFA,Awakening Zone triggered Smeagol every turn; Rampaging Baloths + Invasion of Lorwyn closed out +5/15/24,Terence,Emry,Ryan,Omnath,Patrick,Niv-Mizzet,,,,,,,Terence,Emry,9,9,combat damage,FFA,"T3 Kappa Cannoneer survived 2 board wipes, then Echo Storm created 4 more" +5/15/24,Patrick,Niv-Mizzet,Terence,Emry,Ryan,Omnath,,,,,,,Ryan,Omnath,8,8,aristocrats/burn,FFA,Nissa + Ancient Greenwarden into Crackle With Power +5/18/24,Jack J,Syr Ginger,Patrick,Maarika,Jack F,"Brenard, Ginger Sculptor",Brandon,"Dogmeat, Ever Loyal",,,,,Brandon,"Dogmeat, Ever Loyal",9,6,combat damage,FFA,31-power Dogmeat (Strong Back+Mantle of the Ancients) archenemy'ed the table +5/18/24,Patrick,Maarika,Jack F,Jeskai (The War Doctor/Clara Oswald (blue)),Brandon,"Caesar, Legion's Emperor",Jack J,Ghyrson Starn,,,,,Patrick,Maarika,12,7,combat damage,FFA,Maarika/Tergrid stole Fervent Charge and Iroas to lethal Brandon +5/18/24,Jack F,Jeskai (Kate Stewart),Brandon,"Morska, Undersea Sleuth",Jack J,Ghyrson Starn,Patrick,Kamahl/Prava,,,,,Jack J,Ghyrson Starn,7,7,aristocrats/burn,FFA,"Niv-Mizzet, Ghyrson Starn, Ophidian Eye" +5/18/24,Jack F,"Astor, Bearer of Blades",Brandon,Commodore Guff,Ryan,Don Andres,Jack J,Jon Irenicus,Patrick,Kamahl/Prava,,,Jack F,"Astor, Bearer of Blades",15,9,combat damage,FFA,Lae'Zel and 5 planeswalkers (unanswered for 5 turns) played archenemy until Holy Day and 2 Bruenor swings to the face +5/21/24,Terence,Wilson/Cultist,Patrick,Tekuthal,Ryan,Emiel,,,,,,,Terence,Wilson/Cultist,16,14,combat damage,FFA,Wilson's card-advantage engines never stopped; Jace TMS ult'ed (after a 5*2 proliferate turn) on Emiel; Emiel's White Dragon tapped down Tekuthal's blockers +5/21/24,Ryan,Toxrill,Terence,Rigo,Patrick,Jared [Jegantha],,,,,,,Terence,Rigo,10,8,poison,FFA,"Norn's Decree: incentivized Jared to only attack Toxrill (which halted the 1/1 swarm), then was lethal to a first strike + normal strike would-be attack" +5/21/24,Terence,Vial Smasher/Sidar Kondo,Patrick,Ayara,Ryan,Gitrog,,,,,,,Terence,Vial Smasher/Sidar Kondo,9,8,combat damage,FFA,"Vial Smasher literally only hit Ayara (~20 damage); Ayara jumped from 2 to 20 (taking out mana-screwed Gitrog) w/ Gary, but unblockable + Mercadia's Downfall was exactly lethal" +5/25/24,Jeff,Mondrak,Terence,Viconia/Cultist,Patrick,Kiora,,,,,,,Terence,Viconia/Cultist,9,9,aristocrats/burn,FFA,Living Death: Ayara + Abhorrent Overlord devotion 32 insta lethal +5/25/24,Patrick,Kiora,Jeff,Mondrak,Terence,Viconia/Cultist,,,,,,,Terence,Viconia/Cultist,16,15,combat damage,FFA,Slow one: multiple board bounces and one wipe; bestowed Nighthowler for 19 closed it out +5/26/24,stranger,Jeska/Vial Smasher,stranger,Riku of Many Paths,stranger,Sovereign Okinec Ahau,Patrick,Rafiq,,,,,Patrick,Rafiq,10,5,21+ commander,FFA,"T3 Rafiq w/ mom protection took out Riku, then rebuilt post board wipe (mom survived) and 3/3 commander lethal" +5/26/24,Patrick,Ayara,stranger,"Kellan, the Fae-Blooded",stranger,Sovereign Okinec Ahau,,,,,,,stranger,"Kellan, the Fae-Blooded",13,8,combat damage,FFA,(opted not to play Bolas' Citadel b/c had just won previous game); Kellan won due to fog effect stopping Sovereign from hitting for 100+ +5/26/24,stranger,The Swarmlord,stranger,Tergrid,Patrick,Ashad,stranger,"Rocco, Street Chef",,,,,stranger,"Rocco, Street Chef",10,9,combat damage,FFA,Rocco and Tergrid emerged as heavyweights; Rocco Final Showdown'ed in response to Tergrid's Myojin of Night's Reach; Tergrid had to leave and Rocco cleaned up fast +6/2/24,Brandon,Sliver Gravemother,Ajit,Brago,Terence,Umori,Jeff,Go-Shintai of Life's Origin,Patrick,"Smeagol, Helpful Guide",,,Terence,Umori,12,8,combat damage,Star,"Smeagol was a punching bag; Aetherspouts stopped a lethal Rumbleweed, but Sanctum of Stone Fangs inadvertently took out Brandon before Ajit could be taken out" +6/2/24,Patrick,"Smeagol, Helpful Guide",Brandon,Abaddon,Ajit,Jhoira of the Ghitu,Terence,Borborygmos Enraged,Jeff,Mondrak,,,Patrick,"Smeagol, Helpful Guide",6,6,mill,Star,Naturally drew the mill combo +6/2/24,Terence,Borborygmos Enraged,Jeff,Mondrak,Patrick,Duke Ulder Ravengard,Brandon,"Morska, Undersea Sleuth",Ajit,Jhoira of the Ghitu,,,Terence,Borborygmos Enraged,10,8,combat damage,Star,"Windshaper Planetar rerouted 21 Mondrak damage from Brandon to Terence; Mondrak's Starlight Spectacular (would've won if we'd done math precombat to play 1 more creature) was short of killing Brandon so took out Patrick; Morska's Kappa Cannoneer ended Jeff, leaving Terence last one standing" +6/2/24,Brandon,"Atraxa, Grand Unifier",Patrick,Talion,Jeff,Illuna,Ajit,Kadena,Terence,Rograkh/Silas ninjas,,,Patrick,Talion,11,9,combat damage,Star,"Kadena early Tempt for Discovery and stole Talion; Atraxa early Breach the Multiverse (Mommy Norn, Oko, Apex Altisaur, Elspeth Knight Errant) became archenemy; loads of interaction; eventually Talion+Sakashima + Sheoldred closed the door" +6/11/24,Ryan,Reyav,Patrick,Talion,Terence,Umori,,,,,,,Patrick,Talion,13,11,21+ commander,FFA,T2 Hunted Horror centaurs nearly solo'ed Patrick; Sword of W&P stabilized from 5 life +6/11/24,Patrick,Duke Ulder Ravengard,Terence,Ardenn/Esior,Ryan,Emiel,,,,,,,Ryan,Emiel,11,9,combat damage,FFA,Esior w/ Sword of F&F and double strike was dominant until double Manglehorn +6/11/24,Terence,Slimefoot and Squee,Ryan,Chatterfang,Patrick,Ayara,,,,,,,Patrick,Ayara,9,8,aristocrats/burn,FFA,Bolas' Citadel activation lethaled Terence; Muranda Petroglyphs buffed more zombies than squirrels (thus keeping big Liliana alive) +7/5/24,Jeff,Illuna,Natalie,Anikthea,Jack J,Gale/Scion of Halaster,,,,,,,Jeff,Illuna,15,15,aristocrats/burn,FFA,A large Setessan Champion provided ample fuel for a triple Brash Taunter activation +7/5/24,Jeff,Ulalek,Natalie,(Borrowed) Jeff's Go-Shintai,Jack J,Slivers,,,,,,,Jack J,Slivers,8,8,combat damage,FFA,Jack snookered himself as Crystalline Sliver blocked Magma Sliver from going off. Magma Sliver + Sliver Queen + Heart Sliver + Ashnod's Altar is nuts (as is Mana Echoes) +7/5/24,Natalie,(Borrowed) Jack's Ghyrson Starn,Jack J,Evra,Jeff,Ulalek,,,,,,,Natalie,(Borrowed) Jack's Ghyrson Starn,9,9,aristocrats/burn,FFA,"Natalie won by playing a pointless Mana Geyser which triggered a bunch of ""on instant/sorcery"" effects" +7/5/24,Jeff,Mondrak,Natalie,(Borrowed) Jack's Melek,Jack J,Evra,,,,,,,Jack J,Evra,6,6,alt win-con,FFA,Jack wins with Felidar Sovereign after a slow start and with Aetherflux Reservoir +7/5/24,Jack J,Evra,Jeff,Mondrak,Natalie,(Borrowed) Jack's Melek,,,,,,,Jeff,Mondrak,11,11,combat damage,FFA,Guardian of Faith whiffed a huge Evra swing +7/5/24,Natalie,Anikthea,Jack J,Syr Ginger,Jeff,"Liesa, Forgotten Archangel",,,,,,,Natalie,Anikthea,15,15,combat damage,FFA,Enchantments are oppressive! +7/13/24,Ryan,Ulalek,Patrick,Oops all Kayas,Jeff,Ulalek,,,,,,,Jeff,Ulalek,9,7,combat damage,FFA,MH3 Ulamog as a 19/19 w/ Annihilator 12 > two It That Betrays; Patrick mana screwed +7/13/24,Patrick,Oops all Kayas,Jeff,Ulalek,Ryan,Ulalek,,,,,,,Patrick,Oops all Kayas,11,10,combat damage,FFA,13 spirit and bird tokens ftw! Buffed by Intangible Virtue +7/13/24,Patrick,Ashad,Jeff,"Liesa, Forgotten Archangel",Ryan,Rin and Seri,,,,,,,Ryan,Rin and Seri,11,10,combat damage,FFA,"Liesa + Luminous Broodmoth + Vito = redundancy and fast drains! But, double Combustible Gearhulk dropped Liesa from 39 to 4 before dying; Rin and Seri had exactly enough creatures for lethal (upon noting Dauthi Voidwalker has shadow and couldn't block!)" +7/13/24,Patrick,Tekuthal,Jeff,Mondrak,Ryan,Gitrog,,,,,,,Jeff,Mondrak,8,7,combat damage,FFA,"Sol Ring -> Throne of Eldraine -> Cathar's Crusade -> Elesh Norn, GC took out Tekuthal + Flux Channeler + Danny Pink w/ Sword of Truth and Justice one turn before lethal; Gitrog mana screwed" +7/13/24,Jeff,Go-Shintai of Life's Origin,Ryan,Breya,Patrick,Gwenna,,,,,,,Patrick,Gwenna,12,12,combat damage,FFA,Heavyweight battle! KO turn: topdecked Return of the Wildspeaker for 10 cards -> Old Gnawbone + Pest Infestation to clear out leftovers after exiling Darksteel Forge. Double Portal to Phyrexia ate enough Shrines; red Shrine took Daretti and 1 Garruk +7/13/24,Ryan,Reyav,Patrick,Dihada,Jeff,Mondrak,,,,,,,Ryan,Reyav,5,4,21+ commander,FFA,Sol Ring -> Reyav + Shadowspear + 3 more equipment +7/13/24,Patrick,Dihada,Jeff,Mondrak,Ryan,Reyav,,,,,,,Patrick,Dihada,8,7,combat damage,FFA,Reaver Cleaver on Mogis into Gallifrey Falls+No More to (mostly) 1-sided wipe +7/25/24,Ryan,Ulalek,Terence,Ardenn/Esior,Patrick,Ashad,,,,,,,Terence,Ardenn/Esior,12,7,21+ commander,FFA,Ardenn w/ Colossus Hammer for 22; Ugin's Binding bought Ryan a couple turns before the inevitable +7/25/24,Terence,Ardenn/Esior,Patrick,Ashad,Ryan,Goose Mother,,,,,,,Ryan,Goose Mother,12,12,combat damage,FFA,Esior stalled big time; 2x Blinkmoth Urn accelerated large Goose Mothers; critical Silence stopped Ashad; Momentous Fall for 12 into 2x counterspell + Questing Beast lethal +7/25/24,Terence,Vial Smasher/Sidar Kondo,Patrick,Niv-Mizzet,Ryan,Emiel,,,,,,,Terence,Vial Smasher/Sidar Kondo,11,7,combat damage,FFA,Mercadia's Downfall adds 20+ damage to oust Niv-Mizzet \ No newline at end of file diff --git a/tests/test_fresh_db_tests.py b/tests/test_fresh_db_tests.py index ab65a2e..81881bd 100644 --- a/tests/test_fresh_db_tests.py +++ b/tests/test_fresh_db_tests.py @@ -62,6 +62,36 @@ def test_add_and_retrieve_deck(test_client: TestClient, cleanups): cleanups.add_success(success_cleanup) +def test_incremental_add_of_games(test_client: TestClient, cleanups): + latest_deck_response = _json_get(test_client, "/game/latest_game") + assert latest_deck_response.status_code == 404 + + # https://github.com/tiangolo/fastapi/issues/1536#issuecomment-640781718 + with open("seed-data/all-in-one.csv", "rb") as f: + test_client.post( + "/api/seed/all_in_one", + files={"file": ("fake_all_in_one_filename.csv", f, "text/csv")}, + ) + + latest_deck_response = _json_get(test_client, "/game/latest_game") + assert latest_deck_response.status_code == 200 + print(latest_deck_response.json()) + assert latest_deck_response.json()["date"] == "2024-07-05T00:00:00" + + # then seed again, and check that it successfully gets the expected latest + with open( + "seed-data/all-in-one-updated-for-incremental-add-testing.csv", "rb" + ) as f: + test_client.post( + "/api/seed/all_in_one", + files={"file": ("fake_all_in_one_filename.csv", f, "text/csv")}, + ) + + latest_deck_response = _json_get(test_client, "/game/latest_game") + assert latest_deck_response.status_code == 200 + assert latest_deck_response.json()["date"] == "2024-07-25T00:00:00" + + def _json_get(c: TestClient, path: str) -> httpx.Response: return c.get(f"/api{path}", headers={"Content-Type": "application/json"})