__init__.py:14 hardcoded "0.1.0" while pyproject.toml, the README
install pin, and the README changelog all said 0.1.1 - a consumer
gating on __version__ read one release behind the actual tag. Derive
__version__ from installed package metadata
(importlib.metadata.version("dpy_cache")) so the hardcoded literal
can no longer drift from the release tag; keep a fallback literal for
the not-installed/editable case, synced to the current pyproject
version.
Bump 0.1.1 -> 0.1.2.
Signed-off-by: disqualifier <dev@disqualifier.me>
126 lines
5.3 KiB
Markdown
126 lines
5.3 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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):
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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.File`s 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.
|