Signed-off-by: disqualifier <dev@disqualifier.me>
dpy_cache
Persist images/files to Discord by uploading them to a storage channel, and get back durable references — a message jump URL plus per-file CDN URLs keyed by their original filename. Discord attachments live on the CDN permanently; this lib wraps "stash it, hand me a URL" and also returns a re-resolvable reference so the ~24h URL-signature expiry never bites you.
Install
dpy_cache @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_cache.git@v0.1.0
Usage
The headline is dead-simple single-file caching:
from dpy_cache import DPYCache
cache = DPYCache(bot.get_channel(STORAGE_CHANNEL_ID))
url = (await cache.cache_one("img.png", image_bytes)).url # raw bytes -> url
url = (await cache.cache_url("img.png", "https://...")).url # remote url -> url
Several files at once (batches automatically over 10 per message):
result = await cache.cache({
"avatar.png": avatar_bytes,
"banner.png": "/tmp/banner.png",
})
result.files["avatar.png"].url # cdn url now
result.message_url # jump link to the stored message
# later, if the url may have expired:
fresh = await cache.resolve(result.files["avatar.png"])
Reverse lookup a stored message back into refs:
refs = await cache.lookup(result.message_url) # jump url, message id, or discord.Message
refs["avatar.png"].url
What you inject
A resolved Discord channel object (discord.abc.Messageable that supports .send and
.fetch_message — a TextChannel or Thread). The lib holds no client and reads no
global config; it calls those two methods on the channel directly. The bot must already
have access to the channel — an inaccessible or bad channel raises Discord's own error on
first use.
FileContent inputs
cache_one / cache accept, per file: bytes, a filesystem path (str or
os.PathLike), or a discord.File. In a list input, raw bytes have no filename and
raise ValueError — pass a dict {name: bytes} or a discord.File to name bytes.
Why resolve() exists
Since late 2023, Discord attachment CDN URLs are signed and the signature expires (~24h).
The attachment itself is permanent; only the URL goes stale (a raw stored URL 403s after
expiry). FileRef carries the message/channel/attachment ids, and resolve(ref) re-fetches
the message and returns a freshly-signed URL. Use the URL immediately and you can ignore
resolve(); persist a reference and you call resolve() when you read it back.
API & contract
The module docstring (help(dpy_cache) / IDE hover) is the source of truth for the full
API. In short:
cache(files, *, content=None) -> CacheResultcache_one(name, data, *, content=None) -> FileRefcache_url(name, url, *, content=None) -> FileRefresolve(ref) -> strlookup(message) -> dict[str, FileRef]
Fail-loud. Nothing is swallowed. Lib-specific invariants raise DPYCacheError
(attachment-count mismatch after a send, cache_url non-200, a lookup jump-URL pointing
at a different channel than the injected one). Unnamed bytes in a list raise ValueError.
Raw Discord errors (Forbidden / HTTPException / NotFound) propagate unwrapped so
you can still branch on Discord's own types.
Versioning
Tagged vX.Y.Z; pin a tag in your install line. Targets discord.py>=2.0 (not
discord.py-self).