Skip to main content

discogs-sdk

PyPI version Python versions CI PyPI downloads License

discogs-sdk is a modern Python client for the Discogs API covering every documented v2 endpoint, with a fluent chainable syntax and built-in response caching.

from discogs_sdk import Discogs

with Discogs() as client:
    release = client.releases.get(352665)
    print(f"{release.title} ({release.year})")

    for result in client.search(query="Nine Inch Nails", type="artist"):
        print(result.title)

Installation

pip install discogs-sdk
# or
uv add discogs-sdk

Requires Python 3.10+.

Features

  • Complete Endpoint Coverage — Every route in the Discogs API v2 documentation
  • Fluent API — Chain sub-resources naturally: client.releases.get(id).rating.get()
  • Lazy Loading — No HTTP calls until you actually need the data
  • Effortless Pagination — Browse results without managing pages or offsets
  • Rate Limit Aware — Bounded retries with Retry-After support, the live budget on client.ratelimit, and a structured event per request
  • Built-in Caching — Optional TTL-based caching reduces API calls
  • Flexible Auth — Supports personal tokens, consumer key/secret, or full OAuth 1.0a
  • Type Safe — Get autocomplete and IDE support
  • Async & Sync — Full support for both synchronous and asynchronous workflows

How it compares

The established alternative is python3-discogs-client. Both load data lazily, paginate automatically, support OAuth 1.0a and back off on HTTP 429, so the differences that matter are:

discogs-sdk python3-discogs-client
Documented v2 API coverage Complete Partial †
Sync and async Both Sync only
Responses Typed Pydantic models Untyped attributes
Response cache In-memory or SQLite None

† No inventory export or upload, release ratings or have/want stats, collection fields, folder creation, contributions or submissions.

Quick start

Authentication

Set your personal access token from your Discogs developer settings as an environment variable:

export DISCOGS_TOKEN="your-token-here"
from discogs_sdk import Discogs

client = Discogs()  # reads DISCOGS_TOKEN from environment

You can also pass credentials explicitly:

client = Discogs(token="your-token-here")

The SDK supports three auth modes: personal token, consumer key/secret, and OAuth 1.0a. Exactly one mode is selected when the client is constructed, and only that mode's credentials are used. The precedence is:

  1. An explicit token selects personal-token auth and overrides every environment credential.
  2. Explicit OAuth access-token credentials select OAuth; any missing half may come from the matching DISCOGS_* variable, but an unrelated environment token never takes over.
  3. Explicit consumer_key/consumer_secret select consumer auth, without borrowing environment access tokens.
  4. With no auth arguments, the environment resolves the mode: DISCOGS_TOKEN, then a complete OAuth set, then DISCOGS_CONSUMER_KEY/DISCOGS_CONSUMER_SECRET, then unauthenticated.

An explicitly selected but incomplete credential set raises ValueError rather than quietly falling back to a different account or mode. See examples/authentication.py for the full OAuth flow.

Every transport failure surfaces as DiscogsConnectionError, chained to the underlying httpx2 error (exc.__cause__). Reads retry the transient ones: network errors, timeouts, and a keep-alive connection the server closed (RemoteProtocolError). Failures that would repeat identically — LocalProtocolError, UnsupportedProtocol, ProxyError, DecodingError, TooManyRedirects — are raised at once for every method.

Fetching resources

# Releases, artists, masters, labels
release = client.releases.get(352665)
print(release.title)  # lazy — HTTP fires here → "The Downward Spiral"

artist = client.artists.get(3857)
print(artist.name)  # "Nine Inch Nails"

master = client.masters.get(3719)
label = client.labels.get(647)

These lazy resources are read-only views: assigning to one of their attributes raises AttributeError, since the value would never reach Discogs. Where the API accepts writes, they go through a resource method such as release.rating.update() or user.wantlist.update().

Each identifier you pass (username, order id, release id and so on) is sent as a single, percent-encoded path segment, so a /, ? or # in it cannot reach a different endpoint. "." and ".." raise ValueError before any request is made.

