valentina-python-client 1.20.1__tar.gz → 1.21.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 (79) hide show
  1. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/PKG-INFO +1 -1
  2. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/pyproject.toml +1 -1
  3. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/__init__.py +5 -1
  4. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_codegen.py +5 -0
  5. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/__init__.py +2 -0
  6. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/client.py +15 -0
  7. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/registry.py +24 -0
  8. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/__init__.py +2 -0
  9. valentina_python_client-1.21.0/src/vclient/_sync/services/user_lookup.py +68 -0
  10. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/client.py +15 -0
  11. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/endpoints.py +3 -0
  12. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/__init__.py +4 -0
  13. valentina_python_client-1.21.0/src/vclient/models/user_lookup.py +22 -0
  14. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/registry.py +24 -0
  15. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/__init__.py +2 -0
  16. valentina_python_client-1.21.0/src/vclient/services/user_lookup.py +67 -0
  17. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/testing/__init__.py +2 -0
  18. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/testing/_factories.py +8 -0
  19. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/testing/_router.py +3 -0
  20. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/testing/_routes.py +3 -0
  21. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/LICENSE +0 -0
  22. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/README.md +0 -0
  23. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/base.py +0 -0
  24. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/campaign_book_chapters.py +0 -0
  25. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/campaign_books.py +0 -0
  26. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/campaigns.py +0 -0
  27. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/character_autogen.py +0 -0
  28. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/character_blueprint.py +0 -0
  29. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/character_traits.py +0 -0
  30. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/characters.py +0 -0
  31. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/companies.py +0 -0
  32. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/developers.py +0 -0
  33. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/dicerolls.py +0 -0
  34. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/dictionary.py +0 -0
  35. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/global_admin.py +0 -0
  36. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/options.py +0 -0
  37. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/system.py +0 -0
  38. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/services/users.py +0 -0
  39. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/testing/__init__.py +0 -0
  40. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/_sync/testing/_client.py +0 -0
  41. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/config.py +0 -0
  42. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/constants.py +0 -0
  43. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/exceptions.py +0 -0
  44. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/books.py +0 -0
  45. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/campaigns.py +0 -0
  46. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/chapters.py +0 -0
  47. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/character_autogen.py +0 -0
  48. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/character_blueprint.py +0 -0
  49. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/character_trait.py +0 -0
  50. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/characters.py +0 -0
  51. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/companies.py +0 -0
  52. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/developers.py +0 -0
  53. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/diceroll.py +0 -0
  54. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/dictionary.py +0 -0
  55. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/full_sheet.py +0 -0
  56. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/global_admin.py +0 -0
  57. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/pagination.py +0 -0
  58. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/shared.py +0 -0
  59. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/system.py +0 -0
  60. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/models/users.py +0 -0
  61. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/py.typed +0 -0
  62. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/base.py +0 -0
  63. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/campaign_book_chapters.py +0 -0
  64. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/campaign_books.py +0 -0
  65. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/campaigns.py +0 -0
  66. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/character_autogen.py +0 -0
  67. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/character_blueprint.py +0 -0
  68. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/character_traits.py +0 -0
  69. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/characters.py +0 -0
  70. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/companies.py +0 -0
  71. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/developers.py +0 -0
  72. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/dicerolls.py +0 -0
  73. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/dictionary.py +0 -0
  74. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/global_admin.py +0 -0
  75. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/options.py +0 -0
  76. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/system.py +0 -0
  77. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/services/users.py +0 -0
  78. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/testing/_client.py +0 -0
  79. {valentina_python_client-1.20.1 → valentina_python_client-1.21.0}/src/vclient/validate_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valentina-python-client
3
- Version: 1.20.1
3
+ Version: 1.21.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 = "1.20.1"
19
+ version = "1.21.0"
20
20
 
21
21
  [project.optional-dependencies]
22
22
  testing = ["polyfactory>=3.3.0"]
@@ -49,6 +49,7 @@ from vclient._sync import ( # noqa: E402
49
49
  sync_global_admin_service,
50
50
  sync_options_service,
51
51
  sync_system_service,
52
+ sync_user_lookup_service,
52
53
  sync_users_service,
53
54
  )
54
55
  from vclient.client import VClient # noqa: E402
@@ -67,6 +68,7 @@ from vclient.registry import ( # noqa: E402
67
68
  global_admin_service,
68
69
  options_service,
69
70
  system_service,
71
+ user_lookup_service,
70
72
  users_service,
71
73
  )
