valentina-python-client 2.5.0__tar.gz → 2.7.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.

Potentially problematic release.


This version of valentina-python-client might be problematic. Click here for more details.

Files changed (86) hide show
  1. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/PKG-INFO +1 -1
  2. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/pyproject.toml +7 -6
  3. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/__init__.py +5 -5
  4. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_codegen.py +5 -5
  5. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/__init__.py +2 -2
  6. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/client.py +12 -13
  7. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/registry.py +12 -13
  8. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/__init__.py +2 -2
  9. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/base.py +10 -3
  10. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/developers.py +3 -1
  11. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/global_admin.py +3 -1
  12. valentina_python_client-2.7.0/src/vclient/_sync/services/identity.py +78 -0
  13. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/user_lookup.py +13 -1
  14. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/users.py +53 -10
  15. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/testing/_client.py +6 -0
  16. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/client.py +12 -13
  17. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/constants.py +5 -0
  18. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/endpoints.py +4 -1
  19. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/exceptions.py +25 -0
  20. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/__init__.py +10 -2
  21. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/developers.py +4 -1
  22. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/global_admin.py +3 -0
  23. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/shared.py +12 -0
  24. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/users.py +61 -20
  25. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/registry.py +12 -13
  26. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/__init__.py +2 -2
  27. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/base.py +15 -4
  28. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/developers.py +3 -1
  29. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/global_admin.py +3 -1
  30. valentina_python_client-2.7.0/src/vclient/services/identity.py +81 -0
  31. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/user_lookup.py +13 -1
  32. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/users.py +59 -10
  33. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/testing/__init__.py +4 -0
  34. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/testing/_client.py +6 -0
  35. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/testing/_factories.py +14 -0
  36. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/testing/_router.py +3 -0
  37. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/testing/_routes.py +5 -2
  38. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/validate_constants.py +39 -11
  39. valentina_python_client-2.5.0/src/vclient/_sync/services/user_self_registration.py +0 -71
  40. valentina_python_client-2.5.0/src/vclient/services/user_self_registration.py +0 -74
  41. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/LICENSE +0 -0
  42. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/README.md +0 -0
  43. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/_audit_params.py +0 -0
  44. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/campaign_book_chapters.py +0 -0
  45. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/campaign_books.py +0 -0
  46. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/campaigns.py +0 -0
  47. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/character_autogen.py +0 -0
  48. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/character_blueprint.py +0 -0
  49. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/character_traits.py +0 -0
  50. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/characters.py +0 -0
  51. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/companies.py +0 -0
  52. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/dicerolls.py +0 -0
  53. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/dictionary.py +0 -0
  54. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/options.py +0 -0
  55. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/services/system.py +0 -0
  56. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/_sync/testing/__init__.py +0 -0
  57. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/config.py +0 -0
  58. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/audit_logs.py +0 -0
  59. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/books.py +0 -0
  60. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/campaigns.py +0 -0
  61. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/chapters.py +0 -0
  62. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/character_autogen.py +0 -0
  63. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/character_blueprint.py +0 -0
  64. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/character_trait.py +0 -0
  65. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/characters.py +0 -0
  66. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/companies.py +0 -0
  67. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/diceroll.py +0 -0
  68. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/dictionary.py +0 -0
  69. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/full_sheet.py +0 -0
  70. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/pagination.py +0 -0
  71. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/system.py +0 -0
  72. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/models/user_lookup.py +0 -0
  73. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/py.typed +0 -0
  74. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/_audit_params.py +0 -0
  75. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/campaign_book_chapters.py +0 -0
  76. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/campaign_books.py +0 -0
  77. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/campaigns.py +0 -0
  78. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/character_autogen.py +0 -0
  79. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/character_blueprint.py +0 -0
  80. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/character_traits.py +0 -0
  81. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/characters.py +0 -0
  82. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/companies.py +0 -0
  83. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/dicerolls.py +0 -0
  84. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/dictionary.py +0 -0
  85. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/options.py +0 -0
  86. {valentina_python_client-2.5.0 → valentina_python_client-2.7.0}/src/vclient/services/system.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valentina-python-client
