valentina-python-client 3.4.0__tar.gz → 3.5.1__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 (85) hide show
  1. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/PKG-INFO +1 -1
  2. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/pyproject.toml +1 -1
  3. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/pyproject.toml.orig +1 -1
  4. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/__init__.py +1 -1
  5. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/client.py +9 -0
  6. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/base.py +8 -7
  7. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/campaign_book_chapters.py +7 -3
  8. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/campaign_books.py +5 -2
  9. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/campaigns.py +5 -2
  10. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/character_traits.py +12 -23
  11. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/characters.py +12 -24
  12. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/developers.py +7 -3
  13. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/identity.py +7 -2
  14. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/options.py +3 -3
  15. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/users.py +19 -14
  16. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/client.py +9 -0
  17. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/constants.py +0 -1
  18. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/__init__.py +0 -2
  19. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/character_trait.py +28 -56
  20. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/characters.py +1 -1
  21. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/users.py +1 -1
  22. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/base.py +8 -11
  23. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/campaign_book_chapters.py +7 -3
  24. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/campaign_books.py +5 -2
  25. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/campaigns.py +5 -2
  26. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/character_traits.py +11 -18
  27. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/characters.py +11 -19
  28. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/developers.py +7 -3
  29. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/identity.py +7 -2
  30. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/options.py +3 -3
  31. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/users.py +19 -14
  32. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/testing/__init__.py +0 -2
  33. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/testing/_factories.py +18 -38
  34. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/testing/_router.py +0 -3
  35. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/testing/_routes.py +5 -5
  36. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/validate_constants.py +0 -1
  37. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/LICENSE +0 -0
  38. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/README.md +0 -0
  39. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_codegen.py +0 -0
  40. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/__init__.py +0 -0
  41. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/registry.py +0 -0
  42. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/__init__.py +0 -0
  43. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/_audit_params.py +0 -0
  44. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/character_autogen.py +0 -0
  45. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/character_blueprint.py +0 -0
  46. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/companies.py +0 -0
  47. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/dicerolls.py +0 -0
  48. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/dictionary.py +0 -0
  49. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/global_admin.py +0 -0
  50. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/system.py +0 -0
  51. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/services/user_lookup.py +0 -0
  52. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/testing/__init__.py +0 -0
  53. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/_sync/testing/_client.py +0 -0
  54. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/config.py +0 -0
  55. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/endpoints.py +0 -0
  56. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/exceptions.py +0 -0
  57. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/audit_logs.py +0 -0
  58. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/books.py +0 -0
  59. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/campaigns.py +0 -0
  60. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/chapters.py +0 -0
  61. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/character_autogen.py +0 -0
  62. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/character_blueprint.py +0 -0
  63. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/companies.py +0 -0
  64. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/developers.py +0 -0
  65. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/diceroll.py +0 -0
  66. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/dictionary.py +0 -0
  67. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/full_sheet.py +0 -0
  68. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/global_admin.py +0 -0
  69. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/pagination.py +0 -0
  70. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/shared.py +0 -0
  71. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/system.py +0 -0
  72. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/models/user_lookup.py +0 -0
  73. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/py.typed +0 -0
  74. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/registry.py +0 -0
  75. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/__init__.py +0 -0
  76. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/_audit_params.py +0 -0
  77. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/character_autogen.py +0 -0
  78. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/character_blueprint.py +0 -0
  79. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/companies.py +0 -0
  80. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/dicerolls.py +0 -0
  81. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/dictionary.py +0 -0
  82. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/global_admin.py +0 -0
  83. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/system.py +0 -0
  84. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/services/user_lookup.py +0 -0
  85. {valentina_python_client-3.4.0 → valentina_python_client-3.5.1}/src/vclient/testing/_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valentina-python-client
3
- Version: 3.4.0
3
+ Version: 3.5.1
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>
@@ -15,7 +15,7 @@ description = "Async Python client library for the Valentina Noir API"
15
15
  name = "valentina-python-client"
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.13"
18
- version = "3.4.0"
18
+ version = "3.5.1"
19
19
 
