Compare commits

...

3 Commits

Author SHA1 Message Date
228e0043d5 fix debug in docker cmd 2022-06-21 18:33:09 -05:00
7d57f00e5d memory management and loading animations 2022-06-21 18:31:05 -05:00
0090862359 atomicity fixes 2022-06-21 18:30:54 -05:00
3 changed files with 164 additions and 140 deletions

View File

@@ -2,9 +2,10 @@
docker build -t wordlinator:latest . docker build -t wordlinator:latest .
if [ "$1" = "--debug" ]; then if [ "$1" == "--debug" ]; then
shift shift
docker run -d --rm -p 8050:8050 -e DEBUG=true -e DB_PORT -e DB_HOST -e DB_PASS "$@" --name wordlinator wordlinator:latest echo "Running debug mode..."
docker run -d --rm -p 8050:8050 -e DASH_DEBUG=true -e DB_PORT -e DB_HOST -e DB_PASS "$@" --name wordlinator wordlinator:latest
else else
docker run -d --rm -p 8050:8050 -e DB_PORT -e DB_HOST -e DB_PASS "$@" --name wordlinator wordlinator:latest docker run -d --rm -p 8050:8050 -e DB_PORT -e DB_HOST -e DB_PASS "$@" --name wordlinator wordlinator:latest
fi fi

View File

