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(). Those methods take the fields Discogs documents as typed keyword parameters: an optional field you omit, or pass as None, is not sent, so an update leaves it unchanged, and a misspelled name raises TypeError before any request is made.

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)
# The response identifies the new listing; it is not a full Listing.
created = client.marketplace.listings.create(
    release_id=352665,
    condition="Very Good Plus (VG+)",
    price=25.00,
)
client.marketplace.listings.update(created.listing_id, price=22.50)
client.marketplace.listings.delete(created.listing_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 as exc:
    print(f"Not found: {exc.method} {exc.url}")
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")

Every DiscogsAPIError names the request that failed: exc.method (upper-case) and exc.url (fully resolved, query string included, as in RequestEvent.url), so an error raised while iterating a listing points at the page that failed. exc.ratelimit holds the RateLimit from the failing response's X-Discogs-Ratelimit* headers for any status, or None when they are missing. All three are None on an exception you construct yourself without them.

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.

client.exports.request() and client.uploads.create(), change() and delete() return the new job's id, read from the Location header of the response, for exports.get(), exports.download() and uploads.get(). A successful response whose Location is missing or does not end in an integer raises DiscogsAPIError with that 2xx status: Discogs accepted the request, so the job probably exists. Find it with exports.list() or uploads.list() instead of submitting again.

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 errors above that the SDK raises on a 2xx describe the problem instead. 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 ResponseCache instance (see Custom backends), 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 built-in caches drop an expired entry when the same request looks it up again, and remove every expired entry at least once per 100 stores; the SQLite cache also does when it opens, so cache.db does not carry expired rows over from earlier runs. Entries still within cache_ttl stay, so the cache's size is bounded by the TTL times the rate of new responses, not by a fixed count. Deleted rows free pages inside cache.db for reuse; the file itself does not shrink.

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}")

Custom backends

The cache types are exported from discogs_sdk. To store responses elsewhere, subclass ResponseCache and implement four methods: get(key) returns a CacheEntry, the (status_code, headers, body) tuple, or None on a miss or an expired entry; set(key, status_code, headers, body) stores one; clear() drops every entry; close() releases whatever the backend holds. The base constructor takes the ttl in seconds, and the backend is the one that enforces it. A backend must be thread-safe: so that disk I/O never blocks the event loop, AsyncDiscogs may call get, set and clear from worker threads, while clear_cache() and close() run on the calling thread, and concurrent requests overlap. The example takes a lock for that:

import threading

from discogs_sdk import CacheEntry, Discogs, ResponseCache


class DictCache(ResponseCache):
    """Unbounded dict, no expiry."""

    def __init__(self) -> None:
        super().__init__(ttl=0)
        self._lock = threading.Lock()
        self._store: dict[str, CacheEntry] = {}

    def get(self, key: str) -> CacheEntry | None:
        with self._lock:
            return self._store.get(key)

    def set(
        self, key: str, status_code: int, headers: dict[str, str], body: bytes
    ) -> None:
        with self._lock:
            self._store[key] = (status_code, headers, body)

    def clear(self) -> None:
        with self._lock:
            self._store.clear()

    def close(self) -> None:
        pass


client = Discogs(cache=DictCache())

MemoryCache(ttl) and SQLiteCache(ttl, cache_dir) are the backends cache=True builds. Create one yourself to pass the same instance to several clients, Discogs and AsyncDiscogs alike. Clients with the same credentials share entries; keys carry a non-reversible digest of the credentials, so clients with different ones still never see each other's private responses. Close the cache yourself once every client using it is done:

from pathlib import Path

from discogs_sdk import Discogs, SQLiteCache

shared = SQLiteCache(ttl=3600, cache_dir=Path.home() / ".cache" / "my-app")
first = Discogs(token="YOUR_TOKEN", cache=shared)
second = Discogs(token="YOUR_TOKEN", cache=shared)
print(first.releases.get(352665).title)  # fetched from the API
print(second.releases.get(352665).title)  # served from the shared cache
first.close()
second.close()
shared.close()  # a client never closes a cache passed in

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 or CacheMissError 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

An unresolved AsyncDiscogs proxy awaited from several tasks at once sends one request and hands every task the same model (or the same exception; a failure is not kept, so the next await tries again). That request runs in the context of the task that awaited first, so its no_cache() or cache_only() scope, or the lack of one, applies to all of them. Enter the scope before the first await, or call get() again inside it for a proxy of your own. Cancelling one waiting task leaves the request and the other tasks alone.

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 needs its own locking whenever several threads share it, and with AsyncDiscogs that is always the case: the async client may call the cache from worker threads so that disk I/O never blocks the event loop, and it 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_
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.7.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.7.0
File Size Uploaded
discogs_sdk-0.7.0.tar.gz 101.1 kB Details

Built distribution (wheel)

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

Total release size: 216.4 kB

Release files / discogs_sdk-0.7.0.tar.gz

Download URL discogs_sdk-0.7.0.tar.gz
Size 101.1 kB
Tags Source
SHA-256 checksum
How to use checksums
f198c899afb30292b55dbae7c780d3aed24b6f34d0a80ae72da4a4910382a993
BLAKE2b-256 checksum
How to use checksums
2608e7196bb065aadf011917dc9abbd19b01dcdc72e2074d4c8d711189495f86
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.7.0-py3-none-any.whl

Download URL discogs_sdk-0.7.0-py3-none-any.whl
Size 115.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
39717a20033c5d8b477ce2aea17c7511eb8416705218343109a7e434dafc4d63
BLAKE2b-256 checksum
How to use checksums
931d9d3a4233749f3aca32e134fd18ed80b3b09246cff2366f34ed209222d04c
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

This release

0.7.0 This release

2 release files

0.6.0

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