3
- Version: 2.5.0
3
+ Version: 2.7.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.5.0"
19
+ version = "2.7.0"
20
20
 
21
21
  [project.optional-dependencies]
22
22
  testing = ["polyfactory>=3.3.0"]
@@ -37,7 +37,7 @@
37
37
  "commitizen>=4.16.3",
38
38
  "coverage>=7.14.1",
39
39
  "duty>=1.9.0",
40
- "prek>=0.4.3",
40
+ "prek>=0.4.4",
41
41
  "pytest-anyio>=0.0.0",
42
42
  "pytest-clarity>=1.0.1",
43
43
  "pytest-cov>=7.1.0",
@@ -48,14 +48,14 @@
48
48
  "pytest-xdist>=3.8.0",
49
49
  "pytest>=9.0.3",
50
50
  "respx>=0.23.1",
51
- "ruff>=0.15.15",
51
+ "ruff>=0.15.16",
52
52
  "shellcheck-py>=0.11.0.1",
53
- "ty>=0.0.42",
54
- "typos>=1.47.0",
53
+ "ty>=0.0.44",
54
+ "typos>=1.47.2",
55
55
  "vulture>=2.16",
56
56
  "yamllint>=1.38.0",
57
57
  ]
58
- docs = ["zensical>=0.0.43"]
58
+ docs = ["zensical>=0.0.44"]
59
59
 
60
60
  [tool.commitizen]
61
61
  bump_message = "bump(release): v$current_version → v$new_version"
@@ -132,6 +132,7 @@
132
132
  ]
