From 8f41f6fc4e7fdb3c39b0d3abb60adc0f927cdf26 Mon Sep 17 00:00:00 2001 From: Kevin Lu <6320810+kevinlul@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:46:37 -0500 Subject: [PATCH] Format and lint with ruff, leaving one intentional warning to test --- .github/workflows/python.yml | 17 ++++++++++ src/antiabuse.py | 8 +++-- src/bastion.py | 4 +-- src/bot_thread.py | 6 ++-- src/card.py | 62 ++++++++++++++++++++++++------------ src/clients.py | 4 +-- src/limit_regulation.py | 16 +++++++--- src/mention.py | 16 +++++++--- src/stream.py | 22 ++++++++++--- 9 files changed, 111 insertions(+), 44 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 50f5abc..1d15251 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,3 +1,5 @@ +# SPDX-FileCopyrightText: © 2024 Kevin Lu +# SPDX-Licence-Identifier: AGPL-3.0-or-later name: Lint on: push: @@ -23,3 +25,18 @@ jobs: with: category: guarddog-builtin sarif_file: guarddog.sarif + # https://github.com/astral-sh/ruff + ruff: + runs-on: ubuntu-latest + permissions: {} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - run: pip install ruff + - run: ruff format --check src + - run: ruff check src + if: ${{ !cancelled() }} + - run: ruff check --output-format=github src + if: ${{ !cancelled() }} diff --git a/src/antiabuse.py b/src/antiabuse.py index 586b8d6..e0a5d21 100644 --- a/src/antiabuse.py +++ b/src/antiabuse.py @@ -1,15 +1,17 @@ -# SPDX-FileCopyrightText: © 2023 Kevin Lu, Luna Brand +# SPDX-FileCopyrightText: © 2023–2024 Kevin Lu, Luna Brand # SPDX-Licence-Identifier: AGPL-3.0-or-later from typing import TYPE_CHECKING - if TYPE_CHECKING: from praw.models import Comment, Submission from praw.models.comment_forest import CommentForest def is_author_me(comment: "Comment") -> bool: - return comment.author is not None and comment.author.name == comment._reddit.user.me().name + return ( + comment.author is not None + and comment.author.name == comment._reddit.user.me().name + ) def is_summon_chain(comment: "Comment") -> bool: diff --git a/src/bastion.py b/src/bastion.py index 41110b4..f29c86a 100644 --- a/src/bastion.py +++ b/src/bastion.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: © 2023 Kevin Lu, Luna Brand +# SPDX-FileCopyrightText: © 2023–2024 Kevin Lu, Luna Brand # SPDX-Licence-Identifier: AGPL-3.0-or-later import logging @@ -7,7 +7,7 @@ from clients import get_api_client from limit_regulation import master_duel_limit_regulation from mention import MentionsThread -from stream import SubmissionsThread, CommentsThread +from stream import CommentsThread, SubmissionsThread def main(): diff --git a/src/bot_thread.py b/src/bot_thread.py index fec3a7a..7966d1b 100644 --- a/src/bot_thread.py +++ b/src/bot_thread.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: © 2023 Kevin Lu, Luna Brand +# SPDX-FileCopyrightText: © 2023–2024 Kevin Lu, Luna Brand # SPDX-Licence-Identifier: AGPL-3.0-or-later from collections import Counter from datetime import datetime, timezone @@ -43,7 +43,9 @@ def _reply(self, target: Union["Comment", "Submission"], text: str) -> None: for item in e.items: if item.error_type == "TOO_LONG": self._logger.warning(f"{target.id}: reply too long", exc_info=e) - reply: "Comment" = target.reply(f"Sorry, the cards are too long to fit into one comment.{FOOTER}") + reply: "Comment" = target.reply( + f"Sorry, the cards are too long to fit into one comment.{FOOTER}" + ) self._logger.info(f"{target.id}: posted error {reply.id}") self._reply_counter[target.submission.id] += 1 reply.disable_inbox_replies() diff --git a/src/card.py b/src/card.py index ef8474a..6b32c7d 100644 --- a/src/card.py +++ b/src/card.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: © 2023 Kevin Lu, Luna Brand +# SPDX-FileCopyrightText: © 2023–2024 Kevin Lu, Luna Brand # SPDX-Licence-Identifier: AGPL-3.0-or-later from os import getenv import re @@ -26,12 +26,17 @@ def parse_summons(text: str) -> List[str]: """ summons: List[str] = summon_regex.findall(text) summons = [summon.strip() for summon in summons] - return list(dict.fromkeys(summon.lower() for summon in summons if summon))[:summon_limit] + return list(dict.fromkeys(summon.lower() for summon in summons if summon))[ + :summon_limit + ] def get_cards(client: "Client", names: List[str]) -> List[Dict[str, Any]]: # Could be parallelized, even in a synchronous context - responses = [client.get(f"{getenv('API_URL')}/ocg-tcg/search?name={quote_plus(name)}") for name in names] + responses = [ + client.get(f"{getenv('API_URL')}/ocg-tcg/search?name={quote_plus(name)}") + for name in names + ] return [response.json() for response in responses if response.status_code == 200] @@ -63,40 +68,41 @@ def get_master_duel_limit_regulation(card: Any) -> int | None: "N": "Common", "R": "Rare", "SR": "Super Rare", - "UR": "Ultra Rare" + "UR": "Ultra Rare", } + def format_card_text(text: str | None) -> str: return text.replace("\n", "\n\n") if text else "\u200b" def format_footer(card: Any) -> str: - if card['password'] and card['konami_id']: + if card["password"] and card["konami_id"]: text = f"Password: {card['password']} | Konami ID #{card['konami_id']}" - elif not card['password'] and card['konami_id']: + elif not card["password"] and card["konami_id"]: text = f"No password | Konami ID #{card['konami_id']}" - elif card['password'] and not card['konami_id']: + elif card["password"] and not card["konami_id"]: text = f"Password: {card['password']} | Not yet released" else: text = "Not yet released" - if card.get('fake_password') is not None: + if card.get("fake_password") is not None: text += f" | Placeholder ID: {card['fake_password']}" return f"^({text})" def generate_card_display(card: Any) -> str: - yugipedia_page = card['konami_id'] or quote_plus(card['name']['en']) + yugipedia_page = card["konami_id"] or quote_plus(card["name"]["en"]) yugipedia = f"https://yugipedia.com/wiki/{yugipedia_page}?utm_source=bastion&utm_medium=reddit" - ygoprodeck_term = card['password'] or quote_plus(card['name']['en']) + ygoprodeck_term = card["password"] or quote_plus(card["name"]["en"]) ygoprodeck = f"https://ygoprodeck.com/card/?search={ygoprodeck_term}&utm_source=bastion&utm_medium=reddit" full_text = f"## [{card['name']['en']}]({ygoprodeck})\n" links = "" - if card.get('images'): + if card.get("images"): image_link = f"https://yugipedia.com/wiki/Special:Redirect/file/{card['images'][0]['image']}?utm_source=bastion&utm_medium=reddit" links += f"[Card Image]({image_link}) | " - if card['konami_id'] is not None: + if card["konami_id"] != None: # Official database, does not work for zh locales official = f"https://www.db.yugioh-card.com/yugiohdb/card_search.action?ope=2&request_locale=en&cid={card['konami_id']}" rulings = f"https://www.db.yugioh-card.com/yugiohdb/faq_search.action?ope=4&request_locale=ja&cid={card['konami_id']}" @@ -106,13 +112,23 @@ def generate_card_display(card: Any) -> str: description = "" limit_regulations = [ - {"label": "TCG: ", "value": format_limit_regulation(card["limit_regulation"].get("tcg"))}, - {"label": "OCG: ", "value": format_limit_regulation(card["limit_regulation"].get("ocg"))}, + { + "label": "TCG: ", + "value": format_limit_regulation(card["limit_regulation"].get("tcg")), + }, + { + "label": "OCG: ", + "value": format_limit_regulation(card["limit_regulation"].get("ocg")), + }, {"label": "Speed: ", "value": card["limit_regulation"].get("speed")}, {"label": "MD: ", "value": get_master_duel_limit_regulation(card)}, ] - limit_regulation_display = " / ".join(f"{reg['label']}{reg['value']}" for reg in limit_regulations if reg["value"] is not None) + limit_regulation_display = " / ".join( + f"{reg['label']}{reg['value']}" + for reg in limit_regulations + if reg["value"] is not None + ) if len(limit_regulation_display) > 0: description += f"^(**Limit**: {limit_regulation_display}) \n" @@ -120,28 +136,30 @@ def generate_card_display(card: Any) -> str: if card.get("master_duel_rarity"): md_rarity_code = card["master_duel_rarity"] md_rarity = MASTER_DUEL_RARITY[md_rarity_code] - description += f"^(**Master Duel rarity**: {md_rarity} ({md_rarity_code})) \n" + description += ( + f"^(**Master Duel rarity**: {md_rarity} ({md_rarity_code})) \n" + ) - if card['card_type'] == "Monster": + if card["card_type"] == "Monster": description += f"^(**Type**: {card['monster_type_line']}) \n" description += f"^(**Attribute**: {card['attribute']}) \n" if "rank" in card: description += f"^(**Rank**: {card['rank']} **ATK**: {card['atk']} **DEF**: {card['def']})" elif "link_arrows" in card: - arrows = "".join(card['link_arrows']) + arrows = "".join(card["link_arrows"]) description += f"^(**Link Rating**: {len(card['link_arrows'])} **ATK**: {card['atk']} **Link Arrows**: {arrows})" else: description += f"^(**Level**: {card['level']} **ATK**: {card['atk']} **DEF**: {card['def']})" - if card.get('pendulum_scale') != None: + if card.get("pendulum_scale") is not None: formatted_scale = f"{card['pendulum_scale']} / {card['pendulum_scale']}" description += " " description += f"^(**Pendulum Scale**: {formatted_scale})" full_text += f"{description}\n\n" - if card.get('pendulum_effect') != None: + if card.get("pendulum_effect") is not None: full_text += f"**Pendulum Effect**\n\n{format_card_text(card['pendulum_effect']['en'])}\n\n" full_text += f"**Card Text**\n\n{format_card_text(card['text']['en'])}" @@ -149,7 +167,9 @@ def generate_card_display(card: Any) -> str: # Spells and Traps description += "\n\n" description += f"{card['property']} {card['card_type']}" - full_text += f"{description}\n\n**Card Text**\n\n{format_card_text(card['text']['en'])}" + full_text += ( + f"{description}\n\n**Card Text**\n\n{format_card_text(card['text']['en'])}" + ) full_text += f"\n\n{links}\n\n{format_footer(card)}" return full_text diff --git a/src/clients.py b/src/clients.py index b23695c..49df18b 100644 --- a/src/clients.py +++ b/src/clients.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: © 2023 Kevin Lu, Luna Brand +# SPDX-FileCopyrightText: © 2023–2024 Kevin Lu, Luna Brand # SPDX-Licence-Identifier: AGPL-3.0-or-later from os import getenv from platform import python_version @@ -23,7 +23,7 @@ def get_reddit_client() -> praw.Reddit: client_secret=getenv("REDDIT_CLIENT_SECRET"), username=getenv("REDDIT_USERNAME"), password=getenv("REDDIT_PASSWORD"), - user_agent=user_agent("praw") + user_agent=user_agent("praw"), ) diff --git a/src/limit_regulation.py b/src/limit_regulation.py index bca55d1..18af846 100644 --- a/src/limit_regulation.py +++ b/src/limit_regulation.py @@ -1,3 +1,5 @@ +# SPDX-FileCopyrightText: © 2023–2024 Kevin Lu +# SPDX-Licence-Identifier: AGPL-3.0-or-later from datetime import datetime import logging from threading import Timer @@ -26,9 +28,11 @@ def update(self, initial: bool = False) -> None: try: response = self._client.get(self._url) regulation = response.json()["regulation"] - self._vector = { int(konami_id): limit for konami_id, limit in regulation.items() } + self._vector = { + int(konami_id): limit for konami_id, limit in regulation.items() + } self._logger.info(f"Read {len(self._vector)} entries") - except: + except Exception: self._logger.error(f"Failed GET [{self._url}]", exc_info=1) def get(self, konami_id: int) -> int | None: @@ -42,5 +46,9 @@ def cancel(self) -> None: # Globals, to eventually remove -master_duel_limit_regulation = UpdatingLimitRegulationVector("https://dawnbrandbots.github.io/yaml-yugi-limit-regulation/master-duel/current.vector.json") -rush_duel_limit_regulation = UpdatingLimitRegulationVector("https://dawnbrandbots.github.io/yaml-yugi-limit-regulation/rush/current.vector.json") +master_duel_limit_regulation = UpdatingLimitRegulationVector( + "https://dawnbrandbots.github.io/yaml-yugi-limit-regulation/master-duel/current.vector.json" +) +rush_duel_limit_regulation = UpdatingLimitRegulationVector( + "https://dawnbrandbots.github.io/yaml-yugi-limit-regulation/rush/current.vector.json" +) diff --git a/src/mention.py b/src/mention.py index be7f00b..1018310 100644 --- a/src/mention.py +++ b/src/mention.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: © 2023 Kevin Lu, Luna Brand +# SPDX-FileCopyrightText: © 2023–2024 Kevin Lu, Luna Brand # SPDX-Licence-Identifier: AGPL-3.0-or-later from os import getenv from typing import TYPE_CHECKING @@ -10,7 +10,6 @@ from card import parse_summons, get_cards, display_cards from footer import FOOTER - if TYPE_CHECKING: import httpx @@ -33,10 +32,17 @@ def _run(self) -> None: if not comment.new: self._logger.info(f"{comment.id}|{comment.context}| skip, read") continue - self._logger.info(f"{comment.id}|{comment.context}|{timestamp_to_iso(comment.created_utc)}") + self._logger.info( + f"{comment.id}|{comment.context}|{timestamp_to_iso(comment.created_utc)}" + ) comment.mark_read() - if self._reply_counter[comment.submission.id] >= self.MAX_REPLIES_PER_SUBMISSION: - self._logger.warning(f"{comment.id}: skip, exceeded limit for {comment.submission.id}") + if ( + self._reply_counter[comment.submission.id] + >= self.MAX_REPLIES_PER_SUBMISSION + ): + self._logger.warning( + f"{comment.id}: skip, exceeded limit for {comment.submission.id}" + ) continue if is_summon_chain(comment): self._logger.info(f"{comment.id}: skip, parent comment is me") diff --git a/src/stream.py b/src/stream.py index 1870880..2836fd0 100644 --- a/src/stream.py +++ b/src/stream.py @@ -1,10 +1,15 @@ -# SPDX-FileCopyrightText: © 2023 Kevin Lu, Luna Brand +# SPDX-FileCopyrightText: © 2023–2024 Kevin Lu, Luna Brand # SPDX-Licence-Identifier: AGPL-3.0-or-later from abc import abstractmethod from os import getenv from typing import Generator, Generic, List, TypeVar, TYPE_CHECKING -from antiabuse import already_replied_to_comment, already_replied_to_submission, is_author_me, is_summon_chain +from antiabuse import ( + already_replied_to_comment, + already_replied_to_submission, + is_author_me, + is_summon_chain, +) from card import parse_summons, get_cards, display_cards from bot_thread import BotThread, timestamp_to_iso @@ -23,7 +28,9 @@ def _parse_summons(self, post: Post) -> List[str]: def _main_loop(self, stream: Generator[Post, None, None]): for post in stream: - self._logger.info(f"{post.id}|{post.permalink}|{timestamp_to_iso(post.created_utc)}") + self._logger.info( + f"{post.id}|{post.permalink}|{timestamp_to_iso(post.created_utc)}" + ) summons = self._parse_summons(post) if len(summons): cards = get_cards(self._client, summons) @@ -61,8 +68,13 @@ def _parse_summons(self, comment): if is_author_me(comment): self._logger.info(f"{comment.id}: skip, self") return [] - if self._reply_counter[comment.submission.id] >= self.MAX_REPLIES_PER_SUBMISSION: - self._logger.warning(f"{comment.id}: skip, exceeded limit for {comment.submission.id}") + if ( + self._reply_counter[comment.submission.id] + >= self.MAX_REPLIES_PER_SUBMISSION + ): + self._logger.warning( + f"{comment.id}: skip, exceeded limit for {comment.submission.id}" + ) return [] summons = parse_summons(comment.body) self._logger.info(f"{comment.id}| summons: {summons}")