20
20
  [[project.authors]]
21
21
  name = "Nate Landau"
@@ -17,7 +17,7 @@
17
17
  name = "valentina-python-client"
18
18
  readme = "README.md"
19
19
  requires-python = ">=3.13"
20
- version = "3.4.0"
20
+ version = "3.5.1"
21
21
 
22
22
  [project.optional-dependencies]
23
23
  testing = ["polyfactory>=3.3.0"]
@@ -116,4 +116,4 @@ __all__ = (
116
116
  "users_service",
117
117
  )
118
118
 
119
- __version__ = "3.4.0"
119
+ __version__ = "3.5.1"
@@ -200,6 +200,15 @@ class SyncVClient:
200
200
  """Check if the client has been closed."""
201
201
  return self._http.is_closed
202
202
 
203
+ def _set_api_key(self, api_key: str) -> None:
204
+ """Send a new API key on every later request from this client and its services.
205
+
206
+ Args:
207
+ api_key: The API key to authenticate with from now on.
208
+ """
209
+ self._config.api_key = api_key
210
+ self._http.headers[API_KEY_HEADER] = api_key
211
+
203
212
  @property
204
213
  def default_company_id(self) -> str | None:
205
214
  """Return the default company ID from config."""
@@ -91,21 +91,22 @@ class SyncBaseService:
91
91
  def _calculate_backoff_delay(self, attempt: int, retry_after: int | None) -> float:
92
92
  """Calculate the delay before the next retry attempt.
93
93
 
94
- Uses exponential backoff with jitter: base_delay * (2 ** attempt) + random jitter.
95
- If retry_after is provided from the RateLimit header, uses that as the base.
94
+ Wait the server's ``retry_after`` when it gives one, since the server knows
95
+ when capacity returns. Otherwise use exponential backoff:
96
+ ``retry_delay * 2 ** attempt``. Either way, add up to 25% random jitter so
97
+ clients that were limited together do not retry together.
96
98
 
97
99
  Args:
98
100
  attempt: The current attempt number (0-indexed).
99
- retry_after: Seconds to wait from the RateLimit header, or None.
101
+ retry_after: Seconds to wait from the RateLimit or Retry-After header, or None.
100
102
 
101
103
  Returns:
102
104
  Number of seconds to wait before retrying.
103
105
  """
104
- config = self._client._config
105
- base_delay = config.retry_delay
106
106
  if retry_after is not None and retry_after > 0:
107
- base_delay = max(base_delay, float(retry_after))
108
- delay = base_delay * 2**attempt
107
+ delay = float(retry_after)
108
+ else:
109
+ delay = self._client._config.retry_delay * 2**attempt
109
110
  jitter = random.uniform(0, delay * 0.25)
110
111
  return delay + jitter
111
112
 
@@ -26,7 +26,11 @@ if TYPE_CHECKING:
26
26
 
27
27
 
28
28
  class SyncChaptersService(SyncBaseService):
29
- """Service for managing campaign book chapters within a campaign book in the Valentina API."""
29
+ """Service for managing campaign book chapters within a campaign book in the Valentina API.
30
+
31
+ Requests return 403 (``AuthorizationError``) when the ``on_behalf_of`` user is
32
+ UNAPPROVED or DEACTIVATED.
33
+ """
30
34
 
