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/base.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Shared utilities for sync and async API clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .errors import PySplashError
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("pySplash.py")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PySplashBase:
|
|
15
|
+
"""Shared validation, error handling, and URL construction for both sync and async clients."""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _compute_hash(value: str) -> str:
|
|
19
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _validate_required(value: Any, field_name: str) -> None:
|
|
23
|
+
if value is None or value == "":
|
|
24
|
+
if field_name == "id":
|
|
25
|
+
message = "Parameter : id is required!"
|
|
26
|
+
elif field_name == "query":
|
|
27
|
+
message = "Parameter : query is missing!"
|
|
28
|
+
else:
|
|
29
|
+
message = f"Parameter : {field_name} is required and cannot be empty!"
|
|
30
|
+
raise PySplashError(message)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _validate_supported_value(value: str | None, allowed_values: list, field_name: str) -> None:
|
|
34
|
+
if value is not None and value not in allowed_values:
|
|
35
|
+
raise PySplashError(f"Parameter : {field_name} has an unsupported value!")
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _validate_int(value: Any, field_name: str, minimum: int = 1, maximum: int | None = None) -> None:
|
|
39
|
+
if value is not None:
|
|
40
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
41
|
+
raise PySplashError(f"Parameter : {field_name} must be an integer!")
|
|
42
|
+
if value < minimum:
|
|
43
|
+
raise PySplashError(f"Parameter : {field_name} must be >= {minimum}!")
|
|
44
|
+
if maximum is not None and value > maximum:
|
|
45
|
+
raise PySplashError(f"Parameter : {field_name} must be <= {maximum}!")
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _validate_bool(value: Any, field_name: str) -> None:
|
|
49
|
+
if value is not None and not isinstance(value, bool):
|
|
50
|
+
raise PySplashError(f"Parameter : {field_name} must be a boolean or optional!")
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _build_query_parameters(params: dict[str, Any]) -> dict[str, Any]:
|
|
54
|
+
return {k: v for k, v in params.items() if v is not None and v != ""}
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _build_url(base: str, path: str, **replacements: str) -> str:
|
|
58
|
+
url = base + path
|
|
59
|
+
for key, value in replacements.items():
|
|
60
|
+
url = url.replace(f":{key}", value)
|
|
61
|
+
return url
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _handle_response_status(data: Any, status: int, status_text: str) -> Any:
|
|
65
|
+
if status == 204:
|
|
66
|
+
logger.debug("204 No Content - resource deleted")
|
|
67
|
+
return {"status": status, "statusText": status_text, "message": "Content Deleted"}
|
|
68
|
+
if status >= 400:
|
|
69
|
+
logger.warning("%d %s", status, status_text)
|
|
70
|
+
raise PySplashError(
|
|
71
|
+
f"HTTP {status} {status_text}",
|
|
72
|
+
status_code=status,
|
|
73
|
+
status_text=status_text,
|
|
74
|
+
)
|
|
75
|
+
return data
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _create_pysplash_error(error: Exception) -> PySplashError:
|
|
79
|
+
if isinstance(error, PySplashError):
|
|
80
|
+
return error
|
|
81
|
+
status_code = getattr(getattr(error, "response", None), "status_code", None)
|
|
82
|
+
status_text = getattr(
|
|
83
|
+
getattr(error, "response", None),
|
|
84
|
+
"reason",
|
|
85
|
+
getattr(getattr(error, "response", None), "reason_phrase", None),
|
|
86
|
+
)
|
|
87
|
+
return PySplashError(
|
|
88
|
+
str(error),
|
|
89
|
+
status_code=status_code,
|
|
90
|
+
status_text=status_text,
|
|
91
|
+
cause=error,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _default_headers(bearer_token: str | None = None, access_key: str | None = None) -> dict[str, str]:
|
|
96
|
+
headers: dict[str, str] = {
|
|
97
|
+
"Content-type": "application/json",
|
|
98
|
+
"X-Requested-With": "PySplash.py",
|
|
99
|
+
}
|
|
100
|
+
if bearer_token:
|
|
101
|
+
headers["Authorization"] = f"Bearer {bearer_token}"
|
|
102
|
+
headers["X-PySplash-Header"] = PySplashBase._compute_hash(bearer_token)
|
|
103
|
+
elif access_key:
|
|
104
|
+
headers["Authorization"] = f"Client-ID {access_key}"
|
|
105
|
+
headers["X-PySplash-Header"] = PySplashBase._compute_hash(access_key)
|
|
106
|
+
return headers
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _extract_location_params(location: dict[str, Any] | None) -> dict[str, Any]:
|
|
110
|
+
if not location:
|
|
111
|
+
return {}
|
|
112
|
+
result: dict[str, Any] = {}
|
|
113
|
+
mapping = {
|
|
114
|
+
"latitude": "location[latitude]",
|
|
115
|
+
"longitude": "location[longitude]",
|
|
116
|
+
"name": "location[name]",
|
|
117
|
+
"city": "location[city]",
|
|
118
|
+
"country": "location[country]",
|
|
119
|
+
"confidential": "location[confidential]",
|
|
120
|
+
}
|
|
121
|
+
for key, param_name in mapping.items():
|
|
122
|
+
if key in location and location[key] is not None:
|
|
123
|
+
result[param_name] = location[key]
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _extract_exif_params(exif: dict[str, Any] | None) -> dict[str, Any]:
|
|
128
|
+
if not exif:
|
|
129
|
+
return {}
|
|
130
|
+
result: dict[str, Any] = {}
|
|
131
|
+
mapping = {
|
|
132
|
+
"make": "exif[make]",
|
|
133
|
+
"model": "exif[model]",
|
|
134
|
+
"exposure_time": "exif[exposure_time]",
|
|
135
|
+
"aperture_value": "exif[aperture_value]",
|
|
136
|
+
"focal_length": "exif[focal_length]",
|
|
137
|
+
"iso_speed_ratings": "exif[iso_speed_ratings]",
|
|
138
|
+
}
|
|
139
|
+
for key, param_name in mapping.items():
|
|
140
|
+
if key in exif and exif[key] is not None:
|
|
141
|
+
result[param_name] = exif[key]
|
|
142
|
+
return result
|
pySplash/config.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Configuration for pySplash.py - URL templates and default HTTP settings."""
|
|
2
|
+
|
|
3
|
+
API_LOCATION = "https://api.unsplash.com/"
|
|
4
|
+
BEARER_TOKEN_URL = "https://unsplash.com/oauth/token"
|
|
5
|
+
|
|
6
|
+
URL_CONFIG = {
|
|
7
|
+
"API_LOCATION": API_LOCATION,
|
|
8
|
+
"BEARER_TOKEN_URL": BEARER_TOKEN_URL,
|
|
9
|
+
"USERS_PUBLIC_PROFILE": "users/",
|
|
10
|
+
"USERS_PORTFOLIO": "users/:username/portfolio",
|
|
11
|
+
"USERS_PHOTOS": "users/:username/photos",
|
|
12
|
+
"USERS_LIKED_PHOTOS": "users/:username/likes",
|
|
13
|
+
"USERS_COLLECTIONS": "users/:username/collections",
|
|
14
|
+
"USERS_STATISTICS": "users/:username/statistics",
|
|
15
|
+
"LIST_PHOTOS": "photos",
|
|
16
|
+
"LIST_CURATED_PHOTOS": "photos/curated",
|
|
17
|
+
"GET_A_PHOTO": "photos/:id",
|
|
18
|
+
"GET_A_RANDOM_PHOTO": "photos/random",
|
|
19
|
+
"GET_A_PHOTO_STATISTICS": "photos/:id/statistics",
|
|
20
|
+
"GET_A_PHOTO_DOWNLOAD_LINK": "photos/:id/download",
|
|
21
|
+
"UPDATE_A_PHOTO": "photos/:id",
|
|
22
|
+
"LIKE_A_PHOTO": "photos/:id/like",
|
|
23
|
+
"UNLIKE_A_PHOTO": "photos/:id/like",
|
|
24
|
+
"SEARCH_PHOTOS": "search/photos",
|
|
25
|
+
"SEARCH_COLLECTIONS": "search/collections",
|
|
26
|
+
"SEARCH_USERS": "search/users",
|
|
27
|
+
"CURRENT_USER_PROFILE": "me",
|
|
28
|
+
"UPDATE_CURRENT_USER_PROFILE": "me",
|
|
29
|
+
"STATS_TOTALS": "stats/total",
|
|
30
|
+
"STATS_MONTH": "stats/month",
|
|
31
|
+
"LIST_COLLECTIONS": "collections",
|
|
32
|
+
"LIST_FEATURED_COLLECTIONS": "collections/featured",
|
|
33
|
+
"LIST_CURATED_COLLECTIONS": "collections/curated",
|
|
34
|
+
"GET_COLLECTION": "collections/:id",
|
|
35
|
+
"GET_CURATED_COLLECTION": "collections/curated/:id",
|
|
36
|
+
"GET_COLLECTION_PHOTOS": "collections/:id/photos",
|
|
37
|
+
"GET_CURATED_COLLECTION_PHOTOS": "collections/curated/:id/photos",
|
|
38
|
+
"LIST_RELATED_COLLECTION": "collections/:id/related",
|
|
39
|
+
"CREATE_NEW_COLLECTION": "collections",
|
|
40
|
+
"UPDATE_EXISTING_COLLECTION": "collections/:id",
|
|
41
|
+
"DELETE_COLLECTION": "collections/:id",
|
|
42
|
+
"ADD_PHOTO_TO_COLLECTION": "collections/:collection_id/add",
|
|
43
|
+
"REMOVE_PHOTO_FROM_COLLECTION": "collections/:collection_id/remove",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
DEFAULT_TIMEOUT = 10
|
|
47
|
+
DEFAULT_RETRIES = 2
|
|
48
|
+
DEFAULT_RETRY_DELAY = 0.1
|
|
49
|
+
DEFAULT_HEADERS = {
|
|
50
|
+
"Content-type": "application/json",
|
|
51
|
+
"X-Requested-With": "PySplash.py",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
AVAILABLE_ORDERS = ["latest", "oldest", "popular"]
|
|
55
|
+
AVAILABLE_ORIENTATIONS = ["landscape", "portrait", "squarish"]
|
pySplash/errors.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Custom exception class for pySplash.py errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PySplashError(Exception):
|
|
9
|
+
"""Exception raised by pySplash.py for API and validation errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
status_code: int | None = None,
|
|
15
|
+
status_text: str | None = None,
|
|
16
|
+
cause: Any | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.name = "PySplashError"
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.statusText = status_text
|
|
22
|
+
self.cause = cause
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
parts = [f"PySplashError({self.args[0]!r}"]
|
|
26
|
+
if self.status_code is not None:
|
|
27
|
+
parts.append(f"status_code={self.status_code}")
|
|
28
|
+
if self.statusText is not None:
|
|
29
|
+
parts.append(f"status_text={self.statusText!r}")
|
|
30
|
+
return ", ".join(parts) + ")"
|
pySplash/http_client.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Synchronous HTTP client with retry support using requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .config import DEFAULT_HEADERS, DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, DEFAULT_TIMEOUT
|
|
12
|
+
from .errors import PySplashError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("pySplash.py")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpResponse:
|
|
18
|
+
"""Wrapper around requests.Response to provide a uniform interface."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, response: requests.Response) -> None:
|
|
21
|
+
self.status: int = response.status_code
|
|
22
|
+
self.statusText: str = response.reason or ""
|
|
23
|
+
try:
|
|
24
|
+
self.data: Any = response.json() if response.content else None
|
|
25
|
+
except ValueError:
|
|
26
|
+
self.data = response.text or None
|
|
27
|
+
|
|
28
|
+
def raise_for_status(self) -> None:
|
|
29
|
+
if self.status >= 400:
|
|
30
|
+
raise PySplashError(
|
|
31
|
+
f"HTTP {self.status} {self.statusText}",
|
|
32
|
+
status_code=self.status,
|
|
33
|
+
status_text=self.statusText,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HttpClient:
|
|
38
|
+
"""Sync HTTP client with session reuse, retry logic, and error handling."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
headers: dict[str, str] | None = None,
|
|
43
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
44
|
+
retries: int = DEFAULT_RETRIES,
|
|
45
|
+
retry_delay: float = DEFAULT_RETRY_DELAY,
|
|
46
|
+
) -> None:
|
|
47
|
+
self._session = requests.Session()
|
|
48
|
+
self._session.headers.update(headers or DEFAULT_HEADERS)
|
|
49
|
+
self.timeout = timeout
|
|
50
|
+
self.retries = retries
|
|
51
|
+
self.retry_delay = retry_delay
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
self._session.close()
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> HttpClient:
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __exit__(self, *args: Any) -> None:
|
|
60
|
+
self.close()
|
|
61
|
+
|
|
62
|
+
def make_request(
|
|
63
|
+
self,
|
|
64
|
+
url: str,
|
|
65
|
+
method: str,
|
|
66
|
+
query_parameters: dict[str, Any] | None = None,
|
|
67
|
+
body: Any | None = None,
|
|
68
|
+
) -> HttpResponse:
|
|
69
|
+
if not url:
|
|
70
|
+
raise ValueError("URL required")
|
|
71
|
+
|
|
72
|
+
last_error: Exception | None = None
|
|
73
|
+
|
|
74
|
+
for attempt in range(self.retries + 1):
|
|
75
|
+
try:
|
|
76
|
+
logger.debug("%s %s (attempt %d/%d)", method, url, attempt + 1, self.retries + 1)
|
|
77
|
+
response = self._session.request(
|
|
78
|
+
method=method or "get",
|
|
79
|
+
url=url,
|
|
80
|
+
params=query_parameters or {},
|
|
81
|
+
json=body,
|
|
82
|
+
timeout=self.timeout,
|
|
83
|
+
allow_redirects=True,
|
|
84
|
+
)
|
|
85
|
+
result = HttpResponse(response)
|
|
86
|
+
if result.status >= 400:
|
|
87
|
+
result.raise_for_status()
|
|
88
|
+
return result
|
|
89
|
+
except PySplashError:
|
|
90
|
+
raise
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
last_error = exc
|
|
93
|
+
logger.warning("Request failed (attempt %d/%d): %s", attempt + 1, self.retries + 1, exc)
|
|
94
|
+
if attempt < self.retries and self.retry_delay > 0:
|
|
95
|
+
time.sleep(self.retry_delay)
|
|
96
|
+
|
|
97
|
+
raise last_error # type: ignore[misc]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Asynchronous HTTP client with retry support using httpx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .config import DEFAULT_HEADERS, DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, DEFAULT_TIMEOUT
|
|
12
|
+
from .errors import PySplashError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("pySplash.py")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AsyncHttpResponse:
|
|
18
|
+
"""Wrapper around httpx.Response to provide a uniform interface."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, response: httpx.Response) -> None:
|
|
21
|
+
self.status: int = response.status_code
|
|
22
|
+
self.statusText: str = response.reason_phrase or ""
|
|
23
|
+
try:
|
|
24
|
+
self.data: Any = response.json() if response.content else None
|
|
25
|
+
except ValueError:
|
|
26
|
+
self.data = response.text or None
|
|
27
|
+
|
|
28
|
+
def raise_for_status(self) -> None:
|
|
29
|
+
if self.status >= 400:
|
|
30
|
+
raise PySplashError(
|
|
31
|
+
f"HTTP {self.status} {self.statusText}",
|
|
32
|
+
status_code=self.status,
|
|
33
|
+
status_text=self.statusText,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AsyncHttpClient:
|
|
38
|
+
"""Async HTTP client with connection pooling, retry logic, and error handling."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
headers: dict[str, str] | None = None,
|
|
43
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
44
|
+
retries: int = DEFAULT_RETRIES,
|
|
45
|
+
retry_delay: float = DEFAULT_RETRY_DELAY,
|
|
46
|
+
) -> None:
|
|
47
|
+
self._headers = headers or DEFAULT_HEADERS
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
self.retries = retries
|
|
50
|
+
self.retry_delay = retry_delay
|
|
51
|
+
self._client: httpx.AsyncClient | None = None
|
|
52
|
+
|
|
53
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
54
|
+
if self._client is None or self._client.is_closed:
|
|
55
|
+
self._client = httpx.AsyncClient(
|
|
56
|
+
headers=self._headers,
|
|
57
|
+
timeout=self.timeout,
|
|
58
|
+
max_redirects=5,
|
|
59
|
+
)
|
|
60
|
+
return self._client
|
|
61
|
+
|
|
62
|
+
async def close(self) -> None:
|
|
63
|
+
if self._client is not None and not self._client.is_closed:
|
|
64
|
+
await self._client.aclose()
|
|
65
|
+
self._client = None
|
|
66
|
+
|
|
67
|
+
async def __aenter__(self) -> AsyncHttpClient:
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
71
|
+
await self.close()
|
|
72
|
+
|
|
73
|
+
async def make_request(
|
|
74
|
+
self,
|
|
75
|
+
url: str,
|
|
76
|
+
method: str,
|
|
77
|
+
query_parameters: dict[str, Any] | None = None,
|
|
78
|
+
body: Any | None = None,
|
|
79
|
+
) -> AsyncHttpResponse:
|
|
80
|
+
if not url:
|
|
81
|
+
raise ValueError("URL required")
|
|
82
|
+
|
|
83
|
+
last_error: Exception | None = None
|
|
84
|
+
|
|
85
|
+
for attempt in range(self.retries + 1):
|
|
86
|
+
try:
|
|
87
|
+
logger.debug("%s %s (attempt %d/%d)", method, url, attempt + 1, self.retries + 1)
|
|
88
|
+
client = self._get_client()
|
|
89
|
+
response = await client.request(
|
|
90
|
+
method=method or "get",
|
|
91
|
+
url=url,
|
|
92
|
+
params=query_parameters or {},
|
|
93
|
+
json=body,
|
|
94
|
+
)
|
|
95
|
+
result = AsyncHttpResponse(response)
|
|
96
|
+
if result.status >= 400:
|
|
97
|
+
result.raise_for_status()
|
|
98
|
+
return result
|
|
99
|
+
except PySplashError:
|
|
100
|
+
raise
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
last_error = exc
|
|
103
|
+
logger.warning("Request failed (attempt %d/%d): %s", attempt + 1, self.retries + 1, exc)
|
|
104
|
+
if attempt < self.retries and self.retry_delay > 0:
|
|
105
|
+
await asyncio.sleep(self.retry_delay)
|
|
106
|
+
|
|
107
|
+
raise last_error # type: ignore[misc]
|
pySplash/models.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Typed response models for the Unsplash API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserLinks(TypedDict, total=False):
|
|
9
|
+
self: str
|
|
10
|
+
html: str
|
|
11
|
+
photos: str
|
|
12
|
+
likes: str
|
|
13
|
+
portfolio: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserUrls(TypedDict, total=False):
|
|
17
|
+
raw: str
|
|
18
|
+
full: str
|
|
19
|
+
regular: str
|
|
20
|
+
small: str
|
|
21
|
+
medium: str
|
|
22
|
+
large: str
|
|
23
|
+
profile_image: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UserProfileImages(TypedDict, total=False):
|
|
27
|
+
small: str
|
|
28
|
+
medium: str
|
|
29
|
+
large: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class User(TypedDict, total=False):
|
|
33
|
+
id: str
|
|
34
|
+
username: str
|
|
35
|
+
name: str
|
|
36
|
+
first_name: str
|
|
37
|
+
last_name: str
|
|
38
|
+
instagram_username: str
|
|
39
|
+
total_likes: int
|
|
40
|
+
total_photos: int
|
|
41
|
+
total_collections: int
|
|
42
|
+
followers_count: int
|
|
43
|
+
following_count: int
|
|
44
|
+
downloads: int
|
|
45
|
+
bio: str | None
|
|
46
|
+
location: str | None
|
|
47
|
+
total_promoted_photos: int
|
|
48
|
+
followers: int
|
|
49
|
+
following: int
|
|
50
|
+
profile_image: UserProfileImages
|
|
51
|
+
social: dict[str, Any]
|
|
52
|
+
links: UserLinks
|
|
53
|
+
tags: list[dict[str, Any]]
|
|
54
|
+
photos: list[dict[str, Any]]
|
|
55
|
+
portfolio_url: str | None
|
|
56
|
+
updated_at: str
|
|
57
|
+
badge: dict[str, Any]
|
|
58
|
+
user_links: UserLinks
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UserPortfolio(TypedDict, total=False):
|
|
62
|
+
id: str
|
|
63
|
+
title: str
|
|
64
|
+
url: str
|
|
65
|
+
published_at: str
|
|
66
|
+
updated_at: str
|
|
67
|
+
links: dict[str, str]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class UserStatistics(TypedDict, total=False):
|
|
71
|
+
id: str
|
|
72
|
+
username: str
|
|
73
|
+
downloads: dict[str, Any]
|
|
74
|
+
views: dict[str, Any]
|
|
75
|
+
likes: dict[str, Any]
|
|
76
|
+
historical: dict[str, Any]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Urls(TypedDict, total=False):
|
|
80
|
+
raw: str
|
|
81
|
+
full: str
|
|
82
|
+
regular: str
|
|
83
|
+
small: str
|
|
84
|
+
thumb: str
|
|
85
|
+
small_s3: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PhotoLinks(TypedDict, total=False):
|
|
89
|
+
self: str
|
|
90
|
+
html: str
|
|
91
|
+
download: str
|
|
92
|
+
download_location: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Photo(TypedDict, total=False):
|
|
96
|
+
id: str
|
|
97
|
+
slug: str
|
|
98
|
+
created_at: str
|
|
99
|
+
updated_at: str
|
|
100
|
+
promoted_at: str | None
|
|
101
|
+
width: int
|
|
102
|
+
height: int
|
|
103
|
+
color: str
|
|
104
|
+
blur_hash: str
|
|
105
|
+
description: str | None
|
|
106
|
+
alt_description: str | None
|
|
107
|
+
breadcrumbs: list[dict[str, Any]]
|
|
108
|
+
urls: Urls
|
|
109
|
+
links: PhotoLinks
|
|
110
|
+
likes: int
|
|
111
|
+
liked_by_user: bool
|
|
112
|
+
current_user_collections: list[dict[str, Any]]
|
|
113
|
+
user: User
|
|
114
|
+
exif: dict[str, Any]
|
|
115
|
+
location: dict[str, Any]
|
|
116
|
+
views: int
|
|
117
|
+
downloads: int
|
|
118
|
+
related_collections: dict[str, Any]
|
|
119
|
+
tags: list[dict[str, Any]]
|
|
120
|
+
sponsorship: dict[str, Any] | None
|
|
121
|
+
topic_submissions: dict[str, Any]
|
|
122
|
+
user_links: UserLinks
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PhotoStatistics(TypedDict, total=False):
|
|
126
|
+
id: str
|
|
127
|
+
downloads: dict[str, Any]
|
|
128
|
+
views: dict[str, Any]
|
|
129
|
+
likes: dict[str, Any]
|
|
130
|
+
historical: dict[str, Any]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class CollectionLinks(TypedDict, total=False):
|
|
134
|
+
self: str
|
|
135
|
+
html: str
|
|
136
|
+
photos: str
|
|
137
|
+
related: str
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Collection(TypedDict, total=False):
|
|
141
|
+
id: str
|
|
142
|
+
title: str
|
|
143
|
+
description: str | None
|
|
144
|
+
published_at: str
|
|
145
|
+
updated_at: str
|
|
146
|
+
curated: bool
|
|
147
|
+
featured: bool
|
|
148
|
+
total_photos: int
|
|
149
|
+
private: bool
|
|
150
|
+
share_key: str
|
|
151
|
+
tags: list[dict[str, Any]]
|
|
152
|
+
links: CollectionLinks
|
|
153
|
+
user: User
|
|
154
|
+
cover_photo: Photo
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SearchResult(TypedDict, total=False):
|
|
158
|
+
total: int
|
|
159
|
+
total_pages: int
|
|
160
|
+
results: list[dict[str, Any]]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class BearerTokenResponse(TypedDict, total=False):
|
|
164
|
+
access_token: str
|
|
165
|
+
token_type: str
|
|
166
|
+
scope: str
|
|
167
|
+
created_at: int
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class DeleteResponse(TypedDict, total=False):
|
|
171
|
+
status: int
|
|
172
|
+
statusText: str
|
|
173
|
+
message: str
|
pySplash/py.typed
ADDED
|
File without changes
|