133
133
  per-file-ignores = { "src/vclient/_sync/**" = [
134
134
  "C416", # Unnecessary list comprehension (AST unparser artifact)
135
+ "C901", # Function too complex (noqa stripped by AST codegen)
135
136
  "D", # Docstring formatting (AST unparser doesn't preserve style)
136
137
  "E501", # Line too long
137
138
  "ERA001", # Commented-out code false positives
@@ -50,10 +50,10 @@ from vclient._sync import ( # noqa: E402
50
50
  sync_dicerolls_service,
51
51
  sync_dictionary_service,
52
52
  sync_global_admin_service,
53
+ sync_identity_service,
53
54
  sync_options_service,
54
55
  sync_system_service,
55
56
  sync_user_lookup_service,
56
- sync_user_self_registration_service,
57
57
  sync_users_service,
58
58
  )
59
59
  from vclient.client import VClient # noqa: E402
@@ -70,10 +70,10 @@ from vclient.registry import ( # noqa: E402
70
70
  dicerolls_service,
71
71
  dictionary_service,
72
72
  global_admin_service,
73
+ identity_service,
73
74
  options_service,
74
75
  system_service,
75
76
  user_lookup_service,
76
- user_self_registration_service,
77
77
  users_service,
78
78
  )
79
79
 
@@ -92,6 +92,7 @@ __all__ = (
92
92
  "dicerolls_service",
93
93
  "dictionary_service",
94
94
  "global_admin_service",
95
+ "identity_service",
95
96
  "options_service",
96
97
  "sync_books_service",
97
98
  "sync_campaigns_service",
@@ -105,15 +106,14 @@ __all__ = (
105
106
  "sync_dicerolls_service",
106
107
  "sync_dictionary_service",
107
108
  "sync_global_admin_service",
109
+ "sync_identity_service",
108
110
  "sync_options_service",
109
111
  "sync_system_service",
110
112
  "sync_user_lookup_service",
111
- "sync_user_self_registration_service",
112
113
  "sync_users_service",
113
114
  "system_service",
114
115
  "user_lookup_service",
115
- "user_self_registration_service",
116
116
  "users_service",
117
117
  )
118
118
 
119
- __version__ = "2.5.0"
119
+ __version__ = "2.7.0"
@@ -26,10 +26,10 @@ RENAME_CLASSES: dict[str, str] = {
26
26
  "DicerollService": "SyncDicerollService",
27
27
  "DictionaryService": "SyncDictionaryService",
28
28
  "GlobalAdminService": "SyncGlobalAdminService",
29
+ "IdentityService": "SyncIdentityService",
29
30
  "OptionsService": "SyncOptionsService",
30
31
  "SystemService": "SyncSystemService",
31
32
  "UserLookupService": "SyncUserLookupService",
32
- "UserSelfRegistrationService": "SyncUserSelfRegistrationService",
33
33
  "UsersService": "SyncUsersService",
34
34
  "VClient": "SyncVClient",
35
35
  "FakeVClient": "SyncFakeVClient",
@@ -39,6 +39,7 @@ FACTORY_RENAMES: dict[str, str] = {
39
39
  "companies_service": "sync_companies_service",
40
40
  "developer_service": "sync_developer_service",
41
41
  "global_admin_service": "sync_global_admin_service",
42
+ "identity_service": "sync_identity_service",
42
43
  "system_service": "sync_system_service",
43
44
  "users_service": "sync_users_service",
44
45
  "campaigns_service": "sync_campaigns_service",
@@ -52,7 +53,6 @@ FACTORY_RENAMES: dict[str, str] = {
52
53
  "options_service": "sync_options_service",
53
54
  "character_autogen_service": "sync_character_autogen_service",
54
55
  "user_lookup_service": "sync_user_lookup_service",
55
- "user_self_registration_service": "sync_user_self_registration_service",
56
56
  "configure_default_client": "sync_configure_default_client",
57
57
  "clear_default_client": "sync_clear_default_client",
58
58
  "default_client": "sync_default_client",
@@ -78,7 +78,7 @@ IMPORT_REWRITES: dict[str, str] = {
78
78
  "vclient.services.options": "vclient._sync.services.options",
79
79
  "vclient.services.character_autogen": "vclient._sync.services.character_autogen",
80
80
  "vclient.services.user_lookup": "vclient._sync.services.user_lookup",
81
- "vclient.services.user_self_registration": "vclient._sync.services.user_self_registration",
81
+ "vclient.services.identity": "vclient._sync.services.identity",
82
82
  "vclient.registry": "vclient._sync.registry",
83
83
  "vclient.testing._client": "vclient._sync.testing._client",
84
84
  }
@@ -311,10 +311,10 @@ def _write_sync_init(path: Path) -> None:
311
311
  " sync_dicerolls_service,",
312
312
  " sync_dictionary_service,",
313
313
  " sync_global_admin_service,",
314
+ " sync_identity_service,",
314
315
  " sync_options_service,",
315
316
  " sync_system_service,",
316
317
  " sync_user_lookup_service,",
317
- " sync_user_self_registration_service,",
318
318
  " sync_users_service,",
319
319
  ")",
320
320
  "",
@@ -335,10 +335,10 @@ def _write_sync_init(path: Path) -> None:
335
335
  ' "sync_dicerolls_service",',
336
336
  ' "sync_dictionary_service",',
337
337
  ' "sync_global_admin_service",',
338
+ ' "sync_identity_service",',
338
339
  ' "sync_options_service",',
339
340
  ' "sync_system_service",',
340
341
  ' "sync_user_lookup_service",',
341
- ' "sync_user_self_registration_service",',
342
342
  ' "sync_users_service",',
343
343
  "]",
344
344
  "",
@@ -17,10 +17,10 @@ from vclient._sync.registry import (
17
17
  sync_dicerolls_service,
18
18
  sync_dictionary_service,
19
19
  sync_global_admin_service,
20
+ sync_identity_service,
20
21
  sync_options_service,
21
22
  sync_system_service,
22
23
  sync_user_lookup_service,
23
- sync_user_self_registration_service,
24
24
  sync_users_service,
25
25
  )
26
26
 
@@ -41,9 +41,9 @@ __all__ = [
41
41
  "sync_dicerolls_service",
42
42
  "sync_dictionary_service",
43
43
  "sync_global_admin_service",
44
+ "sync_identity_service",
44
45
  "sync_options_service",
45
46
  "sync_system_service",
46
47
  "sync_user_lookup_service",
47
- "sync_user_self_registration_service",
48
48
  "sync_users_service",
49
49
  ]
@@ -35,10 +35,10 @@ if TYPE_CHECKING:
35
35
  SyncDicerollService,
36
36
  SyncDictionaryService,
37
37
  SyncGlobalAdminService,
38
+ SyncIdentityService,
38
39
  SyncOptionsService,
39
40
  SyncSystemService,
40
41
  SyncUserLookupService,
41
- SyncUserSelfRegistrationService,
42
42
  SyncUsersService,
43
43
  )
44
44
 
@@ -322,31 +322,30 @@ class SyncVClient:
322
322
  self, company_id=self._resolve_company_id(company_id), on_behalf_of=on_behalf_of
323
323
  )
324
324
 
325
- def user_self_registration(
326
- self, *, company_id: str | None = None
327
- ) -> "SyncUserSelfRegistrationService":
328
- """Get a SyncUserSelfRegistrationService scoped to a specific company.
325
+ def identity(self, *, company_id: str | None = None) -> "SyncIdentityService":
326
+ """Get a company-scoped SyncIdentityService.
329
327
 
330
- Handles user self-registration via SSO onboarding. Does not require
331
- an acting user only developer API key authentication.
328
+ Resolves verified provider logins (Apple/Google ID tokens,
329
+ Discord/GitHub access tokens) to canonical users. Does not require
330
+ an acting user, only developer API key authentication.
332
331
 
333
332
  Args:
334
- company_id: The ID of the company to register users in. If not
333
+ company_id: The ID of the company to resolve identities in. If not
335
334
  provided, uses the default_company_id from config.
336
335
 
337
336
  Returns:
338
- A SyncUserSelfRegistrationService instance.
337
+ An SyncIdentityService instance.
339
338
 
340
339
  Raises:
341
340
  ValueError: If no company_id provided and no default configured.
342
341
 
343
342
  Example:
344
- >>> registration = client.user_self_registration()
345
- >>> user = await registration.register(username="alice", email="a@b.com")
343
+ >>> identity = client.identity()
344
+ >>> result = await identity.identify(provider="google", token="<id-token>")
346
345
  """
347
- from vclient._sync.services.user_self_registration import SyncUserSelfRegistrationService
346
+ from vclient._sync.services.identity import SyncIdentityService
348
347
 
349
- return SyncUserSelfRegistrationService(self, self._resolve_company_id(company_id))
348
+ return SyncIdentityService(self, self._resolve_company_id(company_id))
350
349
 
351
350
  def campaigns(
352
351
  self, on_behalf_of: str, *, company_id: str | None = None
@@ -36,10 +36,10 @@ if TYPE_CHECKING:
36
36
  SyncDicerollService,
37
37
  SyncDictionaryService,
38
38
  SyncGlobalAdminService,
39
+ SyncIdentityService,
39
40
  SyncOptionsService,
40
41
  SyncSystemService,
41
42
  SyncUserLookupService,
42
- SyncUserSelfRegistrationService,
43
43
  SyncUsersService,
44
44
  )
45
45
  _default_client: "SyncVClient | None" = None
@@ -195,7 +195,7 @@ def sync_user_lookup_service() -> "SyncUserLookupService":
195
195
  """Create a SyncUserLookupService using the default client.
196
196
 
197
197
  Discover which companies a person has a user record in by searching
198
- via email, Discord ID, Google ID, or GitHub ID.
198
+ via email, Discord ID, Google ID, GitHub ID, or Apple ID.
199
199
 
200
200
  Returns:
201
201
  SyncUserLookupService: A service instance for cross-company user lookup.
@@ -242,20 +242,19 @@ def sync_users_service(on_behalf_of: str, *, company_id: str | None = None) -> "
242
242
  return sync_default_client().users(on_behalf_of, company_id=company_id)
243
243
 
244
244
 
245
- def sync_user_self_registration_service(
246
- *, company_id: str | None = None
247
- ) -> "SyncUserSelfRegistrationService":
248
- """Create a SyncUserSelfRegistrationService using the default client.
245
+ def sync_identity_service(*, company_id: str | None = None) -> "SyncIdentityService":
246
+ """Create a company-scoped SyncIdentityService using the default client.
249
247
 
250
- Handles user self-registration via SSO onboarding. Does not require
251
- an acting user only developer API key authentication.
248
+ Resolves verified provider logins (Apple/Google ID tokens, Discord/GitHub
249
+ access tokens) to canonical users. Does not require an acting user, only
250
+ developer API key authentication.
252
251
 
253
252
  Args:
254
- company_id: The ID of the company to register users in. If not
253
+ company_id: The ID of the company to resolve identities in. If not
255
254
  provided, uses the default_company_id from the client config.
256
255
 
257
256
  Returns:
258
- SyncUserSelfRegistrationService: A service instance for user self-registration.
257
+ SyncIdentityService: A service instance for identity resolution.
259
258
 
260
259
  Raises:
261
260
  RuntimeError: If no default client has been configured.
@@ -263,11 +262,11 @@ def sync_user_self_registration_service(
263
262
 
264
263
  Example:
265
264
  ```python
266
- registration = sync_user_self_registration_service()
267
- user = await registration.register(username="alice", email="a@b.com")
265
+ identity = sync_identity_service()
266
+ result = await identity.identify(provider="google", token="<id-token>")
268
267
  ```
269
268
  """
270
- return sync_default_client().user_self_registration(company_id=company_id)
269
+ return sync_default_client().identity(company_id=company_id)
271
270
 
272
271
 
273
272
  def sync_campaigns_service(
@@ -14,10 +14,10 @@ from .developers import SyncDeveloperService
14
14
  from .dicerolls import SyncDicerollService
15
15
  from .dictionary import SyncDictionaryService
16
16
  from .global_admin import SyncGlobalAdminService
17
+ from .identity import SyncIdentityService
17
18
  from .options import SyncOptionsService
18
19
  from .system import SyncSystemService
19
20
  from .user_lookup import SyncUserLookupService
20
- from .user_self_registration import SyncUserSelfRegistrationService
21
21
  from .users import SyncUsersService
22
22
 
23
23
  __all__ = [
@@ -34,9 +34,9 @@ __all__ = [
34
34
  "SyncDicerollService",
35
35
  "SyncDictionaryService",
36
36
  "SyncGlobalAdminService",
37
+ "SyncIdentityService",
37
38
  "SyncOptionsService",
38
39
  "SyncSystemService",
39
40
  "SyncUserLookupService",
40
- "SyncUserSelfRegistrationService",
41
41
  "SyncUsersService",
42
42
  ]
@@ -31,6 +31,7 @@ from vclient.exceptions import (
31
31
  RateLimitError,
32
32
  RequestValidationError,
33
33
  ServerError,
34
+ UnprocessableEntityError,
34
35
  ValidationError,
35
36
  )
36
37
  from vclient.models.pagination import PaginatedResponse
@@ -278,6 +279,7 @@ class SyncBaseService:
278
279
  NotFoundError: For 404 responses.
279
280
  ValidationError: For 400 responses.
280
281
  ConflictError: For 409 responses.
282
+ UnprocessableEntityError: For 422 responses.
281
283
  RateLimitError: For 429 responses.
282
284
  ServerError: For 5xx responses.
283
285
  APIError: For other error responses.
@@ -287,10 +289,12 @@ class SyncBaseService:
287
289
  status_code = response.status_code
288
290
  try:
289
291
  response_data = response.json()
290
- message = response_data.get("detail", response.text)
291
- except (ValueError, KeyError, TypeError):
292
+ except ValueError:
293
+ response_data = {}
294
+ if not isinstance(response_data, dict):
292
295
  response_data = {}
293
- message = response.text or f"HTTP {status_code}"
296
+ detail = response_data.get("detail")
297
+ message = detail if isinstance(detail, str) else response.text or f"HTTP {status_code}"
294
298
  self._inject_request_id_fallback(response_data, response)
295
299
  error_logger = logger.bind(
296
300
  method=method,
@@ -320,6 +324,9 @@ class SyncBaseService:
320
324
  if status_code == 409:
321
325
  error_logger.warning("Return 409 conflict")
322
326
  raise ConflictError(message, status_code, response_data)
327
+ if status_code == 422:
328
+ error_logger.warning("Reject with unprocessable entity error")
329
+ raise UnprocessableEntityError(message, status_code, response_data)
323
330
  if HTTP_500_INTERNAL_SERVER_ERROR <= status_code < HTTP_600_UPPER_BOUND:
324
331
  raise ServerError(message, status_code, response_data)
325
332
  raise APIError(message, status_code, response_data)
@@ -41,7 +41,9 @@ class SyncDeveloperService(SyncBaseService):
41
41
  Args:
42
42
  request: A MeDeveloperUpdate model, OR pass fields as keyword arguments.
43
43
  **kwargs: Fields for MeDeveloperUpdate if request is not provided.
44
- Accepts: username (str | None), email (str | None).
44
+ Accepts: username (str | None), email (str | None),
45
+ provider_audiences (ProviderAudiences | None): per-provider OIDC
46
+ audience allowlists ({"apple": [...], "google": [...]}).
45
47
 
46
48
  Returns:
47
49
  The updated MeDeveloper object.
@@ -188,7 +188,9 @@ class SyncGlobalAdminService(SyncBaseService):
188
188
  request: A DeveloperUpdate model, OR pass fields as keyword arguments.
189
189
  **kwargs: Fields for DeveloperUpdate if request is not provided.
190
190
  Accepts: username (str | None), email (str | None),
191
- is_global_admin (bool | None).
191
+ is_global_admin (bool | None),
192
+ provider_audiences (ProviderAudiences | None): per-provider OIDC
193
+ audience allowlists ({"apple": [...], "google": [...]}).
192
194
 
193
195
  Returns:
194
196
  The updated Developer object.
@@ -0,0 +1,78 @@
1
+ # AUTO-GENERATED — do not edit. Run 'uv run duty generate_sync' to regenerate.
2
+ """Service for verified identity resolution."""
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ from vclient._sync.services.base import SyncBaseService
7
+ from vclient.endpoints import Endpoints
8
+ from vclient.models import IdentityResolution, UserIdentifyDTO
9
+
10
+ if TYPE_CHECKING:
11
+ from vclient._sync.client import SyncVClient
12
+
13
+
14
+ class SyncIdentityService(SyncBaseService):
15
+ """Service for resolving verified provider logins to canonical users.
16
+
17
+ Forward the credential obtained from a provider's own login flow (an OIDC
18
+ ID token for apple/google, an OAuth access token for discord/github). The
19
+ API verifies the credential with the provider and resolves the user in
20
+ order: match by provider ID, auto-link by provider-verified email, or
21
+ create a new UNAPPROVED user.
22
+
23
+ This service does not require an acting user (no On-Behalf-Of header).
24
+ Authentication is via developer API key only.
25
+
26
+ Example:
27
+ >>> async with SyncVClient() as client:
28
+ ... identity = client.identity()
29
+ ... result = await identity.identify(provider="google", token="<id-token>")
30
+ ... print(result.resolution, result.user.id)
31
+ """
32
+
33
+ def __init__(self, client: "SyncVClient", company_id: str) -> None:
34
+ """Initialize the service scoped to a specific company.
35
+
36
+ Args:
37
+ client: The SyncVClient instance to use for requests.
38
+ company_id: The ID of the company to resolve identities in.
39
+ """
40
+ super().__init__(client)
41
+ self._company_id = company_id
42
+
43
+ def _format_endpoint(self, endpoint: str, **kwargs: str) -> str:
44
+ """Format an endpoint with the scoped company_id plus any extra params."""
45
+ return endpoint.format(company_id=self._company_id, **kwargs)
46
+
47
+ def identify(self, request: UserIdentifyDTO | None = None, **kwargs) -> IdentityResolution:
48
+ """Resolve a verified provider login to a canonical user.
49
+
50
+ The returned ``resolution`` field reports which path the API took:
51
+ ``matched`` (an existing user holds this provider identity), ``linked``
52
+ (the identity was auto-linked onto an existing user by provider-verified
53
+ email), or ``created`` (a new UNAPPROVED user was registered).
54
+
55
+ Args:
56
+ request: A UserIdentifyDTO model, OR pass fields as keyword arguments.
57
+ **kwargs: Fields for UserIdentifyDTO if request is not provided.
58
+ Accepts: provider (IdentityProvider, required), token (str,
59
+ required), username (str | None), email (str | None). The
60
+ username and email apply only when a new user is created;
61
+ email is required there only if the provider supplied none.
62
+
63
+ Returns:
64
+ The IdentityResolution with the resolved user.
65
+
66
+ Raises:
67
+ RequestValidationError: If the input parameters fail client-side validation.
68
+ UnprocessableEntityError: If the provider token fails verification
69
+ (code TOKEN_VERIFICATION_FAILED) or creation needs an email the
70
+ provider did not supply (code EMAIL_REQUIRED).
71
+ ServerError: If the provider is unreachable (503, code PROVIDER_UNAVAILABLE).
72
+ """
73
+ body = request if request is not None else self._validate_request(UserIdentifyDTO, **kwargs)
74
+ response = self._post(
75
+ self._format_endpoint(Endpoints.AUTH_IDENTIFY),
76
+ json=body.model_dump(exclude_none=True, exclude_unset=True, mode="json"),
77
+ )
78
+ return IdentityResolution.model_validate(response.json())
@@ -10,7 +10,7 @@ class SyncUserLookupService(SyncBaseService):
10
10
  """Service for looking up users across companies.
11
11
 
12
12
  Discover which companies a person has a user record in by searching
13
- via email, Discord ID, Google ID, or GitHub ID.
13
+ via email, Discord ID, Google ID, GitHub ID, or Apple ID.
14
14
 
15
15
  Example:
16
16
  >>> async with SyncVClient() as client:
@@ -66,3 +66,15 @@ class SyncUserLookupService(SyncBaseService):
66
66
  """
67
67
  response = self._get(Endpoints.USERS_LOOKUP, params={"github_id": github_id})
68
68
  return [UserLookupResult.model_validate(item) for item in response.json()]
69
+
70
+ def by_apple_id(self, apple_id: str) -> list[UserLookupResult]:
71
+ """Look up a user by Apple profile ID.
72
+
73
+ Args:
74
+ apple_id: Apple profile ID to search for.
75
+
76
+ Returns:
77
+ List of companies where a matching user was found. Empty list if no matches.
78
+ """
79
+ response = self._get(Endpoints.USERS_LOOKUP, params={"apple_id": apple_id})
80
+ return [UserLookupResult.model_validate(item) for item in response.json()]
@@ -6,7 +6,7 @@ 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, UserInclude, UserRole
9
+ from vclient.constants import DEFAULT_PAGE_LIMIT, IdentityProvider, 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
  UserDetail,
26
+ UserIdentityLinkDTO,
26
27
  UserMergeDTO,
27
28
  UserUpdate,
28
29
  _ExperienceAddRemove,
@@ -172,6 +173,41 @@ class SyncUsersService(SyncBaseService):
172
173
  )
173
174
  return User.model_validate(response.json())
174
175
 
176
+ def link_identity(self, user_id: str, *, provider: IdentityProvider, token: str) -> User:
177
+ """Attach an additional verified provider identity to a user.
178
+
179
+ Use for "connect your account" settings flows: a user already
180
+ authenticated via one provider presents a credential from a second
181
+ provider, which the API verifies and links to the same user. Only the
182
+ acting user themselves or a company ADMIN may link identities.
183
+ Re-linking the same identity is idempotent and refreshes the stored
184
+ profile.
185
+
186
+ Args:
187
+ user_id: The ID of the user receiving the identity.
188
+ provider: The identity provider that issued the credential.
189
+ token: The provider credential (OIDC ID token for apple/google,
190
+ OAuth access token for discord/github).
191
+
192
+ Returns:
193
+ The updated User object.
194
+
195
+ Raises:
196
+ RequestValidationError: If the input parameters fail client-side validation.
197
+ AuthorizationError: If the acting user is neither the target user nor an admin.
198
+ ConflictError: If the identity belongs to another user, or the user
199
+ already has a different identity from this provider
200
+ (code IDENTITY_ALREADY_LINKED).
201
+ UnprocessableEntityError: If the provider token fails verification
202
+ (code TOKEN_VERIFICATION_FAILED).
203
+ """
204
+ body = self._validate_request(UserIdentityLinkDTO, provider=provider, token=token)
205
+ response = self._post(
206
+ self._format_endpoint(Endpoints.USER_IDENTITIES, user_id=user_id),
207
+ json=body.model_dump(exclude_none=True, exclude_unset=True, mode="json"),
208
+ )
209
+ return User.model_validate(response.json())
210
+
175
211
  def get_page(
176
212
  self,
177
213
  *,
@@ -275,20 +311,22 @@ class SyncUsersService(SyncBaseService):
275
311
  def create(self, request: UserCreate | None = None, **kwargs) -> User:
276
312
  """Create a new user within a company.
277
313
 
278
- The user is automatically added to the company's user list. The Discord profile
279
- is optional and is not used for authentication but is included for Discord bot
280
- integration.
314
+ The user is automatically added to the company's user list.
281
315
 
282
- The initial ``role`` cannot be ``UNAPPROVED`` (use :meth:`register` for SSO
283
- onboarding) or ``DEACTIVATED`` (not a creation path); either will surface as
284
- ``ValidationError``.
316
+ Provider identities (Discord, Google, GitHub, Apple) cannot be set here;
317
+ any provider-profile values passed are silently ignored. Resolve them
318
+ through the verified identity endpoints instead
319
+ (:meth:`SyncIdentityService.identify` or :meth:`SyncUsersService.link_identity`).
320
+
321
+ The initial ``role`` cannot be ``UNAPPROVED`` or ``DEACTIVATED`` (not a
322
+ creation path); either will surface as ``ValidationError``.
285
323
 
286
324
  Args:
287
325
  request: A UserCreate model, OR pass fields as keyword arguments.
288
326
  **kwargs: Fields for UserCreate if request is not provided.
289
327
  Accepts: username (str, required), email (str, required),
290
328
  role (UserRole, required),
291
- discord_profile (DiscordProfile | None).
329
+ name_first (str | None), name_last (str | None).
292
330
 
293
331
  Returns:
294
332
  The newly created User object.
@@ -322,9 +360,14 @@ class SyncUsersService(SyncBaseService):
322
360
  user_id: The ID of the user to update.
323
361
  request: A UserUpdate model, OR pass fields as keyword arguments.
324
362
  **kwargs: Fields for UserUpdate if request is not provided.
325
- Accepts: name (str | None), email (str | None),
363
+ Accepts: username (str | None), email (str | None),
326
364
  role (UserRole | None),
327
- discord_profile (DiscordProfile | None).
365
+ name_first (str | None), name_last (str | None).
366
+
367
+ Provider identities (Discord, Google, GitHub, Apple) cannot be changed
368
+ here; any provider-profile values passed are silently ignored. Resolve
369
+ them through the verified identity endpoints instead
370
+ (:meth:`SyncIdentityService.identify` or :meth:`SyncUsersService.link_identity`).
328
371
 
329
372
  Returns:
330
373
  The updated User object.
@@ -164,6 +164,7 @@ class SyncFakeVClient(SyncVClient):
164
164
  *,
165
165
  status_code: int,
166
166
  detail: str | None = None,
167
+ code: str | None = None,
167
168
  params: dict[str, str] | None = None,
168
169
  ) -> None:
169
170
  """Override a route to return an error response.
@@ -172,6 +173,9 @@ class SyncFakeVClient(SyncVClient):
172
173
  route: A ``RouteSpec`` from the ``Routes`` class identifying the endpoint.
173
174
  status_code: The HTTP status code to return.
174
175
  detail: Optional error detail message. Defaults to ``"Error {status_code}"``.
176
+ code: Optional machine-readable error code, an RFC 9457 extension
177
+ member exposed as ``APIError.code`` (e.g.
178
+ ``"TOKEN_VERIFICATION_FAILED"``). Omitted from the body when not set.
175
179
  params: Optional path parameter values to match against. When set,
176
180
  the error override only applies to requests whose URL path segments
177
181
  match all specified values.
@@ -182,4 +186,6 @@ class SyncFakeVClient(SyncVClient):
182
186
  "detail": detail or f"Error {status_code}",
183
187
  "request_id": request_id,
184
188
  }
189
+ if code is not None:
190
+ body["code"] = code
185
191
  self._router.add_route(method, pattern, json=body, status_code=status_code, params=params)