Compare commits

..

6 commits

Author SHA1 Message Date
a563a42d2e
Merge branch 'main' into aurora/v3
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been skipped
Actions / Build Documentation (MkDocs) (pull_request) Has been skipped
Actions / Lint Code (Ruff & Pylint) (push) Failing after 43s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 43s
2025-01-25 19:40:55 -05:00
2859f93501
fix(bible): close asyncio session after unloading the cog
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 40s
2025-01-26 00:39:09 +00:00
7d1a9cc01a
fix(hotreload): only add dest_package_name to the cogs_to_reload list if dest_package_name != src_package_name
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 34s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 40s
fix(hotreload): only add `dest_package_name` to the `cogs_to_reload` list if `dest_package_name != src_package_name`
2025-01-26 00:32:29 +00:00
5adc7a2c7b
feat(hotreload): Add more events (#50)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 35s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 38s
# More HotReload Events
<!-- Create a new issue, if it doesn't exist yet -->
Currently, HotReload only supports file modification events. It should also support file moves, and some other event types.

- [x] By submitting this pull request, I permit cswimr to license my work under
  the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE).

Reviewed-on: https://www.coastalcommits.com/cswimr/SeaCogs/pulls/50
2025-01-25 19:25:29 -05:00
5384809780
feat(hotreload): init (#49)
Some checks failed
Actions / Build Documentation (MkDocs) (push) Has been skipped
Actions / Lint Code (Ruff & Pylint) (push) Failing after 42s
Actions / Build Documentation (MkDocs) (pull_request) Has been skipped
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
This pull request adds a cog that allows for automatic reloading of local cogs.

- [x] By submitting this pull request, I permit [cswimr](https://www.coastalcommits.com) to license my work under
  the [Mozilla Public License Version 2.0](https://www.coastalcommits.com/cswimr/SeaCogs/src/branch/main/LICENSE).

Reviewed-on: https://www.coastalcommits.com/cswimr/SeaCogs/pulls/49
2025-01-25 23:55:37 +00:00
78f036da48
feat(devcontainer): initialize redbot instance in postCreateCommand
Some checks failed
Actions / Build Documentation (MkDocs) (push) Successful in 37s
Actions / Lint Code (Ruff & Pylint) (push) Failing after 41s
2025-01-25 22:50:35 +00:00
8 changed files with 150 additions and 33 deletions

View file

@ -30,6 +30,6 @@
"PROJECT_DIR": "/workspaces/SeaCogs" "PROJECT_DIR": "/workspaces/SeaCogs"
}, },
"mounts": ["source=seacogs-persistent-data,target=/workspaces/SeaCogs/.data,type=volume"], "mounts": ["source=seacogs-persistent-data,target=/workspaces/SeaCogs/.data,type=volume"],
"postCreateCommand": "uv sync --frozen && sudo chown -R vscode:vscode /workspaces/SeaCogs/.data", "postCreateCommand": "uv sync --frozen && sudo chown -R vscode:vscode /workspaces/SeaCogs/.data && uv run redbot-setup --no-prompt --instance-name=local --data-path=/workspaces/SeaCogs/.data --backend=json",
"remoteUser": "vscode" "remoteUser": "vscode"
} }

View file

