the docstring listed an unsupported content type under DPYCacheError, but _normalize/ _prevalidate raise ValueError for it (DPYCacheError is only the duplicate-filename and missing-path cases). a caller following the docstring with except DPYCacheError would let the ValueError escape. group the raises correctly: ValueError = unnamed bytes / unsupported type; DPYCacheError = duplicate filename / missing path / count mismatch. doc-only. 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.2
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
lookup() accepts a message id as an int or as a digit-only str (the snowflake shape
JSON/DBs store), e.g. cache.lookup("123456789012345678").
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. A
list input whose sources resolve to the same name (two paths with the same basename,
two discord.Files with the same .filename, ...) raises DPYCacheError before any
upload — a dict input can't collide since dict keys are already unique.
A discord.File source is safe to reuse across multiple names/batches: cache() always
builds a genuinely fresh discord.File per send (re-opening a path-backed source, or
seeking + copying a caller-supplied stream) rather than reusing the object you passed in.
An opaque stream that can't be safely re-read (already closed, not seekable) raises
DPYCacheError instead of silently sending stale or empty data.
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, a duplicate filename in a list input to cache(),
cache_url non-200, a lookup jump-URL pointing at a different channel than the injected
one, a discord.File source that can't be safely rebuilt for a repeat send). 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).
v0.1.2
__version__is now derived from installed package metadata (importlib.metadata.version("dpy_cache")) instead of a hardcoded string, so it can no longer drift from thepyproject.tomlversion that the release tag is cut from. A not-installed/editable checkout falls back to a literal that is kept in sync with the currentpyproject.tomlversion at each release.
v0.1.1
cache()now rejects a duplicate filename in a list input withDPYCacheErrorbefore any upload, instead of silently uploading both files and losing the firstFileRefat merge time._to_filenow builds a genuinely freshdiscord.Fileper send fordiscord.Filesources (previously returned the same object, which could swap filename/URL when oneFilewas reused under two names, or re-send 0 bytes / raiseValueErroron a spent stream).lookup()now accepts a digit-only message-id string in addition toint.- Dropped the redundant
discord.Forbiddenmember from anexcepttuple (it already subclassesdiscord.HTTPException); no behavior change.