valentina-python-client 2.7.0__tar.gz → 2.9.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.7.0 → valentina_python_client-2.9.0}/PKG-INFO +1 -1
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/pyproject.toml +1 -1
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/__init__.py +1 -1
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/base.py +28 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/campaign_book_chapters.py +10 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/campaign_books.py +10 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/campaigns.py +10 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/characters.py +10 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/users.py +85 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/endpoints.py +2 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/users.py +4 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/base.py +33 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/campaign_book_chapters.py +10 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/campaign_books.py +10 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/campaigns.py +10 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/characters.py +10 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/users.py +99 -2
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/_routes.py +5 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/LICENSE +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/README.md +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_codegen.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/__init__.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/client.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/registry.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/__init__.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/_audit_params.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/character_autogen.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/character_blueprint.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/character_traits.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/companies.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/developers.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/dicerolls.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/dictionary.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/global_admin.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/identity.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/options.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/system.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/user_lookup.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/testing/__init__.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/testing/_client.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/client.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/config.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/constants.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/exceptions.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/__init__.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/audit_logs.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/books.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/campaigns.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/chapters.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/character_autogen.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/character_blueprint.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/character_trait.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/characters.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/companies.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/developers.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/diceroll.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/dictionary.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/full_sheet.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/global_admin.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/pagination.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/shared.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/system.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/user_lookup.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/py.typed +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/registry.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/__init__.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/_audit_params.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/character_autogen.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/character_blueprint.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/character_traits.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/companies.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/developers.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/dicerolls.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/dictionary.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/global_admin.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/identity.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/options.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/system.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/user_lookup.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/__init__.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/_client.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/_factories.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/_router.py +0 -0
- {valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/validate_constants.py +0 -0
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/base.py
RENAMED
|
@@ -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
|
+
) -> httpx.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.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/users.py
RENAMED
|
@@ -208,6 +208,37 @@ class SyncUsersService(SyncBaseService):
|
|
|
208
208
|
)
|
|
209
209
|
return User.model_validate(response.json())
|
|
210
210
|
|
|
211
|
+
def unlink_identity(self, user_id: str, *, provider: IdentityProvider) -> User:
|
|
212
|
+
"""Disconnect a verified provider identity from a user.
|
|
213
|
+
|
|
214
|
+
The counterpart to link_identity for "disconnect your account" settings
|
|
215
|
+
flows: remove a single OAuth provider identity so it can no longer be
|
|
216
|
+
used to log in. Only the acting user themselves or a company ADMIN may
|
|
217
|
+
unlink identities. The provider's profile field on the returned User is
|
|
218
|
+
cleared to None.
|
|
219
|
+
|
|
220
|
+
The final remaining identity cannot be removed, since that would leave
|
|
221
|
+
the account unable to authenticate. Link another provider first.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
user_id: The ID of the user losing the identity.
|
|
225
|
+
provider: The identity provider to disconnect.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The updated User object with the provider's profile field cleared.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
AuthorizationError: If the acting user is neither the target user nor an admin.
|
|
232
|
+
NotFoundError: If the user has no identity from this provider
|
|
233
|
+
(code IDENTITY_NOT_LINKED).
|
|
234
|
+
ConflictError: If the provider is the user's only linked identity,
|
|
235
|
+
which cannot be removed (code LAST_IDENTITY).
|
|
236
|
+
"""
|
|
237
|
+
response = self._delete(
|
|
238
|
+
self._format_endpoint(Endpoints.USER_IDENTITY, user_id=user_id, provider=provider)
|
|
239
|
+
)
|
|
240
|
+
return User.model_validate(response.json())
|
|
241
|
+
|
|
211
242
|
def get_page(
|
|
212
243
|
self,
|
|
213
244
|
*,
|
|
@@ -526,9 +557,17 @@ class SyncUsersService(SyncBaseService):
|
|
|
526
557
|
def upload_asset(
|
|
527
558
|
self, user_id: str, filename: str, content: bytes, content_type: str | None = None
|
|
528
559
|
) -> Asset:
|
|
529
|
-
"""Upload a new asset for a user.
|
|
560
|
+
"""Upload a new image asset for a user.
|
|
561
|
+
|
|
562
|
+
Uploads an image to S3 storage and associates it with the user.
|
|
530
563
|
|
|
531
|
-
|
|
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"``.
|
|
532
571
|
|
|
533
572
|
Args:
|
|
534
573
|
user_id: The ID of the user to upload the asset for.
|
|
@@ -552,6 +591,50 @@ class SyncUsersService(SyncBaseService):
|
|
|
552
591
|
)
|
|
553
592
|
return Asset.model_validate(response.json())
|
|
554
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
|
+
|
|
555
638
|
def get_experience(self, user_id: str, campaign_id: str) -> CampaignExperience:
|
|
556
639
|
"""Retrieve a user's experience points and cool points for a specific campaign.
|
|
557
640
|
|
|
@@ -49,12 +49,14 @@ class Endpoints:
|
|
|
49
49
|
USER = f"{USERS}/{{user_id}}"
|
|
50
50
|
USER_MERGE = f"{USERS}/merge"
|
|
51
51
|
USER_IDENTITIES = f"{USER}/identities"
|
|
52
|
+
USER_IDENTITY = f"{USER_IDENTITIES}/{{provider}}"
|
|
52
53
|
USER_APPROVE = f"{USER}/approve"
|
|
53
54
|
USER_DENY = f"{USER}/deny"
|
|
54
55
|
USER_STATISTICS = f"{USER}/statistics"
|
|
55
56
|
USER_ASSETS = f"{USER}/assets"
|
|
56
57
|
USER_ASSET = f"{USER_ASSETS}/{{asset_id}}"
|
|
57
58
|
USER_ASSET_UPLOAD = f"{USER_ASSETS}/upload"
|
|
59
|
+
USER_AVATAR = f"{USER}/avatar"
|
|
58
60
|
USER_EXPERIENCE_CAMPAIGN = f"{USER}/experience/{{campaign_id}}"
|
|
59
61
|
USER_EXPERIENCE_XP_ADD = f"{USER}/experience/xp/add"
|
|
60
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
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/base.py
RENAMED
|
@@ -667,6 +667,39 @@ class BaseService:
|
|
|
667
667
|
headers=self._build_idempotency_headers(idempotency_key),
|
|
668
668
|
)
|
|
669
669
|
|
|
670
|
+
async def _put_file(
|
|
671
|
+
self,
|
|
672
|
+
path: str,
|
|
673
|
+
*,
|
|
674
|
+
file: tuple[str, bytes, str],
|
|
675
|
+
idempotency_key: str | None = None,
|
|
676
|
+
) -> httpx.Response:
|
|
677
|
+
"""Make a PUT request with a file upload (multipart/form-data).
|
|
678
|
+
|
|
679
|
+
Mirrors ``_post_file``: an idempotency key is sent only when supplied.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
path: API endpoint path.
|
|
683
|
+
file: Tuple of (filename, content, content_type) for the file to upload.
|
|
684
|
+
idempotency_key: Optional idempotency key for safe retries.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
The HTTP response.
|
|
688
|
+
|
|
689
|
+
Raises:
|
|
690
|
+
ServerError: When server error occurs and max retries are exhausted.
|
|
691
|
+
RateLimitError: When rate limit is exceeded and max retries are exhausted.
|
|
692
|
+
APIError: For other API error responses.
|
|
693
|
+
"""
|
|
694
|
+
filename, content, content_type = file
|
|
695
|
+
|
|
696
|
+
return await self._request(
|
|
697
|
+
"PUT",
|
|
698
|
+
path,
|
|
699
|
+
files={"data": (filename, content, content_type)},
|
|
700
|
+
headers=self._build_idempotency_headers(idempotency_key),
|
|
701
|
+
)
|
|
702
|
+
|
|
670
703
|
# -------------------------------------------------------------------------
|
|
671
704
|
# Pagination Methods
|
|
672
705
|
# -------------------------------------------------------------------------
|
|
@@ -486,9 +486,17 @@ class ChaptersService(BaseService):
|
|
|
486
486
|
content: bytes,
|
|
487
487
|
content_type: str | None = None,
|
|
488
488
|
) -> Asset:
|
|
489
|
-
"""Upload a new asset for a chapter.
|
|
489
|
+
"""Upload a new image asset for a chapter.
|
|
490
490
|
|
|
491
|
-
Uploads
|
|
491
|
+
Uploads an image to S3 storage and associates it with the chapter.
|
|
492
|
+
|
|
493
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
494
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
495
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
496
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
497
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
498
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
499
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
492
500
|
|
|
493
501
|
Args:
|
|
494
502
|
chapter_id: The ID of the chapter to upload the asset for.
|
|
@@ -562,9 +562,17 @@ class BooksService(BaseService):
|
|
|
562
562
|
content: bytes,
|
|
563
563
|
content_type: str | None = None,
|
|
564
564
|
) -> Asset:
|
|
565
|
-
"""Upload a new asset for a book.
|
|
565
|
+
"""Upload a new image asset for a book.
|
|
566
566
|
|
|
567
|
-
Uploads
|
|
567
|
+
Uploads an image to S3 storage and associates it with the book.
|
|
568
|
+
|
|
569
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
570
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
571
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
572
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
573
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
574
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
575
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
568
576
|
|
|
569
577
|
Args:
|
|
570
578
|
book_id: The ID of the book to upload the asset for.
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/campaigns.py
RENAMED
|
@@ -380,9 +380,17 @@ class CampaignsService(BaseService):
|
|
|
380
380
|
content: bytes,
|
|
381
381
|
content_type: str | None = None,
|
|
382
382
|
) -> Asset:
|
|
383
|
-
"""Upload a new asset for a campaign.
|
|
383
|
+
"""Upload a new image asset for a campaign.
|
|
384
384
|
|
|
385
|
-
Uploads
|
|
385
|
+
Uploads an image to S3 storage and associates it with the campaign.
|
|
386
|
+
|
|
387
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
388
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
389
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
390
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
391
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
392
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
393
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
386
394
|
|
|
387
395
|
Args:
|
|
388
396
|
campaign_id: The ID of the campaign to upload the asset for.
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/characters.py
RENAMED
|
@@ -555,9 +555,17 @@ class CharactersService(BaseService):
|
|
|
555
555
|
content: bytes,
|
|
556
556
|
content_type: str | None = None,
|
|
557
557
|
) -> Asset:
|
|
558
|
-
"""Upload a new asset for a
|
|
558
|
+
"""Upload a new image asset for a character.
|
|
559
559
|
|
|
560
|
-
Uploads
|
|
560
|
+
Uploads an image to S3 storage and associates it with the character.
|
|
561
|
+
|
|
562
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
563
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
564
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
565
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
566
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
567
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
568
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
561
569
|
|
|
562
570
|
Args:
|
|
563
571
|
character_id: The ID of the character to upload the asset for.
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/users.py
RENAMED
|
@@ -242,6 +242,42 @@ class UsersService(BaseService):
|
|
|
242
242
|
)
|
|
243
243
|
return User.model_validate(response.json())
|
|
244
244
|
|
|
245
|
+
async def unlink_identity(
|
|
246
|
+
self,
|
|
247
|
+
user_id: str,
|
|
248
|
+
*,
|
|
249
|
+
provider: IdentityProvider,
|
|
250
|
+
) -> User:
|
|
251
|
+
"""Disconnect a verified provider identity from a user.
|
|
252
|
+
|
|
253
|
+
The counterpart to link_identity for "disconnect your account" settings
|
|
254
|
+
flows: remove a single OAuth provider identity so it can no longer be
|
|
255
|
+
used to log in. Only the acting user themselves or a company ADMIN may
|
|
256
|
+
unlink identities. The provider's profile field on the returned User is
|
|
257
|
+
cleared to None.
|
|
258
|
+
|
|
259
|
+
The final remaining identity cannot be removed, since that would leave
|
|
260
|
+
the account unable to authenticate. Link another provider first.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
user_id: The ID of the user losing the identity.
|
|
264
|
+
provider: The identity provider to disconnect.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
The updated User object with the provider's profile field cleared.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
AuthorizationError: If the acting user is neither the target user nor an admin.
|
|
271
|
+
NotFoundError: If the user has no identity from this provider
|
|
272
|
+
(code IDENTITY_NOT_LINKED).
|
|
273
|
+
ConflictError: If the provider is the user's only linked identity,
|
|
274
|
+
which cannot be removed (code LAST_IDENTITY).
|
|
275
|
+
"""
|
|
276
|
+
response = await self._delete(
|
|
277
|
+
self._format_endpoint(Endpoints.USER_IDENTITY, user_id=user_id, provider=provider),
|
|
278
|
+
)
|
|
279
|
+
return User.model_validate(response.json())
|
|
280
|
+
|
|
245
281
|
# User CRUD Methods
|
|
246
282
|
|
|
247
283
|
async def get_page(
|
|
@@ -620,9 +656,17 @@ class UsersService(BaseService):
|
|
|
620
656
|
content: bytes,
|
|
621
657
|
content_type: str | None = None,
|
|
622
658
|
) -> Asset:
|
|
623
|
-
"""Upload a new asset for a user.
|
|
659
|
+
"""Upload a new image asset for a user.
|
|
624
660
|
|
|
625
|
-
Uploads
|
|
661
|
+
Uploads an image to S3 storage and associates it with the user.
|
|
662
|
+
|
|
663
|
+
Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
|
|
664
|
+
upload (documents, audio, video, archives, SVG, or a non-image payload
|
|
665
|
+
mislabeled as an image) is rejected with 400 Bad Request, as are
|
|
666
|
+
oversized images that trip the decompression-bomb guard. The stored
|
|
667
|
+
``mime_type`` is detected from the file's bytes; the ``content_type``
|
|
668
|
+
argument is ignored server-side and cannot change or bypass the stored
|
|
669
|
+
type. Uploaded assets therefore always have ``asset_type == "image"``.
|
|
626
670
|
|
|
627
671
|
Args:
|
|
628
672
|
user_id: The ID of the user to upload the asset for.
|
|
@@ -647,6 +691,59 @@ class UsersService(BaseService):
|
|
|
647
691
|
)
|
|
648
692
|
return Asset.model_validate(response.json())
|
|
649
693
|
|
|
694
|
+
# Avatar Methods
|
|
695
|
+
|
|
696
|
+
async def upload_avatar(
|
|
697
|
+
self,
|
|
698
|
+
user_id: str,
|
|
699
|
+
filename: str,
|
|
700
|
+
content: bytes,
|
|
701
|
+
content_type: str | None = None,
|
|
702
|
+
) -> User:
|
|
703
|
+
"""Upload a custom avatar for a user, replacing any existing one.
|
|
704
|
+
|
|
705
|
+
Accepts PNG, JPEG, WEBP, or GIF (first frame) up to 5 MB. The server
|
|
706
|
+
normalizes the image to a 512x512 WebP and it overrides any
|
|
707
|
+
identity-provider-derived avatar. Requires the ``On-Behalf-Of`` header
|
|
708
|
+
(permitted for the user themselves or an admin).
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
user_id: The ID of the user to set the avatar for.
|
|
712
|
+
filename: The original filename of the image.
|
|
713
|
+
content: The raw bytes of the image to upload.
|
|
714
|
+
content_type: The MIME type. If not provided, inferred from filename.
|
|
715
|
+
|
|
716
|
+
Returns:
|
|
717
|
+
The updated User object with the new ``avatar_url``.
|
|
718
|
+
"""
|
|
719
|
+
if content_type is None:
|
|
720
|
+
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
|
721
|
+
|
|
722
|
+
response = await self._put_file(
|
|
723
|
+
self._format_endpoint(Endpoints.USER_AVATAR, user_id=user_id),
|
|
724
|
+
file=(filename, content, content_type),
|
|
725
|
+
)
|
|
726
|
+
return User.model_validate(response.json())
|
|
727
|
+
|
|
728
|
+
async def delete_avatar(self, user_id: str) -> User:
|
|
729
|
+
"""Remove a user's custom avatar.
|
|
730
|
+
|
|
731
|
+
The avatar falls back to the identity-provider avatar, or ``None`` if
|
|
732
|
+
none exists. Requires the ``On-Behalf-Of`` header (permitted for the
|
|
733
|
+
user themselves or an admin). Responds 200 OK with the updated user
|
|
734
|
+
body (not 204).
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
user_id: The ID of the user whose custom avatar to remove.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
The updated User object with the resolved ``avatar_url``.
|
|
741
|
+
"""
|
|
742
|
+
response = await self._delete(
|
|
743
|
+
self._format_endpoint(Endpoints.USER_AVATAR, user_id=user_id),
|
|
744
|
+
)
|
|
745
|
+
return User.model_validate(response.json())
|
|
746
|
+
|
|
650
747
|
# Experience Methods
|
|
651
748
|
|
|
652
749
|
async def get_experience(
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/_routes.py
RENAMED
|
@@ -149,6 +149,7 @@ class Routes:
|
|
|
149
149
|
USERS_DENY = RouteSpec("POST", Endpoints.USER_DENY, NO_CONTENT, None)
|
|
150
150
|
USERS_MERGE = RouteSpec("POST", Endpoints.USER_MERGE, SINGLE, User)
|
|
151
151
|
USERS_IDENTITY_LINK = RouteSpec("POST", Endpoints.USER_IDENTITIES, SINGLE, User)
|
|
152
|
+
USERS_IDENTITY_UNLINK = RouteSpec("DELETE", Endpoints.USER_IDENTITY, SINGLE, User)
|
|
152
153
|
|
|
153
154
|
USERS_STATISTICS = RouteSpec("GET", Endpoints.USER_STATISTICS, SINGLE, RollStatistics)
|
|
154
155
|
# User lookup (cross-company)
|
|
@@ -163,6 +164,10 @@ class Routes:
|
|
|
163
164
|
USERS_ASSETS_DELETE = RouteSpec("DELETE", Endpoints.USER_ASSET, NO_CONTENT, None)
|
|
164
165
|
USERS_ASSETS_UPLOAD = RouteSpec("POST", Endpoints.USER_ASSET_UPLOAD, SINGLE, Asset)
|
|
165
166
|
|
|
167
|
+
# User avatar
|
|
168
|
+
USERS_AVATAR_UPLOAD = RouteSpec("PUT", Endpoints.USER_AVATAR, SINGLE, User)
|
|
169
|
+
USERS_AVATAR_DELETE = RouteSpec("DELETE", Endpoints.USER_AVATAR, SINGLE, User)
|
|
170
|
+
|
|
166
171
|
# User experience
|
|
167
172
|
USERS_EXPERIENCE_GET = RouteSpec(
|
|
168
173
|
"GET", Endpoints.USER_EXPERIENCE_CAMPAIGN, SINGLE, CampaignExperience
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/services/system.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/_sync/testing/_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/__init__.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/audit_logs.py
RENAMED
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/campaigns.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/chapters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/characters.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/companies.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/developers.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/diceroll.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/dictionary.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/full_sheet.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/global_admin.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/pagination.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/shared.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/system.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/models/user_lookup.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/companies.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/developers.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/dicerolls.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/dictionary.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/global_admin.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/identity.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/options.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/system.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/services/user_lookup.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/__init__.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/_client.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/_factories.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/testing/_router.py
RENAMED
|
File without changes
|
{valentina_python_client-2.7.0 → valentina_python_client-2.9.0}/src/vclient/validate_constants.py
RENAMED
|
File without changes
|