valentina-python-client 1.4.0__tar.gz → 1.5.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.
Files changed (47) hide show
  1. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/PKG-INFO +3 -9
  2. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/README.md +2 -8
  3. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/pyproject.toml +2 -1
  4. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/__init__.py +1 -1
  5. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/constants.py +2 -2
  6. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/__init__.py +2 -2
  7. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/shared.py +8 -8
  8. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/campaign_book_chapters.py +61 -12
  9. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/campaign_books.py +61 -12
  10. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/campaigns.py +61 -12
  11. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/characters.py +62 -13
  12. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/users.py +61 -12
  13. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/validate_constants.py +2 -2
  14. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/LICENSE +0 -0
  15. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/client.py +0 -0
  16. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/config.py +0 -0
  17. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/endpoints.py +0 -0
  18. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/exceptions.py +0 -0
  19. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/books.py +0 -0
  20. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/campaigns.py +0 -0
  21. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/chapters.py +0 -0
  22. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/character_autogen.py +0 -0
  23. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/character_blueprint.py +0 -0
  24. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/character_trait.py +0 -0
  25. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/characters.py +0 -0
  26. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/companies.py +0 -0
  27. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/developers.py +0 -0
  28. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/diceroll.py +0 -0
  29. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/dictionary.py +0 -0
  30. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/global_admin.py +0 -0
  31. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/pagination.py +0 -0
  32. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/system.py +0 -0
  33. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/models/users.py +0 -0
  34. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/py.typed +0 -0
  35. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/registry.py +0 -0
  36. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/__init__.py +0 -0
  37. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/base.py +0 -0
  38. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/character_autogen.py +0 -0
  39. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/character_blueprint.py +0 -0
  40. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/character_traits.py +0 -0
  41. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/companies.py +0 -0
  42. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/developers.py +0 -0
  43. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/dicerolls.py +0 -0
  44. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/dictionary.py +0 -0
  45. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/global_admin.py +0 -0
  46. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/options.py +0 -0
  47. {valentina_python_client-1.4.0 → valentina_python_client-1.5.0}/src/vclient/services/system.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valentina-python-client
3
- Version: 1.4.0
3
+ Version: 1.5.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>
@@ -38,11 +38,11 @@ Async Python client library for accessing the Valentina Noir API.
38
38
  - **Idempotency support** - Optional automatic idempotency keys for safe retries
39
39
  - **Rate limit handling** - Built-in support for automatic rate limit retries
40
40
 
