From d8240aeae0cd4a8c2bfaf438bea65a6b412ea218 Mon Sep 17 00:00:00 2001 From: Brad Brown Date: Mon, 13 Jun 2022 17:19:47 -0500 Subject: [PATCH] speed up load and display logic --- wordlinator/db/pg.py | 11 ++- wordlinator/utils/web.py | 165 ++++++++++++++++++++++++++++++------ wordlinator/web/__init__.py | 54 +++--------- 3 files changed, 161 insertions(+), 69 deletions(-) diff --git a/wordlinator/db/pg.py b/wordlinator/db/pg.py index d2c4a27..8fc8555 100644 --- a/wordlinator/db/pg.py +++ b/wordlinator/db/pg.py @@ -128,8 +128,17 @@ class WordleDb: def get_scores(self, round_no): round = self.get_or_create_round(round_no) res = ( - Score.select(Score, Player.game_id) + Score.select( + Score, + Hole.hole, + 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) ) diff --git a/wordlinator/utils/web.py b/wordlinator/utils/web.py index 6cab711..85a8817 100644 --- a/wordlinator/utils/web.py +++ b/wordlinator/utils/web.py @@ -1,23 +1,142 @@ import collections import typing +import wordlinator.db.pg -def golf_score(score_list: typing.List) -> int: - scores = [s.score for s in score_list] - score_count = len(scores) - score = sum(scores) - (score_count * 4) - return score +############ +# Mappings # +############ + +SCORE_NAME_MAP = { + 1: "Hole-in-1", + 2: "Eagle", + 3: "Birdie", + 4: "Par", + 5: "Bogey", + 6: "Double Bogey", + 7: "Fail", +} -def get_user_scorelist( - username: str, scores: typing.List -) -> typing.Dict[str, typing.Any]: - scores = list(sorted(scores, key=lambda s: s.hole_id.hole)) - return { - "Name": username, - "Score": golf_score(scores), - **{f"Hole {s.hole_id.hole}": s.score for s in scores}, - } +############### +# ScoreMatrix # +############### + +T = typing.TypeVar("T", bound="ScoreContainer") + + +class ScoreContainer: + def __init__(self, scores: typing.List[wordlinator.db.pg.Score]): + self._scores = scores + + @staticmethod + def _get_attribute( + score: wordlinator.db.pg.Score, attribute_path: typing.List[str] + ): + attribute = score + for path_part in attribute_path: + attribute = getattr(attribute, path_part) + return attribute + + def dict_by( + self, attribute_path: str, container_class: typing.Type[T] + ) -> typing.Dict[typing.Any, T]: + data_dict = collections.defaultdict(list) + path_parts = attribute_path.split(".") + + for score in self._scores: + data_dict[self._get_attribute(score, path_parts)].append(score) + + return {k: container_class(v) for k, v in data_dict.items()} + + +class ScoreRow(ScoreContainer): + @property + def total(self) -> int: + return sum(s.score for s in self._scores) + + @property + def count(self) -> int: + return len(self._scores) + + @property + def average(self) -> float: + return round(self.total / len(self._scores), 2) + + +class UserRow(ScoreRow): + def __init__(self, scores, username=None): + super().__init__(scores) + self.username = username or scores[0].user_id.username + + @property + def golf_score(self) -> int: + return self.total - (self.count * 4) + + def sorted_scores(self): + yield from sorted(self._scores, key=lambda s: s.hole_id.hole) + + def raw_values(self): + yield from (s.score for s in self.sorted_scores()) + + def _present_format(self, score): + if score.tweet_id: + return ( + f"[{score.score}]" + f"(https://twitter.com/{self.username}/status/{score.tweet_id})" + ) + return score.score + + def presentation_values(self, hole_no=None): + res = {s.hole_id.hole: self._present_format(s) for s in self.sorted_scores()} + if hole_no: + for i in range(1, hole_no + 1): + if i not in res: + res[i] = "" + return res + + def user_row(self, hole_no=None): + return { + "Name": self.username, + "Score": self.golf_score, + **self.presentation_values(hole_no=hole_no), + } + + +class ScoreMatrix(ScoreContainer): + def by_user(self): + return self.dict_by("user_id.username", UserRow) + + def for_user(self, username): + user_scores = [s for s in self._scores if s.user_id.username == username] + return UserRow(scores=user_scores, username=username) + + def by_hole(self): + return self.dict_by("hole_id.hole", ScoreRow) + + def for_hole(self, hole_no): + hole_scores = [s for s in self._scores if s.hole_id.hole == hole_no] + return ScoreRow(hole_scores) + + def _level_counts(self, level_scores: "ScoreMatrix"): + hole_dict = level_scores.by_hole() + return {k: v.count for k, v in sorted(hole_dict.items())} + + def score_breakdown(self): + by_score_dict = self.dict_by("score", ScoreMatrix) + return { + SCORE_NAME_MAP[k]: self._level_counts(v) + for k, v in sorted(by_score_dict.items()) + } + + def user_rows(self, wordle_day): + hole_no = wordle_day.golf_hole.hole_no + return [u.user_row(hole_no=hole_no) for u in self.by_user().values()] + + +###################### +# Formatting Helpers # +###################### def format_string(col, condition): @@ -60,17 +179,13 @@ def column_formats(col, pct): }, "backgroundColor": "white", }, - ] - - -def table_rows(score_list): - scores_by_user = collections.defaultdict(list) - for score in score_list: - scores_by_user[score.user_id.username].append(score) - - return [ - get_user_scorelist(username, scores) - for username, scores in scores_by_user.items() + { + "if": { + "column_id": col["id"], + "filter_query": format_string(col, "= ''"), + }, + "backgroundColor": "white", + }, ] diff --git a/wordlinator/web/__init__.py b/wordlinator/web/__init__.py index 9d50d31..c2ba252 100644 --- a/wordlinator/web/__init__.py +++ b/wordlinator/web/__init__.py @@ -1,4 +1,3 @@ -import collections import datetime import functools import time @@ -56,7 +55,7 @@ def _scores_from_db(ttl_hash=None): def scores_from_db(): - return _scores_from_db(get_ttl_hash()) + return wordlinator.utils.web.ScoreMatrix(_scores_from_db(get_ttl_hash())) ################# @@ -65,11 +64,11 @@ def scores_from_db(): def get_scores(): - score_list = scores_from_db() - table_rows = wordlinator.utils.web.table_rows(score_list) + score_matrix = scores_from_db() + table_rows = score_matrix.user_rows(wordle_today()) hole_columns = [ - {"name": f"Hole {i}", "id": f"Hole {i}", "type": "numeric"} + {"name": f"{i}", "id": f"{i}", "type": "text", "presentation": "markdown"} for i in range(1, wordle_today().golf_hole.hole_no + 1) ] columns = [ @@ -119,58 +118,27 @@ def get_scores(): # Stats Helpers # ################# -SCORE_NAME_MAP = { - 1: "Hole-in-1", - 2: "Eagle", - 3: "Birdie", - 4: "Par", - 5: "Bogey", - 6: "Double Bogey", - 7: "Fail", -} +def _get_summary_rows(score_matrix): + day_dict = score_matrix.by_hole() -def _get_score_breakdown(score, holes): - score_row = {"Score": SCORE_NAME_MAP[score]} - days = sorted(set(holes)) - for day in days: - score_row[day] = holes.count(day) - return score_row - - -def _get_summary_rows(score_list): - days = list(sorted(set((score.hole_id.hole for score in score_list)))) - day_dict = { - day: [score.score for score in score_list if score.hole_id.hole == day] - for day in days - } totals = { "Score": "Total", - **{day: len(scores) for day, scores in day_dict.items()}, + **{day: scores.count for day, scores in day_dict.items()}, } averages = { "Score": "Daily Average", - **{ - day: round(sum(scores) / len(scores), 2) for day, scores in day_dict.items() - }, + **{day: scores.average for day, scores in day_dict.items()}, } return [totals, averages] def _stats_dict(): - score_list = scores_from_db() - - scores_by_value = collections.defaultdict(list) - for score in score_list: - scores_by_value[score.score].append(score.hole_id.hole) - - table_rows = [] - for score in sorted(scores_by_value.keys()): - table_rows.append(_get_score_breakdown(score, scores_by_value[score])) - - table_rows.extend(_get_summary_rows(score_list)) + score_matrix = scores_from_db() + table_rows = [{"Score": k, **v} for k, v in score_matrix.score_breakdown().items()] + table_rows.extend(_get_summary_rows(score_matrix)) return table_rows