pySplash.py 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.
- pySplash/__init__.py +38 -0
- pySplash/api.py +758 -0
- pySplash/api_async.py +704 -0
- pySplash/base.py +142 -0
- pySplash/config.py +55 -0
- pySplash/errors.py +30 -0
- pySplash/http_client.py +97 -0
- pySplash/http_client_async.py +107 -0
- pySplash/models.py +173 -0
- pySplash/py.typed +0 -0
- pysplash_py-1.0.0.dist-info/METADATA +942 -0
- pysplash_py-1.0.0.dist-info/RECORD +15 -0
- pysplash_py-1.0.0.dist-info/WHEEL +5 -0
- pysplash_py-1.0.0.dist-info/licenses/LICENSE +21 -0
- pysplash_py-1.0.0.dist-info/top_level.txt +1 -0
pySplash/api.py
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
"""Synchronous pySplash.py API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .base import PySplashBase
|
|
9
|
+
from .config import AVAILABLE_ORDERS, AVAILABLE_ORIENTATIONS, URL_CONFIG
|
|
10
|
+
from .errors import PySplashError
|
|
11
|
+
from .http_client import HttpClient
|
|
12
|
+
from .models import (
|
|
13
|
+
BearerTokenResponse,
|
|
14
|
+
Collection,
|
|
15
|
+
DeleteResponse,
|
|
16
|
+
Photo,
|
|
17
|
+
PhotoLinks,
|
|
18
|
+
PhotoStatistics,
|
|
19
|
+
SearchResult,
|
|
20
|
+
User,
|
|
21
|
+
UserPortfolio,
|
|
22
|
+
UserStatistics,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("pySplash.py")
|
|
26
|
+
|
|
27
|
+
_BASE = URL_CONFIG["API_LOCATION"]
|
|
28
|
+
_TOKEN_URL = URL_CONFIG["BEARER_TOKEN_URL"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PySplashApi(PySplashBase):
|
|
32
|
+
"""Sync wrapper for the Unsplash API.
|
|
33
|
+
|
|
34
|
+
Supports context manager protocol for clean resource management::
|
|
35
|
+
|
|
36
|
+
with PySplashApi() as api:
|
|
37
|
+
api.init(bearer_token='...')
|
|
38
|
+
result = api.get_photo('id')
|
|
39
|
+
|
|
40
|
+
Also supports explicit client reuse::
|
|
41
|
+
|
|
42
|
+
api = PySplashApi()
|
|
43
|
+
try:
|
|
44
|
+
api.init(bearer_token='...')
|
|
45
|
+
result = api.get_photo('id')
|
|
46
|
+
finally:
|
|
47
|
+
api.close()
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._access_key: str = ""
|
|
52
|
+
self._secret_key: str = ""
|
|
53
|
+
self._redirect_uri: str = ""
|
|
54
|
+
self._code: str = ""
|
|
55
|
+
self._grant_type: str = "authorization_code"
|
|
56
|
+
self._bearer_token: str = ""
|
|
57
|
+
self._timeout: int = 10
|
|
58
|
+
self._retries: int = 2
|
|
59
|
+
self._retry_delay: float = 0.1
|
|
60
|
+
self._headers: dict[str, str] = {}
|
|
61
|
+
self._client: HttpClient | None = None
|
|
62
|
+
|
|
63
|
+
def close(self) -> None:
|
|
64
|
+
if self._client is not None:
|
|
65
|
+
self._client.close()
|
|
66
|
+
self._client = None
|
|
67
|
+
|
|
68
|
+
def __enter__(self) -> PySplashApi:
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, *args: Any) -> None:
|
|
72
|
+
self.close()
|
|
73
|
+
|
|
74
|
+
def _get_client(self) -> HttpClient:
|
|
75
|
+
if self._client is None:
|
|
76
|
+
self._client = HttpClient(
|
|
77
|
+
headers=self._headers,
|
|
78
|
+
timeout=self._timeout,
|
|
79
|
+
retries=self._retries,
|
|
80
|
+
retry_delay=self._retry_delay,
|
|
81
|
+
)
|
|
82
|
+
return self._client
|
|
83
|
+
|
|
84
|
+
def _fetch_url(
|
|
85
|
+
self,
|
|
86
|
+
url: str,
|
|
87
|
+
method: str,
|
|
88
|
+
query_parameters: dict[str, Any] | None = None,
|
|
89
|
+
body: Any | None = None,
|
|
90
|
+
) -> Any:
|
|
91
|
+
client = self._get_client()
|
|
92
|
+
try:
|
|
93
|
+
res = client.make_request(url, method, self._build_query_parameters(query_parameters or {}), body)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
raise self._create_pysplash_error(exc) from exc
|
|
96
|
+
|
|
97
|
+
return self._handle_response_status(res.data, res.status, res.statusText)
|
|
98
|
+
|
|
99
|
+
def init(
|
|
100
|
+
self,
|
|
101
|
+
access_key: str | None = None,
|
|
102
|
+
secret_key: str | None = None,
|
|
103
|
+
redirect_uri: str | None = None,
|
|
104
|
+
code: str | None = None,
|
|
105
|
+
bearer_token: str | None = None,
|
|
106
|
+
timeout: int = 10,
|
|
107
|
+
retries: int = 2,
|
|
108
|
+
retry_delay: float = 0.1,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Initialize the client with API credentials or a bearer token."""
|
|
111
|
+
if timeout > 0:
|
|
112
|
+
self._timeout = timeout
|
|
113
|
+
if retries >= 0:
|
|
114
|
+
self._retries = retries
|
|
115
|
+
if retry_delay >= 0:
|
|
116
|
+
self._retry_delay = retry_delay
|
|
117
|
+
|
|
118
|
+
self._bearer_token = bearer_token or ""
|
|
119
|
+
self._headers = self._default_headers(bearer_token=bearer_token, access_key=access_key)
|
|
120
|
+
|
|
121
|
+
if bearer_token:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
if not access_key:
|
|
125
|
+
raise PySplashError("Access Key missing!")
|
|
126
|
+
if not secret_key:
|
|
127
|
+
raise PySplashError("Secret Key missing!")
|
|
128
|
+
if not redirect_uri:
|
|
129
|
+
raise PySplashError("Redirect URI missing!")
|
|
130
|
+
if not code:
|
|
131
|
+
raise PySplashError("Authorization Code missing!")
|
|
132
|
+
|
|
133
|
+
self._access_key = access_key
|
|
134
|
+
self._secret_key = secret_key
|
|
135
|
+
self._redirect_uri = redirect_uri
|
|
136
|
+
self._code = code
|
|
137
|
+
|
|
138
|
+
# ── Auth ──────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
def generate_bearer_token(self) -> BearerTokenResponse:
|
|
141
|
+
"""Exchange the authorization code for a bearer token."""
|
|
142
|
+
self._validate_required(self._access_key, "access_key")
|
|
143
|
+
self._validate_required(self._secret_key, "secret_key")
|
|
144
|
+
self._validate_required(self._redirect_uri, "redirect_uri")
|
|
145
|
+
self._validate_required(self._code, "code")
|
|
146
|
+
|
|
147
|
+
return self._fetch_url(
|
|
148
|
+
_TOKEN_URL,
|
|
149
|
+
"POST",
|
|
150
|
+
{
|
|
151
|
+
"client_id": self._access_key,
|
|
152
|
+
"client_secret": self._secret_key,
|
|
153
|
+
"redirect_uri": self._redirect_uri,
|
|
154
|
+
"code": self._code,
|
|
155
|
+
"grant_type": self._grant_type,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# ── Current User ──────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
def get_current_user_profile(self) -> User:
|
|
162
|
+
"""Get the current user's profile."""
|
|
163
|
+
return self._fetch_url(_BASE + URL_CONFIG["CURRENT_USER_PROFILE"], "GET")
|
|
164
|
+
|
|
165
|
+
def update_current_user_profile(
|
|
166
|
+
self,
|
|
167
|
+
username: str | None = None,
|
|
168
|
+
first_name: str | None = None,
|
|
169
|
+
last_name: str | None = None,
|
|
170
|
+
email: str | None = None,
|
|
171
|
+
url: str | None = None,
|
|
172
|
+
location: str | None = None,
|
|
173
|
+
bio: str | None = None,
|
|
174
|
+
instagram_username: str | None = None,
|
|
175
|
+
) -> User:
|
|
176
|
+
"""Update the current user's profile."""
|
|
177
|
+
return self._fetch_url(
|
|
178
|
+
_BASE + URL_CONFIG["UPDATE_CURRENT_USER_PROFILE"],
|
|
179
|
+
"PUT",
|
|
180
|
+
{
|
|
181
|
+
"username": username,
|
|
182
|
+
"first_name": first_name,
|
|
183
|
+
"last_name": last_name,
|
|
184
|
+
"email": email,
|
|
185
|
+
"url": url,
|
|
186
|
+
"location": location,
|
|
187
|
+
"bio": bio,
|
|
188
|
+
"instagram_username": instagram_username,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# ── Users ─────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
def get_public_profile(
|
|
195
|
+
self,
|
|
196
|
+
username: str,
|
|
197
|
+
width: int | None = None,
|
|
198
|
+
height: int | None = None,
|
|
199
|
+
) -> User:
|
|
200
|
+
"""Get a user's public profile."""
|
|
201
|
+
self._validate_required(username, "username")
|
|
202
|
+
self._validate_int(width, "width", minimum=1)
|
|
203
|
+
self._validate_int(height, "height", minimum=1)
|
|
204
|
+
return self._fetch_url(
|
|
205
|
+
_BASE + URL_CONFIG["USERS_PUBLIC_PROFILE"] + username,
|
|
206
|
+
"GET",
|
|
207
|
+
{"w": width, "h": height},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def get_user_portfolio(self, username: str) -> UserPortfolio:
|
|
211
|
+
"""Get a user's portfolio link."""
|
|
212
|
+
self._validate_required(username, "username")
|
|
213
|
+
return self._fetch_url(
|
|
214
|
+
self._build_url(_BASE, URL_CONFIG["USERS_PORTFOLIO"], username=username),
|
|
215
|
+
"GET",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def get_user_photos(
|
|
219
|
+
self,
|
|
220
|
+
username: str,
|
|
221
|
+
page: int | None = None,
|
|
222
|
+
per_page: int | None = None,
|
|
223
|
+
stats: bool | None = None,
|
|
224
|
+
resolution: str | None = None,
|
|
225
|
+
quantity: int | None = None,
|
|
226
|
+
order_by: str | None = None,
|
|
227
|
+
) -> list[Photo]:
|
|
228
|
+
"""Get a user's photos."""
|
|
229
|
+
self._validate_required(username, "username")
|
|
230
|
+
self._validate_supported_value(order_by, AVAILABLE_ORDERS, "order_by")
|
|
231
|
+
self._validate_bool(stats, "stats")
|
|
232
|
+
self._validate_int(page, "page", minimum=1)
|
|
233
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
234
|
+
self._validate_int(quantity, "quantity", minimum=1, maximum=30)
|
|
235
|
+
return self._fetch_url(
|
|
236
|
+
self._build_url(_BASE, URL_CONFIG["USERS_PHOTOS"], username=username),
|
|
237
|
+
"GET",
|
|
238
|
+
{
|
|
239
|
+
"page": page if page is not None else 1,
|
|
240
|
+
"per_page": per_page if per_page is not None else 10,
|
|
241
|
+
"order_by": order_by if order_by is not None else "latest",
|
|
242
|
+
"stats": stats if stats is not None else False,
|
|
243
|
+
"resolution": resolution if resolution is not None else "days",
|
|
244
|
+
"quantity": quantity if quantity is not None else 30,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def get_user_liked_photos(
|
|
249
|
+
self,
|
|
250
|
+
username: str,
|
|
251
|
+
page: int | None = None,
|
|
252
|
+
per_page: int | None = None,
|
|
253
|
+
order_by: str | None = None,
|
|
254
|
+
) -> list[Photo]:
|
|
255
|
+
"""Get a user's liked photos."""
|
|
256
|
+
self._validate_required(username, "username")
|
|
257
|
+
self._validate_supported_value(order_by, AVAILABLE_ORDERS, "order_by")
|
|
258
|
+
self._validate_int(page, "page", minimum=1)
|
|
259
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
260
|
+
return self._fetch_url(
|
|
261
|
+
self._build_url(_BASE, URL_CONFIG["USERS_LIKED_PHOTOS"], username=username),
|
|
262
|
+
"GET",
|
|
263
|
+
{
|
|
264
|
+
"page": page if page is not None else 1,
|
|
265
|
+
"per_page": per_page if per_page is not None else 10,
|
|
266
|
+
"order_by": order_by if order_by is not None else "latest",
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def get_user_collections(
|
|
271
|
+
self,
|
|
272
|
+
username: str,
|
|
273
|
+
page: int | None = None,
|
|
274
|
+
per_page: int | None = None,
|
|
275
|
+
) -> list[Collection]:
|
|
276
|
+
"""Get a user's collections."""
|
|
277
|
+
self._validate_required(username, "username")
|
|
278
|
+
self._validate_int(page, "page", minimum=1)
|
|
279
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
280
|
+
return self._fetch_url(
|
|
281
|
+
self._build_url(_BASE, URL_CONFIG["USERS_COLLECTIONS"], username=username),
|
|
282
|
+
"GET",
|
|
283
|
+
{
|
|
284
|
+
"page": page if page is not None else 1,
|
|
285
|
+
"per_page": per_page if per_page is not None else 10,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def get_user_statistics(
|
|
290
|
+
self,
|
|
291
|
+
username: str,
|
|
292
|
+
resolution: str | None = None,
|
|
293
|
+
quantity: int | None = None,
|
|
294
|
+
) -> UserStatistics:
|
|
295
|
+
"""Get a user's statistics."""
|
|
296
|
+
self._validate_required(username, "username")
|
|
297
|
+
self._validate_int(quantity, "quantity", minimum=1, maximum=30)
|
|
298
|
+
return self._fetch_url(
|
|
299
|
+
self._build_url(_BASE, URL_CONFIG["USERS_STATISTICS"], username=username),
|
|
300
|
+
"GET",
|
|
301
|
+
{
|
|
302
|
+
"resolution": resolution if resolution is not None else "days",
|
|
303
|
+
"quantity": quantity if quantity is not None else 30,
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# ── Photos ────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
def list_photos(
|
|
310
|
+
self,
|
|
311
|
+
page: int | None = None,
|
|
312
|
+
per_page: int | None = None,
|
|
313
|
+
order_by: str | None = None,
|
|
314
|
+
) -> list[Photo]:
|
|
315
|
+
"""List photos."""
|
|
316
|
+
self._validate_supported_value(order_by, AVAILABLE_ORDERS, "order_by")
|
|
317
|
+
self._validate_int(page, "page", minimum=1)
|
|
318
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
319
|
+
return self._fetch_url(
|
|
320
|
+
_BASE + URL_CONFIG["LIST_PHOTOS"],
|
|
321
|
+
"GET",
|
|
322
|
+
{
|
|
323
|
+
"page": page if page is not None else 1,
|
|
324
|
+
"per_page": per_page if per_page is not None else 10,
|
|
325
|
+
"order_by": order_by if order_by is not None else "latest",
|
|
326
|
+
},
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def list_curated_photos(
|
|
330
|
+
self,
|
|
331
|
+
page: int | None = None,
|
|
332
|
+
per_page: int | None = None,
|
|
333
|
+
order_by: str | None = None,
|
|
334
|
+
) -> list[Photo]:
|
|
335
|
+
"""List curated photos."""
|
|
336
|
+
self._validate_supported_value(order_by, AVAILABLE_ORDERS, "order_by")
|
|
337
|
+
self._validate_int(page, "page", minimum=1)
|
|
338
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
339
|
+
return self._fetch_url(
|
|
340
|
+
_BASE + URL_CONFIG["LIST_CURATED_PHOTOS"],
|
|
341
|
+
"GET",
|
|
342
|
+
{
|
|
343
|
+
"page": page if page is not None else 1,
|
|
344
|
+
"per_page": per_page if per_page is not None else 10,
|
|
345
|
+
"order_by": order_by if order_by is not None else "latest",
|
|
346
|
+
},
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def get_photo(
|
|
350
|
+
self,
|
|
351
|
+
id: str,
|
|
352
|
+
width: int | None = None,
|
|
353
|
+
height: int | None = None,
|
|
354
|
+
rect: str | None = None,
|
|
355
|
+
) -> Photo:
|
|
356
|
+
"""Get a photo by ID."""
|
|
357
|
+
self._validate_required(id, "id")
|
|
358
|
+
self._validate_int(width, "width", minimum=1)
|
|
359
|
+
self._validate_int(height, "height", minimum=1)
|
|
360
|
+
return self._fetch_url(
|
|
361
|
+
self._build_url(_BASE, URL_CONFIG["GET_A_PHOTO"], id=id),
|
|
362
|
+
"GET",
|
|
363
|
+
{"w": width, "h": height, "rect": rect},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def get_a_photo(
|
|
367
|
+
self,
|
|
368
|
+
id: str,
|
|
369
|
+
width: int | None = None,
|
|
370
|
+
height: int | None = None,
|
|
371
|
+
rect: str | None = None,
|
|
372
|
+
) -> Photo:
|
|
373
|
+
"""Get a photo by ID (alias for get_photo)."""
|
|
374
|
+
return self.get_photo(id, width, height, rect)
|
|
375
|
+
|
|
376
|
+
def get_random_photo(
|
|
377
|
+
self,
|
|
378
|
+
collections: str | int | None = None,
|
|
379
|
+
featured: bool | None = None,
|
|
380
|
+
username: str | None = None,
|
|
381
|
+
query: str | None = None,
|
|
382
|
+
width: int | None = None,
|
|
383
|
+
height: int | None = None,
|
|
384
|
+
orientation: str | None = None,
|
|
385
|
+
count: int | None = None,
|
|
386
|
+
) -> Photo | list[Photo]:
|
|
387
|
+
"""Get a random photo.
|
|
388
|
+
|
|
389
|
+
Returns a single Photo unless count is specified, in which case
|
|
390
|
+
a list of Photos is returned.
|
|
391
|
+
"""
|
|
392
|
+
self._validate_supported_value(orientation, AVAILABLE_ORIENTATIONS, "orientation")
|
|
393
|
+
self._validate_bool(featured, "featured")
|
|
394
|
+
self._validate_int(width, "width", minimum=1)
|
|
395
|
+
self._validate_int(height, "height", minimum=1)
|
|
396
|
+
self._validate_int(count, "count", minimum=1, maximum=30)
|
|
397
|
+
return self._fetch_url(
|
|
398
|
+
_BASE + URL_CONFIG["GET_A_RANDOM_PHOTO"],
|
|
399
|
+
"GET",
|
|
400
|
+
{
|
|
401
|
+
"collections": str(collections) if collections is not None else None,
|
|
402
|
+
"featured": featured if featured is not None else False,
|
|
403
|
+
"username": username,
|
|
404
|
+
"query": query,
|
|
405
|
+
"width": width,
|
|
406
|
+
"height": height,
|
|
407
|
+
"orientation": orientation if orientation is not None else "landscape",
|
|
408
|
+
"count": count if count is not None else 1,
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def get_a_random_photo(
|
|
413
|
+
self,
|
|
414
|
+
collections: str | int | None = None,
|
|
415
|
+
featured: bool | None = None,
|
|
416
|
+
username: str | None = None,
|
|
417
|
+
query: str | None = None,
|
|
418
|
+
width: int | None = None,
|
|
419
|
+
height: int | None = None,
|
|
420
|
+
orientation: str | None = None,
|
|
421
|
+
count: int | None = None,
|
|
422
|
+
) -> Photo | list[Photo]:
|
|
423
|
+
"""Get a random photo (alias for get_random_photo)."""
|
|
424
|
+
return self.get_random_photo(collections, featured, username, query, width, height, orientation, count)
|
|
425
|
+
|
|
426
|
+
def get_photo_statistics(
|
|
427
|
+
self,
|
|
428
|
+
id: str,
|
|
429
|
+
resolution: str | None = None,
|
|
430
|
+
quantity: int | None = None,
|
|
431
|
+
) -> PhotoStatistics:
|
|
432
|
+
"""Get a photo's statistics."""
|
|
433
|
+
self._validate_required(id, "id")
|
|
434
|
+
self._validate_int(quantity, "quantity", minimum=1, maximum=30)
|
|
435
|
+
return self._fetch_url(
|
|
436
|
+
self._build_url(_BASE, URL_CONFIG["GET_A_PHOTO_STATISTICS"], id=id),
|
|
437
|
+
"GET",
|
|
438
|
+
{
|
|
439
|
+
"resolution": resolution if resolution is not None else "days",
|
|
440
|
+
"quantity": quantity if quantity is not None else 30,
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def get_photo_link(self, id: str) -> PhotoLinks:
|
|
445
|
+
"""Get a photo's download link."""
|
|
446
|
+
self._validate_required(id, "id")
|
|
447
|
+
return self._fetch_url(
|
|
448
|
+
self._build_url(_BASE, URL_CONFIG["GET_A_PHOTO_DOWNLOAD_LINK"], id=id),
|
|
449
|
+
"GET",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def update_photo(
|
|
453
|
+
self,
|
|
454
|
+
id: str,
|
|
455
|
+
location: dict[str, Any] | None = None,
|
|
456
|
+
exif: dict[str, Any] | None = None,
|
|
457
|
+
) -> Photo:
|
|
458
|
+
"""Update a photo's location and EXIF data."""
|
|
459
|
+
self._validate_required(id, "id")
|
|
460
|
+
params = {**self._extract_location_params(location), **self._extract_exif_params(exif)}
|
|
461
|
+
return self._fetch_url(
|
|
462
|
+
self._build_url(_BASE, URL_CONFIG["UPDATE_A_PHOTO"], id=id),
|
|
463
|
+
"PUT",
|
|
464
|
+
params,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def like_photo(self, id: str) -> Photo:
|
|
468
|
+
"""Like a photo."""
|
|
469
|
+
self._validate_required(id, "id")
|
|
470
|
+
return self._fetch_url(
|
|
471
|
+
self._build_url(_BASE, URL_CONFIG["LIKE_A_PHOTO"], id=id),
|
|
472
|
+
"POST",
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def unlike_photo(self, id: str) -> Photo:
|
|
476
|
+
"""Unlike a photo."""
|
|
477
|
+
self._validate_required(id, "id")
|
|
478
|
+
return self._fetch_url(
|
|
479
|
+
self._build_url(_BASE, URL_CONFIG["UNLIKE_A_PHOTO"], id=id),
|
|
480
|
+
"DELETE",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# ── Search ────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
def search(
|
|
486
|
+
self,
|
|
487
|
+
query: str,
|
|
488
|
+
page: int | None = None,
|
|
489
|
+
per_page: int | None = None,
|
|
490
|
+
collections: str | int | None = None,
|
|
491
|
+
orientation: str | None = None,
|
|
492
|
+
) -> SearchResult:
|
|
493
|
+
"""Search photos."""
|
|
494
|
+
self._validate_required(query, "query")
|
|
495
|
+
self._validate_supported_value(orientation, AVAILABLE_ORIENTATIONS, "orientation")
|
|
496
|
+
self._validate_int(page, "page", minimum=1)
|
|
497
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
498
|
+
return self._fetch_url(
|
|
499
|
+
_BASE + URL_CONFIG["SEARCH_PHOTOS"],
|
|
500
|
+
"GET",
|
|
501
|
+
{
|
|
502
|
+
"query": query,
|
|
503
|
+
"page": page if page is not None else 1,
|
|
504
|
+
"per_page": per_page if per_page is not None else 10,
|
|
505
|
+
"collections": str(collections) if collections is not None else None,
|
|
506
|
+
"orientation": orientation,
|
|
507
|
+
},
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def search_collections(
|
|
511
|
+
self,
|
|
512
|
+
query: str,
|
|
513
|
+
page: int | None = None,
|
|
514
|
+
per_page: int | None = None,
|
|
515
|
+
) -> SearchResult:
|
|
516
|
+
"""Search collections."""
|
|
517
|
+
self._validate_required(query, "query")
|
|
518
|
+
self._validate_int(page, "page", minimum=1)
|
|
519
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
520
|
+
return self._fetch_url(
|
|
521
|
+
_BASE + URL_CONFIG["SEARCH_COLLECTIONS"],
|
|
522
|
+
"GET",
|
|
523
|
+
{
|
|
524
|
+
"query": query,
|
|
525
|
+
"page": page if page is not None else 1,
|
|
526
|
+
"per_page": per_page if per_page is not None else 10,
|
|
527
|
+
},
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
def search_users(
|
|
531
|
+
self,
|
|
532
|
+
query: str,
|
|
533
|
+
page: int | None = None,
|
|
534
|
+
per_page: int | None = None,
|
|
535
|
+
) -> SearchResult:
|
|
536
|
+
"""Search users."""
|
|
537
|
+
self._validate_required(query, "query")
|
|
538
|
+
self._validate_int(page, "page", minimum=1)
|
|
539
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
540
|
+
return self._fetch_url(
|
|
541
|
+
_BASE + URL_CONFIG["SEARCH_USERS"],
|
|
542
|
+
"GET",
|
|
543
|
+
{
|
|
544
|
+
"query": query,
|
|
545
|
+
"page": page if page is not None else 1,
|
|
546
|
+
"per_page": per_page if per_page is not None else 10,
|
|
547
|
+
},
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# ── Stats ─────────────────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
def get_stats_totals(self) -> dict[str, Any]:
|
|
553
|
+
"""Get Unsplash platform statistics totals."""
|
|
554
|
+
return self._fetch_url(_BASE + URL_CONFIG["STATS_TOTALS"], "GET")
|
|
555
|
+
|
|
556
|
+
def get_stats_month(self) -> dict[str, Any]:
|
|
557
|
+
"""Get Unsplash platform monthly statistics."""
|
|
558
|
+
return self._fetch_url(_BASE + URL_CONFIG["STATS_MONTH"], "GET")
|
|
559
|
+
|
|
560
|
+
# ── Collections ───────────────────────────────────────────────────
|
|
561
|
+
|
|
562
|
+
def list_collections(
|
|
563
|
+
self,
|
|
564
|
+
page: int | None = None,
|
|
565
|
+
per_page: int | None = None,
|
|
566
|
+
) -> list[Collection]:
|
|
567
|
+
"""List collections."""
|
|
568
|
+
self._validate_int(page, "page", minimum=1)
|
|
569
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
570
|
+
return self._fetch_url(
|
|
571
|
+
_BASE + URL_CONFIG["LIST_COLLECTIONS"],
|
|
572
|
+
"GET",
|
|
573
|
+
{
|
|
574
|
+
"page": page if page is not None else 1,
|
|
575
|
+
"per_page": per_page if per_page is not None else 10,
|
|
576
|
+
},
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def list_featured_collections(
|
|
580
|
+
self,
|
|
581
|
+
page: int | None = None,
|
|
582
|
+
per_page: int | None = None,
|
|
583
|
+
) -> list[Collection]:
|
|
584
|
+
"""List featured collections."""
|
|
585
|
+
self._validate_int(page, "page", minimum=1)
|
|
586
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
587
|
+
return self._fetch_url(
|
|
588
|
+
_BASE + URL_CONFIG["LIST_FEATURED_COLLECTIONS"],
|
|
589
|
+
"GET",
|
|
590
|
+
{
|
|
591
|
+
"page": page if page is not None else 1,
|
|
592
|
+
"per_page": per_page if per_page is not None else 10,
|
|
593
|
+
},
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
def list_curated_collections(
|
|
597
|
+
self,
|
|
598
|
+
page: int | None = None,
|
|
599
|
+
per_page: int | None = None,
|
|
600
|
+
) -> list[Collection]:
|
|
601
|
+
"""List curated collections."""
|
|
602
|
+
self._validate_int(page, "page", minimum=1)
|
|
603
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
604
|
+
return self._fetch_url(
|
|
605
|
+
_BASE + URL_CONFIG["LIST_CURATED_COLLECTIONS"],
|
|
606
|
+
"GET",
|
|
607
|
+
{
|
|
608
|
+
"page": page if page is not None else 1,
|
|
609
|
+
"per_page": per_page if per_page is not None else 10,
|
|
610
|
+
},
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
def get_collection(self, id: str) -> Collection:
|
|
614
|
+
"""Get a collection by ID."""
|
|
615
|
+
self._validate_required(id, "id")
|
|
616
|
+
return self._fetch_url(
|
|
617
|
+
self._build_url(_BASE, URL_CONFIG["GET_COLLECTION"], id=id),
|
|
618
|
+
"GET",
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
def get_curated_collection(self, id: str) -> Collection:
|
|
622
|
+
"""Get a curated collection by ID."""
|
|
623
|
+
self._validate_required(id, "id")
|
|
624
|
+
return self._fetch_url(
|
|
625
|
+
self._build_url(_BASE, URL_CONFIG["GET_CURATED_COLLECTION"], id=id),
|
|
626
|
+
"GET",
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
def get_collection_photos(
|
|
630
|
+
self,
|
|
631
|
+
id: str,
|
|
632
|
+
page: int | None = None,
|
|
633
|
+
per_page: int | None = None,
|
|
634
|
+
) -> list[Photo]:
|
|
635
|
+
"""Get a collection's photos."""
|
|
636
|
+
self._validate_required(id, "id")
|
|
637
|
+
self._validate_int(page, "page", minimum=1)
|
|
638
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
639
|
+
return self._fetch_url(
|
|
640
|
+
self._build_url(_BASE, URL_CONFIG["GET_COLLECTION_PHOTOS"], id=id),
|
|
641
|
+
"GET",
|
|
642
|
+
{
|
|
643
|
+
"page": page if page is not None else 1,
|
|
644
|
+
"per_page": per_page if per_page is not None else 10,
|
|
645
|
+
},
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
def get_curated_collection_photos(
|
|
649
|
+
self,
|
|
650
|
+
id: str,
|
|
651
|
+
page: int | None = None,
|
|
652
|
+
per_page: int | None = None,
|
|
653
|
+
) -> list[Photo]:
|
|
654
|
+
"""Get a curated collection's photos."""
|
|
655
|
+
self._validate_required(id, "id")
|
|
656
|
+
self._validate_int(page, "page", minimum=1)
|
|
657
|
+
self._validate_int(per_page, "per_page", minimum=1, maximum=30)
|
|
658
|
+
return self._fetch_url(
|
|
659
|
+
self._build_url(_BASE, URL_CONFIG["GET_CURATED_COLLECTION_PHOTOS"], id=id),
|
|
660
|
+
"GET",
|
|
661
|
+
{
|
|
662
|
+
"page": page if page is not None else 1,
|
|
663
|
+
"per_page": per_page if per_page is not None else 10,
|
|
664
|
+
},
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
def list_related_collections(self, id: str) -> list[Collection]:
|
|
668
|
+
"""List a collection's related collections."""
|
|
669
|
+
self._validate_required(id, "id")
|
|
670
|
+
return self._fetch_url(
|
|
671
|
+
self._build_url(_BASE, URL_CONFIG["LIST_RELATED_COLLECTION"], id=id),
|
|
672
|
+
"GET",
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def create_collection(
|
|
676
|
+
self,
|
|
677
|
+
title: str,
|
|
678
|
+
description: str | None = None,
|
|
679
|
+
private_collection: bool = False,
|
|
680
|
+
) -> Collection:
|
|
681
|
+
"""Create a new collection."""
|
|
682
|
+
self._validate_required(title, "title")
|
|
683
|
+
return self._fetch_url(
|
|
684
|
+
_BASE + URL_CONFIG["CREATE_NEW_COLLECTION"],
|
|
685
|
+
"POST",
|
|
686
|
+
{
|
|
687
|
+
"title": title,
|
|
688
|
+
"description": description,
|
|
689
|
+
"private": private_collection,
|
|
690
|
+
},
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
def create_new_collection(
|
|
694
|
+
self,
|
|
695
|
+
title: str,
|
|
696
|
+
description: str | None = None,
|
|
697
|
+
private_collection: bool = False,
|
|
698
|
+
) -> Collection:
|
|
699
|
+
"""Create a new collection (alias for create_collection)."""
|
|
700
|
+
return self.create_collection(title, description, private_collection)
|
|
701
|
+
|
|
702
|
+
def update_collection(
|
|
703
|
+
self,
|
|
704
|
+
id: str,
|
|
705
|
+
title: str,
|
|
706
|
+
description: str | None = None,
|
|
707
|
+
private_collection: bool = False,
|
|
708
|
+
) -> Collection:
|
|
709
|
+
"""Update an existing collection."""
|
|
710
|
+
self._validate_required(id, "id")
|
|
711
|
+
self._validate_required(title, "title")
|
|
712
|
+
return self._fetch_url(
|
|
713
|
+
self._build_url(_BASE, URL_CONFIG["UPDATE_EXISTING_COLLECTION"], id=id),
|
|
714
|
+
"PUT",
|
|
715
|
+
{
|
|
716
|
+
"title": title,
|
|
717
|
+
"description": description,
|
|
718
|
+
"private": private_collection,
|
|
719
|
+
},
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
def update_existing_collection(
|
|
723
|
+
self,
|
|
724
|
+
id: str,
|
|
725
|
+
title: str,
|
|
726
|
+
description: str | None = None,
|
|
727
|
+
private_collection: bool = False,
|
|
728
|
+
) -> Collection:
|
|
729
|
+
"""Update an existing collection (alias for update_collection)."""
|
|
730
|
+
return self.update_collection(id, title, description, private_collection)
|
|
731
|
+
|
|
732
|
+
def delete_collection(self, id: str) -> DeleteResponse:
|
|
733
|
+
"""Delete a collection."""
|
|
734
|
+
self._validate_required(id, "id")
|
|
735
|
+
return self._fetch_url(
|
|
736
|
+
self._build_url(_BASE, URL_CONFIG["DELETE_COLLECTION"], id=id),
|
|
737
|
+
"DELETE",
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
def add_photo_to_collection(self, collection_id: str, photo_id: str) -> Collection:
|
|
741
|
+
"""Add a photo to a collection."""
|
|
742
|
+
self._validate_required(collection_id, "collection_id")
|
|
743
|
+
self._validate_required(photo_id, "photo_id")
|
|
744
|
+
return self._fetch_url(
|
|
745
|
+
self._build_url(_BASE, URL_CONFIG["ADD_PHOTO_TO_COLLECTION"], collection_id=collection_id),
|
|
746
|
+
"POST",
|
|
747
|
+
{"photo_id": photo_id},
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
def remove_photo_from_collection(self, collection_id: str, photo_id: str) -> DeleteResponse:
|
|
751
|
+
"""Remove a photo from a collection."""
|
|
752
|
+
self._validate_required(collection_id, "collection_id")
|
|
753
|
+
self._validate_required(photo_id, "photo_id")
|
|
754
|
+
return self._fetch_url(
|
|
755
|
+
self._build_url(_BASE, URL_CONFIG["REMOVE_PHOTO_FROM_COLLECTION"], collection_id=collection_id),
|
|
756
|
+
"DELETE",
|
|
757
|
+
{"photo_id": photo_id},
|
|
758
|
+
)
|