@@ -85,140 +85,155 @@ class WordleDb:
return list(User.select()) return list(User.select())
def get_users_by_round(self, round_no=None, round_id=None): def get_users_by_round(self, round_no=None, round_id=None):
query = ( with db.atomic():
User.select(User, Player.user_id, Game.game) query = (
.join(Player, on=(Player.user_id == User.user_id)) User.select(User, Player.user_id, Game.game)
.join(Game, on=(Game.game_id == Player.game_id)) .join(Player, on=(Player.user_id == User.user_id))
) .join(Game, on=(Game.game_id == Player.game_id))
if round_no: )
query = query.filter(Game.game == round_no) if round_no:
elif round_id: query = query.filter(Game.game == round_no)
query = query.filter(Game.game_id == round_id) elif round_id:
return list(query) query = query.filter(Game.game_id == round_id)
return list(query)
def get_user_id(self, username): def get_user_id(self, username):
user = self.get_user(username) with db.atomic():
return user.twitter_id if user else None user = self.get_user(username)
return user.twitter_id if user else None
def add_user(self, username, user_id, check_twitter=True): def add_user(self, username, user_id, check_twitter=True):
return User.create( with db.atomic():
username=username, twitter_id=user_id, check_twitter=check_twitter return User.create(
) username=username, twitter_id=user_id, check_twitter=check_twitter
)
def get_rounds(self): def get_rounds(self):
return list(sorted(Game.select(), key=lambda d: d.start_date)) with db.atomic():
return list(sorted(Game.select(), key=lambda d: d.start_date))
def get_or_create_round(self, round_no, start_date=None): def get_or_create_round(self, round_no, start_date=None):
try: with db.atomic():
return Game.get(Game.game == round_no) try:
except peewee.DoesNotExist: return Game.get(Game.game == round_no)
start_date = ( except peewee.DoesNotExist:
start_date or wordlinator.utils.WORDLE_GOLF_ROUND_DATES[round_no - 1] start_date = (
) start_date
return Game.create(game=round_no, start_date=start_date) or wordlinator.utils.WORDLE_GOLF_ROUND_DATES[round_no - 1]
)
return Game.create(game=round_no, start_date=start_date)
def get_or_create_hole(self, round_no, hole_no): def get_or_create_hole(self, round_no, hole_no):
round = self.get_or_create_round(round_no) with db.atomic():
round = self.get_or_create_round(round_no)
try: try:
return Hole.get(Hole.hole == hole_no, Hole.game_id == round.game_id) return Hole.get(Hole.hole == hole_no, Hole.game_id == round.game_id)
except peewee.DoesNotExist: except peewee.DoesNotExist:
return Hole.create(hole=hole_no, game_id=round.game_id) return Hole.create(hole=hole_no, game_id=round.game_id)
def get_holes(self, round_no, ensure_all=False): def get_holes(self, round_no, ensure_all=False):
round = self.get_or_create_round(round_no) with db.atomic():
if ensure_all: round = self.get_or_create_round(round_no)
self.create_round_holes(round_no) if ensure_all:
return list(Hole.select().filter(game_id=round.game_id)) self.create_round_holes(round_no)
return list(Hole.select().filter(game_id=round.game_id))
def create_round_holes(self, round_no): def create_round_holes(self, round_no):
for hole_no in range(1, 19): with db.atomic():
self.get_or_create_hole(round_no, hole_no) for hole_no in range(1, 19):
self.get_or_create_hole(round_no, hole_no)
def get_or_create_player_round(self, user_id, game_id): def get_or_create_player_round(self, user_id, game_id):
try: with db.atomic():
return Player.get(Player.user_id == user_id, Player.game_id == game_id) try:
except peewee.DoesNotExist: return Player.get(Player.user_id == user_id, Player.game_id == game_id)
return Player.create(user_id=user_id, game_id=game_id) except peewee.DoesNotExist:
return Player.create(user_id=user_id, game_id=game_id)
def add_user_to_round(self, username, round_no): def add_user_to_round(self, username, round_no):
user = self.get_user(username) with db.atomic():
if not user: user = self.get_user(username)
raise ValueError(f"No user found with username {username}") if not user:
round = self.get_or_create_round(round_no) raise ValueError(f"No user found with username {username}")
return self.get_or_create_player_round(user.user_id, round.game_id) round = self.get_or_create_round(round_no)
return self.get_or_create_player_round(user.user_id, round.game_id)
def remove_user_from_round(self, username, round_no): def remove_user_from_round(self, username, round_no):
user = self.get_user(username) with db.atomic():
if not user: user = self.get_user(username)
raise ValueError(f"No user found with username {username}") if not user:
round = self.get_or_create_round(round_no) raise ValueError(f"No user found with username {username}")
try: round = self.get_or_create_round(round_no)
player = Player.get(user_id=user.user_id, game_id=round.game_id) try:
player.delete_instance() player = Player.get(user_id=user.user_id, game_id=round.game_id)
except peewee.DoesNotExist: player.delete_instance()
return except peewee.DoesNotExist:
return
def copy_players_from_round(self, from_round_no, to_round_no): def copy_players_from_round(self, from_round_no, to_round_no):
to_round = self.get_or_create_round(to_round_no) with db.atomic():
to_round = self.get_or_create_round(to_round_no)
for user in self.get_users_by_round(from_round_no): for user in self.get_users_by_round(from_round_no):
self.get_or_create_player_round(user.user_id, to_round.game_id) self.get_or_create_player_round(user.user_id, to_round.game_id)
def add_score(self, username, game, hole, score): def add_score(self, username, game, hole, score):
if not score: with db.atomic():
return if not score:
return
user = self.get_user(username) user = self.get_user(username)
if not user: if not user:
raise ValueError(f'No Such User "{username}"') raise ValueError(f'No Such User "{username}"')
hole = self.get_or_create_hole(game, hole) hole = self.get_or_create_hole(game, hole)
try: try:
score_obj = Score.get( score_obj = Score.get(
Score.user_id == user.user_id, Score.user_id == user.user_id,
Score.game_id == hole.game_id, Score.game_id == hole.game_id,
Score.hole_id == hole.hole_id, Score.hole_id == hole.hole_id,
) )
score_obj.score = score score_obj.score = score
score_obj.save() score_obj.save()
return score_obj return score_obj
except peewee.DoesNotExist: except peewee.DoesNotExist:
return Score.create( return Score.create(
score=score, score=score,
user_id=user.user_id, user_id=user.user_id,
game_id=hole.game_id, game_id=hole.game_id,
hole_id=hole.hole_id, hole_id=hole.hole_id,
) )
def get_scores(self, round_no=None, round_id=None): def get_scores(self, round_no=None, round_id=None):
if round_no: with db.atomic():
round = self.get_or_create_round(round_no) if round_no:
elif round_id: round = self.get_or_create_round(round_no)
round = Game.get_by_id(round_id) elif round_id:
else: round = Game.get_by_id(round_id)
raise ValueError("Must provide Round Number or Round ID") else:
res = ( raise ValueError("Must provide Round Number or Round ID")
Score.select( res = (
Score, Score.select(
Hole.hole, Score,
User.username, Hole.hole,
Player.game_id, User.username,
Player.game_id,
)
.join(Player, on=(Score.user_id == Player.user_id))
.switch(Score)
.join(Hole, on=(Score.hole_id == Hole.hole_id))
.switch(Score)
.join(User, on=(Score.user_id == User.user_id))
.filter(Player.game_id == round.game_id)
.filter(Score.game_id == round.game_id)
) )
.join(Player, on=(Score.user_id == Player.user_id)) return list(res) if res else []
.switch(Score)
.join(Hole, on=(Score.hole_id == Hole.hole_id))
.switch(Score)
.join(User, on=(Score.user_id == User.user_id))
.filter(Player.game_id == round.game_id)
.filter(Score.game_id == round.game_id)
)
return list(res)
def bulk_insert_scores(self, scores: typing.List[typing.Dict]): def bulk_insert_scores(self, scores: typing.List[typing.Dict]):
with db.atomic(): with db.atomic() as txn:
for batch in peewee.chunked(scores, 50): for batch in peewee.chunked(scores, 50):
Score.insert_many(batch).execute() Score.insert_many(batch).execute(txn)
def bulk_update_scores(self, scores: typing.List[Score]): def bulk_update_scores(self, scores: typing.List[Score]):
with db.atomic(): with db.atomic():
@@ -226,24 +241,25 @@ class WordleDb:
score.save() score.save()
def get_users_without_score(self, round_no, hole_no, tweetable=True): def get_users_without_score(self, round_no, hole_no, tweetable=True):
hole = self.get_or_create_hole(round_no, hole_no) with db.atomic() as txn:
# Find users who *have* played in this round, hole = self.get_or_create_hole(round_no, hole_no)
# but have no score on the current hole # Find users who *have* played in this round,
query_str = """SELECT u.username, player.game_id # but have no score on the current hole
FROM user_tbl u query_str = """SELECT u.username, player.game_id
JOIN player ON player.user_id = u.user_id FROM user_tbl u
WHERE ( JOIN player ON player.user_id = u.user_id
player.game_id = {} WHERE (
) AND NOT EXISTS ( player.game_id = {}
SELECT FROM score WHERE score.user_id = u.user_id AND score.hole_id = {} ) AND NOT EXISTS (
) SELECT FROM score WHERE score.user_id = u.user_id AND score.hole_id = {}
""".format( )
hole.game_id, hole.hole_id """.format(
) hole.game_id, hole.hole_id
)
if tweetable: if tweetable:
# Restrict to users who post scores on twitter # Restrict to users who post scores on twitter
query_str += " AND u.check_twitter = true" query_str += " AND u.check_twitter = true"
res = db.execute_sql(query_str) res = txn.execute_sql(query_str)
return [r[0] for r in res] return [r[0] for r in res]

