dsql b2bbca0cec fix: drop log-and-raise on the storage-channel send failure
the HTTPException handler logged at ERROR then re-raised - raise XOR log. HTTPException
propagates unwrapped per the documented error contract, so it already carries the failure
to the caller; the error log double-reported it. drop the log, keep the raise. the
genuine swallow-path WARNING elsewhere in the file is unchanged.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-08-09 02:12:29 -04:00
2026-07-09 18:53:15 -04:00
2026-07-09 18:53:15 -04:00

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@v1.0.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

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) -> CacheResult
  • cache_one(name, data, *, content=None) -> FileRef
  • cache_url(name, url, *, content=None) -> FileRef
  • resolve(ref) -> str
  • lookup(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 the pyproject.toml version that the release tag is cut from. A not-installed/editable checkout falls back to a literal that is kept in sync with the current pyproject.toml version at each release.

v0.1.1

  • cache() now rejects a duplicate filename in a list input with DPYCacheError before any upload, instead of silently uploading both files and losing the first FileRef at merge time.
  • _to_file now builds a genuinely fresh discord.File per send for discord.File sources (previously returned the same object, which could swap filename/URL when one File was reused under two names, or re-send 0 bytes / raise ValueError on a spent stream).
  • lookup() now accepts a digit-only message-id string in addition to int.
  • Dropped the redundant discord.Forbidden member from an except tuple (it already subclasses discord.HTTPException); no behavior change.
S
Description
Discord-channel asset cacher for discord.py
Readme
92 KiB
Languages
Python 100%