valentina-python-client 2.2.0__tar.gz → 2.4.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 (84) hide show
  1. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/PKG-INFO +1 -1
  2. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/pyproject.toml +7 -7
  3. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/__init__.py +1 -1
  4. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/registry.py +1 -1
  5. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/global_admin.py +182 -21
  6. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/constants.py +1 -0
  7. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/endpoints.py +2 -0
  8. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/__init__.py +6 -0
  9. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/books.py +3 -0
  10. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/campaigns.py +6 -0
  11. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/chapters.py +2 -0
  12. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/characters.py +7 -0
  13. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/companies.py +4 -0
  14. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/users.py +39 -0
  15. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/registry.py +1 -1
  16. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/global_admin.py +198 -19
  17. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/testing/__init__.py +2 -0
  18. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/testing/_factories.py +8 -0
  19. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/testing/_router.py +3 -0
  20. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/testing/_routes.py +8 -0
  21. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/validate_constants.py +1 -0
  22. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/LICENSE +0 -0
  23. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/README.md +0 -0
  24. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_codegen.py +0 -0
  25. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/__init__.py +0 -0
  26. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/client.py +0 -0
  27. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/__init__.py +0 -0
  28. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/_audit_params.py +0 -0
  29. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/base.py +0 -0
  30. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/campaign_book_chapters.py +0 -0
  31. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/campaign_books.py +0 -0
  32. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/campaigns.py +0 -0
  33. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/character_autogen.py +0 -0
  34. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/character_blueprint.py +0 -0
  35. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/character_traits.py +0 -0
  36. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/characters.py +0 -0
  37. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/companies.py +0 -0
  38. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/developers.py +0 -0
  39. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/dicerolls.py +0 -0
  40. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/dictionary.py +0 -0
  41. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/options.py +0 -0
  42. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/system.py +0 -0
  43. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/user_lookup.py +0 -0
  44. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/user_self_registration.py +0 -0
  45. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/services/users.py +0 -0
  46. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/testing/__init__.py +0 -0
  47. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/_sync/testing/_client.py +0 -0
  48. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/client.py +0 -0
  49. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/config.py +0 -0
  50. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/exceptions.py +0 -0
  51. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/audit_logs.py +0 -0
  52. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/character_autogen.py +0 -0
  53. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/character_blueprint.py +0 -0
  54. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/character_trait.py +0 -0
  55. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/developers.py +0 -0
  56. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/diceroll.py +0 -0
  57. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/dictionary.py +0 -0
  58. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/full_sheet.py +0 -0
  59. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/global_admin.py +0 -0
  60. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/pagination.py +0 -0
  61. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/shared.py +0 -0
  62. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/system.py +0 -0
  63. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/models/user_lookup.py +0 -0
  64. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/py.typed +0 -0
  65. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/__init__.py +0 -0
  66. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/_audit_params.py +0 -0
  67. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/base.py +0 -0
  68. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/campaign_book_chapters.py +0 -0
  69. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/campaign_books.py +0 -0
  70. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/campaigns.py +0 -0
  71. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/character_autogen.py +0 -0
  72. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/character_blueprint.py +0 -0
  73. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/character_traits.py +0 -0
  74. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/characters.py +0 -0
  75. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/companies.py +0 -0
  76. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/developers.py +0 -0
  77. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/dicerolls.py +0 -0
  78. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/dictionary.py +0 -0
  79. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/options.py +0 -0
  80. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/system.py +0 -0
  81. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/user_lookup.py +0 -0
  82. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/user_self_registration.py +0 -0
  83. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/src/vclient/services/users.py +0 -0
  84. {valentina_python_client-2.2.0 → valentina_python_client-2.4.0}/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: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: Async Python client library for the Valentina Noir API
5
5
  Author: Nate Landau
6
6
  Author-email: Nate Landau <github@natenate.org>
@@ -16,7 +16,7 @@
16
16
  name = "valentina-python-client"
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.13"
19
- version = "2.2.0"
19
+ version = "2.4.0"
20
20
 
21
21
  [project.optional-dependencies]
22
22
  testing = ["polyfactory>=3.3.0"]
@@ -34,10 +34,10 @@
34
34
 
35
35
  [dependency-groups]
36
36
  dev = [
37
- "commitizen>=4.16.2",
38
- "coverage>=7.14.0",
37
+ "commitizen>=4.16.3",
38
+ "coverage>=7.14.1",
39
39
  "duty>=1.9.0",
40
- "prek>=0.4.1",
40
+ "prek>=0.4.3",
41
41
  "pytest-anyio>=0.0.0",
42
42
  "pytest-clarity>=1.0.1",
43
43
  "pytest-cov>=7.1.0",
@@ -48,10 +48,10 @@
48
48
  "pytest-xdist>=3.8.0",
49
49
  "pytest>=9.0.3",
50
50
  "respx>=0.23.1",
51
- "ruff>=0.15.14",
51
+ "ruff>=0.15.15",
52
52
  "shellcheck-py>=0.11.0.1",
53
- "ty>=0.0.39",
54
- "typos>=1.46.2",
53
+ "ty>=0.0.42",
54
+ "typos>=1.47.0",
55
55
  "vulture>=2.16",
56
56
  "yamllint>=1.38.0",
57
57
  ]
@@ -113,4 +113,4 @@ __all__ = (
113
113
  "users_service",
114
114
  )
115
115
 
116
- __version__ = "2.2.0"
116
+ __version__ = "2.4.0"
@@ -160,7 +160,7 @@ def sync_global_admin_service() -> "SyncGlobalAdminService":
160
160
  Example:
161
161
  ```python
162
162
  admins = sync_global_admin_service()
163
- developers = await admins.list_all()
163
+ developers = await admins.list_all_developers()
164
164
  ```
165
165
  """
166
166
  from vclient._sync.services.global_admin import SyncGlobalAdminService
@@ -15,9 +15,13 @@ from vclient.constants import (
15
15
  AuditLogInclude,
16
16
  AuditOperation,
17
17
  LogLevel,
18
+ UserRole,
18
19
  )
19
20
  from vclient.endpoints import Endpoints
20
21
  from vclient.models import (
22
+ AdminUser,
23
+ AdminUserCreate,
24
+ AdminUserUpdate,
21
25
  AuditLog,
22
26
  AuditLogDetail,
23
27
  Developer,
@@ -57,16 +61,16 @@ def _filename_from_content_disposition(header: str | None, *, fallback: str) ->
57
61
  class SyncGlobalAdminService(SyncBaseService):
58
62
  """Service for global admin operations in the Valentina API.
59
63
 
60
- Provides methods to create, retrieve, update, and delete developer accounts,
61
- as well as manage API keys. Requires global admin privileges.
64
+ Provides cross-company management of developer accounts and their API keys,
65
+ plus server log access. Requires global admin privileges.
62
66
 
63
67
  Example:
64
68
  >>> async with SyncVClient() as client:
65
- ... developers = await client.global_admin.list_all()
66
- ... developer = await client.global_admin.get("developer_id")
69
+ ... developers = await client.global_admin.list_all_developers()
70
+ ... developer = await client.global_admin.get_developer("developer_id")
67
71
  """
68
72
 
69
- def get_page(
73
+ def get_developer_page(
70
74
  self,
71
75
  *,
72
76
  limit: int = DEFAULT_PAGE_LIMIT,
@@ -90,11 +94,11 @@ class SyncGlobalAdminService(SyncBaseService):
90
94
  Endpoints.ADMIN_DEVELOPERS, Developer, limit=limit, offset=offset, params=params
91
95
  )
92
96
 
93
- def list_all(self, *, is_global_admin: bool | None = None) -> list[Developer]:
97
+ def list_all_developers(self, *, is_global_admin: bool | None = None) -> list[Developer]:
94
98
  """Retrieve all developer accounts.
95
99
 
96
- Automatically paginates through all results. Use `get_page()` for paginated access
97
- or `iter_all()` for memory-efficient streaming of large datasets.
100
+ Automatically paginates through all results. Use `get_developer_page()` for paginated
101
+ access or `iter_all_developers()` for memory-efficient streaming of large datasets.
98
102
 
99
103
  Args:
100
104
  is_global_admin: Optional filter by global admin status.
@@ -102,9 +106,11 @@ class SyncGlobalAdminService(SyncBaseService):
102
106
  Returns:
103
107
  A list of all Developer objects.
104
108
  """
105
- return [developer for developer in self.iter_all(is_global_admin=is_global_admin)]
109
+ return [
110
+ developer for developer in self.iter_all_developers(is_global_admin=is_global_admin)
111
+ ]
106
112
 
107
- def iter_all(
113
+ def iter_all_developers(
108
114
  self, *, limit: int = 100, is_global_admin: bool | None = None
109
115
  ) -> Iterator[Developer]:
110
116
  """Iterate through all developer accounts.
@@ -120,18 +126,14 @@ class SyncGlobalAdminService(SyncBaseService):
120
126
  Individual Developer objects.
121
127
 
122
128
  Example:
123
- >>> async for developer in client.global_admin.iter_all():
129
+ >>> async for developer in client.global_admin.iter_all_developers():
124
130
  ... print(developer.username)
125
131
  """
126
- params = {}
127
- if is_global_admin is not None:
128
- params["is_global_admin"] = is_global_admin
129
- for item in self._iter_all_pages(
130
- Endpoints.ADMIN_DEVELOPERS, limit=limit, params=params or None
131
- ):
132
+ params = self._build_params(is_global_admin=is_global_admin)
133
+ for item in self._iter_all_pages(Endpoints.ADMIN_DEVELOPERS, limit=limit, params=params):
132
134
  yield Developer.model_validate(item)
133
135
 
134
- def get(self, developer_id: str) -> Developer:
136
+ def get_developer(self, developer_id: str) -> Developer:
135
137
  """Retrieve detailed information about a specific developer.
136
138
 
137
139
  Args:
@@ -147,7 +149,7 @@ class SyncGlobalAdminService(SyncBaseService):
147
149
  response = self._get(Endpoints.ADMIN_DEVELOPER.format(developer_id=developer_id))
148
150
  return Developer.model_validate(response.json())
149
151
 
150
- def create(self, request: DeveloperCreate | None = None, **kwargs) -> Developer:
152
+ def create_developer(self, request: DeveloperCreate | None = None, **kwargs) -> Developer:
151
153
  """Create a new developer account.
152
154
 
153
155
  This creates the account but does not create an API key or grant access to any
@@ -174,7 +176,7 @@ class SyncGlobalAdminService(SyncBaseService):
174
176
  )
175
177
  return Developer.model_validate(response.json())
176
178
 
177
- def update(
179
+ def update_developer(
178
180
  self, developer_id: str, request: DeveloperUpdate | None = None, **kwargs
179
181
  ) -> Developer:
180
182
  """Modify a developer account's properties.
@@ -204,7 +206,7 @@ class SyncGlobalAdminService(SyncBaseService):
204
206
  )
205
207
  return Developer.model_validate(response.json())
206
208
 
