arize-phoenix 11.23.1__py3-none-any.whl → 12.28.1__py3-none-any.whl

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 (221) hide show
  1. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +61 -36
  2. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/RECORD +212 -162
  3. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
  4. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/IP_NOTICE +1 -1
  5. phoenix/__generated__/__init__.py +0 -0
  6. phoenix/__generated__/classification_evaluator_configs/__init__.py +20 -0
  7. phoenix/__generated__/classification_evaluator_configs/_document_relevance_classification_evaluator_config.py +17 -0
  8. phoenix/__generated__/classification_evaluator_configs/_hallucination_classification_evaluator_config.py +17 -0
  9. phoenix/__generated__/classification_evaluator_configs/_models.py +18 -0
  10. phoenix/__generated__/classification_evaluator_configs/_tool_selection_classification_evaluator_config.py +17 -0
  11. phoenix/__init__.py +2 -1
  12. phoenix/auth.py +27 -2
  13. phoenix/config.py +1594 -81
  14. phoenix/db/README.md +546 -28
  15. phoenix/db/bulk_inserter.py +119 -116
  16. phoenix/db/engines.py +140 -33
  17. phoenix/db/facilitator.py +22 -1
  18. phoenix/db/helpers.py +818 -65
  19. phoenix/db/iam_auth.py +64 -0
  20. phoenix/db/insertion/dataset.py +133 -1
  21. phoenix/db/insertion/document_annotation.py +9 -6
  22. phoenix/db/insertion/evaluation.py +2 -3
  23. phoenix/db/insertion/helpers.py +2 -2
  24. phoenix/db/insertion/session_annotation.py +176 -0
  25. phoenix/db/insertion/span_annotation.py +3 -4
  26. phoenix/db/insertion/trace_annotation.py +3 -4
  27. phoenix/db/insertion/types.py +41 -18
  28. phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
  29. phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
  30. phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
  31. phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
  32. phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
  33. phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
  34. phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
  35. phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
  36. phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
  37. phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
  38. phoenix/db/models.py +364 -56
  39. phoenix/db/pg_config.py +10 -0
  40. phoenix/db/types/trace_retention.py +7 -6
  41. phoenix/experiments/functions.py +69 -19
  42. phoenix/inferences/inferences.py +1 -2
  43. phoenix/server/api/auth.py +9 -0
  44. phoenix/server/api/auth_messages.py +46 -0
  45. phoenix/server/api/context.py +60 -0
  46. phoenix/server/api/dataloaders/__init__.py +36 -0
  47. phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
  48. phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
  49. phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
  50. phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
  51. phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
  52. phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
  53. phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
  54. phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
  55. phoenix/server/api/dataloaders/dataset_labels.py +36 -0
  56. phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
  57. phoenix/server/api/dataloaders/document_evaluations.py +6 -9
  58. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
  59. phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
  60. phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
  61. phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
  62. phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
  63. phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
  64. phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
  65. phoenix/server/api/dataloaders/record_counts.py +37 -10
  66. phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
  67. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
  68. phoenix/server/api/dataloaders/span_cost_summary_by_project.py +28 -14
  69. phoenix/server/api/dataloaders/span_costs.py +3 -9
  70. phoenix/server/api/dataloaders/table_fields.py +2 -2
  71. phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
  72. phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
  73. phoenix/server/api/exceptions.py +5 -1
  74. phoenix/server/api/helpers/playground_clients.py +263 -83
  75. phoenix/server/api/helpers/playground_spans.py +2 -1
  76. phoenix/server/api/helpers/playground_users.py +26 -0
  77. phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
  78. phoenix/server/api/helpers/prompts/models.py +61 -19
  79. phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
  80. phoenix/server/api/input_types/ChatCompletionInput.py +3 -0
  81. phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
  82. phoenix/server/api/input_types/DatasetFilter.py +5 -2
  83. phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
  84. phoenix/server/api/input_types/GenerativeModelInput.py +3 -0
  85. phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
  86. phoenix/server/api/input_types/PromptVersionInput.py +47 -1
  87. phoenix/server/api/input_types/SpanSort.py +3 -2
  88. phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
  89. phoenix/server/api/input_types/UserRoleInput.py +1 -0
  90. phoenix/server/api/mutations/__init__.py +8 -0
  91. phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
  92. phoenix/server/api/mutations/api_key_mutations.py +15 -20
  93. phoenix/server/api/mutations/chat_mutations.py +106 -37
  94. phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
  95. phoenix/server/api/mutations/dataset_mutations.py +21 -16
  96. phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
  97. phoenix/server/api/mutations/experiment_mutations.py +2 -2
  98. phoenix/server/api/mutations/export_events_mutations.py +3 -3
  99. phoenix/server/api/mutations/model_mutations.py +11 -9
  100. phoenix/server/api/mutations/project_mutations.py +4 -4
  101. phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
  102. phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
  103. phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
  104. phoenix/server/api/mutations/prompt_mutations.py +65 -129
  105. phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
  106. phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
  107. phoenix/server/api/mutations/trace_annotations_mutations.py +13 -8
  108. phoenix/server/api/mutations/trace_mutations.py +3 -3
  109. phoenix/server/api/mutations/user_mutations.py +55 -26
  110. phoenix/server/api/queries.py +501 -617
  111. phoenix/server/api/routers/__init__.py +2 -2
  112. phoenix/server/api/routers/auth.py +141 -87
  113. phoenix/server/api/routers/ldap.py +229 -0
  114. phoenix/server/api/routers/oauth2.py +349 -101
  115. phoenix/server/api/routers/v1/__init__.py +22 -4
  116. phoenix/server/api/routers/v1/annotation_configs.py +19 -30
  117. phoenix/server/api/routers/v1/annotations.py +455 -13
  118. phoenix/server/api/routers/v1/datasets.py +355 -68
  119. phoenix/server/api/routers/v1/documents.py +142 -0
  120. phoenix/server/api/routers/v1/evaluations.py +20 -28
  121. phoenix/server/api/routers/v1/experiment_evaluations.py +16 -6
  122. phoenix/server/api/routers/v1/experiment_runs.py +335 -59
  123. phoenix/server/api/routers/v1/experiments.py +475 -47
  124. phoenix/server/api/routers/v1/projects.py +16 -50
  125. phoenix/server/api/routers/v1/prompts.py +50 -39
  126. phoenix/server/api/routers/v1/sessions.py +108 -0
  127. phoenix/server/api/routers/v1/spans.py +156 -96
  128. phoenix/server/api/routers/v1/traces.py +51 -77
  129. phoenix/server/api/routers/v1/users.py +64 -24
  130. phoenix/server/api/routers/v1/utils.py +3 -7
  131. phoenix/server/api/subscriptions.py +257 -93
  132. phoenix/server/api/types/Annotation.py +90 -23
  133. phoenix/server/api/types/ApiKey.py +13 -17
  134. phoenix/server/api/types/AuthMethod.py +1 -0
  135. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
  136. phoenix/server/api/types/Dataset.py +199 -72
  137. phoenix/server/api/types/DatasetExample.py +88 -18
  138. phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
  139. phoenix/server/api/types/DatasetLabel.py +57 -0
  140. phoenix/server/api/types/DatasetSplit.py +98 -0
  141. phoenix/server/api/types/DatasetVersion.py +49 -4
  142. phoenix/server/api/types/DocumentAnnotation.py +212 -0
  143. phoenix/server/api/types/Experiment.py +215 -68
  144. phoenix/server/api/types/ExperimentComparison.py +3 -9
  145. phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
  146. phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
  147. phoenix/server/api/types/ExperimentRun.py +120 -70
  148. phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
  149. phoenix/server/api/types/GenerativeModel.py +95 -42
  150. phoenix/server/api/types/GenerativeProvider.py +1 -1
  151. phoenix/server/api/types/ModelInterface.py +7 -2
  152. phoenix/server/api/types/PlaygroundModel.py +12 -2
  153. phoenix/server/api/types/Project.py +218 -185
  154. phoenix/server/api/types/ProjectSession.py +146 -29
  155. phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
  156. phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
  157. phoenix/server/api/types/Prompt.py +119 -39
  158. phoenix/server/api/types/PromptLabel.py +42 -25
  159. phoenix/server/api/types/PromptVersion.py +11 -8
  160. phoenix/server/api/types/PromptVersionTag.py +65 -25
  161. phoenix/server/api/types/Span.py +130 -123
  162. phoenix/server/api/types/SpanAnnotation.py +189 -42
  163. phoenix/server/api/types/SystemApiKey.py +65 -1
  164. phoenix/server/api/types/Trace.py +184 -53
  165. phoenix/server/api/types/TraceAnnotation.py +149 -50
  166. phoenix/server/api/types/User.py +128 -33
  167. phoenix/server/api/types/UserApiKey.py +73 -26
  168. phoenix/server/api/types/node.py +10 -0
  169. phoenix/server/api/types/pagination.py +11 -2
  170. phoenix/server/app.py +154 -36
  171. phoenix/server/authorization.py +5 -4
  172. phoenix/server/bearer_auth.py +13 -5
  173. phoenix/server/cost_tracking/cost_model_lookup.py +42 -14
  174. phoenix/server/cost_tracking/model_cost_manifest.json +1085 -194
  175. phoenix/server/daemons/generative_model_store.py +61 -9
  176. phoenix/server/daemons/span_cost_calculator.py +10 -8
  177. phoenix/server/dml_event.py +13 -0
  178. phoenix/server/email/sender.py +29 -2
  179. phoenix/server/grpc_server.py +9 -9
  180. phoenix/server/jwt_store.py +8 -6
  181. phoenix/server/ldap.py +1449 -0
  182. phoenix/server/main.py +9 -3
  183. phoenix/server/oauth2.py +330 -12
  184. phoenix/server/prometheus.py +43 -6
  185. phoenix/server/rate_limiters.py +4 -9
  186. phoenix/server/retention.py +33 -20
  187. phoenix/server/session_filters.py +49 -0
  188. phoenix/server/static/.vite/manifest.json +51 -53
  189. phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
  190. phoenix/server/static/assets/{index-BPCwGQr8.js → index-CTQoemZv.js} +42 -35
  191. phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
  192. phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
  193. phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
  194. phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
  195. phoenix/server/static/assets/{vendor-recharts-Bw30oz1A.js → vendor-recharts-V9cwpXsm.js} +7 -7
  196. phoenix/server/static/assets/{vendor-shiki-DZajAPeq.js → vendor-shiki-Do--csgv.js} +1 -1
  197. phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
  198. phoenix/server/templates/index.html +7 -1
  199. phoenix/server/thread_server.py +1 -2
  200. phoenix/server/utils.py +74 -0
  201. phoenix/session/client.py +55 -1
  202. phoenix/session/data_extractor.py +5 -0
  203. phoenix/session/evaluation.py +8 -4
  204. phoenix/session/session.py +44 -8
  205. phoenix/settings.py +2 -0
  206. phoenix/trace/attributes.py +80 -13
  207. phoenix/trace/dsl/query.py +2 -0
  208. phoenix/trace/projects.py +5 -0
  209. phoenix/utilities/template_formatters.py +1 -1
  210. phoenix/version.py +1 -1
  211. phoenix/server/api/types/Evaluation.py +0 -39
  212. phoenix/server/static/assets/components-D0DWAf0l.js +0 -5650
  213. phoenix/server/static/assets/pages-Creyamao.js +0 -8612
  214. phoenix/server/static/assets/vendor-CU36oj8y.js +0 -905
  215. phoenix/server/static/assets/vendor-CqDb5u4o.css +0 -1
  216. phoenix/server/static/assets/vendor-arizeai-Ctgw0e1G.js +0 -168
  217. phoenix/server/static/assets/vendor-codemirror-Cojjzqb9.js +0 -25
  218. phoenix/server/static/assets/vendor-three-BLWp5bic.js +0 -2998
  219. phoenix/utilities/deprecation.py +0 -31
  220. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
  221. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
