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.
Files changed (35) hide show
  1. mediaviz_sdk-1.0.5.81.dev0/LICENSE +21 -0
  2. mediaviz_sdk-1.0.5.81.dev0/PKG-INFO +7 -0
  3. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/__init__.py +20 -0
  4. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/admin.py +16 -0
  5. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/ai_model_credits.py +16 -0
  6. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/client.py +141 -0
  7. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/company.py +33 -0
  8. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/curated_albums.py +110 -0
  9. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/custom_albums.py +104 -0
  10. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/email_tokens.py +63 -0
  11. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/errors.py +70 -0
  12. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/health.py +29 -0
  13. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/keywords.py +165 -0
  14. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/o_auth_authorization.py +75 -0
  15. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/o_auth_token.py +51 -0
  16. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/oauth_login.py +33 -0
  17. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/person.py +83 -0
  18. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/photo_upload.py +150 -0
  19. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/photos.py +140 -0
  20. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/projects.py +197 -0
  21. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/search.py +258 -0
  22. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk/users.py +127 -0
  23. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/PKG-INFO +7 -0
  24. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/SOURCES.txt +33 -0
  25. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/dependency_links.txt +1 -0
  26. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/requires.txt +1 -0
  27. mediaviz_sdk-1.0.5.81.dev0/mediaviz_sdk.egg-info/top_level.txt +2 -0
  28. mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/__init__.py +25 -0
  29. mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_client.py +232 -0
  30. mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_errors.py +26 -0
  31. mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_http.py +43 -0
  32. mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_pkce.py +16 -0
  33. mediaviz_sdk-1.0.5.81.dev0/oauth_sdk/_types.py +77 -0
  34. mediaviz_sdk-1.0.5.81.dev0/pyproject.toml +12 -0
  35. 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,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: mediaviz-sdk
3
+ Version: 1.0.5.81.dev0
4
+ Requires-Python: >=3.12
5
+ License-File: LICENSE
6
+ Requires-Dist: httpx>=0.27
7
+ Dynamic: license-file
@@ -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))