31
35
  def __init__(
32
36
  self,
@@ -413,7 +417,7 @@ class SyncChaptersService(SyncBaseService):
413
417
  Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
414
418
  upload (documents, audio, video, archives, SVG, or a non-image payload
415
419
  mislabeled as an image) is rejected with 400 Bad Request, as are
416
- oversized images that trip the decompression-bomb guard. The stored
420
+ images over 24 megapixels (width x height). The stored
417
421
  ``mime_type`` is detected from the file's bytes; the ``content_type``
418
422
  argument is ignored server-side and cannot change or bypass the stored
419
423
  type. Uploaded assets therefore always have ``asset_type == "image"``.
@@ -450,7 +454,7 @@ class SyncChaptersService(SyncBaseService):
450
454
  asset_id: The ID of the asset to delete.
451
455
 
452
456
  Raises:
453
- NotFoundError: If the asset does not exist.
457
+ NotFoundError: If the asset does not exist or belongs to a different chapter.
454
458
  AuthorizationError: If you don't have appropriate access.
455
459
  """
456
460
  self._delete(
@@ -34,6 +34,9 @@ class SyncBooksService(SyncBaseService):
34
34
  Provides methods to create, retrieve, update, and delete campaign books,
35
35
  as well as access book notes and assets.
36
36
 
37
+ Requests return 403 (``AuthorizationError``) when the ``on_behalf_of`` user is
38
+ UNAPPROVED or DEACTIVATED.
39
+
37
40
  Example:
38
41
  >>> async with SyncVClient() as client:
39
42
  ... books = client.books("company_id", "campaign_id")
@@ -477,7 +480,7 @@ class SyncBooksService(SyncBaseService):
477
480
  Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
478
481
  upload (documents, audio, video, archives, SVG, or a non-image payload
479
482
  mislabeled as an image) is rejected with 400 Bad Request, as are
480
- oversized images that trip the decompression-bomb guard. The stored
483
+ images over 24 megapixels (width x height). The stored
481
484
  ``mime_type`` is detected from the file's bytes; the ``content_type``
482
485
  argument is ignored server-side and cannot change or bypass the stored
483
486
  type. Uploaded assets therefore always have ``asset_type == "image"``.
@@ -514,7 +517,7 @@ class SyncBooksService(SyncBaseService):
514
517
  asset_id: The ID of the asset to delete.
515
518
 
516
519
  Raises:
517
- NotFoundError: If the asset does not exist.
520
+ NotFoundError: If the asset does not exist or belongs to a different book.
518
521
  AuthorizationError: If you don't have appropriate access.
519
522
  """
520
523
  self._delete(
@@ -33,6 +33,9 @@ class SyncCampaignsService(SyncBaseService):
33
33
  Provides methods to create, retrieve, update, and delete campaigns,
34
34
  as well as access campaign statistics, assets, and notes.
35
35
 
36
+ Requests return 403 (``AuthorizationError``) when the ``on_behalf_of`` user is
37
+ UNAPPROVED or DEACTIVATED.
38
+
36
39
  Example:
37
40
  >>> async with SyncVClient() as client:
38
41
  ... campaigns = client.campaigns("company_id")
@@ -308,7 +311,7 @@ class SyncCampaignsService(SyncBaseService):
308
311
  asset_id: The ID of the asset to delete.
309
312
 
310
313
  Raises:
311
- NotFoundError: If the asset does not exist.
314
+ NotFoundError: If the asset does not exist or belongs to a different campaign.
312
315
  AuthorizationError: If you don't have appropriate access.
313
316
  """
314
317
  self._delete(
@@ -327,7 +330,7 @@ class SyncCampaignsService(SyncBaseService):
327
330
  Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
328
331
  upload (documents, audio, video, archives, SVG, or a non-image payload
329
332
  mislabeled as an image) is rejected with 400 Bad Request, as are
330
- oversized images that trip the decompression-bomb guard. The stored
333
+ images over 24 megapixels (width x height). The stored
331
334
  ``mime_type`` is detected from the file's bytes; the ``content_type``
332
335
  argument is ignored server-side and cannot change or bypass the stored
333
336
  type. Uploaded assets therefore always have ``asset_type == "image"``.
@@ -5,7 +5,7 @@ from collections.abc import Iterator
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from vclient._sync.services.base import SyncBaseService
8
- from vclient.constants import DEFAULT_PAGE_LIMIT, DamageType, TrackerAction, TraitModifyCurrency
8
+ from vclient.constants import DEFAULT_PAGE_LIMIT, TrackerAction, TraitModifyCurrency
9
9
  from vclient.endpoints import Endpoints
10
10
  from vclient.models import (
11
11
  _TRACKER_ADAPTER,
@@ -407,7 +407,7 @@ class SyncCharacterTraitsService(SyncBaseService):
407
407
  character_trait_id: The ID of the character trait.
408
408
 
409
409
  Returns:
410
- A CounterTracker, DamageTracker, or StainsTracker. Switch on ``type``.
410
+ A CounterTracker or StainsTracker. Switch on ``type``.
411
411
 
412
412
  Raises:
413
413
  NotFoundError: If the trait has no tracker for the character's game version.
@@ -425,16 +425,15 @@ class SyncCharacterTraitsService(SyncBaseService):
425
425
  ) -> Tracker:
426
426
  """Replace the full state of a trait tracker, such as to correct a mistake.
427
427
 
428
- Send only the fields for the tracker's type: ``current`` for counters (required),
429
- ``superficial`` and ``aggravated`` for damage tracks, ``stains`` for stains. Omitted
430
- fields reset to 0, so an empty request clears a damage or stains tracker. Use
428
+ Send only the field for the tracker's type: ``current`` for counters (required) or
429
+ ``stains`` for stains. An empty request clears a stains tracker. Use
431
430
  ``apply_tracker_action`` for in-play changes instead.
432
431
 
433
432
  Args:
434
433
  character_trait_id: The ID of the character trait.
435
434
  request: A TrackerStateSet model, OR pass fields as keyword arguments.
436
435
  **kwargs: Fields for TrackerStateSet if request is not provided.
437
- Accepts: current, superficial, aggravated, stains (int >= 0).
436
+ Accepts: current, stains (int >= 0).
438
437
 
439
438
  Returns:
440
439
  The updated tracker.
@@ -442,7 +441,7 @@ class SyncCharacterTraitsService(SyncBaseService):
442
441
  Raises:
443
442
  NotFoundError: If the trait has no tracker for the character's game version.
444
443
  AuthorizationError: If you are not the character's player or a storyteller.
445
- RequestValidationError: If a value is negative.
444
+ RequestValidationError: If a value is negative or a field is unknown.
446
445
  ValidationError: If a field does not fit the tracker type, a counter has no
447
446
  ``current``, or a value exceeds the tracker size.
448
447
  """
@@ -456,25 +455,17 @@ class SyncCharacterTraitsService(SyncBaseService):
456
455
  return _TRACKER_ADAPTER.validate_python(response.json())
457
456
 
458
457
  def apply_tracker_action(
459
- self,
460
- character_trait_id: str,
461
- action: TrackerAction,
462
- *,
463
- amount: int = 1,
464
- damage_type: DamageType | None = None,
458
+ self, character_trait_id: str, action: TrackerAction, *, amount: int = 1
465
459
  ) -> Tracker:
466
460
  """Apply one in-play action to a trait tracker, such as spending Willpower.
467
461
 
468
- The server applies the game rules for the character's game version. Which actions
469
- and damage types fit each tracker is listed in ``characters.TrackerRules`` from
470
- ``options.get_options()``. A STAINS response reports overflow into Willpower in
471
- ``willpower_damage_applied``.
462
+ The actions each tracker accepts are listed in
463
+ ``characters.TrackerRules[game_version][tracker.type]`` from ``options.get_options()``.
472
464
 
473
465
  Args:
474
466
  character_trait_id: The ID of the character trait.
475
467
  action: The action to apply.
476
468
  amount: How many points to apply. Must be at least 1.
477
- damage_type: The damage type for ``damage`` and ``heal`` on damage tracks.
478
469
 
479
470
  Returns:
480
471
  The updated tracker.
@@ -483,12 +474,10 @@ class SyncCharacterTraitsService(SyncBaseService):
483
474
  NotFoundError: If the trait has no tracker for the character's game version.
484
475
  AuthorizationError: If you are not the character's player or a storyteller.
485
476
  RequestValidationError: If ``amount`` is less than 1.
486
- ValidationError: If the action or damage type does not fit the tracker, a
487
- damage type is missing or not allowed, or a spend exceeds the current value.
477
+ ValidationError: If the action does not fit the tracker or a spend exceeds the
478
+ current value.
488
479
  """
489
- body = self._validate_request(
490
- _TrackerAction, action=action, amount=amount, damage_type=damage_type
491
- )
480
+ body = self._validate_request(_TrackerAction, action=action, amount=amount)
492
481
  response = self._post(
493
482
  self._format_endpoint(
494
483
  Endpoints.CHARACTER_TRAIT_TRACKER_ACTIONS, character_trait_id=character_trait_id
@@ -12,7 +12,6 @@ from vclient.constants import (
12
12
  CharacterInclude,
13
13
  CharacterStatus,
14
14
  CharacterType,
15
- DamageType,
16
15
  TrackerAction,
17
16
  )
18
17
  from vclient.endpoints import Endpoints
@@ -496,7 +495,7 @@ class SyncCharactersService(SyncBaseService):
496
495
  asset_id: The ID of the asset to delete.
497
496
 
498
497
  Raises:
499
- NotFoundError: If the asset does not exist.
498
+ NotFoundError: If the asset does not exist or belongs to a different character.
500
499
  AuthorizationError: If you don't have appropriate access.
501
500
  """
502
501
  self._delete(
@@ -515,7 +514,7 @@ class SyncCharactersService(SyncBaseService):
515
514
  Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
516
515
  upload (documents, audio, video, archives, SVG, or a non-image payload
517
516
  mislabeled as an image) is rejected with 400 Bad Request, as are
518
- oversized images that trip the decompression-bomb guard. The stored
517
+ images over 24 megapixels (width x height). The stored
519
518
  ``mime_type`` is detected from the file's bytes; the ``content_type``
520
519
  argument is ignored server-side and cannot change or bypass the stored
521
520
  type. Uploaded assets therefore always have ``asset_type == "image"``.
@@ -905,15 +904,14 @@ class SyncCharactersService(SyncBaseService):
905
904
  ) -> Health:
906
905
  """Replace a character's full health state, such as to correct a mistake.
907
906
 
908
- Every field is sent, so omitted fields reset to 0. V4 uses ``bashing``,
909
- ``lethal``, and ``aggravated``; V5 uses ``superficial`` and ``aggravated``.
910
- ``extra_levels`` applies to both. Use ``apply_health_action`` for in-play changes.
907
+ Every field is sent, so omitted fields reset to 0. Use ``apply_health_action`` for
908
+ in-play changes.
911
909
 
912
910
  Args:
913
911
  character_id: The ID of the character.
914
912
  request: A HealthStateSet model, OR pass fields as keyword arguments.
915
913
  **kwargs: Fields for HealthStateSet if request is not provided.
916
- Accepts: extra_levels, superficial, aggravated, bashing, lethal (int >= 0).
914
+ Accepts: extra_levels, damage (int >= 0).
917
915
 
918
916
  Returns:
919
917
  The updated Health.
@@ -921,9 +919,8 @@ class SyncCharactersService(SyncBaseService):
921
919
  Raises:
922
920
  NotFoundError: If the character does not exist or has no health track.
923
921
  AuthorizationError: If you are not the character's player or a storyteller.
924
- RequestValidationError: If a value is negative.
925
- ValidationError: If a nonzero field is not used by the game version, or the
926
- marks exceed the track size.
922
+ RequestValidationError: If a value is negative or a field is unknown.
923
+ ValidationError: If the damage exceeds the track size.
927
924
  """
928
925
  body = request if request is not None else self._validate_request(HealthStateSet, **kwargs)
929
926
  response = self._put(
@@ -933,24 +930,17 @@ class SyncCharactersService(SyncBaseService):
933
930
  return Health.model_validate(response.json())
934
931
 
935
932
  def apply_health_action(
936
- self,
937
- character_id: str,
938
- action: TrackerAction,
939
- *,
940
- amount: int = 1,
941
- damage_type: DamageType | None = None,
933
+ self, character_id: str, action: TrackerAction, *, amount: int = 1
942
934
  ) -> Health:
943
935
  """Apply one in-play action to a character's health, such as taking damage.
944
936
 
945
- Health accepts ``damage``, ``heal``, and ``reset``. V4 damage types are
946
- ``BASHING``, ``LETHAL``, and ``AGGRAVATED``; V5 damage types are ``SUPERFICIAL``
947
- and ``AGGRAVATED``. The server applies overflow and upgrade rules.
937
+ Health accepts ``damage``, ``heal``, and ``reset``. Damage past a full track and
938
+ healing past 0 are dropped.
948
939
 
949
940
  Args:
950
941
  character_id: The ID of the character.
951
942
  action: The action to apply.
952
943
  amount: How many points to apply. Must be at least 1.
953
- damage_type: The damage type for ``damage`` and ``heal``.
954
944
 
955
945
  Returns:
956
946
  The updated Health.
@@ -959,11 +949,9 @@ class SyncCharactersService(SyncBaseService):
959
949
  NotFoundError: If the character does not exist or has no health track.
960
950
  AuthorizationError: If you are not the character's player or a storyteller.
961
951
  RequestValidationError: If ``amount`` is less than 1.
962
- ValidationError: If the action or damage type does not fit the game version.
952
+ ValidationError: If the action does not apply to health.
963
953
  """
964
- body = self._validate_request(
965
- _TrackerAction, action=action, amount=amount, damage_type=damage_type
966
- )
954
+ body = self._validate_request(_TrackerAction, action=action, amount=amount)
967
955
  response = self._post(
968
956
  self._format_endpoint(Endpoints.CHARACTER_HEALTH_ACTIONS, character_id=character_id),
969
957
  json=body.model_dump(exclude_none=True, mode="json"),
@@ -64,8 +64,10 @@ class SyncDeveloperService(SyncBaseService):
64
64
  def regenerate_api_key(self) -> MeDeveloperWithApiKey:
65
65
  """Generate a new API key for the current developer.
66
66
 
67
- The current key will be immediately invalidated and all cached
68
- authentication data will be cleared.
67
+ The API invalidates the current key at once, so this client switches to
68
+ the new key for every later request, including those from services it
69
+ already created. Other clients that still hold the old key get
70
+ ``AuthenticationError`` until they are given the new one.
69
71
 
70
72
  **Be certain to save the API key from the response. It will not be displayed again.**
71
73
 
@@ -76,4 +78,6 @@ class SyncDeveloperService(SyncBaseService):
76
78
  AuthenticationError: If the current API key is invalid.
77
79
  """
78
80
  response = self._post(Endpoints.DEVELOPER_ME_NEW_KEY)
79
- return MeDeveloperWithApiKey.model_validate(response.json())
81
+ developer = MeDeveloperWithApiKey.model_validate(response.json())
82
+ self._client._set_api_key(developer.api_key)
83
+ return developer
@@ -18,7 +18,10 @@ class SyncIdentityService(SyncBaseService):
18
18
  ID token for apple/google, an OAuth access token for discord/github). The
19
19
  API verifies the credential with the provider and resolves the user in
20
20
  order: match by provider ID, auto-link by provider-verified email, or
21
- create a new UNAPPROVED user.
21
+ create a new UNAPPROVED user. Auto-link never replaces an identity: when
22
+ the email's user already holds a different identity from the same
23
+ provider, a new user is created instead. OIDC ID tokens must carry
24
+ ``exp`` and ``iat`` claims.
22
25
 
23
26
  This service does not require an acting user (no On-Behalf-Of header).
24
27
  Authentication is via developer API key only.
@@ -50,7 +53,9 @@ class SyncIdentityService(SyncBaseService):
50
53
  The returned ``resolution`` field reports which path the API took:
51
54
  ``matched`` (an existing user holds this provider identity), ``linked``
52
55
  (the identity was auto-linked onto an existing user by provider-verified
53
- email), or ``created`` (a new UNAPPROVED user was registered).
56
+ email), or ``created`` (a new UNAPPROVED user was registered). An email
57
+ match whose user already holds a different identity from the same
58
+ provider resolves to ``created``, not ``linked``.
54
59
 
55
60
  Args:
56
61
  request: A UserIdentifyDTO model, OR pass fields as keyword arguments.
@@ -36,9 +36,9 @@ class SyncOptionsService(SyncBaseService):
36
36
  """Retrieve all options and enumerations for the api.
37
37
 
38
38
  Most values are lists of enum values. ``characters.TrackerRules`` is a nested map
39
- ``{game_version: {tracker_key: {"actions": [...], "damage_types": [...]}}}`` where
40
- ``tracker_key`` is a tracker type or ``HEALTH``. Use it to choose which tracker
41
- actions and damage types to offer instead of hardcoding them.
39
+ ``{game_version: {tracker_type: {"actions": [...]}}}``. Health is keyed by its
40
+ ``type``, ``DAMAGE``. Use it to choose which tracker actions to offer instead of
41
+ hardcoding them.
42
42
 
43
43
  Returns:
44
44
  A dictionary of options and enumerations for the api.
@@ -547,8 +547,9 @@ class SyncUsersService(SyncBaseService):
547
547
  asset_id: The ID of the asset to delete.
548
548
 
549
549
  Raises:
550
- NotFoundError: If the asset does not exist.
551
- AuthorizationError: If you don't have appropriate access.
550
+ NotFoundError: If the asset does not exist or belongs to a different user.
551
+ AuthorizationError: If the acting user is neither ``user_id`` nor a
552
+ company admin.
552
553
  """
553
554
  self._delete(
554
555
  self._format_endpoint(Endpoints.USER_ASSET, user_id=user_id, asset_id=asset_id)
@@ -564,7 +565,7 @@ class SyncUsersService(SyncBaseService):
564
565
  Only image files are accepted: PNG, JPEG, GIF, and WEBP. Any other
565
566
  upload (documents, audio, video, archives, SVG, or a non-image payload
566
567
  mislabeled as an image) is rejected with 400 Bad Request, as are
567
- oversized images that trip the decompression-bomb guard. The stored
568
+ images over 24 megapixels (width x height). The stored
568
569
  ``mime_type`` is detected from the file's bytes; the ``content_type``
569
570
  argument is ignored server-side and cannot change or bypass the stored
570
571
  type. Uploaded assets therefore always have ``asset_type == "image"``.
@@ -580,7 +581,8 @@ class SyncUsersService(SyncBaseService):
580
581
 
581
582
  Raises:
582
583
  NotFoundError: If the user does not exist.
583
- AuthorizationError: If you don't have appropriate access.
584
+ AuthorizationError: If the acting user is neither ``user_id`` nor a
585
+ company admin.
584
586
  ValidationError: If the file is invalid or exceeds size limits.
585
587
  """
586
588
  if content_type is None:
@@ -596,10 +598,10 @@ class SyncUsersService(SyncBaseService):
596
598
  ) -> User:
597
599
  """Upload a custom avatar for a user, replacing any existing one.
598
600
 
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).
601
+ Accepts PNG, JPEG, WEBP, or GIF (first frame) up to 5 MB and 24
602
+ megapixels. The server normalizes the image to a 512x512 WebP and it
603
+ overrides any identity-provider-derived avatar. Requires the
604
+ ``On-Behalf-Of`` header (permitted for the user themselves or an admin).
603
605
 
604
606
  Args:
605
607
  user_id: The ID of the user to set the avatar for.
@@ -667,7 +669,7 @@ class SyncUsersService(SyncBaseService):
667
669
  Args:
668
670
  user_id: The ID of the user to award XP to.
669
671
  campaign_id: The ID of the campaign to add XP for.
670
- amount: The amount of XP to add.
672
+ amount: The amount of XP to add. Must be at least 1.
671
673
 
672
674
  Returns:
673
675
  Updated CampaignExperience object.
@@ -692,7 +694,7 @@ class SyncUsersService(SyncBaseService):
692
694
  Args:
693
695
  user_id: The ID of the user to remove XP from.
694
696
  campaign_id: The ID of the campaign to remove XP for.
695
- amount: The amount of XP to remove.
697
+ amount: The amount of XP to remove. Must be at least 1.
696
698
 
697
699
  Returns:
698
700
  Updated CampaignExperience object.
@@ -719,7 +721,7 @@ class SyncUsersService(SyncBaseService):
719
721
  Args:
720
722
  user_id: The ID of the user to award cool points to.
721
723
  campaign_id: The ID of the campaign to add cool points for.
722
- amount: The amount of cool points to add.
724
+ amount: The amount of cool points to add. Must be at least 1.
723
725
 
724
726
  Returns:
725
727
  Updated CampaignExperience object.
@@ -969,7 +971,8 @@ class SyncUsersService(SyncBaseService):
969
971
  The Quickroll object with full details.
970
972
 
971
973
  Raises:
972
- NotFoundError: If the quickroll does not exist.
974
+ NotFoundError: If the quickroll does not exist or does not belong to
975
+ ``user_id``.
973
976
  AuthorizationError: If you don't have access to the company.
974
977
  """
975
978
  response = self._get(
@@ -1029,7 +1032,8 @@ class SyncUsersService(SyncBaseService):
1029
1032
  The updated Quickroll object.
1030
1033
 
1031
1034
  Raises:
1032
- NotFoundError: If the quickroll does not exist.
1035
+ NotFoundError: If the quickroll does not exist or does not belong to
1036
+ ``user_id``.
1033
1037
  AuthorizationError: If you don't have appropriate access.
1034
1038
  RequestValidationError: If the input parameters fail client-side validation.
1035
1039
  ValidationError: If the request data is invalid.
@@ -1053,7 +1057,8 @@ class SyncUsersService(SyncBaseService):
1053
1057
  quickroll_id: The ID of the quickroll to delete.
1054
1058
 
1055
1059
  Raises:
1056
- NotFoundError: If the quickroll does not exist.
1060
+ NotFoundError: If the quickroll does not exist or does not belong to
1061
+ ``user_id``.
1057
1062
  AuthorizationError: If you don't have appropriate access.
1058
1063
  """
1059
1064
  self._delete(
@@ -212,6 +212,15 @@ class VClient:
212
212
  """Check if the client has been closed."""
213
213
  return self._http.is_closed
214
214
 
215
+ def _set_api_key(self, api_key: str) -> None:
216
+ """Send a new API key on every later request from this client and its services.
217
+
218
+ Args:
219
+ api_key: The API key to authenticate with from now on.
220
+ """
221
+ self._config.api_key = api_key
222
+ self._http.headers[API_KEY_HEADER] = api_key
223
+
215
224
  @property
216
225
  def default_company_id(self) -> str | None:
217
226
  """Return the default company ID from config."""
@@ -106,4 +106,3 @@ BlueprintTraitOrderBy = Literal["NAME", "SHEET"]
106
106
  TraitModifyCurrency = Literal["XP", "STARTING_POINTS", "NO_COST"]
107
107
  TrackerType = Literal["COUNTER", "DAMAGE", "STAINS"]
108
108
  TrackerAction = Literal["spend", "gain", "damage", "heal", "add_stain", "remove_stain", "reset"]
109
- DamageType = Literal["SUPERFICIAL", "AGGRAVATED", "BASHING", "LETHAL"]
@@ -51,7 +51,6 @@ from .character_trait import (
51
51
  CharacterTraitSpecialtyUpdate,
52
52
  CharacterTraitValueOptionsResponse,
53
53
  CounterTracker,
54
- DamageTracker,
55
54
  Health,
56
55
  HealthLevel,
57
56
  HealthStateSet,
@@ -213,7 +212,6 @@ __all__ = [
213
212
  "CompanyUpdate",
214
213
  "CounterTracker",
215
214
  "CreateAutogenerateDTO",
216
- "DamageTracker",
217
215
  "Developer",
218
216
  "DeveloperCompanyPermission",
219
217
  "DeveloperCreate",