72
74
 
@@ -100,9 +102,11 @@ __all__ = (
100
102
  "sync_global_admin_service",
101
103
  "sync_options_service",
102
104
  "sync_system_service",
105
+ "sync_user_lookup_service",
103
106
  "sync_users_service",
104
107
  "system_service",
108
+ "user_lookup_service",
105
109
  "users_service",
106
110
  )
107
111
 
108
- __version__ = "1.20.1"
112
+ __version__ = "1.21.0"
@@ -28,6 +28,7 @@ RENAME_CLASSES: dict[str, str] = {
28
28
  "GlobalAdminService": "SyncGlobalAdminService",
29
29
  "OptionsService": "SyncOptionsService",
30
30
  "SystemService": "SyncSystemService",
31
+ "UserLookupService": "SyncUserLookupService",
31
32
  "UsersService": "SyncUsersService",
32
33
  "VClient": "SyncVClient",
33
34
  "FakeVClient": "SyncFakeVClient",
@@ -49,6 +50,7 @@ FACTORY_RENAMES: dict[str, str] = {
49
50
  "dicerolls_service": "sync_dicerolls_service",
50
51
  "options_service": "sync_options_service",
51
52
  "character_autogen_service": "sync_character_autogen_service",
53
+ "user_lookup_service": "sync_user_lookup_service",
52
54
  "configure_default_client": "sync_configure_default_client",
53
55
  "clear_default_client": "sync_clear_default_client",
54
56
  "default_client": "sync_default_client",
@@ -73,6 +75,7 @@ IMPORT_REWRITES: dict[str, str] = {
73
75
  "vclient.services.dicerolls": "vclient._sync.services.dicerolls",
74
76
  "vclient.services.options": "vclient._sync.services.options",
75
77
  "vclient.services.character_autogen": "vclient._sync.services.character_autogen",
78
+ "vclient.services.user_lookup": "vclient._sync.services.user_lookup",
76
79
  "vclient.registry": "vclient._sync.registry",
77
80
  "vclient.testing._client": "vclient._sync.testing._client",
78
81
  }
@@ -307,6 +310,7 @@ def _write_sync_init(path: Path) -> None:
307
310
  " sync_global_admin_service,",
308
311
  " sync_options_service,",
309
312
  " sync_system_service,",
313
+ " sync_user_lookup_service,",
310
314
  " sync_users_service,",
311
315
  ")",
312
316
  "",
@@ -329,6 +333,7 @@ def _write_sync_init(path: Path) -> None:
329
333
  ' "sync_global_admin_service",',
330
334
  ' "sync_options_service",',
331
335
  ' "sync_system_service",',
336
+ ' "sync_user_lookup_service",',
332
337
  ' "sync_users_service",',
333
338
  "]",
334
339
  "",
@@ -19,6 +19,7 @@ from vclient._sync.registry import (
19
19
  sync_global_admin_service,
20
20
  sync_options_service,
21
21
  sync_system_service,
22
+ sync_user_lookup_service,
22
23
  sync_users_service,
23
24
  )
24
25
 
@@ -41,5 +42,6 @@ __all__ = [
41
42
  "sync_global_admin_service",
42
43
  "sync_options_service",
43
44
  "sync_system_service",
45
+ "sync_user_lookup_service",
44
46
  "sync_users_service",
45
47
  ]
@@ -37,6 +37,7 @@ if TYPE_CHECKING:
37
37
  SyncGlobalAdminService,
38
38
  SyncOptionsService,
39
39
  SyncSystemService,
40
+ SyncUserLookupService,
40
41
  SyncUsersService,
41
42
  )
42
43
 
@@ -147,6 +148,7 @@ class SyncVClient:
147
148
  self._developer: SyncDeveloperService | None = None
148
149
  self._global_admin: SyncGlobalAdminService | None = None
149
150
  self._system: SyncSystemService | None = None
151
+ self._user_lookup: SyncUserLookupService | None = None
150
152
  if set_as_default:
151
153
  from vclient._sync.registry import sync_configure_default_client
152
154
 
@@ -278,6 +280,19 @@ class SyncVClient:
278
280
  self._system = SyncSystemService(self)
279
281
  return self._system
280
282
 
283
+ @property
284
+ def user_lookup(self) -> "SyncUserLookupService":
285
+ """Access the User Lookup service for cross-company user discovery.
286
+
287
+ Returns:
288
+ The SyncUserLookupService instance for user lookup operations.
289
+ """
290
+ if self._user_lookup is None:
291
+ from vclient._sync.services.user_lookup import SyncUserLookupService
292
+
293
+ self._user_lookup = SyncUserLookupService(self)
294
+ return self._user_lookup
295
+
281
296
  def users(self, company_id: str | None = None) -> "SyncUsersService":
282
297
  """Get a SyncUsersService scoped to a specific company.
283
298
 
@@ -38,6 +38,7 @@ if TYPE_CHECKING:
38
38
  SyncGlobalAdminService,
39
39
  SyncOptionsService,
40
40
  SyncSystemService,
41
+ SyncUserLookupService,
41
42
  SyncUsersService,
42
43
  )
43
44
  _default_client: "SyncVClient | None" = None
@@ -189,6 +190,29 @@ def sync_system_service() -> "SyncSystemService":
189
190
  return SyncSystemService(sync_default_client())
190
191
 
191
192
 
193
+ def sync_user_lookup_service() -> "SyncUserLookupService":
194
+ """Create a SyncUserLookupService using the default client.
195
+
196
+ Discover which companies a person has a user record in by searching
197
+ via email, Discord ID, Google ID, or GitHub ID.
198
+
199
+ Returns:
200
+ SyncUserLookupService: A service instance for cross-company user lookup.
201
+
202
+ Raises:
203
+ RuntimeError: If no default client has been configured.
204
+
205
+ Example:
206
+ ```python
207
+ lookup = sync_user_lookup_service()
208
+ results = await lookup.by_email("alice@example.com")
209
+ ```
210
+ """
211
+ from vclient._sync.services.user_lookup import SyncUserLookupService
212
+
213
+ return SyncUserLookupService(sync_default_client())
214
+
215
+
192
216
  def sync_users_service(company_id: str | None = None) -> "SyncUsersService":
193
217
  """Create a SyncUsersService scoped to a specific company using the default client.
194
218
 
@@ -16,6 +16,7 @@ from .dictionary import SyncDictionaryService
16
16
  from .global_admin import SyncGlobalAdminService
17
17
  from .options import SyncOptionsService
18
18
  from .system import SyncSystemService
19
+ from .user_lookup import SyncUserLookupService
19
20
  from .users import SyncUsersService
20
21
 
21
22
  __all__ = [
@@ -34,5 +35,6 @@ __all__ = [
34
35
  "SyncGlobalAdminService",
35
36
  "SyncOptionsService",
36
37
  "SyncSystemService",
38
+ "SyncUserLookupService",
37
39
  "SyncUsersService",
38
40
  ]
@@ -0,0 +1,68 @@
1
+ # AUTO-GENERATED — do not edit. Run 'uv run duty generate_sync' to regenerate.
2
+ """Service for cross-company user lookup."""
3
+
4
+ from vclient._sync.services.base import SyncBaseService
5
+ from vclient.endpoints import Endpoints
6
+ from vclient.models import UserLookupResult
7
+
8
+
9
+ class SyncUserLookupService(SyncBaseService):
10
+ """Service for looking up users across companies.
11
+
12
+ Discover which companies a person has a user record in by searching
13
+ via email, Discord ID, Google ID, or GitHub ID.
14
+
15
+ Example:
16
+ >>> async with SyncVClient() as client:
17
+ ... results = await client.user_lookup.by_email("alice@example.com")
18
+ ... for r in results:
19
+ ... print(f"{r.company_name}: {r.role}")
20
+ """
21
+
22
+ def by_email(self, email: str) -> list[UserLookupResult]:
23
+ """Look up a user by email address.
24
+
25
+ Args:
26
+ email: Exact email address to search for.
27
+
28
+ Returns:
29
+ List of companies where a matching user was found. Empty list if no matches.
30
+ """
31
+ response = self._get(Endpoints.USERS_LOOKUP, params={"email": email})
32
+ return [UserLookupResult.model_validate(item) for item in response.json()]
33
+
34
+ def by_discord_id(self, discord_id: str) -> list[UserLookupResult]:
35
+ """Look up a user by Discord profile ID.
36
+
37
+ Args:
38
+ discord_id: Discord profile ID to search for.
39
+
40
+ Returns:
41
+ List of companies where a matching user was found. Empty list if no matches.
42
+ """
43
+ response = self._get(Endpoints.USERS_LOOKUP, params={"discord_id": discord_id})
44
+ return [UserLookupResult.model_validate(item) for item in response.json()]
45
+
46
+ def by_google_id(self, google_id: str) -> list[UserLookupResult]:
47
+ """Look up a user by Google profile ID.
48
+
49
+ Args:
50
+ google_id: Google profile ID to search for.
51
+
52
+ Returns:
53
+ List of companies where a matching user was found. Empty list if no matches.
54
+ """
55
+ response = self._get(Endpoints.USERS_LOOKUP, params={"google_id": google_id})
56
+ return [UserLookupResult.model_validate(item) for item in response.json()]
57
+
58
+ def by_github_id(self, github_id: str) -> list[UserLookupResult]:
59
+ """Look up a user by GitHub profile ID.
60
+
61
+ Args:
62
+ github_id: GitHub profile ID to search for.
63
+
64
+ Returns:
65
+ List of companies where a matching user was found. Empty list if no matches.
66
+ """
67
+ response = self._get(Endpoints.USERS_LOOKUP, params={"github_id": github_id})
68
+ return [UserLookupResult.model_validate(item) for item in response.json()]
@@ -36,6 +36,7 @@ if TYPE_CHECKING:
36
36
  GlobalAdminService,
37
37
  OptionsService,
38
38
  SystemService,
39
+ UserLookupService,
39
40
  UsersService,
40
41
  )
41
42
 
@@ -150,6 +151,7 @@ class VClient:
150
151
  self._developer: DeveloperService | None = None
151
152
  self._global_admin: GlobalAdminService | None = None
152
153
  self._system: SystemService | None = None
154
+ self._user_lookup: UserLookupService | None = None
153
155
 
154
156
  if set_as_default:
155
157
  from vclient.registry import configure_default_client
@@ -290,6 +292,19 @@ class VClient:
290
292
  self._system = SystemService(self)
291
293
  return self._system
292
294
 
295
+ @property
296
+ def user_lookup(self) -> "UserLookupService":
297
+ """Access the User Lookup service for cross-company user discovery.
298
+
299
+ Returns:
300
+ The UserLookupService instance for user lookup operations.
301
+ """
302
+ if self._user_lookup is None:
303
+ from vclient.services.user_lookup import UserLookupService
304
+
305
+ self._user_lookup = UserLookupService(self)
306
+ return self._user_lookup
307
+
293
308
  def users(self, company_id: str | None = None) -> "UsersService":
294
309
  """Get a UsersService scoped to a specific company.
295
310
 
@@ -19,6 +19,9 @@ class Endpoints:
19
19
  # System endpoints
20
20
  HEALTH = f"{_BASE}/health"
21
21
 
22
+ # User lookup (cross-company, not scoped)
23
+ USERS_LOOKUP = f"{_BASE}/users/lookup"
24
+
22
25
  # Global Admin endpoints
23
26
  ADMIN_DEVELOPERS = f"{_BASE}/admin/developers"
24
27
  ADMIN_DEVELOPER = f"{_BASE}/admin/developers/{{developer_id}}"
@@ -115,6 +115,9 @@ from .shared import (
115
115
  Trait,
116
116
  )
117
117
  from .system import SystemHealth
118
+ from .user_lookup import (
119
+ UserLookupResult,
120
+ )
118
121
  from .users import (
119
122
  CampaignExperience,
120
123
  DiscordProfile,
@@ -221,6 +224,7 @@ __all__ = [
221
224
  "UserCreate",
222
225
  "UserDenyDTO",
223
226
  "UserDetail",
227
+ "UserLookupResult",
224
228
  "UserMergeDTO",
225
229
  "UserRegisterDTO",
226
230
  "UserUpdate",
@@ -0,0 +1,22 @@
1
+ """Pydantic models for User Lookup API responses."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from vclient.constants import UserRole
6
+
7
+
8
+ class UserLookupResult(BaseModel):
9
+ """A single result from a cross-company user lookup.
10
+
11
+ Each result represents a company where the looked-up person has a user record.
12
+ """
13
+
14
+ company_id: str
15
+ company_name: str
16
+ user_id: str
17
+ role: UserRole
18
+
19
+
20
+ __all__ = [
21
+ "UserLookupResult",
22
+ ]
@@ -37,6 +37,7 @@ if TYPE_CHECKING:
37
37
  GlobalAdminService,
38
38
  OptionsService,
39
39
  SystemService,
40
+ UserLookupService,
40
41
  UsersService,
41
42
  )