@ -6,6 +6,7 @@
# |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_| # |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_|
import random import random
from asyncio import create_task
from io import BytesIO from io import BytesIO
import aiohttp import aiohttp
@ -26,20 +27,21 @@ class Bible(commands.Cog):
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"] __author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs" __git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.1.1" __version__ = "1.1.2"
__documentation__ = "https://seacogs.coastalcommits.com/pterodactyl/" __documentation__ = "https://seacogs.coastalcommits.com/pterodactyl/"
def __init__(self, bot: Red): def __init__(self, bot: Red):
super().__init__() super().__init__()
self.bot = bot self.bot = bot
self.session = aiohttp.ClientSession() self.session = aiohttp.ClientSession()
self.config = Config.get_conf( self.config = Config.get_conf(self, identifier=481923957134912, force_registration=True)
self, identifier=481923957134912, force_registration=True
)
self.logger = getLogger("red.SeaCogs.Bible") self.logger = getLogger("red.SeaCogs.Bible")
self.config.register_global(bible="de4e12af7f28f599-02") self.config.register_global(bible="de4e12af7f28f599-02")
self.config.register_user(bible=None) self.config.register_user(bible=None)
def cog_unload(self):
create_task(self.session.close())
def format_help_for_context(self, ctx: commands.Context) -> str: def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or "" pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else "" n = "\n" if "\n\n" not in pre_processed else ""
@ -51,7 +53,6 @@ class Bible(commands.Cog):
] ]
return "\n".join(text) return "\n".join(text)
def get_icon(self, color: Colour) -> File: def get_icon(self, color: Colour) -> File:
"""Get the docs.api.bible favicon with a given color.""" """Get the docs.api.bible favicon with a given color."""
image_path = data_manager.bundled_data_path(self) / "api.bible-logo.png" image_path = data_manager.bundled_data_path(self) / "api.bible-logo.png"
@ -70,9 +71,7 @@ class Bible(commands.Cog):
async def translate_book_name(self, bible_id: str, book_name: str) -> str: async def translate_book_name(self, bible_id: str, book_name: str) -> str:
"""Translate a book name to a book ID.""" """Translate a book name to a book ID."""
book_name_list = [ book_name_list = [w.lower() if w.lower() == "of" else w.title() for w in book_name.split()]
w.lower() if w.lower() == "of" else w.title() for w in book_name.split()
]
book_name = " ".join(book_name_list) book_name = " ".join(book_name_list)
books = await self._get_books(bible_id) books = await self._get_books(bible_id)
for book in books: for book in books:
@ -247,13 +246,9 @@ class Bible(commands.Cog):
from_verse, to_verse = passage.replace(":", ".").split("-") from_verse, to_verse = passage.replace(":", ".").split("-")
if "." not in to_verse: if "." not in to_verse:
to_verse = f"{from_verse.split('.')[0]}.{to_verse}" to_verse = f"{from_verse.split('.')[0]}.{to_verse}"
passage = await self._get_passage( passage = await self._get_passage(ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True)
ctx, bible_id, f"{book_id}.{from_verse}-{book_id}.{to_verse}", True
)
else: else:
passage = await self._get_passage( passage = await self._get_passage(ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False)
ctx, bible_id, f"{book_id}.{passage.replace(':', '.')}", False
)
except ( except (
bible.errors.BibleAccessError, bible.errors.BibleAccessError,
bible.errors.NotFound, bible.errors.NotFound,
@ -275,10 +270,7 @@ class Bible(commands.Cog):
description=passage["content"].replace("", ""), description=passage["content"].replace("", ""),
color=await ctx.embed_color(), color=await ctx.embed_color(),
) )
embed.set_footer( embed.set_footer(text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})", icon_url="attachment://icon.png")
text=f"{ctx.prefix}bible passage - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})",
icon_url="attachment://icon.png"
)
await ctx.send(embed=embed, file=icon) await ctx.send(embed=embed, file=icon)
else: else:
await ctx.send(f"## {passage['reference']}\n{passage['content']}") await ctx.send(f"## {passage['reference']}\n{passage['content']}")
@ -317,10 +309,7 @@ class Bible(commands.Cog):
description=passage["content"].replace("", ""), description=passage["content"].replace("", ""),
color=await ctx.embed_color(), color=await ctx.embed_color(),
) )
embed.set_footer( embed.set_footer(text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})", icon_url="attachment://icon.png")
text=f"{ctx.prefix}bible random - Powered by API.Bible - {version.abbreviationLocal} ({version.languageLocal}, {version.descriptionLocal})",
icon_url="attachment://icon.png"
)
await ctx.send(embed=embed, file=icon) await ctx.send(embed=embed, file=icon)
else: else:
await ctx.send(f"## {passage['reference']}\n{passage['content']}") await ctx.send(f"## {passage['reference']}\n{passage['content']}")

