both HTTPException handlers logged at ERROR then re-raised - raise XOR log. the exception propagates unwrapped to the caller (the documented fail-loud contract), so it already carries the failure; the error log double-reported it. drop the log, keep the raise. the module logger stays defined (emit-only) for future use. Signed-off-by: disqualifier <dev@disqualifier.me>
dpy_appemojis
Mirror a project folder onto your bot's application emojis (bot-owned, usable in any
guild the app is in — not guild emojis) for discord.py. Drop an image in assets/emojis,
call refresh(), and it becomes an application emoji named after the file. The folder is the
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@v1.0.0
Usage
from dpy_appemojis import DPYAppEmojis
appmojis = DPYAppEmojis(bot)
await appmojis.refresh() # e.g. in setup_hook / on_ready
await ctx.send(f"done {appmojis.get.success_mark}") # dot-access -> discord.Emoji
appmojis.emoji("success_mark").url # explicit accessor
appmojis.list() # ["success_mark", ...]
refresh() makes the app's emoji set mirror assets/emojis:
- a new file → the emoji is created (name = filename stem)
- a removed file → its emoji is deleted server-side
- a same-name emoji → left alone (this lib does not diff image content; to replace an image, rename the file or delete + re-add)
Call refresh() at startup, or from a command, to re-sync on demand — the lib adds no
commands of its own.
A missing assets/emojis folder is not the same as an empty one: if the folder
doesn't exist (wrong cwd, missing WorkingDirectory=/WORKDIR), refresh() raises
DPYAppEmojisError naming the path instead of mirroring an empty desired set — which would
otherwise delete every application emoji on the next restart. An empty-but-present folder is
still honored as "delete everything."
Dot-access vs emoji()
self.get is a namespace whose attribute lookup returns the emoji by name. It lives apart
from the methods, so an emoji named list or refresh can't shadow them. For names that
aren't valid Python identifiers, use emoji("name"). .url comes free — get.x /
emoji("x") return a real discord.Emoji.
Fixed constants (not configurable)
| Folder | assets/emojis (relative to cwd) |
| Extensions | .png .jpg .jpeg .gif .webp |
| Max file size | 256 KB (Discord's emoji limit) |
| Name rules | 2–32 chars, [A-Za-z0-9_] |
| App emoji cap | 2000 |
What you inject
Your discord.Client / commands.Bot. Application-emoji methods live on the client
(fetch_application_emojis / create_application_emoji, added in discord.py 2.5), so this
targets discord.py>=2.5.
Contract (fail-loud)
The module docstring (help(dpy_appemojis) / IDE hover) is the source of truth. Nothing is
swallowed:
- an invalid emoji name from a bad filename, an oversized file, or a file whose bytes aren't
a recognized PNG/JPEG/GIF/WEBP image (corrupt, truncated, or mismatched extension) raises
ValueErrornaming the file - two files sharing a name stem across extensions (e.g.
check.pngandcheck.gif) raiseDPYAppEmojisErrornaming both files, instead of silently syncing one and dropping the other - a missing
assets/emojisfolder raisesDPYAppEmojisErrornaming the path (see above) rather than mirroring an empty set - exceeding the 2000 cap raises
DPYAppEmojisErrorbefore any create - a discord API error (
HTTPException, which coversForbidden) propagates unwrapped so a partial sync never hides behind a silent success MissingApplicationID(raised ifrefresh()runs before the client'sapplication_idis set, e.g. beforeon_ready) also propagates unwrapped — it is aClientException, not anHTTPException, so it is never caught by anexcept discord.HTTPExceptionclause
emoji(name) / get.<name> 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
discord.py-self).