View File

@@ -16,7 +16,7 @@ import wordlinator.utils
import wordlinator.utils.scores import wordlinator.utils.scores
import wordlinator.utils.web import wordlinator.utils.web
TTL_TIME = 10 if os.getenv("DEBUG") else 600 TTL_TIME = 30 if os.getenv("DEBUG") else 600
LEADERBOARD_COUNT = 20 LEADERBOARD_COUNT = 20
################### ###################
@@ -39,7 +39,7 @@ long_callback_manager = dash.long_callback.DiskcacheLongCallbackManager(
) )
@functools.lru_cache() @functools.lru_cache(maxsize=1)
def _games_from_db(ttl_hash=None): def _games_from_db(ttl_hash=None):
return db.WordleDb().get_rounds() return db.WordleDb().get_rounds()
@@ -48,7 +48,7 @@ def games_from_db():
return _games_from_db() return _games_from_db()
@functools.lru_cache() @functools.lru_cache(maxsize=1)
def _wordle_today(ttl_hash=None): def _wordle_today(ttl_hash=None):
today = wordlinator.utils.get_wordle_today() today = wordlinator.utils.get_wordle_today()
if today.golf_hole: if today.golf_hole:
@@ -64,22 +64,17 @@ def wordle_today():
return _wordle_today(get_ttl_hash()) return _wordle_today(get_ttl_hash())
@functools.lru_cache() @functools.lru_cache(maxsize=3)
def _scores_from_db(round_id, ttl_hash=None): def _scores_from_db(round_id, ttl_hash=None):
return db.WordleDb().get_scores(round_id=round_id) wordle_db = db.WordleDb()
scores = wordle_db.get_scores(round_id=round_id)
users = wordle_db.get_users_by_round(round_id=round_id)
@functools.lru_cache() usernames = [u.username for u in users]
def _players_from_db(round_id, ttl_hash=None): return wordlinator.utils.scores.ScoreMatrix(scores, usernames=usernames)
return db.WordleDb().get_users_by_round(round_id=round_id)
def scores_from_db(round_id): def scores_from_db(round_id):
users = _players_from_db(round_id) return _scores_from_db(round_id)
usernames = [u.username for u in users]
return wordlinator.utils.scores.ScoreMatrix(
_scores_from_db(round_id, get_ttl_hash()), usernames=usernames
)
####################### #######################
@@ -276,25 +271,37 @@ app.layout = dash.html.Div(
f"Leaderboard - Top {LEADERBOARD_COUNT}", f"Leaderboard - Top {LEADERBOARD_COUNT}",
style={"textAlign": "center"}, style={"textAlign": "center"},
), ),
dash.html.Div("Loading...", id="leaderboard"), dash.dcc.Loading(
id="leaderboard-loading",
children=dash.html.Div("Loading...", id="leaderboard"),
),
] ]
), ),
dash.html.Div( dash.html.Div(
[ [
dash.html.H2("User Scores", style={"textAlign": "center"}), dash.html.H2("User Scores", style={"textAlign": "center"}),
dash.html.Div("Loading...", id="user-scores"), dash.dcc.Loading(
id="user-scores-loading",
children=dash.html.Div("Loading...", id="user-scores"),
),
] ]
), ),
dash.html.Div( dash.html.Div(
[ [
dash.html.H2("Score Graph", style={"textAlign": "center"}), dash.html.H2("Score Graph", style={"textAlign": "center"}),
dash.html.Div("Loading...", id="stats-graph"), dash.dcc.Loading(
id="stats-graph-loading",
children=dash.html.Div("Loading...", id="stats-graph"),
),
] ]
), ),
dash.html.Div( dash.html.Div(
[ [
dash.html.H2("Daily Stats", style={"textAlign": "center"}), dash.html.H2("Daily Stats", style={"textAlign": "center"}),
dash.html.Div("Loading...", id="daily-stats"), dash.dcc.Loading(
id="daily-stats-loading",
children=dash.html.Div("Loading...", id="daily-stats"),
),
] ]
), ),
] ]