View file

@ -4,9 +4,7 @@ from redbot.core.utils.chat_formatting import error
class BibleAccessError(Exception): class BibleAccessError(Exception):
def __init__( def __init__(
self, self,
message: str = error( message: str = error("The provided API key cannot retrieve sections from the configured Bible. Please report this to the bot owner."),
"The provided API key cannot retrieve sections from the configured Bible. Please report this to the bot owner."
),
): ):
super().__init__(message) super().__init__(message)
self.message = message self.message = message
@ -15,9 +13,7 @@ class BibleAccessError(Exception):
class Unauthorized(Exception): class Unauthorized(Exception):
def __init__( def __init__(
self, self,
message: str = error( message: str = error("The API key for API.Bible is missing or invalid. Please report this to the bot owner.\nIf you are the bot owner, please check the documentation [here](<https://seacogs.coastalcommits.com/bible/#setup>)."),
"The API key for API.Bible is missing or invalid. Please report this to the bot owner.\nIf you are the bot owner, please check the documentation [here](<https://seacogs.coastalcommits.com/bible/#setup>)."
),
): ):
super().__init__(message) super().__init__(message)
self.message = message self.message = message
@ -44,9 +40,7 @@ class ServiceUnavailable(Exception):
class InexplicableError(Exception): class InexplicableError(Exception):
def __init__( def __init__(
self, self,
message: str = error( message: str = error("An inexplicable 'Bad Request' error occurred. This error happens occasionally with the API.Bible service. Please try again. If the error persists, please report this to the bot owner."),
"An inexplicable 'Bad Request' error occurred. This error happens occassionally with the API.Bible service. Please try again. If the error persists, please report this to the bot owner."
),
): ):
super().__init__(message) super().__init__(message)
self.message = message self.message = message

5
hotreload/__init__.py Normal file
View file

@ -0,0 +1,5 @@
from .hotreload import HotReload
async def setup(bot):
await bot.add_cog(HotReload(bot))

109
hotreload/hotreload.py Normal file
View file