@@ -27,13 +27,13 @@ from phoenix.auth import (
27
27
  )
28
28
  from phoenix.config import get_env_disable_basic_auth
29
29
  from phoenix.db import models
30
- from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly
30
+ from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly, IsNotViewer
31
31
  from phoenix.server.api.context import Context
32
32
  from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound, Unauthorized
33
33
  from phoenix.server.api.input_types.UserRoleInput import UserRoleInput
34
34
  from phoenix.server.api.types.AuthMethod import AuthMethod
35
35
  from phoenix.server.api.types.node import from_global_id_with_expected_type
36
- from phoenix.server.api.types.User import User, to_gql_user
36
+ from phoenix.server.api.types.User import User
37
37
  from phoenix.server.bearer_auth import PhoenixUser
38
38
  from phoenix.server.types import AccessTokenId, ApiKeyId, PasswordResetTokenId, RefreshTokenId
39
39
 
@@ -50,7 +50,10 @@ class CreateUserInput:
50
50
  auth_method: Optional[AuthMethod] = AuthMethod.LOCAL
51
51
 
52
52
  def __post_init__(self) -> None:
53
- if self.auth_method is AuthMethod.OAUTH2:
53
+ if self.auth_method is AuthMethod.LDAP:
54
+ if self.password:
55
+ raise BadRequest("Password is not allowed for LDAP authentication")
56
+ elif self.auth_method is AuthMethod.OAUTH2:
54
57
  if self.password:
55
58
  raise BadRequest("Password is not allowed for OAuth2 authentication")
56
59
  elif get_env_disable_basic_auth():
@@ -103,6 +106,11 @@ class DeleteUsersInput:
103
106
  user_ids: list[GlobalID]
104
107
 
105
108
 
109
+ @strawberry.type
110
+ class DeleteUsersPayload:
111
+ user_ids: list[GlobalID]
112
+
113
+
106
114
  @strawberry.type
107
115
  class UserMutationPayload:
108
116
  user: User
@@ -110,7 +118,7 @@ class UserMutationPayload:
110
118
 
111
119
  @strawberry.type
112
120
  class UserMutationMixin:
113
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
121
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin, IsLocked]) # type: ignore
114
122
  async def create_user(
115
123
  self,
116
124
  info: Info[Context, None],
@@ -120,7 +128,12 @@ class UserMutationMixin:
120
128
  email = sanitize_email(input.email)
121
129
 
122
130
  user: models.User
123
- if input.auth_method is AuthMethod.OAUTH2:
131
+ if input.auth_method is AuthMethod.LDAP:
132
+ user = models.LDAPUser(
133
+ email=email,
134
+ username=input.username,
135
+ )
136
+ elif input.auth_method is AuthMethod.OAUTH2:
124
137
  user = models.OAuth2User(
125
138
  email=email,
126
139
  username=input.username,
@@ -155,9 +168,9 @@ class UserMutationMixin:
155
168
  except Exception as error:
156
169
  # Log the error but do not raise it
157
170
  logger.error(f"Failed to send welcome email: {error}")
158
- return UserMutationPayload(user=to_gql_user(user))
171
+ return UserMutationPayload(user=User(id=user.id, db_record=user))
159
172
 
160
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin]) # type: ignore
173
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin]) # type: ignore
161
174
  async def patch_user(
162
175
  self,
163
176
  info: Info[Context, None],
@@ -174,6 +187,7 @@ class UserMutationMixin:
174
187
  if not (user := await session.scalar(_select_user_by_id(user_id))):
175
188
  raise NotFound("User not found")
176
189
  stack.enter_context(session.no_autoflush)
190
+ should_log_out = False
177
191
  if input.new_role:
178
192
  if user.email == DEFAULT_ADMIN_EMAIL:
179
193
  raise Unauthorized("Cannot modify role for the default admin user")
@@ -183,6 +197,7 @@ class UserMutationMixin:
183
197
  if user_role_id is None:
184
198
  raise NotFound(f"Role {input.new_role.value} not found")
185
199
  user.user_role_id = user_role_id
200
+ should_log_out = True
186
201
  if password := input.new_password:
187
202
  if user.auth_method != "LOCAL":
188
203
  raise Conflict("Cannot modify password for non-local user")
@@ -191,6 +206,7 @@ class UserMutationMixin:
191
206
  user.password_salt = salt
192
207
  user.password_hash = await info.context.hash_password(Secret(password), salt)
193
208
  user.reset_password = True
209
+ should_log_out = True
194
210
  if username := input.new_username:
195
211
  user.username = username
196
212
  assert user in session.dirty
@@ -199,9 +215,9 @@ class UserMutationMixin:
199
215
  except (PostgreSQLIntegrityError, SQLiteIntegrityError) as error:
200
216
  raise Conflict(_user_operation_error_message(error, "modify"))
201
217
  assert user
202
- if input.new_password:
218
+ if should_log_out:
203
219
  await info.context.log_out(user.id)
204
- return UserMutationPayload(user=to_gql_user(user))
220
+ return UserMutationPayload(user=User(id=user.id, db_record=user))
205
221
 
206
222
  @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
207
223
  async def patch_viewer(
@@ -243,23 +259,26 @@ class UserMutationMixin:
243
259
  response = info.context.get_response()
244
260
  response.delete_cookie(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)
245
261
  response.delete_cookie(PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
246
- return UserMutationPayload(user=to_gql_user(user))
262
+ return UserMutationPayload(user=User(id=user.id, db_record=user))
247
263
 
248
- @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
264
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin, IsLocked]) # type: ignore
249
265
  async def delete_users(
250
266
  self,
251
267
  info: Info[Context, None],
252
268
  input: DeleteUsersInput,
253
- ) -> None:
269
+ ) -> DeleteUsersPayload:
254
270
  assert (token_store := info.context.token_store) is not None
