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