checkleaked-spotify-api 1.0.0__py3-none-any.whl
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.
- checkleaked_spotify_api-1.0.0.dist-info/METADATA +201 -0
- checkleaked_spotify_api-1.0.0.dist-info/RECORD +15 -0
- checkleaked_spotify_api-1.0.0.dist-info/WHEEL +4 -0
- checkleaked_spotify_api-1.0.0.dist-info/licenses/LICENSE +22 -0
- spotify_api_sdk/__init__.py +53 -0
- spotify_api_sdk/_resource.py +81 -0
- spotify_api_sdk/client.py +71 -0
- spotify_api_sdk/config.py +134 -0
- spotify_api_sdk/errors.py +68 -0
- spotify_api_sdk/http.py +447 -0
- spotify_api_sdk/ids.py +42 -0
- spotify_api_sdk/pagination.py +70 -0
- spotify_api_sdk/py.typed +1 -0
- spotify_api_sdk/resources.py +2666 -0
- spotify_api_sdk/types.py +31 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: checkleaked-spotify-api
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Typed Python SDK for the CheckLeaked Spotify Data API and RapidAPI spotify81.
|
|
5
|
+
Project-URL: Homepage, https://spotify-proxy.checkleaked.cc
|
|
6
|
+
Project-URL: Documentation, https://spotify-proxy.checkleaked.cc/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/eduair94/checkleaked-spotify-api-python
|
|
8
|
+
Project-URL: Issues, https://github.com/eduair94/checkleaked-spotify-api-python/issues
|
|
9
|
+
Project-URL: OpenAPI, https://spotify-proxy.checkleaked.cc/openapi.json
|
|
10
|
+
Author-email: Eduardo Airaudo <airaudoeduardo@gmail.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: charts,checkleaked,concerts,lyrics,music,rapidapi,sdk,spotify,spotify-api,spotify-data,spotify81
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Requires-Dist: requests>=2.25
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
31
|
+
Requires-Dist: twine; extra == 'dev'
|
|
32
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# checkleaked-spotify-api
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/checkleaked-spotify-api/)
|
|
38
|
+
[](https://pypi.org/project/checkleaked-spotify-api/)
|
|
39
|
+
[](https://github.com/eduair94/checkleaked-spotify-api-python/actions/workflows/ci.yml)
|
|
40
|
+
[](./LICENSE)
|
|
41
|
+
|
|
42
|
+
Typed Python SDK for the **Spotify Data API**, covering the same 110 methods
|
|
43
|
+
and 25 namespaces as
|
|
44
|
+
[`@checkleaked/spotify-api`](https://www.npmjs.com/package/@checkleaked/spotify-api).
|
|
45
|
+
It works with both the
|
|
46
|
+
[RapidAPI spotify81 API](https://rapidapi.com/airaudoeduardo/api/spotify81)
|
|
47
|
+
and the direct
|
|
48
|
+
[CheckLeaked proxy](https://spotify-proxy.checkleaked.cc/docs).
|
|
49
|
+
|
|
50
|
+
This is a data API client. It does not require Spotify OAuth credentials.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install checkleaked-spotify-api
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from spotify_api_sdk import SpotifyApiClient
|
|
62
|
+
|
|
63
|
+
spotify = SpotifyApiClient(api_key="your-rapidapi-key")
|
|
64
|
+
|
|
65
|
+
results = spotify.search.search(
|
|
66
|
+
"daft punk",
|
|
67
|
+
type="artists",
|
|
68
|
+
limit=5,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
artist = spotify.artists.overview(
|
|
72
|
+
"spotify:artist:4tZwfgrHOc3mvqYlEYSvVi"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
lyrics = spotify.tracks.lyrics(
|
|
76
|
+
"https://open.spotify.com/track/0DiWol3AO6WpXZgp0goxAV",
|
|
77
|
+
format="lrc",
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Methods return the API's unwrapped `data` payload. Use
|
|
82
|
+
`client.request_raw(...)` when you need the complete success envelope.
|
|
83
|
+
|
|
84
|
+
## Providers
|
|
85
|
+
|
|
86
|
+
RapidAPI is the default and automatically adds
|
|
87
|
+
`x-rapidapi-host: spotify81.p.rapidapi.com`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
spotify = SpotifyApiClient(
|
|
91
|
+
api_key="your-rapidapi-key",
|
|
92
|
+
provider="rapidapi",
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The direct proxy exposes the same methods and paths. Use a key issued by the
|
|
97
|
+
direct portal:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
spotify = SpotifyApiClient(
|
|
101
|
+
api_key="your-direct-proxy-key",
|
|
102
|
+
provider="proxy",
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The API key may be omitted when `SPOTIFY_API_KEY` or `RAPIDAPI_KEY` is set.
|
|
107
|
+
|
|
108
|
+
## Pythonic and npm-compatible names
|
|
109
|
+
|
|
110
|
+
Python methods use `snake_case`, while npm-style `camelCase` aliases are also
|
|
111
|
+
available:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
spotify.artists.top_tracks(id, market="US")
|
|
115
|
+
spotify.artists.topTracks(id, market="US") # exact npm-style alias
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Every method supports per-call `request_timeout`, `request_retries`,
|
|
119
|
+
`request_headers`, and an additional `extra_query` mapping. POST methods also
|
|
120
|
+
accept a free-form `body` mapping.
|
|
121
|
+
|
|
122
|
+
## IDs, URIs, and URLs
|
|
123
|
+
|
|
124
|
+
Spotify IDs may be supplied as bare IDs, Spotify URIs, or open.spotify.com
|
|
125
|
+
URLs. Multi-ID methods accept comma-separated strings or Python sequences.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from spotify_api_sdk import normalize_id, normalize_ids
|
|
129
|
+
|
|
130
|
+
normalize_id("spotify:track:0DiWol3AO6WpXZgp0goxAV")
|
|
131
|
+
normalize_ids([
|
|
132
|
+
"spotify:artist:4tZwfgrHOc3mvqYlEYSvVi",
|
|
133
|
+
"https://open.spotify.com/artist/06HL4z0CvFAxyc27GXpf02",
|
|
134
|
+
])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Pagination
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from spotify_api_sdk import collect
|
|
141
|
+
|
|
142
|
+
items = collect(
|
|
143
|
+
lambda **page: spotify.artists.albums(id=artist_id, **page),
|
|
144
|
+
get_items=lambda response: (
|
|
145
|
+
response.get("data", {})
|
|
146
|
+
.get("artist", {})
|
|
147
|
+
.get("discography", {})
|
|
148
|
+
.get("albums", {})
|
|
149
|
+
.get("items", [])
|
|
150
|
+
),
|
|
151
|
+
limit=50,
|
|
152
|
+
max_items=500,
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Errors
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from spotify_api_sdk import (
|
|
160
|
+
AuthError,
|
|
161
|
+
HttpError,
|
|
162
|
+
RateLimitError,
|
|
163
|
+
SpotifyApiError,
|
|
164
|
+
TimeoutError,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
spotify.search.search("daft punk")
|
|
169
|
+
except RateLimitError as error:
|
|
170
|
+
print(error.retry_after_ms)
|
|
171
|
+
except AuthError:
|
|
172
|
+
print("Invalid or unsubscribed API key")
|
|
173
|
+
except SpotifyApiError as error:
|
|
174
|
+
print(error.status, error.code, error.url)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
spotify = SpotifyApiClient(
|
|
181
|
+
api_key="...",
|
|
182
|
+
provider="rapidapi", # rapidapi (default) | proxy
|
|
183
|
+
timeout=30,
|
|
184
|
+
retries=2,
|
|
185
|
+
retry_delay=0.5,
|
|
186
|
+
headers={"x-my-header": "value"},
|
|
187
|
+
debug=True,
|
|
188
|
+
on_request=lambda info: print("request", info["url"]),
|
|
189
|
+
on_response=lambda info: print("response", info["status"]),
|
|
190
|
+
on_retry=lambda info: print("retry", info["attempt"]),
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The client is also a context manager:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
with SpotifyApiClient(api_key="...") as spotify:
|
|
198
|
+
markets = spotify.markets.get()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
See [API_REFERENCE.md](./API_REFERENCE.md) for every namespace and method.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
spotify_api_sdk/__init__.py,sha256=xfaEZRCd9Swchh3zN6YoJlxjECHaGnjfES9u261qnJs,991
|
|
2
|
+
spotify_api_sdk/_resource.py,sha256=XaoLgNeavhdIaZPSHKCnp9A48D8MU8C6u24HwbAOijU,2603
|
|
3
|
+
spotify_api_sdk/client.py,sha256=lb6coFMXCsHiPL2Mj_P9EZVq4N_ygn1DmdH5I10ZqZg,2054
|
|
4
|
+
spotify_api_sdk/config.py,sha256=PCK4WSnJqYE4QmTIa-SBgSA_zyNqU9-cem9hT-KvOu8,3837
|
|
5
|
+
spotify_api_sdk/errors.py,sha256=Kk7hlBl1uRcelngpXusYzrmBqnU0jivqzfAIvDUWcFE,1513
|
|
6
|
+
spotify_api_sdk/http.py,sha256=U_kTCZs189OBapCsg-vuXitfnfVnLk3kR1ZiCtDRXro,13292
|
|
7
|
+
spotify_api_sdk/ids.py,sha256=eDltbjCWTx1ZhHfj6AeQZW0r0aqi1FfBV6IV5JeH2Kc,1059
|
|
8
|
+
spotify_api_sdk/pagination.py,sha256=BfBTxtf7RqzBTc7MV6uR9Jaun6EEuSPAgJid7tz81Oo,1873
|
|
9
|
+
spotify_api_sdk/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
10
|
+
spotify_api_sdk/resources.py,sha256=WrNBwXVXwh1EWt_wom3DscvFsGlHmruGoIXGn_XQ2ME,72838
|
|
11
|
+
spotify_api_sdk/types.py,sha256=M3aTr2JPjL3tu6r9-uCCWdvXhAAg3LDzkSvT7uZ0xPY,818
|
|
12
|
+
checkleaked_spotify_api-1.0.0.dist-info/METADATA,sha256=ei6WqaQCjAniqbBmcszPnEmGxAILCLt8pwOaSvAjoVE,5942
|
|
13
|
+
checkleaked_spotify_api-1.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
14
|
+
checkleaked_spotify_api-1.0.0.dist-info/licenses/LICENSE,sha256=23E_lxAHxPs2-W7_j2ZgS_hLCXUgsgR0Snj0nCrns74,1073
|
|
15
|
+
checkleaked_spotify_api-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eduardo Airaudo
|
|
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.
|
|
22
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Python SDK for the CheckLeaked Spotify Data API."""
|
|
2
|
+
|
|
3
|
+
from .client import SpotifyApiClient, SpotifyClient, create_client
|
|
4
|
+
from .errors import (
|
|
5
|
+
AuthError,
|
|
6
|
+
HttpError,
|
|
7
|
+
NetworkError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
SpotifyApiError,
|
|
10
|
+
TimeoutError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from .ids import normalize_id, normalize_ids
|
|
14
|
+
from .pagination import collect, paginate
|
|
15
|
+
from .types import (
|
|
16
|
+
ErrorEnvelope,
|
|
17
|
+
Headers,
|
|
18
|
+
IdInput,
|
|
19
|
+
IdList,
|
|
20
|
+
JsonValue,
|
|
21
|
+
QueryParams,
|
|
22
|
+
QueryValue,
|
|
23
|
+
SuccessEnvelope,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "1.0.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AuthError",
|
|
30
|
+
"ErrorEnvelope",
|
|
31
|
+
"Headers",
|
|
32
|
+
"HttpError",
|
|
33
|
+
"IdInput",
|
|
34
|
+
"IdList",
|
|
35
|
+
"JsonValue",
|
|
36
|
+
"NetworkError",
|
|
37
|
+
"QueryParams",
|
|
38
|
+
"QueryValue",
|
|
39
|
+
"RateLimitError",
|
|
40
|
+
"SpotifyApiClient",
|
|
41
|
+
"SpotifyApiError",
|
|
42
|
+
"SpotifyClient",
|
|
43
|
+
"SuccessEnvelope",
|
|
44
|
+
"TimeoutError",
|
|
45
|
+
"ValidationError",
|
|
46
|
+
"__version__",
|
|
47
|
+
"collect",
|
|
48
|
+
"create_client",
|
|
49
|
+
"normalize_id",
|
|
50
|
+
"normalize_ids",
|
|
51
|
+
"paginate",
|
|
52
|
+
]
|
|
53
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Resource primitives used by generated namespace classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from .errors import ValidationError
|
|
10
|
+
from .ids import normalize_id
|
|
11
|
+
from .types import Headers, QueryValue
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Endpoint:
|
|
16
|
+
key: str
|
|
17
|
+
method: str
|
|
18
|
+
path: str
|
|
19
|
+
path_fields: Tuple[Tuple[str, str], ...] = ()
|
|
20
|
+
query_fields: Tuple[Tuple[str, str], ...] = ()
|
|
21
|
+
body_fields: Tuple[Tuple[str, str], ...] = ()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Resource:
|
|
25
|
+
"""Base class for generated resource namespaces."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, client: Any) -> None:
|
|
28
|
+
self._client = client
|
|
29
|
+
|
|
30
|
+
def _invoke(self, endpoint: Endpoint, values: Mapping[str, Any]) -> Any:
|
|
31
|
+
path = endpoint.path
|
|
32
|
+
for python_name, wire_name in endpoint.path_fields:
|
|
33
|
+
value = values.get(python_name)
|
|
34
|
+
if value is None:
|
|
35
|
+
raise ValidationError(
|
|
36
|
+
f"{endpoint.key}: {python_name} is required",
|
|
37
|
+
code="VALIDATION",
|
|
38
|
+
)
|
|
39
|
+
path = path.replace(
|
|
40
|
+
"{" + wire_name + "}",
|
|
41
|
+
quote(normalize_id(value), safe=""),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
query_value = (
|
|
45
|
+
values.get("extra_query")
|
|
46
|
+
if "extra_query" in values
|
|
47
|
+
else values.get("query")
|
|
48
|
+
)
|
|
49
|
+
query: Dict[str, QueryValue] = dict(query_value or {})
|
|
50
|
+
for python_name, wire_name in endpoint.query_fields:
|
|
51
|
+
value = values.get(python_name)
|
|
52
|
+
if value is not None:
|
|
53
|
+
query[wire_name] = value
|
|
54
|
+
|
|
55
|
+
body = values.get("body")
|
|
56
|
+
explicit_body = {
|
|
57
|
+
wire_name: values[python_name]
|
|
58
|
+
for python_name, wire_name in endpoint.body_fields
|
|
59
|
+
if values.get(python_name) is not None
|
|
60
|
+
}
|
|
61
|
+
if explicit_body:
|
|
62
|
+
if body is None:
|
|
63
|
+
body = explicit_body
|
|
64
|
+
elif isinstance(body, Mapping):
|
|
65
|
+
body = {**body, **explicit_body}
|
|
66
|
+
else:
|
|
67
|
+
raise ValidationError(
|
|
68
|
+
f"{endpoint.key}: body must be a mapping when body fields "
|
|
69
|
+
"are supplied",
|
|
70
|
+
code="VALIDATION",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return self._client.request(
|
|
74
|
+
endpoint.method,
|
|
75
|
+
path,
|
|
76
|
+
query=query or None,
|
|
77
|
+
body=body,
|
|
78
|
+
timeout=values.get("request_timeout"),
|
|
79
|
+
retries=values.get("request_retries"),
|
|
80
|
+
headers=values.get("request_headers"),
|
|
81
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""High-level Spotify Data API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
from .config import Debug, Hook, ResolvedConfig, resolve_config
|
|
8
|
+
from .http import SpotifyHttpClient
|
|
9
|
+
from .resources import RESOURCE_CLASSES, ClientResources
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SpotifyApiClient(ClientResources, SpotifyHttpClient):
|
|
13
|
+
"""Client exposing all 25 typed Spotify Data API namespaces."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_key: Optional[str] = None,
|
|
18
|
+
*,
|
|
19
|
+
provider: str = "rapidapi",
|
|
20
|
+
transport: Optional[str] = None,
|
|
21
|
+
base_url: Optional[str] = None,
|
|
22
|
+
host: Optional[str] = None,
|
|
23
|
+
timeout: float = 30.0,
|
|
24
|
+
retries: int = 2,
|
|
25
|
+
retry_delay: float = 0.5,
|
|
26
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
27
|
+
user_agent: Optional[str] = None,
|
|
28
|
+
session: Any = None,
|
|
29
|
+
debug: Debug = False,
|
|
30
|
+
on_request: Optional[Hook] = None,
|
|
31
|
+
on_response: Optional[Hook] = None,
|
|
32
|
+
on_retry: Optional[Hook] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
config: ResolvedConfig = resolve_config(
|
|
35
|
+
api_key,
|
|
36
|
+
provider=provider,
|
|
37
|
+
transport=transport,
|
|
38
|
+
base_url=base_url,
|
|
39
|
+
host=host,
|
|
40
|
+
timeout=timeout,
|
|
41
|
+
retries=retries,
|
|
42
|
+
retry_delay=retry_delay,
|
|
43
|
+
headers=headers,
|
|
44
|
+
user_agent=user_agent,
|
|
45
|
+
session=session,
|
|
46
|
+
debug=debug,
|
|
47
|
+
on_request=on_request,
|
|
48
|
+
on_response=on_response,
|
|
49
|
+
on_retry=on_retry,
|
|
50
|
+
)
|
|
51
|
+
super().__init__(config)
|
|
52
|
+
for name, resource_class in RESOURCE_CLASSES.items():
|
|
53
|
+
setattr(self, name, resource_class(self))
|
|
54
|
+
|
|
55
|
+
def __enter__(self) -> "SpotifyApiClient":
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, *_: Any) -> None:
|
|
59
|
+
self.close()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
SpotifyClient = SpotifyApiClient
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_client(
|
|
66
|
+
api_key: Optional[str] = None,
|
|
67
|
+
**config: Any,
|
|
68
|
+
) -> SpotifyApiClient:
|
|
69
|
+
"""Create a :class:`SpotifyApiClient`."""
|
|
70
|
+
|
|
71
|
+
return SpotifyApiClient(api_key, **config)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Client configuration and provider presets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Callable, Dict, Mapping, Optional, Union
|
|
8
|
+
|
|
9
|
+
from .errors import ValidationError
|
|
10
|
+
|
|
11
|
+
RAPIDAPI_BASE_URL = "https://spotify81.p.rapidapi.com"
|
|
12
|
+
RAPIDAPI_HOST = "spotify81.p.rapidapi.com"
|
|
13
|
+
PROXY_BASE_URL = "https://spotify-proxy.checkleaked.cc/spotify-data"
|
|
14
|
+
DEFAULT_USER_AGENT = (
|
|
15
|
+
"checkleaked-spotify-api-python/1.0.0 "
|
|
16
|
+
"(+https://pypi.org/project/checkleaked-spotify-api/)"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Hook = Callable[[Mapping[str, Any]], None]
|
|
20
|
+
Debug = Union[bool, Callable[[str, Optional[Any]], None]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Channel:
|
|
25
|
+
"""A resolved base URL and its optional RapidAPI host header."""
|
|
26
|
+
|
|
27
|
+
base_url: str
|
|
28
|
+
host: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ResolvedConfig:
|
|
33
|
+
api_key: str
|
|
34
|
+
provider: str
|
|
35
|
+
channel: Channel
|
|
36
|
+
timeout_s: float
|
|
37
|
+
retries: int
|
|
38
|
+
retry_delay_s: float
|
|
39
|
+
headers: Dict[str, str]
|
|
40
|
+
user_agent: str
|
|
41
|
+
session: Any
|
|
42
|
+
debug: Optional[Callable[[str, Optional[Any]], None]]
|
|
43
|
+
on_request: Optional[Hook]
|
|
44
|
+
on_response: Optional[Hook]
|
|
45
|
+
on_retry: Optional[Hook]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _debug_logger(value: Debug) -> Optional[Callable[[str, Optional[Any]], None]]:
|
|
49
|
+
if value is True:
|
|
50
|
+
def log(message: str, detail: Optional[Any] = None) -> None:
|
|
51
|
+
suffix = "" if detail is None else f" {detail!r}"
|
|
52
|
+
print(f"[spotify-api] {message}{suffix}")
|
|
53
|
+
|
|
54
|
+
return log
|
|
55
|
+
if callable(value):
|
|
56
|
+
return value
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_config(
|
|
61
|
+
api_key: Optional[str] = None,
|
|
62
|
+
*,
|
|
63
|
+
provider: str = "rapidapi",
|
|
64
|
+
transport: Optional[str] = None,
|
|
65
|
+
base_url: Optional[str] = None,
|
|
66
|
+
host: Optional[str] = None,
|
|
67
|
+
timeout: float = 30.0,
|
|
68
|
+
retries: int = 2,
|
|
69
|
+
retry_delay: float = 0.5,
|
|
70
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
71
|
+
user_agent: Optional[str] = None,
|
|
72
|
+
session: Any = None,
|
|
73
|
+
debug: Debug = False,
|
|
74
|
+
on_request: Optional[Hook] = None,
|
|
75
|
+
on_response: Optional[Hook] = None,
|
|
76
|
+
on_retry: Optional[Hook] = None,
|
|
77
|
+
) -> ResolvedConfig:
|
|
78
|
+
"""Resolve and validate a client configuration."""
|
|
79
|
+
|
|
80
|
+
if transport is not None:
|
|
81
|
+
provider = transport
|
|
82
|
+
if provider not in ("rapidapi", "proxy"):
|
|
83
|
+
raise ValidationError(
|
|
84
|
+
f"Invalid provider: {provider!r}. Expected 'rapidapi' or 'proxy'.",
|
|
85
|
+
code="CONFIG",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
resolved_key = (
|
|
89
|
+
api_key
|
|
90
|
+
or os.environ.get("SPOTIFY_API_KEY")
|
|
91
|
+
or os.environ.get("RAPIDAPI_KEY")
|
|
92
|
+
)
|
|
93
|
+
if not isinstance(resolved_key, str) or not resolved_key.strip():
|
|
94
|
+
raise ValidationError(
|
|
95
|
+
"api_key is required (or set SPOTIFY_API_KEY / RAPIDAPI_KEY)",
|
|
96
|
+
code="CONFIG",
|
|
97
|
+
)
|
|
98
|
+
if timeout <= 0:
|
|
99
|
+
raise ValidationError("timeout must be greater than 0", code="CONFIG")
|
|
100
|
+
if retry_delay < 0:
|
|
101
|
+
raise ValidationError("retry_delay must be at least 0", code="CONFIG")
|
|
102
|
+
|
|
103
|
+
if provider == "rapidapi":
|
|
104
|
+
channel = Channel(
|
|
105
|
+
base_url=(base_url or RAPIDAPI_BASE_URL).rstrip("/"),
|
|
106
|
+
host=RAPIDAPI_HOST if host is None else host,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
channel = Channel(
|
|
110
|
+
base_url=(base_url or PROXY_BASE_URL).rstrip("/"),
|
|
111
|
+
host=host,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if session is None:
|
|
115
|
+
import requests
|
|
116
|
+
|
|
117
|
+
session = requests.Session()
|
|
118
|
+
|
|
119
|
+
return ResolvedConfig(
|
|
120
|
+
api_key=resolved_key.strip(),
|
|
121
|
+
provider=provider,
|
|
122
|
+
channel=channel,
|
|
123
|
+
timeout_s=float(timeout),
|
|
124
|
+
retries=max(0, int(retries)),
|
|
125
|
+
retry_delay_s=float(retry_delay),
|
|
126
|
+
headers=dict(headers or {}),
|
|
127
|
+
user_agent=user_agent or DEFAULT_USER_AGENT,
|
|
128
|
+
session=session,
|
|
129
|
+
debug=_debug_logger(debug),
|
|
130
|
+
on_request=on_request,
|
|
131
|
+
on_response=on_response,
|
|
132
|
+
on_retry=on_retry,
|
|
133
|
+
)
|
|
134
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Structured errors raised by the Spotify Data API SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SpotifyApiError(Exception):
|
|
9
|
+
"""Base error for all SDK failures."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
status: Optional[int] = None,
|
|
16
|
+
code: Optional[str] = None,
|
|
17
|
+
body: Any = None,
|
|
18
|
+
url: Optional[str] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status = status
|
|
22
|
+
self.code = code
|
|
23
|
+
self.body = body
|
|
24
|
+
self.url = url
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HttpError(SpotifyApiError):
|
|
28
|
+
"""A non-successful HTTP response."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthError(HttpError):
|
|
32
|
+
"""Authentication failed."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RateLimitError(HttpError):
|
|
36
|
+
"""The API returned HTTP 429."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
message: str,
|
|
41
|
+
*,
|
|
42
|
+
status: int = 429,
|
|
43
|
+
code: Optional[str] = None,
|
|
44
|
+
body: Any = None,
|
|
45
|
+
url: Optional[str] = None,
|
|
46
|
+
retry_after_ms: Optional[int] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__(
|
|
49
|
+
message,
|
|
50
|
+
status=status,
|
|
51
|
+
code=code,
|
|
52
|
+
body=body,
|
|
53
|
+
url=url,
|
|
54
|
+
)
|
|
55
|
+
self.retry_after_ms = retry_after_ms
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NetworkError(SpotifyApiError):
|
|
59
|
+
"""A transport-level network failure."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TimeoutError(NetworkError): # noqa: A001 - intentional public API
|
|
63
|
+
"""The request timed out."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ValidationError(SpotifyApiError):
|
|
67
|
+
"""Invalid SDK configuration or method arguments."""
|
|
68
|
+
|