255
271
  if not input.user_ids:
256
- return
257
- user_ids = tuple(
258
- map(
259
- lambda gid: from_global_id_with_expected_type(gid, User.__name__),
260
- set(input.user_ids),
261
- )
262
- )
272
+ raise BadRequest("At least one user ID is required")
273
+ user_rowid_to_gid: dict[int, GlobalID] = {}
274
+ for user_gid in input.user_ids:
275
+ try:
276
+ user_rowid = from_global_id_with_expected_type(user_gid, User.__name__)
277
+ except ValueError:
278
+ raise BadRequest(f"Invalid user ID: '{user_gid}'")
279
+ user_rowid_to_gid[user_rowid] = user_gid
280
+
281
+ user_rowids = list(user_rowid_to_gid.keys())
263
282
  system_user_role_id = select(models.UserRole.id).filter_by(name="SYSTEM").scalar_subquery()
264
283
  admin_user_role_id = select(models.UserRole.id).filter_by(name="ADMIN").scalar_subquery()
265
284
  default_admin_user_id = (
@@ -298,7 +317,7 @@ class UserMutationMixin:
298
317
  .select_from(models.User)
299
318
  .where(
300
319
  and_(
301
- models.User.id.in_(user_ids),
320
+ models.User.id.in_(user_rowids),
302
321
  models.User.user_role_id != system_user_role_id,
303
322
  )
304
323
  )
@@ -306,41 +325,51 @@ class UserMutationMixin:
306
325
  ).all()
