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.
- addictune_sdk-0.1.0/.gitignore +10 -0
- addictune_sdk-0.1.0/LICENSE +21 -0
- addictune_sdk-0.1.0/PKG-INFO +523 -0
- addictune_sdk-0.1.0/README.md +497 -0
- addictune_sdk-0.1.0/addictune_sdk/__init__.py +116 -0
- addictune_sdk-0.1.0/addictune_sdk/api/__init__.py +15 -0
- addictune_sdk-0.1.0/addictune_sdk/api/_helpers.py +166 -0
- addictune_sdk-0.1.0/addictune_sdk/api/auth.py +53 -0
- addictune_sdk-0.1.0/addictune_sdk/api/channels.py +211 -0
- addictune_sdk-0.1.0/addictune_sdk/api/mixshows.py +142 -0
- addictune_sdk-0.1.0/addictune_sdk/api/playlists.py +170 -0
- addictune_sdk-0.1.0/addictune_sdk/api/tracks.py +202 -0
- addictune_sdk-0.1.0/addictune_sdk/api/user.py +51 -0
- addictune_sdk-0.1.0/addictune_sdk/cache.py +189 -0
- addictune_sdk-0.1.0/addictune_sdk/client.py +163 -0
- addictune_sdk-0.1.0/addictune_sdk/config.py +177 -0
- addictune_sdk-0.1.0/addictune_sdk/exceptions.py +39 -0
- addictune_sdk-0.1.0/addictune_sdk/headers.py +69 -0
- addictune_sdk-0.1.0/addictune_sdk/models/__init__.py +70 -0
- addictune_sdk-0.1.0/addictune_sdk/models/auth.py +35 -0
- addictune_sdk-0.1.0/addictune_sdk/models/channel.py +146 -0
- addictune_sdk-0.1.0/addictune_sdk/models/common.py +53 -0
- addictune_sdk-0.1.0/addictune_sdk/models/mixshow.py +103 -0
- addictune_sdk-0.1.0/addictune_sdk/models/network.py +61 -0
- addictune_sdk-0.1.0/addictune_sdk/models/playlist.py +123 -0
- addictune_sdk-0.1.0/addictune_sdk/models/track.py +231 -0
- addictune_sdk-0.1.0/addictune_sdk/models/user.py +95 -0
- addictune_sdk-0.1.0/addictune_sdk/network_client.py +42 -0
- addictune_sdk-0.1.0/addictune_sdk/py.typed +0 -0
- addictune_sdk-0.1.0/addictune_sdk/transport.py +144 -0
- addictune_sdk-0.1.0/pyproject.toml +57 -0
- addictune_sdk-0.1.0/tests/__init__.py +0 -0
- addictune_sdk-0.1.0/tests/conftest.py +166 -0
- addictune_sdk-0.1.0/tests/fixtures/__init__.py +0 -0
- addictune_sdk-0.1.0/tests/fixtures/auth.json +10 -0
- addictune_sdk-0.1.0/tests/fixtures/channel.json +18 -0
- addictune_sdk-0.1.0/tests/fixtures/channel_minimal.json +6 -0
- addictune_sdk-0.1.0/tests/fixtures/favorites.json +5 -0
- addictune_sdk-0.1.0/tests/fixtures/liked_track.json +26 -0
- addictune_sdk-0.1.0/tests/fixtures/liked_tracks.json +54 -0
- addictune_sdk-0.1.0/tests/fixtures/mixshow.json +27 -0
- addictune_sdk-0.1.0/tests/fixtures/mixshows_list.json +20 -0
- addictune_sdk-0.1.0/tests/fixtures/now_playing.json +24 -0
- addictune_sdk-0.1.0/tests/fixtures/payment_method.json +31 -0
- addictune_sdk-0.1.0/tests/fixtures/ping.json +7 -0
- addictune_sdk-0.1.0/tests/fixtures/playlist.json +22 -0
- addictune_sdk-0.1.0/tests/fixtures/playlist_content.json +36 -0
- addictune_sdk-0.1.0/tests/fixtures/playlist_listen_history.json +20 -0
- addictune_sdk-0.1.0/tests/fixtures/playlists_featured.json +43 -0
- addictune_sdk-0.1.0/tests/fixtures/playlists_followed.json +22 -0
- addictune_sdk-0.1.0/tests/fixtures/preferred_quality.json +8 -0
- addictune_sdk-0.1.0/tests/fixtures/premium_status.json +9 -0
- addictune_sdk-0.1.0/tests/fixtures/qualities.json +65 -0
- addictune_sdk-0.1.0/tests/fixtures/show_episodes.json +31 -0
- addictune_sdk-0.1.0/tests/fixtures/track.json +33 -0
- addictune_sdk-0.1.0/tests/fixtures/track_history.json +22 -0
- addictune_sdk-0.1.0/tests/fixtures/upcoming_episodes.json +26 -0
- addictune_sdk-0.1.0/tests/integration/__init__.py +4 -0
- addictune_sdk-0.1.0/tests/integration/channels.py +166 -0
- addictune_sdk-0.1.0/tests/integration/mixshows.py +141 -0
- addictune_sdk-0.1.0/tests/integration/playlists.py +200 -0
- addictune_sdk-0.1.0/tests/integration/session.py +68 -0
- addictune_sdk-0.1.0/tests/integration/tracks.py +173 -0
- addictune_sdk-0.1.0/tests/integration/user.py +95 -0
- addictune_sdk-0.1.0/tests/unit/__init__.py +0 -0
- addictune_sdk-0.1.0/tests/unit/test_auth.py +77 -0
- addictune_sdk-0.1.0/tests/unit/test_channels.py +559 -0
- addictune_sdk-0.1.0/tests/unit/test_client.py +186 -0
- addictune_sdk-0.1.0/tests/unit/test_exceptions.py +61 -0
- addictune_sdk-0.1.0/tests/unit/test_headers.py +48 -0
- addictune_sdk-0.1.0/tests/unit/test_helpers.py +178 -0
- addictune_sdk-0.1.0/tests/unit/test_mixshows.py +264 -0
- addictune_sdk-0.1.0/tests/unit/test_playlists.py +354 -0
- addictune_sdk-0.1.0/tests/unit/test_tracks.py +345 -0
- addictune_sdk-0.1.0/tests/unit/test_user.py +142 -0
- addictune_sdk-0.1.0/uv.lock +306 -0
|
@@ -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
|
+
[](https://pypi.org/project/addictune-sdk/)
|
|
36
|
+
[](https://pypi.org/project/addictune-sdk/)
|
|
37
|
+
[](https://github.com/ukw2d/addictune-sdk/blob/main/LICENSE)
|
|
38
|
+
[](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
|