for result in client.search(query="Pretty Hate Machine", type="release", year="1989"):
    print(f"[{result.type}] {result.title}")
    # [release] Nine Inch Nails - Pretty Hate Machine

Sub-resources

# Community rating
rating = client.releases.get(352665).rating.get()
print(f"Average: {rating.rating.average}")  # Average: 4.49

# Artist releases with sorting
for rel in client.artists.get(3857).releases.list(sort="year", sort_order="desc"):
    print(f"{rel.title} ({rel.year})")

# Master versions with filters
for v in client.masters.get(3719).versions.list(format="Vinyl", country="US"):
    print(f"{v.title} [{v.format}]")

Async usage

import asyncio
from discogs_sdk import AsyncDiscogs


async def main():
    async with AsyncDiscogs() as client:  # reads DISCOGS_TOKEN from environment
        # Must await lazy resources in async mode
        release = await client.releases.get(352665)
        print(release.title)

        # Async iteration for paginated results
        async for result in client.search(query="Nine Inch Nails"):
            print(result.title)


asyncio.run(main())

Collection

user = client.users.get("your_username")

# Folders
folders = user.collection.folders.list()
user.collection.folders.create(name="Industrial")

# Browse folder contents
for item in user.collection.folders.get(0).releases.list(sort="added"):
    print(item.basic_information.title)

# Add a release. The response identifies the copy you just created, which is
# how you tell it apart from copies of the same release you already own.
created = user.collection.folders.get(1).releases.create(release_id=352665)

# Deep chaining: folder -> release -> instance -> fields
instances = user.collection.folders.get(1).releases.get(352665).instances
instances.get(created.instance_id).fields.update(field_id=1, value="Signed copy")

# Collection value
value = user.collection.value.get()
print(f"Median: {value.median}, Maximum: {value.maximum}")

# Wantlist — the add takes only the release id; Discogs drops notes and ratings
# sent to it, so they go in the update call
user.wantlist.create(release_id=352665)
user.wantlist.update(352665, notes="Original pressing", rating=4)
for want in user.wantlist.list():
    print(want.basic_information.title)

Marketplace

# Listings
listing = client.marketplace.listings.get(123456789)
new = client.marketplace.listings.create(
    release_id=352665,
    condition="Very Good Plus (VG+)",
    price=25.00,
)
client.marketplace.listings.update(new.id, price=22.50)
client.marketplace.listings.delete(new.id)

# Orders
for order in client.marketplace.orders.list(status="Payment Received"):
    print(f"Order {order.id}: {order.status}")

# Fee lookup
fee = client.marketplace.fee.get(price=25.00, currency="USD")

Error handling

from discogs_sdk import NotFoundError, RateLimitError, AuthenticationError

try:
    release = client.releases.get(999999999)
    _ = release.title
except NotFoundError:
    print("Not found")
except RateLimitError as exc:
    print(f"Rate limited, retry after {exc.retry_after}s")
    if exc.ratelimit:  # the 429's own X-Discogs-Ratelimit* headers, when present
        print(f"{exc.ratelimit.used}/{exc.ratelimit.limit} used this window")
except AuthenticationError:
    print("Bad credentials")

The full exception hierarchy:

DiscogsError
├── DiscogsConnectionError
├── CacheMissError
└── DiscogsAPIError
    ├── AuthenticationError  (401)
    ├── ForbiddenError       (403)
    ├── NotFoundError        (404)
    ├── ValidationError      (422)
    └── RateLimitError       (429)

The OAuth helpers in discogs_sdk.oauth (get_request_token, get_access_token and their async_ variants) raise from the same hierarchy: an error status maps as above (401 → AuthenticationError, 400 → DiscogsAPIError, …), a transport failure raises DiscogsConnectionError, and a successful response missing either oauth_token or oauth_token_secret raises DiscogsAPIError naming any oauth_problem. They never retry: the access-token exchange is sent at most once.

str(exc) on a DiscogsAPIError is a short summary, "<status>: <message>". The message is the body's message when that is a non-empty string, otherwise the body itself, cut to 500 characters so a gateway error page does not flood logs. The full payload stays in exc.response_body.