207
- def delete(self, developer_id: str) -> None:
209
+ def delete_developer(self, developer_id: str) -> None:
208
210
  """Remove a developer account from the system.
209
211
 
210
212
  The developer's API key will be invalidated immediately.
@@ -218,6 +220,165 @@ class SyncGlobalAdminService(SyncBaseService):
218
220
  """
219
221
  self._delete(Endpoints.ADMIN_DEVELOPER.format(developer_id=developer_id))
220
222
 
223
+ def get_user_page(
224
+ self,
225
+ *,
226
+ company_id: str | None = None,
227
+ role: UserRole | None = None,
228
+ email: str | None = None,
229
+ is_archived: bool | None = None,
230
+ limit: int = DEFAULT_PAGE_LIMIT,
231
+ offset: int = 0,
232
+ ) -> PaginatedResponse[AdminUser]:
233
+ """Retrieve a paginated page of users across all companies.
234
+
235
+ Authenticates with the global-admin API key only; no On-Behalf-Of header
236
+ is sent. Archived (soft-deleted) users are included when
237
+ ``is_archived=True``.
238
+
239
+ Args:
240
+ company_id: Filter to a single company.
241
+ role: Filter by user role.
242
+ email: Filter by exact email match.
243
+ is_archived: Filter by archived state.
244
+ limit: Maximum number of items to return (0-100, default 10).
245
+ offset: Number of items to skip from the beginning (default 0).
246
+
247
+ Returns:
248
+ A PaginatedResponse containing AdminUser objects and pagination metadata.
249
+ """
250
+ params = self._build_params(
251
+ company_id=company_id, role=role, email=email, is_archived=is_archived
252
+ )
253
+ return self._get_paginated_as(
254
+ Endpoints.ADMIN_USERS, AdminUser, limit=limit, offset=offset, params=params
255
+ )
256
+
257
+ def list_all_users(
258
+ self,
259
+ *,
260
+ company_id: str | None = None,
261
+ role: UserRole | None = None,
262
+ email: str | None = None,
263
+ is_archived: bool | None = None,
264
+ ) -> list[AdminUser]:
265
+ """Retrieve all users across all companies.
266
+
267
+ Automatically paginates through all results. Use ``get_user_page()`` for
268
+ paginated access or ``iter_all_users()`` for memory-efficient streaming.
269
+
270
+ Args:
271
+ company_id: Filter to a single company.
272
+ role: Filter by user role.
273
+ email: Filter by exact email match.
274
+ is_archived: Filter by archived state.
275
+
276
+ Returns:
277
+ A list of all matching AdminUser objects.
278
+ """
279
+ return [
280
+ user
281
+ for user in self.iter_all_users(
282
+ company_id=company_id, role=role, email=email, is_archived=is_archived
283
+ )
284
+ ]
285
+
286
+ def iter_all_users(
287
+ self,
288
+ *,
289
+ limit: int = 100,
290
+ company_id: str | None = None,
291
+ role: UserRole | None = None,
292
+ email: str | None = None,
293
+ is_archived: bool | None = None,
294
+ ) -> Iterator[AdminUser]:
295
+ """Iterate through all users across all companies.
296
+
297
+ Yields individual users, automatically fetching subsequent pages until all
298
+ matching users have been retrieved.
299
+
300
+ Args:
301
+ limit: Items per page (default 100 for efficiency).
302
+ company_id: Filter to a single company.
303
+ role: Filter by user role.
304
+ email: Filter by exact email match.
305
+ is_archived: Filter by archived state.
306
+
307
+ Yields:
308
+ Individual AdminUser objects.
309
+ """
310
+ params = self._build_params(
311
+ company_id=company_id, role=role, email=email, is_archived=is_archived
312
+ )
313
+ for item in self._iter_all_pages(Endpoints.ADMIN_USERS, limit=limit, params=params):
314
+ yield AdminUser.model_validate(item)
315
+
316
+ def get_user(self, user_id: str) -> AdminUser:
317
+ """Retrieve a single user by ID, including archived users.
318
+
319
+ Args:
320
+ user_id: The ID of the user to retrieve.
321
+
322
+ Returns:
323
+ The AdminUser object, with ``is_archived`` reflecting soft-delete state.
324
+ """
325
+ response = self._get(Endpoints.ADMIN_USER.format(user_id=user_id))
326
+ return AdminUser.model_validate(response.json())
327
+
328
+ def create_user(self, request: AdminUserCreate | None = None, **kwargs) -> AdminUser:
329
+ """Create a user in a target company.
330
+
331
+ The role assignment matrix enforced on company-scoped endpoints does not
332
+ apply here, but the server still rejects ``UNAPPROVED``/``DEACTIVATED`` on
333
+ create.
334
+
335
+ Args:
336
+ request: An AdminUserCreate model, OR pass fields as keyword arguments.
337
+ **kwargs: Fields for AdminUserCreate if request is not provided.
338
+ Requires: company_id, username, email, role.
339
+
340
+ Returns:
341
+ The newly created AdminUser object.
342
+ """
343
+ body = request if request is not None else self._validate_request(AdminUserCreate, **kwargs)
344
+ response = self._post(
345
+ Endpoints.ADMIN_USERS,
346
+ json=body.model_dump(exclude_none=True, exclude_unset=True, mode="json"),
347
+ )
348
+ return AdminUser.model_validate(response.json())
349
+
350
+ def update_user(
351
+ self, user_id: str, request: AdminUserUpdate | None = None, **kwargs
352
+ ) -> AdminUser:
353
+ """Update any user by ID, bypassing the company-scoped role hierarchy.
354
+
355
+ Set ``is_archived=False`` to restore a soft-deleted user.
356
+
357
+ Args:
358
+ user_id: The ID of the user to update.
359
+ request: An AdminUserUpdate model, OR pass fields as keyword arguments.
360
+ **kwargs: Fields for AdminUserUpdate if request is not provided.
361
+
362
+ Returns:
363
+ The updated AdminUser object.
364
+ """
365
+ body = request if request is not None else self._validate_request(AdminUserUpdate, **kwargs)
366
+ response = self._patch(
367
+ Endpoints.ADMIN_USER.format(user_id=user_id),
368
+ json=body.model_dump(exclude_none=True, exclude_unset=True, mode="json"),
369
+ )
370
+ return AdminUser.model_validate(response.json())
371
+
372
+ def delete_user(self, user_id: str) -> None:
373
+ """Soft-delete a user by ID.
374
+
375
+ The deletion is reversible via ``update_user(user_id, is_archived=False)``.
376
+
377
+ Args:
378
+ user_id: The ID of the user to soft-delete.
379
+ """
380
+ self._delete(Endpoints.ADMIN_USER.format(user_id=user_id))
381
+
221
382
  def create_api_key(self, developer_id: str) -> DeveloperWithApiKey:
222
383
  """Generate a new API key for a developer.
223
384
 
@@ -84,6 +84,7 @@ HunterCreed = Literal["ENTREPRENEURIAL", "FAITHFUL", "INQUISITIVE", "MARTIAL", "
84
84
  HunterEdgeType = Literal["ASSETS", "APTITUDES", "ENDOWMENTS"]
85
85
  LogLevel = Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
86
86
  ManageCampaignPermission = Literal["UNRESTRICTED", "STORYTELLER"]
87
+ ManageNPCPermission = Literal["UNRESTRICTED", "STORYTELLER"]
87
88
  PermissionLevel = Literal["USER", "ADMIN", "OWNER", "REVOKE"]
88
89
  RecoupXPPermission = Literal["UNRESTRICTED", "DENIED", "WITHIN_SESSION"]
89
90
  RollResultType = Literal["SUCCESS", "FAILURE", "BOTCH", "CRITICAL", "OTHER"]
@@ -29,6 +29,8 @@ class Endpoints:
29
29
  ADMIN_DEVELOPER_AUDIT_LOGS = f"{ADMIN_DEVELOPER}/audit-logs"
30
30
  ADMIN_LOGS = f"{_BASE}/admin/logs"
31
31
  ADMIN_LOGS_DOWNLOAD = f"{ADMIN_LOGS}/download"
32
+ ADMIN_USERS = f"{_BASE}/admin/users"
33
+ ADMIN_USER = f"{_BASE}/admin/users/{{user_id}}"
32
34
 
33
35
  # Developer endpoints (self-service)
34
36
  DEVELOPER_ME = f"{_BASE}/developers/me"
@@ -127,6 +127,9 @@ from .user_lookup import (
127
127
  UserLookupResult,
128
128
  )
129
129
  from .users import (
130
+ AdminUser,
131
+ AdminUserCreate,
132
+ AdminUserUpdate,
130
133
  CampaignExperience,
131
134
  DiscordProfile,
132
135
  DiscordProfileUpdate,
@@ -146,6 +149,9 @@ from .users import (
146
149
  )
147
150
 
148
151
  __all__ = [
152
+ "AdminUser",
153
+ "AdminUserCreate",
154
+ "AdminUserUpdate",
149
155
  "Asset",
150
156
  "AuditLog",
151
157
  "AuditLogDetail",
@@ -22,6 +22,9 @@ class CampaignBook(BaseModel):
22
22
  asset_ids: list[str] = Field(default_factory=list, description="List of associated asset IDs.")
23
23
  number: int = Field(..., description="Book number within the campaign.")
24
24
  campaign_id: str = Field(..., description="ID of the parent campaign.")
25
+ num_chapters: int = Field(default=0, description="Number of active chapters in the book.")
26
+ num_notes: int = Field(default=0, description="Number of active notes on the book.")
27
+ num_assets: int = Field(default=0, description="Number of active assets on the book.")
25
28
 
26
29
 
27
30
  class CampaignBookDetail(CampaignBook):
@@ -26,6 +26,12 @@ class Campaign(BaseModel):
26
26
  desperation: int = 0
27
27
  danger: int = 0
28
28
  company_id: str
29
+ num_books: int = 0
30
+ num_chapters: int = 0
31
+ num_notes: int = 0
32
+ num_player_characters: int = 0
33
+ num_storyteller_characters: int = 0
34
+ num_npc_characters: int = 0
29
35
 
30
36
 
31
37
  # -----------------------------------------------------------------------------
@@ -23,6 +23,8 @@ class CampaignChapter(BaseModel):
23
23
  asset_ids: list[str] = Field(default_factory=list, description="List of associated asset IDs.")
24
24
  number: int = Field(..., description="Chapter number within the book.")
25
25
  book_id: str = Field(..., description="ID of the parent book.")
26
+ num_notes: int = Field(default=0, description="Number of active notes on the chapter.")
27
+ num_assets: int = Field(default=0, description="Number of active assets on the chapter.")
26
28
 
27
29
 
28
30
  class CampaignChapterDetail(CampaignChapter):
@@ -222,6 +222,13 @@ class Character(BaseModel):
222
222
  default=None, description="Hunter-specific attributes."
223
223
  )
224
224
 
225
+ # Child-resource counts
226
+ num_inventory_items: int = Field(
227
+ default=0, description="Number of active inventory items on the character."
228
+ )
229
+ num_notes: int = Field(default=0, description="Number of active notes on the character.")
230
+ num_assets: int = Field(default=0, description="Number of active assets on the character.")
231
+
225
232
 
226
233
  class CharacterDetail(Character):
227
234
  """Character response with optional embedded child resources.
