diff --git a/README.md b/README.md index 884bfc8..97543df 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ source of truth: a file removed means its emoji is deleted. ## Install ``` -dpy_appemojis @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_appemojis.git@v0.1.0 +dpy_appemojis @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_appemojis.git@v0.1.2 ``` ## Usage @@ -79,10 +79,19 @@ swallowed: - exceeding the 2000 cap raises `DPYAppEmojisError` **before** any create - a discord API error (`HTTPException`, which covers `Forbidden`) propagates **unwrapped** so a partial sync never hides behind a silent success +- `MissingApplicationID` (raised if `refresh()` runs before the client's `application_id` is + set, e.g. before `on_ready`) also propagates **unwrapped** — it is a `ClientException`, not + an `HTTPException`, so it is never caught by an `except discord.HTTPException` clause `emoji(name)` / `get.` for a name that wasn't synced raise `KeyError` / `AttributeError` (call `refresh()` first). +## Async stance + +`refresh()` offloads its blocking filesystem work (folder scan, magic-byte sniff, per-file +image read) to a worker thread via `asyncio.to_thread`, so the event loop stays responsive +during a large sync. Only the discord API calls run on the loop directly. + ## Versioning Tagged `vX.Y.Z`; pin a tag in your install line. Targets `discord.py>=2.5` (not diff --git a/pyproject.toml b/pyproject.toml index 7f99d9c..3b73aac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "dpy_appemojis" -version = "0.1.1" +version = "0.1.2" description = "Mirror a project folder onto the bot's application emojis for discord.py — folder is the source of truth. Config-free, injectable, installable." requires-python = ">=3.10" dependencies = [ diff --git a/src/dpy_appemojis/__init__.py b/src/dpy_appemojis/__init__.py index 5ba9404..df2906f 100644 --- a/src/dpy_appemojis/__init__.py +++ b/src/dpy_appemojis/__init__.py @@ -12,7 +12,7 @@ from .dpy_appemojis import ( RefreshResult, ) -__version__ = "0.1.1" +__version__ = "0.1.2" __all__ = [ "DPYAppEmojis", diff --git a/src/dpy_appemojis/dpy_appemojis.py b/src/dpy_appemojis/dpy_appemojis.py index 5067be3..b59d394 100644 --- a/src/dpy_appemojis/dpy_appemojis.py +++ b/src/dpy_appemojis/dpy_appemojis.py @@ -55,12 +55,19 @@ whose bytes aren't a recognized image (corrupt/truncated/mismatched extension) r silently syncing one and dropping the other. a missing ``assets/emojis`` folder raises :class:`DPYAppEmojisError` naming the path (see above) rather than mirroring an empty set. exceeding the app emoji cap raises before any create. discord API errors (``HTTPException``, -which covers ``Forbidden``) propagate UNWRAPPED — a partial sync never hides behind a silent -success. :meth:`emoji` / ``get.`` for an unsynced name raise ``KeyError`` / -``AttributeError``. +which covers ``Forbidden``, and ``MissingApplicationID`` if the client's application_id +isn't set yet) propagate UNWRAPPED — a partial sync never hides behind a silent success. +:meth:`emoji` / ``get.`` for an unsynced name raise ``KeyError`` / ``AttributeError``. + +async stance +------------ +:meth:`refresh` offloads its blocking filesystem work (folder scan, per-file magic-byte +sniff, per-file image read) to a thread via ``asyncio.to_thread`` so the event loop stays +responsive during a large sync; only the discord API calls run on the loop directly. """ from __future__ import annotations +import asyncio import logging import os import re @@ -78,6 +85,12 @@ MAX_APP_EMOJIS = 2000 _NAME_RE = re.compile(r"^[A-Za-z0-9_]{2,32}$") +def _read_file(path: str) -> bytes: + """read a file's full bytes; run off-loop via asyncio.to_thread""" + with open(path, "rb") as fh: + return fh.read() + + def _looks_like_image(header: bytes) -> bool: """mirror discord.py's own magic-byte sniff (png/jpeg/gif/webp) so a bad file is caught here, with the offending path, instead of as an anonymous ValueError from create""" @@ -141,9 +154,11 @@ class DPYAppEmojis: image format discord.HTTPException: any discord API error (includes Forbidden), propagated unwrapped + discord.MissingApplicationID: the client's application_id is not set yet (e.g. + refresh() called before login completes), propagated unwrapped """ existing = {emoji.name: emoji for emoji in await self._client.fetch_application_emojis()} - desired = self._scan_folder() + desired = await asyncio.to_thread(self._scan_folder) result = RefreshResult() current = dict(existing) @@ -156,8 +171,7 @@ class DPYAppEmojis: ) for name in to_create: - with open(desired[name], "rb") as fh: - image = fh.read() + image = await asyncio.to_thread(_read_file, desired[name]) try: emoji = await self._client.create_application_emoji(name=name, image=image) except discord.HTTPException: