spotifyify 0.2.0__tar.gz → 0.4.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.
- {spotifyify-0.2.0/spotifyify.egg-info → spotifyify-0.4.0}/PKG-INFO +38 -5
- spotifyify-0.2.0/PKG-INFO → spotifyify-0.4.0/README.md +37 -14
- {spotifyify-0.2.0 → spotifyify-0.4.0}/pyproject.toml +1 -1
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/__init__.py +130 -123
- spotifyify-0.4.0/spotifyify/auth.py +9 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/cache_handler.py +63 -42
- spotifyify-0.4.0/spotifyify/client.py +189 -0
- spotifyify-0.4.0/spotifyify/http/__init__.py +24 -0
- spotifyify-0.4.0/spotifyify/http/response.py +57 -0
- spotifyify-0.4.0/spotifyify/http/retry_context.py +8 -0
- spotifyify-0.4.0/spotifyify/http/retry_event.py +25 -0
- spotifyify-0.4.0/spotifyify/http/retry_policy.py +54 -0
- spotifyify-0.4.0/spotifyify/http/serialization.py +26 -0
- spotifyify-0.4.0/spotifyify/http/transport.py +122 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/playlists.py +31 -6
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/oauth2/oauth2.py +359 -318
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/spotifyify.py +141 -126
- spotifyify-0.2.0/README.md → spotifyify-0.4.0/spotifyify.egg-info/PKG-INFO +47 -4
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify.egg-info/SOURCES.txt +8 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/tests/test_cache_handler.py +3 -1
- spotifyify-0.4.0/tests/test_client.py +90 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/tests/test_oauth2.py +36 -0
- spotifyify-0.2.0/spotifyify/client.py +0 -279
- spotifyify-0.2.0/tests/test_client.py +0 -131
- {spotifyify-0.2.0 → spotifyify-0.4.0}/setup.cfg +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/credentials.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/exceptions.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/__init__.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/albums.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/artists.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/episodes.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/library.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/player.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/shows.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/tracks.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/namespaces/users.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/oauth2/__init__.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/oauth2/views.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/schemas.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify/utils.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify.egg-info/dependency_links.txt +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify.egg-info/requires.txt +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/spotifyify.egg-info/top_level.txt +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/tests/test_credentials.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/tests/test_exceptions.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/tests/test_scopes.py +0 -0
- {spotifyify-0.2.0 → spotifyify-0.4.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spotifyify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Async wrapper for spotipy with a focus on integration with MCP agents.
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -10,7 +10,7 @@ Requires-Dist: pydantic-settings>=2.12.0
|
|
|
10
10
|
|
|
11
11
|
# spotifyify
|
|
12
12
|
|
|
13
|
-
An async-first Spotify client with a namespaced API and fully typed response models
|
|
13
|
+
An async-first Spotify client with a namespaced API and fully typed response models maintained in the codebase and aligned with the [official Spotify OpenAPI specification](https://developer.spotify.com/reference/web-api/open-api-schema.yaml).
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
@@ -130,11 +130,12 @@ Spotifyify
|
|
|
130
130
|
| `find(query, *, limit, offset)` | Search for playlists |
|
|
131
131
|
| `get(playlist_id, *, market)` | Get a single playlist |
|
|
132
132
|
| `list(*, user_id, limit, offset)` | Current user's (or another user's) playlists |
|
|
133
|
+
| `tracks(playlist_id, *, market, fields, limit, offset, additional_types)` | Get playlist tracks |
|
|
133
134
|
| `create(name, *, public, collaborative, description, user_id)` | Create a playlist |
|
|
134
135
|
| `update(playlist_id, *, name, public, collaborative, description)` | Update playlist details |
|
|
135
|
-
| `add(playlist_id, uris, *, position)` | Add
|
|
136
|
-
| `remove(playlist_id, uris)` | Remove
|
|
137
|
-
| `reorder(playlist_id, *, range_start, insert_before, range_length, snapshot_id)` | Reorder
|
|
136
|
+
| `add(playlist_id, uris, *, position)` | Add items to a playlist |
|
|
137
|
+
| `remove(playlist_id, uris)` | Remove items from a playlist |
|
|
138
|
+
| `reorder(playlist_id, *, range_start, insert_before, range_length, snapshot_id)` | Reorder items |
|
|
138
139
|
| `cover_image(playlist_id)` | Get playlist cover images |
|
|
139
140
|
|
|
140
141
|
### Player — `sp.player`
|
|
@@ -209,6 +210,38 @@ Spotifyify
|
|
|
209
210
|
| `unfollow(type, ids)` | Unfollow artists or users |
|
|
210
211
|
| `check_following(type, ids)` | Check if following artists or users |
|
|
211
212
|
|
|
213
|
+
## Retries
|
|
214
|
+
|
|
215
|
+
Spotify API requests automatically retry rate limits (`429`) and temporary server
|
|
216
|
+
errors (`500`, `502`, `503`, `504`). Rate limits honor Spotify's `Retry-After`
|
|
217
|
+
header. Server errors are only retried for idempotent HTTP methods to avoid
|
|
218
|
+
duplicating mutations. Configure the defaults with `max_retries` and
|
|
219
|
+
`retry_backoff_seconds` when constructing `Spotifyify`.
|
|
220
|
+
|
|
221
|
+
Use a request-context retry hook when retries should be reported to a caller.
|
|
222
|
+
The hook is isolated per async task, so one shared `Spotifyify` instance can be
|
|
223
|
+
used by concurrent conversations. `retry_number` is one-based and `retry_at`
|
|
224
|
+
contains the planned retry time in UTC:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from spotifyify import RetryEvent
|
|
228
|
+
|
|
229
|
+
async def on_retry(event: RetryEvent) -> None:
|
|
230
|
+
await sse_bus.emit(
|
|
231
|
+
conversation_id,
|
|
232
|
+
{
|
|
233
|
+
"status_code": event.status_code,
|
|
234
|
+
"retry_number": event.retry_number,
|
|
235
|
+
"max_retries": event.max_retries,
|
|
236
|
+
"retry_in_seconds": event.retry_in_seconds,
|
|
237
|
+
"retry_at": event.retry_at.isoformat(),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
with spotify.retry_hook(on_retry):
|
|
242
|
+
track = await spotify.tracks.get(track_id)
|
|
243
|
+
```
|
|
244
|
+
|
|
212
245
|
## Scopes
|
|
213
246
|
|
|
214
247
|
Use `SpotifyScope` to declare the OAuth scopes your app requires:
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: spotifyify
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Async wrapper for spotipy with a focus on integration with MCP agents.
|
|
5
|
-
Requires-Python: >=3.13
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: httpx>=0.28.0
|
|
8
|
-
Requires-Dist: pydantic>=2.12.0
|
|
9
|
-
Requires-Dist: pydantic-settings>=2.12.0
|
|
10
|
-
|
|
11
1
|
# spotifyify
|
|
12
2
|
|
|
13
|
-
An async-first Spotify client with a namespaced API and fully typed response models
|
|
3
|
+
An async-first Spotify client with a namespaced API and fully typed response models maintained in the codebase and aligned with the [official Spotify OpenAPI specification](https://developer.spotify.com/reference/web-api/open-api-schema.yaml).
|
|
14
4
|
|
|
15
5
|
## Requirements
|
|
16
6
|
|
|
@@ -130,11 +120,12 @@ Spotifyify
|
|
|
130
120
|
| `find(query, *, limit, offset)` | Search for playlists |
|
|
131
121
|
| `get(playlist_id, *, market)` | Get a single playlist |
|
|
132
122
|
| `list(*, user_id, limit, offset)` | Current user's (or another user's) playlists |
|
|
123
|
+
| `tracks(playlist_id, *, market, fields, limit, offset, additional_types)` | Get playlist tracks |
|
|
133
124
|
| `create(name, *, public, collaborative, description, user_id)` | Create a playlist |
|
|
134
125
|
| `update(playlist_id, *, name, public, collaborative, description)` | Update playlist details |
|
|
135
|
-
| `add(playlist_id, uris, *, position)` | Add
|
|
136
|
-
| `remove(playlist_id, uris)` | Remove
|
|
137
|
-
| `reorder(playlist_id, *, range_start, insert_before, range_length, snapshot_id)` | Reorder
|
|
126
|
+
| `add(playlist_id, uris, *, position)` | Add items to a playlist |
|
|
127
|
+
| `remove(playlist_id, uris)` | Remove items from a playlist |
|
|
128
|
+
| `reorder(playlist_id, *, range_start, insert_before, range_length, snapshot_id)` | Reorder items |
|
|
138
129
|
| `cover_image(playlist_id)` | Get playlist cover images |
|
|
139
130
|
|
|
140
131
|
### Player — `sp.player`
|
|
@@ -209,6 +200,38 @@ Spotifyify
|
|
|
209
200
|
| `unfollow(type, ids)` | Unfollow artists or users |
|
|
210
201
|
| `check_following(type, ids)` | Check if following artists or users |
|
|
211
202
|
|
|
203
|
+
## Retries
|
|
204
|
+
|
|
205
|
+
Spotify API requests automatically retry rate limits (`429`) and temporary server
|
|
206
|
+
errors (`500`, `502`, `503`, `504`). Rate limits honor Spotify's `Retry-After`
|
|
207
|
+
header. Server errors are only retried for idempotent HTTP methods to avoid
|
|
208
|
+
duplicating mutations. Configure the defaults with `max_retries` and
|
|
209
|
+
`retry_backoff_seconds` when constructing `Spotifyify`.
|
|
210
|
+
|
|
211
|
+
Use a request-context retry hook when retries should be reported to a caller.
|
|
212
|
+
The hook is isolated per async task, so one shared `Spotifyify` instance can be
|
|
213
|
+
used by concurrent conversations. `retry_number` is one-based and `retry_at`
|
|
214
|
+
contains the planned retry time in UTC:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from spotifyify import RetryEvent
|
|
218
|
+
|
|
219
|
+
async def on_retry(event: RetryEvent) -> None:
|
|
220
|
+
await sse_bus.emit(
|
|
221
|
+
conversation_id,
|
|
222
|
+
{
|
|
223
|
+
"status_code": event.status_code,
|
|
224
|
+
"retry_number": event.retry_number,
|
|
225
|
+
"max_retries": event.max_retries,
|
|
226
|
+
"retry_in_seconds": event.retry_in_seconds,
|
|
227
|
+
"retry_at": event.retry_at.isoformat(),
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
with spotify.retry_hook(on_retry):
|
|
232
|
+
track = await spotify.tracks.get(track_id)
|
|
233
|
+
```
|
|
234
|
+
|
|
212
235
|
## Scopes
|
|
213
236
|
|
|
214
237
|
Use `SpotifyScope` to declare the OAuth scopes your app requires:
|
|
@@ -1,123 +1,130 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from .
|
|
4
|
-
from .
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
#
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .spotifyify import Spotifyify
|
|
4
|
+
from .credentials import SpotifyCredentials
|
|
5
|
+
from .oauth2 import SpotifyScope
|
|
6
|
+
from .http import OnRetryHook, RetryEvent
|
|
7
|
+
from .schemas import (
|
|
8
|
+
# Core models
|
|
9
|
+
Track,
|
|
10
|
+
Album,
|
|
11
|
+
Artist,
|
|
12
|
+
Playlist,
|
|
13
|
+
Episode,
|
|
14
|
+
Show,
|
|
15
|
+
Device,
|
|
16
|
+
PlaybackState,
|
|
17
|
+
PlayerQueue,
|
|
18
|
+
AudioFeatures,
|
|
19
|
+
AudioAnalysis,
|
|
20
|
+
User,
|
|
21
|
+
PublicUser,
|
|
22
|
+
Image,
|
|
23
|
+
ExternalUrl,
|
|
24
|
+
Followers,
|
|
25
|
+
Context,
|
|
26
|
+
Category,
|
|
27
|
+
PlaylistTrack,
|
|
28
|
+
# Simplified models
|
|
29
|
+
SimplifiedTrack,
|
|
30
|
+
SimplifiedAlbum,
|
|
31
|
+
SimplifiedArtist,
|
|
32
|
+
SimplifiedPlaylist,
|
|
33
|
+
SimplifiedEpisode,
|
|
34
|
+
SimplifiedShow,
|
|
35
|
+
ArtistDiscographyAlbum,
|
|
36
|
+
# Saved-item wrappers
|
|
37
|
+
SavedTrack,
|
|
38
|
+
SavedAlbum,
|
|
39
|
+
SavedShow,
|
|
40
|
+
SavedEpisode,
|
|
41
|
+
# Compound models
|
|
42
|
+
PlayHistory,
|
|
43
|
+
Recommendations,
|
|
44
|
+
# Paging bases
|
|
45
|
+
Paging,
|
|
46
|
+
CursorPaging,
|
|
47
|
+
# Typed paging
|
|
48
|
+
PagingTrack,
|
|
49
|
+
PagingArtist,
|
|
50
|
+
PagingPlaylist,
|
|
51
|
+
PagingPlaylistTrack,
|
|
52
|
+
PagingSimplifiedTrack,
|
|
53
|
+
PagingSimplifiedAlbum,
|
|
54
|
+
PagingSimplifiedEpisode,
|
|
55
|
+
PagingSimplifiedShow,
|
|
56
|
+
PagingArtistDiscographyAlbum,
|
|
57
|
+
PagingSavedTrack,
|
|
58
|
+
PagingSavedAlbum,
|
|
59
|
+
PagingSavedShow,
|
|
60
|
+
PagingSavedEpisode,
|
|
61
|
+
CursorPagingPlayHistory,
|
|
62
|
+
CursorPagingSimplifiedArtist,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
# Client & config
|
|
68
|
+
"Spotifyify",
|
|
69
|
+
"SpotifyCredentials",
|
|
70
|
+
"SpotifyScope",
|
|
71
|
+
"OnRetryHook",
|
|
72
|
+
"RetryEvent",
|
|
73
|
+
# Core models
|
|
74
|
+
"Track",
|
|
75
|
+
"Album",
|
|
76
|
+
"Artist",
|
|
77
|
+
"Playlist",
|
|
78
|
+
"Episode",
|
|
79
|
+
"Show",
|
|
80
|
+
"Device",
|
|
81
|
+
"PlaybackState",
|
|
82
|
+
"PlayerQueue",
|
|
83
|
+
"AudioFeatures",
|
|
84
|
+
"AudioAnalysis",
|
|
85
|
+
"User",
|
|
86
|
+
"PublicUser",
|
|
87
|
+
"Image",
|
|
88
|
+
"ExternalUrl",
|
|
89
|
+
"Followers",
|
|
90
|
+
"Context",
|
|
91
|
+
"Category",
|
|
92
|
+
"PlaylistTrack",
|
|
93
|
+
# Simplified models
|
|
94
|
+
"SimplifiedTrack",
|
|
95
|
+
"SimplifiedAlbum",
|
|
96
|
+
"SimplifiedArtist",
|
|
97
|
+
"SimplifiedPlaylist",
|
|
98
|
+
"SimplifiedEpisode",
|
|
99
|
+
"SimplifiedShow",
|
|
100
|
+
"ArtistDiscographyAlbum",
|
|
101
|
+
# Saved-item wrappers
|
|
102
|
+
"SavedTrack",
|
|
103
|
+
"SavedAlbum",
|
|
104
|
+
"SavedShow",
|
|
105
|
+
"SavedEpisode",
|
|
106
|
+
# Compound models
|
|
107
|
+
"PlayHistory",
|
|
108
|
+
"Recommendations",
|
|
109
|
+
# Paging bases
|
|
110
|
+
"Paging",
|
|
111
|
+
"CursorPaging",
|
|
112
|
+
# Typed paging
|
|
113
|
+
"PagingTrack",
|
|
114
|
+
"PagingArtist",
|
|
115
|
+
"PagingPlaylist",
|
|
116
|
+
"PagingPlaylistTrack",
|
|
117
|
+
"PagingSimplifiedTrack",
|
|
118
|
+
"PagingSimplifiedAlbum",
|
|
119
|
+
"PagingSimplifiedEpisode",
|
|
120
|
+
"PagingSimplifiedShow",
|
|
121
|
+
"PagingArtistDiscographyAlbum",
|
|
122
|
+
"PagingSavedTrack",
|
|
123
|
+
"PagingSavedAlbum",
|
|
124
|
+
"PagingSavedShow",
|
|
125
|
+
"PagingSavedEpisode",
|
|
126
|
+
"CursorPagingPlayHistory",
|
|
127
|
+
"CursorPagingSimplifiedArtist",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -1,42 +1,63 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import json
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
self.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
import abc
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CacheHandler(abc.ABC):
|
|
11
|
+
@abc.abstractmethod
|
|
12
|
+
def get_cached_token(self) -> dict[str, Any] | None:
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
|
|
15
|
+
@abc.abstractmethod
|
|
16
|
+
def save_token_to_cache(self, token_info: dict[str, Any]) -> None:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MemoryCacheHandler(CacheHandler):
|
|
21
|
+
def __init__(self, token_info: dict[str, Any] | None = None) -> None:
|
|
22
|
+
self._token_info = token_info
|
|
23
|
+
|
|
24
|
+
def get_cached_token(self) -> dict[str, Any] | None:
|
|
25
|
+
logger.debug("Reading token from memory cache")
|
|
26
|
+
return self._token_info
|
|
27
|
+
|
|
28
|
+
def save_token_to_cache(self, token_info: dict[str, Any]) -> None:
|
|
29
|
+
logger.debug("Saving token to memory cache")
|
|
30
|
+
self._token_info = token_info
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CacheFileHandler(CacheHandler):
|
|
34
|
+
def __init__(self, cache_path: str | Path = ".cache") -> None:
|
|
35
|
+
self.cache_path = Path(cache_path)
|
|
36
|
+
|
|
37
|
+
def get_cached_token(self) -> dict[str, Any] | None:
|
|
38
|
+
if not self.cache_path.exists():
|
|
39
|
+
logger.debug("Token cache file does not exist: path=%s", self.cache_path)
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
token_info = json.loads(self.cache_path.read_text(encoding="utf-8"))
|
|
43
|
+
except (OSError, json.JSONDecodeError):
|
|
44
|
+
logger.warning(
|
|
45
|
+
"Unable to read token cache file: path=%s",
|
|
46
|
+
self.cache_path,
|
|
47
|
+
exc_info=True,
|
|
48
|
+
)
|
|
49
|
+
return None
|
|
50
|
+
logger.debug("Read token from cache file: path=%s", self.cache_path)
|
|
51
|
+
return token_info
|
|
52
|
+
|
|
53
|
+
def save_token_to_cache(self, token_info: dict[str, Any]) -> None:
|
|
54
|
+
try:
|
|
55
|
+
self.cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
self.cache_path.write_text(json.dumps(token_info), encoding="utf-8")
|
|
57
|
+
except OSError:
|
|
58
|
+
logger.exception(
|
|
59
|
+
"Unable to save token cache file: path=%s",
|
|
60
|
+
self.cache_path,
|
|
61
|
+
)
|
|
62
|
+
raise
|
|
63
|
+
logger.debug("Saved token to cache file: path=%s", self.cache_path)
|