@@ -9,6 +9,7 @@ from vclient.constants import (
9
9
  FreeTraitChangesPermission,
10
10
  GrantXPPermission,
11
11
  ManageCampaignPermission,
12
+ ManageNPCPermission,
12
13
  PermissionLevel,
13
14
  RecoupXPPermission,
14
15
  )
@@ -27,6 +28,7 @@ class CompanySettings(BaseModel):
27
28
  character_autogen_num_choices: int
28
29
  character_autogen_starting_points: int
29
30
  permission_manage_campaign: ManageCampaignPermission
31
+ permission_manage_npc: ManageNPCPermission
30
32
  permission_grant_xp: GrantXPPermission
31
33
  permission_free_trait_changes: FreeTraitChangesPermission
32
34
  permission_recoup_xp: RecoupXPPermission
@@ -42,6 +44,7 @@ class CompanySettingsCreate(BaseModel):
42
44
  character_autogen_num_choices: int | None = None
43
45
  character_autogen_starting_points: int | None = None
44
46
  permission_manage_campaign: ManageCampaignPermission | None = None
47
+ permission_manage_npc: ManageNPCPermission | None = None
45
48
  permission_grant_xp: GrantXPPermission | None = None
46
49
  permission_free_trait_changes: FreeTraitChangesPermission | None = None
47
50
  permission_recoup_xp: RecoupXPPermission | None = None
@@ -57,6 +60,7 @@ class CompanySettingsUpdate(BaseModel):
57
60
  character_autogen_num_choices: int | None = None
58
61
  character_autogen_starting_points: int | None = None
59
62
  permission_manage_campaign: ManageCampaignPermission | None = None
63
+ permission_manage_npc: ManageNPCPermission | None = None
60
64
  permission_grant_xp: GrantXPPermission | None = None
61
65
  permission_free_trait_changes: FreeTraitChangesPermission | None = None
62
66
  permission_recoup_xp: RecoupXPPermission | None = None
@@ -96,6 +96,10 @@ class User(BaseModel):
96
96
  asset_ids: list[str] = Field(default_factory=list)
97
97
  lifetime_xp: int = 0
98
98
  lifetime_cool_points: int = 0
99
+ num_quickrolls: int = 0
100
+ num_notes: int = 0
101
+ num_assets: int = 0
102
+ num_characters: int = 0
99
103
 
100
104
 
101
105
  class UserDetail(User):
@@ -122,6 +126,17 @@ class UserDetail(User):
122
126
  )
123
127
 
124
128
 
129
+ class AdminUser(User):
130
+ """Response model for a user returned by the global-admin user endpoints.
131
+
132
+ Extends the tenant-scoped ``User`` with ``is_archived``, which is always
133
+ present on the admin endpoints so callers can identify soft-deleted users
134
+ directly from the response body.
135
+ """
136
+
137
+ is_archived: bool
138
+
139
+
125
140
  # -----------------------------------------------------------------------------
126
141
  # User Request Models
127
142
  # -----------------------------------------------------------------------------
@@ -194,6 +209,27 @@ class UserUpdate(BaseModel):
194
209
  github_profile: GitHubProfile | None = None
195
210
 
196
211
 
212
+ class AdminUserCreate(UserCreate):
213
+ """Request body for creating a user as a global admin.
214
+
215
+ Extends the tenant-scoped ``UserCreate`` with an explicit ``company_id`` for
216
+ the target company. The server rejects ``UNAPPROVED``/``DEACTIVATED`` roles
217
+ on create, so no client-side role restriction is applied here.
218
+ """
219
+
220
+ company_id: str
221
+
222
+
223
+ class AdminUserUpdate(UserUpdate):
224
+ """Request body for updating any user as a global admin.
225
+
226
+ Extends the tenant-scoped ``UserUpdate`` with ``is_archived``. Set it to
227
+ ``False`` to restore a soft-deleted user.
228
+ """
229
+
230
+ is_archived: bool | None = None
231
+
232
+
197
233
  class UserApproveDTO(BaseModel):
198
234
  """Approve an unapproved user and assign a role."""
199
235
 
@@ -262,6 +298,9 @@ class _ExperienceAddRemove(BaseModel):
262
298
 
263
299
 