42
43
 
@@ -189,6 +190,29 @@ def system_service() -> "SystemService":
189
190
  return SystemService(default_client())
190
191
 
191
192
 
193
+ def user_lookup_service() -> "UserLookupService":
194
+ """Create a UserLookupService using the default client.
195
+
196
+ Discover which companies a person has a user record in by searching
197
+ via email, Discord ID, Google ID, or GitHub ID.
198
+
199
+ Returns:
200
+ UserLookupService: A service instance for cross-company user lookup.
201
+
202
+ Raises:
203
+ RuntimeError: If no default client has been configured.
204
+
205
+ Example:
206
+ ```python
207
+ lookup = user_lookup_service()
208
+ results = await lookup.by_email("alice@example.com")
209
+ ```
210
+ """
211
+ from vclient.services.user_lookup import UserLookupService
212
+
213
+ return UserLookupService(default_client())
214
+
215
+
192
216
  def users_service(company_id: str | None = None) -> "UsersService":
193
217
  """Create a UsersService scoped to a specific company using the default client.
194
218
 
@@ -15,6 +15,7 @@ from .dictionary import DictionaryService
15
15
  from .global_admin import GlobalAdminService
16
16
  from .options import OptionsService
17
17
  from .system import SystemService
18
+ from .user_lookup import UserLookupService
18
19
  from .users import UsersService
19
20
 
20
21
  __all__ = [
@@ -33,5 +34,6 @@ __all__ = [
33
34
  "GlobalAdminService",
34
35
  "OptionsService",
35
36
  "SystemService",
37
+ "UserLookupService",
36
38
  "UsersService",
37
39
  ]
@@ -0,0 +1,67 @@
1
+ """Service for cross-company user lookup."""
2
+
3
+ from vclient.endpoints import Endpoints
4
+ from vclient.models import UserLookupResult
5
+ from vclient.services.base import BaseService
6
+
7
+
8
+ class UserLookupService(BaseService):
9
+ """Service for looking up users across companies.
10
+
11
+ Discover which companies a person has a user record in by searching
12
+ via email, Discord ID, Google ID, or GitHub ID.
13
+
14
+ Example:
15
+ >>> async with VClient() as client:
16
+ ... results = await client.user_lookup.by_email("alice@example.com")
17
+ ... for r in results:
18
+ ... print(f"{r.company_name}: {r.role}")
19
+ """
20
+
21
+ async def by_email(self, email: str) -> list[UserLookupResult]:
22
+ """Look up a user by email address.
23
+
24
+ Args:
25
+ email: Exact email address to search for.
26
+
27
+ Returns:
28
+ List of companies where a matching user was found. Empty list if no matches.
29
+ """
30
+ response = await self._get(Endpoints.USERS_LOOKUP, params={"email": email})
31
+ return [UserLookupResult.model_validate(item) for item in response.json()]
32
+
33
+ async def by_discord_id(self, discord_id: str) -> list[UserLookupResult]:
34
+ """Look up a user by Discord profile ID.
35
+
36
+ Args:
37
+ discord_id: Discord profile ID to search for.
38
+
39
+ Returns:
40
+ List of companies where a matching user was found. Empty list if no matches.
41
+ """
42
+ response = await self._get(Endpoints.USERS_LOOKUP, params={"discord_id": discord_id})
43
+ return [UserLookupResult.model_validate(item) for item in response.json()]
44
+
45
+ async def by_google_id(self, google_id: str) -> list[UserLookupResult]:
46
+ """Look up a user by Google profile ID.
47
+
48
+ Args:
49
+ google_id: Google profile ID to search for.
50
+
51
+ Returns:
52
+ List of companies where a matching user was found. Empty list if no matches.
53
+ """
54
+ response = await self._get(Endpoints.USERS_LOOKUP, params={"google_id": google_id})
55
+ return [UserLookupResult.model_validate(item) for item in response.json()]
56
+
57
+ async def by_github_id(self, github_id: str) -> list[UserLookupResult]:
58
+ """Look up a user by GitHub profile ID.
59
+
60
+ Args:
61
+ github_id: GitHub profile ID to search for.
62
+
63
+ Returns:
64
+ List of companies where a matching user was found. Empty list if no matches.
65
+ """
66
+ response = await self._get(Endpoints.USERS_LOOKUP, params={"github_id": github_id})
67
+ return [UserLookupResult.model_validate(item) for item in response.json()]
@@ -57,6 +57,7 @@ from vclient.testing._factories import (
57
57
  TraitSubcategoryFactory,
58
58
  UserDetailFactory,
59
59
  UserFactory,
60
+ UserLookupResultFactory,
60
61
  VampireClanFactory,
61
62
  WerewolfAuspiceFactory,
62
63
  WerewolfTribeFactory,
@@ -110,6 +111,7 @@ __all__ = [
110
111
  "TraitSubcategoryFactory",
111
112
  "UserDetailFactory",
112
113
  "UserFactory",
114
+ "UserLookupResultFactory",
113
115
  "VampireClanFactory",
114
116
  "WerewolfAuspiceFactory",
115
117
  "WerewolfTribeFactory",
@@ -43,6 +43,7 @@ from vclient.models import (
43
43
  TraitSubcategory,
44
44
  User,
45
45
  UserDetail,
46
+ UserLookupResult,
46
47
  VampireClan,
47
48
  WerewolfAuspice,
48
49
  WerewolfTribe,
@@ -259,6 +260,12 @@ class UserFactory(ModelFactory[User]):
259
260
  role = "PLAYER"
260
261
 
261
262
 
263
+ class UserLookupResultFactory(ModelFactory[UserLookupResult]):
264
+ __model__ = UserLookupResult
265
+ __use_defaults__ = True
266
+ role = "PLAYER"
267
+
268
+
262
269
  class UserDetailFactory(ModelFactory[UserDetail]):
263
270
  __model__ = UserDetail
264
271
  __use_defaults__ = True
@@ -323,6 +330,7 @@ __all__ = [
323
330
  "TraitSubcategoryFactory",
324
331
  "UserDetailFactory",
325
332
  "UserFactory",
333
+ "UserLookupResultFactory",
326
334
  "VampireClanFactory",
327
335
  "WerewolfAuspiceFactory",
328
336
  "WerewolfTribeFactory",
@@ -48,6 +48,7 @@ from vclient.models import (
48
48
  TraitSubcategory,
49
49
  User,
50
50
  UserDetail,
51
+ UserLookupResult,
51
52
  VampireClan,
52
53
  WerewolfAuspice,
53
54
  WerewolfTribe,
@@ -91,6 +92,7 @@ from vclient.testing._factories import (
91
92
  TraitSubcategoryFactory,
92
93
  UserDetailFactory,
93
94
  UserFactory,
95
+ UserLookupResultFactory,
94
96
  VampireClanFactory,
95
97
  WerewolfAuspiceFactory,
96
98
  WerewolfTribeFactory,
@@ -133,6 +135,7 @@ _FACTORY_MAP: dict[type, type[ModelFactory]] = {
133
135
  TraitCategory: TraitCategoryFactory,
134
136
  TraitSubcategory: TraitSubcategoryFactory,
135
137
  User: UserFactory,
138
+ UserLookupResult: UserLookupResultFactory,
136
139
  UserDetail: UserDetailFactory,
137
140
  VampireClan: VampireClanFactory,
138
141
  WerewolfAuspice: WerewolfAuspiceFactory,
@@ -40,6 +40,7 @@ from vclient.models import (
40
40
  TraitSubcategory,
41
41
  User,
42
42
  UserDetail,
43
+ UserLookupResult,
43
44
  VampireClan,
44
45
  WerewolfAuspice,
45
46
  WerewolfTribe,
@@ -131,6 +132,8 @@ class Routes:
131
132
  USERS_REGISTER = RouteSpec("POST", Endpoints.USER_REGISTER, SINGLE, User)
132
133
  USERS_MERGE = RouteSpec("POST", Endpoints.USER_MERGE, SINGLE, User)
133
134
  USERS_STATISTICS = RouteSpec("GET", Endpoints.USER_STATISTICS, SINGLE, RollStatistics)
135
+ # User lookup (cross-company)
136
+ USERS_LOOKUP = RouteSpec("GET", Endpoints.USERS_LOOKUP, LIST, UserLookupResult)
134
137
 
135
138
  # User assets
136
139
  USERS_ASSETS_LIST = RouteSpec("GET", Endpoints.USER_ASSETS, PAGINATED, Asset)