From 69b8c0995f0a5adfef703c55cca268646efee87c Mon Sep 17 00:00:00 2001 From: Lucas Feliciano Date: Sun, 1 Mar 2026 17:27:57 +0100 Subject: [PATCH 1/5] Add type annotations to foundation layer (entities, constants, utils) Annotate all function signatures, __init__ params, and return types. Modernize typing: replace Optional[X] with X | None, List with list, etc. Remove redundant self.x: type = x where param type suffices. --- pauperformance_bot/constant/flags.py | 2 +- .../constant/pauperformance/academy.py | 2 +- .../constant/pauperformance/myr.py | 2 +- pauperformance_bot/credentials.py | 4 +- pauperformance_bot/entity/academy_video.py | 22 ++--- pauperformance_bot/entity/api/archetype.py | 26 +++--- pauperformance_bot/entity/api/deck.py | 46 +++++----- pauperformance_bot/entity/api/miscellanea.py | 44 +++++----- pauperformance_bot/entity/api/phd.py | 84 +++++++++---------- pauperformance_bot/entity/api/tournament.py | 12 +-- pauperformance_bot/entity/api/video.py | 24 +++--- .../entity/arena/indexable_video.py | 6 +- .../entity/arena/twitch_user.py | 4 +- .../entity/arena/twitch_video.py | 28 +++---- .../entity/arena/youtube_video.py | 30 +++---- pauperformance_bot/entity/config/archetype.py | 84 +++++++++---------- pauperformance_bot/entity/config/phd.py | 34 ++++---- .../entity/deck/archive/abstract.py | 30 +++---- .../entity/deck/archive/local.py | 12 +-- .../entity/deck/archive/mtggoldfish.py | 22 ++--- pauperformance_bot/entity/deck/deckstats.py | 26 +++--- pauperformance_bot/entity/deck/playable.py | 59 +++++++------ pauperformance_bot/entity/mtg/mtggoldfish.py | 10 +-- .../entity/mtg/mtgo_standings.py | 23 +++-- pauperformance_bot/util/cache.py | 2 +- pauperformance_bot/util/config.py | 20 +++-- pauperformance_bot/util/decklist_parser.py | 4 +- pauperformance_bot/util/decorators.py | 17 ++-- pauperformance_bot/util/entities.py | 17 ++-- pauperformance_bot/util/log.py | 2 +- pauperformance_bot/util/math.py | 2 +- pauperformance_bot/util/naming.py | 4 +- pauperformance_bot/util/path.py | 5 +- pauperformance_bot/util/request.py | 19 ++++- pauperformance_bot/util/template.py | 13 +-- pauperformance_bot/util/time.py | 8 +- pauperformance_bot/util/web_page.py | 6 +- 37 files changed, 393 insertions(+), 362 deletions(-) diff --git a/pauperformance_bot/constant/flags.py b/pauperformance_bot/constant/flags.py index a5666c08..74e285fa 100644 --- a/pauperformance_bot/constant/flags.py +++ b/pauperformance_bot/constant/flags.py @@ -1,7 +1,7 @@ from pauperformance_bot.exceptions import UnsupportedLanguage -def get_language_flag(language_name): +def get_language_flag(language_name: str) -> str: language_name = language_name.lower() if language_name == "ita" or language_name == "it": return "🇮🇹" diff --git a/pauperformance_bot/constant/pauperformance/academy.py b/pauperformance_bot/constant/pauperformance/academy.py index cbc994da..421d6979 100644 --- a/pauperformance_bot/constant/pauperformance/academy.py +++ b/pauperformance_bot/constant/pauperformance/academy.py @@ -37,7 +37,7 @@ class AcademyFileSystem: - def __init__(self, root_dir=ACADEMY_PATH): + def __init__(self, root_dir: str = ACADEMY_PATH) -> None: self.ROOT_DIR: str = root_dir self.ASSETS_DIR: str = posix_path(self.ROOT_DIR, "assets") self.ASSETS_DATA_DIR: str = posix_path(self.ASSETS_DIR, "data") diff --git a/pauperformance_bot/constant/pauperformance/myr.py b/pauperformance_bot/constant/pauperformance/myr.py index 59d18b24..fbe74c46 100644 --- a/pauperformance_bot/constant/pauperformance/myr.py +++ b/pauperformance_bot/constant/pauperformance/myr.py @@ -89,7 +89,7 @@ def __init__( self, root_dir: str = TOP_PATH.as_posix(), resources_dir: str = RESOURCES_DIR, - ): + ) -> None: self.ROOT_DIR: str = root_dir self.SOURCE_DIR: str = posix_path(self.ROOT_DIR, "pauperformance_bot") diff --git a/pauperformance_bot/credentials.py b/pauperformance_bot/credentials.py index fb0e424d..e5f8ff63 100644 --- a/pauperformance_bot/credentials.py +++ b/pauperformance_bot/credentials.py @@ -4,7 +4,7 @@ from pauperformance_bot.constant.pauperformance.myr import SECRETS_UNTRACKED_FILE -def _get_credential_from_secrets(credential_key): +def _get_credential_from_secrets(credential_key: str) -> str | None: try: # will succeed locally if secret.py file is available secret_module = import_module(SECRETS_UNTRACKED_FILE.rstrip(".py")) return getattr(secret_module, credential_key) @@ -12,7 +12,7 @@ def _get_credential_from_secrets(credential_key): return None -def get_credential(credential_key): +def get_credential(credential_key: str) -> str | None: return os.environ.get(credential_key, _get_credential_from_secrets(credential_key)) diff --git a/pauperformance_bot/entity/academy_video.py b/pauperformance_bot/entity/academy_video.py index 41fa1f7f..8a94b3b5 100644 --- a/pauperformance_bot/entity/academy_video.py +++ b/pauperformance_bot/entity/academy_video.py @@ -7,17 +7,17 @@ class AcademyVideo: def __init__( self, - video_id, - user_name, - title, - language, - published_at, - deck_name, - archetype, - phd, - url, - fa_icon, - ): + video_id: str, + user_name: str, + title: str, + language: str, + published_at: str, + deck_name: str, + archetype: str, + phd: str, + url: str, + fa_icon: str, + ) -> None: self.video_id = video_id self.user_name = user_name self.title = title diff --git a/pauperformance_bot/entity/api/archetype.py b/pauperformance_bot/entity/api/archetype.py index db0d1c3f..a10a412d 100644 --- a/pauperformance_bot/entity/api/archetype.py +++ b/pauperformance_bot/entity/api/archetype.py @@ -1,5 +1,3 @@ -from typing import Optional - from pauperformance_bot.entity.config.archetype import ( ArchetypeConfig, DiscordResource, @@ -18,12 +16,12 @@ def __init__( name: str, link: str, preview: str, - ): - self.name: str = name - self.link: str = link - self.preview: str = preview + ) -> None: + self.name = name + self.link = link + self.preview = preview - def __hash__(self): + def __hash__(self) -> int: return hash(self.name) @@ -34,20 +32,20 @@ def __init__( self, *, name: str, - aliases: Optional[list[str]], - family: Optional[str], + aliases: list[str] | None, + family: str | None, dominant_mana: list[str], game_type: list[str], description: str, must_have_cards: list[str], must_not_have_cards: list[str], reference_decks: list[str], - resource_sideboard: Optional[SideboardResource], + resource_sideboard: SideboardResource | None, resources_discord: list[DiscordResource], resources: list[Resource], staples: list[ArchetypeCard], frequent: list[ArchetypeCard], - ): + ) -> None: super().__init__( name=name, aliases=aliases, @@ -62,8 +60,8 @@ def __init__( resources_discord=resources_discord, resources=resources, ) - self.staples: list[ArchetypeCard] = staples - self.frequent: list[ArchetypeCard] = frequent + self.staples = staples + self.frequent = frequent - def __hash__(self): + def __hash__(self) -> int: return hash(self.name) diff --git a/pauperformance_bot/entity/api/deck.py b/pauperformance_bot/entity/api/deck.py index 1902d1b3..68008f45 100644 --- a/pauperformance_bot/entity/api/deck.py +++ b/pauperformance_bot/entity/api/deck.py @@ -1,5 +1,3 @@ -from typing import Optional - from pauperformance_bot.util.decorators import auto_repr, auto_str @@ -15,15 +13,15 @@ def __init__( set_name: str, set_date: str, legal: bool, - ): - self.name: str = name - self.url: str = url - self.archetype: str = archetype - self.set_name: str = set_name - self.set_date: str = set_date - self.legal: bool = legal - - def __hash__(self): + ) -> None: + self.name = name + self.url = url + self.archetype = archetype + self.set_name = set_name + self.set_date = set_date + self.legal = legal + + def __hash__(self) -> int: return hash(self.name) @@ -37,25 +35,25 @@ def __init__( archetype: str, place: str, pilot: str, - tabletop_price: Optional[int], - mtgo_price: Optional[int], + tabletop_price: int | None, + mtgo_price: int | None, tournament_id: str, tournament_name: str, tournament_date: str, - ): - self.url: str = url - self.archetype: str = archetype - self.place: str = place - self.pilot: str = pilot - self.tabletop_price: Optional[int] = tabletop_price - self.mtgo_price: Optional[int] = mtgo_price - self.tournament_id: str = tournament_id - self.tournament_name: str = tournament_name - self.tournament_date: str = tournament_date + ) -> None: + self.url = url + self.archetype = archetype + self.place = place + self.pilot = pilot + self.tabletop_price = tabletop_price + self.mtgo_price = mtgo_price + self.tournament_id = tournament_id + self.tournament_name = tournament_name + self.tournament_date = tournament_date @property def identifier(self) -> str: return self.url.rsplit("/", maxsplit=1)[-1] - def __hash__(self): + def __hash__(self) -> int: return hash(self.identifier) diff --git a/pauperformance_bot/entity/api/miscellanea.py b/pauperformance_bot/entity/api/miscellanea.py index 46edc3e8..571d7bc9 100644 --- a/pauperformance_bot/entity/api/miscellanea.py +++ b/pauperformance_bot/entity/api/miscellanea.py @@ -10,10 +10,10 @@ def __init__( self, *, news: list[Resource], - ): - self.news: list[Resource] = news + ) -> None: + self.news = news - def __hash__(self): + def __hash__(self) -> int: return hash(self.news) @@ -24,10 +24,10 @@ def __init__( self, *, changes: list[ChangelogEntry], - ): - self.changes: list[ChangelogEntry] = changes + ) -> None: + self.changes = changes - def __hash__(self): + def __hash__(self) -> int: return hash(self.changes) @@ -41,23 +41,23 @@ def __init__( meta_share: float, archetype_name: str, accuracy: float, - ): - self.mtggolfish_urls: list[str] = mtggolfish_urls - self.meta_share: float = meta_share - self.archetype_name: str = archetype_name - self.accuracy: float = accuracy + ) -> None: + self.mtggolfish_urls = mtggolfish_urls + self.meta_share = meta_share + self.archetype_name = archetype_name + self.accuracy = accuracy - def __hash__(self): + def __hash__(self) -> int: return hash(self.mtggolfish_urls) - def __lt__(self, other): + def __lt__(self, other: object) -> bool: if isinstance(other, MetaShare): return self.meta_share < other.meta_share raise ValueError( f"Cannot compare instance of MetaShare with instance of {type(other)}" ) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, MetaShare): return self.archetype_name == other.archetype_name return False @@ -70,35 +70,35 @@ def __init__( self, *, meta_shares: list[MetaShare], - ): - self.meta_shares: list[MetaShare] = meta_shares + ) -> None: + self.meta_shares = meta_shares - def __hash__(self): + def __hash__(self) -> int: return hash(self.meta_shares) @auto_repr @auto_str class DPLDeck: - def __init__(self, *, identifier: str, archetype: str, accuracy: float): + def __init__(self, *, identifier: str, archetype: str, accuracy: float) -> None: self.identifier = identifier self.archetype = archetype self.accuracy = accuracy - def __hash__(self): + def __hash__(self) -> int: return hash(self.identifier) @auto_repr @auto_str class DPLMeta: - def __init__(self, *, name: str, dpl_decks: list[DPLDeck]): + def __init__(self, *, name: str, dpl_decks: list[DPLDeck]) -> None: self.name = name - self.dpl_decks: list[DPLDeck] = dpl_decks + self.dpl_decks = dpl_decks @property def identifier(self) -> str: return self.name - def __hash__(self): + def __hash__(self) -> int: return hash(self.identifier) diff --git a/pauperformance_bot/entity/api/phd.py b/pauperformance_bot/entity/api/phd.py index c2b2adb7..5de5410e 100644 --- a/pauperformance_bot/entity/api/phd.py +++ b/pauperformance_bot/entity/api/phd.py @@ -1,5 +1,3 @@ -from typing import Optional - from pauperformance_bot.util.decorators import auto_repr, auto_str @@ -11,49 +9,45 @@ def __init__( *, name: str, bio: str, - mtgo_name: Optional[str], - twitch_channel_url: Optional[str], - youtube_channel_url: Optional[str], - favorite_format: Optional[str], - favorite_pauper_archetype: Optional[str], - favorite_pauper_card_name: Optional[str], - favorite_pauper_card_url: Optional[str], - favorite_pauper_card_image_url: Optional[str], - favorite_flavor_text_name: Optional[str], - favorite_flavor_text_url: Optional[str], - favorite_flavor_text_image_url: Optional[str], - favorite_flavor_text_lines: Optional[str], - favorite_artwork_name: Optional[str], - favorite_artwork_url: Optional[str], - favorite_artwork_image_url: Optional[str], - favorite_artist_name: Optional[str], - favorite_artist_gallery_url: Optional[str], - favorite_magic_quote_lines: Optional[str], - ): - self.name: str = name - self.bio: str = bio - self.mtgo_name: Optional[str] = mtgo_name - self.twitch_channel_url: Optional[str] = twitch_channel_url - self.youtube_channel_url: Optional[str] = youtube_channel_url - self.favorite_format: Optional[str] = favorite_format - self.favorite_pauper_archetype: Optional[str] = favorite_pauper_archetype - self.favorite_pauper_card_name: Optional[str] = favorite_pauper_card_name - self.favorite_pauper_card_url: Optional[str] = favorite_pauper_card_url - self.favorite_pauper_card_image_url: Optional[str] = ( - favorite_pauper_card_image_url - ) - self.favorite_flavor_text_name: Optional[str] = favorite_flavor_text_name - self.favorite_flavor_text_url: Optional[str] = favorite_flavor_text_url - self.favorite_flavor_text_image_url: Optional[str] = ( - favorite_flavor_text_image_url - ) - self.favorite_flavor_text_lines: Optional[str] = favorite_flavor_text_lines - self.favorite_artwork_name: Optional[str] = favorite_artwork_name - self.favorite_artwork_url: Optional[str] = favorite_artwork_url - self.favorite_artwork_image_url: Optional[str] = favorite_artwork_image_url - self.favorite_artist_name: Optional[str] = favorite_artist_name - self.favorite_artist_gallery_url: Optional[str] = favorite_artist_gallery_url - self.favorite_magic_quote_lines: Optional[str] = favorite_magic_quote_lines + mtgo_name: str | None, + twitch_channel_url: str | None, + youtube_channel_url: str | None, + favorite_format: str | None, + favorite_pauper_archetype: str | None, + favorite_pauper_card_name: str | None, + favorite_pauper_card_url: str | None, + favorite_pauper_card_image_url: str | None, + favorite_flavor_text_name: str | None, + favorite_flavor_text_url: str | None, + favorite_flavor_text_image_url: str | None, + favorite_flavor_text_lines: str | None, + favorite_artwork_name: str | None, + favorite_artwork_url: str | None, + favorite_artwork_image_url: str | None, + favorite_artist_name: str | None, + favorite_artist_gallery_url: str | None, + favorite_magic_quote_lines: str | None, + ) -> None: + self.name = name + self.bio = bio + self.mtgo_name = mtgo_name + self.twitch_channel_url = twitch_channel_url + self.youtube_channel_url = youtube_channel_url + self.favorite_format = favorite_format + self.favorite_pauper_archetype = favorite_pauper_archetype + self.favorite_pauper_card_name = favorite_pauper_card_name + self.favorite_pauper_card_url = favorite_pauper_card_url + self.favorite_pauper_card_image_url = favorite_pauper_card_image_url + self.favorite_flavor_text_name = favorite_flavor_text_name + self.favorite_flavor_text_url = favorite_flavor_text_url + self.favorite_flavor_text_image_url = favorite_flavor_text_image_url + self.favorite_flavor_text_lines = favorite_flavor_text_lines + self.favorite_artwork_name = favorite_artwork_name + self.favorite_artwork_url = favorite_artwork_url + self.favorite_artwork_image_url = favorite_artwork_image_url + self.favorite_artist_name = favorite_artist_name + self.favorite_artist_gallery_url = favorite_artist_gallery_url + self.favorite_magic_quote_lines = favorite_magic_quote_lines def __hash__(self) -> int: return hash(self.name) diff --git a/pauperformance_bot/entity/api/tournament.py b/pauperformance_bot/entity/api/tournament.py index e3343bd3..60c7a118 100644 --- a/pauperformance_bot/entity/api/tournament.py +++ b/pauperformance_bot/entity/api/tournament.py @@ -4,15 +4,15 @@ @auto_repr @auto_str class Tournament: - def __init__(self, *, url: str, name: str, date: str, deck_ids: list[str]): - self.url: str = url - self.name: str = name - self.date: str = date - self.deck_ids: list[str] = deck_ids + def __init__(self, *, url: str, name: str, date: str, deck_ids: list[str]) -> None: + self.url = url + self.name = name + self.date = date + self.deck_ids = deck_ids @property def identifier(self) -> str: return self.url.rsplit("/", maxsplit=1)[-1] - def __hash__(self): + def __hash__(self) -> int: return hash(self.identifier) diff --git a/pauperformance_bot/entity/api/video.py b/pauperformance_bot/entity/api/video.py index 919513e3..c9791d1f 100644 --- a/pauperformance_bot/entity/api/video.py +++ b/pauperformance_bot/entity/api/video.py @@ -1,5 +1,3 @@ -from typing import Optional - from pauperformance_bot.util.decorators import auto_repr, auto_str @@ -16,16 +14,16 @@ def __init__( date: str, archetype: str, video_id: str, - deck_name: Optional[str], - ): - self.name: str = name - self.link: str = link - self.language: str = language - self.phd_name: str = phd_name - self.date: str = date - self.archetype: str = archetype - self.video_id: str = video_id - self.deck_name: Optional[str] = deck_name + deck_name: str | None, + ) -> None: + self.name = name + self.link = link + self.language = language + self.phd_name = phd_name + self.date = date + self.archetype = archetype + self.video_id = video_id + self.deck_name = deck_name - def __hash__(self): + def __hash__(self) -> int: return hash(self.video_id) diff --git a/pauperformance_bot/entity/arena/indexable_video.py b/pauperformance_bot/entity/arena/indexable_video.py index dd3cb8d4..d4ab32fc 100644 --- a/pauperformance_bot/entity/arena/indexable_video.py +++ b/pauperformance_bot/entity/arena/indexable_video.py @@ -9,11 +9,11 @@ @auto_repr @auto_str class IndexableVideo: - def __init__(self, description): + def __init__(self, description: str) -> None: self.description = description @property - def deck_name(self): + def deck_name(self) -> str | None: deck_name = None for line in self.description.split("\n"): line = line.strip() @@ -27,7 +27,7 @@ def deck_name(self): return deck_name @property - def archetype(self): + def archetype(self) -> str | None: archetype = None for line in self.description.split("\n"): line = line.strip() diff --git a/pauperformance_bot/entity/arena/twitch_user.py b/pauperformance_bot/entity/arena/twitch_user.py index 723f10b3..b1cd5de9 100644 --- a/pauperformance_bot/entity/arena/twitch_user.py +++ b/pauperformance_bot/entity/arena/twitch_user.py @@ -4,7 +4,9 @@ @auto_repr @auto_str class TwitchUser: - def __init__(self, user_id, login_name, display_name, description): + def __init__( + self, user_id: str, login_name: str, display_name: str, description: str + ) -> None: self.user_id = user_id self.login_name = login_name self.display_name = display_name diff --git a/pauperformance_bot/entity/arena/twitch_video.py b/pauperformance_bot/entity/arena/twitch_video.py index 5a135522..629bb35c 100644 --- a/pauperformance_bot/entity/arena/twitch_video.py +++ b/pauperformance_bot/entity/arena/twitch_video.py @@ -7,20 +7,20 @@ class TwitchVideo(IndexableVideo): def __init__( self, - video_id, - stream_id, - user_id, - user_login_name, - user_display_name, - title, - description, - created_at, - published_at, - url, - viewable, - language, - duration, - ): + video_id: str, + stream_id: str, + user_id: str, + user_login_name: str, + user_display_name: str, + title: str, + description: str, + created_at: str, + published_at: str, + url: str, + viewable: str, + language: str, + duration: str, + ) -> None: super().__init__(description) self.video_id = video_id self.stream_id = stream_id diff --git a/pauperformance_bot/entity/arena/youtube_video.py b/pauperformance_bot/entity/arena/youtube_video.py index f638c39d..32687dbb 100644 --- a/pauperformance_bot/entity/arena/youtube_video.py +++ b/pauperformance_bot/entity/arena/youtube_video.py @@ -7,21 +7,21 @@ class YouTubeVideo(IndexableVideo): def __init__( self, - video_id, # YouTube internals - etag, - content_video_id, # as displayed in the URL - published_at, - channel_id, - channel_title, - description, - playlist_id, - position, - created_at, - title, - privacy_status, - url, - language, - ): + video_id: str, # YouTube internals + etag: str, + content_video_id: str, # as displayed in the URL + published_at: str, + channel_id: str, + channel_title: str, + description: str, + playlist_id: str, + position: int, + created_at: str, + title: str, + privacy_status: str, + url: str, + language: str, + ) -> None: super().__init__(description) self.video_id = video_id self.etag = etag diff --git a/pauperformance_bot/entity/config/archetype.py b/pauperformance_bot/entity/config/archetype.py index a0c4f4bd..e8e6eaed 100644 --- a/pauperformance_bot/entity/config/archetype.py +++ b/pauperformance_bot/entity/config/archetype.py @@ -1,5 +1,3 @@ -from typing import Optional - from pauperformance_bot.util.decorators import auto_repr, auto_str @@ -10,10 +8,10 @@ def __init__( self, *, link: str, - ): - self.link: str = link + ) -> None: + self.link = link - def __hash__(self): + def __hash__(self) -> int: return hash(self.link) @@ -26,12 +24,12 @@ def __init__( name: str, link: str, language: str, - ): - self.name: str = name - self.link: str = link - self.language: str = language + ) -> None: + self.name = name + self.link = link + self.language = language - def __hash__(self): + def __hash__(self) -> int: return hash(self.link) @@ -46,14 +44,14 @@ def __init__( language: str, author: str, date: str, - ): - self.name: str = name - self.link: str = link - self.language: str = language - self.author: str = author - self.date: str = date - - def __hash__(self): + ) -> None: + self.name = name + self.link = link + self.language = language + self.author = author + self.date = date + + def __hash__(self) -> int: return hash(self.link) @@ -66,14 +64,14 @@ def __init__( text: str, date: str, scope: str, - link: Optional[str], - ): - self.text: str = text - self.date: str = date - self.scope: str = scope - self.link: Optional[str] = link - - def __hash__(self): + link: str | None, + ) -> None: + self.text = text + self.date = date + self.scope = scope + self.link = link + + def __hash__(self) -> int: return hash(self.text) @@ -85,29 +83,29 @@ def __init__( *, name: str, aliases: list[str], - family: Optional[str], + family: str | None, dominant_mana: list[str], game_type: list[str], description: str, must_have_cards: list[str], must_not_have_cards: list[str], reference_decks: list[str], - resource_sideboard: Optional[SideboardResource], + resource_sideboard: SideboardResource | None, resources_discord: list[DiscordResource], resources: list[Resource], - ): - self.name: str = name - self.aliases: list[str] = aliases - self.family: Optional[str] = family - self.dominant_mana: list[str] = dominant_mana - self.game_type: list[str] = game_type - self.description: str = description - self.must_have_cards: list[str] = must_have_cards - self.must_not_have_cards: list[str] = must_not_have_cards - self.reference_decks: list[str] = reference_decks - self.resource_sideboard: Optional[SideboardResource] = resource_sideboard - self.resources_discord: list[DiscordResource] = resources_discord - self.resources: list[Resource] = resources - - def __hash__(self): + ) -> None: + self.name = name + self.aliases = aliases + self.family = family + self.dominant_mana = dominant_mana + self.game_type = game_type + self.description = description + self.must_have_cards = must_have_cards + self.must_not_have_cards = must_not_have_cards + self.reference_decks = reference_decks + self.resource_sideboard = resource_sideboard + self.resources_discord = resources_discord + self.resources = resources + + def __hash__(self) -> int: return hash(self.name) diff --git a/pauperformance_bot/entity/config/phd.py b/pauperformance_bot/entity/config/phd.py index b271935e..bfbe4bbe 100644 --- a/pauperformance_bot/entity/config/phd.py +++ b/pauperformance_bot/entity/config/phd.py @@ -1,5 +1,3 @@ -from typing import Optional - from pauperformance_bot.util.decorators import auto_repr, auto_str @@ -10,22 +8,22 @@ def __init__( self, *, name: str, - mtgo_name: Optional[str], - twitch_login_name: Optional[str], - youtube_channel_id: Optional[str], - default_youtube_language: Optional[str], - discord_id: Optional[int], - deckstats_name: Optional[str], - deckstats_id: Optional[str], - ): - self.name: str = name - self.mtgo_name: Optional[str] = mtgo_name - self.twitch_login_name: Optional[str] = twitch_login_name - self.youtube_channel_id: Optional[str] = youtube_channel_id - self.default_youtube_language: Optional[str] = default_youtube_language - self.discord_id: Optional[int] = discord_id - self.deckstats_name: Optional[str] = deckstats_name - self.deckstats_id: Optional[str] = deckstats_id + mtgo_name: str | None, + twitch_login_name: str | None, + youtube_channel_id: str | None, + default_youtube_language: str | None, + discord_id: int | None, + deckstats_name: str | None, + deckstats_id: str | None, + ) -> None: + self.name = name + self.mtgo_name = mtgo_name + self.twitch_login_name = twitch_login_name + self.youtube_channel_id = youtube_channel_id + self.default_youtube_language = default_youtube_language + self.discord_id = discord_id + self.deckstats_name = deckstats_name + self.deckstats_id = deckstats_id def __hash__(self) -> int: return hash(self.name) diff --git a/pauperformance_bot/entity/deck/archive/abstract.py b/pauperformance_bot/entity/deck/archive/abstract.py index 280d1476..302d3d57 100644 --- a/pauperformance_bot/entity/deck/archive/abstract.py +++ b/pauperformance_bot/entity/deck/archive/abstract.py @@ -9,10 +9,10 @@ class AbstractArchivedDeck(metaclass=ABCMeta): def __init__( self, - name, - creation_date, - deck_id, - ): + name: str, + creation_date: int, + deck_id: str, + ) -> None: # The format of the name in the Archive is: # Archetype name p12e_code.revision.player | Name of the set (set_code) self.name = name @@ -20,38 +20,38 @@ def __init__( self.deck_id = deck_id @property - def p12e_name(self): + def p12e_name(self) -> str: p12e_name, friendly_name = self.name.split(" | ") return p12e_name @property - def archetype(self): + def archetype(self) -> str: p12e_name, friendly_name = self.name.split(" | ") archetype_and_p12e_code = p12e_name.split(".")[0] return archetype_and_p12e_code.rsplit(" ", maxsplit=1)[0] @property - def owner_name(self): + def owner_name(self) -> str: p12e_name, friendly_name = self.name.split(" | ") return ".".join(p12e_name.rsplit(".")[2:]) @property - def p12e_code(self): + def p12e_code(self) -> str: p12e_name, friendly_name = self.name.split(" | ") archetype_and_p12e_code = p12e_name.split(".")[0] return archetype_and_p12e_code.rsplit(" ", maxsplit=1)[1] @property - def revision(self): + def revision(self) -> str: p12e_name, _ = self.name.split(" | ") return p12e_name.split(".")[1] @property @abstractmethod - def url(self): # TODO: rename to URI + def url(self) -> str: # TODO: rename to URI pass - def __str__(self): + def __str__(self) -> str: return ( f"name: {self.name}; " f"creation_date: {pretty_str(self.creation_date)}; " @@ -59,11 +59,13 @@ def __str__(self): f"url: {self.url}" ) - def __repr__(self): + def __repr__(self) -> str: return str(self) - def __hash__(self): + def __hash__(self) -> int: return hash(self.deck_id) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, AbstractArchivedDeck): + return NotImplemented return self.deck_id == other.deck_id diff --git a/pauperformance_bot/entity/deck/archive/local.py b/pauperformance_bot/entity/deck/archive/local.py index b4fc0f6f..bea3a29b 100644 --- a/pauperformance_bot/entity/deck/archive/local.py +++ b/pauperformance_bot/entity/deck/archive/local.py @@ -9,14 +9,14 @@ class LocalArchivedDeck(AbstractArchivedDeck): def __init__( self, - name, - creation_date, - deck_id, - decks_path, - ): + name: str, + creation_date: int, + deck_id: str, + decks_path: str, + ) -> None: super().__init__(name, creation_date, deck_id) self.decks_path = decks_path @property - def url(self): + def url(self) -> str: return f"{self.decks_path}{sep}{self.deck_id}" diff --git a/pauperformance_bot/entity/deck/archive/mtggoldfish.py b/pauperformance_bot/entity/deck/archive/mtggoldfish.py index c69bb6e0..926263ec 100644 --- a/pauperformance_bot/entity/deck/archive/mtggoldfish.py +++ b/pauperformance_bot/entity/deck/archive/mtggoldfish.py @@ -8,40 +8,40 @@ class MTGGoldfishArchivedDeck(AbstractArchivedDeck): def __init__( self, - name, - creation_date, - deck_id, - deck_api_endpoint=DECK_API_ENDPOINT, - deck_download_api_endpoint=DECK_DOWNLOAD_API_ENDPOINT, - ): + name: str, + creation_date: int, + deck_id: str, + deck_api_endpoint: str = DECK_API_ENDPOINT, + deck_download_api_endpoint: str = DECK_DOWNLOAD_API_ENDPOINT, + ) -> None: super().__init__(name, creation_date, deck_id) self.deck_api_endpoint = deck_api_endpoint self.deck_download_api_endpoint = deck_download_api_endpoint @property - def url(self): + def url(self) -> str: return f"{self.deck_api_endpoint}/{self.deck_id}" @property - def download_txt_url(self): + def download_txt_url(self) -> str: return f"{self.deck_download_api_endpoint}/{self.deck_id}" @property - def download_tabletop_url(self): + def download_tabletop_url(self) -> str: return ( f"{self.deck_download_api_endpoint}/{self.deck_id}" f"?output=mtggoldfish&type=tabletop" ) @property - def download_arena_url(self): + def download_arena_url(self) -> str: return ( f"{self.deck_download_api_endpoint}/{self.deck_id}" f"?output=mtggoldfish&type=arena" ) @property - def download_mtgo_url(self): + def download_mtgo_url(self) -> str: return ( f"{self.deck_download_api_endpoint}/{self.deck_id}" f"?output=mtggoldfish&type=online" diff --git a/pauperformance_bot/entity/deck/deckstats.py b/pauperformance_bot/entity/deck/deckstats.py index bcaa8668..3c954a98 100644 --- a/pauperformance_bot/entity/deck/deckstats.py +++ b/pauperformance_bot/entity/deck/deckstats.py @@ -10,15 +10,15 @@ class DeckstatsDeck: def __init__( self, - owner_id, - owner_name, - saved_id, - folder_id, - name, - added, - updated, - url, - ): + owner_id: str, + owner_name: str, + saved_id: str, + folder_id: str, + name: str, + added: int, + updated: int, + url: str, + ) -> None: self.owner_id = owner_id self.owner_name = owner_name self.saved_id = saved_id @@ -29,15 +29,15 @@ def __init__( self.url = url @property - def archetype(self): + def archetype(self) -> str: return self.name.rsplit(" ", maxsplit=1)[0] @property - def p12e_code(self): + def p12e_code(self) -> str: return self.name.rsplit(" ", maxsplit=1)[1].split(".")[0] @property - def description(self): + def description(self) -> str: url = self.url method = requests.get response = execute_http_request(method, url) @@ -54,7 +54,7 @@ def description(self): except StopIteration: # no description return "" - def __str__(self): + def __str__(self) -> str: return ( f"owner_id: {self.owner_id}; " f"owner_name: {self.name}; " diff --git a/pauperformance_bot/entity/deck/playable.py b/pauperformance_bot/entity/deck/playable.py index f9859961..9771f9a5 100644 --- a/pauperformance_bot/entity/deck/playable.py +++ b/pauperformance_bot/entity/deck/playable.py @@ -1,5 +1,4 @@ from itertools import chain -from typing import List, Tuple from pauperformance_bot.entity.config.archetype import ArchetypeConfig from pauperformance_bot.util.decorators import auto_repr @@ -7,26 +6,26 @@ @auto_repr class PlayedCard: - def __init__(self, quantity, card_name): + def __init__(self, quantity: int | str, card_name: str) -> None: if isinstance(quantity, str): quantity = int(quantity) self.quantity = quantity self.card_name = card_name - def __str__(self): + def __str__(self) -> str: return f"{self.quantity} {self.card_name}" def __hash__(self) -> int: return hash(repr(self)) - def __lt__(self, other): + def __lt__(self, other: object) -> bool: if isinstance(other, PlayedCard): return self.card_name.lower() < other.card_name.lower() raise ValueError( f"Cannot compare instance of PlayedCard with instance of {type(other)}" ) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, PlayedCard): return ( self.quantity == other.quantity @@ -41,7 +40,9 @@ class PlayableDeck: MAX_NON_LAND_QUANTITY = 4 @classmethod - def validate_boards(cls, mainboard, sideboard) -> Tuple[bool, List[str]]: + def validate_boards( + cls, mainboard: list["PlayedCard"], sideboard: list["PlayedCard"] + ) -> tuple[bool, list[str]]: errors = [] main_amt = sum(c.quantity for c in mainboard) @@ -64,45 +65,45 @@ def __init__( self, mainboard: list[PlayedCard], sideboard: list[PlayedCard], - raise_error_if_invalid=True, - ): + raise_error_if_invalid: bool = True, + ) -> None: valid, errors = PlayableDeck.validate_boards(mainboard, sideboard) if raise_error_if_invalid and not valid: raise ValueError("\n".join(errors)) - self.mainboard: list[PlayedCard] = mainboard - self.sideboard: list[PlayedCard] = sideboard + self.mainboard = mainboard + self.sideboard = sideboard @property - def mainboard_mtggoldfish(self): + def mainboard_mtggoldfish(self) -> str: return "\n".join((f"{c.quantity} {c.card_name}" for c in self.mainboard)) @property - def sideboard_mtggoldfish(self): + def sideboard_mtggoldfish(self) -> str: return "\n".join((f"{c.quantity} {c.card_name}" for c in self.sideboard)) @property - def mainboard_cards_map(self): + def mainboard_cards_map(self) -> dict[str, int]: return { played_card.card_name: played_card.quantity for played_card in self.mainboard } @property - def sideboard_cards_map(self): + def sideboard_cards_map(self) -> dict[str, int]: return { played_card.card_name: played_card.quantity for played_card in self.sideboard } @property - def len_mainboard(self): + def len_mainboard(self) -> int: return sum(c.quantity for c in self.mainboard) @property - def len_sideboard(self): + def len_sideboard(self) -> int: return sum(c.quantity for c in self.sideboard) - def is_legal(self, banned_cards_names): + def is_legal(self, banned_cards_names: list[str] | set[str]) -> bool: banned_cards_names = set(banned_cards_names) if len({c.card_name for c in self.mainboard} & banned_cards_names) != 0: return False @@ -110,30 +111,30 @@ def is_legal(self, banned_cards_names): return False return True - def __str__(self): + def __str__(self) -> str: return ( f"Main ({self.len_mainboard}):\n{self.mainboard_mtggoldfish}\n\n" f"Sideboard ({self.len_sideboard}):\n{self.sideboard_mtggoldfish}" ) - def __repr__(self): + def __repr__(self) -> str: return ( " ".join((repr(c) for c in self.mainboard)) + "|" + " ".join((repr(c) for c in self.sideboard)) ) - def __hash__(self): + def __hash__(self) -> int: return hash(repr(self)) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if not other or not isinstance(other, PlayableDeck): return False return sorted(self.mainboard) == sorted(other.mainboard) and sorted( self.sideboard ) == sorted(other.sideboard) - def __contains__(self, item): + def __contains__(self, item: str) -> bool: return any(c.card_name == item for c in chain(self.mainboard, self.sideboard)) def can_belong_to_archetype(self, archetype: ArchetypeConfig) -> bool: @@ -146,7 +147,9 @@ def can_belong_to_archetype(self, archetype: ArchetypeConfig) -> bool: return True -def parse_playable_deck_from_lines(lines, raise_error_if_invalid=True) -> PlayableDeck: +def parse_playable_deck_from_lines( + lines: list[str], raise_error_if_invalid: bool = True +) -> PlayableDeck: separator = lines.index("") maindeck = lines[:separator] maindeck.sort(key=lambda pc: pc.split(" ", maxsplit=1)[1]) @@ -159,7 +162,9 @@ def parse_playable_deck_from_lines(lines, raise_error_if_invalid=True) -> Playab ) -def _get_plus_minus_diff(deck1_cards_map, deck2_cards_map): +def _get_plus_minus_diff( + deck1_cards_map: dict[str, int], deck2_cards_map: dict[str, int] +) -> tuple[list[str], list[str]]: minus_list, plus_list = [], [] for card, qty in deck1_cards_map.items(): if card not in deck2_cards_map: @@ -178,7 +183,9 @@ def _get_plus_minus_diff(deck1_cards_map, deck2_cards_map): return minus_list, plus_list -def get_decks_diff(deck1, deck2): +def get_decks_diff( + deck1: PlayableDeck, deck2: PlayableDeck +) -> tuple[list[str], list[str], list[str], list[str]]: main_minus_list, main_plus_list = _get_plus_minus_diff( deck1.mainboard_cards_map, deck2.mainboard_cards_map, @@ -190,7 +197,7 @@ def get_decks_diff(deck1, deck2): return main_minus_list, main_plus_list, side_minus_list, side_plus_list -def print_decks_diff(deck1, deck2): +def print_decks_diff(deck1: PlayableDeck, deck2: PlayableDeck) -> None: ( main_minus_list, main_plus_list, diff --git a/pauperformance_bot/entity/mtg/mtggoldfish.py b/pauperformance_bot/entity/mtg/mtggoldfish.py index b54ce8f6..dfa5701a 100644 --- a/pauperformance_bot/entity/mtg/mtggoldfish.py +++ b/pauperformance_bot/entity/mtg/mtggoldfish.py @@ -4,14 +4,14 @@ @auto_repr @auto_str class MTGGoldfishTournamentSearchResult: - def __init__(self, *, url: str, name: str, date: str): - self.url: str = url - self.name: str = name - self.date: str = date + def __init__(self, *, url: str, name: str, date: str) -> None: + self.url = url + self.name = name + self.date = date @property def identifier(self) -> str: return self.url.rsplit("/", maxsplit=1)[-1] - def __hash__(self): + def __hash__(self) -> int: return hash(self.identifier) diff --git a/pauperformance_bot/entity/mtg/mtgo_standings.py b/pauperformance_bot/entity/mtg/mtgo_standings.py index 495d15b3..eb0cdbe9 100644 --- a/pauperformance_bot/entity/mtg/mtgo_standings.py +++ b/pauperformance_bot/entity/mtg/mtgo_standings.py @@ -5,19 +5,19 @@ class MTGOStandingMatch: def __init__( self, - player1, - player1_ranking, - player2, - player2_ranking, - match_score, - ): + player1: str, + player1_ranking: int, + player2: str, + player2_ranking: int, + match_score: str, + ) -> None: self.player1 = player1 self.player1_ranking = player1_ranking self.player2 = player2 self.player2_ranking = player2_ranking self.match_score = match_score - def __str__(self): + def __str__(self) -> str: return ( f"{self.player1} ({self.player1_ranking}) vs " f"{self.player2} ({self.player2_ranking}) ({self.match_score}))" @@ -29,12 +29,17 @@ def __hash__(self) -> int: @auto_repr class MTGOStandings: - def __init__(self, quarterfinals, semifinals, finals): + def __init__( + self, + quarterfinals: list[MTGOStandingMatch], + semifinals: list[MTGOStandingMatch], + finals: list[MTGOStandingMatch], + ) -> None: self.quarterfinals = quarterfinals self.semifinals = semifinals self.finals = finals - def __str__(self): + def __str__(self) -> str: return ( f"Quarterfinals: {self.quarterfinals}. " f"Semifinals: {self.semifinals}. " diff --git a/pauperformance_bot/util/cache.py b/pauperformance_bot/util/cache.py index 85c30dbe..2dc32f82 100644 --- a/pauperformance_bot/util/cache.py +++ b/pauperformance_bot/util/cache.py @@ -1,2 +1,2 @@ -def to_pkl_name(name): +def to_pkl_name(name: str) -> str: return name.replace("/", "_").replace("\\", "_") + ".pkl" diff --git a/pauperformance_bot/util/config.py b/pauperformance_bot/util/config.py index 718ac6d2..3d9718c4 100644 --- a/pauperformance_bot/util/config.py +++ b/pauperformance_bot/util/config.py @@ -11,7 +11,7 @@ @deprecated(reason="Migrated") -def read_config(config_file_path): +def read_config(config_file_path: str) -> configparser.ConfigParser: config = configparser.ConfigParser(allow_no_value=True) config.optionxform = lambda option: option # preserve case config.read(config_file_path) @@ -20,7 +20,9 @@ def read_config(config_file_path): @deprecated(reason="Migrated") -def read_archetype_config(config_file_path): +def read_archetype_config( + config_file_path: str, +) -> dict[str, dict[str, str] | list[dict[str, str]]]: config = read_config(config_file_path) # read values values = { @@ -63,7 +65,7 @@ def read_archetype_config(config_file_path): @deprecated(reason="Migrated") -def read_family_config(config_file_path): +def read_family_config(config_file_path: str) -> dict[str, str]: config = read_config(config_file_path) values = { **config["values"], @@ -72,7 +74,9 @@ def read_family_config(config_file_path): @deprecated(reason="Migrated") -def read_config_with_sequential_resources(config_file_path): +def read_config_with_sequential_resources( + config_file_path: str, +) -> dict[str, list[dict[str, str]]]: config = read_config(config_file_path) return { "resources": _read_sequential_resources(config, "resource"), @@ -80,7 +84,9 @@ def read_config_with_sequential_resources(config_file_path): @deprecated(reason="Migrated") -def _read_sequential_resources(config, key): +def _read_sequential_resources( + config: configparser.ConfigParser, key: str +) -> list[dict[str, str]]: resources = [] for i in count(1): if f"{key}{i}" in config: @@ -95,7 +101,7 @@ def _read_sequential_resources(config, key): @deprecated(reason="Migrated") -def _format_and_sort_resources(resources): +def _format_and_sort_resources(resources: list[dict[str, str]]) -> list[dict[str, str]]: for resource in resources: if "language" in resource: resource["language"] = get_language_flag(resource["language"]) @@ -107,5 +113,5 @@ def _format_and_sort_resources(resources): @deprecated(reason="Migrated") -def _parse_list_value(raw_value): +def _parse_list_value(raw_value: str) -> list[str]: return [value.strip(" ") for value in raw_value.split(",")] if raw_value else [] diff --git a/pauperformance_bot/util/decklist_parser.py b/pauperformance_bot/util/decklist_parser.py index 90b8e78a..202f56ba 100644 --- a/pauperformance_bot/util/decklist_parser.py +++ b/pauperformance_bot/util/decklist_parser.py @@ -10,12 +10,12 @@ class DeckListParser(metaclass=ABCMeta): SIDEBOARD_MAX = 15 @abstractmethod - def parse_lines(self, lines) -> PlayableDeck: + def parse_lines(self, lines: list[str]) -> PlayableDeck: pass class MtgoDeckListParser(DeckListParser): - def parse_lines(self, lines) -> PlayableDeck: + def parse_lines(self, lines: list[str]) -> PlayableDeck: try: separator = lines.index("") maindeck = lines[:separator] diff --git a/pauperformance_bot/util/decorators.py b/pauperformance_bot/util/decorators.py index aaabf2cf..f19ae421 100644 --- a/pauperformance_bot/util/decorators.py +++ b/pauperformance_bot/util/decorators.py @@ -1,18 +1,23 @@ -def auto_repr(cls): - def __repr__(self): +from typing import TypeVar + +_C = TypeVar("_C") + + +def auto_repr(cls: type[_C]) -> type[_C]: + def __repr__(self: _C) -> str: fq_class_name = ".".join([type(self).__module__, type(self).__qualname__]) class_attributes = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{fq_class_name}({class_attributes})" - cls.__repr__ = __repr__ + cls.__repr__ = __repr__ # type: ignore[attr-defined] return cls -def auto_str(cls): - def __str__(self): +def auto_str(cls: type[_C]) -> type[_C]: + def __str__(self: _C) -> str: class_name = type(self).__name__ class_attributes = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{class_name}({class_attributes})" - cls.__str__ = __str__ + cls.__str__ = __str__ # type: ignore[attr-defined] return cls diff --git a/pauperformance_bot/util/entities.py b/pauperformance_bot/util/entities.py index aaabf2cf..f19ae421 100644 --- a/pauperformance_bot/util/entities.py +++ b/pauperformance_bot/util/entities.py @@ -1,18 +1,23 @@ -def auto_repr(cls): - def __repr__(self): +from typing import TypeVar + +_C = TypeVar("_C") + + +def auto_repr(cls: type[_C]) -> type[_C]: + def __repr__(self: _C) -> str: fq_class_name = ".".join([type(self).__module__, type(self).__qualname__]) class_attributes = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{fq_class_name}({class_attributes})" - cls.__repr__ = __repr__ + cls.__repr__ = __repr__ # type: ignore[attr-defined] return cls -def auto_str(cls): - def __str__(self): +def auto_str(cls: type[_C]) -> type[_C]: + def __str__(self: _C) -> str: class_name = type(self).__name__ class_attributes = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{class_name}({class_attributes})" - cls.__str__ = __str__ + cls.__str__ = __str__ # type: ignore[attr-defined] return cls diff --git a/pauperformance_bot/util/log.py b/pauperformance_bot/util/log.py index 04330d20..742e5fdb 100644 --- a/pauperformance_bot/util/log.py +++ b/pauperformance_bot/util/log.py @@ -24,7 +24,7 @@ logger.addHandler(file_handler) -def get_application_logger(cli_group=None): +def get_application_logger(cli_group: str | None = None) -> logging.Logger: return ( logging.getLogger("{}.{}".format(APPLICATION_NAME, cli_group)) if cli_group diff --git a/pauperformance_bot/util/math.py b/pauperformance_bot/util/math.py index eecc4ea4..bbe80bcd 100644 --- a/pauperformance_bot/util/math.py +++ b/pauperformance_bot/util/math.py @@ -1,4 +1,4 @@ -def truncate(f, n): +def truncate(f: float, n: int) -> float: # truncates/pads a float f to n decimal places without rounding s = "%.12f" % f i, p, d = s.partition(".") diff --git a/pauperformance_bot/util/naming.py b/pauperformance_bot/util/naming.py index 09b429ee..09467af5 100644 --- a/pauperformance_bot/util/naming.py +++ b/pauperformance_bot/util/naming.py @@ -1,4 +1,4 @@ -def is_valid_p12e_deckstats_name(deck_name): +def is_valid_p12e_deckstats_name(deck_name: str) -> bool: # A valid name for a deckstats deck has the following format: # Archetype Name magic_set_id.revision_id tokens = deck_name.rsplit(" ", maxsplit=1) @@ -14,7 +14,7 @@ def is_valid_p12e_deckstats_name(deck_name): return len(deck_number) == 3 -def is_valid_p12e_deck_name(deck_name): +def is_valid_p12e_deck_name(deck_name: str) -> bool: # A valid name for a deck has the following format: # Archetype Name magic_set_id.revision_id.player_id tokens = deck_name.rsplit(" ", maxsplit=1) diff --git a/pauperformance_bot/util/path.py b/pauperformance_bot/util/path.py index fd99e714..c61b35bf 100644 --- a/pauperformance_bot/util/path.py +++ b/pauperformance_bot/util/path.py @@ -2,6 +2,7 @@ import re import unicodedata from pathlib import Path +from typing import Any import jsonpickle @@ -28,12 +29,12 @@ def posix_path(*args: str) -> str: return Path().joinpath(*args).as_posix() -def safe_dump_json_to_file(path, file_name, obj): +def safe_dump_json_to_file(path: str, file_name: str, obj: Any) -> None: os.makedirs(path, exist_ok=True) with open(posix_path(path, file_name), "w") as out_f: out_f.write(jsonpickle.encode(obj, make_refs=False, warn=True)) -def load_json_from_file(file_path): +def load_json_from_file(file_path: str) -> Any: with open(file_path, "r") as in_f: return jsonpickle.decode(in_f.read()) diff --git a/pauperformance_bot/util/request.py b/pauperformance_bot/util/request.py index 1b75db16..d4b313b7 100644 --- a/pauperformance_bot/util/request.py +++ b/pauperformance_bot/util/request.py @@ -1,14 +1,18 @@ from functools import wraps +from typing import Any, Callable from requests import ConnectionError +from requests.models import Response from retrying import retry -def _retry_on_connection_error(exc): +def _retry_on_connection_error(exc: BaseException) -> bool: return isinstance(exc, ConnectionError) -def _http_request_retry(http_request): +def _http_request_retry( + http_request: Callable[..., Response], +) -> Callable[..., Response]: # retries an HTTP request in case of ConnectionError with an exponential # backoff policy and additional random waits between 1 and 2 seconds. # It gives up after 5 attempts or after 60 seconds have passed. @@ -20,14 +24,21 @@ def _http_request_retry(http_request): wait_random_max=2 * 1000, wait_exponential_multiplier=1000, ) - def _execute_with_retry(request, *args, **kwargs): + def _execute_with_retry( + request: Callable[..., Response], *args: Any, **kwargs: Any + ) -> Response: return http_request(request, *args, **kwargs) return wraps(http_request)(_execute_with_retry) @_http_request_retry -def execute_http_request(request_fn, url, timeout=(3 * 20, 120), headers={}): +def execute_http_request( + request_fn: Callable[..., Response], + url: str, + timeout: tuple[int, int] = (3 * 20, 120), + headers: dict[str, str] = {}, +) -> Response: # Assumes the request_fn is from requests module. # Timeout is a tuple (connection_timeout, read_timeout). # Further details here: diff --git a/pauperformance_bot/util/template.py b/pauperformance_bot/util/template.py index 728a3871..67803dd7 100644 --- a/pauperformance_bot/util/template.py +++ b/pauperformance_bot/util/template.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from typing import Any from urllib.parse import quote from deprecated import deprecated @@ -15,12 +16,12 @@ @deprecated(reason="Migrated") -def tagify(name): +def tagify(name: str) -> str: return f"`{name}`" @deprecated(reason="Migrated") -def to_archetype_page_mana(mana): +def to_archetype_page_mana(mana: str) -> str: return ( f'' @@ -28,17 +29,19 @@ def to_archetype_page_mana(mana): @deprecated(reason="Migrated") -def to_github_anchor(name): +def to_github_anchor(name: str) -> str: return name.lower().replace(" ", "-").replace(".", "").replace(":", "") @deprecated(reason="Migrated") -def to_url_encoded(name): +def to_url_encoded(name: str) -> str: return quote(name) @deprecated(reason="Migrated") -def render_template(template_dir, template_file, output_file, values): +def render_template( + template_dir: str, template_file: str, output_file: str, values: dict[str, Any] +) -> None: os.makedirs(Path(output_file).parent.as_posix(), exist_ok=True) env = Environment( loader=FileSystemLoader(template_dir), diff --git a/pauperformance_bot/util/time.py b/pauperformance_bot/util/time.py index dd6bf1a7..bdbef1bf 100644 --- a/pauperformance_bot/util/time.py +++ b/pauperformance_bot/util/time.py @@ -7,7 +7,7 @@ ) -def now(): +def now() -> int: return int(round(time.time() * 1000)) @@ -17,15 +17,15 @@ def now_utc() -> int: return datetime_to_ms(datetime.utcnow()) -def datetime_to_ms(dt): +def datetime_to_ms(dt: datetime) -> int: return int(dt.timestamp() * 1000) -def pretty_str(now_ms: int, date_format=DEFAULT_DATE_FORMAT): +def pretty_str(now_ms: int, date_format: str = DEFAULT_DATE_FORMAT) -> str: now_dt = datetime.fromtimestamp(now_ms / 1000.0) return now_dt.strftime(date_format) -def simple_str(now_ms: int, date_format=USA_DATE_FORMAT): +def simple_str(now_ms: int, date_format: str = USA_DATE_FORMAT) -> str: now_dt = datetime.fromtimestamp(now_ms / 1000.0) return now_dt.strftime(date_format) diff --git a/pauperformance_bot/util/web_page.py b/pauperformance_bot/util/web_page.py index bea9176b..927d21c6 100644 --- a/pauperformance_bot/util/web_page.py +++ b/pauperformance_bot/util/web_page.py @@ -3,11 +3,11 @@ @deprecated(reason="Migrated") class WebPage: - def __init__(self, page_name): + def __init__(self, page_name: str) -> None: self.page_name = page_name - def as_markdown(self): + def as_markdown(self) -> str: return f"{self.page_name}.md" - def as_html(self): + def as_html(self) -> str: return f"{self.page_name}.html" From c17e57ec4cfa5ece39228ad72c6702ecff2ac29a Mon Sep 17 00:00:00 2001 From: Lucas Feliciano Date: Sun, 1 Mar 2026 17:39:00 +0100 Subject: [PATCH 2/5] Add type annotations to service layer Annotate all function signatures across service/mtg, service/academy, service/arena, service/nexus, and service/pauperformance. Modernize typing: replace Optional/List/Dict/Tuple/DefaultDict with modern equivalents (X | None, list, dict, tuple, defaultdict). --- pauperformance_bot/service/academy/academy.py | 88 ++++++++------- .../service/academy/data_exporter.py | 51 +++++---- .../service/academy/data_loader.py | 9 +- pauperformance_bot/service/arena/twitch.py | 16 +-- pauperformance_bot/service/arena/youtube.py | 22 ++-- pauperformance_bot/service/mtg/deckstats.py | 33 +++--- .../service/mtg/downloader/abstract.py | 5 +- .../service/mtg/downloader/downloader.py | 6 +- .../service/mtg/downloader/moxfield.py | 4 +- .../service/mtg/downloader/mtgdecks.py | 2 +- .../service/mtg/downloader/service.py | 8 +- pauperformance_bot/service/mtg/mtggoldfish.py | 22 ++-- pauperformance_bot/service/mtg/scryfall.py | 29 ++--- pauperformance_bot/service/mtg/wizards.py | 11 +- .../service/nexus/abstract_discord_service.py | 21 ++-- .../service/nexus/async_discord_service.py | 29 ++--- .../service/nexus/sync/members_fetcher.py | 22 ++-- .../service/nexus/sync/messages_sender.py | 20 ++-- .../service/nexus/sync_discord_service.py | 29 ++--- .../pauperformance/archive/abstract.py | 63 ++++++----- .../service/pauperformance/archive/local.py | 16 ++- .../pauperformance/archive/mtggoldfish.py | 57 ++++++---- .../pauperformance/async_pauperformance.py | 52 ++++++--- .../service/pauperformance/checker.py | 6 +- .../service/pauperformance/config_reader.py | 15 ++- .../service/pauperformance/pauperformance.py | 54 ++++----- .../service/pauperformance/silver.py | 21 ++-- .../pauperformance/silver/decklassifier.py | 32 +++--- .../pauperformance/silver/deckstatistics.py | 28 ++--- .../pauperformance/silver/deckstructor.py | 8 +- .../pauperformance/storage/abstract.py | 105 ++++++++++-------- .../pauperformance/storage/dropbox_.py | 43 +++---- .../service/pauperformance/storage/local.py | 31 +++--- 33 files changed, 528 insertions(+), 430 deletions(-) diff --git a/pauperformance_bot/service/academy/academy.py b/pauperformance_bot/service/academy/academy.py index ebd26ccd..6fd88382 100644 --- a/pauperformance_bot/service/academy/academy.py +++ b/pauperformance_bot/service/academy/academy.py @@ -1,3 +1,4 @@ +import collections import glob from collections import defaultdict from pathlib import Path @@ -35,6 +36,7 @@ TEMPLATES_PAGES_DIR, ) from pauperformance_bot.service.academy.data_loader import AcademyDataLoader +from pauperformance_bot.service.mtg.scryfall import ScryfallService from pauperformance_bot.service.pauperformance.pauperformance import ( PauperformanceService, ) @@ -59,12 +61,14 @@ class AcademyService: def __init__( self, pauperformance: PauperformanceService, - ): + ) -> None: self.pauperformance: PauperformanceService = pauperformance - self.scryfall = pauperformance.scryfall - self.set_index = pauperformance.set_index + self.scryfall: ScryfallService = pauperformance.scryfall + self.set_index: collections.OrderedDict[int, dict[str, str]] = ( + pauperformance.set_index + ) - def update_all(self, update_dev=True): + def update_all(self, update_dev: bool = True) -> None: self.update_home() self.update_archetypes_index() self.update_set_index() @@ -76,12 +80,12 @@ def update_all(self, update_dev=True): def update_home( self, - config_dir=CONFIG_DIR, - templates_pages_dir=TEMPLATES_PAGES_DIR, - newspauper_file=CONFIG_NEWSPAUPER_FILE, - home_template_file=HOME_TEMPLATE_FILE, - home_output_file=HOME_OUTPUT_FILE, - ): + config_dir: str = CONFIG_DIR, + templates_pages_dir: str = TEMPLATES_PAGES_DIR, + newspauper_file: str = CONFIG_NEWSPAUPER_FILE, + home_template_file: str = HOME_TEMPLATE_FILE, + home_output_file: str = HOME_OUTPUT_FILE, + ) -> None: logger.info( f"Rendering home in {templates_pages_dir} from " f"{home_template_file}..." ) @@ -106,13 +110,13 @@ def update_home( def update_archetypes_index( self, - config_pages_dir=CONFIG_ARCHETYPES_DIR, - templates_pages_dir=TEMPLATES_PAGES_DIR, - archetypes_dir=ARCHETYPES_DIR_RELATIVE_URL, - families_dir=FAMILIES_DIR_RELATIVE_URL, - archetypes_index_template_file=ARCHETYPES_INDEX_TEMPLATE_FILE, - archetypes_index_output_file=ARCHETYPES_INDEX_OUTPUT_FILE, - ): + config_pages_dir: str = CONFIG_ARCHETYPES_DIR, + templates_pages_dir: str = TEMPLATES_PAGES_DIR, + archetypes_dir: str = ARCHETYPES_DIR_RELATIVE_URL, + families_dir: str = FAMILIES_DIR_RELATIVE_URL, + archetypes_index_template_file: str = ARCHETYPES_INDEX_TEMPLATE_FILE, + archetypes_index_output_file: str = ARCHETYPES_INDEX_OUTPUT_FILE, + ) -> None: logger.info( f"Rendering archetype index in {templates_pages_dir} from " f"{archetypes_index_template_file}..." @@ -145,10 +149,10 @@ def update_archetypes_index( def update_set_index( self, - templates_pages_dir=TEMPLATES_PAGES_DIR, - set_index_template_file=SET_INDEX_TEMPLATE_FILE, - set_index_output_file=SET_INDEX_OUTPUT_FILE, - ): + templates_pages_dir: str = TEMPLATES_PAGES_DIR, + set_index_template_file: str = SET_INDEX_TEMPLATE_FILE, + set_index_output_file: str = SET_INDEX_OUTPUT_FILE, + ) -> None: logger.info( f"Rendering set index in {templates_pages_dir} from " f"{set_index_template_file}..." @@ -167,10 +171,10 @@ def update_set_index( def update_pauper_pool( self, - templates_pages_dir=TEMPLATES_PAGES_DIR, - pauper_pool_template_file=PAUPER_POOL_TEMPLATE_FILE, - pauper_pool_output_file=PAUPER_POOL_OUTPUT_FILE, - ): + templates_pages_dir: str = TEMPLATES_PAGES_DIR, + pauper_pool_template_file: str = PAUPER_POOL_TEMPLATE_FILE, + pauper_pool_output_file: str = PAUPER_POOL_OUTPUT_FILE, + ) -> None: logger.info( f"Rendering pauper pool in {templates_pages_dir} from " f"{pauper_pool_template_file}..." @@ -191,11 +195,11 @@ def update_pauper_pool( def update_archetypes( self, - config_pages_dir=CONFIG_ARCHETYPES_DIR, - templates_archetypes_dir=TEMPLATES_ARCHETYPES_DIR, - archetype_template_file=ARCHETYPE_TEMPLATE_FILE, - pauperformance_archetypes_dir=ARCHETYPES_DIR, - ): + config_pages_dir: str = CONFIG_ARCHETYPES_DIR, + templates_archetypes_dir: str = TEMPLATES_ARCHETYPES_DIR, + archetype_template_file: str = ARCHETYPE_TEMPLATE_FILE, + pauperformance_archetypes_dir: str = ARCHETYPES_DIR, + ) -> None: logger.info("Generating archetypes...") all_decks = self.pauperformance.list_archived_decks() banned_cards = [c["name"] for c in self.scryfall.get_banned_cards()] @@ -316,13 +320,13 @@ def update_archetypes( def update_families( self, - config_families_dir=CONFIG_FAMILIES_DIR, - config_archetypes_dir=CONFIG_ARCHETYPES_DIR, - templates_families_dir=TEMPLATES_FAMILIES_DIR, - archetypes_dir=ARCHETYPES_DIR_RELATIVE_URL, - family_template_file=FAMILY_TEMPLATE_FILE, - pauperformance_families_dir=FAMILIES_DIR, - ): + config_families_dir: str = CONFIG_FAMILIES_DIR, + config_archetypes_dir: str = CONFIG_ARCHETYPES_DIR, + templates_families_dir: str = TEMPLATES_FAMILIES_DIR, + archetypes_dir: str = ARCHETYPES_DIR_RELATIVE_URL, + family_template_file: str = FAMILY_TEMPLATE_FILE, + pauperformance_families_dir: str = FAMILIES_DIR, + ) -> None: logger.info("Generating families...") logger.debug("Building families-archetypes map...") families_map = defaultdict(list) @@ -370,10 +374,10 @@ def update_families( def update_dev( self, - templates_pages_dir=TEMPLATES_PAGES_DIR, - dev_template_file=DEV_TEMPLATE_FILE, - dev_output_file=DEV_OUTPUT_FILE, - ): + templates_pages_dir: str = TEMPLATES_PAGES_DIR, + dev_template_file: str = DEV_TEMPLATE_FILE, + dev_output_file: str = DEV_OUTPUT_FILE, + ) -> None: logger.info( f"Rendering dev in {templates_pages_dir} from " f"{dev_template_file}..." ) @@ -387,7 +391,7 @@ def update_dev( ) logger.info(f"Rendered dev to {dev_output_file}.") - def _boldify_sets_with_new_cards(self): + def _boldify_sets_with_new_cards(self) -> list[dict[str, str]]: card_index = self.pauperformance.incremental_card_index bolded_index = [] for item in self.set_index.values(): diff --git a/pauperformance_bot/service/academy/data_exporter.py b/pauperformance_bot/service/academy/data_exporter.py index 0b8e1571..75eeb4ac 100644 --- a/pauperformance_bot/service/academy/data_exporter.py +++ b/pauperformance_bot/service/academy/data_exporter.py @@ -20,6 +20,7 @@ parse_playable_deck_from_lines, ) from pauperformance_bot.service.academy.data_loader import AcademyDataLoader +from pauperformance_bot.service.mtg.scryfall import ScryfallService from pauperformance_bot.service.pauperformance.config_reader import ConfigReader from pauperformance_bot.service.pauperformance.pauperformance import ( PauperformanceService, @@ -43,10 +44,10 @@ def __init__( self, pauperformance: PauperformanceService, academy_fs: AcademyFileSystem = ACADEMY_FILE_SYSTEM, - ): + ) -> None: self.academy_fs: AcademyFileSystem = academy_fs self.pauperformance: PauperformanceService = pauperformance - self.scryfall = self.pauperformance.scryfall + self.scryfall: ScryfallService = self.pauperformance.scryfall self.config_reader: ConfigReader = self.pauperformance.config_reader self.decks: list[AbstractArchivedDeck] = ( self.pauperformance.list_archived_decks() @@ -54,7 +55,7 @@ def __init__( self.silver: Decklassifier = Decklassifier(self.pauperformance) self.academy_loader: AcademyDataLoader = AcademyDataLoader() - def export_all(self): + def export_all(self) -> None: self.export_archetypes() self.export_decks() self.export_intel_decks() @@ -63,7 +64,7 @@ def export_all(self): # self.export_videos() # self.export_miscellanea() - def export_phd_sheets(self): + def export_phd_sheets(self) -> None: logger.info(f"Exporting phd sheets to {self.academy_fs.ASSETS_DATA_PHD_DIR}...") for phd_sheet in self.config_reader.list_phd_sheets( scryfall_service=self.scryfall @@ -75,7 +76,7 @@ def export_phd_sheets(self): ) logger.info(f"Exported phd sheets to {self.academy_fs.ASSETS_DATA_PHD_DIR}.") - def export_archetypes(self): + def export_archetypes(self) -> None: logger.info( f"Exporting archetypes to {self.academy_fs.ASSETS_DATA_ARCHETYPE_DIR}..." ) @@ -125,7 +126,7 @@ def export_archetypes(self): f"Exported archetypes to {self.academy_fs.ASSETS_DATA_ARCHETYPE_DIR}." ) - def export_decks(self): + def export_decks(self) -> None: logger.info(f"Exporting decks to {self.academy_fs.ASSETS_DATA_DECK_DIR}...") banned_cards = [c["name"] for c in self.scryfall.get_banned_cards()] for deck in self.decks: @@ -151,7 +152,7 @@ def export_decks(self): logger.info(f"Exported decks to {self.academy_fs.ASSETS_DATA_DECK_DIR}.") # TODO: clean up this: below it's just a draft - def export_intel_cards(self): + def export_intel_cards(self) -> None: logger.info( f"Exporting cards intel to {self.academy_fs.ASSETS_DATA_INTEL_CARD_DIR}..." ) @@ -176,12 +177,12 @@ def export_intel_cards(self): f"Exported cards intel to {self.academy_fs.ASSETS_DATA_INTEL_CARD_DIR}." ) - def export_miscellanea(self): + def export_miscellanea(self) -> None: self.export_changelog() self.export_newspauper() self.export_metagame() - def export_changelog(self): + def export_changelog(self) -> None: logger.info(f"Exporting Changelog to {self.academy_fs.ASSETS_DATA_DIR}...") changelog: Changelog = self.config_reader.get_changelog() safe_dump_json_to_file( @@ -191,7 +192,7 @@ def export_changelog(self): ) logger.info(f"Exported Changelog to {self.academy_fs.ASSETS_DATA_DIR}.") - def export_newspauper(self): + def export_newspauper(self) -> None: logger.info(f"Exporting Newspauper to {self.academy_fs.ASSETS_DATA_DIR}...") newspauper: Newspauper = self.config_reader.get_newspauper() safe_dump_json_to_file( @@ -201,7 +202,7 @@ def export_newspauper(self): ) logger.info(f"Exported Newspauper to {self.academy_fs.ASSETS_DATA_DIR}.") - def export_metagame(self, top_n_chart=TOP_N_ARCHETYPES_PIE_CHART): + def export_metagame(self, top_n_chart: int = TOP_N_ARCHETYPES_PIE_CHART) -> None: logger.info( f"Exporting Metagame data to {self.academy_fs.ASSETS_DATA_INTEL_DIR}..." ) @@ -235,11 +236,11 @@ def export_metagame(self, top_n_chart=TOP_N_ARCHETYPES_PIE_CHART): f"Exported Metagame picture to {self.academy_fs.ASSETS_DATA_INTEL_DIR}." ) - def export_videos(self): + def export_videos(self) -> None: self.export_twitch_videos() self.export_youtube_videos() - def export_twitch_videos(self): + def export_twitch_videos(self) -> None: logger.info( f"Exporting Twitch videos to {self.academy_fs.ASSETS_DATA_VIDEO_DIR}..." ) @@ -248,7 +249,7 @@ def export_twitch_videos(self): f"Exported Twitch videos to {self.academy_fs.ASSETS_DATA_VIDEO_DIR}." ) - def export_youtube_videos(self): + def export_youtube_videos(self) -> None: logger.info( f"Exporting YouTube videos to {self.academy_fs.ASSETS_DATA_VIDEO_DIR}..." ) @@ -257,7 +258,7 @@ def export_youtube_videos(self): f"Exported YouTube videos to {self.academy_fs.ASSETS_DATA_VIDEO_DIR}." ) - def _export_videos(self, video_keys): + def _export_videos(self, video_keys: list[str]) -> None: for video_key in video_keys: video_path = posix_path( self.pauperformance.storage.youtube_video_path, @@ -282,8 +283,8 @@ def _export_videos(self, video_keys): ) def _load_training_data( - self, training_file, assets_data_deck_dir - ) -> list[tuple[PlayableDeck, ArchetypeConfig]]: + self, training_file: str, assets_data_deck_dir: str + ) -> tuple[list[tuple[PlayableDeck, ArchetypeConfig]], str]: # Note: this method assumes all the decks in the training data are available in # the academy as .txt to load and parse. archetypes: list[ArchetypeConfig] = ( @@ -317,7 +318,7 @@ def _load_training_data( def _load_mtggoldfish_tournament_training_data( self, - ) -> list[tuple[PlayableDeck, ArchetypeConfig]]: + ) -> tuple[list[tuple[PlayableDeck, ArchetypeConfig]], str]: return self._load_training_data( self.config_reader.myr_file_system.MTGGOLDFISH_DECK_TRAINING_DATA, self.academy_fs.ASSETS_DATA_DECK_MTGGOLDFISH_TOURNAMENT_DIR, @@ -325,13 +326,13 @@ def _load_mtggoldfish_tournament_training_data( def _load_dpl_training_data( self, - ) -> list[tuple[PlayableDeck, ArchetypeConfig]]: + ) -> tuple[list[tuple[PlayableDeck, ArchetypeConfig]], str]: return self._load_training_data( self.config_reader.myr_file_system.DPL_DECK_TRAINING_DATA, self.academy_fs.ASSETS_DATA_DECK_DPL_DIR, ) - def export_intel_decks(self): + def export_intel_decks(self) -> None: logger.info( f"Exporting decks intel to {self.academy_fs.ASSETS_DATA_INTEL_DECK_DIR}..." ) @@ -344,7 +345,7 @@ def export_intel_decks(self): f"Exported decks intel to {self.academy_fs.ASSETS_DATA_INTEL_DECK_DIR}." ) - def _classify_mtggoldfish_tournament_decks(self): + def _classify_mtggoldfish_tournament_decks(self) -> None: banned_cards = [c["name"] for c in self.scryfall.get_banned_cards()] already_classified_deck_ids = set( p.as_posix().split("/")[-1].replace(".json", "") @@ -420,7 +421,9 @@ def _classify_mtggoldfish_tournament_decks(self): logger.info(f"Classified decks: {len(already_classified_deck_ids)}") logger.warning(f"Unclassified decks: {unclassified_decks_count}") - def classify_deck(self, playable_deck): + def classify_deck( + self, playable_deck: PlayableDeck + ) -> tuple[str | None, float | None]: most_similar_archetype, highest_similarity = self.silver.classify_deck( playable_deck ) @@ -433,7 +436,9 @@ def classify_deck(self, playable_deck): return most_similar_archetype.name, highest_similarity # TODO: this is a temporary function to create the dataset - def _label_mtggoldfish_tournament_decks(self, latest_training_sample): + def _label_mtggoldfish_tournament_decks( + self, latest_training_sample: str | None + ) -> None: banned_cards = [c["name"] for c in self.scryfall.get_banned_cards()] already_classified_deck_ids = set( p.as_posix().split("/")[-1].replace(".json", "") diff --git a/pauperformance_bot/service/academy/data_loader.py b/pauperformance_bot/service/academy/data_loader.py index 1564100c..3bb05159 100644 --- a/pauperformance_bot/service/academy/data_loader.py +++ b/pauperformance_bot/service/academy/data_loader.py @@ -1,7 +1,6 @@ import os import re from os import path -from typing import List, Optional import jsonpickle @@ -20,10 +19,10 @@ class AcademyDataLoader: """Service to load in memory all the Academy assets.""" - def __init__(self, academy_fs=AcademyFileSystem()): + def __init__(self, academy_fs: AcademyFileSystem = AcademyFileSystem()) -> None: self._academy_fs = academy_fs - def load_classified_decks(self, archetype: str) -> List[PlayableDeck]: + def load_classified_decks(self, archetype: str) -> list[PlayableDeck]: """Returns a list of decks for the given archetype.""" deck_dir = posix_path(self._academy_fs.ASSETS_DATA_INTEL_DECK_DIR, archetype) if not path.exists(deck_dir): @@ -42,7 +41,7 @@ def load_classified_decks(self, archetype: str) -> List[PlayableDeck]: return playable_decks @staticmethod - def __load_deck(playable_deck_txt) -> Optional[PlayableDeck]: + def __load_deck(playable_deck_txt: str) -> PlayableDeck | None: playable_deck = None try: with open(playable_deck_txt) as playable_f: @@ -53,7 +52,7 @@ def __load_deck(playable_deck_txt) -> Optional[PlayableDeck]: finally: return playable_deck - def __load_all(self, deck_dir): + def __load_all(self, deck_dir: str) -> tuple[list[PlayableDeck], list[str]]: missing = [] playable_decks = [] for deck_file in os.listdir(deck_dir): diff --git a/pauperformance_bot/service/arena/twitch.py b/pauperformance_bot/service/arena/twitch.py index 79032a33..b2d70788 100644 --- a/pauperformance_bot/service/arena/twitch.py +++ b/pauperformance_bot/service/arena/twitch.py @@ -1,3 +1,5 @@ +from typing import Any + from twitchAPI.twitch import Twitch from twitchAPI.types import TimePeriod @@ -15,13 +17,13 @@ class TwitchService: def __init__( self, - myr_client_id=TWITCH_APP_CLIENT_ID, - myr_client_secret=TWITCH_APP_CLIENT_SECRET, - ): + myr_client_id: str = TWITCH_APP_CLIENT_ID, + myr_client_secret: str = TWITCH_APP_CLIENT_SECRET, + ) -> None: self._service = Twitch(myr_client_id, myr_client_secret) - def get_user(self, login_name): - user = self._service.get_users(logins=[login_name])["data"][0] + def get_user(self, login_name: str) -> TwitchUser: + user: dict[str, Any] = self._service.get_users(logins=[login_name])["data"][0] return TwitchUser( user["id"], user["login"], @@ -29,7 +31,7 @@ def get_user(self, login_name): user["description"], ) - def get_users(self, login_names): + def get_users(self, login_names: list[str]) -> list[TwitchUser]: return [ TwitchUser( user["id"], @@ -40,7 +42,7 @@ def get_users(self, login_names): for user in self._service.get_users(logins=login_names)["data"] ] - def get_user_videos(self, user_id): + def get_user_videos(self, user_id: str) -> list[TwitchVideo]: return [ TwitchVideo( video["id"], diff --git a/pauperformance_bot/service/arena/youtube.py b/pauperformance_bot/service/arena/youtube.py index 4a3206d3..fbbeb930 100644 --- a/pauperformance_bot/service/arena/youtube.py +++ b/pauperformance_bot/service/arena/youtube.py @@ -1,3 +1,5 @@ +from typing import Any + from pyyoutube import Api from pauperformance_bot.constant.arena.youtube import YOUTUBE_VIDEO_URL @@ -12,27 +14,29 @@ class YouTubeService: def __init__( self, - myr_client_id=YOUTUBE_API_KEY, - ): + myr_client_id: str = YOUTUBE_API_KEY, + ) -> None: self._service = Api(api_key=myr_client_id) - def get_channel_info(self, channel_id): + def get_channel_info(self, channel_id: str) -> Any: return self._service.get_channel_info(channel_id=channel_id) - def get_video(self, video_id): + def get_video(self, video_id: str) -> Any: video_by_id = self._service.get_video_by_id(video_id=video_id) return video_by_id - def list_playlists(self, channel_id): + def list_playlists(self, channel_id: str) -> Any: return self._service.get_playlists(channel_id=channel_id, count=None) - def list_playlist_videos(self, playlist_id): + def list_playlist_videos(self, playlist_id: str) -> Any: return self._service.get_playlist_items( playlist_id=playlist_id, count=None, ).items - def get_channel_videos(self, channel_id, default_language) -> list[YouTubeVideo]: + def get_channel_videos( + self, channel_id: str, default_language: str + ) -> list[YouTubeVideo]: channel = self.get_channel_info(channel_id) uploads = channel.items[0].contentDetails.relatedPlaylists.uploads return [ @@ -59,8 +63,8 @@ def get_channel_videos(self, channel_id, default_language) -> list[YouTubeVideo] ] @staticmethod - def get_video_language(video_description, default_language): - language = None + def get_video_language(video_description: str, default_language: str) -> str: + language: str | None = None for line in video_description.split("\n"): line = line.strip() if line.lower().startswith(VIDEO_LANGUAGE_TAG.lower()): diff --git a/pauperformance_bot/service/mtg/deckstats.py b/pauperformance_bot/service/mtg/deckstats.py index b3096c51..925c3f1e 100644 --- a/pauperformance_bot/service/mtg/deckstats.py +++ b/pauperformance_bot/service/mtg/deckstats.py @@ -1,6 +1,7 @@ import json import pickle from functools import partial +from typing import Any import requests @@ -23,13 +24,13 @@ class DeckstatsService: def __init__( self, - owner_id, - endpoint=API_ENDPOINT, - ): + owner_id: str, + endpoint: str = API_ENDPOINT, + ) -> None: self.owner_id = owner_id self.endpoint = endpoint - def list_user_folders_id(self): + def list_user_folders_id(self) -> dict[str, str]: url = self.endpoint method = requests.get params = { @@ -43,15 +44,17 @@ def list_user_folders_id(self): method = partial(method, params=params) response = execute_http_request(method, url) response = json.loads(response.content) - folders = {"": "0"} + folders: dict[str, str] = {"": "0"} for subfolder in response["folder"].get("subfolders", []): folders[subfolder["name"]] = str(subfolder["id"]) return folders - def list_public_decks_in_folder(self, owner_name, folder_id): + def list_public_decks_in_folder( + self, owner_name: str, folder_id: str + ) -> list[DeckstatsDeck]: url = self.endpoint method = requests.get - fetched_decks = [] + fetched_decks: list[DeckstatsDeck] = [] decks_total = -1 curr_page = 1 while len(fetched_decks) != decks_total: @@ -91,7 +94,7 @@ def list_public_decks_in_folder(self, owner_name, folder_id): curr_page += 1 return sorted(fetched_decks, key=lambda d: d.added) - def list_pauperformance_decks(self, owner_name): + def list_pauperformance_decks(self, owner_name: str) -> list[DeckstatsDeck]: folders = self.list_user_folders_id() if MONITORED_PAUPERFORMANCE_FOLDER not in folders: return [] @@ -105,10 +108,10 @@ def list_pauperformance_decks(self, owner_name): def get_deck( self, - deck_id, - decks_cache_dir=DECKSTATS_DECKS_CACHE_DIR, - use_cache=True, - ): + deck_id: str, + decks_cache_dir: str = DECKSTATS_DECKS_CACHE_DIR, + use_cache: bool = True, + ) -> dict[str, Any]: if use_cache: try: with open( @@ -136,12 +139,12 @@ def get_deck( pickle.dump(deck, cache_f) return deck - def to_playable_deck(self, deckstats_deck): + def to_playable_deck(self, deckstats_deck: dict[str, Any]) -> PlayableDeck: logger.info(f"Parsing deckstats deck {deckstats_deck['saved_id']} list...") main_section = next( s for s in deckstats_deck["sections"] if s["name"] == "Main" ) - playable_main = [] + playable_main: list[PlayedCard] = [] for c in main_section["cards"]: if not c["valid"]: raise DeckstatsException( @@ -150,7 +153,7 @@ def to_playable_deck(self, deckstats_deck): f"{c['amount']} {c['name']}" ) playable_main.append(PlayedCard(c["amount"], c["name"])) - playable_sideboard = [] + playable_sideboard: list[PlayedCard] = [] for c in deckstats_deck["sideboard"]: if not c["valid"]: raise DeckstatsException( diff --git a/pauperformance_bot/service/mtg/downloader/abstract.py b/pauperformance_bot/service/mtg/downloader/abstract.py index 38a04b66..86c9845e 100644 --- a/pauperformance_bot/service/mtg/downloader/abstract.py +++ b/pauperformance_bot/service/mtg/downloader/abstract.py @@ -1,13 +1,12 @@ from abc import ABCMeta, abstractmethod -from typing import Optional from pauperformance_bot.entity.deck.playable import PlayableDeck class AbstractDeckDownloader(metaclass=ABCMeta): - def __init__(self, url) -> None: + def __init__(self, url: str) -> None: self._url = url @abstractmethod - def download(self) -> Optional[PlayableDeck]: + def download(self) -> PlayableDeck | None: pass diff --git a/pauperformance_bot/service/mtg/downloader/downloader.py b/pauperformance_bot/service/mtg/downloader/downloader.py index 5458c9e7..ddb59030 100644 --- a/pauperformance_bot/service/mtg/downloader/downloader.py +++ b/pauperformance_bot/service/mtg/downloader/downloader.py @@ -1,5 +1,3 @@ -from typing import Dict - import cloudscraper from pauperformance_bot.entity.deck.playable import PlayableDeck @@ -25,7 +23,7 @@ class MtgoDeckDownloader(AbstractDeckDownloader): - https://www.mtgtop8.com/mtgo?d=473002 """ - def __init__(self, url, headers: Dict = None) -> None: + def __init__(self, url: str, headers: dict[str, str] | None = None) -> None: super().__init__(url) _headers = headers if not headers: @@ -37,7 +35,7 @@ def download(self) -> PlayableDeck: logger.debug(f"fetching deck list from {self._url}") resp = execute_http_request(self._scraper.get, self._url, headers=self._headers) pl = "start" - lines = [] + lines: list[str] = [] for ln in resp.text.strip().split("\n"): sl = ln.strip() sbl = sl.lower().startswith("sideboard") diff --git a/pauperformance_bot/service/mtg/downloader/moxfield.py b/pauperformance_bot/service/mtg/downloader/moxfield.py index 5688c7f7..7519f4a7 100644 --- a/pauperformance_bot/service/mtg/downloader/moxfield.py +++ b/pauperformance_bot/service/mtg/downloader/moxfield.py @@ -17,7 +17,7 @@ class MoxfieldDeckDownloader(AbstractDeckDownloader): """Downloads a deck from https://www.moxfield.com/""" - def __init__(self, url) -> None: + def __init__(self, url: str) -> None: super().__init__(url) self._downloader = MtgoDeckDownloader( url, @@ -38,7 +38,7 @@ def download(self) -> PlayableDeck: logger.debug("Fetched deck.") deck = resp.json() logger.debug(f"Author name: {deck['name']}") - lines = [] + lines: list[str] = [] for card in deck["boards"]["mainboard"]["cards"].values(): quantity = card["quantity"] name = card["card"]["name"] diff --git a/pauperformance_bot/service/mtg/downloader/mtgdecks.py b/pauperformance_bot/service/mtg/downloader/mtgdecks.py index e9b330d0..3cc69a09 100644 --- a/pauperformance_bot/service/mtg/downloader/mtgdecks.py +++ b/pauperformance_bot/service/mtg/downloader/mtgdecks.py @@ -6,7 +6,7 @@ class MtgdecksDeckDownloader(AbstractDeckDownloader): """Downloads a deck from mtgdecks.net""" - def __init__(self, url) -> None: + def __init__(self, url: str) -> None: super().__init__(url) self._downloader = MtgoDeckDownloader( url, diff --git a/pauperformance_bot/service/mtg/downloader/service.py b/pauperformance_bot/service/mtg/downloader/service.py index 5802e2f0..0b85581e 100644 --- a/pauperformance_bot/service/mtg/downloader/service.py +++ b/pauperformance_bot/service/mtg/downloader/service.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Type +from typing import Type from pauperformance_bot.entity.deck.playable import PlayableDeck from pauperformance_bot.service.mtg.downloader.abstract import AbstractDeckDownloader @@ -13,13 +13,13 @@ class DeckDownloaderService: """Service to download a deck from different sources given an input url""" - _downloaders: Dict[str, Type[AbstractDeckDownloader]] = { + _downloaders: dict[str, Type[AbstractDeckDownloader]] = { "mtgdecks.net": MtgdecksDeckDownloader, "moxfield.com": MoxfieldDeckDownloader, } @classmethod - def from_url(cls, url: str) -> Optional[PlayableDeck]: + def from_url(cls, url: str) -> PlayableDeck | None: """Downloads a deck from a given url. Args: @@ -27,7 +27,7 @@ def from_url(cls, url: str) -> Optional[PlayableDeck]: "https://www.mtgtop8.com/mtgo?d=473002" Returns: - Optional[PlayableDeck]: the representation of the deck or None. + PlayableDeck | None: the representation of the deck or None. """ for domain, cls in DeckDownloaderService._downloaders.items(): if domain in url: diff --git a/pauperformance_bot/service/mtg/mtggoldfish.py b/pauperformance_bot/service/mtg/mtggoldfish.py index 29412b54..e796b1c3 100644 --- a/pauperformance_bot/service/mtg/mtggoldfish.py +++ b/pauperformance_bot/service/mtg/mtggoldfish.py @@ -19,6 +19,7 @@ METAGAME_SHARE_CLASS, REQUESTS_SLEEP_SECONDS, ) +from pauperformance_bot.constant.pauperformance.academy import AcademyFileSystem from pauperformance_bot.entity.api.deck import MTGGoldfishTournamentDeck from pauperformance_bot.entity.api.tournament import Tournament from pauperformance_bot.entity.deck.archive.mtggoldfish import MTGGoldfishArchivedDeck @@ -63,7 +64,7 @@ def get_pauper_meta( raise MTGGoldfishException( "Mismatch with archetype shares after parsing meta." ) - meta_decks = {} + meta_decks: dict[str, tuple[str, PlayableDeck]] = {} for share, link in zip(archetype_shares, archetype_links): logger.info(f"Archetype {link}: {share}.") logger.debug(f"Retrieving sample deck for archetype {link}...") @@ -99,9 +100,9 @@ def get_pauper_meta( def get_pauper_tournaments( from_date: datetime.datetime, to_date: datetime.datetime, - tournament_name="", + tournament_name: str = "", ) -> list[MTGGoldfishTournamentSearchResult]: - tournaments = [] + tournaments: list[MTGGoldfishTournamentSearchResult] = [] for page in range(1, 5): # MTGGoldfish returns at most 4 results pages from_param = f"{from_date.month}%2F{from_date.day}%2F{from_date.year}" to_param = f"{to_date.month}%2F{to_date.day}%2F{to_date.year}" @@ -164,10 +165,10 @@ def get_tournament_decks( columns = tr.contents if len(columns) < 10: continue - # sometimes the special ' ' character is used instead of ' ': fix it - place = columns[1].text.strip().replace(" ", " ") - archetype = columns[3].text.strip().replace(" ", " ") - pilot = columns[5].text.strip().replace(" ", " ") + # sometimes the special '\xa0' character is used instead of ' ': fix it + place = columns[1].text.strip().replace("\xa0", " ") + archetype = columns[3].text.strip().replace("\xa0", " ") + pilot = columns[5].text.strip().replace("\xa0", " ") # Sometimes MTGGoldfish does not report deck prices. # Maybe in the past they could not get this info. # Let's take into account missing values. @@ -195,7 +196,12 @@ def get_tournament_decks( logger.debug(f"Extracted tournament decks from {url}.") return tournament_decks - def download_mtggoldfish_tournaments(self, start_date, end_date, academy_fs): + def download_mtggoldfish_tournaments( + self, + start_date: datetime.datetime, + end_date: datetime.datetime, + academy_fs: AcademyFileSystem, + ) -> None: processed_tournament_ids = { tournament_id.name.rstrip(".json") for tournament_id in Path( diff --git a/pauperformance_bot/service/mtg/scryfall.py b/pauperformance_bot/service/mtg/scryfall.py index e7a9d5b8..a379a993 100644 --- a/pauperformance_bot/service/mtg/scryfall.py +++ b/pauperformance_bot/service/mtg/scryfall.py @@ -2,6 +2,7 @@ import pickle import urllib.parse from functools import lru_cache, partial +from typing import Any import requests @@ -18,12 +19,14 @@ class ScryfallService: - def __init__(self, website_url=WEBSITE_URL, endpoint=API_ENDPOINT): + def __init__( + self, website_url: str = WEBSITE_URL, endpoint: str = API_ENDPOINT + ) -> None: self.website_url = website_url self.endpoint = endpoint @lru_cache(maxsize=1) - def get_sets(self): + def get_sets(self) -> dict[str, Any]: url = f"{self.endpoint}/sets" method = requests.get response = execute_http_request(method, url) @@ -31,9 +34,9 @@ def get_sets(self): def get_card_named( self, - exact_card_name, - cards_cache_dir=SCRYFALL_CARDS_CACHE_DIR, - ): + exact_card_name: str, + cards_cache_dir: str = SCRYFALL_CARDS_CACHE_DIR, + ) -> dict[str, Any]: try: with open( posix_path(cards_cache_dir, to_pkl_name(exact_card_name)), "rb" @@ -64,13 +67,13 @@ def get_card_named( else: raise - def search_cards(self, query): + def search_cards(self, query: str) -> list[dict[str, Any]] | dict[str, Any]: url = f"{self.endpoint}/cards/search" method = requests.get params = {"q": query} method = partial(method, params=params) has_more = True - cards = [] + cards: list[dict[str, Any]] = [] try: while has_more: response = execute_http_request(method, url) @@ -86,27 +89,27 @@ def search_cards(self, query): return {} @lru_cache(maxsize=1) - def get_legal_lands(self): + def get_legal_lands(self) -> list[dict[str, Any]] | dict[str, Any]: query = "type:land legal:pauper" return self.search_cards(query) @lru_cache(maxsize=1) - def get_legal_artifact_lands(self): + def get_legal_artifact_lands(self) -> list[dict[str, Any]] | dict[str, Any]: query = "(type:artifact type:land) legal:pauper" return self.search_cards(query) @lru_cache(maxsize=1) - def get_banned_cards(self): + def get_banned_cards(self) -> list[dict[str, Any]] | dict[str, Any]: query = "banned:pauper" return self.search_cards(query) - def get_artist_gallery_search_url(self, artist_name: str): + def get_artist_gallery_search_url(self, artist_name: str) -> str: query = f"a:'{artist_name}'" encoded_query = urllib.parse.quote(query) query_params = "&".join(("unique=art", "as=grid", "order=name")) return f"{self.website_url}/search?q={encoded_query}&{query_params}" - def get_card_from_url(self, card_url: str): + def get_card_from_url(self, card_url: str) -> dict[str, Any]: logger.debug(f"Retrieving card from url: {card_url}...") # make an educated guess on the card name from the URL card_name = card_url.split("/")[-1].replace("-", " ") @@ -115,7 +118,7 @@ def get_card_from_url(self, card_url: str): logger.debug(f"Retrieved card from url: {card_url}.") return card - def get_archetype_cards(self, cards) -> list[ArchetypeCard]: + def get_archetype_cards(self, cards: list[str]) -> list[ArchetypeCard]: rendered_cards: list[ArchetypeCard] = [] for card in sorted(cards): scryfall_card = self.get_card_named(card) diff --git a/pauperformance_bot/service/mtg/wizards.py b/pauperformance_bot/service/mtg/wizards.py index b5d2f290..09bb7f0c 100644 --- a/pauperformance_bot/service/mtg/wizards.py +++ b/pauperformance_bot/service/mtg/wizards.py @@ -1,5 +1,4 @@ import re -from typing import List import requests from pyquery import PyQuery @@ -15,7 +14,7 @@ logger = get_application_logger() -def _parse_match_result(player1_line, player2_line) -> MTGOStandingMatch: +def _parse_match_result(player1_line: str, player2_line: str) -> MTGOStandingMatch: result = re.search(r"\(([0-9]+)\) (\w+), ([0-9-]+)", player1_line) player1_ranking, player1, match_score = result.groups() result = re.search(r"\(([0-9]+)\) (\w+)", player2_line) @@ -29,10 +28,10 @@ def _parse_match_result(player1_line, player2_line) -> MTGOStandingMatch: ) -def _parse_round_results(pq, round_class) -> List[MTGOStandingMatch]: +def _parse_round_results(pq: PyQuery, round_class: str) -> list[MTGOStandingMatch]: logger.debug(f"Inspecting class: {round_class}...") - matches: List[MTGOStandingMatch] = [] - buffer = [] + matches: list[MTGOStandingMatch] = [] + buffer: list[str] = [] for player in pq(round_class).items(): logger.debug(f"Parsing: {player.text()}") buffer.append(player.text()) @@ -45,7 +44,7 @@ def _parse_round_results(pq, round_class) -> List[MTGOStandingMatch]: class WizardsService: - def __init__(self): + def __init__(self) -> None: pass def parse_mtgo_pauper_challenge(self, url: str) -> MTGOStandings: diff --git a/pauperformance_bot/service/nexus/abstract_discord_service.py b/pauperformance_bot/service/nexus/abstract_discord_service.py index a55e39dc..c77398c4 100644 --- a/pauperformance_bot/service/nexus/abstract_discord_service.py +++ b/pauperformance_bot/service/nexus/abstract_discord_service.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import Any import discord @@ -10,12 +11,12 @@ class AbstractDiscordService(discord.Client, ABC): def __init__( self, - myr_bot_token, - import_deck_channel_id, - welcome_channel_id, - myr_log_channel_id, - **options, - ): + myr_bot_token: str, + import_deck_channel_id: int, + welcome_channel_id: int, + myr_log_channel_id: int, + **options: Any, + ) -> None: super().__init__(**options) self.myr_bot_token = myr_bot_token self.import_deck_channel_id = import_deck_channel_id @@ -24,15 +25,15 @@ def __init__( @property @abstractmethod - def log_channel(self): + def log_channel(self) -> discord.TextChannel | None: pass - async def send_log_message(self, message): + async def send_log_message(self, message: str) -> None: await self.log_channel.send(message) - async def send_user_message(self, user_id, message): + async def send_user_message(self, user_id: int, message: str) -> None: user = await self.fetch_user(user_id) await user.send(message) - def list_roles(self, guild_id): + def list_roles(self, guild_id: int) -> list[discord.Role]: return self.get_guild(guild_id).roles diff --git a/pauperformance_bot/service/nexus/async_discord_service.py b/pauperformance_bot/service/nexus/async_discord_service.py index baeade91..71175648 100644 --- a/pauperformance_bot/service/nexus/async_discord_service.py +++ b/pauperformance_bot/service/nexus/async_discord_service.py @@ -1,4 +1,7 @@ import asyncio +from typing import Any + +import discord from pauperformance_bot.constant.pauperformance.nexus import ( DISCORD_CHANNEL_IMPORT_DECK_ID, @@ -22,12 +25,12 @@ class AsyncDiscordService(AbstractDiscordService): def __init__( self, - myr_bot_token=DISCORD_BOT_TOKEN, - import_deck_channel_id=DISCORD_CHANNEL_IMPORT_DECK_ID, - welcome_channel_id=DISCORD_CHANNEL_WELCOME_ID, - myr_log_channel_id=DISCORD_CHANNEL_MYR_LOG_ID, - **options, - ): + myr_bot_token: str = DISCORD_BOT_TOKEN, + import_deck_channel_id: int = DISCORD_CHANNEL_IMPORT_DECK_ID, + welcome_channel_id: int = DISCORD_CHANNEL_WELCOME_ID, + myr_log_channel_id: int = DISCORD_CHANNEL_MYR_LOG_ID, + **options: Any, + ) -> None: super().__init__( myr_bot_token=myr_bot_token, import_deck_channel_id=import_deck_channel_id, @@ -35,25 +38,25 @@ def __init__( myr_log_channel_id=myr_log_channel_id, **options, ) - self._log_channel = None + self._log_channel: discord.TextChannel | None = None asyncio.create_task(self._run_background()) # fire and forget call - async def _run_background(self): + async def _run_background(self) -> None: logger.info("AsyncDiscordService starting in background...") logger.info("Logging on Discord with token...") await self.start(self.myr_bot_token) # will call on_ready() logger.info("AsyncDiscordService stopped in background.") - async def wait_until_ready(self): + async def wait_until_ready(self) -> None: await super().wait_until_ready() logger.info(f"Retrieving log channel (id: {self.myr_log_channel_id})...") self.log_channel = self.get_channel(self.myr_log_channel_id) logger.info("Retrieved log channel.") - async def on_ready(self): + async def on_ready(self) -> None: logger.info(f"Logged on Discord as {self.user}.") - async def _clean_my_emoji(self, channel_id): + async def _clean_my_emoji(self, channel_id: int) -> None: channel = self.get_channel(channel_id) messages = await channel.history(limit=DISCORD_MAX_HISTORY_LIMIT).flatten() for m in messages: @@ -63,9 +66,9 @@ async def _clean_my_emoji(self, channel_id): await m.remove_reaction(DISCORD_MYR_REACTION_WARNING, self.user) @property - def log_channel(self): + def log_channel(self) -> discord.TextChannel | None: return self._log_channel @log_channel.setter - def log_channel(self, value): + def log_channel(self, value: discord.TextChannel | None) -> None: self._log_channel = value diff --git a/pauperformance_bot/service/nexus/sync/members_fetcher.py b/pauperformance_bot/service/nexus/sync/members_fetcher.py index b3755a08..158a877e 100644 --- a/pauperformance_bot/service/nexus/sync/members_fetcher.py +++ b/pauperformance_bot/service/nexus/sync/members_fetcher.py @@ -1,3 +1,5 @@ +from typing import Any + from pauperformance_bot.constant.pauperformance.nexus import ( DISCORD_CHANNEL_IMPORT_DECK_ID, DISCORD_CHANNEL_MYR_LOG_ID, @@ -16,12 +18,12 @@ class DiscordMembersFetcherSyncService(AbstractSyncDiscordService): def __init__( self, - myr_bot_token=DISCORD_BOT_TOKEN, - import_deck_channel_id=DISCORD_CHANNEL_IMPORT_DECK_ID, - welcome_channel_id=DISCORD_CHANNEL_WELCOME_ID, - myr_log_channel_id=DISCORD_CHANNEL_MYR_LOG_ID, - **options, - ): + myr_bot_token: str = DISCORD_BOT_TOKEN, + import_deck_channel_id: int = DISCORD_CHANNEL_IMPORT_DECK_ID, + welcome_channel_id: int = DISCORD_CHANNEL_WELCOME_ID, + myr_log_channel_id: int = DISCORD_CHANNEL_MYR_LOG_ID, + **options: Any, + ) -> None: super().__init__( myr_bot_token=myr_bot_token, import_deck_channel_id=import_deck_channel_id, @@ -29,17 +31,17 @@ def __init__( myr_log_channel_id=myr_log_channel_id, **options, ) - self.result = {} + self.result: dict[str, int] = {} - async def _task(self): + async def _task(self) -> dict[str, int]: return await self._fetch_members() - async def _fetch_members(self): + async def _fetch_members(self) -> dict[str, int]: welcome_channel = self.get_channel(self.welcome_channel_id) messages = await welcome_channel.history( limit=DISCORD_MAX_HISTORY_LIMIT ).flatten() - users = {} + users: dict[str, int] = {} for message in messages: users[message.author.display_name] = message.author.id logger.info(f"Discord users: {users}") diff --git a/pauperformance_bot/service/nexus/sync/messages_sender.py b/pauperformance_bot/service/nexus/sync/messages_sender.py index ff887fd0..48cdaddb 100644 --- a/pauperformance_bot/service/nexus/sync/messages_sender.py +++ b/pauperformance_bot/service/nexus/sync/messages_sender.py @@ -1,3 +1,5 @@ +from typing import Any + from pauperformance_bot.constant.pauperformance.nexus import ( DISCORD_CHANNEL_IMPORT_DECK_ID, DISCORD_CHANNEL_MYR_LOG_ID, @@ -15,13 +17,13 @@ class DiscordMessagesSenderSyncService(AbstractSyncDiscordService): def __init__( self, - messages, - myr_bot_token=DISCORD_BOT_TOKEN, - import_deck_channel_id=DISCORD_CHANNEL_IMPORT_DECK_ID, - welcome_channel_id=DISCORD_CHANNEL_WELCOME_ID, - myr_log_channel_id=DISCORD_CHANNEL_MYR_LOG_ID, - **options, - ): + messages: list[str], + myr_bot_token: str = DISCORD_BOT_TOKEN, + import_deck_channel_id: int = DISCORD_CHANNEL_IMPORT_DECK_ID, + welcome_channel_id: int = DISCORD_CHANNEL_WELCOME_ID, + myr_log_channel_id: int = DISCORD_CHANNEL_MYR_LOG_ID, + **options: Any, + ) -> None: super().__init__( myr_bot_token=myr_bot_token, import_deck_channel_id=import_deck_channel_id, @@ -30,9 +32,9 @@ def __init__( **options, ) self.messages = messages - self.result = False + self.result: bool = False - async def _task(self): + async def _task(self) -> bool: for message in self.messages: await self.send_log_message(message) return True diff --git a/pauperformance_bot/service/nexus/sync_discord_service.py b/pauperformance_bot/service/nexus/sync_discord_service.py index 49026742..105ba782 100644 --- a/pauperformance_bot/service/nexus/sync_discord_service.py +++ b/pauperformance_bot/service/nexus/sync_discord_service.py @@ -1,5 +1,8 @@ import asyncio from abc import ABC, abstractmethod +from typing import Any + +import discord from pauperformance_bot.constant.pauperformance.nexus import ( DISCORD_CHANNEL_IMPORT_DECK_ID, @@ -18,12 +21,12 @@ class AbstractSyncDiscordService(AbstractDiscordService, ABC): def __init__( self, - myr_bot_token=DISCORD_BOT_TOKEN, - import_deck_channel_id=DISCORD_CHANNEL_IMPORT_DECK_ID, - welcome_channel_id=DISCORD_CHANNEL_WELCOME_ID, - myr_log_channel_id=DISCORD_CHANNEL_MYR_LOG_ID, - **options, - ): + myr_bot_token: str = DISCORD_BOT_TOKEN, + import_deck_channel_id: int = DISCORD_CHANNEL_IMPORT_DECK_ID, + welcome_channel_id: int = DISCORD_CHANNEL_WELCOME_ID, + myr_log_channel_id: int = DISCORD_CHANNEL_MYR_LOG_ID, + **options: Any, + ) -> None: super().__init__( myr_bot_token=myr_bot_token, import_deck_channel_id=import_deck_channel_id, @@ -31,14 +34,14 @@ def __init__( myr_log_channel_id=myr_log_channel_id, **options, ) - self._log_channel = None - self.result = None + self._log_channel: discord.TextChannel | None = None + self.result: Any = None - def run_task(self): + def run_task(self) -> None: logger.info("Logging on Discord with token...") self.run(self.myr_bot_token) # will call on_ready() - async def on_ready(self): + async def on_ready(self) -> None: logger.info(f"Logged on Discord as {self.user}.") logger.info(f"Retrieving log channel (id: {self.myr_log_channel_id})...") self.log_channel = self.get_channel(self.myr_log_channel_id) @@ -49,13 +52,13 @@ async def on_ready(self): asyncio.set_event_loop(asyncio.new_event_loop()) @abstractmethod - async def _task(self): + async def _task(self) -> Any: pass @property - def log_channel(self): + def log_channel(self) -> discord.TextChannel | None: return self._log_channel @log_channel.setter - def log_channel(self, value): + def log_channel(self, value: discord.TextChannel | None) -> None: self._log_channel = value diff --git a/pauperformance_bot/service/pauperformance/archive/abstract.py b/pauperformance_bot/service/pauperformance/archive/abstract.py index e08df2bd..e7aad49d 100644 --- a/pauperformance_bot/service/pauperformance/archive/abstract.py +++ b/pauperformance_bot/service/pauperformance/archive/abstract.py @@ -2,6 +2,7 @@ import time from abc import ABCMeta, abstractmethod from datetime import datetime +from typing import Any from urllib.request import urlopen from bs4 import BeautifulSoup @@ -39,11 +40,13 @@ class AbstractArchiveService(metaclass=ABCMeta): @abstractmethod - def get_uri(self, deck_id): + def get_uri(self, deck_id: str) -> str: pass @abstractmethod - def create_deck(self, name, description, playable_deck): + def create_deck( + self, name: str, description: str, playable_deck: PlayableDeck + ) -> str: pass @abstractmethod @@ -51,13 +54,15 @@ def list_decks(self) -> list[AbstractArchivedDeck]: pass @abstractmethod - def delete_deck(self, deck_id): + def delete_deck(self, deck_id: str) -> None: pass @staticmethod @abstractmethod def to_playable_deck( - listed_deck: AbstractArchivedDeck, decks_cache_dir=None, use_cache=True + listed_deck: AbstractArchivedDeck, + decks_cache_dir: str | None = None, + use_cache: bool = True, ) -> PlayableDeck: pass @@ -67,14 +72,14 @@ def get_deck(self, deck_name: str) -> AbstractArchivedDeck: async def import_player_decks_from_deckstats( self, - player, - storage, - players_by_deckstats_id, - set_index, - discord, - warning_player, - send_notification=True, - ): # TODO: get rid of players_by_deckstats_id + player: PhDConfig, + storage: Any, + players_by_deckstats_id: dict[int, PhDConfig], + set_index: dict[int, dict[str, Any]], + discord: Any, + warning_player: PhDConfig, + send_notification: bool = True, + ) -> None: # TODO: get rid of players_by_deckstats_id logger.info(f"Updating archive decks for {player.name}...") deckstats = DeckstatsService(owner_id=player.deckstats_id) imported_deckstats_deck = storage.list_imported_deckstats_deck_ids() @@ -153,12 +158,12 @@ async def import_player_decks_from_deckstats( async def archive_player_videos_from_twitch( self, player: PhDConfig, - videos, - storage, - discord, - warning_player, - send_notification=True, - ): + videos: list[Any], + storage: Any, + discord: Any, + warning_player: PhDConfig, + send_notification: bool = True, + ) -> None: logger.info(f"Updating Archive videos for {player.name}...") imported_twitch_videos = storage.list_imported_twitch_videos_ids() config_reader = ConfigReader() @@ -246,11 +251,11 @@ async def archive_player_videos_from_youtube( self, player: PhDConfig, videos: list[YouTubeVideo], - storage, - discord, - warning_player, - send_notification=True, - ): + storage: Any, + discord: Any, + warning_player: PhDConfig, + send_notification: bool = True, + ) -> None: logger.info(f"Updating Archive videos for {player.name}...") imported_youtube_videos = storage.list_imported_youtube_videos_ids() config_reader = ConfigReader() @@ -337,13 +342,13 @@ async def archive_player_videos_from_youtube( async def import_player_deck_from_mtggoldfish( self, - url, + url: str, player: PhDConfig, - pauperformance, - discord_message, - p12e_name=None, - send_notification=True, - ): + pauperformance: Any, + discord_message: Any, + p12e_name: str | None = None, + send_notification: bool = True, + ) -> None: author = discord_message.author discord = pauperformance.discord storage = pauperformance.storage diff --git a/pauperformance_bot/service/pauperformance/archive/local.py b/pauperformance_bot/service/pauperformance/archive/local.py index ae966d52..6275f744 100644 --- a/pauperformance_bot/service/pauperformance/archive/local.py +++ b/pauperformance_bot/service/pauperformance/archive/local.py @@ -29,14 +29,16 @@ class LocalArchiveService(AbstractArchiveService): def __init__( self, - root_dir=posix_path(ARCHIVE_DIR, MTGGOLDFISH_ARCHIVE_SUBDIR), - ): + root_dir: str = posix_path(ARCHIVE_DIR, MTGGOLDFISH_ARCHIVE_SUBDIR), + ) -> None: self._root_dir = root_dir - def get_uri(self, deck_id): + def get_uri(self, deck_id: str) -> str: return f"{self._root_dir}{sep}{deck_id}" - def create_deck(self, name, description, playable_deck): + def create_deck( + self, name: str, description: str, playable_deck: PlayableDeck + ) -> str: deck_id = uuid.uuid4().hex output_file = f"{self.get_uri(deck_id)}" if os.path.isfile(output_file): @@ -77,7 +79,7 @@ def list_decks(self) -> list[AbstractArchivedDeck]: logger.info(f"Listed files in {self._root_dir}...") return decks - def delete_deck(self, deck_id): + def delete_deck(self, deck_id: str) -> None: file_name = self.get_uri(deck_id) logger.info(f"Deleting deck {file_name}...") os.remove(file_name) @@ -85,7 +87,9 @@ def delete_deck(self, deck_id): @staticmethod def to_playable_deck( - listed_deck: AbstractArchivedDeck, decks_cache_dir="USELESS!", use_cache=False + listed_deck: AbstractArchivedDeck, + decks_cache_dir: str | None = "USELESS!", + use_cache: bool = False, ) -> PlayableDeck: if use_cache: logger.info("Ignoring cache on local Archive...") diff --git a/pauperformance_bot/service/pauperformance/archive/mtggoldfish.py b/pauperformance_bot/service/pauperformance/archive/mtggoldfish.py index 010d3b6c..e37706d0 100644 --- a/pauperformance_bot/service/pauperformance/archive/mtggoldfish.py +++ b/pauperformance_bot/service/pauperformance/archive/mtggoldfish.py @@ -3,6 +3,7 @@ from datetime import datetime from functools import cache, wraps from itertools import count +from typing import Any, Callable from urllib.request import urlopen from pyquery import PyQuery @@ -38,9 +39,9 @@ logger = get_application_logger() -def with_login(func): +def with_login(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) - def maybe_login(*args, **kwargs): + def maybe_login(*args: Any, **kwargs: Any) -> Any: mtggoldfish = args[0] # "self" is the 1st argument of method calls if not mtggoldfish.logged: mtggoldfish.login() @@ -53,12 +54,12 @@ def maybe_login(*args, **kwargs): class MTGGoldfishArchiveService(AbstractArchiveService): def __init__( self, - storage, - email=MTGGOLDFISH_PAUPERFORMANCE_USERNAME, - password=MTGGOLDFISH_PAUPERFORMANCE_PASSWORD, - endpoint=API_ENDPOINT, - deck_api_endpoint=DECK_API_ENDPOINT, - ): + storage: Any, + email: str = MTGGOLDFISH_PAUPERFORMANCE_USERNAME, + password: str = MTGGOLDFISH_PAUPERFORMANCE_PASSWORD, + endpoint: str = API_ENDPOINT, + deck_api_endpoint: str = DECK_API_ENDPOINT, + ) -> None: self.email = email self.password = password self.endpoint = endpoint @@ -84,7 +85,7 @@ def __init__( self._decks_cache: list[AbstractArchivedDeck] = [] # will act as a cache @staticmethod - def _parse_login_authenticity_token(response): + def _parse_login_authenticity_token(response: Any) -> str: for line in response.text.split("\n"): if "authenticity_token" not in line: continue @@ -104,7 +105,7 @@ def _parse_login_authenticity_token(response): raise MTGGoldfishException("Unable to get authenticity_token from MTGGoldfish.") @staticmethod - def _parse_meta_authenticity_token(response): + def _parse_meta_authenticity_token(response: Any) -> str: for line in response.text.split("\n"): if "meta" not in line or "csrf-token" not in line: continue @@ -117,7 +118,7 @@ def _parse_meta_authenticity_token(response): return authenticity_token raise MTGGoldfishException("Unable to get authenticity_token from MTGGoldfish.") - def _get_login_info(self): + def _get_login_info(self) -> tuple[str, str]: logger.info("Getting dynamic login info from MTGGoldfish...") response = self.session.get(f"{self.endpoint}") if "_mtg_session" not in response.cookies.get_dict(): @@ -128,7 +129,7 @@ def _get_login_info(self): logger.debug(f"Found mtg_session_cookie: {mtg_session_cookie}") return mtg_session_cookie, self._parse_login_authenticity_token(response) - def login(self): + def login(self) -> None: mtg_session_cookie, authenticity_token = self._get_login_info() logger.info(f"Logging to MTGGoldfish as {self.email}...") header = { @@ -156,14 +157,18 @@ def login(self): ) logger.info(f"Logged to MTGGoldfish as {self.email}.") - def get_uri(self, deck_id): + def get_uri(self, deck_id: str) -> str: return f"{self.deck_api_endpoint}/{deck_id}" @with_login - def create_deck(self, name, description, playable_deck): + def create_deck( + self, name: str, description: str, playable_deck: PlayableDeck + ) -> str: return self._create_deck(name, description, playable_deck, format_="pauper") - def _create_deck(self, name, description, playable_deck, format_): + def _create_deck( + self, name: str, description: str, playable_deck: PlayableDeck, format_: str + ) -> str: logger.info(f"Creating deck {name} for {self.email}...") # we need to perform a dummy request to parse the authenticity_token response = self.session.get(f"{self.endpoint}/decks/new") @@ -205,7 +210,7 @@ def list_decks(self) -> list[AbstractArchivedDeck]: self._update_decks_cache() return self._decks_cache - def _update_decks_cache(self): + def _update_decks_cache(self) -> None: all_decks = [] for page in count(1): new_decks = self._list_decks_in_page(page) @@ -227,8 +232,12 @@ def _update_decks_cache(self): @with_login def _list_decks_in_page( - self, page, filter_name="", mtg_format="pauper", visibility="public" - ): + self, + page: int, + filter_name: str = "", + mtg_format: str = "pauper", + visibility: str = "public", + ) -> list[AbstractArchivedDeck]: # possible filter_visibility values: '', 'private', 'public' logger.info(f"Listing decks for {self.email} in page {page}...") params = { @@ -263,7 +272,9 @@ def _list_decks_in_page( logger.info(f"Listed decks for {self.email}.") return decks - def _workaround_retrieve_missing_decks(self, all_decks): + def _workaround_retrieve_missing_decks( + self, all_decks: list[AbstractArchivedDeck] + ) -> list[AbstractArchivedDeck]: logger.debug("Fixing bug in MTGGoldfish pager...") # Due to a bug in the pagination mechanism, decks are sometimes @@ -286,7 +297,7 @@ def _workaround_retrieve_missing_decks(self, all_decks): return all_decks @with_login - def delete_deck(self, deck_id): + def delete_deck(self, deck_id: str) -> None: logger.info(f"Deleting deck with id {deck_id} for {self.email}...") # we need to perform a dummy request to parse the authenticity_token params = { @@ -327,8 +338,10 @@ def delete_deck(self, deck_id): @staticmethod def to_playable_deck( listed_deck: AbstractArchivedDeck, - decks_cache_dir=AcademyFileSystem().ASSETS_DATA_DECK_MTGGOLDFISH_TOURNAMENT_DIR, - use_cache=True, + decks_cache_dir: ( + str | None + ) = AcademyFileSystem().ASSETS_DATA_DECK_MTGGOLDFISH_TOURNAMENT_DIR, + use_cache: bool = True, ) -> PlayableDeck: # TODO: fix ASSETS_DATA_DECK_MTGGOLDFISH_TOURNAMENT_DIR lines = None diff --git a/pauperformance_bot/service/pauperformance/async_pauperformance.py b/pauperformance_bot/service/pauperformance/async_pauperformance.py index 9a3d1f00..6e99e3a5 100644 --- a/pauperformance_bot/service/pauperformance/async_pauperformance.py +++ b/pauperformance_bot/service/pauperformance/async_pauperformance.py @@ -1,3 +1,5 @@ +from typing import Any + from pauperformance_bot.constant.mtg.mtggoldfish import DECK_API_ENDPOINT from pauperformance_bot.constant.pauperformance.nexus import ( DISCORD_MAX_HISTORY_LIMIT, @@ -13,10 +15,16 @@ from pauperformance_bot.service.nexus.sync.messages_sender import ( DiscordMessagesSenderSyncService, ) +from pauperformance_bot.service.pauperformance.archive.abstract import ( + AbstractArchiveService, +) from pauperformance_bot.service.pauperformance.config_reader import ConfigReader from pauperformance_bot.service.pauperformance.pauperformance import ( PauperformanceService, ) +from pauperformance_bot.service.pauperformance.storage.abstract import ( + AbstractStorageService, +) from pauperformance_bot.util.log import get_application_logger logger = get_application_logger() @@ -25,14 +33,14 @@ class AsyncPauperformanceService(PauperformanceService): def __init__( self, - discord, - storage, - archive, - scryfall=ScryfallService(), - twitch=TwitchService(), - youtube=YouTubeService(), - config_reader=ConfigReader(), - ): + discord: AsyncDiscordService, + storage: AbstractStorageService, + archive: AbstractArchiveService, + scryfall: ScryfallService = ScryfallService(), + twitch: TwitchService = TwitchService(), + youtube: YouTubeService = YouTubeService(), + config_reader: ConfigReader = ConfigReader(), + ) -> None: super().__init__( storage, archive, @@ -43,7 +51,7 @@ def __init__( ) self.discord: AsyncDiscordService = discord - async def import_decks_from_deckstats(self, send_notification=True): + async def import_decks_from_deckstats(self, send_notification: bool = True) -> None: logger.info("Updating Archive decks for all users...") players_by_deckstats_id = { int(p.deckstats_id): p for p in self.players if p.deckstats_id @@ -67,7 +75,9 @@ async def import_decks_from_deckstats(self, send_notification=True): ) logger.info("Updated Archive decks for all users.") - async def import_players_videos_from_twitch(self, send_notification=True): + async def import_players_videos_from_twitch( + self, send_notification: bool = True + ) -> None: logger.info("Updating Twitch videos for all users...") for player in self.players: if not player.twitch_login_name: @@ -80,7 +90,9 @@ async def import_players_videos_from_twitch(self, send_notification=True): ) logger.info("Updated Twitch videos for all users.") - async def import_player_videos_from_twitch(self, player, send_notification=True): + async def import_player_videos_from_twitch( + self, player: PhDConfig, send_notification: bool = True + ) -> None: logger.info(f"Processing videos from Twitch user {player.twitch_login_name}...") twitch_user = self.twitch.get_user(player.twitch_login_name) warning_player: PhDConfig = self.config_reader.get_pauperformance_phd() @@ -94,7 +106,9 @@ async def import_player_videos_from_twitch(self, player, send_notification=True) ) logger.info(f"Processed videos from Twitch user {player.twitch_login_name}.") - async def import_players_videos_from_youtube(self, send_notification=True): + async def import_players_videos_from_youtube( + self, send_notification: bool = True + ) -> None: logger.info("Updating YouTube videos for all users...") for player in self.players: if not player.youtube_channel_id: @@ -107,7 +121,9 @@ async def import_players_videos_from_youtube(self, send_notification=True): ) logger.info("Updated YouTube videos for all users.") - async def import_player_videos_from_youtube(self, player, send_notification=True): + async def import_player_videos_from_youtube( + self, player: PhDConfig, send_notification: bool = True + ) -> None: logger.info( f"Processing videos from YouTube user " f"{player.youtube_channel_id}..." ) @@ -125,7 +141,7 @@ async def import_player_videos_from_youtube(self, player, send_notification=True ) logger.info(f"Processed videos from YouTube user {player.youtube_channel_id}.") - async def import_decks_from_discord(self, send_notification=True): + async def import_decks_from_discord(self, send_notification: bool = True) -> None: import_deck_channel_id = self.discord.import_deck_channel_id logger.info(f"Importing new decks from channel {import_deck_channel_id}...") import_deck_channel = self.discord.get_channel(import_deck_channel_id) @@ -136,7 +152,9 @@ async def import_decks_from_discord(self, send_notification=True): await self._process_discord_import_deck_message(message, send_notification) logger.info(f"Imported new decks from channel {import_deck_channel_id}.") - async def _process_discord_import_deck_message(self, message, send_notification): + async def _process_discord_import_deck_message( + self, message: Any, send_notification: bool + ) -> None: logger.debug( f"Processing message {message.id} by {message.author.id} " f"({message.author.name})..." @@ -171,8 +189,8 @@ async def _process_discord_import_deck_message(self, message, send_notification) ) async def _try_import_mtggoldfish_deck_from_discord( - self, message, send_notification - ): + self, message: Any, send_notification: bool + ) -> None: url = message.content.strip() if "#" in url: url = url[: url.index("#")] diff --git a/pauperformance_bot/service/pauperformance/checker.py b/pauperformance_bot/service/pauperformance/checker.py index 2e7ec3f4..0a094280 100644 --- a/pauperformance_bot/service/pauperformance/checker.py +++ b/pauperformance_bot/service/pauperformance/checker.py @@ -26,14 +26,14 @@ def __init__( self, pauperformance: PauperformanceService, academy_fs: AcademyFileSystem = AcademyFileSystem(), - ): + ) -> None: self.pauperformance: PauperformanceService = pauperformance self.archetypes: list[ArchetypeConfig] = ( self.pauperformance.config_reader.list_archetypes() ) self.academy_fs: AcademyFileSystem = academy_fs - def check_all(self): + def check_all(self) -> bool: return all( ( self.check_archetypes_rules_for_archived_decks(), @@ -100,7 +100,7 @@ def check_archetypes_rules_for_classified_decks(self) -> bool: # print("\n".join(rm_cmds)) return True - def check_mtg_tournament_decks_are_downloaded(self): + def check_mtg_tournament_decks_are_downloaded(self) -> bool: fs = self.academy_fs goldfish_tournaments_dir = fs.ASSETS_DATA_TOURNAMENT_MTGGOLDFISH_DIR json_goldfish_decks_dir = fs.ASSETS_DATA_TOURNAMENT_MTGGOLDFISH_DECKS_DIR diff --git a/pauperformance_bot/service/pauperformance/config_reader.py b/pauperformance_bot/service/pauperformance/config_reader.py index e6d399de..ea44670b 100644 --- a/pauperformance_bot/service/pauperformance/config_reader.py +++ b/pauperformance_bot/service/pauperformance/config_reader.py @@ -1,6 +1,7 @@ import configparser import glob from itertools import count +from typing import Any from pauperformance_bot.constant.flags import get_language_flag from pauperformance_bot.constant.pauperformance.myr import MyrFileSystem @@ -26,11 +27,11 @@ @auto_repr @auto_str class ConfigReader: - def __init__(self, myr_file_system: MyrFileSystem = MyrFileSystem()): + def __init__(self, myr_file_system: MyrFileSystem = MyrFileSystem()) -> None: self.myr_file_system: MyrFileSystem = myr_file_system @staticmethod - def _read_config_file(config_file_path): + def _read_config_file(config_file_path: str) -> configparser.ConfigParser: logger.debug(f"Reading configuration file {config_file_path}...") config = configparser.ConfigParser(allow_no_value=True) config.optionxform = lambda option: option # preserve case @@ -39,12 +40,14 @@ def _read_config_file(config_file_path): return config @staticmethod - def _parse_list_value(raw_value) -> list[str]: + def _parse_list_value(raw_value: str) -> list[str]: return [value.strip(" ") for value in raw_value.split(",")] if raw_value else [] @staticmethod - def _read_sequential_resources(config, key): - resources = [] + def _read_sequential_resources( + config: configparser.ConfigParser, key: str + ) -> list[dict[str, Any]]: + resources: list[dict[str, Any]] = [] for i in count(1): if f"{key}{i}" in config: resources.append( @@ -301,7 +304,7 @@ def get_changelog(self) -> Changelog: logger.info(f"Read Changelog from {config_file_path}.") return changelog - def get_archetype_name_from_alias(self, name): + def get_archetype_name_from_alias(self, name: str) -> str: for archetype in self.list_archetypes(): if archetype.name == name: return name diff --git a/pauperformance_bot/service/pauperformance/pauperformance.py b/pauperformance_bot/service/pauperformance/pauperformance.py index e75f5cf4..1c9b6bb6 100644 --- a/pauperformance_bot/service/pauperformance/pauperformance.py +++ b/pauperformance_bot/service/pauperformance/pauperformance.py @@ -5,7 +5,7 @@ from datetime import datetime from pathlib import Path from time import sleep -from typing import Any, Dict, List +from typing import Any from requests.exceptions import HTTPError @@ -52,13 +52,13 @@ class PauperformanceService: def __init__( self, - storage, - archive, - scryfall=ScryfallService(), - twitch=TwitchService(), - youtube=YouTubeService(), - config_reader=ConfigReader(), - ): + storage: AbstractStorageService, + archive: AbstractArchiveService, + scryfall: ScryfallService = ScryfallService(), + twitch: TwitchService = TwitchService(), + youtube: YouTubeService = YouTubeService(), + config_reader: ConfigReader = ConfigReader(), + ) -> None: self.storage: AbstractStorageService = storage self.archive: AbstractArchiveService = archive self.scryfall = scryfall @@ -70,7 +70,9 @@ def __init__( self.card_index = self._build_card_index() self.incremental_card_index = self._build_incremental_card_index() - def _build_set_index(self, set_index_file=SET_INDEX_FILE): + def _build_set_index( + self, set_index_file: str = SET_INDEX_FILE + ) -> collections.OrderedDict[int, dict[str, Any]]: try: logger.info("Building Scryfall set index...") scryfall_sets = self.scryfall.get_sets() @@ -119,9 +121,9 @@ def _build_set_index(self, set_index_file=SET_INDEX_FILE): def _build_card_index( self, - skip_sets=KNOWN_SETS_WITH_NO_PAUPER_CARDS, - cards_index_cache_dir=PAUPER_CARDS_INDEX_CACHE_DIR, - ) -> Dict[str, List[Dict[str, Any]]]: + skip_sets: list[int] = KNOWN_SETS_WITH_NO_PAUPER_CARDS, + cards_index_cache_dir: str = PAUPER_CARDS_INDEX_CACHE_DIR, + ) -> dict[int, list[dict[str, Any]]]: card_index = {} os.makedirs(cards_index_cache_dir, exist_ok=True) for item in self.set_index.values(): @@ -171,8 +173,8 @@ def _build_card_index( def _build_incremental_card_index( self, - skip_sets=INCREMENTAL_CARDS_INDEX_SKIP_SETS, - ): + skip_sets: list[int] = INCREMENTAL_CARDS_INDEX_SKIP_SETS, + ) -> dict[int, list[dict[str, Any]]]: incremental_card_index = {} existing_card_names = set() useless_sets = set() @@ -211,7 +213,7 @@ def _build_incremental_card_index( ) return incremental_card_index - def list_deckstats_decks(self): + def list_deckstats_decks(self) -> list[Any]: all_decks = [] for player in self.players: if not player.deckstats_id: @@ -238,20 +240,22 @@ def list_archived_decks(self) -> list[AbstractArchivedDeck]: return self.archive.list_decks() @staticmethod - def get_archetypes(config_pages_dir=CONFIG_ARCHETYPES_DIR): + def get_archetypes(config_pages_dir: str = CONFIG_ARCHETYPES_DIR) -> set[str]: return set( Path(a).name.replace(".ini", "") for a in glob.glob(f"{config_pages_dir}/*.ini") ) @staticmethod - def get_families(config_pages_dir=CONFIG_FAMILIES_DIR): + def get_families(config_pages_dir: str = CONFIG_FAMILIES_DIR) -> set[str]: return set( Path(a).name.replace(".ini", "") for a in glob.glob(f"{config_pages_dir}/*.ini") ) - def analyze_cards_frequency(self, archetype_decks): + def analyze_cards_frequency( + self, archetype_decks: list[AbstractArchivedDeck] + ) -> tuple[list[str], list[str]]: # Note: this method uses archived decks to compute frequent and staples. # An alternative approach is to use a larger pool of classified decks, # by leveraging silver.deckstatistics. @@ -272,7 +276,7 @@ def analyze_cards_frequency(self, archetype_decks): frequents = all_cards - staples - lands return list(staples), list(frequents) - def get_set_index_by_date(self, usa_date): + def get_set_index_by_date(self, usa_date: str) -> dict[str, Any]: logger.debug(f"Getting set index for USA date {usa_date}") return [ s @@ -281,10 +285,10 @@ def get_set_index_by_date(self, usa_date): and len(self.incremental_card_index.get(s["p12e_code"])) > 0 ][-1] - def get_current_set_index(self): + def get_current_set_index(self) -> dict[str, Any]: return self.get_set_index_by_date(datetime.today().strftime(USA_DATE_FORMAT)) - def delete_deck(self, deck_name): + def delete_deck(self, deck_name: str) -> None: # a deck needs to be deleted both from the archive and from the storage archived_deck_id = None for deck in self.list_archived_decks(): @@ -308,7 +312,7 @@ def delete_deck(self, deck_name): discord_logger = DiscordMessagesSenderSyncService([message]) discord_logger.run_task() - def _list_twitch_videos(self): + def _list_twitch_videos(self) -> list[AcademyVideo]: logger.debug("Retrieving stored Twitch videos...") academy_videos = [] for video in self.storage.list_imported_twitch_videos(): @@ -335,7 +339,7 @@ def _list_twitch_videos(self): logger.debug("Retrieved stored Twitch videos.") return academy_videos - def _list_youtube_videos(self): + def _list_youtube_videos(self) -> list[AcademyVideo]: logger.debug("Retrieving stored YouTube videos...") academy_videos = [] for video in self.storage.list_imported_youtube_videos(): @@ -367,8 +371,8 @@ def list_videos(self) -> list[AcademyVideo]: def print_stats( self, - archetypes_config_dir=CONFIG_ARCHETYPES_DIR, - ): + archetypes_config_dir: str = CONFIG_ARCHETYPES_DIR, + ) -> None: print(f"PhDs: {len(self.players) - 1}") print(f"Archetypes: {len(self.get_archetypes())}") print(f"Families: {len(self.get_families())}") diff --git a/pauperformance_bot/service/pauperformance/silver.py b/pauperformance_bot/service/pauperformance/silver.py index 66059b61..8c172518 100644 --- a/pauperformance_bot/service/pauperformance/silver.py +++ b/pauperformance_bot/service/pauperformance/silver.py @@ -3,7 +3,6 @@ import os import time from collections import defaultdict -from typing import DefaultDict, Tuple from scipy import spatial @@ -40,8 +39,8 @@ class SilverService: def __init__( self, pauperformance: PauperformanceService, - known_decks: list[tuple[PlayableDeck, ArchetypeConfig]] = None, - ): + known_decks: list[tuple[PlayableDeck, ArchetypeConfig]] | None = None, + ) -> None: self.pauperformance: PauperformanceService = pauperformance self.archetypes: list[ArchetypeConfig] = ( self.pauperformance.config_reader.list_archetypes() @@ -51,17 +50,21 @@ def __init__( ) self._decks_cache: dict[str, PlayableDeck] = {} - def add_known_decks(self, known_decks: list[tuple[PlayableDeck, ArchetypeConfig]]): + def add_known_decks( + self, known_decks: list[tuple[PlayableDeck, ArchetypeConfig]] + ) -> None: self.known_decks += known_decks @staticmethod - def _cosine_similarity(v1, v2, w=1.0): + def _cosine_similarity(v1: list[float], v2: list[float], w: float = 1.0) -> float: if w == 0: return 1 return 1 - spatial.distance.cosine(v1, v2, w=len(v1) * [w]) @staticmethod - def _vectorize(cards_map1, cards_map2): + def _vectorize( + cards_map1: dict[str, int], cards_map2: dict[str, int] + ) -> tuple[list[int], list[int]]: # In Pauper, only few decks truly take advantage of Snow-Covered lands. # However, Snow-Covered lands are often used instead of basics for no reason. # For better similarity results, we treat Snow-Covered lands as basics. @@ -224,7 +227,7 @@ def _is_azorius_prowess(self, deck: PlayableDeck) -> bool: def classify_deck( self, deck: PlayableDeck, - ) -> Tuple[ArchetypeConfig, float]: + ) -> tuple[ArchetypeConfig, float]: logger.debug("Classifying deck...") most_similar_archetype, highest_similarity = None, 0 @@ -282,7 +285,7 @@ def classify_deck( def get_metagame(self) -> Metagame: mtggoldfish = MTGGoldfish() mtggoldfish_meta = mtggoldfish.get_pauper_meta() - meta_shares: DefaultDict[str, list[MetaShare]] = defaultdict(list) + meta_shares: defaultdict[str, list[MetaShare]] = defaultdict(list) for link, values in mtggoldfish_meta.items(): share, playable_deck = values similar_archetype, similarity_score = self.classify_deck(playable_deck) @@ -318,7 +321,7 @@ def get_metagame(self) -> Metagame: ) return Metagame(meta_shares=grouped_meta_shares) - def get_dpl_metagame(self, leg_file, output_file): + def get_dpl_metagame(self, leg_file: str, output_file: str) -> None: logger.info(f"Getting Dutch Pauper League meta from {leg_file}...") dpl_decks = [] diff --git a/pauperformance_bot/service/pauperformance/silver/decklassifier.py b/pauperformance_bot/service/pauperformance/silver/decklassifier.py index 6c5daad6..bd4f23c3 100644 --- a/pauperformance_bot/service/pauperformance/silver/decklassifier.py +++ b/pauperformance_bot/service/pauperformance/silver/decklassifier.py @@ -1,6 +1,6 @@ import itertools from collections import defaultdict -from typing import DefaultDict, Tuple +from typing import Any from scipy import spatial @@ -34,8 +34,8 @@ class Decklassifier: def __init__( self, pauperformance: PauperformanceService, - known_decks: list[tuple[PlayableDeck, ArchetypeConfig]] = None, - ): + known_decks: list[tuple[PlayableDeck, ArchetypeConfig]] | None = None, + ) -> None: self.pauperformance: PauperformanceService = pauperformance self.archetypes: list[ArchetypeConfig] = ( self.pauperformance.config_reader.list_archetypes() @@ -45,17 +45,21 @@ def __init__( ) self._decks_cache: dict[str, PlayableDeck] = {} - def add_known_decks(self, known_decks: list[tuple[PlayableDeck, ArchetypeConfig]]): + def add_known_decks( + self, known_decks: list[tuple[PlayableDeck, ArchetypeConfig]] + ) -> None: self.known_decks += known_decks @staticmethod - def _cosine_similarity(v1, v2, w=1.0): + def _cosine_similarity(v1: list[float], v2: list[float], w: float = 1.0) -> float: if w == 0: return 1 return 1 - spatial.distance.cosine(v1, v2, w=len(v1) * [w]) @staticmethod - def _vectorize(cards_map1, cards_map2): + def _vectorize( + cards_map1: dict[str, int], cards_map2: dict[str, int] + ) -> tuple[list[int], list[int]]: # In Pauper, only few decks take advantage of Snow-Covered lands. # However, Snow-Covered lands are often used. # For better similarity results, we want Snow-Covered lands to be treated as @@ -219,7 +223,7 @@ def _is_azorius_prowess(self, deck: PlayableDeck) -> bool: def classify_deck( self, deck: PlayableDeck, - ) -> Tuple[ArchetypeConfig, float]: + ) -> tuple[ArchetypeConfig, float]: logger.debug("Classifying deck...") most_similar_archetype, highest_similarity = None, 0 @@ -277,7 +281,7 @@ def classify_deck( def get_metagame(self) -> Metagame: mtggoldfish = MTGGoldfish() mtggoldfish_meta = mtggoldfish.get_pauper_meta() - meta_shares: DefaultDict[str, list[MetaShare]] = defaultdict(list) + meta_shares: defaultdict[str, list[MetaShare]] = defaultdict(list) for link, values in mtggoldfish_meta.items(): share, playable_deck = values similar_archetype, similarity_score = self.classify_deck(playable_deck) @@ -313,7 +317,7 @@ def get_metagame(self) -> Metagame: ) return Metagame(meta_shares=grouped_meta_shares) - def parse_dpl_deck(self, deck): + def parse_dpl_deck(self, deck: dict[str, Any]) -> tuple[str, PlayableDeck]: deck_id = deck["id"] lines = [f"{pc['quantity']} {pc['name']}" for pc in deck["cards"]["mainboard"]] lines += [""] @@ -326,11 +330,11 @@ def parse_dpl_deck(self, deck): def get_dpl_metagame( self, - decks, - name="DPL metagame", - brew_threshold=BREW_CLASSIFICATION_THRESHOLD, - learn_on_the_fly=True, - ): + decks: list[dict[str, Any]], + name: str = "DPL metagame", + brew_threshold: float = BREW_CLASSIFICATION_THRESHOLD, + learn_on_the_fly: bool = True, + ) -> DPLMeta: dpl_decks = [] for deck in decks: deck_id, playable_deck = self.parse_dpl_deck(deck) diff --git a/pauperformance_bot/service/pauperformance/silver/deckstatistics.py b/pauperformance_bot/service/pauperformance/silver/deckstatistics.py index f9a2d40a..2bb3500a 100644 --- a/pauperformance_bot/service/pauperformance/silver/deckstatistics.py +++ b/pauperformance_bot/service/pauperformance/silver/deckstatistics.py @@ -1,5 +1,3 @@ -from typing import Dict, List, Tuple - from pauperformance_bot.entity.deck.playable import PlayableDeck from pauperformance_bot.exceptions import CardNotFoundException from pauperformance_bot.service.academy.data_loader import AcademyDataLoader @@ -15,13 +13,13 @@ class Deckstatistics: def __init__( self, name: str, - playable_decks: List[PlayableDeck], - cards: Dict[str, Dict[str, int]], - ): + playable_decks: list[PlayableDeck], + cards: dict[str, dict[str, int]], + ) -> None: self.name = name self._playable_decks = playable_decks self._cards = cards - self.most_played_card = None + self.most_played_card: str | None = None total = 0 most_played = 0 @@ -38,12 +36,12 @@ def nr_decks(self) -> int: """Returns the total amount of known decks for the archetype.""" return len(self._playable_decks) - def cards(self) -> List[str]: + def cards(self) -> list[str]: """Returns the list of all card names which have been played (either main or side) for the archetype.""" return list(self._cards.keys()) - def deck_occurrences_ratio(self) -> List[Tuple[str, float]]: + def deck_occurrences_ratio(self) -> list[tuple[str, float]]: """Returns a list of all the cards ever played for this archetype and their ratio of decks containing the card over the total count of decks.""" return sorted( @@ -57,7 +55,7 @@ def deck_occurrences_ratio(self) -> List[Tuple[str, float]]: reverse=True, ) - def card_playing_rate(self) -> List[Tuple[str, float]]: + def card_playing_rate(self) -> list[tuple[str, float]]: """Returns a list of all cards ever played for this archetype and the corresponding ratio of total quantity and the total nr. of cards in all decks.""" @@ -75,7 +73,7 @@ def card_playing_rate(self) -> List[Tuple[str, float]]: reverse=True, ) - def cards_breakdown(self) -> Dict[str, List[int]]: + def cards_breakdown(self) -> dict[str, list[float]]: """Returns a list of all cards ever played for this archetype and the relative list of rate of observations of decks in which this card is played in either 1, 2, 3, or 4 copies.""" @@ -91,14 +89,14 @@ def cards_breakdown(self) -> Dict[str, List[int]]: ) ) - def get_cards_above_frequency(self, threshold: float): + def get_cards_above_frequency(self, threshold: float) -> list[tuple[str, float]]: return sorted( list(filter(lambda f: f[1] >= threshold, self.deck_occurrences_ratio())) ) def get_staple_and_frequent_cards( - self, staple_threshold=0.9, frequent_threshold=0.7 - ): + self, staple_threshold: float = 0.9, frequent_threshold: float = 0.7 + ) -> tuple[list[str], list[str]]: staple_cards = set( c[0] for c in self.get_cards_above_frequency(staple_threshold) ) @@ -109,7 +107,9 @@ def get_staple_and_frequent_cards( class DeckstatisticsFactory: - def __init__(self, scryfall: ScryfallService, academy_loader: AcademyDataLoader): + def __init__( + self, scryfall: ScryfallService, academy_loader: AcademyDataLoader + ) -> None: self._scryfall: ScryfallService = scryfall self._academy_loader = academy_loader diff --git a/pauperformance_bot/service/pauperformance/silver/deckstructor.py b/pauperformance_bot/service/pauperformance/silver/deckstructor.py index 244aab68..8326c968 100644 --- a/pauperformance_bot/service/pauperformance/silver/deckstructor.py +++ b/pauperformance_bot/service/pauperformance/silver/deckstructor.py @@ -9,7 +9,9 @@ class Deckstructor: @staticmethod - def __merge_deck_sideboard(deck, sideboard): + def __merge_deck_sideboard( + deck: dict[str, int], sideboard: dict[str, int] + ) -> dict[str, int]: cards = deck.copy() for key, value in sideboard.items(): @@ -21,13 +23,13 @@ def __merge_deck_sideboard(deck, sideboard): return cards @staticmethod - def __validate_pool(cards, pool): + def __validate_pool(cards: dict[str, int], pool: set[str]) -> None: for card in cards: if card not in pool: logger.warning(f"{card} not in pool") @staticmethod - def extract_deck(text, pool) -> PlayableDeck: + def extract_deck(text: str, pool: set[str]) -> PlayableDeck: # TODO: autogenerate pool from Scryfall with all pauper cards (legal and banned) mainboard = {} sideboard = {} diff --git a/pauperformance_bot/service/pauperformance/storage/abstract.py b/pauperformance_bot/service/pauperformance/storage/abstract.py index 30c26933..f287c67a 100644 --- a/pauperformance_bot/service/pauperformance/storage/abstract.py +++ b/pauperformance_bot/service/pauperformance/storage/abstract.py @@ -1,4 +1,5 @@ from abc import ABCMeta, abstractmethod +from typing import Any from pauperformance_bot.constant.pauperformance.myr import ( STORAGE_DECKS_SUBDIR, @@ -13,94 +14,100 @@ class AbstractStorageService(metaclass=ABCMeta): @property @abstractmethod - def _root(self): + def _root(self) -> str: pass @property @abstractmethod - def _dir_separator(self): + def _dir_separator(self) -> str: pass @property - def decks_path(self, decks_subdir=STORAGE_DECKS_SUBDIR): + def decks_path(self, decks_subdir: str = STORAGE_DECKS_SUBDIR) -> str: return f"{self._root}{self._dir_separator}{decks_subdir}" @property - def deckstats_deck_path(self, deckstats_subdir=STORAGE_DECKSTATS_DECKS_SUBDIR): + def deckstats_deck_path( + self, deckstats_subdir: str = STORAGE_DECKSTATS_DECKS_SUBDIR + ) -> str: return f"{self.decks_path}{self._dir_separator}{deckstats_subdir}" @property def mtggoldfish_deck_path( - self, mtggoldfish_subdir=STORAGE_MTGGOLDFISH_DECKS_SUBDIR - ): + self, mtggoldfish_subdir: str = STORAGE_MTGGOLDFISH_DECKS_SUBDIR + ) -> str: return f"{self.decks_path}{self._dir_separator}{mtggoldfish_subdir}" @property - def videos_path(self, videos_subdir=STORAGE_VIDEOS_SUBDIR): + def videos_path(self, videos_subdir: str = STORAGE_VIDEOS_SUBDIR) -> str: return f"{self._root}{self._dir_separator}{videos_subdir}" @property - def twitch_video_path(self, twitch_subdir=STORAGE_TWITCH_VIDEOS_SUBDIR): + def twitch_video_path( + self, twitch_subdir: str = STORAGE_TWITCH_VIDEOS_SUBDIR + ) -> str: return f"{self.videos_path}{self._dir_separator}{twitch_subdir}" @property - def youtube_video_path(self, youtube_subdir=STORAGE_YOUTUBE_VIDEOS_SUBDIR): + def youtube_video_path( + self, youtube_subdir: str = STORAGE_YOUTUBE_VIDEOS_SUBDIR + ) -> str: return f"{self.videos_path}{self._dir_separator}{youtube_subdir}" @abstractmethod - def _list_files(self, path, cursor=None): + def _list_files(self, path: str, cursor: Any = None) -> list[Any]: pass @abstractmethod - def create_file(self, name, content=""): + def create_file(self, name: str, content: str = "") -> None: pass @abstractmethod - def get_file(self, name): + def get_file(self, name: str) -> Any: pass @abstractmethod - def list_imported_deckstats_deck_ids(self): + def list_imported_deckstats_deck_ids(self) -> set[str]: pass @abstractmethod - def list_imported_deckstats_deck_names(self): + def list_imported_deckstats_deck_names(self) -> set[str]: pass @abstractmethod - def list_imported_mtggoldfish_deck_ids(self): + def list_imported_mtggoldfish_deck_ids(self) -> set[str]: pass @abstractmethod - def list_imported_mtggoldfish_deck_names(self): + def list_imported_mtggoldfish_deck_names(self) -> set[str]: pass @abstractmethod - def list_imported_twitch_videos(self): + def list_imported_twitch_videos(self) -> set[str]: pass @abstractmethod - def list_imported_twitch_videos_ids(self): + def list_imported_twitch_videos_ids(self) -> set[str]: pass @abstractmethod - def list_imported_youtube_videos(self): + def list_imported_youtube_videos(self) -> set[str]: pass @abstractmethod - def list_imported_youtube_videos_ids(self): + def list_imported_youtube_videos_ids(self) -> set[str]: pass @abstractmethod - def delete_deck_by_name(self, deck_name): + def delete_deck_by_name(self, deck_name: str) -> None: pass def get_imported_deckstats_deck_key( self, - deckstats_deck_saved_id, - mtggoldfish_deck_id, - deck_name, - ): + deckstats_deck_saved_id: str, + mtggoldfish_deck_id: str, + deck_name: str, + ) -> str: return ( f"{self.deckstats_deck_path}" f"{self._dir_separator}" @@ -111,10 +118,10 @@ def get_imported_deckstats_deck_key( def get_imported_mtggoldfish_deck_key( self, - mtggoldfish_original_deck_id, - mtggoldfish_archived_deck_id, - deck_name, - ): + mtggoldfish_original_deck_id: str, + mtggoldfish_archived_deck_id: str, + deck_name: str, + ) -> str: return ( f"{self.mtggoldfish_deck_path}" f"{self._dir_separator}" @@ -125,12 +132,12 @@ def get_imported_mtggoldfish_deck_key( def get_imported_twitch_video_key( self, - video_id, - user_login_name, - language, - date, - deck_name, - ): + video_id: str, + user_login_name: str, + language: str, + date: str, + deck_name: str, + ) -> str: return ( f"{self.twitch_video_path}" f"{self._dir_separator}" @@ -143,12 +150,12 @@ def get_imported_twitch_video_key( def get_imported_youtube_video_key( self, - video_id, - channel_title, - language, - date, - deck_name, - ): + video_id: str, + channel_title: str, + language: str, + date: str, + deck_name: str, + ) -> str: return ( f"{self.youtube_video_path}" f"{self._dir_separator}" @@ -160,33 +167,33 @@ def get_imported_youtube_video_key( ) @staticmethod - def get_imported_deckstats_deck_id_from_key(key): + def get_imported_deckstats_deck_id_from_key(key: str) -> str: return key.rsplit("/", maxsplit=1)[1].split(">")[0] @staticmethod - def get_imported_deckstats_deck_name_from_key(key): + def get_imported_deckstats_deck_name_from_key(key: str) -> str: return key.rsplit("/", maxsplit=1)[1].split(">")[2][:-4] # drop .txt @staticmethod - def get_imported_mtggoldfish_deck_id_from_key(key): + def get_imported_mtggoldfish_deck_id_from_key(key: str) -> str: return key.rsplit("/", maxsplit=1)[1].split(">")[0] @staticmethod - def get_imported_mtggoldfish_deck_name_from_key(key): + def get_imported_mtggoldfish_deck_name_from_key(key: str) -> str: return key.rsplit("/", maxsplit=1)[1].split(">")[2][:-4] # drop .txt @staticmethod - def get_imported_twitch_video_id_from_key(key): + def get_imported_twitch_video_id_from_key(key: str) -> str: return key.rsplit("/", maxsplit=1)[1].split(">")[0] @staticmethod - def get_imported_twitch_video(key): + def get_imported_twitch_video(key: str) -> str: return key.rsplit("/", maxsplit=1)[1][:-4] # drop .txt @staticmethod - def get_imported_youtube_video_id_from_key(key): + def get_imported_youtube_video_id_from_key(key: str) -> str: return key.rsplit("/", maxsplit=1)[1].split(">")[0] @staticmethod - def get_imported_youtube_video(key): + def get_imported_youtube_video(key: str) -> str: return key.rsplit("/", maxsplit=1)[1][:-4] # drop .txt diff --git a/pauperformance_bot/service/pauperformance/storage/dropbox_.py b/pauperformance_bot/service/pauperformance/storage/dropbox_.py index 7669c59d..ae614c33 100644 --- a/pauperformance_bot/service/pauperformance/storage/dropbox_.py +++ b/pauperformance_bot/service/pauperformance/storage/dropbox_.py @@ -1,5 +1,6 @@ import json import tempfile +from typing import Any from dropbox import Dropbox as OfficialDropbox from dropbox import DropboxOAuth2FlowNoRedirect @@ -22,11 +23,11 @@ class DropboxService(AbstractStorageService): def __init__( self, - root_dir=MYR_ROOT_DIR, - refresh_token=DROPBOX_REFRESH_TOKEN, - app_key=DROPBOX_APP_KEY, - app_secret=DROPBOX_APP_SECRET, - ): + root_dir: str = MYR_ROOT_DIR, + refresh_token: str = DROPBOX_REFRESH_TOKEN, + app_key: str = DROPBOX_APP_KEY, + app_secret: str = DROPBOX_APP_SECRET, + ) -> None: self._root_dir = root_dir self.app_key = app_key self.app_secret = app_secret @@ -37,18 +38,18 @@ def __init__( ) @property - def _root(self): + def _root(self) -> str: return self._root_dir @property - def _dir_separator(self): + def _dir_separator(self) -> str: return "/" @property - def service(self): + def service(self) -> OfficialDropbox: return self._service - def _list_files(self, path, cursor=None): + def _list_files(self, path: str, cursor: str | None = None) -> list[Any]: if not cursor: results = self._service.files_list_folder(path) else: @@ -58,12 +59,12 @@ def _list_files(self, path, cursor=None): return items return items + self._list_files(path, results.cursor) - def create_file(self, name, content=""): + def create_file(self, name: str, content: str = "") -> None: logger.info(f"Storing file {name}...") results = self._service.files_upload(content.encode("utf-8"), name, mute=True) logger.info(f"Stored file {name}: {results}") - def get_file(self, name): + def get_file(self, name: str) -> Any: logger.info(f"Downloading file {name}...") with tempfile.NamedTemporaryFile("w+") as out_f: self._service.files_download_to_file(out_f.name, name) @@ -71,55 +72,55 @@ def get_file(self, name): logger.info(f"Downloaded file {name}.") return json.loads(content) - def list_imported_deckstats_deck_ids(self): + def list_imported_deckstats_deck_ids(self) -> set[str]: return set( self.get_imported_deckstats_deck_id_from_key(file.path_display) for file in self._list_files(self.deckstats_deck_path) ) - def list_imported_deckstats_deck_names(self): + def list_imported_deckstats_deck_names(self) -> set[str]: return set( self.get_imported_deckstats_deck_name_from_key(file.path_display) for file in self._list_files(self.deckstats_deck_path) ) - def list_imported_mtggoldfish_deck_ids(self): + def list_imported_mtggoldfish_deck_ids(self) -> set[str]: return set( self.get_imported_mtggoldfish_deck_id_from_key(file.path_display) for file in self._list_files(self.mtggoldfish_deck_path) ) - def list_imported_mtggoldfish_deck_names(self): + def list_imported_mtggoldfish_deck_names(self) -> set[str]: return set( self.get_imported_mtggoldfish_deck_name_from_key(file.path_display) for file in self._list_files(self.mtggoldfish_deck_path) ) - def list_imported_twitch_videos(self): + def list_imported_twitch_videos(self) -> set[str]: return set( self.get_imported_twitch_video(file.path_display) for file in self._list_files(self.twitch_video_path) ) - def list_imported_twitch_videos_ids(self): + def list_imported_twitch_videos_ids(self) -> set[str]: return set( self.get_imported_twitch_video_id_from_key(file.path_display) for file in self._list_files(self.twitch_video_path) ) - def list_imported_youtube_videos(self): + def list_imported_youtube_videos(self) -> set[str]: return set( self.get_imported_youtube_video(file.path_display) for file in self._list_files(self.youtube_video_path) ) - def list_imported_youtube_videos_ids(self): + def list_imported_youtube_videos_ids(self) -> set[str]: return set( self.get_imported_youtube_video_id_from_key(file.path_display) for file in self._list_files(self.youtube_video_path) ) - def delete_deck_by_name(self, deck_name): + def delete_deck_by_name(self, deck_name: str) -> None: logger.info(f"Deleting file containing {deck_name}...") file_path = None for file in self._list_files(self.deckstats_deck_path): @@ -132,7 +133,7 @@ def delete_deck_by_name(self, deck_name): self._service.files_delete_v2(file_path) logger.info(f"Deleted file containing {deck_name}.") - def _oauth_interactive_flow(self): + def _oauth_interactive_flow(self) -> None: auth_flow = DropboxOAuth2FlowNoRedirect( self.app_key, use_pkce=True, token_access_type="offline" ) diff --git a/pauperformance_bot/service/pauperformance/storage/local.py b/pauperformance_bot/service/pauperformance/storage/local.py index 593170ea..d57490f7 100644 --- a/pauperformance_bot/service/pauperformance/storage/local.py +++ b/pauperformance_bot/service/pauperformance/storage/local.py @@ -1,6 +1,7 @@ import json from os import listdir, remove from os.path import isfile, join, sep +from typing import Any from pauperformance_bot.constant.pauperformance.myr import STORAGE_DIR from pauperformance_bot.exceptions import StoredFileNotFound @@ -13,82 +14,82 @@ class LocalStorageService(AbstractStorageService): - def __init__(self, root_dir=STORAGE_DIR): + def __init__(self, root_dir: str = STORAGE_DIR) -> None: self._root_dir = root_dir @property - def _root(self): + def _root(self) -> str: return self._root_dir @property - def _dir_separator(self): + def _dir_separator(self) -> str: return sep - def _list_files(self, path, cursor=None): + def _list_files(self, path: str, cursor: Any = None) -> list[str]: return [join(path, f) for f in listdir(path) if isfile(join(path, f))] - def create_file(self, name, content=""): + def create_file(self, name: str, content: str = "") -> None: logger.info(f"Storing file {name}...") with open(name, "w", encoding="utf-8") as out_f: out_f.write(content) logger.info(f"Stored file {name}.") - def get_file(self, name): + def get_file(self, name: str) -> Any: logger.info(f"Reading file {name}...") with open(name, "r") as in_f: content = in_f.read() logger.info(f"Read file {name}.") return json.loads(content) - def list_imported_deckstats_deck_ids(self): + def list_imported_deckstats_deck_ids(self) -> set[str]: return set( self.get_imported_deckstats_deck_id_from_key(file) for file in self._list_files(self.deckstats_deck_path) ) - def list_imported_deckstats_deck_names(self): + def list_imported_deckstats_deck_names(self) -> set[str]: return set( self.get_imported_deckstats_deck_name_from_key(file) for file in self._list_files(self.deckstats_deck_path) ) - def list_imported_mtggoldfish_deck_ids(self): + def list_imported_mtggoldfish_deck_ids(self) -> set[str]: return set( self.get_imported_mtggoldfish_deck_id_from_key(file) for file in self._list_files(self.mtggoldfish_deck_path) ) - def list_imported_mtggoldfish_deck_names(self): + def list_imported_mtggoldfish_deck_names(self) -> set[str]: return set( self.get_imported_mtggoldfish_deck_name_from_key(file) for file in self._list_files(self.mtggoldfish_deck_path) ) - def list_imported_twitch_videos(self): + def list_imported_twitch_videos(self) -> set[str]: return set( self.get_imported_twitch_video(file) for file in self._list_files(self.twitch_video_path) ) - def list_imported_twitch_videos_ids(self): + def list_imported_twitch_videos_ids(self) -> set[str]: return set( self.get_imported_twitch_video_id_from_key(file) for file in self._list_files(self.twitch_video_path) ) - def list_imported_youtube_videos(self): + def list_imported_youtube_videos(self) -> set[str]: return set( self.get_imported_youtube_video(file) for file in self._list_files(self.youtube_video_path) ) - def list_imported_youtube_videos_ids(self): + def list_imported_youtube_videos_ids(self) -> set[str]: return set( self.get_imported_youtube_video_id_from_key(file) for file in self._list_files(self.youtube_video_path) ) - def delete_deck_by_name(self, deck_name): + def delete_deck_by_name(self, deck_name: str) -> None: logger.info(f"Deleting file containing {deck_name}...") file_path = None for file in self._list_files(self.deckstats_deck_path): From 3cb353a4313d4fed2c3ec12ab069d4e2b8bb3f9f Mon Sep 17 00:00:00 2001 From: Lucas Feliciano Date: Sun, 1 Mar 2026 17:41:17 +0100 Subject: [PATCH 3/5] Add type annotations to CLI and task layer Annotate all function signatures in cli/builder, cli/group, cli/main, and task modules. Use modern typing syntax throughout. --- pauperformance_bot/cli/builder/command.py | 10 +++-- pauperformance_bot/cli/builder/group.py | 10 +++-- pauperformance_bot/cli/builder/options.py | 50 +++++++++++----------- pauperformance_bot/cli/builder/runnable.py | 16 ++++--- pauperformance_bot/cli/builder/utils.py | 13 ++++-- pauperformance_bot/cli/group/academy.py | 14 +++--- pauperformance_bot/cli/group/silver.py | 12 ++++-- pauperformance_bot/cli/group/test.py | 10 +++-- pauperformance_bot/cli/main.py | 15 ++++--- pauperformance_bot/task/academy.py | 4 +- pauperformance_bot/task/academy2.py | 4 +- pauperformance_bot/task/silver.py | 12 ++++-- 12 files changed, 100 insertions(+), 70 deletions(-) diff --git a/pauperformance_bot/cli/builder/command.py b/pauperformance_bot/cli/builder/command.py index 8239cb99..a673204a 100644 --- a/pauperformance_bot/cli/builder/command.py +++ b/pauperformance_bot/cli/builder/command.py @@ -1,7 +1,9 @@ from argparse import ArgumentParser +from typing import Any import argcomplete +from pauperformance_bot.cli.builder.options import CLIOption from pauperformance_bot.cli.builder.runnable import CLIRunnable from pauperformance_bot.cli.builder.utils import ( get_default_parent_parser, @@ -10,16 +12,16 @@ class CLICommand(CLIRunnable): - def __init__(self, name, description, options): + def __init__(self, name: str, description: str, options: list[CLIOption]) -> None: super().__init__(name) self.description = description self.options = options - def add_parser_argument(self, tool_parser): + def add_parser_argument(self, tool_parser: ArgumentParser) -> None: for option in self.options: option.add_to_parser(tool_parser, self.name) - def get_cli_parser(self): + def get_cli_parser(self) -> ArgumentParser: parser = ArgumentParser( description=self.description, parents=[get_default_parent_parser()] ) @@ -27,5 +29,5 @@ def get_cli_parser(self): argcomplete.autocomplete(parser) return parser - def dispatch_cmd(self, *args, **kwargs): + def dispatch_cmd(self, *args: Any, **kwargs: Any) -> None: handle_default_cli_options(*args, **kwargs) diff --git a/pauperformance_bot/cli/builder/group.py b/pauperformance_bot/cli/builder/group.py index 1876dcdb..d075bb7f 100644 --- a/pauperformance_bot/cli/builder/group.py +++ b/pauperformance_bot/cli/builder/group.py @@ -1,7 +1,9 @@ from argparse import ArgumentParser +from typing import Any import argcomplete +from pauperformance_bot.cli.builder.command import CLICommand from pauperformance_bot.cli.builder.runnable import CLIRunnable from pauperformance_bot.cli.builder.utils import ( build_commands_sub_parser, @@ -11,14 +13,14 @@ class CLIGroup(CLIRunnable): - def __init__(self, name, cli_commands): + def __init__(self, name: str, cli_commands: list[CLICommand]) -> None: super().__init__(name) self.cli_commands = cli_commands - def add_parser_argument(self, tool_parser): + def add_parser_argument(self, tool_parser: ArgumentParser) -> None: build_commands_sub_parser(tool_parser, self.cli_commands) - def get_cli_parser(self): + def get_cli_parser(self) -> ArgumentParser: parser = ArgumentParser( description="A collection of {}-related tools.".format(self.name), parents=[get_default_parent_parser()], @@ -27,7 +29,7 @@ def get_cli_parser(self): argcomplete.autocomplete(parser) return parser - def dispatch_cmd(self, command, *args, **kwargs): + def dispatch_cmd(self, command: str, *args: Any, **kwargs: Any) -> None: handle_default_cli_options(*args, **kwargs) dispatcher = next(filter(lambda c: c.name == command, self.cli_commands)) dispatcher.dispatch_cmd(*args, **kwargs) diff --git a/pauperformance_bot/cli/builder/options.py b/pauperformance_bot/cli/builder/options.py index 283828f7..86478261 100644 --- a/pauperformance_bot/cli/builder/options.py +++ b/pauperformance_bot/cli/builder/options.py @@ -1,19 +1,21 @@ from abc import abstractmethod +from argparse import ArgumentParser, _ActionsContainer from functools import partial +from typing import Any class CLIOption: - def __init__(self, dest_var, help_msg): + def __init__(self, dest_var: str, help_msg: str) -> None: self.dest_var = dest_var self.help_msg = help_msg @abstractmethod - def add_to_parser(self, arg_parser, command_name): + def add_to_parser(self, arg_parser: ArgumentParser, command_name: str) -> None: pass class FlagCLIOption(CLIOption): - def add_to_parser(self, arg_parser, command_name): + def add_to_parser(self, arg_parser: ArgumentParser, command_name: str) -> None: arg_parser.add_argument( "--" + self.dest_var, action="store_true", help=self.help_msg ) @@ -22,13 +24,13 @@ def add_to_parser(self, arg_parser, command_name): class ValuedCLIOption(CLIOption): def __init__( self, - dest_var, - choices, - default_value, - required, - multiple_allowed, - help_msg, - ): + dest_var: str, + choices: list[str] | None, + default_value: str | None, + required: bool, + multiple_allowed: bool, + help_msg: str, + ) -> None: super().__init__(dest_var, help_msg) if default_value and required: @@ -38,11 +40,11 @@ def __init__( ) ) self.choices = choices - self.default_value = default_value + self.default_value: Any = default_value self.required = required self.multiple_allowed = multiple_allowed - def add_to_parser(self, arg_parser, command_name): + def add_to_parser(self, arg_parser: ArgumentParser, command_name: str) -> None: add_args_fn = arg_parser.add_argument if self.multiple_allowed: @@ -60,23 +62,23 @@ def add_to_parser(self, arg_parser, command_name): class QuietCLIOption(FlagCLIOption): - def __init__(self): + def __init__(self) -> None: super().__init__("quiet", "suppress non-error messages") class VerboseCLIOption(FlagCLIOption): - def __init__(self): + def __init__(self) -> None: super().__init__("verbose", "verbose logging") class InputFileCLIOption(ValuedCLIOption): def __init__( self, - choices=None, - default_value=None, - required=False, - multiple_allowed=False, - ): + choices: list[str] | None = None, + default_value: str | None = None, + required: bool = False, + multiple_allowed: bool = False, + ) -> None: super().__init__( "input-file", choices, @@ -90,11 +92,11 @@ def __init__( class OutputFileCLIOption(ValuedCLIOption): def __init__( self, - choices=None, - default_value=None, - required=False, - multiple_allowed=False, - ): + choices: list[str] | None = None, + default_value: str | None = None, + required: bool = False, + multiple_allowed: bool = False, + ) -> None: super().__init__( "output-file", choices, diff --git a/pauperformance_bot/cli/builder/runnable.py b/pauperformance_bot/cli/builder/runnable.py index a4e235ca..8942a87e 100644 --- a/pauperformance_bot/cli/builder/runnable.py +++ b/pauperformance_bot/cli/builder/runnable.py @@ -1,25 +1,27 @@ from abc import abstractmethod +from argparse import ArgumentParser, Namespace +from typing import Any class CLIRunnable: - def __init__(self, name): + def __init__(self, name: str) -> None: self._name = name @property - def name(self): + def name(self) -> str: return self._name @abstractmethod - def get_cli_parser(self): + def get_cli_parser(self) -> ArgumentParser: pass @abstractmethod - def dispatch_cmd(self, *args, **kwargs): + def dispatch_cmd(self, *args: Any, **kwargs: Any) -> None: pass - def run(self): - args = self.get_cli_parser().parse_args() + def run(self) -> None: + args: Namespace = self.get_cli_parser().parse_args() # CLI params may contain '-', an invalid character for python variables # identifiers: replace it with '_' - params = {k.replace("-", "_"): v for k, v in vars(args).items()} + params: dict[str, Any] = {k.replace("-", "_"): v for k, v in vars(args).items()} self.dispatch_cmd(**params) diff --git a/pauperformance_bot/cli/builder/utils.py b/pauperformance_bot/cli/builder/utils.py index 721e532b..557d6bf8 100644 --- a/pauperformance_bot/cli/builder/utils.py +++ b/pauperformance_bot/cli/builder/utils.py @@ -1,23 +1,26 @@ import argparse import logging +from typing import Any from pauperformance_bot.cli.builder.options import QuietCLIOption, VerboseCLIOption from pauperformance_bot.constant.cli import GROUP_CLI_DEST_ID from pauperformance_bot.util.log import get_application_logger -def get_default_parent_parser(): +def get_default_parent_parser() -> argparse.ArgumentParser: arg_parser = argparse.ArgumentParser(add_help=False) add_default_options(arg_parser) return arg_parser -def add_default_options(arg_parser): +def add_default_options(arg_parser: argparse.ArgumentParser) -> None: QuietCLIOption().add_to_parser(arg_parser, "") VerboseCLIOption().add_to_parser(arg_parser, "") -def handle_default_cli_options(quiet, verbose, *args, **kwargs): +def handle_default_cli_options( + quiet: bool, verbose: bool, *args: Any, **kwargs: Any +) -> None: logger = get_application_logger() if quiet and verbose: raise ValueError("Unable to set both quiet and verbose flags") @@ -27,7 +30,9 @@ def handle_default_cli_options(quiet, verbose, *args, **kwargs): logger.setLevel(logging.DEBUG) -def build_commands_sub_parser(tool_parser, cli_commands): +def build_commands_sub_parser( + tool_parser: argparse.ArgumentParser, cli_commands: list[Any] +) -> None: description = ( "Type 'command-name -h' to show its help message " "(e.g. '{} -h)'".format(next(iter(cli_commands)).name) diff --git a/pauperformance_bot/cli/group/academy.py b/pauperformance_bot/cli/group/academy.py index 5db91544..de9aae16 100644 --- a/pauperformance_bot/cli/group/academy.py +++ b/pauperformance_bot/cli/group/academy.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from typing import Any + from pauperformance_bot.cli.builder.command import CLICommand from pauperformance_bot.cli.builder.group import CLIGroup from pauperformance_bot.constant.cli import ( @@ -14,35 +16,35 @@ class UpdateCommand(CLICommand): - def __init__(self): + def __init__(self) -> None: super().__init__( UPDATE_ACADEMY_CMD, "Update Academy web site (old version).", [], ) - def dispatch_cmd(self, *args, **kwargs): + def dispatch_cmd(self, *args: Any, **kwargs: Any) -> None: super().dispatch_cmd(*args, **kwargs) main_v1() class UpdateCommand2(CLICommand): - def __init__(self): + def __init__(self) -> None: super().__init__( UPDATE2_ACADEMY_CMD, "Update Academy web site (new version).", [], ) - def dispatch_cmd(self, *args, **kwargs): + def dispatch_cmd(self, *args: Any, **kwargs: Any) -> None: super().dispatch_cmd(*args, **kwargs) main_v2() class AcademyGroup(CLIGroup): - _cli_commands = [UpdateCommand(), UpdateCommand2()] + _cli_commands: list[CLICommand] = [UpdateCommand(), UpdateCommand2()] - def __init__(self): + def __init__(self) -> None: super().__init__(ACADEMY_CLI_GROUP, self._cli_commands) diff --git a/pauperformance_bot/cli/group/silver.py b/pauperformance_bot/cli/group/silver.py index 4cbc6bb1..bcc05455 100644 --- a/pauperformance_bot/cli/group/silver.py +++ b/pauperformance_bot/cli/group/silver.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from typing import Any + from pauperformance_bot.cli.builder.command import CLICommand from pauperformance_bot.cli.builder.group import CLIGroup from pauperformance_bot.cli.builder.options import ( @@ -13,7 +15,7 @@ class DPLMetaCommand(CLICommand): - def __init__(self): + def __init__(self) -> None: super().__init__( DPL_META_SILVER_CMD, "Generate meta for a Dutch Pauper League tournament.", @@ -23,15 +25,17 @@ def __init__(self): ], ) - def dispatch_cmd(self, input_file, output_file, *args, **kwargs): + def dispatch_cmd( + self, input_file: str, output_file: str, *args: Any, **kwargs: Any + ) -> None: super().dispatch_cmd(*args, **kwargs) main(input_file, output_file) class SilverGroup(CLIGroup): - _cli_commands = [DPLMetaCommand()] + _cli_commands: list[CLICommand] = [DPLMetaCommand()] - def __init__(self): + def __init__(self) -> None: super().__init__(SILVER_CLI_GROUP, self._cli_commands) diff --git a/pauperformance_bot/cli/group/test.py b/pauperformance_bot/cli/group/test.py index f6a29f4d..d5ded4b8 100644 --- a/pauperformance_bot/cli/group/test.py +++ b/pauperformance_bot/cli/group/test.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from typing import Any + from pauperformance_bot.cli.builder.command import CLICommand from pauperformance_bot.cli.builder.group import CLIGroup from pauperformance_bot.constant.cli import HELLO_TEST_CMD, TEST_CLI_GROUP @@ -8,22 +10,22 @@ class HelloCommand(CLICommand): - def __init__(self): + def __init__(self) -> None: super().__init__( HELLO_TEST_CMD, "Call Myr.", [], ) - def dispatch_cmd(self, *args, **kwargs): + def dispatch_cmd(self, *args: Any, **kwargs: Any) -> None: super().dispatch_cmd(*args, **kwargs) print("Myr ready to serve you, Milord!") class TestGroup(CLIGroup): - _cli_commands = [HelloCommand()] + _cli_commands: list[CLICommand] = [HelloCommand()] - def __init__(self): + def __init__(self) -> None: super().__init__(TEST_CLI_GROUP, self._cli_commands) diff --git a/pauperformance_bot/cli/main.py b/pauperformance_bot/cli/main.py index f3d2aaee..16bf00d8 100644 --- a/pauperformance_bot/cli/main.py +++ b/pauperformance_bot/cli/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # PYTHON_ARGCOMPLETE_OK from argparse import ArgumentParser +from typing import Any import argcomplete @@ -14,12 +15,16 @@ class MyrCLI(CLIRunnable): - _cli_tools = [academy.AcademyGroup(), test.TestGroup(), silver.SilverGroup()] + _cli_tools: list[CLIRunnable] = [ + academy.AcademyGroup(), + test.TestGroup(), + silver.SilverGroup(), + ] - def __init__(self): + def __init__(self) -> None: super().__init__(APPLICATION_NAME) - def get_cli_parser(self): + def get_cli_parser(self) -> ArgumentParser: parser = ArgumentParser(description="A collection of Myr tasks") add_default_options(parser) @@ -39,13 +44,13 @@ def get_cli_parser(self): argcomplete.autocomplete(parser) return parser - def dispatch_cmd(self, tool, *args, **kwargs): + def dispatch_cmd(self, tool: str, *args: Any, **kwargs: Any) -> None: handle_default_cli_options(*args, **kwargs) dispatcher = next(filter(lambda t: t.name == tool, MyrCLI._cli_tools)) dispatcher.dispatch_cmd(*args, **kwargs) -def main(): +def main() -> None: MyrCLI().run() diff --git a/pauperformance_bot/task/academy.py b/pauperformance_bot/task/academy.py index c74a98cd..64516466 100644 --- a/pauperformance_bot/task/academy.py +++ b/pauperformance_bot/task/academy.py @@ -26,7 +26,7 @@ stop=stop_after_attempt(3), wait=wait_random_exponential(multiplier=1, max=60), ) -async def async_academy_update(): +async def async_academy_update() -> None: storage = DropboxService() archive = MTGGoldfishArchiveService(storage) discord = AsyncDiscordService() @@ -58,7 +58,7 @@ async def async_academy_update(): stop=stop_after_attempt(3), wait=wait_random_exponential(multiplier=1, max=60), ) -def main(): +def main() -> None: loop = asyncio.get_event_loop() try: loop.run_until_complete(async_academy_update()) diff --git a/pauperformance_bot/task/academy2.py b/pauperformance_bot/task/academy2.py index 73ce26f3..6078d890 100644 --- a/pauperformance_bot/task/academy2.py +++ b/pauperformance_bot/task/academy2.py @@ -8,7 +8,7 @@ from pauperformance_bot.service.pauperformance.storage.dropbox_ import DropboxService -def academy_update(): +def academy_update() -> None: storage = DropboxService() archive = MTGGoldfishArchiveService(storage) pauperformance = PauperformanceService(storage, archive) @@ -16,7 +16,7 @@ def academy_update(): exporter.export_all() -def main(): +def main() -> None: academy_update() diff --git a/pauperformance_bot/task/silver.py b/pauperformance_bot/task/silver.py index 4429f4e2..ec0dbb74 100644 --- a/pauperformance_bot/task/silver.py +++ b/pauperformance_bot/task/silver.py @@ -1,9 +1,11 @@ import json import os +from typing import Any import jsonpickle from pauperformance_bot.constant.pauperformance.myr import TOP_PATH +from pauperformance_bot.entity.api.miscellanea import DPLMeta from pauperformance_bot.service.academy.data_exporter import AcademyDataExporter from pauperformance_bot.service.pauperformance.archive.mtggoldfish import ( MTGGoldfishArchiveService, @@ -22,7 +24,7 @@ logger = get_application_logger() -def get_dpl_classifier(): +def get_dpl_classifier() -> Decklassifier: # from pauperformance_bot.service.pauperformance.archive.local import ( # LocalArchiveService # ) @@ -46,11 +48,13 @@ def get_dpl_classifier(): DPL_SILVER = get_dpl_classifier() -def generate_dpl_meta(data, name="DPL metagame"): +def generate_dpl_meta( + data: list[dict[str, Any]], name: str = "DPL metagame" +) -> DPLMeta: return DPL_SILVER.get_dpl_metagame(data, name=name) -def main(input_file, output_file): +def main(input_file: str, output_file: str) -> None: logger.info(f"Getting DPL decks from {input_file}...") data = json.load(open(input_file)) dpl_meta = generate_dpl_meta(data, name=input_file) @@ -63,7 +67,7 @@ def main(input_file, output_file): logger.info(f"Stored DPL meta in {output_file}...") -def dpl_classifier(environ, start_response): +def dpl_classifier(environ: dict[str, Any], start_response: Any) -> list[bytes]: try: method = environ["REQUEST_METHOD"] if method != "POST": From 162e49e66805a34f56d9aced49f05432246b1215 Mon Sep 17 00:00:00 2001 From: Lucas Feliciano Date: Sun, 1 Mar 2026 17:45:14 +0100 Subject: [PATCH 4/5] Add type annotations to test files Annotate all test functions, fixtures, and helpers with parameter and return types. Use modern typing syntax throughout. --- tests/__test_centroid.py | 35 +++++++++++--------- tests/__test_deckstatistics.py | 13 ++++---- tests/conftest.py | 6 ++-- tests/data.py | 4 +-- tests/test_build.py | 2 +- tests/test_config_reader.py | 4 +-- tests/test_deck_analyser.py | 14 ++++---- tests/test_deckstructor.py | 12 +++---- tests/test_downloader.py | 59 ++++++++++++++++++---------------- tests/test_dropbox_.py | 4 +-- tests/test_playable.py | 26 +++++++-------- tests/test_servitor.py | 12 +++---- 12 files changed, 101 insertions(+), 90 deletions(-) diff --git a/tests/__test_centroid.py b/tests/__test_centroid.py index f384f020..c10f6146 100644 --- a/tests/__test_centroid.py +++ b/tests/__test_centroid.py @@ -1,3 +1,4 @@ +import logging from statistics import mean, stdev, variance from pauperformance_bot.service.academy.academy import AcademyService @@ -14,38 +15,40 @@ from pauperformance_bot.service.pauperformance.storage.dropbox_ import DropboxService from pauperformance_bot.util.log import get_application_logger -logger = get_application_logger() +logger: logging.Logger = get_application_logger() -def test_centroid(): - storage = DropboxService() - archive = MTGGoldfishArchiveService(storage) - pauperformance = PauperformanceService(storage, archive) - academy = AcademyService(pauperformance) - archetype = "Burn" - loader = AcademyDataLoader() +def test_centroid() -> None: + storage: DropboxService = DropboxService() + archive: MTGGoldfishArchiveService = MTGGoldfishArchiveService(storage) + pauperformance: PauperformanceService = PauperformanceService(storage, archive) + academy: AcademyService = AcademyService(pauperformance) + archetype: str = "Burn" + loader: AcademyDataLoader = AcademyDataLoader() am = DeckstatisticsFactory(academy.scryfall, loader).build_metadata_for(archetype) logger.info(am.cards) logger.info(f"nr. decks: {am.nr_decks} max: {am.most_played_card}") - dfq = am.deck_occurrences_ratio() - cfq = am.card_playing_rate() + dfq: list[tuple[str, float]] = am.deck_occurrences_ratio() + cfq: list[tuple[str, float]] = am.card_playing_rate() logger.info("# DECKS") logger.info(f"max deck freq.: {dfq[0]}") - centroid = sorted(list(map(lambda f: f[0], filter(lambda f: f[1] >= 0.9, dfq)))) + centroid: list[str] = sorted( + list(map(lambda f: f[0], filter(lambda f: f[1] >= 0.9, dfq))) + ) logger.info(f"centroid(decks): {centroid}") - deck_freqs = [f[1] for f in dfq] - mean_deck_freq = mean(deck_freqs) + deck_freqs: list[float] = [f[1] for f in dfq] + mean_deck_freq: float = mean(deck_freqs) logger.info(f"stdev: {stdev(deck_freqs)} variance: {variance(deck_freqs)}") logger.info(f"mean {mean_deck_freq}") logger.info("# CARDS") logger.info(f"max card freq {cfq[0]}") centroid = sorted(list(map(lambda f: f[0], filter(lambda f: f[1] >= 0.07, cfq)))) logger.info(f"centroid(cards): {centroid}") - card_freqs = [f[1] for f in cfq] - mean_card_freq = mean(card_freqs) + card_freqs: list[float] = [f[1] for f in cfq] + mean_card_freq: float = mean(card_freqs) logger.info(f"stdev: {stdev(card_freqs)} variance: {variance(card_freqs)}") logger.info(f"mean {mean_card_freq}") - s_card_fqs = sorted( + s_card_fqs: list[str] = sorted( list( map( lambda f: f[0], diff --git a/tests/__test_deckstatistics.py b/tests/__test_deckstatistics.py index 2e3ad28e..281169ca 100644 --- a/tests/__test_deckstatistics.py +++ b/tests/__test_deckstatistics.py @@ -11,14 +11,15 @@ from pauperformance_bot.service.pauperformance.storage.dropbox_ import DropboxService -def test_deckstatistics(): - storage = DropboxService() - archive = MTGGoldfishArchiveService(storage) - pauperformance = PauperformanceService(storage, archive) - archetype = "Burn" - loader = AcademyDataLoader() +def test_deckstatistics() -> None: + storage: DropboxService = DropboxService() + archive: MTGGoldfishArchiveService = MTGGoldfishArchiveService(storage) + pauperformance: PauperformanceService = PauperformanceService(storage, archive) + archetype: str = "Burn" + loader: AcademyDataLoader = AcademyDataLoader() deckstatistics = DeckstatisticsFactory( pauperformance.scryfall, loader ).build_metadata_for(archetype) + staple: list[str] staple, _ = deckstatistics.get_staple_and_frequent_cards() assert "Lightning Bolt" in staple diff --git a/tests/conftest.py b/tests/conftest.py index 0b71cd39..9f635a01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,10 +2,12 @@ import pytest -REQUIRED_SECRETS = "RUN_TESTS_WITH_SECRETS" +REQUIRED_SECRETS: str = "RUN_TESTS_WITH_SECRETS" -def pytest_collection_modifyitems(config, items): +def pytest_collection_modifyitems( + config: pytest.Config, items: list[pytest.Item] +) -> None: if os.getenv(REQUIRED_SECRETS): return diff --git a/tests/data.py b/tests/data.py index 17274449..8049f000 100644 --- a/tests/data.py +++ b/tests/data.py @@ -1,6 +1,6 @@ from pauperformance_bot.entity.deck.playable import PlayedCard -DECK_MAIN = [ +DECK_MAIN: list[PlayedCard] = [ PlayedCard(4, "Thermo-Alchemist"), PlayedCard(4, "Voldaren Epicure"), PlayedCard(4, "Needle Drop"), @@ -15,7 +15,7 @@ PlayedCard(1, "Forgotten Cave"), PlayedCard(16, "Mountain"), ] -DECK_SIDE = [ +DECK_SIDE: list[PlayedCard] = [ PlayedCard(2, "Molten Rain"), PlayedCard(2, "Martyr of Ashes"), PlayedCard(3, "Electrickery"), diff --git a/tests/test_build.py b/tests/test_build.py index b6b0396f..7fbb5d98 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -1,5 +1,5 @@ import pauperformance_bot -def test_version(): +def test_version() -> None: assert pauperformance_bot.version diff --git a/tests/test_config_reader.py b/tests/test_config_reader.py index 5d1a66b3..a77b293c 100644 --- a/tests/test_config_reader.py +++ b/tests/test_config_reader.py @@ -1,9 +1,9 @@ from pauperformance_bot.service.pauperformance.config_reader import ConfigReader -def test_list_phd_sheets(): +def test_list_phd_sheets() -> None: assert len(ConfigReader().list_phd_sheets()) >= 3 -def test_list_phds(): +def test_list_phds() -> None: assert len(ConfigReader().list_phds()) >= 3 diff --git a/tests/test_deck_analyser.py b/tests/test_deck_analyser.py index f07cce14..5f030825 100644 --- a/tests/test_deck_analyser.py +++ b/tests/test_deck_analyser.py @@ -3,21 +3,21 @@ # from pauperformance_bot.service.deck_analyser import get_similarity -def test_get_similarity(): - mainboard1 = [ +def test_get_similarity() -> None: + mainboard1: list[PlayedCard] = [ PlayedCard(3, "Island"), PlayedCard(3, "Gush"), PlayedCard(54, "X"), ] - sideboard1 = [PlayedCard(11, "Plains"), PlayedCard(4, "Forest")] - deck1 = PlayableDeck(mainboard1, sideboard1) + sideboard1: list[PlayedCard] = [PlayedCard(11, "Plains"), PlayedCard(4, "Forest")] + deck1: PlayableDeck = PlayableDeck(mainboard1, sideboard1) - mainboard2 = [ + mainboard2: list[PlayedCard] = [ PlayedCard(2, "Island"), PlayedCard(4, "Gush"), PlayedCard(54, "X"), ] - sideboard2 = [PlayedCard(11, "Plains"), PlayedCard(4, "Forest")] - deck2 = PlayableDeck(mainboard2, sideboard2) + sideboard2: list[PlayedCard] = [PlayedCard(11, "Plains"), PlayedCard(4, "Forest")] + deck2: PlayableDeck = PlayableDeck(mainboard2, sideboard2) # assert get_similarity(deck1, deck2) > 0.99 assert deck1 != deck2 # TODO: remove diff --git a/tests/test_deckstructor.py b/tests/test_deckstructor.py index a3b4bec6..03543d26 100644 --- a/tests/test_deckstructor.py +++ b/tests/test_deckstructor.py @@ -2,8 +2,8 @@ from pauperformance_bot.service.pauperformance.silver.deckstructor import Deckstructor -def test_deckstructor(): - expected_deck = PlayableDeck( +def test_deckstructor() -> None: + expected_deck: PlayableDeck = PlayableDeck( mainboard=[ PlayedCard(2, "Boseiju, Who Endures"), PlayedCard(4, "Cavern of Souls"), @@ -39,11 +39,11 @@ def test_deckstructor(): ) with open("tests/mock_data/modern_mtg_elves.txt", "r") as f: - text = f.read() + text: str = f.read() with open("tests/mock_data/pool.txt", "r") as f: - pool = f.readlines() + pool_lines: list[str] = f.readlines() - pool = [p.strip() for p in pool] - deck = Deckstructor().extract_deck(text, pool) + pool: list[str] = [p.strip() for p in pool_lines] + deck: PlayableDeck = Deckstructor().extract_deck(text, pool) assert deck == expected_deck diff --git a/tests/test_downloader.py b/tests/test_downloader.py index 1a37a746..b8080ede 100644 --- a/tests/test_downloader.py +++ b/tests/test_downloader.py @@ -1,5 +1,6 @@ import unittest from unittest import mock +from unittest.mock import MagicMock from pauperformance_bot.entity.deck.playable import ( PlayableDeck, @@ -9,7 +10,7 @@ from pauperformance_bot.service.mtg.downloader.downloader import MtgoDeckDownloader from pauperformance_bot.service.mtg.downloader.mtgdecks import MtgdecksDeckDownloader -EXPECTED_DECK_AETHERHUB_MAIN = [ +EXPECTED_DECK_AETHERHUB_MAIN: list[PlayedCard] = [ PlayedCard(3, "Thoughtcast"), PlayedCard(2, "Krark-Clan Shaman"), PlayedCard(3, "Great Furnace"), @@ -35,7 +36,7 @@ PlayedCard(1, "Nihil Spellbomb"), PlayedCard(1, "Aether Spellbomb"), ] -EXPECTED_DECK_AETHERHUB_SIDE = [ +EXPECTED_DECK_AETHERHUB_SIDE: list[PlayedCard] = [ PlayedCard(2, "Gorilla Shaman"), PlayedCard(3, "Hydroblast"), PlayedCard(4, "Pyroblast"), @@ -44,7 +45,7 @@ PlayedCard(1, "Electrickery"), PlayedCard(1, "Makeshift Munitions"), ] -EXPECTED_DECK_TAPPEDOUT_MAIN = [ +EXPECTED_DECK_TAPPEDOUT_MAIN: list[PlayedCard] = [ PlayedCard(4, "Chromatic Star"), PlayedCard(4, "Drossforge Bridge"), PlayedCard(1, "Duress"), @@ -69,7 +70,7 @@ PlayedCard(4, "Unearth"), PlayedCard(2, "Vault of Whispers"), ] -EXPECTED_DECK_TAPPEDOUT_SIDE = [ +EXPECTED_DECK_TAPPEDOUT_SIDE: list[PlayedCard] = [ PlayedCard(2, "Abrade"), PlayedCard(2, "Cuombajj Witches"), PlayedCard(2, "Duress"), @@ -79,7 +80,7 @@ PlayedCard(1, "Nameless Inversion"), PlayedCard(4, "Pyroblast"), ] -EXPECTED_DECK_MTGTOP8_MAIN = [ +EXPECTED_DECK_MTGTOP8_MAIN: list[PlayedCard] = [ PlayedCard(2, "River Boa"), PlayedCard(2, "Silhana Ledgewalker"), PlayedCard(3, "Vault Skirge"), @@ -95,7 +96,7 @@ PlayedCard(4, "Rancor"), PlayedCard(17, "Forest"), ] -EXPECTED_DECK_MTGTOP8_SIDE = [ +EXPECTED_DECK_MTGTOP8_SIDE: list[PlayedCard] = [ PlayedCard(2, "Weather the Storm"), PlayedCard(2, "Epic Confrontation"), PlayedCard(2, "Relic of Progenitus"), @@ -103,7 +104,7 @@ PlayedCard(3, "Gut Shot"), PlayedCard(4, "Gleeful Sabotage"), ] -EXPECTED_DECK_MTGDECKS_MAIN = [ +EXPECTED_DECK_MTGDECKS_MAIN: list[PlayedCard] = [ PlayedCard(4, "Thermo-Alchemist"), PlayedCard(4, "Voldaren Epicure"), PlayedCard(4, "Needle Drop"), @@ -118,7 +119,7 @@ PlayedCard(1, "Forgotten Cave"), PlayedCard(16, "Mountain"), ] -EXPECTED_DECK_MTGDECKS_SIDE = [ +EXPECTED_DECK_MTGDECKS_SIDE: list[PlayedCard] = [ PlayedCard(2, "Molten Rain"), PlayedCard(2, "Martyr of Ashes"), PlayedCard(3, "Electrickery"), @@ -128,68 +129,72 @@ class TestMtgoDownloader(unittest.TestCase): - def _read_mock_data(self, filename): + def _read_mock_data(self, filename: str) -> str: with open(filename) as fd: return fd.read() - def _validate_result(self, expected, res): + def _validate_result(self, expected: PlayableDeck, res: PlayableDeck) -> None: self.assertIsNotNone(res, "Should return a deck") - diff = get_decks_diff(expected, res) + diff: tuple[list[str], list[str], list[str], list[str]] = get_decks_diff( + expected, res + ) self.assertTupleEqual(([], [], [], []), diff, "Should not have any difference") @mock.patch("pauperformance_bot.util.request.execute_http_request") - def test_mtgo_downloader_aetherhub(self, mock_ehr): + def test_mtgo_downloader_aetherhub(self, mock_ehr: MagicMock) -> None: mock_ehr.return_value = self._read_mock_data( "tests/mock_data/aetherhub_deck.txt" ) - expected_deck = PlayableDeck( + expected_deck: PlayableDeck = PlayableDeck( EXPECTED_DECK_AETHERHUB_MAIN, EXPECTED_DECK_AETHERHUB_SIDE ) - downloader = MtgoDeckDownloader( + downloader: MtgoDeckDownloader = MtgoDeckDownloader( "https://aetherhub.com/Deck/MtgoDeckExport/883786" ) - res = downloader.download() + res: PlayableDeck = downloader.download() self._validate_result(expected_deck, res) @mock.patch("pauperformance_bot.util.request.execute_http_request") - def test_mtgo_downloader_mtgtop8(self, mock_ehr): + def test_mtgo_downloader_mtgtop8(self, mock_ehr: MagicMock) -> None: mock_ehr.return_value = self._read_mock_data("tests/mock_data/mtgtop8_deck.txt") - expected_deck = PlayableDeck( + expected_deck: PlayableDeck = PlayableDeck( EXPECTED_DECK_MTGTOP8_MAIN, EXPECTED_DECK_MTGTOP8_SIDE ) - downloader = MtgoDeckDownloader("https://www.mtgtop8.com/mtgo?d=473002") + downloader: MtgoDeckDownloader = MtgoDeckDownloader( + "https://www.mtgtop8.com/mtgo?d=473002" + ) - res = downloader.download() + res: PlayableDeck = downloader.download() self._validate_result(expected_deck, res) @mock.patch("pauperformance_bot.util.request.execute_http_request") - def test_mtgo_downloader_tappedout(self, mock_ehr): + def test_mtgo_downloader_tappedout(self, mock_ehr: MagicMock) -> None: mock_ehr.return_value = self._read_mock_data( "tests/mock_data/tappedout_deck.txt" ) - expected_deck = PlayableDeck( + expected_deck: PlayableDeck = PlayableDeck( EXPECTED_DECK_TAPPEDOUT_MAIN, EXPECTED_DECK_TAPPEDOUT_SIDE ) - downloader = MtgoDeckDownloader( + downloader: MtgoDeckDownloader = MtgoDeckDownloader( "https://tappedout.net/mtg-decks/gobliny-combo/?fmt=dek&cb=1653244312" ) - res = downloader.download() + res: PlayableDeck = downloader.download() self._validate_result(expected_deck, res) - def test_downloader_mtgdeck(self): - expected_deck = PlayableDeck( + def test_downloader_mtgdeck(self) -> None: + expected_deck: PlayableDeck = PlayableDeck( EXPECTED_DECK_MTGDECKS_MAIN, EXPECTED_DECK_MTGDECKS_SIDE ) - downloader = MtgdecksDeckDownloader( + downloader: MtgdecksDeckDownloader = MtgdecksDeckDownloader( "https://mtgdecks.net/Pauper/burn-decklist-by-thormyn-1380435/txt" ) - res = downloader.download() + res: PlayableDeck = downloader.download() self._validate_result(expected_deck, res) diff --git a/tests/test_dropbox_.py b/tests/test_dropbox_.py index 94931991..da4787ca 100644 --- a/tests/test_dropbox_.py +++ b/tests/test_dropbox_.py @@ -4,14 +4,14 @@ @pytest.mark.secrets -def test_list_imported_youtube_videos(): +def test_list_imported_youtube_videos() -> None: assert any( "Pauperformance" in v for v in DropboxService().list_imported_youtube_videos() ) @pytest.mark.secrets -def test_list_imported_mtggoldfish_deck_names(): +def test_list_imported_mtggoldfish_deck_names() -> None: assert any( "Jund" in d for d in DropboxService().list_imported_mtggoldfish_deck_names() ) diff --git a/tests/test_playable.py b/tests/test_playable.py index 55e64269..9b00cc64 100644 --- a/tests/test_playable.py +++ b/tests/test_playable.py @@ -5,42 +5,42 @@ class TestPlayableDeck(unittest.TestCase): - def test_playable_deck_with_no_sideboard(self): - pd = PlayableDeck(data.DECK_MAIN, []) + def test_playable_deck_with_no_sideboard(self) -> None: + pd: PlayableDeck = PlayableDeck(data.DECK_MAIN, []) self.assertIsNotNone(pd) self.assertListEqual(data.DECK_MAIN, pd.mainboard) self.assertEqual(len(pd.sideboard), 0) - def test_playable_deck_with_more_than_sixty_main(self): - main_deck = data.DECK_MAIN + [PlayedCard(4, "Fake Card")] + def test_playable_deck_with_more_than_sixty_main(self) -> None: + main_deck: list[PlayedCard] = data.DECK_MAIN + [PlayedCard(4, "Fake Card")] - pd = PlayableDeck(main_deck, []) + pd: PlayableDeck = PlayableDeck(main_deck, []) self.assertIsNotNone(pd) self.assertListEqual(main_deck, pd.mainboard) self.assertEqual(len(pd.sideboard), 0) - def test_playable_deck_with_less_than_sixty_main(self): - main_deck = data.DECK_MAIN[:-1] + def test_playable_deck_with_less_than_sixty_main(self) -> None: + main_deck: list[PlayedCard] = data.DECK_MAIN[:-1] with self.assertRaises(ValueError): PlayableDeck(main_deck, []) - def test_playable_deck_with_more_than_fifteen_side(self): - side_deck = data.DECK_SIDE + [PlayedCard(4, "Fake Card")] + def test_playable_deck_with_more_than_fifteen_side(self) -> None: + side_deck: list[PlayedCard] = data.DECK_SIDE + [PlayedCard(4, "Fake Card")] with self.assertRaises(ValueError): PlayableDeck(data.DECK_MAIN, side_deck) - def test_playable_deck(self): - pd = PlayableDeck(data.DECK_MAIN, data.DECK_SIDE) + def test_playable_deck(self) -> None: + pd: PlayableDeck = PlayableDeck(data.DECK_MAIN, data.DECK_SIDE) self.assertListEqual(data.DECK_MAIN, pd.mainboard) self.assertListEqual(data.DECK_SIDE, pd.sideboard) - def test_playable_deck_with_less_than_fifteen_side(self): - pd = PlayableDeck(data.DECK_MAIN, data.DECK_SIDE[:-1]) + def test_playable_deck_with_less_than_fifteen_side(self) -> None: + pd: PlayableDeck = PlayableDeck(data.DECK_MAIN, data.DECK_SIDE[:-1]) self.assertListEqual(data.DECK_MAIN, pd.mainboard) self.assertListEqual(data.DECK_SIDE[:-1], pd.sideboard) diff --git a/tests/test_servitor.py b/tests/test_servitor.py index 8d4f9735..e55b7b12 100644 --- a/tests/test_servitor.py +++ b/tests/test_servitor.py @@ -2,8 +2,8 @@ from pauperformance_bot.service.pauperformance.silver.deckstructor import Deckstructor -def test_servitor(): - expected_deck = PlayableDeck( +def test_servitor() -> None: + expected_deck: PlayableDeck = PlayableDeck( mainboard=[ PlayedCard(2, "Boseiju, Who Endures"), PlayedCard(4, "Cavern of Souls"), @@ -39,11 +39,11 @@ def test_servitor(): ) with open("tests/mock_data/modern_mtg_elves.txt", "r") as f: - text = f.read() + text: str = f.read() with open("tests/mock_data/pool.txt", "r") as f: - pool = f.readlines() + pool_lines: list[str] = f.readlines() - pool = [p.strip() for p in pool] - deck = Deckstructor.extract_deck(text, pool) + pool: list[str] = [p.strip() for p in pool_lines] + deck: PlayableDeck = Deckstructor.extract_deck(text, pool) assert deck == expected_deck From 50c54f9fab8769087911d2b0ae76a52f40a40bd2 Mon Sep 17 00:00:00 2001 From: Lucas Feliciano Date: Sun, 1 Mar 2026 18:06:45 +0100 Subject: [PATCH 5/5] Fix mypy strict errors and enable ignore_missing_imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix type annotations to satisfy mypy strict mode (148 → 0 errors) - Widen credential param types to str | None to match defaults - Add type: ignore comments for unfixable issues (untyped libs, etc.) - Fix unused type: ignore comments flagged by warn_unused_ignores - Add ignore_missing_imports and warn_unused_ignores to mypy config --- pauperformance_bot/__init__.py | 4 +-- pauperformance_bot/cli/builder/runnable.py | 3 ++ .../constant/pauperformance/myr.py | 2 +- pauperformance_bot/credentials.py | 2 +- pauperformance_bot/entity/api/archetype.py | 6 ++-- pauperformance_bot/service/academy/academy.py | 32 ++++++++++--------- .../service/academy/data_exporter.py | 30 +++++++++-------- pauperformance_bot/service/arena/twitch.py | 4 +-- pauperformance_bot/service/arena/youtube.py | 2 +- pauperformance_bot/service/mtg/deckstats.py | 4 +-- .../service/mtg/downloader/moxfield.py | 2 +- .../service/mtg/downloader/service.py | 4 +-- pauperformance_bot/service/mtg/mtggoldfish.py | 10 +++--- pauperformance_bot/service/mtg/scryfall.py | 7 ++-- pauperformance_bot/service/mtg/wizards.py | 6 ++-- .../service/nexus/abstract_discord_service.py | 8 ++--- .../service/nexus/async_discord_service.py | 2 +- .../service/nexus/sync/members_fetcher.py | 2 +- .../service/nexus/sync/messages_sender.py | 2 +- .../service/nexus/sync_discord_service.py | 2 +- .../pauperformance/archive/abstract.py | 12 ++++--- .../service/pauperformance/archive/local.py | 2 +- .../pauperformance/archive/mtggoldfish.py | 24 +++++++------- .../pauperformance/async_pauperformance.py | 3 ++ .../service/pauperformance/config_reader.py | 2 +- .../service/pauperformance/pauperformance.py | 32 +++++++++---------- .../pauperformance/silver/decklassifier.py | 19 ++++++----- .../pauperformance/silver/deckstatistics.py | 6 ++-- .../pauperformance/storage/dropbox_.py | 10 +++--- pauperformance_bot/task/silver.py | 4 +-- pauperformance_bot/util/config.py | 4 +-- pauperformance_bot/util/decorators.py | 4 +-- pauperformance_bot/util/entities.py | 4 +-- pauperformance_bot/util/path.py | 4 +-- pauperformance_bot/util/request.py | 2 +- tox.ini | 2 ++ 36 files changed, 148 insertions(+), 120 deletions(-) diff --git a/pauperformance_bot/__init__.py b/pauperformance_bot/__init__.py index 0de4bdae..e0433b11 100644 --- a/pauperformance_bot/__init__.py +++ b/pauperformance_bot/__init__.py @@ -1,3 +1,3 @@ -from importlib.metadata import version +from importlib.metadata import version as _get_version -version = version("pauperformance_bot") +version: str = _get_version("pauperformance_bot") diff --git a/pauperformance_bot/cli/builder/runnable.py b/pauperformance_bot/cli/builder/runnable.py index 8942a87e..1d1fd3dd 100644 --- a/pauperformance_bot/cli/builder/runnable.py +++ b/pauperformance_bot/cli/builder/runnable.py @@ -11,6 +11,9 @@ def __init__(self, name: str) -> None: def name(self) -> str: return self._name + def add_parser_argument(self, tool_parser: ArgumentParser) -> None: + pass + @abstractmethod def get_cli_parser(self) -> ArgumentParser: pass diff --git a/pauperformance_bot/constant/pauperformance/myr.py b/pauperformance_bot/constant/pauperformance/myr.py index fbe74c46..30827444 100644 --- a/pauperformance_bot/constant/pauperformance/myr.py +++ b/pauperformance_bot/constant/pauperformance/myr.py @@ -54,7 +54,7 @@ if ( "VIRTUAL_ENV" in os.environ and "PYCHARM_HOSTED" not in os.environ ): # running in venv, outside PyCharm - RESOURCES_DIR = posix_path(os.getenv("VIRTUAL_ENV"), "resources") + RESOURCES_DIR = posix_path(os.environ["VIRTUAL_ENV"], "resources") else: RESOURCES_DIR = posix_path(TOP_PATH.as_posix(), "resources") diff --git a/pauperformance_bot/credentials.py b/pauperformance_bot/credentials.py index e5f8ff63..35795add 100644 --- a/pauperformance_bot/credentials.py +++ b/pauperformance_bot/credentials.py @@ -7,7 +7,7 @@ def _get_credential_from_secrets(credential_key: str) -> str | None: try: # will succeed locally if secret.py file is available secret_module = import_module(SECRETS_UNTRACKED_FILE.rstrip(".py")) - return getattr(secret_module, credential_key) + return getattr(secret_module, credential_key) # type: ignore[no-any-return] except ModuleNotFoundError: # will fail on Heroku after deployments return None diff --git a/pauperformance_bot/entity/api/archetype.py b/pauperformance_bot/entity/api/archetype.py index a10a412d..6c6e1e65 100644 --- a/pauperformance_bot/entity/api/archetype.py +++ b/pauperformance_bot/entity/api/archetype.py @@ -1,7 +1,9 @@ from pauperformance_bot.entity.config.archetype import ( ArchetypeConfig, DiscordResource, - Resource, +) +from pauperformance_bot.entity.config.archetype import Resource as Resource +from pauperformance_bot.entity.config.archetype import ( SideboardResource, ) from pauperformance_bot.util.decorators import auto_repr, auto_str @@ -32,7 +34,7 @@ def __init__( self, *, name: str, - aliases: list[str] | None, + aliases: list[str], family: str | None, dominant_mana: list[str], game_type: list[str], diff --git a/pauperformance_bot/service/academy/academy.py b/pauperformance_bot/service/academy/academy.py index 6fd88382..5edf72cd 100644 --- a/pauperformance_bot/service/academy/academy.py +++ b/pauperformance_bot/service/academy/academy.py @@ -2,6 +2,7 @@ import glob from collections import defaultdict from pathlib import Path +from typing import Any from deprecated import deprecated @@ -64,7 +65,7 @@ def __init__( ) -> None: self.pauperformance: PauperformanceService = pauperformance self.scryfall: ScryfallService = pauperformance.scryfall - self.set_index: collections.OrderedDict[int, dict[str, str]] = ( + self.set_index: collections.OrderedDict[int, dict[str, Any]] = ( pauperformance.set_index ) @@ -124,8 +125,8 @@ def update_archetypes_index( archetypes = [] for archetype_config_file in glob.glob(f"{config_pages_dir}/*.ini"): logger.info(f"Processing {archetype_config_file}") - config = read_archetype_config(archetype_config_file) - values = config["values"] + config: dict[str, Any] = read_archetype_config(archetype_config_file) + values: dict[str, Any] = config["values"] archetypes.append( { "name": values["name"], @@ -202,13 +203,14 @@ def update_archetypes( ) -> None: logger.info("Generating archetypes...") all_decks = self.pauperformance.list_archived_decks() - banned_cards = [c["name"] for c in self.scryfall.get_banned_cards()] + banned_cards_data: list[dict[str, Any]] = self.scryfall.get_banned_cards() # type: ignore[assignment] + banned_cards: list[str] = [c["name"] for c in banned_cards_data] videos = self.pauperformance.list_videos() loader = AcademyDataLoader() for archetype_config_file in glob.glob(f"{config_pages_dir}/*.ini"): logger.info(f"Processing {archetype_config_file}") - config = read_archetype_config(archetype_config_file) - values = config["values"] + config: dict[str, Any] = read_archetype_config(archetype_config_file) + values: dict[str, Any] = config["values"] resources = config["resources"] archetype_name = values["name"] archetype_decks = [ @@ -217,12 +219,12 @@ def update_archetypes( for deck in archetype_decks: playable_deck = self.pauperformance.archive.to_playable_deck(deck) - deck.legality = ( + deck.legality = ( # type: ignore[attr-defined] "✅" if playable_deck.is_legal(banned_cards) else "Ban 🔨" ) p12e_set = self.pauperformance.set_index[int(deck.p12e_code)] - deck.set_name = p12e_set["name"] - deck.set_date = p12e_set["date"] + deck.set_name = p12e_set["name"] # type: ignore[attr-defined] + deck.set_date = p12e_set["date"] # type: ignore[attr-defined] # Staples and frequents can be built: # a) from archived decks @@ -331,16 +333,16 @@ def update_families( logger.debug("Building families-archetypes map...") families_map = defaultdict(list) for archetype_config_file in glob.glob(f"{config_archetypes_dir}/*.ini"): - config = read_archetype_config(archetype_config_file) - values = config["values"] - if values["family"]: - families_map[values["family"]].append(values["name"]) + config_data: dict[str, Any] = read_archetype_config(archetype_config_file) + arch_values: dict[str, Any] = config_data["values"] + if arch_values["family"]: + families_map[arch_values["family"]].append(arch_values["name"]) logger.info(f"Families map: {families_map}") for family_name in families_map.keys(): family_config_file = posix_path(config_families_dir, f"{family_name}.ini") logger.info(f"Processing {family_config_file}") - values = read_family_config(family_config_file) + values: dict[str, Any] = read_family_config(family_config_file) if values["name"] != family_name: raise ValueError() values["archetypes"] = [ @@ -396,7 +398,7 @@ def _boldify_sets_with_new_cards(self) -> list[dict[str, str]]: bolded_index = [] for item in self.set_index.values(): p12e_code = item["p12e_code"] - if len(card_index[p12e_code]) == 0: + if len(card_index[int(p12e_code)]) == 0: bolded_index.append(item) else: bolded_index.append({k: f"**{v}**" for k, v in item.items()}) diff --git a/pauperformance_bot/service/academy/data_exporter.py b/pauperformance_bot/service/academy/data_exporter.py index 75eeb4ac..0743c66f 100644 --- a/pauperformance_bot/service/academy/data_exporter.py +++ b/pauperformance_bot/service/academy/data_exporter.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Any import jsonpickle import matplotlib.pyplot as plt @@ -99,8 +100,8 @@ def export_archetypes(self) -> None: ).build_metadata_for(archetype.name) staples, frequents = statistics.get_staple_and_frequent_cards() - staples = self.scryfall.get_archetype_cards(staples) - frequents = self.scryfall.get_archetype_cards(frequents) + staples_cards = self.scryfall.get_archetype_cards(staples) + frequents_cards = self.scryfall.get_archetype_cards(frequents) api_archetype = Archetype( name=archetype.name, aliases=archetype.aliases, @@ -114,8 +115,8 @@ def export_archetypes(self) -> None: resource_sideboard=archetype.resource_sideboard, resources_discord=archetype.resources_discord, resources=archetype.resources, - staples=staples, - frequent=frequents, + staples=staples_cards, + frequent=frequents_cards, ) safe_dump_json_to_file( self.academy_fs.ASSETS_DATA_ARCHETYPE_DIR, @@ -128,7 +129,8 @@ def export_archetypes(self) -> None: def export_decks(self) -> None: logger.info(f"Exporting decks to {self.academy_fs.ASSETS_DATA_DECK_DIR}...") - banned_cards = [c["name"] for c in self.scryfall.get_banned_cards()] + banned_cards_data: list[dict[str, Any]] = self.scryfall.get_banned_cards() # type: ignore[assignment] + banned_cards = [c["name"] for c in banned_cards_data] for deck in self.decks: set_index_entry = self.pauperformance.set_index[int(deck.p12e_code)] playable_deck: PlayableDeck = self.pauperformance.archive.to_playable_deck( @@ -156,7 +158,7 @@ def export_intel_cards(self) -> None: logger.info( f"Exporting cards intel to {self.academy_fs.ASSETS_DATA_INTEL_CARD_DIR}..." ) - cards_intel = {} + cards_intel: dict[str, dict[str, Any]] = {} for kd in self.silver.known_decks: deck = kd[0] @@ -258,7 +260,7 @@ def export_youtube_videos(self) -> None: f"Exported YouTube videos to {self.academy_fs.ASSETS_DATA_VIDEO_DIR}." ) - def _export_videos(self, video_keys: list[str]) -> None: + def _export_videos(self, video_keys: set[str] | list[str]) -> None: for video_key in video_keys: video_path = posix_path( self.pauperformance.storage.youtube_video_path, @@ -346,16 +348,17 @@ def export_intel_decks(self) -> None: ) def _classify_mtggoldfish_tournament_decks(self) -> None: - banned_cards = [c["name"] for c in self.scryfall.get_banned_cards()] + banned_cards_data: list[dict[str, Any]] = self.scryfall.get_banned_cards() # type: ignore[assignment] + banned_cards = [c["name"] for c in banned_cards_data] already_classified_deck_ids = set( p.as_posix().split("/")[-1].replace(".json", "") for p in Path(self.academy_fs.ASSETS_DATA_INTEL_DECK_DIR).rglob("*.json") ) unclassified_decks_count = 0 - for playable_deck_file in Path( + for playable_deck_path_obj in Path( self.academy_fs.ASSETS_DATA_DECK_MTGGOLDFISH_TOURNAMENT_DIR ).glob("*.txt"): - playable_deck_file = playable_deck_file.as_posix() + playable_deck_file = playable_deck_path_obj.as_posix() deck_id = playable_deck_file.split("/")[-1].replace(".txt", "") if deck_id in already_classified_deck_ids: logger.debug( @@ -439,7 +442,8 @@ def classify_deck( def _label_mtggoldfish_tournament_decks( self, latest_training_sample: str | None ) -> None: - banned_cards = [c["name"] for c in self.scryfall.get_banned_cards()] + banned_cards_data2: list[dict[str, Any]] = self.scryfall.get_banned_cards() # type: ignore[assignment] + banned_cards = [c["name"] for c in banned_cards_data2] already_classified_deck_ids = set( p.as_posix().split("/")[-1].replace(".json", "") for p in Path(self.academy_fs.ASSETS_DATA_INTEL_DECK_DIR).rglob("*.json") @@ -453,14 +457,14 @@ def _label_mtggoldfish_tournament_decks( skip_list = set( line for line in open(skip_f, "r").read().splitlines() if line != "" ) - for playable_deck_file in sorted( + for playable_deck_path_obj2 in sorted( Path(self.academy_fs.ASSETS_DATA_DECK_MTGGOLDFISH_TOURNAMENT_DIR).glob( "*.txt" ), reverse=True, key=lambda d: int(d.as_posix().split("/")[-1].replace(".txt", "")), ): - playable_deck_file = playable_deck_file.as_posix() + playable_deck_file = playable_deck_path_obj2.as_posix() deck_id = playable_deck_file.split("/")[-1].replace(".txt", "") if deck_id in already_classified_deck_ids: logger.debug( diff --git a/pauperformance_bot/service/arena/twitch.py b/pauperformance_bot/service/arena/twitch.py index b2d70788..d72742c1 100644 --- a/pauperformance_bot/service/arena/twitch.py +++ b/pauperformance_bot/service/arena/twitch.py @@ -17,8 +17,8 @@ class TwitchService: def __init__( self, - myr_client_id: str = TWITCH_APP_CLIENT_ID, - myr_client_secret: str = TWITCH_APP_CLIENT_SECRET, + myr_client_id: str | None = TWITCH_APP_CLIENT_ID, + myr_client_secret: str | None = TWITCH_APP_CLIENT_SECRET, ) -> None: self._service = Twitch(myr_client_id, myr_client_secret) diff --git a/pauperformance_bot/service/arena/youtube.py b/pauperformance_bot/service/arena/youtube.py index fbbeb930..25855ec8 100644 --- a/pauperformance_bot/service/arena/youtube.py +++ b/pauperformance_bot/service/arena/youtube.py @@ -14,7 +14,7 @@ class YouTubeService: def __init__( self, - myr_client_id: str = YOUTUBE_API_KEY, + myr_client_id: str | None = YOUTUBE_API_KEY, ) -> None: self._service = Api(api_key=myr_client_id) diff --git a/pauperformance_bot/service/mtg/deckstats.py b/pauperformance_bot/service/mtg/deckstats.py index 925c3f1e..ea15f74d 100644 --- a/pauperformance_bot/service/mtg/deckstats.py +++ b/pauperformance_bot/service/mtg/deckstats.py @@ -119,7 +119,7 @@ def get_deck( ) as cache_f: deck = pickle.load(cache_f) logger.debug(f"Loaded deck from cache: {deck}") - return deck + return deck # type: ignore[no-any-return] except FileNotFoundError: pass logger.debug("No cache found for deck.") @@ -137,7 +137,7 @@ def get_deck( deck = json.loads(response.content) with open(posix_path(decks_cache_dir, f"{deck_id}.pkl"), "wb") as cache_f: pickle.dump(deck, cache_f) - return deck + return deck # type: ignore[no-any-return] def to_playable_deck(self, deckstats_deck: dict[str, Any]) -> PlayableDeck: logger.info(f"Parsing deckstats deck {deckstats_deck['saved_id']} list...") diff --git a/pauperformance_bot/service/mtg/downloader/moxfield.py b/pauperformance_bot/service/mtg/downloader/moxfield.py index 7519f4a7..1f37a113 100644 --- a/pauperformance_bot/service/mtg/downloader/moxfield.py +++ b/pauperformance_bot/service/mtg/downloader/moxfield.py @@ -21,7 +21,7 @@ def __init__(self, url: str) -> None: super().__init__(url) self._downloader = MtgoDeckDownloader( url, - headers={"User-Agent": MOXFIELD_USER_AGENT}, + headers={"User-Agent": MOXFIELD_USER_AGENT or ""}, ) def download(self) -> PlayableDeck: diff --git a/pauperformance_bot/service/mtg/downloader/service.py b/pauperformance_bot/service/mtg/downloader/service.py index 0b85581e..e57c20d4 100644 --- a/pauperformance_bot/service/mtg/downloader/service.py +++ b/pauperformance_bot/service/mtg/downloader/service.py @@ -29,10 +29,10 @@ def from_url(cls, url: str) -> PlayableDeck | None: Returns: PlayableDeck | None: the representation of the deck or None. """ - for domain, cls in DeckDownloaderService._downloaders.items(): + for domain, downloader_cls in DeckDownloaderService._downloaders.items(): if domain in url: logger.debug(f"Found specific downloader for {url}") - return cls(url).download() + return downloader_cls(url).download() # TODO check if we want to catch only specific errors here try: logger.debug(f"Using mtgo downloader for {url}") diff --git a/pauperformance_bot/service/mtg/mtggoldfish.py b/pauperformance_bot/service/mtg/mtggoldfish.py index e796b1c3..184efb3f 100644 --- a/pauperformance_bot/service/mtg/mtggoldfish.py +++ b/pauperformance_bot/service/mtg/mtggoldfish.py @@ -123,9 +123,9 @@ def get_pauper_tournaments( try: html_page = urllib.request.urlopen(url) bs = BeautifulSoup(html_page.read(), features="lxml") - rows = bs.findAll("tr")[1:] # skip header + rows = bs.find_all("tr")[1:] # skip header i = 0 - for a in bs.findAll("a"): + for a in bs.find_all("a"): if ( "href" in a.attrs and "tournament" in a["href"] @@ -143,7 +143,7 @@ def get_pauper_tournaments( time.sleep(REQUESTS_SLEEP_SECONDS) # avoid flooding (and soft-ban) except HTTPError: # 400: Bad Request if we exceed pages break - if any("No tournaments found." == x.text.strip() for x in bs.findAll("p")): + if any("No tournaments found." == x.text.strip() for x in bs.find_all("p")): break if page == 4: logger.warning( @@ -161,7 +161,7 @@ def get_tournament_decks( tournament_decks: list[MTGGoldfishTournamentDeck] = [] html_page = urllib.request.urlopen(url) bs = BeautifulSoup(html_page.read(), features="lxml") - for tr in bs.findAll("tr")[1:]: # skip header + for tr in bs.find_all("tr")[1:]: # skip header columns = tr.contents if len(columns) < 10: continue @@ -181,7 +181,7 @@ def get_tournament_decks( except (ValueError, IndexError): mtgo_price = None tournament_deck = MTGGoldfishTournamentDeck( - url=f"{API_ENDPOINT}{list(columns[3].children)[1]['href']}", + url=f"{API_ENDPOINT}{list(columns[3].children)[1]['href']}", # type: ignore[attr-defined] archetype=archetype, place=place, pilot=pilot, diff --git a/pauperformance_bot/service/mtg/scryfall.py b/pauperformance_bot/service/mtg/scryfall.py index a379a993..67d0a498 100644 --- a/pauperformance_bot/service/mtg/scryfall.py +++ b/pauperformance_bot/service/mtg/scryfall.py @@ -30,7 +30,7 @@ def get_sets(self) -> dict[str, Any]: url = f"{self.endpoint}/sets" method = requests.get response = execute_http_request(method, url) - return json.loads(response.content) + return json.loads(response.content) # type: ignore[no-any-return] def get_card_named( self, @@ -44,7 +44,7 @@ def get_card_named( card = pickle.load(cache_f) logger.debug(f"Loaded card from cache: {exact_card_name}") # logger.debug(f"Card: {card}") - return card + return card # type: ignore[no-any-return] except FileNotFoundError: logger.debug(f"No cache found for card {exact_card_name}.") url = f"{self.endpoint}/cards/named" @@ -58,7 +58,7 @@ def get_card_named( posix_path(cards_cache_dir, to_pkl_name(exact_card_name)), "wb" ) as cache_f: pickle.dump(card, cache_f) - return card + return card # type: ignore[no-any-return] except requests.exceptions.HTTPError as exc: if exc.response.status_code == 404: message = f"Absent card in Scryfall: {exact_card_name}." @@ -87,6 +87,7 @@ def search_cards(self, query: str) -> list[dict[str, Any]] | dict[str, Any]: response = json.loads(exc.response.content) if exc.response.status_code == 404 and response["code"] == "not_found": return {} + raise @lru_cache(maxsize=1) def get_legal_lands(self) -> list[dict[str, Any]] | dict[str, Any]: diff --git a/pauperformance_bot/service/mtg/wizards.py b/pauperformance_bot/service/mtg/wizards.py index 09bb7f0c..ea60a726 100644 --- a/pauperformance_bot/service/mtg/wizards.py +++ b/pauperformance_bot/service/mtg/wizards.py @@ -16,14 +16,16 @@ def _parse_match_result(player1_line: str, player2_line: str) -> MTGOStandingMatch: result = re.search(r"\(([0-9]+)\) (\w+), ([0-9-]+)", player1_line) + assert result is not None, f"Failed to parse player1 line: {player1_line}" player1_ranking, player1, match_score = result.groups() result = re.search(r"\(([0-9]+)\) (\w+)", player2_line) + assert result is not None, f"Failed to parse player2 line: {player2_line}" player2_ranking, player2 = result.groups() return MTGOStandingMatch( player1, - player1_ranking, + int(player1_ranking), player2, - player2_ranking, + int(player2_ranking), match_score, ) diff --git a/pauperformance_bot/service/nexus/abstract_discord_service.py b/pauperformance_bot/service/nexus/abstract_discord_service.py index c77398c4..2ba4b64d 100644 --- a/pauperformance_bot/service/nexus/abstract_discord_service.py +++ b/pauperformance_bot/service/nexus/abstract_discord_service.py @@ -8,10 +8,10 @@ logger = get_application_logger() -class AbstractDiscordService(discord.Client, ABC): +class AbstractDiscordService(discord.Client, ABC): # type: ignore[misc] def __init__( self, - myr_bot_token: str, + myr_bot_token: str | None, import_deck_channel_id: int, welcome_channel_id: int, myr_log_channel_id: int, @@ -29,11 +29,11 @@ def log_channel(self) -> discord.TextChannel | None: pass async def send_log_message(self, message: str) -> None: - await self.log_channel.send(message) + await self.log_channel.send(message) # type: ignore[union-attr] async def send_user_message(self, user_id: int, message: str) -> None: user = await self.fetch_user(user_id) await user.send(message) def list_roles(self, guild_id: int) -> list[discord.Role]: - return self.get_guild(guild_id).roles + return self.get_guild(guild_id).roles # type: ignore[no-any-return] diff --git a/pauperformance_bot/service/nexus/async_discord_service.py b/pauperformance_bot/service/nexus/async_discord_service.py index 71175648..6a745d8e 100644 --- a/pauperformance_bot/service/nexus/async_discord_service.py +++ b/pauperformance_bot/service/nexus/async_discord_service.py @@ -25,7 +25,7 @@ class AsyncDiscordService(AbstractDiscordService): def __init__( self, - myr_bot_token: str = DISCORD_BOT_TOKEN, + myr_bot_token: str | None = DISCORD_BOT_TOKEN, import_deck_channel_id: int = DISCORD_CHANNEL_IMPORT_DECK_ID, welcome_channel_id: int = DISCORD_CHANNEL_WELCOME_ID, myr_log_channel_id: int = DISCORD_CHANNEL_MYR_LOG_ID, diff --git a/pauperformance_bot/service/nexus/sync/members_fetcher.py b/pauperformance_bot/service/nexus/sync/members_fetcher.py index 158a877e..13f98cff 100644 --- a/pauperformance_bot/service/nexus/sync/members_fetcher.py +++ b/pauperformance_bot/service/nexus/sync/members_fetcher.py @@ -18,7 +18,7 @@ class DiscordMembersFetcherSyncService(AbstractSyncDiscordService): def __init__( self, - myr_bot_token: str = DISCORD_BOT_TOKEN, + myr_bot_token: str | None = DISCORD_BOT_TOKEN, import_deck_channel_id: int = DISCORD_CHANNEL_IMPORT_DECK_ID, welcome_channel_id: int = DISCORD_CHANNEL_WELCOME_ID, myr_log_channel_id: int = DISCORD_CHANNEL_MYR_LOG_ID, diff --git a/pauperformance_bot/service/nexus/sync/messages_sender.py b/pauperformance_bot/service/nexus/sync/messages_sender.py index 48cdaddb..a75cda2e 100644 --- a/pauperformance_bot/service/nexus/sync/messages_sender.py +++ b/pauperformance_bot/service/nexus/sync/messages_sender.py @@ -18,7 +18,7 @@ class DiscordMessagesSenderSyncService(AbstractSyncDiscordService): def __init__( self, messages: list[str], - myr_bot_token: str = DISCORD_BOT_TOKEN, + myr_bot_token: str | None = DISCORD_BOT_TOKEN, import_deck_channel_id: int = DISCORD_CHANNEL_IMPORT_DECK_ID, welcome_channel_id: int = DISCORD_CHANNEL_WELCOME_ID, myr_log_channel_id: int = DISCORD_CHANNEL_MYR_LOG_ID, diff --git a/pauperformance_bot/service/nexus/sync_discord_service.py b/pauperformance_bot/service/nexus/sync_discord_service.py index 105ba782..7f64d025 100644 --- a/pauperformance_bot/service/nexus/sync_discord_service.py +++ b/pauperformance_bot/service/nexus/sync_discord_service.py @@ -21,7 +21,7 @@ class AbstractSyncDiscordService(AbstractDiscordService, ABC): def __init__( self, - myr_bot_token: str = DISCORD_BOT_TOKEN, + myr_bot_token: str | None = DISCORD_BOT_TOKEN, import_deck_channel_id: int = DISCORD_CHANNEL_IMPORT_DECK_ID, welcome_channel_id: int = DISCORD_CHANNEL_WELCOME_ID, myr_log_channel_id: int = DISCORD_CHANNEL_MYR_LOG_ID, diff --git a/pauperformance_bot/service/pauperformance/archive/abstract.py b/pauperformance_bot/service/pauperformance/archive/abstract.py index e7aad49d..aad41ca0 100644 --- a/pauperformance_bot/service/pauperformance/archive/abstract.py +++ b/pauperformance_bot/service/pauperformance/archive/abstract.py @@ -81,6 +81,8 @@ async def import_player_decks_from_deckstats( send_notification: bool = True, ) -> None: # TODO: get rid of players_by_deckstats_id logger.info(f"Updating archive decks for {player.name}...") + assert player.deckstats_id is not None + assert player.deckstats_name is not None deckstats = DeckstatsService(owner_id=player.deckstats_id) imported_deckstats_deck = storage.list_imported_deckstats_deck_ids() for deckstats_deck in deckstats.list_pauperformance_decks( @@ -91,7 +93,7 @@ async def import_player_decks_from_deckstats( f"({deckstats_deck.saved_id}) " f"from {deckstats_deck.owner_name}, " f"uploaded by " - f"{players_by_deckstats_id[deckstats_deck.owner_id].name} " + f"{players_by_deckstats_id[int(deckstats_deck.owner_id)].name} " f"({deckstats_deck.owner_id})..." ) if str(deckstats_deck.saved_id) in imported_deckstats_deck: @@ -309,7 +311,7 @@ async def archive_player_videos_from_youtube( logger.info(f"Archiving information on storage in file {storage_key}...") try: base_archetype_name = config_reader.get_archetype_name_from_alias( - video.archetype + video.archetype # type: ignore[arg-type] ) except PauperformanceException: base_archetype_name = "Brew" @@ -448,9 +450,9 @@ async def import_player_deck_from_mtggoldfish( if len(deck_group) == 0: revision = "001" else: - revision = int(max((d.revision for d in deck_group))) - revision += 1 - revision = str(revision).zfill(3) + revision_num = int(max((d.revision for d in deck_group))) + revision_num += 1 + revision = str(revision_num).zfill(3) logger.debug(f"Computed revision: {revision}.") p12e_name = ( f"{p12e_name} " f"{set_id['p12e_code']}.{revision}.{player.name}" diff --git a/pauperformance_bot/service/pauperformance/archive/local.py b/pauperformance_bot/service/pauperformance/archive/local.py index 6275f744..8f6e0fd6 100644 --- a/pauperformance_bot/service/pauperformance/archive/local.py +++ b/pauperformance_bot/service/pauperformance/archive/local.py @@ -60,7 +60,7 @@ def create_deck( def list_decks(self) -> list[AbstractArchivedDeck]: logger.info(f"Listing decks in {self._root_dir}...") - decks = [] + decks: list[AbstractArchivedDeck] = [] for file in ( join(self._root_dir, f) for f in listdir(self._root_dir) diff --git a/pauperformance_bot/service/pauperformance/archive/mtggoldfish.py b/pauperformance_bot/service/pauperformance/archive/mtggoldfish.py index e37706d0..e14d808d 100644 --- a/pauperformance_bot/service/pauperformance/archive/mtggoldfish.py +++ b/pauperformance_bot/service/pauperformance/archive/mtggoldfish.py @@ -55,8 +55,8 @@ class MTGGoldfishArchiveService(AbstractArchiveService): def __init__( self, storage: Any, - email: str = MTGGOLDFISH_PAUPERFORMANCE_USERNAME, - password: str = MTGGOLDFISH_PAUPERFORMANCE_PASSWORD, + email: str | None = MTGGOLDFISH_PAUPERFORMANCE_USERNAME, + password: str | None = MTGGOLDFISH_PAUPERFORMANCE_PASSWORD, endpoint: str = API_ENDPOINT, deck_api_endpoint: str = DECK_API_ENDPOINT, ) -> None: @@ -101,7 +101,7 @@ def _parse_login_authenticity_token(response: Any) -> str: token = token[0 : token.find('"')] if len(token) >= MIN_AUTHENTICITY_TOKEN_LEN: logger.debug(f"Found authenticity_token: {token}") - return token + return token # type: ignore[no-any-return] raise MTGGoldfishException("Unable to get authenticity_token from MTGGoldfish.") @staticmethod @@ -115,7 +115,7 @@ def _parse_meta_authenticity_token(response: Any) -> str: line.rfind(content_token) + len(content_token) + 1 : -4 ] logger.debug(f"Found authenticity_token: {authenticity_token}") - return authenticity_token + return authenticity_token # type: ignore[no-any-return] raise MTGGoldfishException("Unable to get authenticity_token from MTGGoldfish.") def _get_login_info(self) -> tuple[str, str]: @@ -201,6 +201,7 @@ def _create_deck( ) if response.status_code != 200: raise MTGGoldfishException(f"Failed to create deck {name} for {self.email}") + assert response.request.url is not None deck_id = response.request.url.split("/")[-1] logger.info(f"Created deck {name} for {self.email}. Id: {deck_id}") return deck_id @@ -258,7 +259,7 @@ def _list_decks_in_page( raise MTGGoldfishException(f"Failed to list decks for {self.email}") logger.debug(f"Parsing page with decks for {self.email}...") pq = PyQuery(response.content) - decks = [] + decks: list[AbstractArchivedDeck] = [] for c in pq("table tbody tr").items(): row = c.text() _, name, _, format_, creation_date, visibility, _, _ = row.split("\n") @@ -344,6 +345,7 @@ def to_playable_deck( use_cache: bool = True, ) -> PlayableDeck: # TODO: fix ASSETS_DATA_DECK_MTGGOLDFISH_TOURNAMENT_DIR + assert decks_cache_dir is not None lines = None to_be_cached = True if use_cache: @@ -359,12 +361,12 @@ def to_playable_deck( except FileNotFoundError: pass if not lines: - lines = [MTGGOLDFISH_THROTTLE_ERROR_RESPONSE] - while MTGGOLDFISH_THROTTLE_ERROR_RESPONSE in lines: - content = urlopen(listed_deck.download_txt_url).read() - lines = content.decode("utf-8") + raw_content = MTGGOLDFISH_THROTTLE_ERROR_RESPONSE + while MTGGOLDFISH_THROTTLE_ERROR_RESPONSE in raw_content: + content = urlopen(listed_deck.download_txt_url).read() # type: ignore[attr-defined] + raw_content = content.decode("utf-8") time.sleep(REQUESTS_SLEEP_SECONDS) - lines = lines.split("\r\n") + lines = raw_content.split("\r\n") if to_be_cached: with open( posix_path(decks_cache_dir, f"{listed_deck.deck_id}.txt"), @@ -377,5 +379,5 @@ def to_playable_deck( def get_deck(self, deck_name: str) -> AbstractArchivedDeck: for deck in self.list_decks(): if deck.p12e_name == deck_name: - return deck + return deck # type: ignore[no-any-return] raise ArchiveException(f"Unable to find deck with name {deck_name}.") diff --git a/pauperformance_bot/service/pauperformance/async_pauperformance.py b/pauperformance_bot/service/pauperformance/async_pauperformance.py index 6e99e3a5..9506d433 100644 --- a/pauperformance_bot/service/pauperformance/async_pauperformance.py +++ b/pauperformance_bot/service/pauperformance/async_pauperformance.py @@ -94,6 +94,7 @@ async def import_player_videos_from_twitch( self, player: PhDConfig, send_notification: bool = True ) -> None: logger.info(f"Processing videos from Twitch user {player.twitch_login_name}...") + assert player.twitch_login_name is not None twitch_user = self.twitch.get_user(player.twitch_login_name) warning_player: PhDConfig = self.config_reader.get_pauperformance_phd() await self.archive.archive_player_videos_from_twitch( @@ -127,6 +128,8 @@ async def import_player_videos_from_youtube( logger.info( f"Processing videos from YouTube user " f"{player.youtube_channel_id}..." ) + assert player.youtube_channel_id is not None + assert player.default_youtube_language is not None warning_player: PhDConfig = self.config_reader.get_pauperformance_phd() await self.archive.archive_player_videos_from_youtube( player, diff --git a/pauperformance_bot/service/pauperformance/config_reader.py b/pauperformance_bot/service/pauperformance/config_reader.py index ea44670b..37183269 100644 --- a/pauperformance_bot/service/pauperformance/config_reader.py +++ b/pauperformance_bot/service/pauperformance/config_reader.py @@ -34,7 +34,7 @@ def __init__(self, myr_file_system: MyrFileSystem = MyrFileSystem()) -> None: def _read_config_file(config_file_path: str) -> configparser.ConfigParser: logger.debug(f"Reading configuration file {config_file_path}...") config = configparser.ConfigParser(allow_no_value=True) - config.optionxform = lambda option: option # preserve case + config.optionxform = lambda option: option # type: ignore[method-assign, assignment] # preserve case config.read(config_file_path) logger.debug(f"Read configuration file {config_file_path}.") return config diff --git a/pauperformance_bot/service/pauperformance/pauperformance.py b/pauperformance_bot/service/pauperformance/pauperformance.py index 1c9b6bb6..0d4d5354 100644 --- a/pauperformance_bot/service/pauperformance/pauperformance.py +++ b/pauperformance_bot/service/pauperformance/pauperformance.py @@ -10,6 +10,7 @@ from requests.exceptions import HTTPError import pauperformance_bot +import pauperformance_bot.constant.pauperformance.pauperformance as p12e_constant from pauperformance_bot.constant.arena.twitch import TWITCH_VIDEO_URL from pauperformance_bot.constant.arena.youtube import YOUTUBE_VIDEO_URL from pauperformance_bot.constant.mtg.scryfall import REQUESTS_SLEEP_SECONDS @@ -75,9 +76,10 @@ def _build_set_index( ) -> collections.OrderedDict[int, dict[str, Any]]: try: logger.info("Building Scryfall set index...") - scryfall_sets = self.scryfall.get_sets() - scryfall_sets = sorted( - scryfall_sets["data"], key=lambda s: s["released_at"] + s["code"] + scryfall_sets_response = self.scryfall.get_sets() + scryfall_sets: list[dict[str, Any]] = sorted( + scryfall_sets_response["data"], + key=lambda s: s["released_at"] + s["code"], ) logger.info("Built Scryfall set index.") except HTTPError: # Scryfall may be unreachable @@ -124,7 +126,7 @@ def _build_card_index( skip_sets: list[int] = KNOWN_SETS_WITH_NO_PAUPER_CARDS, cards_index_cache_dir: str = PAUPER_CARDS_INDEX_CACHE_DIR, ) -> dict[int, list[dict[str, Any]]]: - card_index = {} + card_index: dict[int, list[dict[str, Any]]] = {} os.makedirs(cards_index_cache_dir, exist_ok=True) for item in self.set_index.values(): p12e_code = item["p12e_code"] @@ -132,7 +134,7 @@ def _build_card_index( card_index[p12e_code] = [] continue set_cache_file = posix_path(cards_index_cache_dir, f"{p12e_code}.json") - set_index = [] + set_index: list[dict[str, Any]] = [] try: with open(set_cache_file, "r", encoding="utf-8") as cache_f: set_index = json.load(cache_f) @@ -144,7 +146,7 @@ def _build_card_index( logger.debug(f"Missing cache for set {p12e_code}: querying Scryfall...") scryfall_code = item["scryfall_code"] query = f"set:{scryfall_code} rarity:common legal:pauper" - set_index = self.scryfall.search_cards(query) + set_index = self.scryfall.search_cards(query) # type: ignore[assignment] if len(set_index) > 0: with open(set_cache_file, "w") as cache_f: json.dump(set_index, cache_f) @@ -159,9 +161,7 @@ def _build_card_index( to_be_removed_sets = useless_sets - set(KNOWN_SETS_WITH_NO_PAUPER_CARDS) if len(to_be_removed_sets) > 0: constant_module = pauperformance_bot.__path__[0] - constant_file = ( - pauperformance_bot.constant.pauperformance.pauperformance.__file__ - ) + constant_file = p12e_constant.__file__ constant_relative_path = constant_file[len(constant_module) + 1 :] logger.warning( f"Please, update the list of known sets with no pauper cards " @@ -175,8 +175,8 @@ def _build_incremental_card_index( self, skip_sets: list[int] = INCREMENTAL_CARDS_INDEX_SKIP_SETS, ) -> dict[int, list[dict[str, Any]]]: - incremental_card_index = {} - existing_card_names = set() + incremental_card_index: dict[int, list[dict[str, Any]]] = {} + existing_card_names: set[str] = set() useless_sets = set() for p12e_code, cards in self.card_index.items(): logger.debug(f"Processing set with p12e_code: {p12e_code}...") @@ -202,9 +202,7 @@ def _build_incremental_card_index( to_be_removed_sets = useless_sets - set(INCREMENTAL_CARDS_INDEX_SKIP_SETS) if len(to_be_removed_sets) > 0: constant_module = pauperformance_bot.__path__[0] - constant_file = ( - pauperformance_bot.constant.pauperformance.pauperformance.__file__ - ) + constant_file = p12e_constant.__file__ constant_relative_path = constant_file[len(constant_module) + 1 :] logger.warning( f"Please, update the list of known sets to be skipped for the " @@ -222,6 +220,7 @@ def list_deckstats_decks(self) -> list[Any]: ) continue logger.info(f"Processing player {player.name}...") + assert player.deckstats_name is not None deckstats = DeckstatsService(owner_id=player.deckstats_id) player_decks = deckstats.list_pauperformance_decks(player.deckstats_name) logger.info(f"Found {len(player_decks)} decks.") @@ -261,7 +260,8 @@ def analyze_cards_frequency( # by leveraging silver.deckstatistics. if len(archetype_decks) < 2: return [], [] - lands = set(land["name"] for land in self.scryfall.get_legal_lands()) + legal_lands: list[dict[str, Any]] = self.scryfall.get_legal_lands() # type: ignore[assignment] + lands = set(land["name"] for land in legal_lands) decks_cards = {} all_cards = set() for deck in archetype_decks: @@ -282,7 +282,7 @@ def get_set_index_by_date(self, usa_date: str) -> dict[str, Any]: s for s in self.set_index.values() if s["date"] <= usa_date - and len(self.incremental_card_index.get(s["p12e_code"])) > 0 + and len(self.incremental_card_index.get(s["p12e_code"], [])) > 0 ][-1] def get_current_set_index(self) -> dict[str, Any]: diff --git a/pauperformance_bot/service/pauperformance/silver/decklassifier.py b/pauperformance_bot/service/pauperformance/silver/decklassifier.py index bd4f23c3..3fd06069 100644 --- a/pauperformance_bot/service/pauperformance/silver/decklassifier.py +++ b/pauperformance_bot/service/pauperformance/silver/decklassifier.py @@ -51,7 +51,9 @@ def add_known_decks( self.known_decks += known_decks @staticmethod - def _cosine_similarity(v1: list[float], v2: list[float], w: float = 1.0) -> float: + def _cosine_similarity( + v1: list[int] | list[float], v2: list[int] | list[float], w: float = 1.0 + ) -> float: if w == 0: return 1 return 1 - spatial.distance.cosine(v1, v2, w=len(v1) * [w]) @@ -121,7 +123,7 @@ def get_similarity(self, deck1: PlayableDeck, deck2: PlayableDeck) -> float: def _is_affinity(self, deck: PlayableDeck) -> bool: artifact_lands = self.pauperformance.scryfall.get_legal_artifact_lands() - artifact_lands_names = [c["name"] for c in artifact_lands] + artifact_lands_names = [c["name"] for c in artifact_lands] # type: ignore[index] affinity_creatures = [ "Frogmite", "Atog", @@ -223,9 +225,10 @@ def _is_azorius_prowess(self, deck: PlayableDeck) -> bool: def classify_deck( self, deck: PlayableDeck, - ) -> tuple[ArchetypeConfig, float]: + ) -> tuple[ArchetypeConfig | None, float]: logger.debug("Classifying deck...") - most_similar_archetype, highest_similarity = None, 0 + most_similar_archetype: ArchetypeConfig | None = None + highest_similarity: float = 0.0 # TODO: remove this block in the future if it becomes useless # First, check if archetype can be detected with rules. @@ -285,7 +288,7 @@ def get_metagame(self) -> Metagame: for link, values in mtggoldfish_meta.items(): share, playable_deck = values similar_archetype, similarity_score = self.classify_deck(playable_deck) - archetype_name = similar_archetype.name + archetype_name = similar_archetype.name if similar_archetype else "Unknown" if similarity_score < 0.30: archetype_name = "Brew" similarity_score = 1 - similarity_score @@ -343,7 +346,7 @@ def get_dpl_metagame( ) if highest_similarity < brew_threshold: most_similar_archetype = None - elif learn_on_the_fly: + elif learn_on_the_fly and most_similar_archetype is not None: self.known_decks.append((playable_deck, most_similar_archetype)) dpl_decks.append( DPLDeck( @@ -361,13 +364,13 @@ def get_dpl_metagame( dpl_decks=dpl_decks, ) logger.info(dpl_meta) - archetype_maps = defaultdict(int) + archetype_maps: defaultdict[str, int] = defaultdict(int) for dpl_deck in dpl_meta.dpl_decks: if not dpl_deck.archetype: print(f"WARNING: manually count {dpl_deck}") continue archetype_maps[dpl_deck.archetype] += 1 - game_types = defaultdict(int) + game_types: defaultdict[str, int] = defaultdict(int) print() for k, v in sorted(archetype_maps.items()): print(f"{v} {k}") diff --git a/pauperformance_bot/service/pauperformance/silver/deckstatistics.py b/pauperformance_bot/service/pauperformance/silver/deckstatistics.py index 2bb3500a..a5ee1234 100644 --- a/pauperformance_bot/service/pauperformance/silver/deckstatistics.py +++ b/pauperformance_bot/service/pauperformance/silver/deckstatistics.py @@ -1,3 +1,5 @@ +from typing import Any + from pauperformance_bot.entity.deck.playable import PlayableDeck from pauperformance_bot.exceptions import CardNotFoundException from pauperformance_bot.service.academy.data_loader import AcademyDataLoader @@ -14,7 +16,7 @@ def __init__( self, name: str, playable_decks: list[PlayableDeck], - cards: dict[str, dict[str, int]], + cards: dict[str, dict[str, Any]], ) -> None: self.name = name self._playable_decks = playable_decks @@ -117,7 +119,7 @@ def __init__( def build_metadata_for(self, archetype: str) -> Deckstatistics: playable_decks = self._academy_loader.load_classified_decks(archetype) logger.debug(f"Found {len(playable_decks)} decks for {archetype}") - all_cards = {} + all_cards: dict[str, dict[str, Any]] = {} for pd in playable_decks: occurred_in_deck = set() diff --git a/pauperformance_bot/service/pauperformance/storage/dropbox_.py b/pauperformance_bot/service/pauperformance/storage/dropbox_.py index ae614c33..27202776 100644 --- a/pauperformance_bot/service/pauperformance/storage/dropbox_.py +++ b/pauperformance_bot/service/pauperformance/storage/dropbox_.py @@ -24,9 +24,9 @@ class DropboxService(AbstractStorageService): def __init__( self, root_dir: str = MYR_ROOT_DIR, - refresh_token: str = DROPBOX_REFRESH_TOKEN, - app_key: str = DROPBOX_APP_KEY, - app_secret: str = DROPBOX_APP_SECRET, + refresh_token: str | None = DROPBOX_REFRESH_TOKEN, + app_key: str | None = DROPBOX_APP_KEY, + app_secret: str | None = DROPBOX_APP_SECRET, ) -> None: self._root_dir = root_dir self.app_key = app_key @@ -56,8 +56,8 @@ def _list_files(self, path: str, cursor: str | None = None) -> list[Any]: results = self._service.files_list_folder_continue(cursor) items = results.entries if not results.has_more: - return items - return items + self._list_files(path, results.cursor) + return items # type: ignore[no-any-return] + return items + self._list_files(path, results.cursor) # type: ignore[no-any-return] def create_file(self, name: str, content: str = "") -> None: logger.info(f"Storing file {name}...") diff --git a/pauperformance_bot/task/silver.py b/pauperformance_bot/task/silver.py index ec0dbb74..07a74e65 100644 --- a/pauperformance_bot/task/silver.py +++ b/pauperformance_bot/task/silver.py @@ -94,6 +94,6 @@ def dpl_classifier(environ: dict[str, Any], start_response: Any) -> list[bytes]: if __name__ == "__main__": main( - posix_path(TOP_PATH, "dev", "decks-all-tournaments.json"), - posix_path(TOP_PATH, "dev", "decks-all-tournaments-classified.json"), + posix_path(TOP_PATH.as_posix(), "dev", "decks-all-tournaments.json"), + posix_path(TOP_PATH.as_posix(), "dev", "decks-all-tournaments-classified.json"), ) diff --git a/pauperformance_bot/util/config.py b/pauperformance_bot/util/config.py index 3d9718c4..5e68ca2d 100644 --- a/pauperformance_bot/util/config.py +++ b/pauperformance_bot/util/config.py @@ -13,7 +13,7 @@ @deprecated(reason="Migrated") def read_config(config_file_path: str) -> configparser.ConfigParser: config = configparser.ConfigParser(allow_no_value=True) - config.optionxform = lambda option: option # preserve case + config.optionxform = lambda option: option # type: ignore[assignment] # preserve case config.read(config_file_path) logger.debug(f"Read configuration file: {config_file_path}") return config @@ -30,7 +30,7 @@ def read_archetype_config( } list_fields = ["aliases", "mana", "type"] for field in list_fields: - values[field] = _parse_list_value(config["values"][field]) + values[field] = _parse_list_value(config["values"][field]) # type: ignore[assignment] # read references references = {**config["references"]} # quick integrity check diff --git a/pauperformance_bot/util/decorators.py b/pauperformance_bot/util/decorators.py index f19ae421..6f3dfe0a 100644 --- a/pauperformance_bot/util/decorators.py +++ b/pauperformance_bot/util/decorators.py @@ -9,7 +9,7 @@ def __repr__(self: _C) -> str: class_attributes = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{fq_class_name}({class_attributes})" - cls.__repr__ = __repr__ # type: ignore[attr-defined] + cls.__repr__ = __repr__ # type: ignore[method-assign, assignment] return cls @@ -19,5 +19,5 @@ def __str__(self: _C) -> str: class_attributes = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{class_name}({class_attributes})" - cls.__str__ = __str__ # type: ignore[attr-defined] + cls.__str__ = __str__ # type: ignore[method-assign, assignment] return cls diff --git a/pauperformance_bot/util/entities.py b/pauperformance_bot/util/entities.py index f19ae421..6f3dfe0a 100644 --- a/pauperformance_bot/util/entities.py +++ b/pauperformance_bot/util/entities.py @@ -9,7 +9,7 @@ def __repr__(self: _C) -> str: class_attributes = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{fq_class_name}({class_attributes})" - cls.__repr__ = __repr__ # type: ignore[attr-defined] + cls.__repr__ = __repr__ # type: ignore[method-assign, assignment] return cls @@ -19,5 +19,5 @@ def __str__(self: _C) -> str: class_attributes = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{class_name}({class_attributes})" - cls.__str__ = __str__ # type: ignore[attr-defined] + cls.__str__ = __str__ # type: ignore[method-assign, assignment] return cls diff --git a/pauperformance_bot/util/path.py b/pauperformance_bot/util/path.py index c61b35bf..2ac0dae3 100644 --- a/pauperformance_bot/util/path.py +++ b/pauperformance_bot/util/path.py @@ -15,8 +15,8 @@ def safe_posix_path(name: str) -> str: underscores, or hyphens. Converts to lowercase. Converts '//' to '_or_'. Converts spaces or repeated dashes to single dashes. Also strips leading and trailing whitespace, dashes, and underscores.""" - res = unicodedata.normalize("NFKC", name).encode("ASCII").lower() - res = str(res).replace("//", "_or_") + res_bytes = unicodedata.normalize("NFKC", name).encode("ASCII").lower() + res = str(res_bytes).replace("//", "_or_") # remove all except for word characters (letters, digits, and underscores), # whitespace characters, and hyphens. res = re.sub(r"[^\w\s-]", "", res) diff --git a/pauperformance_bot/util/request.py b/pauperformance_bot/util/request.py index d4b313b7..51451a23 100644 --- a/pauperformance_bot/util/request.py +++ b/pauperformance_bot/util/request.py @@ -16,7 +16,7 @@ def _http_request_retry( # retries an HTTP request in case of ConnectionError with an exponential # backoff policy and additional random waits between 1 and 2 seconds. # It gives up after 5 attempts or after 60 seconds have passed. - @retry( + @retry( # type: ignore[untyped-decorator] retry_on_exception=_retry_on_connection_error, stop_max_attempt_number=5, stop_max_delay=60 * 1000, diff --git a/tox.ini b/tox.ini index af1a7e08..c365a7d5 100644 --- a/tox.ini +++ b/tox.ini @@ -93,6 +93,8 @@ commands = [mypy] follow_imports = normal strict = True +warn_unused_ignores = True +ignore_missing_imports = True ;install_types = True ;non_interactive = True