264
300
  __all__ = [
301
+ "AdminUser",
302
+ "AdminUserCreate",
303
+ "AdminUserUpdate",
265
304
  "CampaignExperience",
266
305
  "DiscordProfile",
267
306
  "GitHubProfile",
@@ -160,7 +160,7 @@ def global_admin_service() -> "GlobalAdminService":
160
160
  Example:
161
161
  ```python
162
162
  admins = global_admin_service()
163
- developers = await admins.list_all()
163
+ developers = await admins.list_all_developers()
164
164
  ```
165
165
  """
166
166
  from vclient.services.global_admin import GlobalAdminService
@@ -13,9 +13,13 @@ from vclient.constants import (
13
13
  AuditLogInclude,
14
14
  AuditOperation,
15
15
  LogLevel,
16
+ UserRole,
16
17
  )
17
18
  from vclient.endpoints import Endpoints
18
19
  from vclient.models import (
20
+ AdminUser,
21
+ AdminUserCreate,
22
+ AdminUserUpdate,
19
23
  AuditLog,
20
24
  AuditLogDetail,
21
25
  Developer,
@@ -56,16 +60,16 @@ def _filename_from_content_disposition(header: str | None, *, fallback: str) ->
56
60
  class GlobalAdminService(BaseService):
57
61
  """Service for global admin operations in the Valentina API.
58
62
 
59
- Provides methods to create, retrieve, update, and delete developer accounts,
60
- as well as manage API keys. Requires global admin privileges.
63
+ Provides cross-company management of developer accounts and their API keys,
64
+ plus server log access. Requires global admin privileges.
61
65
 
62
66
  Example:
63
67
  >>> async with VClient() as client:
64
- ... developers = await client.global_admin.list_all()
65
- ... developer = await client.global_admin.get("developer_id")
68
+ ... developers = await client.global_admin.list_all_developers()
69
+ ... developer = await client.global_admin.get_developer("developer_id")
66
70
  """
67
71
 
68
- async def get_page(
72
+ async def get_developer_page(
69
73
  self,
70
74
  *,
71
75
  limit: int = DEFAULT_PAGE_LIMIT,
@@ -93,11 +97,11 @@ class GlobalAdminService(BaseService):
93
97
  params=params,
94
98
  )
95
99
 
96
- async def list_all(self, *, is_global_admin: bool | None = None) -> list[Developer]:
100
+ async def list_all_developers(self, *, is_global_admin: bool | None = None) -> list[Developer]:
97
101
  """Retrieve all developer accounts.
98
102
 
99
- Automatically paginates through all results. Use `get_page()` for paginated access
100
- or `iter_all()` for memory-efficient streaming of large datasets.
103
+ Automatically paginates through all results. Use `get_developer_page()` for paginated
104
+ access or `iter_all_developers()` for memory-efficient streaming of large datasets.
101
105
 
102
106
  Args:
103
107
  is_global_admin: Optional filter by global admin status.
@@ -105,9 +109,12 @@ class GlobalAdminService(BaseService):
105
109
  Returns:
106
110
  A list of all Developer objects.
107
111
  """
108
- return [developer async for developer in self.iter_all(is_global_admin=is_global_admin)]
112
+ return [
113
+ developer
114
+ async for developer in self.iter_all_developers(is_global_admin=is_global_admin)
115
+ ]
109
116
 
110
- async def iter_all(
117
+ async def iter_all_developers(
111
118
  self,
112
119
  *,
113
120
  limit: int = 100,
@@ -126,21 +133,19 @@ class GlobalAdminService(BaseService):
126
133
  Individual Developer objects.
127
134
 
128
135
  Example:
129
- >>> async for developer in client.global_admin.iter_all():
136
+ >>> async for developer in client.global_admin.iter_all_developers():
130
137
  ... print(developer.username)
131
138
  """
132
- params = {}
133
- if is_global_admin is not None:
134
- params["is_global_admin"] = is_global_admin
139
+ params = self._build_params(is_global_admin=is_global_admin)
135
140
 
136
141
  async for item in self._iter_all_pages(
137
142
  Endpoints.ADMIN_DEVELOPERS,
138
143
  limit=limit,
139
- params=params or None,
144
+ params=params,
140
145
  ):
141
146
  yield Developer.model_validate(item)
142
147
 
143
- async def get(self, developer_id: str) -> Developer:
148
+ async def get_developer(self, developer_id: str) -> Developer:
144
149
  """Retrieve detailed information about a specific developer.
145
150
 
146
151
  Args:
@@ -156,7 +161,7 @@ class GlobalAdminService(BaseService):
156
161
  response = await self._get(Endpoints.ADMIN_DEVELOPER.format(developer_id=developer_id))
157
162
  return Developer.model_validate(response.json())
158
163
 
159
- async def create(
164
+ async def create_developer(
160
165
  self,
161
166
  request: DeveloperCreate | None = None,
162
167
  **kwargs,
@@ -187,7 +192,7 @@ class GlobalAdminService(BaseService):
187
192
  )
188
193
  return Developer.model_validate(response.json())
189
194
 
190
- async def update(
195
+ async def update_developer(
191
196
  self,
192
197
  developer_id: str,
193
198
  request: DeveloperUpdate | None = None,
@@ -220,7 +225,7 @@ class GlobalAdminService(BaseService):
220
225
  )
221
226
  return Developer.model_validate(response.json())
222
227
 
223
- async def delete(self, developer_id: str) -> None:
228
+ async def delete_developer(self, developer_id: str) -> None:
224
229
  """Remove a developer account from the system.
225
230
 
226
231
  The developer's API key will be invalidated immediately.
@@ -234,6 +239,180 @@ class GlobalAdminService(BaseService):
234
239
  """
235
240
  await self._delete(Endpoints.ADMIN_DEVELOPER.format(developer_id=developer_id))
236
241
 
242
+ async def get_user_page(
243
+ self,
244
+ *,
245
+ company_id: str | None = None,
246
+ role: UserRole | None = None,
247
+ email: str | None = None,
248
+ is_archived: bool | None = None,
249
+ limit: int = DEFAULT_PAGE_LIMIT,
250
+ offset: int = 0,
251
+ ) -> PaginatedResponse[AdminUser]:
252
+ """Retrieve a paginated page of users across all companies.
253
+
254
+ Authenticates with the global-admin API key only; no On-Behalf-Of header
255
+ is sent. Archived (soft-deleted) users are included when
256
+ ``is_archived=True``.
257
+
258
+ Args:
259
+ company_id: Filter to a single company.
260
+ role: Filter by user role.
261
+ email: Filter by exact email match.
262
+ is_archived: Filter by archived state.
263
+ limit: Maximum number of items to return (0-100, default 10).
264
+ offset: Number of items to skip from the beginning (default 0).
265
+
266
+ Returns:
267
+ A PaginatedResponse containing AdminUser objects and pagination metadata.
268
+ """
269
+ params = self._build_params(
270
+ company_id=company_id, role=role, email=email, is_archived=is_archived
271
+ )
272
+ return await self._get_paginated_as(
273
+ Endpoints.ADMIN_USERS,
274
+ AdminUser,
275
+ limit=limit,
276
+ offset=offset,
277
+ params=params,
278
+ )
279
+
280
+ async def list_all_users(
281
+ self,
282
+ *,
283
+ company_id: str | None = None,
284
+ role: UserRole | None = None,
285
+ email: str | None = None,
286
+ is_archived: bool | None = None,
287
+ ) -> list[AdminUser]:
288
+ """Retrieve all users across all companies.
289
+
290
+ Automatically paginates through all results. Use ``get_user_page()`` for
291
+ paginated access or ``iter_all_users()`` for memory-efficient streaming.
292
+
293
+ Args:
294
+ company_id: Filter to a single company.
295
+ role: Filter by user role.
296
+ email: Filter by exact email match.
297
+ is_archived: Filter by archived state.
298
+
299
+ Returns:
300
+ A list of all matching AdminUser objects.
301
+ """
302
+ return [
303
+ user
304
+ async for user in self.iter_all_users(
305
+ company_id=company_id, role=role, email=email, is_archived=is_archived
306
+ )
307
+ ]
308
+
309
+ async def iter_all_users(
310
+ self,
311
+ *,
312
+ limit: int = 100,
313
+ company_id: str | None = None,
314
+ role: UserRole | None = None,
315
+ email: str | None = None,
316
+ is_archived: bool | None = None,
317
+ ) -> AsyncIterator[AdminUser]:
318
+ """Iterate through all users across all companies.
319
+
320
+ Yields individual users, automatically fetching subsequent pages until all
321
+ matching users have been retrieved.
322
+
323
+ Args:
324
+ limit: Items per page (default 100 for efficiency).
325
+ company_id: Filter to a single company.
326
+ role: Filter by user role.
327
+ email: Filter by exact email match.
328
+ is_archived: Filter by archived state.
329
+
330
+ Yields:
331
+ Individual AdminUser objects.
332
+ """
333
+ params = self._build_params(
334
+ company_id=company_id, role=role, email=email, is_archived=is_archived
335
+ )
336
+ async for item in self._iter_all_pages(
337
+ Endpoints.ADMIN_USERS,
338
+ limit=limit,
339
+ params=params,
340
+ ):
341
+ yield AdminUser.model_validate(item)
342
+
343
+ async def get_user(self, user_id: str) -> AdminUser:
344
+ """Retrieve a single user by ID, including archived users.
345
+
346
+ Args:
347
+ user_id: The ID of the user to retrieve.
348
+
349
+ Returns:
350
+ The AdminUser object, with ``is_archived`` reflecting soft-delete state.
351
+ """
352
+ response = await self._get(Endpoints.ADMIN_USER.format(user_id=user_id))
353
+ return AdminUser.model_validate(response.json())
354
+
355
+ async def create_user(
356
+ self,
357
+ request: AdminUserCreate | None = None,
358
+ **kwargs,
359
+ ) -> AdminUser:
360
+ """Create a user in a target company.
361
+
362
+ The role assignment matrix enforced on company-scoped endpoints does not
363
+ apply here, but the server still rejects ``UNAPPROVED``/``DEACTIVATED`` on
364
+ create.
365
+
366
+ Args:
367
+ request: An AdminUserCreate model, OR pass fields as keyword arguments.
368
+ **kwargs: Fields for AdminUserCreate if request is not provided.
369
+ Requires: company_id, username, email, role.
370
+
371
+ Returns:
372
+ The newly created AdminUser object.
373
+ """
374
+ body = request if request is not None else self._validate_request(AdminUserCreate, **kwargs)
375
+ response = await self._post(
376
+ Endpoints.ADMIN_USERS,
377
+ json=body.model_dump(exclude_none=True, exclude_unset=True, mode="json"),
378
+ )
379
+ return AdminUser.model_validate(response.json())
380
+
381
+ async def update_user(
382
+ self,
383
+ user_id: str,
384
+ request: AdminUserUpdate | None = None,
385
+ **kwargs,
386
+ ) -> AdminUser:
387
+ """Update any user by ID, bypassing the company-scoped role hierarchy.
388
+
389
+ Set ``is_archived=False`` to restore a soft-deleted user.
390
+
391
+ Args:
392
+ user_id: The ID of the user to update.
393
+ request: An AdminUserUpdate model, OR pass fields as keyword arguments.
394
+ **kwargs: Fields for AdminUserUpdate if request is not provided.
395
+
396
+ Returns:
397
+ The updated AdminUser object.
398
+ """
399
+ body = request if request is not None else self._validate_request(AdminUserUpdate, **kwargs)
400
+ response = await self._patch(
401
+ Endpoints.ADMIN_USER.format(user_id=user_id),
402
+ json=body.model_dump(exclude_none=True, exclude_unset=True, mode="json"),
403
+ )
404
+ return AdminUser.model_validate(response.json())
405
+
406
+ async def delete_user(self, user_id: str) -> None:
407
+ """Soft-delete a user by ID.
408
+
409
+ The deletion is reversible via ``update_user(user_id, is_archived=False)``.
410
+
411
+ Args:
412
+ user_id: The ID of the user to soft-delete.
413
+ """
414
+ await self._delete(Endpoints.ADMIN_USER.format(user_id=user_id))
415
+
237
416
  async def create_api_key(self, developer_id: str) -> DeveloperWithApiKey:
238
417
  """Generate a new API key for a developer.
239
418
 
@@ -15,6 +15,7 @@ except ImportError as e:
15
15
  from vclient._sync.testing import SyncFakeVClient
16
16
  from vclient.testing._client import FakeVClient
17
17
  from vclient.testing._factories import (
18
+ AdminUserFactory,
18
19
  AssetFactory,
19
20
  AuditLogDetailFactory,
20
21
  AuditLogFactory,
@@ -68,6 +69,7 @@ from vclient.testing._factories import (
68
69
  from vclient.testing._routes import Routes, RouteSpec
69
70
 
70
71
  __all__ = [
72
+ "AdminUserFactory",
71
73
  "AssetFactory",
72
74
  "AuditLogDetailFactory",
73
75
  "AuditLogFactory",
@@ -3,6 +3,7 @@
3
3
  from polyfactory.factories.pydantic_factory import ModelFactory
4
4
 
5
5
  from vclient.models import (
6
+ AdminUser,
6
7
  Asset,
7
8
  AuditLog,
8
9
  AuditLogDetail,
@@ -272,6 +273,12 @@ class TraitFactory(ModelFactory[Trait]):
272
273
  __use_defaults__ = True
273
274
 
274
275
 
276
+ class AdminUserFactory(ModelFactory[AdminUser]):
277
+ __model__ = AdminUser
278
+ __use_defaults__ = True
279
+ role = "PLAYER"
280
+
281
+
275
282
  class UserFactory(ModelFactory[User]):
276
283
  __model__ = User
277
284
  __use_defaults__ = True
@@ -306,6 +313,7 @@ class WerewolfTribeFactory(ModelFactory[WerewolfTribe]):
306
313
 
307
314
 
308
315
  __all__ = [
316
+ "AdminUserFactory",
309
317
  "AssetFactory",
310
318
  "AuditLogDetailFactory",
311
319
  "AuditLogFactory",
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
14
14
 
15
15
  from vclient.constants import REQUEST_ID_HEADER
16
16
  from vclient.models import (
17
+ AdminUser,
17
18
  Asset,
18
19
  AuditLog,
19
20
  AuditLogDetail,
@@ -59,6 +60,7 @@ from vclient.models import (
59
60
  from vclient.models.character_autogen import ChargenSessionResponse
60
61
  from vclient.models.users import CampaignExperience
61
62
  from vclient.testing._factories import (
63
+ AdminUserFactory,
62
64
  AssetFactory,
63
65
  AuditLogDetailFactory,
64
66
  AuditLogFactory,
@@ -106,6 +108,7 @@ from vclient.testing._factories import (
106
108
  from vclient.testing._routes import BYTES, LIST, NO_CONTENT, PAGINATED, RAW_JSON, Routes, RouteSpec
107
109
 
108
110
  _FACTORY_MAP: dict[type, type[ModelFactory]] = {
111
+ AdminUser: AdminUserFactory,
109
112
  AuditLog: AuditLogFactory,
110
113
  AuditLogDetail: AuditLogDetailFactory,
111
114
  Asset: AssetFactory,
@@ -6,6 +6,7 @@ from typing import NamedTuple
6
6
 
7
7
  from vclient.endpoints import Endpoints
8
8
  from vclient.models import (
9
+ AdminUser,
9
10
  Asset,
10
11
  AuditLog,
11
12
  BulkAssignTraitResponse,
@@ -112,6 +113,13 @@ class Routes:
112
113
  ADMIN_LOGS_TAIL = RouteSpec("GET", Endpoints.ADMIN_LOGS, LIST, ServerLogEntry)
113
114
  ADMIN_LOGS_DOWNLOAD = RouteSpec("GET", Endpoints.ADMIN_LOGS_DOWNLOAD, BYTES, None)
114
115
 
116
+ # Admin users (cross-company, global-admin only)
117
+ ADMIN_USERS_LIST = RouteSpec("GET", Endpoints.ADMIN_USERS, PAGINATED, AdminUser)
118
+ ADMIN_USERS_GET = RouteSpec("GET", Endpoints.ADMIN_USER, SINGLE, AdminUser)
119
+ ADMIN_USERS_CREATE = RouteSpec("POST", Endpoints.ADMIN_USERS, SINGLE, AdminUser)
120
+ ADMIN_USERS_UPDATE = RouteSpec("PATCH", Endpoints.ADMIN_USER, SINGLE, AdminUser)
121
+ ADMIN_USERS_DELETE = RouteSpec("DELETE", Endpoints.ADMIN_USER, NO_CONTENT, None)
122
+
115
123
  # Developer self-service
116
124
  DEVELOPERS_ME_GET = RouteSpec("GET", Endpoints.DEVELOPER_ME, SINGLE, MeDeveloper)
117
125
  DEVELOPERS_ME_UPDATE = RouteSpec("PATCH", Endpoints.DEVELOPER_ME, SINGLE, MeDeveloper)
@@ -77,6 +77,7 @@ CONSTANT_MAP: dict[str, ConstantMapping] = {
77
77
  "HunterCreed": ConstantMapping("characters", "HunterCreed"),
78
78
  "HunterEdgeType": ConstantMapping("characters", "HunterEdgeType"),
79
79
  "ManageCampaignPermission": ConstantMapping("companies", "PermissionManageCampaign"),
80
+ "ManageNPCPermission": ConstantMapping("companies", "PermissionManageNPC"),
80
81
  "PermissionLevel": ConstantMapping("companies", "CompanyPermission"),
81
82
  "RecoupXPPermission": ConstantMapping("companies", "PermissionsRecoupXP"),
82
83
  "RollResultType": ConstantMapping("gameplay", "RollResultType"),