mediaviz-sdk 1.0.5.81.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mediaviz_sdk-1.0.5.81.dev0/LICENSE +21 -0
- mediaviz_sdk-1.0.5.81.dev0/PKG-INFO +7 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/__init__.py +20 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/admin.py +16 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/ai_model_credits.py +16 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/client.py +141 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/company.py +33 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/curated_albums.py +110 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/custom_albums.py +104 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/email_tokens.py +63 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/errors.py +70 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/health.py +29 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/keywords.py +165 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/o_auth_authorization.py +75 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/o_auth_token.py +51 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/oauth_login.py +33 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/person.py +83 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/photo_upload.py +150 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/photos.py +140 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/projects.py +197 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/search.py +258 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/users.py +127 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/PKG-INFO +7 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/SOURCES.txt +33 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/dependency_links.txt +1 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/requires.txt +1 -0
- mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/top_level.txt +2 -0
- mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/__init__.py +25 -0
- mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_client.py +232 -0
- mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_errors.py +26 -0
- mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_http.py +43 -0
- mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_pkce.py +16 -0
- mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_types.py +77 -0
- mediaviz_sdk-1.0.5.81.dev0/pyproject.toml +12 -0
- mediaviz_sdk-1.0.5.81.dev0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MediaViz
|
|
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.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .client import MediaVizClient
|
|
4
|
+
from .errors import ApiError, ValidationError, NotFoundError, RateLimitError, ServerError
|
|
5
|
+
from oauth_sdk import OAuthClient, OAuthError, AuthenticatedResponse, AuthorizationUrlResult, ClientRegistrationRequest, ClientRegistrationResponse, OAuthClientConfig, OAuthErrorCode, TokenPayload, TokenResponse
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'MediaVizClient',
|
|
9
|
+
'ApiError', 'ValidationError', 'NotFoundError', 'RateLimitError', 'ServerError',
|
|
10
|
+
'OAuthClient',
|
|
11
|
+
'OAuthError',
|
|
12
|
+
'AuthenticatedResponse',
|
|
13
|
+
'AuthorizationUrlResult',
|
|
14
|
+
'ClientRegistrationRequest',
|
|
15
|
+
'ClientRegistrationResponse',
|
|
16
|
+
'OAuthClientConfig',
|
|
17
|
+
'OAuthErrorCode',
|
|
18
|
+
'TokenPayload',
|
|
19
|
+
'TokenResponse',
|
|
20
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote, urlencode
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .errors import handle_response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Admin:
|
|
10
|
+
def __init__(self, ctx) -> None:
|
|
11
|
+
self._ctx = ctx
|
|
12
|
+
|
|
13
|
+
def get_category_labels(self, category: str) -> dict[str, Any]:
|
|
14
|
+
self._ctx.require_tokens()
|
|
15
|
+
path = '/api/v1/admin/category_labels/' + quote(str(category), safe='')
|
|
16
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote, urlencode
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .errors import handle_response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AiModelCredits:
|
|
10
|
+
def __init__(self, ctx) -> None:
|
|
11
|
+
self._ctx = ctx
|
|
12
|
+
|
|
13
|
+
def get_model_credit_relationship(self, model_name: str) -> dict[str, Any]:
|
|
14
|
+
self._ctx.require_tokens()
|
|
15
|
+
path = '/api/v1/model_credit/' + quote(str(model_name), safe='')
|
|
16
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from oauth_sdk import OAuthClient, OAuthClientConfig
|
|
6
|
+
from .admin import Admin
|
|
7
|
+
from .ai_model_credits import AiModelCredits
|
|
8
|
+
from .company import Company
|
|
9
|
+
from .curated_albums import CuratedAlbums
|
|
10
|
+
from .custom_albums import CustomAlbums
|
|
11
|
+
from .email_tokens import EmailTokens
|
|
12
|
+
from .health import Health
|
|
13
|
+
from .keywords import Keywords
|
|
14
|
+
from .o_auth_authorization import OAuthAuthorization
|
|
15
|
+
from .o_auth_token import OAuthToken
|
|
16
|
+
from .oauth_login import OauthLogin
|
|
17
|
+
from .person import Person
|
|
18
|
+
from .photo_upload import PhotoUpload
|
|
19
|
+
from .photos import Photos
|
|
20
|
+
from .projects import Projects
|
|
21
|
+
from .search import Search
|
|
22
|
+
from .users import Users
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Context:
|
|
26
|
+
def __init__(self, mv: "MediaVizClient") -> None:
|
|
27
|
+
self._mv = mv
|
|
28
|
+
self.client = mv._tracking_client
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def access_token(self) -> str | None: return self._mv._access_token
|
|
32
|
+
@property
|
|
33
|
+
def refresh_token(self) -> str | None: return self._mv._refresh_token
|
|
34
|
+
@property
|
|
35
|
+
def base_url(self) -> str: return self._mv._config['base_url']
|
|
36
|
+
|
|
37
|
+
def require_host(self, key: str) -> str:
|
|
38
|
+
url = self._mv._hosts.get(key)
|
|
39
|
+
if url is None:
|
|
40
|
+
raise RuntimeError(f"Host '{key}' not configured.")
|
|
41
|
+
return url
|
|
42
|
+
|
|
43
|
+
def require_tokens(self) -> None:
|
|
44
|
+
if self._mv._access_token is None:
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
'Not authenticated. Call authenticate(), handle_callback(), or set_tokens() first.'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _TokenTrackingClient:
|
|
51
|
+
def __init__(self, mv: "MediaVizClient", inner: Any) -> None:
|
|
52
|
+
self._mv = mv
|
|
53
|
+
self._inner = inner
|
|
54
|
+
|
|
55
|
+
def request(self, url: str, method: str, access_token: str, refresh_token: str, body: Any = None) -> Any:
|
|
56
|
+
def _on_refresh_success(new_tokens: Any) -> None:
|
|
57
|
+
self._mv.set_tokens(new_tokens.access_token, new_tokens.refresh_token)
|
|
58
|
+
if self._mv._on_token_refresh:
|
|
59
|
+
self._mv._on_token_refresh(new_tokens)
|
|
60
|
+
return self._inner.request(
|
|
61
|
+
url, method, access_token, refresh_token, body,
|
|
62
|
+
on_refresh_success=_on_refresh_success,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __getattr__(self, name: str) -> Any:
|
|
66
|
+
return getattr(self._inner, name)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MediaVizClient:
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
client_id: str | None = None,
|
|
74
|
+
client_secret: str | None = None,
|
|
75
|
+
base_url: str | None = None,
|
|
76
|
+
redirect_uri: str | None = None,
|
|
77
|
+
hosts: dict[str, str] | None = None,
|
|
78
|
+
access_token: str | None = None,
|
|
79
|
+
refresh_token: str | None = None,
|
|
80
|
+
on_token_refresh=None,
|
|
81
|
+
) -> None:
|
|
82
|
+
self._config = {
|
|
83
|
+
'client_id': client_id or os.environ.get('MEDIAVIZ_CLIENT_ID'),
|
|
84
|
+
'client_secret': client_secret or os.environ.get('MEDIAVIZ_CLIENT_SECRET'),
|
|
85
|
+
'base_url': base_url or os.environ.get('MEDIAVIZ_BASE_URL') or 'https://api.mediaviz.ai',
|
|
86
|
+
'redirect_uri': redirect_uri or os.environ.get('MEDIAVIZ_REDIRECT_URI'),
|
|
87
|
+
}
|
|
88
|
+
_h = hosts or {}
|
|
89
|
+
self._hosts: dict[str, str | None] = {
|
|
90
|
+
'photoUpload': _h.get('photoUpload') or os.environ.get('MEDIAVIZ_PHOTO_UPLOAD_URL'),
|
|
91
|
+
}
|
|
92
|
+
self._hosts.update(_h)
|
|
93
|
+
self._access_token: str | None = access_token
|
|
94
|
+
self._refresh_token: str | None = refresh_token
|
|
95
|
+
self._on_token_refresh = on_token_refresh
|
|
96
|
+
|
|
97
|
+
_inner = OAuthClient(OAuthClientConfig(
|
|
98
|
+
base_url=self._config['base_url'],
|
|
99
|
+
client_id=self._config['client_id'] or '',
|
|
100
|
+
client_secret=self._config['client_secret'] or '',
|
|
101
|
+
redirect_uri=self._config['redirect_uri'] or '',
|
|
102
|
+
))
|
|
103
|
+
self._tracking_client = _TokenTrackingClient(self, _inner)
|
|
104
|
+
|
|
105
|
+
_ctx = _Context(self)
|
|
106
|
+
self.admin = Admin(_ctx)
|
|
107
|
+
self.ai_model_credits = AiModelCredits(_ctx)
|
|
108
|
+
self.company = Company(_ctx)
|
|
109
|
+
self.curated_albums = CuratedAlbums(_ctx)
|
|
110
|
+
self.custom_albums = CustomAlbums(_ctx)
|
|
111
|
+
self.email_tokens = EmailTokens(_ctx)
|
|
112
|
+
self.health = Health(_ctx)
|
|
113
|
+
self.keywords = Keywords(_ctx)
|
|
114
|
+
self.o_auth_authorization = OAuthAuthorization(_ctx)
|
|
115
|
+
self.o_auth_token = OAuthToken(_ctx)
|
|
116
|
+
self.oauth_login = OauthLogin(_ctx)
|
|
117
|
+
self.person = Person(_ctx)
|
|
118
|
+
self.photo_upload = PhotoUpload(_ctx)
|
|
119
|
+
self.photos = Photos(_ctx)
|
|
120
|
+
self.projects = Projects(_ctx)
|
|
121
|
+
self.search = Search(_ctx)
|
|
122
|
+
self.users = Users(_ctx)
|
|
123
|
+
|
|
124
|
+
def authenticate(self) -> Any:
|
|
125
|
+
tokens = self._tracking_client._inner.get_client_credentials_token()
|
|
126
|
+
self._access_token = tokens.access_token
|
|
127
|
+
self._refresh_token = getattr(tokens, 'refresh_token', None)
|
|
128
|
+
return tokens
|
|
129
|
+
|
|
130
|
+
def get_authorization_url(self, state: str | None = None) -> Any:
|
|
131
|
+
return self._tracking_client._inner.generate_authorization_url(state)
|
|
132
|
+
|
|
133
|
+
def handle_callback(self, code: str, code_verifier: str) -> Any:
|
|
134
|
+
tokens = self._tracking_client._inner.exchange_code(code, code_verifier)
|
|
135
|
+
self._access_token = tokens.access_token
|
|
136
|
+
self._refresh_token = tokens.refresh_token
|
|
137
|
+
return tokens
|
|
138
|
+
|
|
139
|
+
def set_tokens(self, access_token: str, refresh_token: str) -> None:
|
|
140
|
+
self._access_token = access_token
|
|
141
|
+
self._refresh_token = refresh_token
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote, urlencode
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .errors import handle_response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Company:
|
|
10
|
+
def __init__(self, ctx) -> None:
|
|
11
|
+
self._ctx = ctx
|
|
12
|
+
|
|
13
|
+
def get_company_by_id(self, company_id: int) -> dict[str, Any]:
|
|
14
|
+
self._ctx.require_tokens()
|
|
15
|
+
path = '/api/v1/company/' + quote(str(company_id), safe='')
|
|
16
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
17
|
+
|
|
18
|
+
def confirm_company_credit_balance(
|
|
19
|
+
self,
|
|
20
|
+
company_id: int,
|
|
21
|
+
photo_count: int | None = None,
|
|
22
|
+
models_list: Any | None = None,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
self._ctx.require_tokens()
|
|
25
|
+
path = '/api/v1/company/credit_balance/' + quote(str(company_id), safe='')
|
|
26
|
+
_q: dict[str, Any] = {}
|
|
27
|
+
if photo_count is not None:
|
|
28
|
+
_q['photo_count'] = photo_count
|
|
29
|
+
if models_list is not None:
|
|
30
|
+
_q['models_list'] = models_list if isinstance(models_list, (list, tuple)) else [models_list]
|
|
31
|
+
if _q:
|
|
32
|
+
path += '?' + urlencode(_q, doseq=True)
|
|
33
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote, urlencode
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .errors import handle_response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CuratedAlbums:
|
|
10
|
+
def __init__(self, ctx) -> None:
|
|
11
|
+
self._ctx = ctx
|
|
12
|
+
|
|
13
|
+
def create_curated_album(
|
|
14
|
+
self,
|
|
15
|
+
project_table_name: str,
|
|
16
|
+
name: str,
|
|
17
|
+
description: str | None = None,
|
|
18
|
+
confidenceValue: float | None = None,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
self._ctx.require_tokens()
|
|
21
|
+
path = '/api/v1/curated_album/project/' + quote(str(project_table_name), safe='')
|
|
22
|
+
body = {k: v for k, v in {
|
|
23
|
+
'name': name,
|
|
24
|
+
'description': description,
|
|
25
|
+
'confidence_value': confidenceValue,
|
|
26
|
+
}.items() if v is not None}
|
|
27
|
+
return self._ctx.client.request(path, 'POST', self._ctx.access_token, self._ctx.refresh_token, body).data
|
|
28
|
+
|
|
29
|
+
def get_all_project_curated_albums(self, project_table_name: str) -> dict[str, Any]:
|
|
30
|
+
self._ctx.require_tokens()
|
|
31
|
+
path = '/api/v1/curated_album/project/' + quote(str(project_table_name), safe='')
|
|
32
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
33
|
+
|
|
34
|
+
def get_curated_album_photos(
|
|
35
|
+
self,
|
|
36
|
+
album_id: int,
|
|
37
|
+
asc_or_desc: str | None = None,
|
|
38
|
+
last_id: Any | None = None,
|
|
39
|
+
limit: Any | None = None,
|
|
40
|
+
confidence_value: Any | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
self._ctx.require_tokens()
|
|
43
|
+
path = '/api/v1/curated_album/photos/' + quote(str(album_id), safe='') + '/'
|
|
44
|
+
_q: dict[str, Any] = {}
|
|
45
|
+
if asc_or_desc is not None:
|
|
46
|
+
_q['asc_or_desc'] = asc_or_desc
|
|
47
|
+
if last_id is not None:
|
|
48
|
+
_q['last_id'] = last_id
|
|
49
|
+
if limit is not None:
|
|
50
|
+
_q['limit'] = limit
|
|
51
|
+
if confidence_value is not None:
|
|
52
|
+
_q['confidence_value'] = confidence_value
|
|
53
|
+
if _q:
|
|
54
|
+
path += '?' + urlencode(_q, doseq=True)
|
|
55
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
56
|
+
|
|
57
|
+
def get_curated_album_photos_ranked(
|
|
58
|
+
self,
|
|
59
|
+
album_id: int,
|
|
60
|
+
asc_or_desc: str | None = None,
|
|
61
|
+
last_id: Any | None = None,
|
|
62
|
+
limit: Any | None = None,
|
|
63
|
+
confidence_value: Any | None = None,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
self._ctx.require_tokens()
|
|
66
|
+
path = '/api/v1/curated_album/photos/ranked/' + quote(str(album_id), safe='') + '/'
|
|
67
|
+
_q: dict[str, Any] = {}
|
|
68
|
+
if asc_or_desc is not None:
|
|
69
|
+
_q['asc_or_desc'] = asc_or_desc
|
|
70
|
+
if last_id is not None:
|
|
71
|
+
_q['last_id'] = last_id
|
|
72
|
+
if limit is not None:
|
|
73
|
+
_q['limit'] = limit
|
|
74
|
+
if confidence_value is not None:
|
|
75
|
+
_q['confidence_value'] = confidence_value
|
|
76
|
+
if _q:
|
|
77
|
+
path += '?' + urlencode(_q, doseq=True)
|
|
78
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
79
|
+
|
|
80
|
+
def get_curated_album_by_id(self, album_id: int) -> dict[str, Any]:
|
|
81
|
+
self._ctx.require_tokens()
|
|
82
|
+
path = '/api/v1/curated_album/' + quote(str(album_id), safe='')
|
|
83
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
84
|
+
|
|
85
|
+
def update_curated_album(
|
|
86
|
+
self,
|
|
87
|
+
album_id: int,
|
|
88
|
+
*,
|
|
89
|
+
name: str | None = None,
|
|
90
|
+
description: str | None = None,
|
|
91
|
+
confidenceValue: float | None = None,
|
|
92
|
+
) -> dict[str, Any]:
|
|
93
|
+
self._ctx.require_tokens()
|
|
94
|
+
path = '/api/v1/curated_album/' + quote(str(album_id), safe='')
|
|
95
|
+
body = {k: v for k, v in {
|
|
96
|
+
'name': name,
|
|
97
|
+
'description': description,
|
|
98
|
+
'confidence_value': confidenceValue,
|
|
99
|
+
}.items() if v is not None}
|
|
100
|
+
return self._ctx.client.request(path, 'PUT', self._ctx.access_token, self._ctx.refresh_token, body).data
|
|
101
|
+
|
|
102
|
+
def delete_curated_album(self, album_id: int) -> dict[str, Any]:
|
|
103
|
+
self._ctx.require_tokens()
|
|
104
|
+
path = '/api/v1/curated_album/' + quote(str(album_id), safe='')
|
|
105
|
+
return self._ctx.client.request(path, 'DELETE', self._ctx.access_token, self._ctx.refresh_token).data
|
|
106
|
+
|
|
107
|
+
def convert_curated_album_to_custom(self, album_id: int) -> dict[str, Any]:
|
|
108
|
+
self._ctx.require_tokens()
|
|
109
|
+
path = '/api/v1/curated_album/' + quote(str(album_id), safe='') + '/convert'
|
|
110
|
+
return self._ctx.client.request(path, 'POST', self._ctx.access_token, self._ctx.refresh_token).data
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote, urlencode
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .errors import handle_response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CustomAlbums:
|
|
10
|
+
def __init__(self, ctx) -> None:
|
|
11
|
+
self._ctx = ctx
|
|
12
|
+
|
|
13
|
+
def get_custom_album_detail_by_id(self, custom_album_id: int) -> dict[str, Any]:
|
|
14
|
+
self._ctx.require_tokens()
|
|
15
|
+
path = '/api/v1/custom_album/' + quote(str(custom_album_id), safe='')
|
|
16
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
17
|
+
|
|
18
|
+
def get_all_project_custom_albums(self, project_table_name: str) -> dict[str, Any]:
|
|
19
|
+
self._ctx.require_tokens()
|
|
20
|
+
path = '/api/v1/custom_album/project/' + quote(str(project_table_name), safe='')
|
|
21
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
22
|
+
|
|
23
|
+
def get_custom_album_photos_by_id(
|
|
24
|
+
self,
|
|
25
|
+
custom_album_id: int,
|
|
26
|
+
asc_or_desc: str | None = None,
|
|
27
|
+
last_id: Any | None = None,
|
|
28
|
+
limit: Any | None = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
self._ctx.require_tokens()
|
|
31
|
+
path = '/api/v1/custom_album/photos/' + quote(str(custom_album_id), safe='') + '/'
|
|
32
|
+
_q: dict[str, Any] = {}
|
|
33
|
+
if asc_or_desc is not None:
|
|
34
|
+
_q['asc_or_desc'] = asc_or_desc
|
|
35
|
+
if last_id is not None:
|
|
36
|
+
_q['last_id'] = last_id
|
|
37
|
+
if limit is not None:
|
|
38
|
+
_q['limit'] = limit
|
|
39
|
+
if _q:
|
|
40
|
+
path += '?' + urlencode(_q, doseq=True)
|
|
41
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
42
|
+
|
|
43
|
+
def get_ranked_custom_album_by_id(
|
|
44
|
+
self,
|
|
45
|
+
custom_album_id: int,
|
|
46
|
+
asc_or_desc: str | None = None,
|
|
47
|
+
last_id: Any | None = None,
|
|
48
|
+
limit: Any | None = None,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
self._ctx.require_tokens()
|
|
51
|
+
path = '/api/v1/custom_album/photos/ranked/' + quote(str(custom_album_id), safe='') + '/'
|
|
52
|
+
_q: dict[str, Any] = {}
|
|
53
|
+
if asc_or_desc is not None:
|
|
54
|
+
_q['asc_or_desc'] = asc_or_desc
|
|
55
|
+
if last_id is not None:
|
|
56
|
+
_q['last_id'] = last_id
|
|
57
|
+
if limit is not None:
|
|
58
|
+
_q['limit'] = limit
|
|
59
|
+
if _q:
|
|
60
|
+
path += '?' + urlencode(_q, doseq=True)
|
|
61
|
+
return self._ctx.client.request(path, 'GET', self._ctx.access_token, self._ctx.refresh_token).data
|
|
62
|
+
|
|
63
|
+
def create_project_custom_album(
|
|
64
|
+
self,
|
|
65
|
+
project_table_name: str,
|
|
66
|
+
*,
|
|
67
|
+
name: str | None = None,
|
|
68
|
+
description: str | None = None,
|
|
69
|
+
photoIdInclusionList: list | None = None,
|
|
70
|
+
photoIdRemovalList: list | None = None,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
self._ctx.require_tokens()
|
|
73
|
+
path = '/api/v1/custom_album/project/' + quote(str(project_table_name), safe='')
|
|
74
|
+
body = {k: v for k, v in {
|
|
75
|
+
'name': name,
|
|
76
|
+
'description': description,
|
|
77
|
+
'photo_id_inclusion_list': photoIdInclusionList,
|
|
78
|
+
'photo_id_removal_list': photoIdRemovalList,
|
|
79
|
+
}.items() if v is not None}
|
|
80
|
+
return self._ctx.client.request(path, 'POST', self._ctx.access_token, self._ctx.refresh_token, body).data
|
|
81
|
+
|
|
82
|
+
def update_custom_album(
|
|
83
|
+
self,
|
|
84
|
+
album_id: int,
|
|
85
|
+
*,
|
|
86
|
+
name: str | None = None,
|
|
87
|
+
description: str | None = None,
|
|
88
|
+
photoIdInclusionList: list | None = None,
|
|
89
|
+
photoIdRemovalList: list | None = None,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
self._ctx.require_tokens()
|
|
92
|
+
path = '/api/v1/custom_album/' + quote(str(album_id), safe='')
|
|
93
|
+
body = {k: v for k, v in {
|
|
94
|
+
'name': name,
|
|
95
|
+
'description': description,
|
|
96
|
+
'photo_id_inclusion_list': photoIdInclusionList,
|
|
97
|
+
'photo_id_removal_list': photoIdRemovalList,
|
|
98
|
+
}.items() if v is not None}
|
|
99
|
+
return self._ctx.client.request(path, 'PUT', self._ctx.access_token, self._ctx.refresh_token, body).data
|
|
100
|
+
|
|
101
|
+
def delete_custom_album(self, album_id: int) -> dict[str, Any]:
|
|
102
|
+
self._ctx.require_tokens()
|
|
103
|
+
path = '/api/v1/custom_album/' + quote(str(album_id), safe='')
|
|
104
|
+
return self._ctx.client.request(path, 'DELETE', self._ctx.access_token, self._ctx.refresh_token).data
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote, urlencode
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .errors import handle_response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EmailTokens:
|
|
10
|
+
def __init__(self, ctx) -> None:
|
|
11
|
+
self._ctx = ctx
|
|
12
|
+
|
|
13
|
+
def request_email_verification(self, email: str | None = None) -> dict[str, Any]:
|
|
14
|
+
path = '/api/v1/request-email-verification'
|
|
15
|
+
_q: dict[str, Any] = {}
|
|
16
|
+
if email is not None:
|
|
17
|
+
_q['email'] = email
|
|
18
|
+
if _q:
|
|
19
|
+
path += '?' + urlencode(_q, doseq=True)
|
|
20
|
+
with httpx.Client() as _client:
|
|
21
|
+
_resp = _client.request('POST', self._ctx.base_url + path)
|
|
22
|
+
return handle_response(_resp.text, _resp.status_code, dict(_resp.headers))
|
|
23
|
+
|
|
24
|
+
def verify_email(self, token: str) -> dict[str, Any]:
|
|
25
|
+
path = '/api/v1/verify-email/' + quote(str(token), safe='')
|
|
26
|
+
with httpx.Client() as _client:
|
|
27
|
+
_resp = _client.request('POST', self._ctx.base_url + path)
|
|
28
|
+
return handle_response(_resp.text, _resp.status_code, dict(_resp.headers))
|
|
29
|
+
|
|
30
|
+
def request_password_reset(self, email: str | None = None) -> dict[str, Any]:
|
|
31
|
+
path = '/api/v1/request-password-reset'
|
|
32
|
+
_q: dict[str, Any] = {}
|
|
33
|
+
if email is not None:
|
|
34
|
+
_q['email'] = email
|
|
35
|
+
if _q:
|
|
36
|
+
path += '?' + urlencode(_q, doseq=True)
|
|
37
|
+
with httpx.Client() as _client:
|
|
38
|
+
_resp = _client.request('POST', self._ctx.base_url + path)
|
|
39
|
+
return handle_response(_resp.text, _resp.status_code, dict(_resp.headers))
|
|
40
|
+
|
|
41
|
+
def validate_token(self, token: str) -> dict[str, Any]:
|
|
42
|
+
path = '/api/v1/validate-token'
|
|
43
|
+
body = {k: v for k, v in {
|
|
44
|
+
'token': token,
|
|
45
|
+
}.items() if v is not None}
|
|
46
|
+
with httpx.Client() as _client:
|
|
47
|
+
_resp = _client.request('POST', self._ctx.base_url + path, json=body)
|
|
48
|
+
return handle_response(_resp.text, _resp.status_code, dict(_resp.headers))
|
|
49
|
+
|
|
50
|
+
def reset_password(self, token: str, newPassword: str) -> dict[str, Any]:
|
|
51
|
+
path = '/api/v1/reset-password'
|
|
52
|
+
body = {k: v for k, v in {
|
|
53
|
+
'token': token,
|
|
54
|
+
'new_password': newPassword,
|
|
55
|
+
}.items() if v is not None}
|
|
56
|
+
with httpx.Client() as _client:
|
|
57
|
+
_resp = _client.request('POST', self._ctx.base_url + path, json=body)
|
|
58
|
+
return handle_response(_resp.text, _resp.status_code, dict(_resp.headers))
|
|
59
|
+
|
|
60
|
+
def delete_user_email_tokens(self, user_id: int) -> dict[str, Any]:
|
|
61
|
+
self._ctx.require_tokens()
|
|
62
|
+
path = '/api/v1/admin/email_tokens/by_user/' + quote(str(user_id), safe='')
|
|
63
|
+
return self._ctx.client.request(path, 'DELETE', self._ctx.access_token, self._ctx.refresh_token).data
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApiError(Exception):
|
|
7
|
+
def __init__(self, message: str, status: int, request_id: str | None, body: Any) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status = status
|
|
10
|
+
self.request_id = request_id
|
|
11
|
+
self.body = body
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ValidationError(ApiError):
|
|
15
|
+
field_errors: list[dict]
|
|
16
|
+
|
|
17
|
+
def __init__(self, body: Any, status: int, request_id: str | None) -> None:
|
|
18
|
+
detail = body.get("detail", []) if isinstance(body, dict) else []
|
|
19
|
+
if isinstance(detail, list) and detail:
|
|
20
|
+
message = "; ".join(
|
|
21
|
+
".".join(str(loc) for loc in d.get("loc", [])) + ": " + d.get("msg", "")
|
|
22
|
+
for d in detail
|
|
23
|
+
)
|
|
24
|
+
self.field_errors = [
|
|
25
|
+
{"loc": d.get("loc", []), "msg": d.get("msg", ""), "type": d.get("type", "")}
|
|
26
|
+
for d in detail
|
|
27
|
+
]
|
|
28
|
+
else:
|
|
29
|
+
message = detail if isinstance(detail, str) else "Validation failed"
|
|
30
|
+
self.field_errors = []
|
|
31
|
+
super().__init__(message, status, request_id, body)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NotFoundError(ApiError):
|
|
35
|
+
def __init__(self, body: Any, status: int, request_id: str | None) -> None:
|
|
36
|
+
detail = body.get("detail", "Resource not found") if isinstance(body, dict) else "Resource not found"
|
|
37
|
+
super().__init__(detail, status, request_id, body)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RateLimitError(ApiError):
|
|
41
|
+
retry_after: int | None
|
|
42
|
+
|
|
43
|
+
def __init__(self, body: Any, status: int, request_id: str | None, retry_after: int | None) -> None:
|
|
44
|
+
detail = body.get("detail", "Rate limited") if isinstance(body, dict) else "Rate limited"
|
|
45
|
+
self.retry_after = retry_after
|
|
46
|
+
super().__init__(detail, status, request_id, body)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ServerError(ApiError):
|
|
50
|
+
def __init__(self, body: Any, status: int, request_id: str | None) -> None:
|
|
51
|
+
detail = body.get("detail", "Internal server error") if isinstance(body, dict) else "Internal server error"
|
|
52
|
+
super().__init__(detail, status, request_id, body)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def handle_response(raw: str, status: int, headers: dict[str, str]) -> Any:
|
|
56
|
+
request_id = headers.get("x-request-id")
|
|
57
|
+
body: Any = json.loads(raw) if raw else {}
|
|
58
|
+
if 200 <= status < 300:
|
|
59
|
+
return body
|
|
60
|
+
if status == 422:
|
|
61
|
+
raise ValidationError(body, status, request_id)
|
|
62
|
+
if status == 404:
|
|
63
|
+
raise NotFoundError(body, status, request_id)
|
|
64
|
+
if status == 429:
|
|
65
|
+
retry_after = int(headers["retry-after"]) if "retry-after" in headers else None
|
|
66
|
+
raise RateLimitError(body, status, request_id, retry_after)
|
|
67
|
+
if status >= 500:
|
|
68
|
+
raise ServerError(body, status, request_id)
|
|
69
|
+
detail = body.get("detail", "Unknown error") if isinstance(body, dict) else "Unknown error"
|
|
70
|
+
raise ApiError(detail, status, request_id, body)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote, urlencode
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .errors import handle_response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Health:
|
|
10
|
+
def __init__(self, ctx) -> None:
|
|
11
|
+
self._ctx = ctx
|
|
12
|
+
|
|
13
|
+
def health_check(self) -> dict[str, Any]:
|
|
14
|
+
path = '/api/v1/health/'
|
|
15
|
+
with httpx.Client() as _client:
|
|
16
|
+
_resp = _client.request('GET', self._ctx.base_url + path)
|
|
17
|
+
return handle_response(_resp.text, _resp.status_code, dict(_resp.headers))
|
|
18
|
+
|
|
19
|
+
def liveness_check(self) -> dict[str, Any]:
|
|
20
|
+
path = '/api/v1/health/live/'
|
|
21
|
+
with httpx.Client() as _client:
|
|
22
|
+
_resp = _client.request('GET', self._ctx.base_url + path)
|
|
23
|
+
return handle_response(_resp.text, _resp.status_code, dict(_resp.headers))
|
|
24
|
+
|
|
25
|
+
def readiness_check(self) -> dict[str, Any]:
|
|
26
|
+
path = '/api/v1/health/ready'
|
|
27
|
+
with httpx.Client() as _client:
|
|
28
|
+
_resp = _client.request('GET', self._ctx.base_url + path)
|
|
29
|
+
return handle_response(_resp.text, _resp.status_code, dict(_resp.headers))
|