307
326
  if deletes_default_admin:
308
327
  raise Conflict("Cannot delete the default admin user")
309
- if num_resolved_user_ids < len(user_ids):
328
+ if num_resolved_user_ids < len(user_rowids):
310
329
  raise NotFound("Some user IDs could not be found")
311
330
  password_reset_token_ids = [
312
331
  PasswordResetTokenId(id_)
313
332
  async for id_ in await session.stream_scalars(
314
333
  select(models.PasswordResetToken.id).where(
315
- models.PasswordResetToken.user_id.in_(user_ids)
334
+ models.PasswordResetToken.user_id.in_(user_rowids)
316
335
  )
317
336
  )
318
337
  ]
319
338
  access_token_ids = [
320
339
  AccessTokenId(id_)
321
340
  async for id_ in await session.stream_scalars(
322
- select(models.AccessToken.id).where(models.AccessToken.user_id.in_(user_ids))
341
+ select(models.AccessToken.id).where(models.AccessToken.user_id.in_(user_rowids))
323
342
  )
324
343
  ]
325
344
  refresh_token_ids = [
326
345
  RefreshTokenId(id_)
327
346
  async for id_ in await session.stream_scalars(
328
- select(models.RefreshToken.id).where(models.RefreshToken.user_id.in_(user_ids))
347
+ select(models.RefreshToken.id).where(
348
+ models.RefreshToken.user_id.in_(user_rowids)
349
+ )
329
350
  )
330
351
  ]
331
352
  api_key_ids = [
332
353
  ApiKeyId(id_)
333
354
  async for id_ in await session.stream_scalars(
334
- select(models.ApiKey.id).where(models.ApiKey.user_id.in_(user_ids))
355
+ select(models.ApiKey.id).where(models.ApiKey.user_id.in_(user_rowids))
335
356
  )
336
357
  ]
337
- await session.execute(delete(models.User).where(models.User.id.in_(user_ids)))
358
+ deleted_user_ids = await session.scalars(
359
+ delete(models.User).where(models.User.id.in_(user_rowids)).returning(models.User.id)
360
+ )
338
361
  await token_store.revoke(
339
362
  *password_reset_token_ids,
340
363
  *access_token_ids,
341
364
  *refresh_token_ids,
342
365
  *api_key_ids,
343
366
  )
367
+ unique_deleted_user_ids = set(deleted_user_ids)
368
+ deleted_user_gids: list[GlobalID] = []
369
+ for user_rowid, user_gid in user_rowid_to_gid.items():
370
+ if user_rowid in unique_deleted_user_ids:
371
+ deleted_user_gids.append(user_gid)
372
+ return DeleteUsersPayload(user_ids=deleted_user_gids)
344
373
 
345
374
 
346
375
  def _select_role_id_by_name(role_name: str) -> Select[tuple[int]]: