addictune-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.
Files changed (76) hide show
  1. addictune_sdk-0.1.0/.gitignore +10 -0
  2. addictune_sdk-0.1.0/LICENSE +21 -0
  3. addictune_sdk-0.1.0/PKG-INFO +523 -0
  4. addictune_sdk-0.1.0/README.md +497 -0
  5. addictune_sdk-0.1.0/addictune_sdk/__init__.py +116 -0
  6. addictune_sdk-0.1.0/addictune_sdk/api/__init__.py +15 -0
  7. addictune_sdk-0.1.0/addictune_sdk/api/_helpers.py +166 -0
  8. addictune_sdk-0.1.0/addictune_sdk/api/auth.py +53 -0
  9. addictune_sdk-0.1.0/addictune_sdk/api/channels.py +211 -0
  10. addictune_sdk-0.1.0/addictune_sdk/api/mixshows.py +142 -0
  11. addictune_sdk-0.1.0/addictune_sdk/api/playlists.py +170 -0
  12. addictune_sdk-0.1.0/addictune_sdk/api/tracks.py +202 -0
  13. addictune_sdk-0.1.0/addictune_sdk/api/user.py +51 -0
  14. addictune_sdk-0.1.0/addictune_sdk/cache.py +189 -0
  15. addictune_sdk-0.1.0/addictune_sdk/client.py +163 -0
  16. addictune_sdk-0.1.0/addictune_sdk/config.py +177 -0
  17. addictune_sdk-0.1.0/addictune_sdk/exceptions.py +39 -0
  18. addictune_sdk-0.1.0/addictune_sdk/headers.py +69 -0
  19. addictune_sdk-0.1.0/addictune_sdk/models/__init__.py +70 -0
  20. addictune_sdk-0.1.0/addictune_sdk/models/auth.py +35 -0
  21. addictune_sdk-0.1.0/addictune_sdk/models/channel.py +146 -0
  22. addictune_sdk-0.1.0/addictune_sdk/models/common.py +53 -0
  23. addictune_sdk-0.1.0/addictune_sdk/models/mixshow.py +103 -0
  24. addictune_sdk-0.1.0/addictune_sdk/models/network.py +61 -0
  25. addictune_sdk-0.1.0/addictune_sdk/models/playlist.py +123 -0
  26. addictune_sdk-0.1.0/addictune_sdk/models/track.py +231 -0
  27. addictune_sdk-0.1.0/addictune_sdk/models/user.py +95 -0
  28. addictune_sdk-0.1.0/addictune_sdk/network_client.py +42 -0
  29. addictune_sdk-0.1.0/addictune_sdk/py.typed +0 -0
  30. addictune_sdk-0.1.0/addictune_sdk/transport.py +144 -0
  31. addictune_sdk-0.1.0/pyproject.toml +57 -0
  32. addictune_sdk-0.1.0/tests/__init__.py +0 -0
  33. addictune_sdk-0.1.0/tests/conftest.py +166 -0
  34. addictune_sdk-0.1.0/tests/fixtures/__init__.py +0 -0
  35. addictune_sdk-0.1.0/tests/fixtures/auth.json +10 -0
  36. addictune_sdk-0.1.0/tests/fixtures/channel.json +18 -0
  37. addictune_sdk-0.1.0/tests/fixtures/channel_minimal.json +6 -0
  38. addictune_sdk-0.1.0/tests/fixtures/favorites.json +5 -0
  39. addictune_sdk-0.1.0/tests/fixtures/liked_track.json +26 -0
  40. addictune_sdk-0.1.0/tests/fixtures/liked_tracks.json +54 -0
  41. addictune_sdk-0.1.0/tests/fixtures/mixshow.json +27 -0
  42. addictune_sdk-0.1.0/tests/fixtures/mixshows_list.json +20 -0
  43. addictune_sdk-0.1.0/tests/fixtures/now_playing.json +24 -0
  44. addictune_sdk-0.1.0/tests/fixtures/payment_method.json +31 -0
  45. addictune_sdk-0.1.0/tests/fixtures/ping.json +7 -0
  46. addictune_sdk-0.1.0/tests/fixtures/playlist.json +22 -0
  47. addictune_sdk-0.1.0/tests/fixtures/playlist_content.json +36 -0
  48. addictune_sdk-0.1.0/tests/fixtures/playlist_listen_history.json +20 -0
  49. addictune_sdk-0.1.0/tests/fixtures/playlists_featured.json +43 -0
  50. addictune_sdk-0.1.0/tests/fixtures/playlists_followed.json +22 -0
  51. addictune_sdk-0.1.0/tests/fixtures/preferred_quality.json +8 -0
  52. addictune_sdk-0.1.0/tests/fixtures/premium_status.json +9 -0
  53. addictune_sdk-0.1.0/tests/fixtures/qualities.json +65 -0
  54. addictune_sdk-0.1.0/tests/fixtures/show_episodes.json +31 -0
  55. addictune_sdk-0.1.0/tests/fixtures/track.json +33 -0
  56. addictune_sdk-0.1.0/tests/fixtures/track_history.json +22 -0
  57. addictune_sdk-0.1.0/tests/fixtures/upcoming_episodes.json +26 -0
  58. addictune_sdk-0.1.0/tests/integration/__init__.py +4 -0
  59. addictune_sdk-0.1.0/tests/integration/channels.py +166 -0
  60. addictune_sdk-0.1.0/tests/integration/mixshows.py +141 -0
  61. addictune_sdk-0.1.0/tests/integration/playlists.py +200 -0
  62. addictune_sdk-0.1.0/tests/integration/session.py +68 -0
  63. addictune_sdk-0.1.0/tests/integration/tracks.py +173 -0
  64. addictune_sdk-0.1.0/tests/integration/user.py +95 -0
  65. addictune_sdk-0.1.0/tests/unit/__init__.py +0 -0
  66. addictune_sdk-0.1.0/tests/unit/test_auth.py +77 -0
  67. addictune_sdk-0.1.0/tests/unit/test_channels.py +559 -0
  68. addictune_sdk-0.1.0/tests/unit/test_client.py +186 -0
  69. addictune_sdk-0.1.0/tests/unit/test_exceptions.py +61 -0
  70. addictune_sdk-0.1.0/tests/unit/test_headers.py +48 -0
  71. addictune_sdk-0.1.0/tests/unit/test_helpers.py +178 -0
  72. addictune_sdk-0.1.0/tests/unit/test_mixshows.py +264 -0
  73. addictune_sdk-0.1.0/tests/unit/test_playlists.py +354 -0
  74. addictune_sdk-0.1.0/tests/unit/test_tracks.py +345 -0
  75. addictune_sdk-0.1.0/tests/unit/test_user.py +142 -0
  76. addictune_sdk-0.1.0/uv.lock +306 -0
