valentina-python-client 2.8.0__tar.gz → 3.0.0__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.
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/PKG-INFO +3 -3
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/README.md +1 -1
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/pyproject.toml +8 -6
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/__init__.py +1 -1
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/client.py +5 -5
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/base.py +46 -18
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/campaign_book_chapters.py +10 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/campaign_books.py +10 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/campaigns.py +10 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/characters.py +10 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/users.py +54 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/testing/_client.py +5 -5
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/client.py +5 -5
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/endpoints.py +1 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/users.py +4 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/base.py +51 -18
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/campaign_book_chapters.py +10 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/campaign_books.py +10 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/campaigns.py +10 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/characters.py +10 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/users.py +63 -2
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/testing/_client.py +5 -5
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/testing/_router.py +17 -17
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/testing/_routes.py +4 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/LICENSE +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_codegen.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/__init__.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/registry.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/__init__.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/_audit_params.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/character_autogen.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/character_blueprint.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/character_traits.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/companies.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/developers.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/dicerolls.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/dictionary.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/global_admin.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/identity.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/options.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/system.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/user_lookup.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/testing/__init__.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/config.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/constants.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/exceptions.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/__init__.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/audit_logs.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/books.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/campaigns.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/chapters.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/character_autogen.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/character_blueprint.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/character_trait.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/characters.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/companies.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/developers.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/diceroll.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/dictionary.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/full_sheet.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/global_admin.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/pagination.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/shared.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/system.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/models/user_lookup.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/py.typed +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/registry.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/__init__.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/_audit_params.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/character_autogen.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/character_blueprint.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/character_traits.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/companies.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/developers.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/dicerolls.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/dictionary.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/global_admin.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/identity.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/options.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/system.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/services/user_lookup.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/testing/__init__.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/testing/_factories.py +0 -0
- {valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/validate_constants.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: valentina-python-client
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Async Python client library for the Valentina Noir API
|
|
5
5
|
Author: Nate Landau
|
|
6
6
|
Author-email: Nate Landau <github@natenate.org>
|
|
@@ -16,7 +16,7 @@ License: MIT License
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
18
|
Requires-Dist: anyio>=4.13.0,<5.0.0
|
|
19
|
-
Requires-Dist:
|
|
19
|
+
Requires-Dist: httpx2>=2.4.0
|
|
20
20
|
Requires-Dist: loguru>=0.7.3
|
|
21
21
|
Requires-Dist: polyfactory>=3.3.0,<4.0.0
|
|
22
22
|
Requires-Dist: pydantic[email]>=2.13.4,<3.0.0
|
|
@@ -33,7 +33,7 @@ Async and sync Python client library for accessing the Valentina Noir API.
|
|
|
33
33
|
|
|
34
34
|
## Features
|
|
35
35
|
|
|
36
|
-
- **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on
|
|
36
|
+
- **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on httpx2
|
|
37
37
|
- **Type-safe** - Full type hints with Pydantic models for request/response validation
|
|
38
38
|
- **Convenient factory pattern** - Create a client once, access services from anywhere
|
|
39
39
|
- **Automatic pagination** - Stream through large datasets with `iter_all()` or fetch everything with `list_all()`
|
|
@@ -4,7 +4,7 @@ Async and sync Python client library for accessing the Valentina Noir API.
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on
|
|
7
|
+
- **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on httpx2
|
|
8
8
|
- **Type-safe** - Full type hints with Pydantic models for request/response validation
|
|
9
9
|
- **Convenient factory pattern** - Create a client once, access services from anywhere
|
|
10
10
|
- **Automatic pagination** - Stream through large datasets with `iter_all()` or fetch everything with `list_all()`
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
]
|
|
7
7
|
dependencies = [
|
|
8
8
|
"anyio>=4.13.0, <5.0.0",
|
|
9
|
-
"
|
|
9
|
+
"httpx2>=2.4.0",
|
|
10
10
|
"loguru>=0.7.3",
|
|
11
11
|
"polyfactory>=3.3.0, <4.0.0",
|
|
12
12
|
"pydantic[email]>=2.13.4, <3.0.0",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
name = "valentina-python-client"
|
|
17
17
|
readme = "README.md"
|
|
18
18
|
requires-python = ">=3.13"
|
|
19
|
-
version = "
|
|
19
|
+
version = "3.0.0"
|
|
20
20
|
|
|
21
21
|
[project.optional-dependencies]
|
|
22
22
|
testing = ["polyfactory>=3.3.0"]
|
|
@@ -37,25 +37,27 @@
|
|
|
37
37
|
"commitizen>=4.16.3",
|
|
38
38
|
"coverage>=7.14.1",
|
|
39
39
|
"duty>=1.9.0",
|
|
40
|
+
"httpx>=0.28.1",
|
|
40
41
|
"prek>=0.4.4",
|
|
41
42
|
"pytest-anyio>=0.0.0",
|
|
42
43
|
"pytest-clarity>=1.0.1",
|
|
43
44
|
"pytest-cov>=7.1.0",
|
|
44
45
|
"pytest-devtools>=1.2.0",
|
|
46
|
+
"pytest-httpx2>=1.0.0",
|
|
45
47
|
"pytest-mock>=3.15.1",
|
|
46
48
|
"pytest-repeat>=0.9.4",
|
|
47
49
|
"pytest-sugar>=1.1.1",
|
|
48
50
|
"pytest-xdist>=3.8.0",
|
|
49
|
-
"pytest>=9.0
|
|
51
|
+
"pytest>=9.1.0",
|
|
50
52
|
"respx>=0.23.1",
|
|
51
|
-
"ruff>=0.15.
|
|
53
|
+
"ruff>=0.15.17",
|
|
52
54
|
"shellcheck-py>=0.11.0.1",
|
|
53
|
-
"ty>=0.0.
|
|
55
|
+
"ty>=0.0.49",
|
|
54
56
|
"typos>=1.47.2",
|
|
55
57
|
"vulture>=2.16",
|
|
56
58
|
"yamllint>=1.38.0",
|
|
57
59
|
]
|
|
58
|
-
docs = ["zensical>=0.0.
|
|
60
|
+
docs = ["zensical>=0.0.45"]
|
|
59
61
|
|
|
60
62
|
[tool.commitizen]
|
|
61
63
|
bump_message = "bump(release): v$current_version → v$new_version"
|
|
@@ -6,7 +6,7 @@ import platform
|
|
|
6
6
|
from types import TracebackType
|
|
7
7
|
from typing import TYPE_CHECKING, Self
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import httpx2
|
|
10
10
|
from loguru import logger
|
|
11
11
|
|
|
12
12
|
from vclient.config import _APIConfig
|
|
@@ -144,7 +144,7 @@ class SyncVClient:
|
|
|
144
144
|
default_company_id=resolved_company_id,
|
|
145
145
|
headers=headers or {},
|
|
146
146
|
)
|
|
147
|
-
self._http:
|
|
147
|
+
self._http: httpx2.Client = self._create_http_client()
|
|
148
148
|
self._companies: SyncCompaniesService | None = None
|
|
149
149
|
self._developer: SyncDeveloperService | None = None
|
|
150
150
|
self._global_admin: SyncGlobalAdminService | None = None
|
|
@@ -160,7 +160,7 @@ class SyncVClient:
|
|
|
160
160
|
max_retries=self._config.max_retries,
|
|
161
161
|
).info("Initialize SyncVClient")
|
|
162
162
|
|
|
163
|
-
def _create_http_client(self) ->
|
|
163
|
+
def _create_http_client(self) -> httpx2.Client:
|
|
164
164
|
"""Create and configure the HTTP client."""
|
|
165
165
|
from vclient import __version__
|
|
166
166
|
|
|
@@ -168,10 +168,10 @@ class SyncVClient:
|
|
|
168
168
|
headers = {"Accept": "application/json", "User-Agent": user_agent, **self._config.headers}
|
|
169
169
|
if self._config.api_key:
|
|
170
170
|
headers[API_KEY_HEADER] = self._config.api_key
|
|
171
|
-
return
|
|
171
|
+
return httpx2.Client(
|
|
172
172
|
base_url=self._config.base_url,
|
|
173
173
|
headers=headers,
|
|
174
|
-
timeout=
|
|
174
|
+
timeout=httpx2.Timeout(self._config.timeout),
|
|
175
175
|
)
|
|
176
176
|
|
|
177
177
|
def __enter__(self) -> Self:
|
{valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/base.py
RENAMED
|
@@ -7,7 +7,7 @@ import uuid
|
|
|
7
7
|
from collections.abc import Iterator
|
|
8
8
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import httpx2
|
|
11
11
|
from loguru import logger
|
|
12
12
|
from pydantic import BaseModel, ValidationError as PydanticValidationError
|
|
13
13
|
|
|
@@ -57,7 +57,7 @@ class SyncBaseService:
|
|
|
57
57
|
self._on_behalf_of: str | None = None
|
|
58
58
|
|
|
59
59
|
@property
|
|
60
|
-
def _http(self) ->
|
|
60
|
+
def _http(self) -> httpx2.Client:
|
|
61
61
|
"""Get the HTTP client from the parent SyncVClient."""
|
|
62
62
|
return self._client._http
|
|
63
63
|
|
|
@@ -137,7 +137,7 @@ class SyncBaseService:
|
|
|
137
137
|
data: dict[str, Any] | None = None,
|
|
138
138
|
headers: dict[str, str] | None = None,
|
|
139
139
|
files: Any | None = None,
|
|
140
|
-
) ->
|
|
140
|
+
) -> httpx2.Response:
|
|
141
141
|
"""Make an HTTP request with automatic retry on transient errors.
|
|
142
142
|
|
|
143
143
|
Retries on rate limits (429), server errors (5xx in retry_statuses),
|
|
@@ -153,7 +153,7 @@ class SyncBaseService:
|
|
|
153
153
|
json: JSON body data.
|
|
154
154
|
data: Form data.
|
|
155
155
|
headers: Additional headers to include in the request.
|
|
156
|
-
files: Files to upload (passed through to
|
|
156
|
+
files: Files to upload (passed through to httpx2).
|
|
157
157
|
|
|
158
158
|
Returns:
|
|
159
159
|
The HTTP response.
|
|
@@ -161,8 +161,8 @@ class SyncBaseService:
|
|
|
161
161
|
Raises:
|
|
162
162
|
RateLimitError: When rate limit is exceeded and max retries are exhausted.
|
|
163
163
|
ServerError: When server error occurs and max retries are exhausted.
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
httpx2.ConnectError: When connection fails and max retries are exhausted.
|
|
165
|
+
httpx2.TimeoutException: When request times out and max retries are exhausted.
|
|
166
166
|
APIError: For other API error responses.
|
|
167
167
|
"""
|
|
168
168
|
headers = self._merge_on_behalf_of_header(headers)
|
|
@@ -183,7 +183,7 @@ class SyncBaseService:
|
|
|
183
183
|
headers=headers,
|
|
184
184
|
files=files,
|
|
185
185
|
)
|
|
186
|
-
except (
|
|
186
|
+
except (httpx2.ConnectError, httpx2.TimeoutException) as exc:
|
|
187
187
|
if not self._is_retryable_method(method, headers) or attempt >= max_attempts - 1:
|
|
188
188
|
raise
|
|
189
189
|
error_type = type(exc).__name__
|
|
@@ -232,7 +232,7 @@ class SyncBaseService:
|
|
|
232
232
|
raise RuntimeError(msg)
|
|
233
233
|
|
|
234
234
|
@staticmethod
|
|
235
|
-
def _log_success_response(response:
|
|
235
|
+
def _log_success_response(response: httpx2.Response, request_logger: Any) -> None:
|
|
236
236
|
"""Log a successful HTTP response with elapsed time and optional request_id.
|
|
237
237
|
|
|
238
238
|
Args:
|
|
@@ -248,7 +248,7 @@ class SyncBaseService:
|
|
|
248
248
|
|
|
249
249
|
@staticmethod
|
|
250
250
|
def _inject_request_id_fallback(
|
|
251
|
-
response_data: dict[str, Any], response:
|
|
251
|
+
response_data: dict[str, Any], response: httpx2.Response
|
|
252
252
|
) -> None:
|
|
253
253
|
"""Inject request_id from X-Request-Id header when the response body omits it.
|
|
254
254
|
|
|
@@ -262,7 +262,7 @@ class SyncBaseService:
|
|
|
262
262
|
response_data["request_id"] = header_id
|
|
263
263
|
|
|
264
264
|
def _raise_for_status(
|
|
265
|
-
self, response:
|
|
265
|
+
self, response: httpx2.Response, method: str, url: str, params: dict[str, Any] | None = None
|
|
266
266
|
) -> None:
|
|
267
267
|
"""Raise appropriate exception for error responses.
|
|
268
268
|
|
|
@@ -356,7 +356,7 @@ class SyncBaseService:
|
|
|
356
356
|
return None
|
|
357
357
|
|
|
358
358
|
@staticmethod
|
|
359
|
-
def _parse_retry_after(response:
|
|
359
|
+
def _parse_retry_after(response: httpx2.Response) -> int | None:
|
|
360
360
|
"""Parse the retry time from response headers.
|
|
361
361
|
|
|
362
362
|
First checks the RateLimit header for the "t" parameter (seconds until next token),
|
|
@@ -382,7 +382,7 @@ class SyncBaseService:
|
|
|
382
382
|
return None
|
|
383
383
|
|
|
384
384
|
@staticmethod
|
|
385
|
-
def _parse_remaining_tokens(response:
|
|
385
|
+
def _parse_remaining_tokens(response: httpx2.Response) -> int | None:
|
|
386
386
|
"""Parse the remaining tokens from the RateLimit header.
|
|
387
387
|
|
|
388
388
|
Args:
|
|
@@ -402,7 +402,7 @@ class SyncBaseService:
|
|
|
402
402
|
*,
|
|
403
403
|
params: dict[str, Any] | None = None,
|
|
404
404
|
headers: dict[str, str] | None = None,
|
|
405
|
-
) ->
|
|
405
|
+
) -> httpx2.Response:
|
|
406
406
|
"""Make a GET request.
|
|
407
407
|
|
|
408
408
|
Args:
|
|
@@ -466,7 +466,7 @@ class SyncBaseService:
|
|
|
466
466
|
data: dict[str, Any] | None = None,
|
|
467
467
|
params: dict[str, Any] | None = None,
|
|
468
468
|
idempotency_key: str | None = None,
|
|
469
|
-
) ->
|
|
469
|
+
) -> httpx2.Response:
|
|
470
470
|
"""Make a POST request.
|
|
471
471
|
|
|
472
472
|
Args:
|
|
@@ -498,7 +498,7 @@ class SyncBaseService:
|
|
|
498
498
|
data: dict[str, Any] | None = None,
|
|
499
499
|
params: dict[str, Any] | None = None,
|
|
500
500
|
idempotency_key: str | None = None,
|
|
501
|
-
) ->
|
|
501
|
+
) -> httpx2.Response:
|
|
502
502
|
"""Make a PUT request.
|
|
503
503
|
|
|
504
504
|
Args:
|
|
@@ -530,7 +530,7 @@ class SyncBaseService:
|
|
|
530
530
|
data: dict[str, Any] | None = None,
|
|
531
531
|
params: dict[str, Any] | None = None,
|
|
532
532
|
idempotency_key: str | None = None,
|
|
533
|
-
) ->
|
|
533
|
+
) -> httpx2.Response:
|
|
534
534
|
"""Make a PATCH request.
|
|
535
535
|
|
|
536
536
|
Args:
|
|
@@ -554,7 +554,7 @@ class SyncBaseService:
|
|
|
554
554
|
headers=self._build_idempotency_headers(idempotency_key),
|
|
555
555
|
)
|
|
556
556
|
|
|
557
|
-
def _delete(self, path: str, *, params: dict[str, Any] | None = None) ->
|
|
557
|
+
def _delete(self, path: str, *, params: dict[str, Any] | None = None) -> httpx2.Response:
|
|
558
558
|
"""Make a DELETE request.
|
|
559
559
|
|
|
560
560
|
Args:
|
|
@@ -568,7 +568,7 @@ class SyncBaseService:
|
|
|
568
568
|
|
|
569
569
|
def _post_file(
|
|
570
570
|
self, path: str, *, file: tuple[str, bytes, str], idempotency_key: str | None = None
|
|
571
|
-
) ->
|
|
571
|
+
) -> httpx2.Response:
|
|
572
572
|
"""Make a POST request with a file upload (multipart/form-data).
|
|
573
573
|
|
|
574
574
|
Args:
|
|
@@ -592,6 +592,34 @@ class SyncBaseService:
|
|
|
592
592
|
headers=self._build_idempotency_headers(idempotency_key),
|
|
593
593
|
)
|
|
594
594
|
|
|
595
|
+
def _put_file(
|
|
596
|
+
self, path: str, *, file: tuple[str, bytes, str], idempotency_key: str | None = None
|
|
597
|
+
) -> httpx2.Response:
|
|
598
|
+
"""Make a PUT request with a file upload (multipart/form-data).
|
|
599
|
+
|
|
600
|
+
Mirrors ``_post_file``: an idempotency key is sent only when supplied.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
path: API endpoint path.
|
|
604
|
+
file: Tuple of (filename, content, content_type) for the file to upload.
|
|
605
|
+
idempotency_key: Optional idempotency key for safe retries.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
The HTTP response.
|
|
609
|
+
|
|
610
|
+
Raises:
|
|
611
|
+
ServerError: When server error occurs and max retries are exhausted.
|
|
612
|
+
RateLimitError: When rate limit is exceeded and max retries are exhausted.
|
|
613
|
+
APIError: For other API error responses.
|
|
614
|
+
"""
|
|
615
|
+
filename, content, content_type = file
|
|
616
|
+
return self._request(
|
|
617
|
+
"PUT",
|
|
618
|
+
path,
|
|
619
|
+
files={"data": (filename, content, content_type)},
|
|
620
|
+
headers=self._build_idempotency_headers(idempotency_key),
|
|
621
|
+
)
|
|
622
|
+
|
|
595
623
|
def _get_paginated(
|
|
596
624
|
self,
|
|
597
625
|
path: str,
|
|
@@ -406,9 +406,17 @@ class SyncChaptersService(SyncBaseService):
|
|
|
406
406
|
def upload_asset(
|
|
407
407
|
self, chapter_id: str, filename: str, content: bytes, content_type: str | None = None
|
|
408
408
|
) -> Asset:
|
|
409
|
-
"""Upload a new asset for a chapter.
|
|
409
|
+
"""Upload a new image asset for a chapter.
|
|
410
410
|
|
|
411
|
-
Uploads
|
|
411
|
+
Uploads an image to S3 storage and associates it with the chapter.
|
|
412
|
+
|
|
413
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
414
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
415
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
416
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
417
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
418
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
419
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
412
420
|
|
|
413
421
|
Args:
|
|
414
422
|
chapter_id: The ID of the chapter to upload the asset for.
|
|
@@ -470,9 +470,17 @@ class SyncBooksService(SyncBaseService):
|
|
|
470
470
|
def upload_asset(
|
|
471
471
|
self, book_id: str, filename: str, content: bytes, content_type: str | None = None
|
|
472
472
|
) -> Asset:
|
|
473
|
-
"""Upload a new asset for a book.
|
|
473
|
+
"""Upload a new image asset for a book.
|
|
474
474
|
|
|
475
|
-
Uploads
|
|
475
|
+
Uploads an image to S3 storage and associates it with the book.
|
|
476
|
+
|
|
477
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
478
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
479
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
480
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
481
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
482
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
483
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
476
484
|
|
|
477
485
|
Args:
|
|
478
486
|
book_id: The ID of the book to upload the asset for.
|
|
@@ -316,9 +316,17 @@ class SyncCampaignsService(SyncBaseService):
|
|
|
316
316
|
def upload_asset(
|
|
317
317
|
self, campaign_id: str, filename: str, content: bytes, content_type: str | None = None
|
|
318
318
|
) -> Asset:
|
|
319
|
-
"""Upload a new asset for a campaign.
|
|
319
|
+
"""Upload a new image asset for a campaign.
|
|
320
320
|
|
|
321
|
-
Uploads
|
|
321
|
+
Uploads an image to S3 storage and associates it with the campaign.
|
|
322
|
+
|
|
323
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
324
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
325
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
326
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
327
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
328
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
329
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
322
330
|
|
|
323
331
|
Args:
|
|
324
332
|
campaign_id: The ID of the campaign to upload the asset for.
|
|
@@ -499,9 +499,17 @@ class SyncCharactersService(SyncBaseService):
|
|
|
499
499
|
def upload_asset(
|
|
500
500
|
self, character_id: str, filename: str, content: bytes, content_type: str | None = None
|
|
501
501
|
) -> Asset:
|
|
502
|
-
"""Upload a new asset for a
|
|
502
|
+
"""Upload a new image asset for a character.
|
|
503
503
|
|
|
504
|
-
Uploads
|
|
504
|
+
Uploads an image to S3 storage and associates it with the character.
|
|
505
|
+
|
|
506
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
507
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
508
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
509
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
510
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
511
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
512
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
505
513
|
|
|
506
514
|
Args:
|
|
507
515
|
character_id: The ID of the character to upload the asset for.
|
{valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/services/users.py
RENAMED
|
@@ -557,9 +557,17 @@ class SyncUsersService(SyncBaseService):
|
|
|
557
557
|
def upload_asset(
|
|
558
558
|
self, user_id: str, filename: str, content: bytes, content_type: str | None = None
|
|
559
559
|
) -> Asset:
|
|
560
|
-
"""Upload a new asset for a user.
|
|
560
|
+
"""Upload a new image asset for a user.
|
|
561
561
|
|
|
562
|
-
Uploads
|
|
562
|
+
Uploads an image to S3 storage and associates it with the user.
|
|
563
|
+
|
|
564
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
565
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
566
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
567
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
568
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
569
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
570
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
563
571
|
|
|
564
572
|
Args:
|
|
565
573
|
user_id: The ID of the user to upload the asset for.
|
|
@@ -583,6 +591,50 @@ class SyncUsersService(SyncBaseService):
|
|
|
583
591
|
)
|
|
584
592
|
return Asset.model_validate(response.json())
|
|
585
593
|
|
|
594
|
+
def upload_avatar(
|
|
595
|
+
self, user_id: str, filename: str, content: bytes, content_type: str | None = None
|
|
596
|
+
) -> User:
|
|
597
|
+
"""Upload a custom avatar for a user, replacing any existing one.
|
|
598
|
+
|
|
599
|
+
Accepts PNG, JPEG, WEBP, or GIF (first frame) up to 5 MB. The server
|
|
600
|
+
normalizes the image to a 512x512 WebP and it overrides any
|
|
601
|
+
identity-provider-derived avatar. Requires the ``On-Behalf-Of`` header
|
|
602
|
+
(permitted for the user themselves or an admin).
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
user_id: The ID of the user to set the avatar for.
|
|
606
|
+
filename: The original filename of the image.
|
|
607
|
+
content: The raw bytes of the image to upload.
|
|
608
|
+
content_type: The MIME type. If not provided, inferred from filename.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
The updated User object with the new ``avatar_url``.
|
|
612
|
+
"""
|
|
613
|
+
if content_type is None:
|
|
614
|
+
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
|
615
|
+
response = self._put_file(
|
|
616
|
+
self._format_endpoint(Endpoints.USER_AVATAR, user_id=user_id),
|
|
617
|
+
file=(filename, content, content_type),
|
|
618
|
+
)
|
|
619
|
+
return User.model_validate(response.json())
|
|
620
|
+
|
|
621
|
+
def delete_avatar(self, user_id: str) -> User:
|
|
622
|
+
"""Remove a user's custom avatar.
|
|
623
|
+
|
|
624
|
+
The avatar falls back to the identity-provider avatar, or ``None`` if
|
|
625
|
+
none exists. Requires the ``On-Behalf-Of`` header (permitted for the
|
|
626
|
+
user themselves or an admin). Responds 200 OK with the updated user
|
|
627
|
+
body (not 204).
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
user_id: The ID of the user whose custom avatar to remove.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
The updated User object with the resolved ``avatar_url``.
|
|
634
|
+
"""
|
|
635
|
+
response = self._delete(self._format_endpoint(Endpoints.USER_AVATAR, user_id=user_id))
|
|
636
|
+
return User.model_validate(response.json())
|
|
637
|
+
|
|
586
638
|
def get_experience(self, user_id: str, campaign_id: str) -> CampaignExperience:
|
|
587
639
|
"""Retrieve a user's experience points and cool points for a specific campaign.
|
|
588
640
|
|
{valentina_python_client-2.8.0 → valentina_python_client-3.0.0}/src/vclient/_sync/testing/_client.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AUTO-GENERATED — do not edit. Run 'uv run duty generate_sync' to regenerate.
|
|
2
2
|
"""Fake async API client for testing downstream applications.
|
|
3
3
|
|
|
4
|
-
FakeSyncVClient is a drop-in replacement for SyncVClient that uses
|
|
4
|
+
FakeSyncVClient is a drop-in replacement for SyncVClient that uses httpx2.MockTransport
|
|
5
5
|
instead of real HTTP. All real service classes work unmodified.
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import secrets
|
|
11
11
|
from typing import TYPE_CHECKING, Any
|
|
12
12
|
|
|
13
|
-
import
|
|
13
|
+
import httpx2
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
|
|
16
16
|
from vclient._sync.client import SyncVClient
|
|
@@ -65,10 +65,10 @@ class SyncFakeVClient(SyncVClient):
|
|
|
65
65
|
**kwargs,
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
def _create_http_client(self) ->
|
|
68
|
+
def _create_http_client(self) -> httpx2.Client:
|
|
69
69
|
"""Create an HTTP client backed by the fake router."""
|
|
70
|
-
return
|
|
71
|
-
transport=
|
|
70
|
+
return httpx2.Client(
|
|
71
|
+
transport=httpx2.MockTransport(self._router.handle),
|
|
72
72
|
base_url="https://fake.valentina-api.test",
|
|
73
73
|
)
|
|
74
74
|
|
|
@@ -5,7 +5,7 @@ import platform
|
|
|
5
5
|
from types import TracebackType
|
|
6
6
|
from typing import TYPE_CHECKING, Self
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import httpx2
|
|
9
9
|
from loguru import logger
|
|
10
10
|
|
|
11
11
|
from vclient.config import _APIConfig
|
|
@@ -147,7 +147,7 @@ class VClient:
|
|
|
147
147
|
headers=headers or {},
|
|
148
148
|
)
|
|
149
149
|
|
|
150
|
-
self._http:
|
|
150
|
+
self._http: httpx2.AsyncClient = self._create_http_client()
|
|
151
151
|
self._companies: CompaniesService | None = None
|
|
152
152
|
self._developer: DeveloperService | None = None
|
|
153
153
|
self._global_admin: GlobalAdminService | None = None
|
|
@@ -165,7 +165,7 @@ class VClient:
|
|
|
165
165
|
max_retries=self._config.max_retries,
|
|
166
166
|
).info("Initialize VClient")
|
|
167
167
|
|
|
168
|
-
def _create_http_client(self) ->
|
|
168
|
+
def _create_http_client(self) -> httpx2.AsyncClient:
|
|
169
169
|
"""Create and configure the HTTP client."""
|
|
170
170
|
from vclient import __version__
|
|
171
171
|
|
|
@@ -180,10 +180,10 @@ class VClient:
|
|
|
180
180
|
if self._config.api_key:
|
|
181
181
|
headers[API_KEY_HEADER] = self._config.api_key
|
|
182
182
|
|
|
183
|
-
return
|
|
183
|
+
return httpx2.AsyncClient(
|
|
184
184
|
base_url=self._config.base_url,
|
|
185
185
|
headers=headers,
|
|
186
|
-
timeout=
|
|
186
|
+
timeout=httpx2.Timeout(self._config.timeout),
|
|
187
187
|
)
|
|
188
188
|
|
|
189
189
|
async def __aenter__(self) -> Self:
|
|
@@ -56,6 +56,7 @@ class Endpoints:
|
|
|
56
56
|
USER_ASSETS = f"{USER}/assets"
|
|
57
57
|
USER_ASSET = f"{USER_ASSETS}/{{asset_id}}"
|
|
58
58
|
USER_ASSET_UPLOAD = f"{USER_ASSETS}/upload"
|
|
59
|
+
USER_AVATAR = f"{USER}/avatar"
|
|
59
60
|
USER_EXPERIENCE_CAMPAIGN = f"{USER}/experience/{{campaign_id}}"
|
|
60
61
|
USER_EXPERIENCE_XP_ADD = f"{USER}/experience/xp/add"
|
|
61
62
|
USER_EXPERIENCE_XP_REMOVE = f"{USER}/experience/xp/remove"
|
|
@@ -101,6 +101,10 @@ class User(BaseModel):
|
|
|
101
101
|
google_profile: GoogleProfile | None = None
|
|
102
102
|
github_profile: GitHubProfile | None = None
|
|
103
103
|
apple_profile: AppleProfile | None = None
|
|
104
|
+
avatar_url: str | None = None
|
|
105
|
+
"""Resolved avatar URL. Precedence: custom uploaded avatar (CloudFront URL),
|
|
106
|
+
then the identity-provider-derived avatar (currently Discord), else ``None``.
|
|
107
|
+
Prefer this over ``discord_profile.avatar_url`` for display."""
|
|
104
108
|
campaign_experience: list[CampaignExperience] = Field(default_factory=list)
|
|
105
109
|
asset_ids: list[str] = Field(default_factory=list)
|
|
106
110
|
lifetime_xp: int = 0
|