valentina-python-client 1.19.0__tar.gz → 1.20.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 (76) hide show
  1. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/PKG-INFO +1 -1
  2. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/pyproject.toml +1 -1
  3. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/__init__.py +1 -1
  4. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/campaign_book_chapters.py +28 -6
  5. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/campaign_books.py +16 -7
  6. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/users.py +54 -15
  7. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/constants.py +5 -1
  8. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/__init__.py +6 -0
  9. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/books.py +23 -0
  10. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/chapters.py +19 -0
  11. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/characters.py +2 -6
  12. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/companies.py +3 -1
  13. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/users.py +30 -0
  14. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/campaign_book_chapters.py +29 -6
  15. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/campaign_books.py +19 -7
  16. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/users.py +53 -11
  17. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/testing/__init__.py +6 -0
  18. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/testing/_factories.py +22 -0
  19. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/testing/_router.py +9 -0
  20. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/testing/_routes.py +6 -3
  21. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/validate_constants.py +1 -0
  22. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/LICENSE +0 -0
  23. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/README.md +0 -0
  24. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_codegen.py +0 -0
  25. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/__init__.py +0 -0
  26. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/client.py +0 -0
  27. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/registry.py +0 -0
  28. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/__init__.py +0 -0
  29. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/base.py +0 -0
  30. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/campaigns.py +0 -0
  31. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/character_autogen.py +0 -0
  32. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/character_blueprint.py +0 -0
  33. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/character_traits.py +0 -0
  34. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/characters.py +0 -0
  35. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/companies.py +0 -0
  36. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/developers.py +0 -0
  37. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/dicerolls.py +0 -0
  38. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/dictionary.py +0 -0
  39. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/global_admin.py +0 -0
  40. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/options.py +0 -0
  41. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/services/system.py +0 -0
  42. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/testing/__init__.py +0 -0
  43. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/_sync/testing/_client.py +0 -0
  44. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/client.py +0 -0
  45. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/config.py +0 -0
  46. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/endpoints.py +0 -0
  47. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/exceptions.py +0 -0
  48. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/campaigns.py +0 -0
  49. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/character_autogen.py +0 -0
  50. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/character_blueprint.py +0 -0
  51. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/character_trait.py +0 -0
  52. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/developers.py +0 -0
  53. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/diceroll.py +0 -0
  54. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/dictionary.py +0 -0
  55. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/full_sheet.py +0 -0
  56. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/global_admin.py +0 -0
  57. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/pagination.py +0 -0
  58. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/shared.py +0 -0
  59. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/models/system.py +0 -0
  60. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/py.typed +0 -0
  61. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/registry.py +0 -0
  62. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/__init__.py +0 -0
  63. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/base.py +0 -0
  64. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/campaigns.py +0 -0
  65. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/character_autogen.py +0 -0
  66. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/character_blueprint.py +0 -0
  67. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/character_traits.py +0 -0
  68. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/characters.py +0 -0
  69. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/companies.py +0 -0
  70. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/developers.py +0 -0
  71. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/dicerolls.py +0 -0
  72. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/dictionary.py +0 -0
  73. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/global_admin.py +0 -0
  74. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/options.py +0 -0
  75. {valentina_python_client-1.19.0 → valentina_python_client-1.20.1}/src/vclient/services/system.py +0 -0
  76. {valentina_python_client-1.19.0 → valentina_python_client-1.20.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: 1.19.0
3
+ Version: 1.20.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>
@@ -16,7 +16,7 @@
16
16
  name = "valentina-python-client"
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.13"
19
- version = "1.19.0"
19
+ version = "1.20.1"
20
20
 
21
21
  [project.optional-dependencies]
22
22
  testing = ["polyfactory>=3.3.0"]
@@ -105,4 +105,4 @@ __all__ = (
105
105
  "users_service",
106
106
  )
107
107
 
108
- __version__ = "1.19.0"
108
+ __version__ = "1.20.1"
@@ -2,15 +2,16 @@
2
2
  """Service for interacting with the Campaign Books Chapters API."""
3
3
 
4
4
  import mimetypes
5
- from collections.abc import Iterator
5
+ from collections.abc import Iterator, Sequence
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from vclient._sync.services.base import SyncBaseService
9
- from vclient.constants import DEFAULT_PAGE_LIMIT
9
+ from vclient.constants import DEFAULT_PAGE_LIMIT, ChapterInclude
10
10
  from vclient.endpoints import Endpoints
11
11
  from vclient.models import (
12
12
  Asset,
13
13
  CampaignChapter,
14
+ CampaignChapterDetail,
14
15
  ChapterCreate,
15
16
  ChapterUpdate,
16
17
  Note,
@@ -69,10 +70,31 @@ class SyncChaptersService(SyncBaseService):
69
70
  ):
70
71
  yield CampaignChapter.model_validate(item)
71
72
 
72
- def get(self, chapter_id: str) -> CampaignChapter:
73
- """Retrieve a specific campaign book chapter."""
74
- response = self._get(self._format_endpoint(Endpoints.BOOK_CHAPTER, chapter_id=chapter_id))
75
- return CampaignChapter.model_validate(response.json())
73
+ def get(
74
+ self, chapter_id: str, *, include: Sequence[ChapterInclude] | None = None
75
+ ) -> CampaignChapterDetail:
76
+ """Retrieve detailed information about a specific campaign book chapter.
77
+
78
+ Fetches the chapter and optionally embeds child resources directly in the
79
+ response, avoiding follow-up requests.
80
+
81
+ Args:
82
+ chapter_id: The ID of the chapter to retrieve.
83
+ include: Child resources to embed in the response. Valid values are
84
+ ``"notes"`` and ``"assets"``.
85
+
86
+ Returns:
87
+ The CampaignChapterDetail object with full details and any requested embeds.
88
+
89
+ Raises:
90
+ NotFoundError: If the chapter does not exist.
91
+ AuthorizationError: If you don't have access.
92
+ """
93
+ response = self._get(
94
+ self._format_endpoint(Endpoints.BOOK_CHAPTER, chapter_id=chapter_id),
95
+ params=self._build_params(include=list(include) if include else None),
96
+ )
97
+ return CampaignChapterDetail.model_validate(response.json())
76
98
 
77
99
  def create(self, request: ChapterCreate | None = None, **kwargs) -> CampaignChapter:
78
100
  """Create a new campaign book chapter.
@@ -2,17 +2,18 @@
2
2
  """Service for interacting with the Campaign Books API."""
3
3
 
4
4
  import mimetypes
5
- from collections.abc import Iterator
5
+ from collections.abc import Iterator, Sequence
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from vclient._sync.services.base import SyncBaseService
9
- from vclient.constants import DEFAULT_PAGE_LIMIT
9
+ from vclient.constants import DEFAULT_PAGE_LIMIT, BookInclude
10
10
  from vclient.endpoints import Endpoints
11
11
  from vclient.models import (
12
12
  Asset,
13
13
  BookCreate,
14
14
  BookUpdate,
15
15
  CampaignBook,
16
+ CampaignBookDetail,
16
17
  Note,
17
18
  NoteCreate,
18
19
  NoteUpdate,
@@ -116,23 +117,31 @@ class SyncBooksService(SyncBaseService):
116
117
  ):
117
118
  yield CampaignBook.model_validate(item)
118
119
 
119
- def get(self, book_id: str) -> CampaignBook:
120
+ def get(
121
+ self, book_id: str, *, include: Sequence[BookInclude] | None = None
122
+ ) -> CampaignBookDetail:
120
123
  """Retrieve detailed information about a specific campaign book.
121
124
 
122
- Fetches the book including its notes and assets.
125
+ Fetches the book and optionally embeds child resources directly in the
126
+ response, avoiding follow-up requests.
123
127
 
124
128
  Args:
125
129
  book_id: The ID of the book to retrieve.
130
+ include: Child resources to embed in the response. Valid values are
131
+ ``"chapters"``, ``"notes"``, and ``"assets"``.
126
132
 
127
133
  Returns:
128
- The CampaignBook object with full details.
134
+ The CampaignBookDetail object with full details and any requested embeds.
129
135
 
130
136
  Raises:
131
137
  NotFoundError: If the book does not exist.
132
138
  AuthorizationError: If you don't have access.
133
139
  """
134
- response = self._get(self._format_endpoint(Endpoints.CAMPAIGN_BOOK, book_id=book_id))
135
- return CampaignBook.model_validate(response.json())
140
+ response = self._get(
141
+ self._format_endpoint(Endpoints.CAMPAIGN_BOOK, book_id=book_id),
142
+ params=self._build_params(include=list(include) if include else None),
143
+ )
144
+ return CampaignBookDetail.model_validate(response.json())
136
145
 
137
146
  def create(self, request: BookCreate | None = None, **kwargs) -> CampaignBook:
138
147
  """Create a new campaign book.
@@ -2,11 +2,11 @@
2
2
  """Service for interacting with the Users API."""
3
3
 
4
4
  import mimetypes
5
- from collections.abc import Iterator
5
+ from collections.abc import Iterator, Sequence
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from vclient._sync.services.base import SyncBaseService
9
- from vclient.constants import DEFAULT_PAGE_LIMIT, UserRole
9
+ from vclient.constants import DEFAULT_PAGE_LIMIT, UserInclude, UserRole
10
10
  from vclient.endpoints import Endpoints
11
11
  from vclient.models import (
12
12
  Asset,
@@ -23,6 +23,7 @@ from vclient.models import (
23
23
  UserApproveDTO,
24
24
  UserCreate,
25
25
  UserDenyDTO,
26
+ UserDetail,
26
27
  UserMergeDTO,
27
28
  UserRegisterDTO,
28
29
  UserUpdate,
@@ -64,13 +65,14 @@ class SyncUsersService(SyncBaseService):
64
65
  return endpoint.format(company_id=self._company_id, **kwargs)
65
66
 
66
67
  def get_unapproved_page(
67
- self, *, limit: int = DEFAULT_PAGE_LIMIT, offset: int = 0
68
+ self, requesting_user_id: str, *, limit: int = DEFAULT_PAGE_LIMIT, offset: int = 0
68
69
  ) -> PaginatedResponse[User]:
69
70
  """Retrieve a paginated page of unapproved users within a company.
70
71
 
71
72
  Unapproved users have registered but have not yet been approved by an admin.
72
73
 
73
74
  Args:
75
+ requesting_user_id: ID of the user making the request (for permissions).
74
76
  limit: Maximum number of items to return (0-100, default 10).
75
77
  offset: Number of items to skip from the beginning (default 0).
76
78
 
@@ -78,40 +80,54 @@ class SyncUsersService(SyncBaseService):
78
80
  A PaginatedResponse containing User objects and pagination metadata.
79
81
  """
80
82
  return self._get_paginated_as(
81
- self._format_endpoint(Endpoints.USERS_UNAPPROVED_LIST), User, limit=limit, offset=offset
83
+ self._format_endpoint(Endpoints.USERS_UNAPPROVED_LIST),
84
+ User,
85
+ limit=limit,
86
+ offset=offset,
87
+ params=self._build_params(requesting_user_id=requesting_user_id),
82
88
  )
83
89
 
84
- def list_all_unapproved(self) -> list[User]:
90
+ def list_all_unapproved(self, requesting_user_id: str) -> list[User]:
85
91
  """Retrieve all unapproved users within a company.
86
92
 
87
93
  Automatically paginates through all results. Use `get_unapproved_page()` for
88
94
  paginated access or `iter_all_unapproved()` for memory-efficient streaming.
89
95
 
96
+ Args:
97
+ requesting_user_id: ID of the user making the request (for permissions).
98
+
90
99
  Returns:
91
100
  A list of all unapproved User objects.
92
101
  """
93
- return [user for user in self.iter_all_unapproved()]
102
+ return [user for user in self.iter_all_unapproved(requesting_user_id)]
94
103
 
95
- def iter_all_unapproved(self, *, limit: int = 100) -> Iterator[User]:
104
+ def iter_all_unapproved(self, requesting_user_id: str, *, limit: int = 100) -> Iterator[User]:
96
105
  """Iterate through all unapproved users within a company.
97
106
 
98
107
  Yields individual unapproved users, automatically fetching subsequent pages
99
108
  until all items have been retrieved.
100
109
 
101
110
  Args:
111
+ requesting_user_id: ID of the user making the request (for permissions).
102
112
  limit: Items per page (default 100 for efficiency).
103
113
 
104
114
  Yields:
105
115
  Individual User objects.
106
116
  """
107
117
  for item in self._iter_all_pages(
108
- self._format_endpoint(Endpoints.USERS_UNAPPROVED_LIST), limit=limit
118
+ self._format_endpoint(Endpoints.USERS_UNAPPROVED_LIST),
119
+ limit=limit,
120
+ params=self._build_params(requesting_user_id=requesting_user_id),
109
121
  ):
110
122
  yield User.model_validate(item)
111
123
 
112
124
  def approve_user(self, user_id: str, role: UserRole, requesting_user_id: str) -> User:
113
125
  """Approve an unapproved user and assign them a role.
114
126
 
127
+ The assigned ``role`` is validated through the server-side role-assignment
128
+ hierarchy — for example, a STORYTELLER cannot approve a user directly to
129
+ ADMIN, and only ADMIN may approve a user to ADMIN or DEACTIVATED.
130
+
115
131
  Args:
116
132
  user_id: The ID of the unapproved user to approve.
117
133
  role: The role to assign to the approved user.
@@ -122,7 +138,8 @@ class SyncUsersService(SyncBaseService):
122
138
 
123
139
  Raises:
124
140
  NotFoundError: If the user does not exist.
125
- AuthorizationError: If you don't have appropriate access.
141
+ AuthorizationError: If the requesting user lacks permission to assign
142
+ the requested role under the hierarchy.
126
143
  """
127
144
  body = UserApproveDTO(role=role, requesting_user_id=requesting_user_id)
128
145
  response = self._post(
@@ -251,23 +268,31 @@ class SyncUsersService(SyncBaseService):
251
268
  ):
252
269
  yield User.model_validate(item)
253
270
 
254
- def get(self, user_id: str) -> User:
271
+ def get(self, user_id: str, *, include: Sequence[UserInclude] | None = None) -> UserDetail:
255
272
  """Retrieve detailed information about a specific user.
256
273
 
257
- Fetches the user including their role, experience, and Discord profile.
274
+ Fetches the user and optionally embeds child resources directly in the
275
+ response, avoiding follow-up requests.
258
276
 
259
277
  Args:
260
278
  user_id: The ID of the user to retrieve.
279
+ include: Child resources to embed in the response. Valid values are
280
+ ``"quickrolls"``, ``"notes"``, ``"assets"``, and ``"characters"``.
281
+ ``"assets"`` returns assets attached to the user (not assets the user
282
+ uploaded). ``"characters"`` returns only characters the user plays.
261
283
 
262
284
  Returns:
263
- The User object with full details.
285
+ The UserDetail object with full details and any requested embeds.
264
286
 
265
287
  Raises:
266
288
  NotFoundError: If the user does not exist.
267
289
  AuthorizationError: If you don't have access to the company.
268
290
  """
269
- response = self._get(self._format_endpoint(Endpoints.USER, user_id=user_id))
270
- return User.model_validate(response.json())
291
+ response = self._get(
292
+ self._format_endpoint(Endpoints.USER, user_id=user_id),
293
+ params=self._build_params(include=list(include) if include else None),
294
+ )
295
+ return UserDetail.model_validate(response.json())
271
296
 
272
297
  def create(self, request: UserCreate | None = None, **kwargs) -> User:
273
298
  """Create a new user within a company.
@@ -276,6 +301,10 @@ class SyncUsersService(SyncBaseService):
276
301
  is optional and is not used for authentication but is included for Discord bot
277
302
  integration.
278
303
 
304
+ The initial ``role`` cannot be ``UNAPPROVED`` (use :meth:`register` for SSO
305
+ onboarding) or ``DEACTIVATED`` (not a creation path); either will surface as
306
+ ``ValidationError``.
307
+
279
308
  Args:
280
309
  request: A UserCreate model, OR pass fields as keyword arguments.
281
310
  **kwargs: Fields for UserCreate if request is not provided.
@@ -332,6 +361,14 @@ class SyncUsersService(SyncBaseService):
332
361
 
333
362
  Only include fields that need to be changed; omitted fields remain unchanged.
334
363
 
364
+ Setting ``role="DEACTIVATED"`` is the canonical way to deactivate a user:
365
+ the account can no longer log in or act, but their characters, assets, XP,
366
+ and notes remain intact and manageable by other users. Reactivate by calling
367
+ this same endpoint with any other valid role. Role changes are subject to a
368
+ server-side role-assignment hierarchy (e.g. only ADMIN may assign or remove
369
+ ADMIN/DEACTIVATED) and the server refuses to demote or deactivate the last
370
+ remaining active admin.
371
+
335
372
  Args:
336
373
  user_id: The ID of the user to update.
337
374
  request: A UserUpdate model, OR pass fields as keyword arguments.
@@ -345,7 +382,9 @@ class SyncUsersService(SyncBaseService):
345
382
 
346
383
  Raises:
347
384
  NotFoundError: If the user does not exist.
348
- AuthorizationError: If you don't have appropriate access.
385
+ AuthorizationError: If the requesting user lacks permission to make
386
+ the change or to assign the requested role under the hierarchy.
387
+ ConflictError: If the change would remove the last active admin.
349
388
  RequestValidationError: If the input parameters fail client-side validation.
350
389
  ValidationError: If the request data is invalid.
351
390
  """
@@ -38,6 +38,9 @@ AbilityFocus = Literal["JACK_OF_ALL_TRADES", "BALANCED", "SPECIALIST"]
38
38
  AutoGenExperienceLevel = Literal["NEW", "INTERMEDIATE", "ADVANCED", "ELITE"]
39
39
  CharacterClass = Literal["VAMPIRE", "WEREWOLF", "MAGE", "HUNTER", "GHOUL", "MORTAL"]
40
40
  CharacterInclude = Literal["traits", "inventory", "notes", "assets"]
41
+ BookInclude = Literal["chapters", "notes", "assets"]
42
+ ChapterInclude = Literal["notes", "assets"]
43
+ UserInclude = Literal["quickrolls", "notes", "assets", "characters"]
41
44
  CharacterInventoryType = Literal[
42
45
  "BOOK",
43
46
  "CONSUMABLE",
@@ -56,10 +59,11 @@ HunterCreed = Literal["ENTREPRENEURIAL", "FAITHFUL", "INQUISITIVE", "MARTIAL", "
56
59
  HunterEdgeType = Literal["ASSETS", "APTITUDES", "ENDOWMENTS"]
57
60
  ManageCampaignPermission = Literal["UNRESTRICTED", "STORYTELLER"]
58
61
  PermissionLevel = Literal["USER", "ADMIN", "OWNER", "REVOKE"]
62
+ RecoupXPPermission = Literal["UNRESTRICTED", "DENIED", "WITHIN_SESSION"]
59
63
  RollResultType = Literal["SUCCESS", "FAILURE", "BOTCH", "CRITICAL", "OTHER"]
60
64
  AssetType = Literal["image", "text", "audio", "video", "document", "archive", "other"]
61
65
  SpecialtyType = Literal["ACTION", "OTHER", "PASSIVE", "RITUAL", "SPELL"]
62
- UserRole = Literal["ADMIN", "STORYTELLER", "PLAYER", "UNAPPROVED"]
66
+ UserRole = Literal["ADMIN", "STORYTELLER", "PLAYER", "UNAPPROVED", "DEACTIVATED"]
63
67
  WerewolfRenown = Literal["HONOR", "GLORY", "WISDOM"]
64
68
  BlueprintTraitOrderBy = Literal["NAME", "SHEET"]
65
69
  TraitModifyCurrency = Literal["XP", "STARTING_POINTS", "NO_COST"]
@@ -5,6 +5,7 @@ from .books import (
5
5
  BookCreate,
6
6
  BookUpdate,
7
7
  CampaignBook,
8
+ CampaignBookDetail,
8
9
  _BookRenumber as _BookRenumber,
9
10
  )
10
11
  from .campaigns import (
@@ -14,6 +15,7 @@ from .campaigns import (
14
15
  )
15
16
  from .chapters import (
16
17
  CampaignChapter,
18
+ CampaignChapterDetail,
17
19
  ChapterCreate,
18
20
  ChapterUpdate,
19
21
  _ChapterRenumber as _ChapterRenumber,
@@ -126,6 +128,7 @@ from .users import (
126
128
  UserApproveDTO,
127
129
  UserCreate,
128
130
  UserDenyDTO,
131
+ UserDetail,
129
132
  UserMergeDTO,
130
133
  UserRegisterDTO,
131
134
  UserUpdate,
@@ -141,7 +144,9 @@ __all__ = [
141
144
  "BulkAssignTraitSuccess",
142
145
  "Campaign",
143
146
  "CampaignBook",
147
+ "CampaignBookDetail",
144
148
  "CampaignChapter",
149
+ "CampaignChapterDetail",
145
150
  "CampaignCreate",
146
151
  "CampaignExperience",
147
152
  "CampaignUpdate",
@@ -215,6 +220,7 @@ __all__ = [
215
220
  "UserApproveDTO",
216
221
  "UserCreate",
217
222
  "UserDenyDTO",
223
+ "UserDetail",
218
224
  "UserMergeDTO",
219
225
  "UserRegisterDTO",
220
226
  "UserUpdate",
@@ -4,6 +4,9 @@ from datetime import datetime
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
 
7
+ from vclient.models.chapters import CampaignChapter
8
+ from vclient.models.shared import Asset, Note
9
+
7
10
 
8
11
  class CampaignBook(BaseModel):
9
12
  """Response model for a campaign book.
@@ -21,6 +24,25 @@ class CampaignBook(BaseModel):
21
24
  campaign_id: str = Field(..., description="ID of the parent campaign.")
22
25
 
23
26
 
27
+ class CampaignBookDetail(CampaignBook):
28
+ """Campaign book response with optional embedded child resources.
29
+
30
+ Returned by the single-book endpoint when the ``include`` query parameter
31
+ is used. Absent resources default to ``None``; present resources are full arrays
32
+ of the same DTOs returned by the dedicated child endpoints.
33
+ """
34
+
35
+ chapters: list[CampaignChapter] | None = Field(
36
+ default=None, description="Embedded chapters, when requested via include."
37
+ )
38
+ notes: list[Note] | None = Field(
39
+ default=None, description="Embedded notes, when requested via include."
40
+ )
41
+ assets: list[Asset] | None = Field(
42
+ default=None, description="Embedded assets, when requested via include."
43
+ )
44
+
45
+
24
46
  class BookCreate(BaseModel):
25
47
  """Request body for creating a new campaign book.
26
48
 
@@ -60,5 +82,6 @@ __all__ = [
60
82
  "BookCreate",
61
83
  "BookUpdate",
62
84
  "CampaignBook",
85
+ "CampaignBookDetail",
63
86
  "_BookRenumber",
64
87
  ]
@@ -4,6 +4,8 @@ from datetime import datetime
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
 
7
+ from vclient.models.shared import Asset, Note
8
+
7
9
 
8
10
  class CampaignChapter(BaseModel):
9
11
  """Response model for a campaign chapter.
@@ -23,6 +25,22 @@ class CampaignChapter(BaseModel):
23
25
  book_id: str = Field(..., description="ID of the parent book.")
24
26
 
25
27
 
28
+ class CampaignChapterDetail(CampaignChapter):
29
+ """Campaign chapter response with optional embedded child resources.
30
+
31
+ Returned by the single-chapter endpoint when the ``include`` query parameter
32
+ is used. Absent resources default to ``None``; present resources are full arrays
33
+ of the same DTOs returned by the dedicated child endpoints.
34
+ """
35
+
36
+ notes: list[Note] | None = Field(
37
+ default=None, description="Embedded notes, when requested via include."
38
+ )
39
+ assets: list[Asset] | None = Field(
40
+ default=None, description="Embedded assets, when requested via include."
41
+ )
42
+
43
+
26
44
  class ChapterCreate(BaseModel):
27
45
  """Request body for creating a new campaign chapter."""
28
46
 
@@ -45,6 +63,7 @@ class _ChapterRenumber(BaseModel):
45
63
 
46
64
  __all__ = [
47
65
  "CampaignChapter",
66
+ "CampaignChapterDetail",
48
67
  "ChapterCreate",
49
68
  "ChapterUpdate",
50
69
  "_ChapterRenumber",
@@ -152,12 +152,8 @@ class Character(BaseModel):
152
152
  """
153
153
 
154
154
  id: str = Field(..., description="MongoDB document ObjectID.")
155
- date_created: datetime | None = Field(
156
- default=None, description="Timestamp when the character was created."
157
- )
158
- date_modified: datetime | None = Field(
159
- default=None, description="Timestamp when the character was last modified."
160
- )
155
+ date_created: datetime
156
+ date_modified: datetime
161
157
  date_killed: datetime | None = Field(
162
158
  default=None, description="Timestamp when the character was killed."
163
159
  )
@@ -10,6 +10,7 @@ from vclient.constants import (
10
10
  GrantXPPermission,
11
11
  ManageCampaignPermission,
12
12
  PermissionLevel,
13
+ RecoupXPPermission,
13
14
  )
14
15
  from vclient.models.users import User
15
16
 
@@ -26,6 +27,7 @@ class CompanySettings(BaseModel):
26
27
  permission_manage_campaign: ManageCampaignPermission | None = None
27
28
  permission_grant_xp: GrantXPPermission | None = None
28
29
  permission_free_trait_changes: FreeTraitChangesPermission | None = None
30
+ permission_recoup_xp: RecoupXPPermission | None = None
29
31
 
30
32
 
31
33
  class Company(BaseModel):
@@ -41,7 +43,7 @@ class Company(BaseModel):
41
43
  description: str | None
42
44
  email: str
43
45
  resources_modified_at: datetime
44
- settings: CompanySettings | None
46
+ settings: CompanySettings
45
47
 
46
48
 
47
49
  class CompanyPermissions(BaseModel):
@@ -6,6 +6,8 @@ from typing import Annotated
6
6
  from pydantic import BaseModel, Field
7
7
 
8
8
  from vclient.constants import UserRole
9
+ from vclient.models.characters import Character
10
+ from vclient.models.shared import Asset, Note
9
11
 
10
12
  # -----------------------------------------------------------------------------
11
13
  # Nested/Shared Models
@@ -96,6 +98,30 @@ class User(BaseModel):
96
98
  lifetime_cool_points: int = 0
97
99
 
98
100
 
101
+ class UserDetail(User):
102
+ """User response with optional embedded child resources.
103
+
104
+ Returned by the single-user endpoint when the ``include`` query parameter
105
+ is used. Absent resources default to ``None``; present resources are full arrays.
106
+
107
+ Note: ``assets`` returns assets attached to the user (not assets the user
108
+ uploaded), and ``characters`` returns only characters the user plays.
109
+ """
110
+
111
+ quickrolls: list["Quickroll"] | None = Field(
112
+ default=None, description="Embedded quickrolls, when requested via include."
113
+ )
114
+ notes: list[Note] | None = Field(
115
+ default=None, description="Embedded notes attached to the user."
116
+ )
117
+ assets: list[Asset] | None = Field(
118
+ default=None, description="Embedded assets attached to the user."
119
+ )
120
+ characters: list[Character] | None = Field(
121
+ default=None, description="Embedded characters the user plays."
122
+ )
123
+
124
+
99
125
  # -----------------------------------------------------------------------------
100
126
  # User Request Models
101
127
  # -----------------------------------------------------------------------------
@@ -205,6 +231,9 @@ class Quickroll(BaseModel):
205
231
  trait_ids: list[str] = Field(default_factory=list)
206
232
 
207
233
 
234
+ UserDetail.model_rebuild()
235
+
236
+
208
237
  class QuickrollCreate(BaseModel):
209
238
  """Request body for creating a new quickroll.
210
239
 
@@ -253,6 +282,7 @@ __all__ = [
253
282
  "QuickrollUpdate",
254
283
  "User",
255
284
  "UserCreate",
285
+ "UserDetail",
256
286
  "UserMergeDTO",
257
287
  "UserRegisterDTO",
258
288
  "UserUpdate",
@@ -1,14 +1,15 @@
1
1
  """Service for interacting with the Campaign Books Chapters API."""
2
2
 
3
3
  import mimetypes
4
- from collections.abc import AsyncIterator
4
+ from collections.abc import AsyncIterator, Sequence
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from vclient.constants import DEFAULT_PAGE_LIMIT
7
+ from vclient.constants import DEFAULT_PAGE_LIMIT, ChapterInclude
8
8
  from vclient.endpoints import Endpoints
9
9
  from vclient.models import (
10
10
  Asset,
11
11
  CampaignChapter,
12
+ CampaignChapterDetail,
12
13
  ChapterCreate,
13
14
  ChapterUpdate,
14
15
  Note,
@@ -80,12 +81,34 @@ class ChaptersService(BaseService):
80
81
  ):
81
82
  yield CampaignChapter.model_validate(item)
82
83
 
83
- async def get(self, chapter_id: str) -> CampaignChapter:
84
- """Retrieve a specific campaign book chapter."""
84
+ async def get(
85
+ self,
86
+ chapter_id: str,
87
+ *,
88
+ include: Sequence[ChapterInclude] | None = None,
89
+ ) -> CampaignChapterDetail:
90
+ """Retrieve detailed information about a specific campaign book chapter.
91
+
92
+ Fetches the chapter and optionally embeds child resources directly in the
93
+ response, avoiding follow-up requests.
94
+
95
+ Args:
96
+ chapter_id: The ID of the chapter to retrieve.
97
+ include: Child resources to embed in the response. Valid values are
98
+ ``"notes"`` and ``"assets"``.
99
+
100
+ Returns:
101
+ The CampaignChapterDetail object with full details and any requested embeds.
102
+
103
+ Raises:
104
+ NotFoundError: If the chapter does not exist.
105
+ AuthorizationError: If you don't have access.
106
+ """
85
107
  response = await self._get(
86
- self._format_endpoint(Endpoints.BOOK_CHAPTER, chapter_id=chapter_id)
108
+ self._format_endpoint(Endpoints.BOOK_CHAPTER, chapter_id=chapter_id),
109
+ params=self._build_params(include=list(include) if include else None),
87
110
  )
88
- return CampaignChapter.model_validate(response.json())
111
+ return CampaignChapterDetail.model_validate(response.json())
89
112
 
90
113
  async def create(
91
114
  self,