Every SDK exception survives pickle, copy.copy and copy.deepcopy with its attributes (status_code, response_body, retry_after, ratelimit, method, url) and any notes, so an error raised in a ProcessPoolExecutor or multiprocessing worker reaches the parent as the same class. As with any pickled exception, __cause__, __context__ and the traceback are not carried over.

A paginated iterator (.list(), search()) that raises keeps its place. Whether the request for a page fails, the page's envelope has the wrong shape, or one of its items fails validation (Pydantic's ValidationError), page, the totals and the URLs stay as they were before that page, and the next next() or anext() requests the same page again. Items already yielded are not repeated, and none are skipped.

A wrong shape raises DiscogsError naming the endpoint, the key and the JSON type received, for example GET /users/trent_reznor/submissions: "submissions" is null, expected an object. That covers a body that is not an object, a pagination or a non-empty pagination.urls that is not an object, and an items key that is not an array (or, for nested items, not an object on the way). An absent items key is an empty page.

Exceptions outside DiscogsError

Apart from the ValueErrors that reject invalid arguments, such as incomplete credentials, the SDK lets three exceptions through on purpose, and except DiscogsError catches none of them:

  • json.JSONDecodeError (a ValueError) when a 2xx response that the SDK reads as JSON has a body that is not JSON; a body that is not even UTF-8 raises UnicodeDecodeError, also a ValueError. Error statuses are mapped before the endpoint parses the body, and a success is not an API error, so the SDK raises the parse failure instead of a fallback value.
  • pydantic.ValidationError (a ValueError) when a 2xx JSON body does not fit the model, such as a release whose id is not a number. The response was a success, so this is a parsing failure rather than an API error. Only a list page's envelope is checked by the SDK itself, as described above; its items go to Pydantic like any other model.
  • FileNotFoundError (an OSError) from client.uploads.create(), change() or delete() when file names a path that does not exist. The file is read before any request is sent, so there is no response to map.

Transport failures are not among them: they surface as DiscogsConnectionError, as described under Authentication.

Two unrelated classes are named ValidationError. discogs_sdk.ValidationError is the HTTP 422 DiscogsAPIError above and is not a ValueError; pydantic.ValidationError is the model parsing failure. Imported under the same name, one silently shadows the other, so alias them:

from discogs_sdk import ValidationError as ApiValidationError
from pydantic import ValidationError as ModelValidationError

Examples

The examples/ directory has runnable scripts for every feature:

Configuration

Parameter Default Description
access_token_secret None OAuth access token secret
access_token None OAuth access token
base_url https://api.discogs.com API base URL; pagination next links are rebased onto it, and a link to any origin other than it or https://api.discogs.com raises DiscogsError
cache_dir None Directory for SQLite cache; in-memory when omitted. Used only with cache=True
cache_ttl 3600.0 Cache time-to-live in seconds. Used only with cache=True
cache False True builds the built-in backend from cache_dir and cache_ttl; or pass a custom ResponseCache instance, which carries its own TTL and storage and is never closed by the client
consumer_key None OAuth consumer key
consumer_secret None OAuth consumer secret
http_client None Custom httpx2.Client or httpx2.AsyncClient
max_retries 3 Max retries, >= 0 (0 disables retries, a negative value raises ValueError); reads retry on 429/5xx, network errors, timeouts and dropped connections, mutations only on pre-send failures. A Retry-After above 60s raises immediately instead
media_type "discogs" Format of text fields: "discogs" (Discogs markup), "html" or "plaintext"; sent in the Accept header, and cached responses are kept apart per value
on_request None Callback receiving a RequestEvent per request
timeout 30.0 Request timeout in seconds for the HTTP client the SDK creates; an injected http_client keeps its own (see Custom HTTP clients)
token None Personal access token
user_agent discogs-sdk/<version> +https://github.com/jmfontaine/discogs-sdk User-Agent header; a non-empty value replaces the default entirely

With cache=False, the default, cache_dir and cache_ttl are ignored and no cache or directory is created; a ResponseCache instance ignores them too. cache_ttl=0 is accepted and expires every entry at once, so each read refetches: it is not a way to say "no limit".