41
- This client is a supported and up-to-date reference implementation for the Valentina Noir API. The full documentation for is available at https://docs.valentina-noir.com/python-api-client/.
41
+ This client is a supported and up-to-date reference implementation for the [Valentina Noir API](https://docs.valentina-noir.com).
42
42
 
43
43
  ## Documentation
44
44
 
45
- For complete documentation including configuration options, all available services, response models, and error handling, see the **[Full Documentation](https://docs.valentina-noir.com/python-api-client/)**.
45
+ The full documentation is available at https://natelandau.github.io/valentina-python-client/.
46
46
 
47
47
  ## Development Tools
48
48
 
@@ -65,9 +65,3 @@ The script reads configuration from (highest precedence first):
65
65
  3. A `.env.secret` file in the project root
66
66
 
67
67
  Exit codes: `0` = all constants match, `1` = mismatches found, `2` = missing configuration.
68
-
69
- ## Resources
70
-
71
- - [Full Client Documentation](https://docs.valentina-noir.com/python-api-client/)
72
- - [API Concepts](https://docs.valentina-noir.com/concepts/)
73
- - [API Reference](https://api.valentina-noir.com/docs)
@@ -12,11 +12,11 @@ Async Python client library for accessing the Valentina Noir API.
12
12
  - **Idempotency support** - Optional automatic idempotency keys for safe retries
13
13
  - **Rate limit handling** - Built-in support for automatic rate limit retries
14
14
 
15
- This client is a supported and up-to-date reference implementation for the Valentina Noir API. The full documentation for is available at https://docs.valentina-noir.com/python-api-client/.
15
+ This client is a supported and up-to-date reference implementation for the [Valentina Noir API](https://docs.valentina-noir.com).
16
16
 
17
17
  ## Documentation
18
18
 
19
- For complete documentation including configuration options, all available services, response models, and error handling, see the **[Full Documentation](https://docs.valentina-noir.com/python-api-client/)**.
19
+ The full documentation is available at https://natelandau.github.io/valentina-python-client/.
20
20
 
21
21
  ## Development Tools
22
22
 
@@ -39,9 +39,3 @@ The script reads configuration from (highest precedence first):
39
39
  3. A `.env.secret` file in the project root
40
40
 
41
41
  Exit codes: `0` = all constants match, `1` = mismatches found, `2` = missing configuration.
42
-
43
- ## Resources
44
-
45
- - [Full Client Documentation](https://docs.valentina-noir.com/python-api-client/)
46
- - [API Concepts](https://docs.valentina-noir.com/concepts/)
47
- - [API Reference](https://api.valentina-noir.com/docs)
@@ -10,7 +10,7 @@
10
10
  name = "valentina-python-client"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.13"
13
- version = "1.4.0"
13
+ version = "1.5.0"
14
14
 
15
15
  [project.urls]
16
16
  Homepage = "https://docs.valentina-noir.com/python-api-client/"
@@ -46,6 +46,7 @@
46
46
  "vulture>=2.14",
47
47
  "yamllint>=1.38.0",
48
48
  ]
49
+ docs = ["zensical>=0.0.23"]
49
50
 
50
51
  [tool.commitizen]
51
52
  bump_message = "bump(release): v$current_version → v$new_version"
@@ -71,4 +71,4 @@ __all__ = (
71
71
  "users_service",
72
72
  )
73
73
 
74
- __version__ = "1.4.0"
74
+ __version__ = "1.5.0"
@@ -55,10 +55,10 @@ HunterEdgeType = Literal["ASSETS", "APTITUDES", "ENDOWMENTS"]
55
55
  ManageCampaignPermission = Literal["UNRESTRICTED", "STORYTELLER"]
56
56
  PermissionLevel = Literal["USER", "ADMIN", "OWNER", "REVOKE"]
57
57
  RollResultType = Literal["SUCCESS", "FAILURE", "BOTCH", "CRITICAL", "OTHER"]
58
- S3AssetParentType = Literal[
58
+ AssetParentType = Literal[
59
59
  "character", "campaign", "campaignbook", "campaignchapter", "user", "company", "unknown"
60
60
  ]
61
- S3AssetType = Literal["image", "text", "audio", "video", "document", "archive", "other"]
61
+ AssetType = Literal["image", "text", "audio", "video", "document", "archive", "other"]
62
62
  SpecialtyType = Literal["ACTION", "OTHER", "PASSIVE", "RITUAL", "SPELL"]
63
63
  UserRole = Literal["ADMIN", "STORYTELLER", "PLAYER"]
64
64
  WerewolfRenown = Literal["HONOR", "GLORY", "WISDOM"]
@@ -95,13 +95,13 @@ from .global_admin import (
95
95
  )
96
96
  from .pagination import PaginatedResponse
97
97
  from .shared import (
98
+ Asset,
98
99
  CharacterSpecialty,
99
100
  NameDescriptionSubDocument,
100
101
  Note,
101
102
  NoteCreate,
102
103
  NoteUpdate,
103
104
  RollStatistics,
104
- S3Asset,
105
105
  Trait,
106
106
  WerewolfGift,
107
107
  WerewolfRite,
@@ -120,6 +120,7 @@ from .users import (
120
120
  )
121
121
 
122
122
  __all__ = [
123
+ "Asset",
123
124
  "BookCreate",
124
125
  "BookUpdate",
125
126
  "Campaign",
@@ -183,7 +184,6 @@ __all__ = [
183
184
  "QuickrollCreate",
184
185
  "QuickrollUpdate",
185
186
  "RollStatistics",
186
- "S3Asset",
187
187
  "SheetSection",
188
188
  "SystemHealth",
189
189
  "Trait",
@@ -6,10 +6,10 @@ from typing import Annotated, Any
6
6
  from pydantic import BaseModel, Field
7
7
 
8
8
  from vclient.constants import (
9
+ AssetParentType,
10
+ AssetType,
9
11
  CharacterClass,
10
12
  GameVersion,
11
- S3AssetParentType,
12
- S3AssetType,
13
13
  SpecialtyType,
14
14
  WerewolfRenown,
15
15
  )
@@ -31,22 +31,22 @@ class NameDescriptionSubDocument(BaseModel):
31
31
  # -----------------------------------------------------------------------------
32
32
 
33
33
 
34
- class S3Asset(BaseModel):
35
- """Response model for an S3 asset.
34
+ class Asset(BaseModel):
35
+ """Response model for an asset.
36
36
 
37
- Represents a file asset stored in S3, including its URL and metadata.
37
+ Represents a file asset, including its URL and metadata.
38
38
  """
39
39
 
40
40
  id: str
41
41
  date_created: datetime
42
42
  date_modified: datetime
43
- asset_type: S3AssetType
43
+ asset_type: AssetType
44
44
  mime_type: str
45
45
  original_filename: str
46
46
  public_url: str
47
47
  uploaded_by: str
48
48
  company_id: str
49
- parent_type: S3AssetParentType | None = None
49
+ parent_type: AssetParentType | None = None
50
50
  parent_id: str | None = None
51
51
 
52
52
 
@@ -197,13 +197,13 @@ class CharacterSpecialty(BaseModel):
197
197
 
198
198
 
199
199
  __all__ = [
200
+ "Asset",
200
201
  "CharacterSpecialty",
201
202
  "NameDescriptionSubDocument",
202
203
  "Note",
203
204
  "NoteCreate",
204
205
  "NoteUpdate",
205
206
  "RollStatistics",
206
- "S3Asset",
207
207
  "Trait",
208
208
  "WerewolfGift",
209
209
  "WerewolfRite",
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
7
7
  from vclient.constants import DEFAULT_PAGE_LIMIT
8
8
  from vclient.endpoints import Endpoints
9
9
  from vclient.models import (
10
+ Asset,
10
11
  CampaignChapter,
11
12
  ChapterCreate,
12
13
  ChapterUpdate,
@@ -14,7 +15,6 @@ from vclient.models import (
14
15
  NoteCreate,
15
16
  NoteUpdate,
16
17
  PaginatedResponse,
17
- S3Asset,
18
18
  _ChapterRenumber,
19
19
  )
20
20
  from vclient.services.base import BaseService
@@ -342,14 +342,14 @@ class ChaptersService(BaseService):
342
342
  # Asset Methods
343
343
  # -------------------------------------------------------------------------
344
344
 
345
- async def list_assets(
345
+ async def get_assets_page(
346
346
  self,
347
347
  chapter_id: str,
348
348
  *,
349
349
  limit: int = DEFAULT_PAGE_LIMIT,
350
350
  offset: int = 0,
351
- ) -> PaginatedResponse[S3Asset]:
352
- """Retrieve a paginated list of assets for a chapter.
351
+ ) -> PaginatedResponse[Asset]:
352
+ """Retrieve a paginated page of assets for a chapter.
353
353
 
354
354
  Args:
355
355
  chapter_id: The ID of the chapter whose assets to list.
@@ -357,7 +357,7 @@ class ChaptersService(BaseService):
357
357
  offset: Number of items to skip from the beginning (default 0).
358
358
 
359
359
  Returns:
360
- A PaginatedResponse containing S3Asset objects and pagination metadata.
360
+ A PaginatedResponse containing Asset objects and pagination metadata.
361
361
 
362
362
  Raises:
363
363
  NotFoundError: If the chapter does not exist.
@@ -365,16 +365,65 @@ class ChaptersService(BaseService):
365
365
  """
366
366
  return await self._get_paginated_as(
367
367
  self._format_endpoint(Endpoints.BOOK_CHAPTER_ASSETS, chapter_id=chapter_id),
368
- S3Asset,
368
+ Asset,
369
369
  limit=limit,
370
370
  offset=offset,
371
371
  )
372
372
 
373
+ async def list_all_assets(
374
+ self,
375
+ chapter_id: str,
376
+ ) -> list[Asset]:
377
+ """Retrieve all assets for a chapter.
378
+
379
+ Automatically paginates through all results. Use `get_assets_page()` for paginated
380
+ access or `iter_all_assets()` for memory-efficient streaming of large datasets.
381
+
382
+ Args:
383
+ chapter_id: The ID of the chapter whose assets to list.
384
+
385
+ Returns:
386
+ A list of all Asset objects.
387
+
388
+ Raises:
389
+ NotFoundError: If the chapter does not exist.
390
+ AuthorizationError: If you don't have access.
391
+ """
392
+ return [asset async for asset in self.iter_all_assets(chapter_id)]
393
+
394
+ async def iter_all_assets(
395
+ self,
396
+ chapter_id: str,
397
+ *,
398
+ limit: int = 100,
399
+ ) -> AsyncIterator[Asset]:
400
+ """Iterate through all assets for a chapter.
401
+
402
+ Yields individual assets, automatically fetching subsequent pages until
403
+ all items have been retrieved.
404
+
405
+ Args:
406
+ chapter_id: The ID of the chapter whose assets to iterate.
407
+ limit: Items per page (default 100 for efficiency).
408
+
409
+ Yields:
410
+ Individual Asset objects.
411
+
412
+ Example:
413
+ >>> async for asset in chapters.iter_all_assets("chapter_id"):
414
+ ... print(asset.original_filename)
415
+ """
416
+ async for item in self._iter_all_pages(
417
+ self._format_endpoint(Endpoints.BOOK_CHAPTER_ASSETS, chapter_id=chapter_id),
418
+ limit=limit,
419
+ ):
420
+ yield Asset.model_validate(item)
421
+
373
422
  async def get_asset(
374
423
  self,
375
424
  chapter_id: str,
376
425
  asset_id: str,
377
- ) -> S3Asset:
426
+ ) -> Asset:
378
427
  """Retrieve details of a specific asset including its URL and metadata.
379
428
 
380
429
  Args:
@@ -382,7 +431,7 @@ class ChaptersService(BaseService):
382
431
  asset_id: The ID of the asset to retrieve.
383
432
 
384
433
  Returns:
385
- The S3Asset object with full details.
434
+ The Asset object with full details.
386
435
 
387
436
  Raises:
388
437
  NotFoundError: If the asset does not exist.
@@ -393,7 +442,7 @@ class ChaptersService(BaseService):
393
442
  Endpoints.BOOK_CHAPTER_ASSET, chapter_id=chapter_id, asset_id=asset_id
394
443
  )
395
444
  )
396
- return S3Asset.model_validate(response.json())
445
+ return Asset.model_validate(response.json())
397
446
 
398
447
  async def upload_asset(
399
448
  self,
@@ -401,7 +450,7 @@ class ChaptersService(BaseService):
401
450
  filename: str,
402
451
  content: bytes,
403
452
  content_type: str | None = None,
404
- ) -> S3Asset:
453
+ ) -> Asset:
405
454
  """Upload a new asset for a chapter.
406
455
 
407
456
  Uploads a file to S3 storage and associates it with the chapter.
@@ -413,7 +462,7 @@ class ChaptersService(BaseService):
413
462
  content_type: The MIME type of the file. If not provided, inferred from filename.
414
463
 
415
464
  Returns:
416
- The created S3Asset object with the public URL and metadata.
465
+ The created Asset object with the public URL and metadata.
417
466
 
418
467
  Raises:
419
468
  NotFoundError: If the chapter does not exist.
@@ -427,7 +476,7 @@ class ChaptersService(BaseService):
427
476
  self._format_endpoint(Endpoints.BOOK_CHAPTER_ASSET_UPLOAD, chapter_id=chapter_id),
428
477
  file=(filename, content, content_type),
429
478
  )
430
- return S3Asset.model_validate(response.json())
479
+ return Asset.model_validate(response.json())
431
480
 
432
481
  async def delete_asset(
433
482
  self,
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
7
7
  from vclient.constants import DEFAULT_PAGE_LIMIT
8
8
  from vclient.endpoints import Endpoints
9
9
  from vclient.models import (
10
+ Asset,
10
11
  BookCreate,
11
12
  BookUpdate,
12
13
  CampaignBook,
@@ -14,7 +15,6 @@ from vclient.models import (
14
15
  NoteCreate,
15
16
  NoteUpdate,
16
17
  PaginatedResponse,
17
- S3Asset,
18
18
  _BookRenumber,
19
19
  )
20
20
  from vclient.services.base import BaseService
@@ -438,14 +438,14 @@ class BooksService(BaseService):
438
438
  # Asset Methods
439
439
  # -------------------------------------------------------------------------
440
440
 
441
- async def list_assets(
441
+ async def get_assets_page(
442
442
  self,
443
443
  book_id: str,
444
444
  *,
445
445
  limit: int = DEFAULT_PAGE_LIMIT,
446
446
  offset: int = 0,
447
- ) -> PaginatedResponse[S3Asset]:
448
- """Retrieve a paginated list of assets for a book.
447
+ ) -> PaginatedResponse[Asset]:
448
+ """Retrieve a paginated page of assets for a book.
449
449
 
450
450
  Args:
451
451
  book_id: The ID of the book whose assets to list.
@@ -453,7 +453,7 @@ class BooksService(BaseService):
453
453
  offset: Number of items to skip from the beginning (default 0).
454
454
 
455
455
  Returns:
456
- A PaginatedResponse containing S3Asset objects and pagination metadata.
456
+ A PaginatedResponse containing Asset objects and pagination metadata.
457
457
 
458
458
  Raises:
459
459
  NotFoundError: If the book does not exist.
@@ -461,16 +461,65 @@ class BooksService(BaseService):
461
461
  """
462
462
  return await self._get_paginated_as(
463
463
  self._format_endpoint(Endpoints.BOOK_ASSETS, book_id=book_id),
464
- S3Asset,
464
+ Asset,
465
465
  limit=limit,
466
466
  offset=offset,
467
467
  )
468
468
 
469
+ async def list_all_assets(
470
+ self,
471
+ book_id: str,
472
+ ) -> list[Asset]:
473
+ """Retrieve all assets for a book.
474
+
475
+ Automatically paginates through all results. Use `get_assets_page()` for paginated
476
+ access or `iter_all_assets()` for memory-efficient streaming of large datasets.
477
+
478
+ Args:
479
+ book_id: The ID of the book whose assets to list.
480
+
481
+ Returns:
482
+ A list of all Asset objects.
483
+
484
+ Raises:
485
+ NotFoundError: If the book does not exist.
486
+ AuthorizationError: If you don't have access.
487
+ """
488
+ return [asset async for asset in self.iter_all_assets(book_id)]
489
+
490
+ async def iter_all_assets(
491
+ self,
492
+ book_id: str,
493
+ *,
494
+ limit: int = 100,
495
+ ) -> AsyncIterator[Asset]:
496
+ """Iterate through all assets for a book.
497
+
498
+ Yields individual assets, automatically fetching subsequent pages until
499
+ all items have been retrieved.
500
+
501
+ Args:
502
+ book_id: The ID of the book whose assets to iterate.
503
+ limit: Items per page (default 100 for efficiency).
504
+
505
+ Yields:
506
+ Individual Asset objects.
507
+
508
+ Example:
509
+ >>> async for asset in books.iter_all_assets("book_id"):
510
+ ... print(asset.original_filename)
511
+ """
512
+ async for item in self._iter_all_pages(
513
+ self._format_endpoint(Endpoints.BOOK_ASSETS, book_id=book_id),
514
+ limit=limit,
515
+ ):
516
+ yield Asset.model_validate(item)
517
+
469
518
  async def get_asset(
470
519
  self,
471
520
  book_id: str,
472
521
  asset_id: str,
473
- ) -> S3Asset:
522
+ ) -> Asset:
474
523
  """Retrieve details of a specific asset including its URL and metadata.
475
524
 
476
525
  Args:
@@ -478,7 +527,7 @@ class BooksService(BaseService):
478
527
  asset_id: The ID of the asset to retrieve.
479
528
 
480
529
  Returns:
481
- The S3Asset object with full details.
530
+ The Asset object with full details.
482
531
 
483
532
  Raises:
484
533
  NotFoundError: If the asset does not exist.
@@ -487,7 +536,7 @@ class BooksService(BaseService):
487
536
  response = await self._get(
488
537
  self._format_endpoint(Endpoints.BOOK_ASSET, book_id=book_id, asset_id=asset_id)
489
538
  )
490
- return S3Asset.model_validate(response.json())
539
+ return Asset.model_validate(response.json())
491
540
 
492
541
  async def upload_asset(
493
542
  self,
@@ -495,7 +544,7 @@ class BooksService(BaseService):
495
544
  filename: str,
496
545
  content: bytes,
497
546
  content_type: str | None = None,
498
- ) -> S3Asset:
547
+ ) -> Asset:
499
548
  """Upload a new asset for a book.
500
549
 
501
550
  Uploads a file to S3 storage and associates it with the book.
@@ -507,7 +556,7 @@ class BooksService(BaseService):
507
556
  content_type: The MIME type of the file. If not provided, inferred from filename.
508
557
 
509
558
  Returns:
510
- The created S3Asset object with the public URL and metadata.
559
+ The created Asset object with the public URL and metadata.
511
560
 
512
561
  Raises:
513
562
  NotFoundError: If the book does not exist.
@@ -521,7 +570,7 @@ class BooksService(BaseService):
521
570
  self._format_endpoint(Endpoints.BOOK_ASSET_UPLOAD, book_id=book_id),
522
571
  file=(filename, content, content_type),
523
572
  )
524
- return S3Asset.model_validate(response.json())
573
+ return Asset.model_validate(response.json())
525
574
 
526
575
  async def delete_asset(
527
576
  self,
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
7
7
  from vclient.constants import DEFAULT_PAGE_LIMIT
8
8
  from vclient.endpoints import Endpoints
9
9
  from vclient.models import (
10
+ Asset,
10
11
  Campaign,
11
12
  CampaignCreate,
12
13
  CampaignUpdate,
@@ -15,7 +16,6 @@ from vclient.models import (
15
16
  NoteUpdate,
16
17
  PaginatedResponse,
17
18
  RollStatistics,
18
- S3Asset,
19
19
  )
20
20
  from vclient.services.base import BaseService
21
21
 
@@ -249,14 +249,14 @@ class CampaignsService(BaseService):
249
249
  # Asset Methods
250
250
  # -------------------------------------------------------------------------
251
251
 
252
- async def list_assets(
252
+ async def get_assets_page(
253
253
  self,
254
254
  campaign_id: str,
255
255
  *,
256
256
  limit: int = DEFAULT_PAGE_LIMIT,
257
257
  offset: int = 0,
258
- ) -> PaginatedResponse[S3Asset]:
259
- """Retrieve a paginated list of assets for a campaign.
258
+ ) -> PaginatedResponse[Asset]:
259
+ """Retrieve a paginated page of assets for a campaign.
260
260
 
261
261
  Args:
262
262
  campaign_id: The ID of the campaign whose assets to list.
@@ -264,7 +264,7 @@ class CampaignsService(BaseService):
264
264
  offset: Number of items to skip from the beginning (default 0).
265
265
 
266
266
  Returns:
267
- A PaginatedResponse containing S3Asset objects and pagination metadata.
267
+ A PaginatedResponse containing Asset objects and pagination metadata.
268
268
 
269
269
  Raises:
270
270
  NotFoundError: If the campaign does not exist.
@@ -272,16 +272,65 @@ class CampaignsService(BaseService):
272
272
  """
273
273
  return await self._get_paginated_as(
274
274
  self._format_endpoint(Endpoints.CAMPAIGN_ASSETS, campaign_id=campaign_id),
275
- S3Asset,
275
+ Asset,
276
276
  limit=limit,
277
277
  offset=offset,
278
278
  )
279
279
 
280
+ async def list_all_assets(
281
+ self,
282
+ campaign_id: str,
283
+ ) -> list[Asset]:
284
+ """Retrieve all assets for a campaign.
285
+
286
+ Automatically paginates through all results. Use `get_assets_page()` for paginated
287
+ access or `iter_all_assets()` for memory-efficient streaming of large datasets.
288
+
289
+ Args:
290
+ campaign_id: The ID of the campaign whose assets to list.
291
+
292
+ Returns:
293
+ A list of all Asset objects.
294
+
295
+ Raises:
296
+ NotFoundError: If the campaign does not exist.
297
+ AuthorizationError: If you don't have access.
298
+ """
299
+ return [asset async for asset in self.iter_all_assets(campaign_id)]
300
+
301
+ async def iter_all_assets(
302
+ self,
303
+ campaign_id: str,
304
+ *,
305
+ limit: int = 100,
306
+ ) -> AsyncIterator[Asset]:
307
+ """Iterate through all assets for a campaign.
308
+
309
+ Yields individual assets, automatically fetching subsequent pages until
310
+ all items have been retrieved.
311
+
312
+ Args:
313
+ campaign_id: The ID of the campaign whose assets to iterate.
314
+ limit: Items per page (default 100 for efficiency).
315
+
316
+ Yields:
317
+ Individual Asset objects.
318
+
319
+ Example:
320
+ >>> async for asset in campaigns.iter_all_assets("campaign_id"):
321
+ ... print(asset.original_filename)
322
+ """
323
+ async for item in self._iter_all_pages(
324
+ self._format_endpoint(Endpoints.CAMPAIGN_ASSETS, campaign_id=campaign_id),
325
+ limit=limit,
326
+ ):
327
+ yield Asset.model_validate(item)
328
+
280
329
  async def get_asset(
281
330
  self,
282
331
  campaign_id: str,
283
332
  asset_id: str,
284
- ) -> S3Asset:
333
+ ) -> Asset:
285
334
  """Retrieve details of a specific asset including its URL and metadata.
286
335
 
287
336
  Args:
@@ -289,7 +338,7 @@ class CampaignsService(BaseService):
289
338
  asset_id: The ID of the asset to retrieve.
290
339
 
291
340
  Returns:
292
- The S3Asset object with full details.
341
+ The Asset object with full details.
293
342
 
294
343
  Raises:
295
344
  NotFoundError: If the asset does not exist.
@@ -300,7 +349,7 @@ class CampaignsService(BaseService):
300
349
  Endpoints.CAMPAIGN_ASSET, campaign_id=campaign_id, asset_id=asset_id
301
350
  )
302
351
  )
303
- return S3Asset.model_validate(response.json())
352
+ return Asset.model_validate(response.json())
304
353
 
305
354
  async def delete_asset(
306
355
  self,
@@ -331,7 +380,7 @@ class CampaignsService(BaseService):
331
380
  filename: str,
332
381
  content: bytes,
333
382
  content_type: str | None = None,
334
- ) -> S3Asset:
383
+ ) -> Asset:
335
384
  """Upload a new asset for a campaign.
336
385
 
337
386
  Uploads a file to S3 storage and associates it with the campaign.
@@ -343,7 +392,7 @@ class CampaignsService(BaseService):
343
392
  content_type: The MIME type of the file. If not provided, inferred from filename.
344
393
 
345
394
  Returns:
346
- The created S3Asset object with the public URL and metadata.
395
+ The created Asset object with the public URL and metadata.
347
396
 
348
397
  Raises:
349
398
  NotFoundError: If the campaign does not exist.
@@ -357,7 +406,7 @@ class CampaignsService(BaseService):
357
406
  self._format_endpoint(Endpoints.CAMPAIGN_ASSET_UPLOAD, campaign_id=campaign_id),
358
407
  file=(filename, content, content_type),
359
408
  )
360
- return S3Asset.model_validate(response.json())
409
+ return Asset.model_validate(response.json())
361
410
 
362
411
  # -------------------------------------------------------------------------
363
412
  # Notes Methods
@@ -12,6 +12,7 @@ from vclient.constants import (
12
12
  )
13
13
  from vclient.endpoints import Endpoints
14
14
  from vclient.models import (
15
+ Asset,
15
16
  Character,
16
17
  CharacterCreate,
17
18
  CharacterUpdate,
@@ -25,7 +26,6 @@ from vclient.models import (
25
26
  PaginatedResponse,
26
27
  Perk,
27
28
  RollStatistics,
28
- S3Asset,
29
29
  WerewolfGift,
30
30
  WerewolfRite,
31
31
  )
@@ -333,14 +333,14 @@ class CharactersService(BaseService):
333
333
  # Asset Methods
334
334
  # -------------------------------------------------------------------------
335
335
 
336
- async def list_assets(
336
+ async def get_assets_page(
337
337
  self,
338
338
  character_id: str,
339
339
  *,
340
340
  limit: int = DEFAULT_PAGE_LIMIT,
341
341
  offset: int = 0,
342
- ) -> PaginatedResponse[S3Asset]:
343
- """Retrieve a paginated list of assets for a campaign.
342
+ ) -> PaginatedResponse[Asset]:
343
+ """Retrieve a paginated page of assets for a character.
344
344
 
345
345
  Args:
346
346
  character_id: The ID of the character whose assets to list.
@@ -348,24 +348,73 @@ class CharactersService(BaseService):
348
348
  offset: Number of items to skip from the beginning (default 0).
349
349
 
350
350
  Returns:
351
- A PaginatedResponse containing S3Asset objects and pagination metadata.
351
+ A PaginatedResponse containing Asset objects and pagination metadata.
352
352
 
353
353
  Raises:
354
- NotFoundError: If the campaign does not exist.
354
+ NotFoundError: If the character does not exist.
355
355
  AuthorizationError: If you don't have access.
356
356
  """
357
357
  return await self._get_paginated_as(
358
358
  self._format_endpoint(Endpoints.CHARACTER_ASSETS, character_id=character_id),
359
- S3Asset,
359
+ Asset,
360
360
  limit=limit,
361
361
  offset=offset,
362
362
  )
363
363
 
364
+ async def list_all_assets(
365
+ self,
366
+ character_id: str,
367
+ ) -> list[Asset]:
368
+ """Retrieve all assets for a character.
369
+
370
+ Automatically paginates through all results. Use `get_assets_page()` for paginated
371
+ access or `iter_all_assets()` for memory-efficient streaming of large datasets.
372
+
373
+ Args:
374
+ character_id: The ID of the character whose assets to list.
375
+
376
+ Returns:
377
+ A list of all Asset objects.
378
+
379
+ Raises:
380
+ NotFoundError: If the character does not exist.
381
+ AuthorizationError: If you don't have access.
382
+ """
383
+ return [asset async for asset in self.iter_all_assets(character_id)]
384
+
385
+ async def iter_all_assets(
386
+ self,
387
+ character_id: str,
388
+ *,
389
+ limit: int = 100,
390
+ ) -> AsyncIterator[Asset]:
391
+ """Iterate through all assets for a character.
392
+
393
+ Yields individual assets, automatically fetching subsequent pages until
394
+ all items have been retrieved.
395
+
396
+ Args:
397
+ character_id: The ID of the character whose assets to iterate.
398
+ limit: Items per page (default 100 for efficiency).
399
+
400
+ Yields:
401
+ Individual Asset objects.
402
+
403
+ Example:
404
+ >>> async for asset in characters.iter_all_assets("character_id"):
405
+ ... print(asset.original_filename)
406
+ """
407
+ async for item in self._iter_all_pages(
408
+ self._format_endpoint(Endpoints.CHARACTER_ASSETS, character_id=character_id),
409
+ limit=limit,
410
+ ):
411
+ yield Asset.model_validate(item)
412
+
364
413
  async def get_asset(
365
414
  self,
366
415
  character_id: str,
367
416
  asset_id: str,
368
- ) -> S3Asset:
417
+ ) -> Asset:
369
418
  """Retrieve details of a specific asset including its URL and metadata.
370
419
 
371
420
  Args:
@@ -373,7 +422,7 @@ class CharactersService(BaseService):
373
422
  asset_id: The ID of the asset to retrieve.
374
423
 
375
424
  Returns:
376
- The S3Asset object with full details.
425
+ The Asset object with full details.
377
426
 
378
427
  Raises:
379
428
  NotFoundError: If the asset does not exist.
@@ -384,7 +433,7 @@ class CharactersService(BaseService):
384
433
  Endpoints.CHARACTER_ASSET, character_id=character_id, asset_id=asset_id
385
434
  )
386
435
  )
387
- return S3Asset.model_validate(response.json())
436
+ return Asset.model_validate(response.json())
388
437
 
389
438
  async def delete_asset(
390
439
  self,
@@ -415,7 +464,7 @@ class CharactersService(BaseService):
415
464
  filename: str,
416
465
  content: bytes,
417
466
  content_type: str | None = None,
418
- ) -> S3Asset:
467
+ ) -> Asset:
419
468
  """Upload a new asset for a campaign.
420
469
 
421
470
  Uploads a file to S3 storage and associates it with the campaign.
@@ -427,7 +476,7 @@ class CharactersService(BaseService):
427
476
  content_type: The MIME type of the file. If not provided, inferred from filename.
428
477
 
429
478
  Returns:
430
- The created S3Asset object with the public URL and metadata.
479
+ The created Asset object with the public URL and metadata.
431
480
 
432
481
  Raises:
433
482
  NotFoundError: If the character does not exist.
@@ -441,7 +490,7 @@ class CharactersService(BaseService):
441
490
  self._format_endpoint(Endpoints.CHARACTER_ASSET_UPLOAD, character_id=character_id),
442
491
  file=(filename, content, content_type),
443
492
  )
444
- return S3Asset.model_validate(response.json())
493
+ return Asset.model_validate(response.json())
445
494
 
446
495
  # -------------------------------------------------------------------------
447
496
  # Notes Methods
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
7
7
  from vclient.constants import DEFAULT_PAGE_LIMIT, UserRole
8
8
  from vclient.endpoints import Endpoints
9
9
  from vclient.models import (
10
+ Asset,
10
11
  CampaignExperience,
11
12
  Note,
12
13
  NoteCreate,
@@ -16,7 +17,6 @@ from vclient.models import (
16
17
  QuickrollCreate,
17
18
  QuickrollUpdate,
18
19
  RollStatistics,
19
- S3Asset,
20
20
  User,
21
21
  UserCreate,
22
22
  UserUpdate,
@@ -275,14 +275,14 @@ class UsersService(BaseService):
275
275
  # Asset Methods
276
276
  # -------------------------------------------------------------------------
277
277
 
278
- async def list_assets(
278
+ async def get_assets_page(
279
279
  self,
280
280
  user_id: str,
281
281
  *,
282
282
  limit: int = DEFAULT_PAGE_LIMIT,
283
283
  offset: int = 0,
284
- ) -> PaginatedResponse[S3Asset]:
285
- """Retrieve a paginated list of assets for a user.
284
+ ) -> PaginatedResponse[Asset]:
285
+ """Retrieve a paginated page of assets for a user.
286
286
 
287
287
  Args:
288
288
  user_id: The ID of the user whose assets to list.
@@ -290,7 +290,7 @@ class UsersService(BaseService):
290
290
  offset: Number of items to skip from the beginning (default 0).
291
291
 
292
292
  Returns:
293
- A PaginatedResponse containing S3Asset objects and pagination metadata.
293
+ A PaginatedResponse containing Asset objects and pagination metadata.
294
294
 
295
295
  Raises:
296
296
  NotFoundError: If the user does not exist.
@@ -298,16 +298,65 @@ class UsersService(BaseService):
298
298
  """
299
299
  return await self._get_paginated_as(
300
300
  self._format_endpoint(Endpoints.USER_ASSETS, user_id=user_id),
301
- S3Asset,
301
+ Asset,
302
302
  limit=limit,
303
303
  offset=offset,
304
304
  )
305
305
 
306
+ async def list_all_assets(
307
+ self,
308
+ user_id: str,
309
+ ) -> list[Asset]:
310
+ """Retrieve all assets for a user.
311
+
312
+ Automatically paginates through all results. Use `get_assets_page()` for paginated
313
+ access or `iter_all_assets()` for memory-efficient streaming of large datasets.
314
+
315
+ Args:
316
+ user_id: The ID of the user whose assets to list.
317
+
318
+ Returns:
319
+ A list of all Asset objects.
320
+
321
+ Raises:
322
+ NotFoundError: If the user does not exist.
323
+ AuthorizationError: If you don't have access to the company.
324
+ """
325
+ return [asset async for asset in self.iter_all_assets(user_id)]
326
+
327
+ async def iter_all_assets(
328
+ self,
329
+ user_id: str,
330
+ *,
331
+ limit: int = 100,
332
+ ) -> AsyncIterator[Asset]:
333
+ """Iterate through all assets for a user.
334
+
335
+ Yields individual assets, automatically fetching subsequent pages until
336
+ all items have been retrieved.
337
+
338
+ Args:
339
+ user_id: The ID of the user whose assets to iterate.
340
+ limit: Items per page (default 100 for efficiency).
341
+
342
+ Yields:
343
+ Individual Asset objects.
344
+
345
+ Example:
346
+ >>> async for asset in users.iter_all_assets("user_id"):
347
+ ... print(asset.original_filename)
348
+ """
349
+ async for item in self._iter_all_pages(
350
+ self._format_endpoint(Endpoints.USER_ASSETS, user_id=user_id),
351
+ limit=limit,
352
+ ):
353
+ yield Asset.model_validate(item)
354
+
306
355
  async def get_asset(
307
356
  self,
308
357
  user_id: str,
309
358
  asset_id: str,
310
- ) -> S3Asset:
359
+ ) -> Asset:
311
360
  """Retrieve details of a specific asset including its URL and metadata.
312
361
 
313
362
  Args:
@@ -315,7 +364,7 @@ class UsersService(BaseService):
315
364
  asset_id: The ID of the asset to retrieve.
316
365
 
317
366
  Returns:
318
- The S3Asset object with full details.
367
+ The Asset object with full details.
319
368
 
320
369
  Raises:
321
370
  NotFoundError: If the asset does not exist.
@@ -324,7 +373,7 @@ class UsersService(BaseService):
324
373
  response = await self._get(
325
374
  self._format_endpoint(Endpoints.USER_ASSET, user_id=user_id, asset_id=asset_id)
326
375
  )
327
- return S3Asset.model_validate(response.json())
376
+ return Asset.model_validate(response.json())
328
377
 
329
378
  async def delete_asset(
330
379
  self,
@@ -353,7 +402,7 @@ class UsersService(BaseService):
353
402
  filename: str,
354
403
  content: bytes,
355
404
  content_type: str | None = None,
356
- ) -> S3Asset:
405
+ ) -> Asset:
357
406
  """Upload a new asset for a user.
358
407
 
359
408
  Uploads a file to S3 storage and associates it with the user.
@@ -365,7 +414,7 @@ class UsersService(BaseService):
365
414
  content_type: The MIME type of the file. If not provided, inferred from filename.
366
415
 
367
416
  Returns:
368
- The created S3Asset object with the public URL and metadata.
417
+ The created Asset object with the public URL and metadata.
369
418
 
370
419
  Raises:
371
420
  NotFoundError: If the user does not exist.
@@ -379,7 +428,7 @@ class UsersService(BaseService):
379
428
  self._format_endpoint(Endpoints.USER_ASSET_UPLOAD, user_id=user_id),
380
429
  file=(filename, content, content_type),
381
430
  )
382
- return S3Asset.model_validate(response.json())
431
+ return Asset.model_validate(response.json())
383
432
 
384
433
  # -------------------------------------------------------------------------
385
434
  # Experience Methods
@@ -77,8 +77,8 @@ CONSTANT_MAP: dict[str, ConstantMapping] = {
77
77
  "ManageCampaignPermission": ConstantMapping("companies", "PermissionManageCampaign"),
78
78
  "PermissionLevel": ConstantMapping("companies", "CompanyPermission"),
79
79
  "RollResultType": ConstantMapping("gameplay", "RollResultType"),
80
- "S3AssetParentType": ConstantMapping("assets", "AssetParentType"),
81
- "S3AssetType": ConstantMapping("assets", "AssetType"),
80
+ "AssetParentType": ConstantMapping("assets", "AssetParentType"),
81
+ "AssetType": ConstantMapping("assets", "AssetType"),
82
82
  "SpecialtyType": ConstantMapping("characters", "SpecialtyType"),
83
83
  "TraitModifyCurrency": ConstantMapping("characters", "TraitModifyCurrency"),
84
84
  "UserRole": ConstantMapping("users", "UserRole"),