@ -0,0 +1,109 @@
from asyncio import run_coroutine_threadsafe
from pathlib import Path
from typing import Sequence
from red_commons.logging import RedTraceLogger, getLogger
from redbot.core import commands
from redbot.core.bot import Red
from redbot.core.core_commands import CoreLogic
from redbot.core.utils.chat_formatting import bold, humanize_list
from watchdog.events import FileSystemEvent, FileSystemMovedEvent, RegexMatchingEventHandler
from watchdog.observers import Observer
class HotReload(commands.Cog):
"""Automatically reload cogs in local cog paths on file change."""
__author__ = ["[cswimr](https://www.coastalcommits.com/cswimr)"]
__git__ = "https://www.coastalcommits.com/cswimr/SeaCogs"
__version__ = "1.1.1"
__documentation__ = "https://seacogs.coastalcommits.com/hotreload/"
def __init__(self, bot: Red) -> None:
super().__init__()
self.bot: Red = bot
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.HotReload")
self.observer = None
watchdog_loggers = [getLogger(name="watchdog.observers.inotify_buffer")]
for watchdog_logger in watchdog_loggers:
watchdog_logger.setLevel("INFO") # SHUT UP!!!!
def cog_load(self) -> None:
"""Start the observer when the cog is loaded."""
self.bot.loop.create_task(self.start_observer())
def cog_unload(self) -> None:
"""Stop the observer when the cog is unloaded."""
if self.observer:
self.observer.stop()
self.observer.join()
self.logger.info("Stopped observer. No longer watching for file changes.")
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
f"{bold('Cog Version:')} [{self.__version__}]({self.__git__})",
f"{bold('Author:')} {humanize_list(self.__author__)}",
f"{bold('Documentation:')} {self.__documentation__}",
]
return "\n".join(text)
async def get_paths(self) -> tuple[Path]:
"""Retrieve user defined paths."""
cog_manager = self.bot._cog_mgr
cog_paths = await cog_manager.user_defined_paths()
return (Path(path) for path in cog_paths)
async def start_observer(self) -> None:
"""Start the observer to watch for file changes."""
self.observer = Observer()
paths = await self.get_paths()
for path in paths:
self.observer.schedule(event_handler=HotReloadHandler(bot=self.bot, path=path), path=path, recursive=True)
self.observer.start()
self.logger.info("Started observer. Watching for file changes.")
class HotReloadHandler(RegexMatchingEventHandler):
"""Handler for file changes."""
def __init__(self, bot: Red, path: Path) -> None:
super().__init__(regexes=[r".*\.py$"])
self.bot: Red = bot
self.path: Path = path
self.logger: RedTraceLogger = getLogger(name="red.SeaCogs.HotReload.Observer")
def on_any_event(self, event: FileSystemEvent) -> None:
"""Handle filesystem events."""
if event.is_directory:
return
allowed_events = ("moved", "deleted", "created", "modified")
if event.event_type not in allowed_events:
return
relative_src_path = Path(event.src_path).relative_to(self.path)
src_package_name = relative_src_path.parts[0]
cogs_to_reload = [src_package_name]
if isinstance(event, FileSystemMovedEvent):
dest = f" to {event.dest_path}"
relative_dest_path = Path(event.dest_path).relative_to(self.path)
dest_package_name = relative_dest_path.parts[0]
if dest_package_name != src_package_name:
cogs_to_reload.append(dest_package_name)
else:
dest = ""
self.logger.info(f"File {event.src_path} has been {event.event_type}{dest}.")
run_coroutine_threadsafe(self.reload_cogs(cogs_to_reload), loop=self.bot.loop)
async def reload_cogs(self, cog_names: Sequence[str]) -> None:
"""Reload modified cog."""
core_logic = CoreLogic(bot=self.bot)
self.logger.info(f"Reloading cogs: {humanize_list(cog_names, style='unit')}")
await core_logic._reload(pkg_names=cog_names)
self.logger.info(f"Reloaded cogs: {humanize_list(cog_names, style='unit')}")

17
hotreload/info.json Normal file
View file

@ -0,0 +1,17 @@
{
"author" : ["cswimr"],
"install_msg" : "Thank you for installing HotReload!",
"name" : "HotReload",
"short" : "Automatically reload cogs in local cog paths on file change.",
"description" : "Automatically reload cogs in local cog paths on file change.",
"end_user_data_statement" : "This cog does not store end user data.",
"hidden": false,
"disabled": false,
"min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0],
"requirements": ["watchdog"],
"tags": [
"utility",
"development"
]
}

View file

@ -18,6 +18,7 @@ dependencies = [
"py-dactyl", "py-dactyl",
"pydantic>=2.9.2", "pydantic>=2.9.2",
"red-discordbot>=3.5.14", "red-discordbot>=3.5.14",
"watchdog>=5.0.3",
"websockets>=13.1", "websockets>=13.1",
] ]

2
uv.lock generated
View file

@ -1667,6 +1667,7 @@ dependencies = [
{ name = "py-dactyl" }, { name = "py-dactyl" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "red-discordbot" }, { name = "red-discordbot" },
{ name = "watchdog" },
{ name = "websockets" }, { name = "websockets" },
] ]
@ -1706,6 +1707,7 @@ requires-dist = [
{ name = "py-dactyl", git = "https://github.com/cswimr/pydactyl" }, { name = "py-dactyl", git = "https://github.com/cswimr/pydactyl" },
{ name = "pydantic", specifier = ">=2.9.2" }, { name = "pydantic", specifier = ">=2.9.2" },
{ name = "red-discordbot", specifier = ">=3.5.14" }, { name = "red-discordbot", specifier = ">=3.5.14" },
{ name = "watchdog", specifier = ">=5.0.3" },
{ name = "websockets", specifier = ">=13.1" }, { name = "websockets", specifier = ">=13.1" },
] ]