@@ -0,0 +1,10 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ .DS_Store
7
+ dist/
8
+ *.egg-info/
9
+ .uv/
10
+ tests/integration/.session_cache.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ukw2d
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,523 @@
1
+ Metadata-Version: 2.4
2
+ Name: addictune-sdk
3
+ Version: 0.1.0
4
+ Summary: Async Python SDK for the AudioAddict radio platform (DI.FM, RadioTunes, RockRadio, JazzRadio, ClassicalRadio, ZenRadio)
5
+ Project-URL: Repository, https://github.com/ukw2d/addictune-sdk
6
+ Author: ukw2d
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: async,audioaddict,classicalradio,di.fm,digitally imported,jazzradio,radio,radiotunes,rockradio,sdk,streaming,zenradio
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Multimedia :: Sound/Audio
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: httpx>=0.28
24
+ Requires-Dist: pydantic>=2.11
25
+ Description-Content-Type: text/markdown
26
+
27
+ <div align="center">
28
+
29
+ # 📻 addictune-sdk
30
+
31
+ **Async Python SDK for the AudioAddict radio platform**
32
+
33
+ DI.FM · RadioTunes · RockRadio · JazzRadio · ClassicalRadio · ZenRadio
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/addictune-sdk?label=PyPI&color=blue)](https://pypi.org/project/addictune-sdk/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/addictune-sdk?label=Python&logo=python&logoColor=white)](https://pypi.org/project/addictune-sdk/)
37
+ [![License](https://img.shields.io/pypi/l/addictune-sdk?label=License&color=green)](https://github.com/ukw2d/addictune-sdk/blob/main/LICENSE)
38
+ [![CI](https://img.shields.io/github/actions/workflow/status/ukw2d/addictune-sdk/ci.yml?label=CI&logo=github)](https://github.com/ukw2d/addictune-sdk/actions)
39
+
40
+ </div>
41
+
42
+ ---
43
+
44
+ ## Features
45
+
46
+ - **Fully async** — built on `httpx` with `async/await` throughout
47
+ - **6 networks** — DI.FM, RadioTunes, RockRadio, JazzRadio, ClassicalRadio, ZenRadio out of the box
48
+ - **Typed models** — Pydantic v2 models for every API response, with IDE autocomplete and validation
49
+ - **ETag caching** — automatic HTTP `If-None-Match` / `304` handling backed by SQLite
50
+ - **Auto-pagination** — `async for` iterators that transparently walk pages
51
+ - **Resilient transport** — retry with exponential backoff + jitter, circuit breaker
52
+ - **Auth helpers** — session and direct login, `SecretStr`-guarded internal storage
53
+ - **Minimal dependencies** — only `httpx` and `pydantic`
54
+ - **Zero-config** — sensible defaults, override via constructor, JSON file, or auto-discovery
55
+
56
+ ### API coverage
57
+
58
+ | Domain | What you can do |
59
+ |--------------|----------------------------------------------------------------------------------|
60
+ | **Auth** | Login (session or direct), retrieve API key + listen key |
61
+ | **Channels** | Browse all channels, get by ID, track history, now playing, stream URLs, favorites |
62
+ | **Tracks** | Get by ID, liked tracks, vote up/down/delete, skip events, audio quality prefs |
63
+ | **Playlists**| Featured playlists, browse by popularity/newest, get tracks, follow, listen history |
64
+ | **Mix Shows**| Browse shows, iterate episodes, upcoming events, followed shows |
65
+ | **User** | Ping API, check premium status, payment methods |
66
+
67
+ ---
68
+
69
+ ## Installation
70
+
71
+ === "pip"
72
+
73
+ ```bash
74
+ pip install addictune-sdk
75
+ ```
76
+
77
+ === "uv"
78
+
79
+ ```bash
80
+ uv add addictune-sdk
81
+ ```
82
+
83
+ === "poetry"
84
+
85
+ ```bash
86
+ poetry add addictune-sdk
87
+ ```
88
+
89
+ === "pipx" *(for scripts)*
90
+
91
+ ```bash
92
+ pipx inject my-tool addictune-sdk
93
+ ```
94
+
95
+ Requires **Python 3.12+**.
96
+
97
+ ---
98
+
99
+ ## Quick start
100
+
101
+ ```python
102
+ import asyncio
103
+ from addictune_sdk import Client
104
+
105
+ async def main():
106
+ async with Client() as client:
107
+ di = client.network("di")
108
+ channels = await di.channels.get_all()
109
+ for ch in channels:
110
+ print(ch.name)
111
+
112
+ asyncio.run(main())
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Authentication
118
+
119
+ ```python
120
+ from addictune_sdk import Client
121
+
122
+ async with Client() as client:
123
+ auth = await client.login("you@example.com", "your-password")
124
+ print(f"Logged in as user {auth.user_id}")
125
+ ```
126
+
127
+ Or pass a pre-existing session key:
128
+
129
+ ```python
130
+ async with Client(session_key="your-session-key") as client:
131
+ ...
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Network-scoped APIs
137
+
138
+ Every network is accessed via `client.network(slug)` and exposes namespaced APIs:
139
+
140
+ ```python
141
+ di = client.network("di")
142
+ ```
143
+
144
+ ### Channels
145
+
146
+ ```python
147
+ # List all channels
148
+ channels = await di.channels.get_all()
149
+
150
+ # Single channel by ID
151
+ channel = await di.channels.get_by_id(123)
152
+
153
+ # What's playing right now across all channels
154
+ now = await di.channels.get_currently_playing()
155
+
156
+ # Build a direct stream URL
157
+ url = di.channels.get_stream_url("trance", "your-listen-key", quality="hi")
158
+
159
+ # Favorites
160
+ await di.channels.add_favorite(user_id, channel_id)
161
+ favs = await di.channels.get_favorites(user_id)
162
+ ```
163
+
164
+ ### Tracks
165
+
166
+ ```python
167
+ # Fetch a track
168
+ track = await di.tracks.get_by_id(12345)
169
+
170
+ # Like / unlike
171
+ await di.tracks.vote(12345, direction="up")
172
+ await di.tracks.vote(12345, direction="delete")
173
+
174
+ # Iterate all liked tracks (auto-paginated)
175
+ async for track in di.tracks.iter_liked_tracks(user_id):
176
+ print(track.title)
177
+
178
+ # Audio quality
179
+ qualities = await di.tracks.get_qualities()
180
+ await di.tracks.set_preferred_quality(user_id, quality_id=3)
181
+ ```
182
+
183
+ ### Playlists
184
+
185
+ ```python
186
+ # Featured playlists
187
+ featured = await di.playlists.get_featured()
188
+
189
+ # Browse with auto-pagination
190
+ async for pl in di.playlists.iter_playlists(order_by="newest"):
191
+ print(pl.name)
192
+
193
+ # Get playable tracks for a playlist
194
+ content = await di.playlists.get_content(playlist_id)
195
+
196
+ # Followed playlists
197
+ async for pl in di.playlists.iter_followed(user_id):
198
+ print(pl.name)
199
+ ```
200
+
201
+ ### Mix Shows
202
+
203
+ ```python
204
+ # Browse shows (auto-paginated)
205
+ async for show in di.mixshows.iter_shows(active=True):
206
+ print(show.name)
207
+
208
+ # Episodes for a specific show
209
+ async for ep in di.mixshows.iter_episodes(show_id):
210
+ print(ep.name)
211
+
212
+ # Upcoming events
213
+ upcoming = await di.mixshows.get_upcoming(limit=10)
214
+ ```
215
+
216
+ ### User
217
+
218
+ ```python
219
+ # Health check
220
+ ping = await client.user.ping()
221
+ print(f"API v{ping.api_version} — {ping.country}")
222
+
223
+ # Premium status for a network
224
+ status = await client.user.check_premium_status("di")
225
+ print(status.listener_type, status.skips_remaining)
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Built-in networks
231
+
232
+ | Slug | Name |
233
+ |-------------------|-----------------|
234
+ | `di` | DI.FM |
235
+ | `radiotunes` | RadioTunes |
236
+ | `rockradio` | RockRadio |
237
+ | `jazzradio` | JazzRadio |
238
+ | `classicalradio` | ClassicalRadio |
239
+ | `zenradio` | ZenRadio |
240
+
241
+ Add custom networks via the `custom_networks` parameter on `Client`.
242
+
243
+ ---
244
+
245
+ ## Configuration
246
+
247
+ The SDK uses a frozen dataclass (`AddictuneConfig`) with sensible defaults. Configuration is explicit and controlled entirely by the host application.
248
+
249
+ `AddictuneConfig` is a plain Python `frozen=True` dataclass — every field has a default, so it works out of the box with zero setup. Override only what you need, using whichever approach fits your application.
250
+
251
+ There are four ways to configure the SDK, in order of precedence:
252
+
253
+ | Approach | When to use |
254
+ |----------|-------------|
255
+ | **No config** | Scripts, prototypes — defaults are production-ready |
256
+ | **Programmatic** | Desktop apps with their own settings layer (QSettings, NSUserDefaults, etc.) |
257
+ | **JSON file** | File-based settings, shared configs, deployment overrides |
258
+ | **Auto-discovery** | Let the SDK find a config file in standard OS locations automatically |
259
+
260
+ ### Defaults only
261
+
262
+ No config object needed — every field ships with a sensible default:
263
+
264
+ ```python
265
+ from addictune_sdk import Client
266
+
267
+ async with Client() as client:
268
+ # Uses AddictuneConfig() under the hood:
269
+ # api_base = "https://api.audioaddict.com/v1"
270
+ # network = "di"
271
+ # timeout = 30.0
272
+ # retry = RetryConfig() (3 attempts, exponential backoff)
273
+ # circuit = CircuitConfig() (5 failures → open, 60s recovery)
274
+ di = client.network("di")
275
+ channels = await di.channels.get_all()
276
+ ```
277
+
278
+ ### Programmatic override
279
+
280
+ #### Override top-level fields
281
+
282
+ Pass an `AddictuneConfig` to the `Client` constructor with just the fields you want to change:
283
+
284
+ ```python
285
+ from addictune_sdk import Client, AddictuneConfig
286
+
287
+ config = AddictuneConfig(
288
+ network="radiotunes", # default to RadioTunes instead of DI.FM
289
+ timeout=15.0, # shorter timeout for latency-sensitive apps
290
+ )
291
+
292
+ async with Client(config=config) as client:
293
+ # client.login() will authenticate against the "radiotunes" network
294
+ auth = await client.login("you@example.com", "password")
295
+ ```
296
+
297
+ #### Override retry and circuit-breaker settings
298
+
299
+ `AddictuneConfig` has two nested dataclasses — `RetryConfig` and `CircuitConfig` — that control resilient transport behaviour:
300
+
301
+ ```python
302
+ from addictune_sdk import AddictuneConfig, RetryConfig, CircuitConfig
303
+
304
+ config = AddictuneConfig(
305
+ retry=RetryConfig(
306
+ max_attempts=5, # retry up to 5 times before giving up
307
+ wait_min=1.0, # wait at least 1s between retries
308
+ wait_max=30.0, # cap backoff at 30s
309
+ wait_jitter=2.0, # add up to 2s random jitter
310
+ ),
311
+ circuit=CircuitConfig(
312
+ failure_threshold=10, # tolerate more failures before tripping
313
+ recovery_timeout=30.0, # recover faster (30s instead of 60s)
314
+ ),
315
+ )
316
+ ```
317
+
318
+ **How retry works:** on each failed attempt the delay is `wait_multiplier × 2^(attempt-1)`, clamped to `[wait_min, wait_max]`, then a random jitter in `[0, wait_jitter]` is added. With defaults (multiplier `1.0`, min `2.0`, max `10.0`) the delays are approximately 2s → 4s → 8s plus jitter.
319
+
320
+ **How the circuit breaker works:** consecutive failures are tracked. Once they reach `failure_threshold`, the circuit opens and all requests are immediately rejected. After `recovery_timeout` seconds the circuit closes and new requests are allowed through.
321
+
322
+ #### Use `dataclasses.replace` for small tweaks
323
+
324
+ If you only need to change one or two fields, use `dataclasses.replace` on the default instance:
325
+
326
+ ```python
327
+ from dataclasses import replace
328
+ from addictune_sdk import AddictuneConfig
329
+
330
+ config = replace(AddictuneConfig(), timeout=10.0, network="jazzradio")
331
+ ```
332
+
333
+ This is equivalent to `AddictuneConfig(timeout=10.0, network="jazzradio")` but reads more naturally when you're overriding a value you already have.
334
+
335
+ ### JSON config file
336
+
337
+ Load config from a JSON file when your application prefers file-based settings:
338
+
339
+ ```python
340
+ from addictune_sdk import Client, AddictuneConfig
341
+
342
+ config = AddictuneConfig.from_json("~/.config/myapp/addictune.json")
343
+ async with Client(config=config) as client:
344
+ ...
345
+ ```
346
+
347
+ All fields are optional — missing keys fall back to their defaults, so your JSON only needs the overrides:
348
+
349
+ ```json
350
+ {
351
+ "network": "di",
352
+ "timeout": 15.0
353
+ }
354
+ ```
355
+
356
+ Full example with every field:
357
+
358
+ ```json
359
+ {
360
+ "api_base": "https://api.audioaddict.com/v1",
361
+ "network": "di",
362
+ "timeout": 30.0,
363
+ "retry": {
364
+ "max_attempts": 3,
365
+ "wait_multiplier": 1.0,
366
+ "wait_min": 2.0,
367
+ "wait_max": 10.0,
368
+ "wait_jitter": 1.0
369
+ },
370
+ "circuit": {
371
+ "failure_threshold": 5,
372
+ "recovery_timeout": 60.0
373
+ }
374
+ }
375
+ ```
376
+
377
+ #### Write a config file from code
378
+
379
+ Persist settings for later use:
380
+
381
+ ```python
382
+ from addictune_sdk import AddictuneConfig
383
+
384
+ config = AddictuneConfig(timeout=15.0, network="rockradio")
385
+ config.to_json("path/to/config.json")
386
+ ```
387
+
388
+ `to_json` creates parent directories automatically if they don't exist.
389
+
390
+ #### Round-trip: read → modify → write
391
+
392
+ ```python
393
+ from addictune_sdk import AddictuneConfig
394
+
395
+ # Load existing config
396
+ config = AddictuneConfig.from_json("config.json")
397
+
398
+ # Modify with dataclasses.replace
399
+ from dataclasses import replace
400
+ config = replace(config, timeout=20.0)
401
+
402
+ # Save back
403
+ config.to_json("config.json")
404
+ ```
405
+
406
+ ### Auto-discovery
407
+
408
+ `load_config()` searches standard OS config locations in order and returns the first file it finds. If nothing exists, it returns a default `AddictuneConfig()` — so your code never needs to handle "no config found" as a special case.
409
+
410
+ | Platform | Search paths (in order) |
411
+ |----------|-------------------------|
412
+ | Linux / macOS | `$XDG_CONFIG_HOME/addictune/config.json`, `~/.addictune/config.json` |
413
+ | Windows | `%APPDATA%\addictune\config.json` |
414
+
415
+ ```python
416
+ from addictune_sdk import Client, load_config
417
+
418
+ # Searches standard paths; falls back to defaults if no file exists
419
+ config = load_config()
420
+
421
+ async with Client(config=config) as client:
422
+ ...
423
+ ```
424
+
425
+ Pass an explicit path to skip auto-discovery:
426
+
427
+ ```python
428
+ config = load_config("/etc/myapp/addictune.json")
429
+ ```
430
+
431
+ ### Configuration reference
432
+
433
+ #### `AddictuneConfig`
434
+
435
+ | Field | Type | Default | Description |
436
+ |-------|------|---------|-------------|
437
+ | `api_base` | `str` | `https://api.audioaddict.com/v1` | API base URL |
438
+ | `network` | `str` | `di` | Default network slug used by `Client.login()` |
439
+ | `timeout` | `float` | `30.0` | HTTP request timeout (seconds) |
440
+ | `retry` | `RetryConfig` | `RetryConfig()` | Retry behaviour (see below) |
441
+ | `circuit` | `CircuitConfig` | `CircuitConfig()` | Circuit-breaker behaviour (see below) |
442
+
443
+ #### `RetryConfig`
444
+
445
+ Controls automatic retry with exponential backoff + jitter.
446
+
447
+ | Field | Type | Default | Description |
448
+ |-------|------|---------|-------------|
449
+ | `max_attempts` | `int` | `3` | Max attempts per request (including initial). Set to `1` to disable retries. |
450
+ | `wait_multiplier` | `float` | `1.0` | Exponential backoff multiplier |
451
+ | `wait_min` | `float` | `2.0` | Minimum delay between retries (seconds) |
452
+ | `wait_max` | `float` | `10.0` | Maximum delay between retries (seconds) |
453
+ | `wait_jitter` | `float` | `1.0` | Upper bound of random jitter added to each delay (seconds) |
454
+
455
+ #### `CircuitConfig`
456
+
457
+ Controls the circuit-breaker that protects against cascading failures.
458
+
459
+ | Field | Type | Default | Description |
460
+ |-------|------|---------|-------------|
461
+ | `failure_threshold` | `int` | `5` | Consecutive failures before the circuit opens |
462
+ | `recovery_timeout` | `float` | `60.0` | Seconds before a tripped circuit allows a retry |
463
+ | `name` | `str \| None` | `None` | Optional label for logging / metrics |
464
+
465
+ ---
466
+
467
+ ## Logging
468
+
469
+ The SDK uses Python's standard `logging` library under the `addictune_sdk` namespace. It does not configure handlers or formatters — that's the host application's responsibility. By default only `WARNING` and above is visible.
470
+
471
+ ### Quick setup
472
+
473
+ The simplest way to see SDK log output:
474
+
475
+ ```python
476
+ import logging
477
+
478
+ logging.basicConfig(level=logging.DEBUG)
479
+ ```
480
+
481
+ ### Target just the SDK
482
+
483
+ To control SDK logging independently of the rest of your application:
484
+
485
+ ```python
486
+ import logging
487
+
488
+ logging.getLogger("addictune_sdk").setLevel(logging.DEBUG)
489
+ ```
490
+
491
+ Or use a dedicated handler with a custom format:
492
+
493
+ ```python
494
+ import logging
495
+
496
+ handler = logging.StreamHandler()
497
+ handler.setFormatter(
498
+ logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
499
+ )
500
+ sdk_logger = logging.getLogger("addictune_sdk")
501
+ sdk_logger.setLevel(logging.DEBUG)
502
+ sdk_logger.addHandler(handler)
503
+ ```
504
+
505
+ ### Log levels by component
506
+
507
+ | Component | `DEBUG` | `INFO` | `WARNING` | `ERROR` |
508
+ |-----------|---------|--------|-----------|---------|
509
+ | **Transport** (retry / circuit breaker) | Each retry attempt with wait time | Retry succeeded; circuit recovered | Circuit tripped open; request rejected by circuit | All attempts exhausted |
510
+ | **Cache** (ETag / SQLite) | Cache hit, miss, expired, stored, indexed | — | — | — |
511
+ | **Client** | Init, connection close | Successful login | — | — |
512
+
513
+ **Recommended levels:**
514
+
515
+ - **Production:** `WARNING` (default) — only circuit-breaker trips and exhausted retries
516
+ - **Development:** `INFO` — adds login events and retry recoveries
517
+ - **Debugging:** `DEBUG` — full visibility into cache behaviour and every retry attempt
518
+
519
+ ---
520
+
521
+ ## License
522
+
523
+ [MIT](LICENSE) © ukw2d