discogs-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- discogs_sdk-0.1.0/PKG-INFO +339 -0
- discogs_sdk-0.1.0/README.md +310 -0
- discogs_sdk-0.1.0/pyproject.toml +80 -0
- discogs_sdk-0.1.0/src/discogs_sdk/__init__.py +134 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/__init__.py +3 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/_client.py +258 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/_lazy.py +94 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/_oauth.py +100 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/_paginator.py +100 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/_resource.py +92 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/__init__.py +75 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/artists.py +39 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/collection.py +248 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/exports.py +30 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/labels.py +32 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/lists.py +33 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/marketplace.py +122 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/masters.py +50 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/releases.py +96 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/search.py +22 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/uploads.py +35 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/users.py +168 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_async/resources/wantlist.py +34 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_base_client.py +177 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_exceptions.py +60 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/__init__.py +6 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/_client.py +230 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/_lazy.py +72 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/_oauth.py +85 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/_paginator.py +89 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/_resource.py +66 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/__init__.py +78 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/artists.py +34 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/collection.py +228 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/exports.py +23 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/labels.py +29 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/lists.py +28 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/marketplace.py +101 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/masters.py +43 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/releases.py +81 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/search.py +19 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/uploads.py +28 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/users.py +160 -0
- discogs_sdk-0.1.0/src/discogs_sdk/_sync/resources/wantlist.py +30 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/__init__.py +109 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/_common.py +171 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/artist.py +34 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/collection.py +38 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/export.py +15 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/label.py +31 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/list_.py +38 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/marketplace.py +90 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/master.py +40 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/release.py +94 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/search.py +28 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/upload.py +17 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/user.py +46 -0
- discogs_sdk-0.1.0/src/discogs_sdk/models/wantlist.py +11 -0
- discogs_sdk-0.1.0/src/discogs_sdk/oauth.py +21 -0
- discogs_sdk-0.1.0/src/discogs_sdk/py.typed +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: discogs-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Modern Python client for the Discogs API
|
|
5
|
+
Keywords: api,discogs,music,python,sdk
|
|
6
|
+
Author: Jean-Marc Fontaine
|
|
7
|
+
Author-email: Jean-Marc Fontaine <jm@jmfontaine.net>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: httpx>=0.28
|
|
20
|
+
Requires-Dist: pydantic>=2.12
|
|
21
|
+
Requires-Dist: typing-extensions>=4
|
|
22
|
+
Requires-Dist: hishel>=0.1 ; extra == 'cache'
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Project-URL: Homepage, https://github.com/jmfontaine/discogs-sdk
|
|
25
|
+
Project-URL: Issues, https://github.com/jmfontaine/discogs-sdk/issues
|
|
26
|
+
Project-URL: Repository, https://github.com/jmfontaine/discogs-sdk
|
|
27
|
+
Provides-Extra: cache
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# discogs-sdk
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<a href="https://pypi.org/project/discogs-sdk/"><img src="https://img.shields.io/pypi/v/discogs-sdk.svg" alt="PyPI version"></a>
|
|
34
|
+
<a href="https://pypi.org/project/discogs-sdk/"><img src="https://img.shields.io/pypi/pyversions/discogs-sdk.svg" alt="Python versions"></a>
|
|
35
|
+
<a href="https://github.com/jmfontaine/discogs-sdk/actions"><img src="https://github.com/jmfontaine/discogs-sdk/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
36
|
+
<a href="https://pypi.org/project/discogs-sdk/"><img src="https://img.shields.io/pypi/dm/discogs-sdk.svg" alt="PyPI downloads"></a>
|
|
37
|
+
<a href="https://github.com/jmfontaine/discogs-sdk/blob/main/LICENSE.txt"><img src="https://img.shields.io/pypi/l/discogs-sdk.svg" alt="License"></a>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
A typed Python client for the [Discogs API](https://www.discogs.com/developers). Sync and async, with Pydantic models, automatic pagination, and lazy resource loading.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from discogs_sdk import Discogs
|
|
44
|
+
|
|
45
|
+
# Reads DISCOGS_TOKEN from environment
|
|
46
|
+
with Discogs() as client:
|
|
47
|
+
release = client.releases.get(352665)
|
|
48
|
+
print(f"{release.title} ({release.year})")
|
|
49
|
+
# The Downward Spiral (1994)
|
|
50
|
+
|
|
51
|
+
for result in client.search(query="Nine Inch Nails", type="artist"):
|
|
52
|
+
print(result.title)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Sync and async** — `Discogs` for synchronous code, `AsyncDiscogs` for async/await
|
|
58
|
+
- **Typed models** — every response is a Pydantic v2 model with `extra="allow"` for undocumented fields
|
|
59
|
+
- **Lazy resources** — `.get()` returns a lightweight proxy; HTTP fires only when you access data
|
|
60
|
+
- **Auto-pagination** — iterate results with `for`/`async for`; pages are fetched on demand
|
|
61
|
+
- **Sub-resource chaining** — `client.releases.get(id).rating.get()` — intermediate accessors never trigger HTTP
|
|
62
|
+
- **Automatic retries** — retries on 429/5xx with exponential backoff and `Retry-After` support
|
|
63
|
+
- **Three auth modes** — personal token, consumer key/secret, or full OAuth 1.0a
|
|
64
|
+
- **Optional caching** — `cache=True` enables [hishel](https://github.com/karpetrosyan/hishel)-backed HTTP caching
|
|
65
|
+
|
|
66
|
+
## Why discogs-sdk?
|
|
67
|
+
|
|
68
|
+
- **Typed models** — Pydantic v2 models give you autocomplete and IDE support instead of untyped dicts
|
|
69
|
+
- **Lazy loading that respects rate limits** — `.get()` returns a proxy; HTTP fires only when you access data, minimizing calls against the 60 req/min limit
|
|
70
|
+
- **Sync + async from one codebase** — async-first source with auto-generated sync client, identical APIs
|
|
71
|
+
- **Auto-pagination** — iterate results with `for`/`async for`; no manual page math
|
|
72
|
+
- **Modern stack** — built on httpx and Pydantic v2, not requests and raw dicts
|
|
73
|
+
|
|
74
|
+
### How it compares
|
|
75
|
+
|
|
76
|
+
| Feature | discogs-sdk | python3-discogs-client | Raw API |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| Typed models (Pydantic) | Yes | No | No |
|
|
79
|
+
| Async support | Yes | No | Manual |
|
|
80
|
+
| Auto-pagination | Yes | Yes | Manual |
|
|
81
|
+
| Lazy loading | Yes | No | N/A |
|
|
82
|
+
| Rate limit handling | Automatic retry | Manual | Manual |
|
|
83
|
+
| OAuth 1.0a | Yes | Yes | Manual |
|
|
84
|
+
| HTTP caching | Built-in opt-in | No | Manual |
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install discogs-sdk
|
|
90
|
+
# or
|
|
91
|
+
uv add discogs-sdk
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
With HTTP caching support:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install discogs-sdk[cache]
|
|
98
|
+
# or
|
|
99
|
+
uv add discogs-sdk[cache]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Requires Python 3.10+.
|
|
103
|
+
|
|
104
|
+
## Quick start
|
|
105
|
+
|
|
106
|
+
### Authentication
|
|
107
|
+
|
|
108
|
+
Set your personal access token from [your Discogs developer settings](https://www.discogs.com/settings/developers) as an environment variable:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export DISCOGS_TOKEN="your-token-here"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from discogs_sdk import Discogs
|
|
116
|
+
|
|
117
|
+
client = Discogs() # reads DISCOGS_TOKEN from environment
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
You can also pass credentials explicitly:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
client = Discogs(token="your-token-here")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The SDK supports three auth modes: personal token, consumer key/secret, and OAuth 1.0a. All credentials resolve via constructor arg > environment variable (`DISCOGS_TOKEN`, `DISCOGS_CONSUMER_KEY`, etc.). See [`examples/authentication.py`](examples/authentication.py) for the full OAuth flow.
|
|
127
|
+
|
|
128
|
+
> [!TIP]
|
|
129
|
+
> Use a `.env` file with [python-dotenv](https://pypi.org/project/python-dotenv/) or
|
|
130
|
+
> [direnv](https://direnv.net/) to avoid exporting tokens manually in every shell.
|
|
131
|
+
|
|
132
|
+
> [!NOTE]
|
|
133
|
+
> Discogs enforces a 60 requests/minute rate limit. The SDK handles this automatically
|
|
134
|
+
> with exponential backoff and `Retry-After` support — no manual throttling needed.
|
|
135
|
+
|
|
136
|
+
### Fetching resources
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Releases, artists, masters, labels
|
|
140
|
+
release = client.releases.get(352665)
|
|
141
|
+
print(release.title) # lazy — HTTP fires here → "The Downward Spiral"
|
|
142
|
+
|
|
143
|
+
artist = client.artists.get(3857)
|
|
144
|
+
print(artist.name) # "Nine Inch Nails"
|
|
145
|
+
|
|
146
|
+
master = client.masters.get(3719)
|
|
147
|
+
label = client.labels.get(647)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Search
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
for result in client.search(query="Pretty Hate Machine", type="release", year="1989"):
|
|
154
|
+
print(f"[{result.type}] {result.title}")
|
|
155
|
+
# [release] Nine Inch Nails - Pretty Hate Machine
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Sub-resources
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# Community rating
|
|
162
|
+
rating = client.releases.get(352665).rating.get()
|
|
163
|
+
print(f"Average: {rating.rating.average}") # Average: 4.49
|
|
164
|
+
|
|
165
|
+
# Artist releases with sorting
|
|
166
|
+
for rel in client.artists.get(3857).releases.list(sort="year", sort_order="desc"):
|
|
167
|
+
print(f"{rel.title} ({rel.year})")
|
|
168
|
+
|
|
169
|
+
# Master versions with filters
|
|
170
|
+
for v in client.masters.get(3719).versions.list(format="Vinyl", country="US"):
|
|
171
|
+
print(f"{v.title} [{v.format}]")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Async usage
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
import asyncio
|
|
178
|
+
from discogs_sdk import AsyncDiscogs
|
|
179
|
+
|
|
180
|
+
async def main():
|
|
181
|
+
async with AsyncDiscogs() as client: # reads DISCOGS_TOKEN from environment
|
|
182
|
+
# Must await lazy resources in async mode
|
|
183
|
+
release = await client.releases.get(352665)
|
|
184
|
+
print(release.title)
|
|
185
|
+
|
|
186
|
+
# Async iteration for paginated results
|
|
187
|
+
async for result in client.search(query="Nine Inch Nails"):
|
|
188
|
+
print(result.title)
|
|
189
|
+
|
|
190
|
+
asyncio.run(main())
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Marketplace
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# Listings
|
|
197
|
+
listing = client.marketplace.listings.get(123456789)
|
|
198
|
+
new = client.marketplace.listings.create(
|
|
199
|
+
release_id=352665, condition="Very Good Plus (VG+)", price=25.00,
|
|
200
|
+
)
|
|
201
|
+
client.marketplace.listings.update(new.id, price=22.50)
|
|
202
|
+
client.marketplace.listings.delete(new.id)
|
|
203
|
+
|
|
204
|
+
# Orders
|
|
205
|
+
for order in client.marketplace.orders.list(status="Payment Received"):
|
|
206
|
+
print(f"Order {order.id}: {order.status}")
|
|
207
|
+
|
|
208
|
+
# Fee lookup
|
|
209
|
+
fee = client.marketplace.fee.get(price=25.00, currency="USD")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Collection
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
user = client.users.get("your_username")
|
|
216
|
+
|
|
217
|
+
# Folders
|
|
218
|
+
folders = user.collection.folders.list()
|
|
219
|
+
user.collection.folders.create(name="Industrial")
|
|
220
|
+
|
|
221
|
+
# Browse folder contents
|
|
222
|
+
for item in user.collection.folders.get(0).releases.list(sort="added"):
|
|
223
|
+
print(item.basic_information.title)
|
|
224
|
+
|
|
225
|
+
# Add a release
|
|
226
|
+
user.collection.folders.get(1).releases.create(release_id=352665)
|
|
227
|
+
|
|
228
|
+
# Deep chaining: folder -> release -> instance -> fields
|
|
229
|
+
user.collection.folders.get(1).releases.get(352665).instances.get(
|
|
230
|
+
98765
|
|
231
|
+
).fields.update(field_id=1, value="Signed copy")
|
|
232
|
+
|
|
233
|
+
# Collection value
|
|
234
|
+
value = user.collection.value.get()
|
|
235
|
+
print(f"Median: {value.median}, Maximum: {value.maximum}")
|
|
236
|
+
|
|
237
|
+
# Wantlist
|
|
238
|
+
user.wantlist.create(release_id=352665, notes="Original pressing", rating=4)
|
|
239
|
+
for want in user.wantlist.list():
|
|
240
|
+
print(want.basic_information.title)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Error handling
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from discogs_sdk import NotFoundError, RateLimitError, AuthenticationError
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
release = client.releases.get(999999999)
|
|
250
|
+
_ = release.title
|
|
251
|
+
except NotFoundError:
|
|
252
|
+
print("Not found")
|
|
253
|
+
except RateLimitError as exc:
|
|
254
|
+
print(f"Rate limited, retry after {exc.retry_after}s")
|
|
255
|
+
except AuthenticationError:
|
|
256
|
+
print("Bad credentials")
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The full exception hierarchy:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
DiscogsError
|
|
263
|
+
+-- DiscogsConnectionError
|
|
264
|
+
+-- DiscogsAPIError
|
|
265
|
+
+-- AuthenticationError (401)
|
|
266
|
+
+-- ForbiddenError (403)
|
|
267
|
+
+-- NotFoundError (404)
|
|
268
|
+
+-- ValidationError (422)
|
|
269
|
+
+-- RateLimitError (429)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## API coverage
|
|
273
|
+
|
|
274
|
+
| Area | Resources |
|
|
275
|
+
|---|---|
|
|
276
|
+
| **Database** | Releases, Artists, Masters, Labels, Search |
|
|
277
|
+
| **Marketplace** | Listings, Orders, Order Messages, Fees |
|
|
278
|
+
| **Collection** | Folders, Releases, Instances, Custom Fields, Value |
|
|
279
|
+
| **User** | Profile, Identity, Wantlist, Contributions, Submissions, Inventory, Lists |
|
|
280
|
+
| **Inventory** | Exports (request/download CSV), Uploads (add/change/delete CSV) |
|
|
281
|
+
| **Lists** | Get list by ID, browse user lists |
|
|
282
|
+
|
|
283
|
+
## Examples
|
|
284
|
+
|
|
285
|
+
The [`examples/`](examples/) directory has runnable scripts for every feature:
|
|
286
|
+
|
|
287
|
+
- [`quickstart.py`](examples/quickstart.py) — first requests, lazy loading, search
|
|
288
|
+
- [`authentication.py`](examples/authentication.py) — all auth modes including OAuth 1.0a
|
|
289
|
+
- [`database.py`](examples/database.py) — releases, artists, masters, labels, search
|
|
290
|
+
- [`marketplace.py`](examples/marketplace.py) — listings, orders, fees, inventory
|
|
291
|
+
- [`collection.py`](examples/collection.py) — folders, instances, fields, wantlist
|
|
292
|
+
- [`async_usage.py`](examples/async_usage.py) — async client with await and async for
|
|
293
|
+
- [`advanced.py`](examples/advanced.py) — error handling, caching, custom HTTP clients, exports
|
|
294
|
+
|
|
295
|
+
## Configuration
|
|
296
|
+
|
|
297
|
+
| Parameter | Default | Description |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `token` | `None` | Personal access token |
|
|
300
|
+
| `consumer_key` | `None` | OAuth consumer key |
|
|
301
|
+
| `consumer_secret` | `None` | OAuth consumer secret |
|
|
302
|
+
| `access_token` | `None` | OAuth access token |
|
|
303
|
+
| `access_token_secret` | `None` | OAuth access token secret |
|
|
304
|
+
| `base_url` | `https://api.discogs.com` | API base URL |
|
|
305
|
+
| `timeout` | `30.0` | Request timeout in seconds |
|
|
306
|
+
| `max_retries` | `3` | Max retries on 429/5xx/connection errors |
|
|
307
|
+
| `cache` | `False` | Enable HTTP caching (requires `discogs-sdk[cache]`) |
|
|
308
|
+
| `http_client` | `None` | Custom `httpx.Client` or `httpx.AsyncClient` |
|
|
309
|
+
|
|
310
|
+
Credentials are resolved in order: constructor args > environment variables.
|
|
311
|
+
|
|
312
|
+
## Field naming
|
|
313
|
+
|
|
314
|
+
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:
|
|
315
|
+
|
|
316
|
+
| API field | Python attribute | Models |
|
|
317
|
+
|---|---|---|
|
|
318
|
+
| `uri150` | `uri_150` | `Image` |
|
|
319
|
+
| `anv` | `name_variation` | `ArtistCredit` |
|
|
320
|
+
| `extraartists` | `extra_artists` | `Release`, `Track` |
|
|
321
|
+
| `namevariations` | `name_variations` | `Artist` |
|
|
322
|
+
| `qty` | `quantity` | `Format` |
|
|
323
|
+
| `catno` | `catalog_number` | `LabelCredit`, `Company`, `LabelRelease`, `SearchResult` |
|
|
324
|
+
| `sublabels` | `sub_labels` | `Label` |
|
|
325
|
+
| `curr_abbr` | `currency_code` | `OriginalPrice`, `User` |
|
|
326
|
+
| `curr_id` | `currency_id` | `OriginalPrice` |
|
|
327
|
+
| `created_ts` | `created_at` | `Export`, `Upload`, `List_` |
|
|
328
|
+
| `finished_ts` | `finished_at` | `Export`, `Upload` |
|
|
329
|
+
| `modified_ts` | `modified_at` | `List_` |
|
|
330
|
+
|
|
331
|
+
Both names work when constructing models manually (`Image(uri150="...")` and `Image(uri_150="...")` are equivalent). When accessing attributes, use the Python name: `image.uri_150`.
|
|
332
|
+
|
|
333
|
+
## Contributing
|
|
334
|
+
|
|
335
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
336
|
+
|
|
337
|
+
## License
|
|
338
|
+
|
|
339
|
+
discogs-sdk is licensed under the [Apache License 2.0](LICENSE.txt).
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# discogs-sdk
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://pypi.org/project/discogs-sdk/"><img src="https://img.shields.io/pypi/v/discogs-sdk.svg" alt="PyPI version"></a>
|
|
5
|
+
<a href="https://pypi.org/project/discogs-sdk/"><img src="https://img.shields.io/pypi/pyversions/discogs-sdk.svg" alt="Python versions"></a>
|
|
6
|
+
<a href="https://github.com/jmfontaine/discogs-sdk/actions"><img src="https://github.com/jmfontaine/discogs-sdk/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
7
|
+
<a href="https://pypi.org/project/discogs-sdk/"><img src="https://img.shields.io/pypi/dm/discogs-sdk.svg" alt="PyPI downloads"></a>
|
|
8
|
+
<a href="https://github.com/jmfontaine/discogs-sdk/blob/main/LICENSE.txt"><img src="https://img.shields.io/pypi/l/discogs-sdk.svg" alt="License"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
A typed Python client for the [Discogs API](https://www.discogs.com/developers). Sync and async, with Pydantic models, automatic pagination, and lazy resource loading.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from discogs_sdk import Discogs
|
|
15
|
+
|
|
16
|
+
# Reads DISCOGS_TOKEN from environment
|
|
17
|
+
with Discogs() as client:
|
|
18
|
+
release = client.releases.get(352665)
|
|
19
|
+
print(f"{release.title} ({release.year})")
|
|
20
|
+
# The Downward Spiral (1994)
|
|
21
|
+
|
|
22
|
+
for result in client.search(query="Nine Inch Nails", type="artist"):
|
|
23
|
+
print(result.title)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Sync and async** — `Discogs` for synchronous code, `AsyncDiscogs` for async/await
|
|
29
|
+
- **Typed models** — every response is a Pydantic v2 model with `extra="allow"` for undocumented fields
|
|
30
|
+
- **Lazy resources** — `.get()` returns a lightweight proxy; HTTP fires only when you access data
|
|
31
|
+
- **Auto-pagination** — iterate results with `for`/`async for`; pages are fetched on demand
|
|
32
|
+
- **Sub-resource chaining** — `client.releases.get(id).rating.get()` — intermediate accessors never trigger HTTP
|
|
33
|
+
- **Automatic retries** — retries on 429/5xx with exponential backoff and `Retry-After` support
|
|
34
|
+
- **Three auth modes** — personal token, consumer key/secret, or full OAuth 1.0a
|
|
35
|
+
- **Optional caching** — `cache=True` enables [hishel](https://github.com/karpetrosyan/hishel)-backed HTTP caching
|
|
36
|
+
|
|
37
|
+
## Why discogs-sdk?
|
|
38
|
+
|
|
39
|
+
- **Typed models** — Pydantic v2 models give you autocomplete and IDE support instead of untyped dicts
|
|
40
|
+
- **Lazy loading that respects rate limits** — `.get()` returns a proxy; HTTP fires only when you access data, minimizing calls against the 60 req/min limit
|
|
41
|
+
- **Sync + async from one codebase** — async-first source with auto-generated sync client, identical APIs
|
|
42
|
+
- **Auto-pagination** — iterate results with `for`/`async for`; no manual page math
|
|
43
|
+
- **Modern stack** — built on httpx and Pydantic v2, not requests and raw dicts
|
|
44
|
+
|
|
45
|
+
### How it compares
|
|
46
|
+
|
|
47
|
+
| Feature | discogs-sdk | python3-discogs-client | Raw API |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| Typed models (Pydantic) | Yes | No | No |
|
|
50
|
+
| Async support | Yes | No | Manual |
|
|
51
|
+
| Auto-pagination | Yes | Yes | Manual |
|
|
52
|
+
| Lazy loading | Yes | No | N/A |
|
|
53
|
+
| Rate limit handling | Automatic retry | Manual | Manual |
|
|
54
|
+
| OAuth 1.0a | Yes | Yes | Manual |
|
|
55
|
+
| HTTP caching | Built-in opt-in | No | Manual |
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install discogs-sdk
|
|
61
|
+
# or
|
|
62
|
+
uv add discogs-sdk
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With HTTP caching support:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install discogs-sdk[cache]
|
|
69
|
+
# or
|
|
70
|
+
uv add discogs-sdk[cache]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Python 3.10+.
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
### Authentication
|
|
78
|
+
|
|
79
|
+
Set your personal access token from [your Discogs developer settings](https://www.discogs.com/settings/developers) as an environment variable:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
export DISCOGS_TOKEN="your-token-here"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from discogs_sdk import Discogs
|
|
87
|
+
|
|
88
|
+
client = Discogs() # reads DISCOGS_TOKEN from environment
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
You can also pass credentials explicitly:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
client = Discogs(token="your-token-here")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The SDK supports three auth modes: personal token, consumer key/secret, and OAuth 1.0a. All credentials resolve via constructor arg > environment variable (`DISCOGS_TOKEN`, `DISCOGS_CONSUMER_KEY`, etc.). See [`examples/authentication.py`](examples/authentication.py) for the full OAuth flow.
|
|
98
|
+
|
|
99
|
+
> [!TIP]
|
|
100
|
+
> Use a `.env` file with [python-dotenv](https://pypi.org/project/python-dotenv/) or
|
|
101
|
+
> [direnv](https://direnv.net/) to avoid exporting tokens manually in every shell.
|
|
102
|
+
|
|
103
|
+
> [!NOTE]
|
|
104
|
+
> Discogs enforces a 60 requests/minute rate limit. The SDK handles this automatically
|
|
105
|
+
> with exponential backoff and `Retry-After` support — no manual throttling needed.
|
|
106
|
+
|
|
107
|
+
### Fetching resources
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Releases, artists, masters, labels
|
|
111
|
+
release = client.releases.get(352665)
|
|
112
|
+
print(release.title) # lazy — HTTP fires here → "The Downward Spiral"
|
|
113
|
+
|
|
114
|
+
artist = client.artists.get(3857)
|
|
115
|
+
print(artist.name) # "Nine Inch Nails"
|
|
116
|
+
|
|
117
|
+
master = client.masters.get(3719)
|
|
118
|
+
label = client.labels.get(647)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Search
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
for result in client.search(query="Pretty Hate Machine", type="release", year="1989"):
|
|
125
|
+
print(f"[{result.type}] {result.title}")
|
|
126
|
+
# [release] Nine Inch Nails - Pretty Hate Machine
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Sub-resources
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Community rating
|
|
133
|
+
rating = client.releases.get(352665).rating.get()
|
|
134
|
+
print(f"Average: {rating.rating.average}") # Average: 4.49
|
|
135
|
+
|
|
136
|
+
# Artist releases with sorting
|
|
137
|
+
for rel in client.artists.get(3857).releases.list(sort="year", sort_order="desc"):
|
|
138
|
+
print(f"{rel.title} ({rel.year})")
|
|
139
|
+
|
|
140
|
+
# Master versions with filters
|
|
141
|
+
for v in client.masters.get(3719).versions.list(format="Vinyl", country="US"):
|
|
142
|
+
print(f"{v.title} [{v.format}]")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Async usage
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import asyncio
|
|
149
|
+
from discogs_sdk import AsyncDiscogs
|
|
150
|
+
|
|
151
|
+
async def main():
|
|
152
|
+
async with AsyncDiscogs() as client: # reads DISCOGS_TOKEN from environment
|
|
153
|
+
# Must await lazy resources in async mode
|
|
154
|
+
release = await client.releases.get(352665)
|
|
155
|
+
print(release.title)
|
|
156
|
+
|
|
157
|
+
# Async iteration for paginated results
|
|
158
|
+
async for result in client.search(query="Nine Inch Nails"):
|
|
159
|
+
print(result.title)
|
|
160
|
+
|
|
161
|
+
asyncio.run(main())
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Marketplace
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# Listings
|
|
168
|
+
listing = client.marketplace.listings.get(123456789)
|
|
169
|
+
new = client.marketplace.listings.create(
|
|
170
|
+
release_id=352665, condition="Very Good Plus (VG+)", price=25.00,
|
|
171
|
+
)
|
|
172
|
+
client.marketplace.listings.update(new.id, price=22.50)
|
|
173
|
+
client.marketplace.listings.delete(new.id)
|
|
174
|
+
|
|
175
|
+
# Orders
|
|
176
|
+
for order in client.marketplace.orders.list(status="Payment Received"):
|
|
177
|
+
print(f"Order {order.id}: {order.status}")
|
|
178
|
+
|
|
179
|
+
# Fee lookup
|
|
180
|
+
fee = client.marketplace.fee.get(price=25.00, currency="USD")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Collection
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
user = client.users.get("your_username")
|
|
187
|
+
|
|
188
|
+
# Folders
|
|
189
|
+
folders = user.collection.folders.list()
|
|
190
|
+
user.collection.folders.create(name="Industrial")
|
|
191
|
+
|
|
192
|
+
# Browse folder contents
|
|
193
|
+
for item in user.collection.folders.get(0).releases.list(sort="added"):
|
|
194
|
+
print(item.basic_information.title)
|
|
195
|
+
|
|
196
|
+
# Add a release
|
|
197
|
+
user.collection.folders.get(1).releases.create(release_id=352665)
|
|
198
|
+
|
|
199
|
+
# Deep chaining: folder -> release -> instance -> fields
|
|
200
|
+
user.collection.folders.get(1).releases.get(352665).instances.get(
|
|
201
|
+
98765
|
|
202
|
+
).fields.update(field_id=1, value="Signed copy")
|
|
203
|
+
|
|
204
|
+
# Collection value
|
|
205
|
+
value = user.collection.value.get()
|
|
206
|
+
print(f"Median: {value.median}, Maximum: {value.maximum}")
|
|
207
|
+
|
|
208
|
+
# Wantlist
|
|
209
|
+
user.wantlist.create(release_id=352665, notes="Original pressing", rating=4)
|
|
210
|
+
for want in user.wantlist.list():
|
|
211
|
+
print(want.basic_information.title)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Error handling
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from discogs_sdk import NotFoundError, RateLimitError, AuthenticationError
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
release = client.releases.get(999999999)
|
|
221
|
+
_ = release.title
|
|
222
|
+
except NotFoundError:
|
|
223
|
+
print("Not found")
|
|
224
|
+
except RateLimitError as exc:
|
|
225
|
+
print(f"Rate limited, retry after {exc.retry_after}s")
|
|
226
|
+
except AuthenticationError:
|
|
227
|
+
print("Bad credentials")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The full exception hierarchy:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
DiscogsError
|
|
234
|
+
+-- DiscogsConnectionError
|
|
235
|
+
+-- DiscogsAPIError
|
|
236
|
+
+-- AuthenticationError (401)
|
|
237
|
+
+-- ForbiddenError (403)
|
|
238
|
+
+-- NotFoundError (404)
|
|
239
|
+
+-- ValidationError (422)
|
|
240
|
+
+-- RateLimitError (429)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## API coverage
|
|
244
|
+
|
|
245
|
+
| Area | Resources |
|
|
246
|
+
|---|---|
|
|
247
|
+
| **Database** | Releases, Artists, Masters, Labels, Search |
|
|
248
|
+
| **Marketplace** | Listings, Orders, Order Messages, Fees |
|
|
249
|
+
| **Collection** | Folders, Releases, Instances, Custom Fields, Value |
|
|
250
|
+
| **User** | Profile, Identity, Wantlist, Contributions, Submissions, Inventory, Lists |
|
|
251
|
+
| **Inventory** | Exports (request/download CSV), Uploads (add/change/delete CSV) |
|
|
252
|
+
| **Lists** | Get list by ID, browse user lists |
|
|
253
|
+
|
|
254
|
+
## Examples
|
|
255
|
+
|
|
256
|
+
The [`examples/`](examples/) directory has runnable scripts for every feature:
|
|
257
|
+
|
|
258
|
+
- [`quickstart.py`](examples/quickstart.py) — first requests, lazy loading, search
|
|
259
|
+
- [`authentication.py`](examples/authentication.py) — all auth modes including OAuth 1.0a
|
|
260
|
+
- [`database.py`](examples/database.py) — releases, artists, masters, labels, search
|
|
261
|
+
- [`marketplace.py`](examples/marketplace.py) — listings, orders, fees, inventory
|
|
262
|
+
- [`collection.py`](examples/collection.py) — folders, instances, fields, wantlist
|
|
263
|
+
- [`async_usage.py`](examples/async_usage.py) — async client with await and async for
|
|
264
|
+
- [`advanced.py`](examples/advanced.py) — error handling, caching, custom HTTP clients, exports
|
|
265
|
+
|
|
266
|
+
## Configuration
|
|
267
|
+
|
|
268
|
+
| Parameter | Default | Description |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| `token` | `None` | Personal access token |
|
|
271
|
+
| `consumer_key` | `None` | OAuth consumer key |
|
|
272
|
+
| `consumer_secret` | `None` | OAuth consumer secret |
|
|
273
|
+
| `access_token` | `None` | OAuth access token |
|
|
274
|
+
| `access_token_secret` | `None` | OAuth access token secret |
|
|
275
|
+
| `base_url` | `https://api.discogs.com` | API base URL |
|
|
276
|
+
| `timeout` | `30.0` | Request timeout in seconds |
|
|
277
|
+
| `max_retries` | `3` | Max retries on 429/5xx/connection errors |
|
|
278
|
+
| `cache` | `False` | Enable HTTP caching (requires `discogs-sdk[cache]`) |
|
|
279
|
+
| `http_client` | `None` | Custom `httpx.Client` or `httpx.AsyncClient` |
|
|
280
|
+
|
|
281
|
+
Credentials are resolved in order: constructor args > environment variables.
|
|
282
|
+
|
|
283
|
+
## Field naming
|
|
284
|
+
|
|
285
|
+
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:
|
|
286
|
+
|
|
287
|
+
| API field | Python attribute | Models |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `uri150` | `uri_150` | `Image` |
|
|
290
|
+
| `anv` | `name_variation` | `ArtistCredit` |
|
|
291
|
+
| `extraartists` | `extra_artists` | `Release`, `Track` |
|
|
292
|
+
| `namevariations` | `name_variations` | `Artist` |
|
|
293
|
+
| `qty` | `quantity` | `Format` |
|
|
294
|
+
| `catno` | `catalog_number` | `LabelCredit`, `Company`, `LabelRelease`, `SearchResult` |
|
|
295
|
+
| `sublabels` | `sub_labels` | `Label` |
|
|
296
|
+
| `curr_abbr` | `currency_code` | `OriginalPrice`, `User` |
|
|
297
|
+
| `curr_id` | `currency_id` | `OriginalPrice` |
|
|
298
|
+
| `created_ts` | `created_at` | `Export`, `Upload`, `List_` |
|
|
299
|
+
| `finished_ts` | `finished_at` | `Export`, `Upload` |
|
|
300
|
+
| `modified_ts` | `modified_at` | `List_` |
|
|
301
|
+
|
|
302
|
+
Both names work when constructing models manually (`Image(uri150="...")` and `Image(uri_150="...")` are equivalent). When accessing attributes, use the Python name: `image.uri_150`.
|
|
303
|
+
|
|
304
|
+
## Contributing
|
|
305
|
+
|
|
306
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
discogs-sdk is licensed under the [Apache License 2.0](LICENSE.txt).
|