See Authentication for how a credential mode is selected.

Caching

Only successful GET/HEAD responses are cached, and a GET body that is not valid JSON (a gateway's HTML page, an empty body) never is, so a bad response fails once instead of on every read; inventory export downloads, which are CSV, are cached as they are. A body that is valid JSON but the wrong shape for its model is still cached until it expires. Entries are keyed by method, fully resolved URL, the effective Accept representation, and a non-reversible digest of the selected mode's credentials, so two clients sharing one cache — or one SQLite directory — never serve each other's private responses, and unauthenticated traffic gets its own namespace. No token or secret is stored in a key.

Job status reads, client.exports.get(id) and client.uploads.get(id), are never cached: each one reaches the API and stores nothing, so a polling loop sees the job finish. A resolved proxy keeps its data, though, so poll with a new get() call each time. Inside cache_only() these reads raise CacheMissError. exports.list(), uploads.list() and exports.download() are cached as usual.

A successful write (POST, PUT or DELETE with a 2xx answer) clears the whole response cache, so no entry cached before the write is served after it: not the written URL, not a list page, not a related folder or count. The rule holds inside no_cache() too, and a cache shared between clients is cleared for all of them; entries are refetched on demand, which costs requests. A failed write leaves the cache alone, and a cache that fails to clear is logged without failing the write. Proxies and pages already resolved keep their in-memory data, so fetch a new proxy or iterate again to see the change. Discogs itself may take a while to show a write, and a refetch made in that window caches what it returned.

A cache built from cache=True belongs to the client, and client.close() closes it. A ResponseCache instance you pass in stays yours, like an injected http_client: client.close() never closes it, so one cache can outlive any of the clients sharing it. Call its close() yourself once every client using it is done.

The SQLite cache never fails a request. If its database is locked by another process, read-only, full or corrupt, it logs a warning on the discogs_sdk logger and carries on: a lookup counts as a miss and a store is skipped, so the response still reaches you. Using the cache after it has been closed raises RuntimeError. The database runs in WAL mode, so cache.db-wal and cache.db-shm may appear next to cache.db.

The SQLite database holds the full bodies of your private responses (identity, orders with buyer addresses, collection), so put cache_dir somewhere only you can read, such as ~/.cache/my-app, never a shared path like /tmp. A directory the cache creates is private (0o700) and cache.db is 0o600; on POSIX, an existing cache.db that is a symlink or belongs to another user raises PermissionError. Entries keep only the Content-Type, ETag and Last-Modified response headers.

client.no_cache() bypasses the cache for the current execution context. Scopes nest, the previous state is restored even when the block raises, and each context carries its own scope, so leaving it in one thread or task never affects another (a pool worker, or a task created before the block, does not see it; see Concurrency):

with client.no_cache():
    fresh = client.releases.get(352665).title  # always hits the API

client.cache_only() is the mirror image: inside the scope a request the cache cannot serve — a miss, an expired entry, a non-GET, a job status read, a disabled cache, an injected http_client without SDK credentials or an enclosing no_cache() — raises CacheMissError before any network I/O. It nests and restores exactly like no_cache(), so you can try everything for free first:

from discogs_sdk import CacheMissError

with client.cache_only():
    try:
        title = client.releases.get(352665).title  # never hits the API
    except CacheMissError as exc:
        print(f"Not cached: {exc.method} {exc.url}")

Observability

Every live response carries Discogs' X-Discogs-Ratelimit, X-Discogs-Ratelimit-Used and X-Discogs-Ratelimit-Remaining headers. client.ratelimit holds them as a RateLimit(limit, used, remaining) parsed from the most recent network response: a cache hit never touches it, and a response without the headers leaves the last known value in place. It is None until the first live response.

on_request receives one RequestEvent per logical request, invoked synchronously — after a cache hit, or after the final network response and before any error is raised, so the event exists even when the call raises. Intermediate retried attempts do not emit; the final event carries attempts. A call that ends in DiscogsConnectionError emits nothing, since there is no response. Exceptions raised by the callback propagate unchanged; keep it cheap and never block in it, because the async client calls it from the event loop.

Field Meaning
method Upper-case HTTP method
url Fully resolved URL, query string included
status_code Status of the response that ended the call
source "network" or "cache"
elapsed_ms Final network attempt, or the cache lookup
attempts Network attempts (1 = no retry); 0 on a cache hit
stored Whether this call wrote the response to the cache
ratelimit RateLimit from the response; always None on a cache hit

A minimal pacing loop:

import time

client = Discogs(
    token="...", on_request=lambda e: print(e.source, e.status_code, e.elapsed_ms)
)
title = client.releases.get(352665).title  # lazy — the GET fires here
if client.ratelimit and client.ratelimit.remaining < 10:
    time.sleep(60)

Concurrency

One client may be shared by several threads (Discogs) or tasks (AsyncDiscogs). A few things are tied to a context or an object rather than to the client.

no_cache() and cache_only() apply to the current execution context only. A task copies its context when it is created, so a task created before the block does not see the scope. A pool worker runs in its own context, so with client.no_cache(): pool.map(...) still serves the workers from the cache; on a free-threaded 3.14+ build, where a new thread starts with a copy of its creator's context, a worker spawned inside the block keeps the scope for its lifetime instead. Either way, enter the scope inside the worker, or run the work in a copy of the context taken inside the scope. Take a new copy for each call: one Context cannot run in two threads at once, and sharing it raises RuntimeError.

import contextvars
from concurrent.futures import ThreadPoolExecutor


def title(release_id):
    return client.releases.get(release_id).title


with ThreadPoolExecutor() as pool:
    with client.no_cache():
        # copy_context() runs here, inside the scope
        future = pool.submit(contextvars.copy_context().run, title, 352665)
    print(future.result())  # The Downward Spiral, fetched from the API

A page returned by .list() or search() is a single-pass iterator for one consumer. Several threads or tasks iterating the same page race on its position and can fetch a page twice and yield its items twice, and a page that is exhausted yields nothing on a second pass. Call .list() or search() again for each consumer and each pass.

on_request runs on the thread or task that made the request, once that request completes, so events arrive in completion order rather than start order, and with several threads the callback can run in several of them at once. client.ratelimit is the value from whichever live response completed last, not necessarily the one with the lowest remaining: under concurrency it is a hint for pacing, not a global count.

The cache that cache=True builds locks internally. A custom ResponseCache shared across threads needs its own locking, because the client calls its methods without synchronisation.

Custom HTTP clients

An injected http_client must be an httpx2 client — the SDK is built on httpx2, Pydantic's maintained continuation of httpx. It owns its transport configuration and its lifecycle: client.close() never closes it. SDK credentials, User-Agent and media type are still applied per request, so they describe the request without mutating your client's defaults, and its own httpx2.Auth cannot replace credentials you gave the SDK.

An injected client's timeout is part of that transport configuration. The timeout argument only configures a client the SDK creates, so Discogs(http_client=httpx2.Client(timeout=99.0), timeout=1.0) sends every request with the injected client's 99-second timeout, even though client.timeout reports 1.0. To control the timeout of an injected client, set it on that client.

When the SDK has no credentials of its own, your client's authentication is preserved and its responses are never cached, even with cache=True, because the SDK cannot tell whose account they belong to. Inside cache_only() every request from such a client therefore raises CacheMissError.

Field naming

Model fields use clean Python names. Where the Discogs API uses inconsistent or cryptic keys, the SDK provides a readable alias while still accepting the raw API name during deserialization:

API field Python attribute Models
anv name_variation ArtistCredit
catno catalog_number LabelCredit, Company, LabelRelease, MasterVersion, SearchResult
created_ts created_at Export, Upload, List_
curr_abbr currency_code OriginalPrice, User
curr_id currency_id OriginalPrice
extraartists extra_artists Release, Track
finished_ts finished_at Export, Upload
from from_user OrderMessage
list_id id List_
listing_id id Listing
modified_ts modified_at List_
namevariations name_variations Artist
qty quantity Format
sublabels sub_labels Label
uri150 uri_150 Image

The Python name is the typed attribute. API names still resolve as attributes at runtime, but only dynamically: a type checker treats them like any misspelled attribute, so read them through getattr where one is in play:

release = client.releases.get(352665)  # The Downward Spiral
print(release.extra_artists)  # Python name, typed
print(getattr(release, "extraartists"))  # API name, runtime only — same value

An API name that is a Python keyword, such as from, is reachable only through getattr(message, "from").

An API name resolves to the same normalized value as the Python name, so getattr(export, "created_ts") is a datetime too. To read what Discogs sent, use raw.

Timestamps

Discogs sends timestamps as strings in several shapes:

Shape Example Where
ISO 8601 with an offset 2005-01-03T23:17:26-08:00 most fields
A space instead of T 2011-08-30 14:21:45-07:00 User.registered
No offset 2026-10-11T07:07:34 Export and Upload (created_at, finished_at)

The SDK parses every one of them into a timezone-aware datetime, so you can compare and sort values from any endpoint. This covers Release (date_added, date_changed), CollectionItem.date_added, ListSummary (date_added, date_changed), List_ (created_at, modified_at), User.registered, Listing.posted, Order (created, last_activity), OrderMessage.timestamp, Export and Upload (created_at, finished_at).

  • An offset that Discogs sends stays as sent.
  • A value without an offset is read as America/Los_Angeles time and gets the fixed offset in force then (-08:00 or -07:00), the same kind of tzinfo as a value that arrives with one. Discogs does not document its zone; this is an assumption. An export requested at 14:07:34 UTC reported 07:07:34, and the offsets Discogs does send switch between -08:00 and -07:00 with daylight saving. In the hour repeated when daylight saving ends, the SDK picks the first occurrence.
  • An empty string becomes None.
  • A value the SDK cannot parse becomes None, and the SDK logs a warning on the discogs_sdk logger. The model still loads, and raw keeps the string.

model_dump(mode="json") writes ISO 8601 with an explicit offset, and model_validate reads it back unchanged.

Original API values

Every model keeps the mapping it was validated from in raw. Use it to read a value as Discogs sent it, before renaming or parsing:

release = client.releases.get(352665)  # The Downward Spiral
print(release.date_added)  # datetime(2005, 1, 3, 23, 17, 26, tzinfo=...)
print(release.raw["date_added"])  # '2005-01-03T23:17:26-08:00'
print(release.extra_artists[0].raw)  # a nested model holds its own part of the payload

raw is the decoded response itself, not a copy; its type is a read-only Mapping, but code holding the same dict can still change it, and later reads see the change. Holding a model keeps the whole payload in memory. model_dump() and == ignore raw. A model you build with keyword arguments or with model_validate(model.model_dump()) reports that input instead of an API response.

Contributing

Contributions are welcome. See CONTRIBUTING.md for guidelines.

License

discogs-sdk is licensed under the Apache License 2.0.

Metadata

Release files for discogs-sdk 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for discogs-sdk 0.6.0
File Size Uploaded
discogs_sdk-0.6.0.tar.gz 92.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for discogs-sdk 0.6.0
File Interpreter ABI Platform
discogs_sdk-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 199.9 kB

Release files / discogs_sdk-0.6.0.tar.gz

Download URL discogs_sdk-0.6.0.tar.gz
Size 92.3 kB
Tags Source
SHA-256 checksum
How to use checksums
a997d2b09cf21cafaf2c7cb149c85e6137655d3bfab6f383659ea8a46c219180
BLAKE2b-256 checksum
How to use checksums
4659bdf0ff1470efc7ef30dccf10d41eddeb2866f82f9a900804e02ce249f07a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release files / discogs_sdk-0.6.0-py3-none-any.whl

Download URL discogs_sdk-0.6.0-py3-none-any.whl
Size 107.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6ae1a798bb54d588239d7a3bd04a8af79bd4d9783363babfc1cc6331339de256
BLAKE2b-256 checksum
How to use checksums
cf51aba52f99d6ecd457364b74403a397ad06878ba40350b832ece4a8609c10a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release history Release notifications | RSS feed

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page