arize-phoenix 12.7.1__py3-none-any.whl → 12.9.0__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.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (76) hide show
  1. {arize_phoenix-12.7.1.dist-info → arize_phoenix-12.9.0.dist-info}/METADATA +3 -1
  2. {arize_phoenix-12.7.1.dist-info → arize_phoenix-12.9.0.dist-info}/RECORD +76 -73
  3. phoenix/config.py +131 -9
  4. phoenix/db/engines.py +127 -14
  5. phoenix/db/iam_auth.py +64 -0
  6. phoenix/db/pg_config.py +10 -0
  7. phoenix/server/api/context.py +23 -0
  8. phoenix/server/api/dataloaders/__init__.py +6 -0
  9. phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +0 -2
  10. phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
  11. phoenix/server/api/dataloaders/span_costs.py +3 -9
  12. phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
  13. phoenix/server/api/helpers/playground_clients.py +3 -3
  14. phoenix/server/api/input_types/PromptVersionInput.py +47 -1
  15. phoenix/server/api/mutations/annotation_config_mutations.py +2 -2
  16. phoenix/server/api/mutations/api_key_mutations.py +2 -15
  17. phoenix/server/api/mutations/chat_mutations.py +3 -2
  18. phoenix/server/api/mutations/dataset_label_mutations.py +109 -157
  19. phoenix/server/api/mutations/dataset_mutations.py +8 -8
  20. phoenix/server/api/mutations/dataset_split_mutations.py +13 -9
  21. phoenix/server/api/mutations/model_mutations.py +4 -4
  22. phoenix/server/api/mutations/project_session_annotations_mutations.py +4 -7
  23. phoenix/server/api/mutations/prompt_label_mutations.py +3 -3
  24. phoenix/server/api/mutations/prompt_mutations.py +24 -117
  25. phoenix/server/api/mutations/prompt_version_tag_mutations.py +8 -5
  26. phoenix/server/api/mutations/span_annotations_mutations.py +10 -5
  27. phoenix/server/api/mutations/trace_annotations_mutations.py +9 -4
  28. phoenix/server/api/mutations/user_mutations.py +4 -4
  29. phoenix/server/api/queries.py +80 -213
  30. phoenix/server/api/subscriptions.py +4 -4
  31. phoenix/server/api/types/Annotation.py +90 -23
  32. phoenix/server/api/types/ApiKey.py +13 -17
  33. phoenix/server/api/types/Dataset.py +88 -48
  34. phoenix/server/api/types/DatasetExample.py +34 -30
  35. phoenix/server/api/types/DatasetLabel.py +47 -13
  36. phoenix/server/api/types/DatasetSplit.py +87 -21
  37. phoenix/server/api/types/DatasetVersion.py +49 -4
  38. phoenix/server/api/types/DocumentAnnotation.py +182 -62
  39. phoenix/server/api/types/Experiment.py +146 -55
  40. phoenix/server/api/types/ExperimentRepeatedRunGroup.py +10 -1
  41. phoenix/server/api/types/ExperimentRun.py +118 -61
  42. phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
  43. phoenix/server/api/types/GenerativeModel.py +95 -42
  44. phoenix/server/api/types/ModelInterface.py +7 -2
  45. phoenix/server/api/types/PlaygroundModel.py +12 -2
  46. phoenix/server/api/types/Project.py +70 -75
  47. phoenix/server/api/types/ProjectSession.py +69 -37
  48. phoenix/server/api/types/ProjectSessionAnnotation.py +166 -47
  49. phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
  50. phoenix/server/api/types/Prompt.py +82 -44
  51. phoenix/server/api/types/PromptLabel.py +47 -13
  52. phoenix/server/api/types/PromptVersion.py +11 -8
  53. phoenix/server/api/types/PromptVersionTag.py +65 -25
  54. phoenix/server/api/types/Span.py +116 -115
  55. phoenix/server/api/types/SpanAnnotation.py +189 -42
  56. phoenix/server/api/types/SystemApiKey.py +65 -1
  57. phoenix/server/api/types/Trace.py +45 -44
  58. phoenix/server/api/types/TraceAnnotation.py +144 -48
  59. phoenix/server/api/types/User.py +103 -33
  60. phoenix/server/api/types/UserApiKey.py +73 -26
  61. phoenix/server/app.py +29 -0
  62. phoenix/server/cost_tracking/model_cost_manifest.json +2 -2
  63. phoenix/server/static/.vite/manifest.json +43 -43
  64. phoenix/server/static/assets/{components-BLK5vehh.js → components-v927s3NF.js} +471 -484
  65. phoenix/server/static/assets/{index-BP0Shd90.js → index-DrD9eSrN.js} +20 -16
  66. phoenix/server/static/assets/{pages-DIVgyYyy.js → pages-GVybXa_W.js} +754 -753
  67. phoenix/server/static/assets/{vendor-3BvTzoBp.js → vendor-D-csRHGZ.js} +1 -1
  68. phoenix/server/static/assets/{vendor-arizeai-C6_oC0y8.js → vendor-arizeai-BJLCG_Gc.js} +1 -1
  69. phoenix/server/static/assets/{vendor-codemirror-DPnZGAZA.js → vendor-codemirror-Cr963DyP.js} +3 -3
  70. phoenix/server/static/assets/{vendor-recharts-CjgSbsB0.js → vendor-recharts-DgmPLgIp.js} +1 -1
  71. phoenix/server/static/assets/{vendor-shiki-CJyhDG0E.js → vendor-shiki-wYOt1s7u.js} +1 -1
  72. phoenix/version.py +1 -1
  73. {arize_phoenix-12.7.1.dist-info → arize_phoenix-12.9.0.dist-info}/WHEEL +0 -0
  74. {arize_phoenix-12.7.1.dist-info → arize_phoenix-12.9.0.dist-info}/entry_points.txt +0 -0
  75. {arize_phoenix-12.7.1.dist-info → arize_phoenix-12.9.0.dist-info}/licenses/IP_NOTICE +0 -0
  76. {arize_phoenix-12.7.1.dist-info → arize_phoenix-12.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional, Union, cast
1
+ from typing import Optional
2
2
 
3
3
  import strawberry
4
4
  from fastapi import Request
@@ -12,18 +12,11 @@ from strawberry.types import Info
12
12
 
13
13
  from phoenix.db import models
14
14
  from phoenix.db.types.identifier import Identifier as IdentifierModel
15
- from phoenix.db.types.model_provider import ModelProvider
16
15
  from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
17
16
  from phoenix.server.api.context import Context
18
17
  from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
19
- from phoenix.server.api.helpers.prompts.models import (
20
- normalize_response_format,
21
- normalize_tools,
22
- validate_invocation_parameters,
23
- )
24
18
  from phoenix.server.api.input_types.PromptVersionInput import (
25
19
  ChatPromptVersionInput,
26
- to_pydantic_prompt_chat_template_v1,
27
20
  )
28
21
  from phoenix.server.api.mutations.prompt_version_tag_mutations import (
29
22
  SetPromptVersionTagInput,
@@ -32,7 +25,7 @@ from phoenix.server.api.mutations.prompt_version_tag_mutations import (
32
25
  from phoenix.server.api.queries import Query
33
26
  from phoenix.server.api.types.Identifier import Identifier
34
27
  from phoenix.server.api.types.node import from_global_id_with_expected_type
35
- from phoenix.server.api.types.Prompt import Prompt, to_gql_prompt_from_orm
28
+ from phoenix.server.api.types.Prompt import Prompt
36
29
  from phoenix.server.bearer_auth import PhoenixUser
37
30
 
38
31
 
@@ -84,63 +77,23 @@ class PromptMutationMixin:
84
77
  if "user" in request.scope:
85
78
  assert isinstance(user := request.user, PhoenixUser)
86
79
  user_id = int(user.identity)
87
-
88
- input_prompt_version = input.prompt_version
89
- tool_definitions = [tool.definition for tool in input_prompt_version.tools]
90
- tool_choice = cast(
91
- Optional[Union[str, dict[str, Any]]],
92
- cast(dict[str, Any], input.prompt_version.invocation_parameters).pop(
93
- "tool_choice", None
94
- ),
95
- )
96
- model_provider = ModelProvider(input_prompt_version.model_provider)
97
80
  try:
98
- tools = (
99
- normalize_tools(tool_definitions, model_provider, tool_choice)
100
- if tool_definitions
101
- else None
102
- )
103
- template = to_pydantic_prompt_chat_template_v1(input_prompt_version.template)
104
- response_format = (
105
- normalize_response_format(
106
- input_prompt_version.response_format.definition,
107
- model_provider,
108
- )
109
- if input_prompt_version.response_format
110
- else None
111
- )
112
- invocation_parameters = validate_invocation_parameters(
113
- input_prompt_version.invocation_parameters,
114
- model_provider,
115
- )
81
+ prompt_version = input.prompt_version.to_orm_prompt_version(user_id)
116
82
  except ValidationError as error:
117
83
  raise BadRequest(str(error))
118
-
84
+ name = IdentifierModel.model_validate(str(input.name))
85
+ prompt = models.Prompt(
86
+ name=name,
87
+ description=input.description,
88
+ prompt_versions=[prompt_version],
89
+ )
119
90
  async with info.context.db() as session:
120
- prompt_version = models.PromptVersion(
121
- description=input_prompt_version.description,
122
- user_id=user_id,
123
- template_type="CHAT",
124
- template_format=input_prompt_version.template_format,
125
- template=template,
126
- invocation_parameters=invocation_parameters,
127
- tools=tools,
128
- response_format=response_format,
129
- model_provider=input_prompt_version.model_provider,
130
- model_name=input_prompt_version.model_name,
131
- )
132
- name = IdentifierModel.model_validate(str(input.name))
133
- prompt = models.Prompt(
134
- name=name,
135
- description=input.description,
136
- prompt_versions=[prompt_version],
137
- )
138
91
  session.add(prompt)
139
92
  try:
140
93
  await session.commit()
141
94
  except (PostgreSQLIntegrityError, SQLiteIntegrityError):
142
95
  raise Conflict(f"A prompt named '{input.name}' already exists")
143
- return to_gql_prompt_from_orm(prompt)
96
+ return Prompt(id=prompt.id, db_record=prompt)
144
97
 
145
98
  @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
146
99
  async def create_chat_prompt_version(
@@ -153,72 +106,26 @@ class PromptMutationMixin:
153
106
  if "user" in request.scope:
154
107
  assert isinstance(user := request.user, PhoenixUser)
155
108
  user_id = int(user.identity)
156
-
157
- input_prompt_version = input.prompt_version
158
- tool_definitions = [tool.definition for tool in input.prompt_version.tools]
159
- tool_choice = cast(
160
- Optional[Union[str, dict[str, Any]]],
161
- cast(dict[str, Any], input.prompt_version.invocation_parameters).pop(
162
- "tool_choice", None
163
- ),
164
- )
165
- model_provider = ModelProvider(input_prompt_version.model_provider)
166
109
  try:
167
- tools = (
168
- normalize_tools(tool_definitions, model_provider, tool_choice)
169
- if tool_definitions
170
- else None
171
- )
172
- template = to_pydantic_prompt_chat_template_v1(input_prompt_version.template)
173
- response_format = (
174
- normalize_response_format(
175
- input_prompt_version.response_format.definition,
176
- model_provider,
177
- )
178
- if input_prompt_version.response_format
179
- else None
180
- )
181
- invocation_parameters = validate_invocation_parameters(
182
- input_prompt_version.invocation_parameters,
183
- model_provider,
184
- )
110
+ prompt_version = input.prompt_version.to_orm_prompt_version(user_id)
185
111
  except ValidationError as error:
186
112
  raise BadRequest(str(error))
187
-
188
113
  prompt_id = from_global_id_with_expected_type(
189
114
  global_id=input.prompt_id, expected_type_name=Prompt.__name__
190
115
  )
116
+ prompt_version.prompt_id = prompt_id
191
117
  async with info.context.db() as session:
192
- prompt = await session.get(models.Prompt, prompt_id)
193
- if not prompt:
194
- raise NotFound(f"Prompt with ID '{input.prompt_id}' not found")
195
-
196
- prompt_version = models.PromptVersion(
197
- prompt_id=prompt_id,
198
- description=input.prompt_version.description,
199
- user_id=user_id,
200
- template_type="CHAT",
201
- template_format=input.prompt_version.template_format,
202
- template=template,
203
- invocation_parameters=invocation_parameters,
204
- tools=tools,
205
- response_format=response_format,
206
- model_provider=input.prompt_version.model_provider,
207
- model_name=input.prompt_version.model_name,
208
- )
209
118
  session.add(prompt_version)
210
-
211
- # ensure prompt_version is flushed to the database before creating tags against the
212
- # prompt_version id
213
- await session.flush()
214
-
215
- if input.tags:
216
- for tag in input.tags:
217
- await upsert_prompt_version_tag(
218
- session, prompt_id, prompt_version.id, tag.name, tag.description
219
- )
220
-
221
- return to_gql_prompt_from_orm(prompt)
119
+ try:
120
+ await session.flush()
121
+ except (PostgreSQLIntegrityError, SQLiteIntegrityError):
122
+ raise NotFound(f"Prompt with ID '{input.prompt_id}' not found")
123
+ if input.tags:
124
+ for tag in input.tags:
125
+ await upsert_prompt_version_tag(
126
+ session, prompt_id, prompt_version.id, tag.name, tag.description
127
+ )
128
+ return Prompt(id=prompt_id)
222
129
 
223
130
  @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
224
131
  async def delete_prompt(
@@ -288,7 +195,7 @@ class PromptMutationMixin:
288
195
  await session.commit()
289
196
  except (PostgreSQLIntegrityError, SQLiteIntegrityError):
290
197
  raise Conflict(f"A prompt named '{input.name}' already exists")
291
- return to_gql_prompt_from_orm(new_prompt)
198
+ return Prompt(id=new_prompt.id, db_record=new_prompt)
292
199
 
293
200
  @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
294
201
  async def patch_prompt(self, info: Info[Context, None], input: PatchPromptInput) -> Prompt:
@@ -310,4 +217,4 @@ class PromptMutationMixin:
310
217
  if prompt is None:
311
218
  raise NotFound(f"Prompt with ID '{input.prompt_id}' not found")
312
219
 
313
- return to_gql_prompt_from_orm(prompt)
220
+ return Prompt(id=prompt.id, db_record=prompt)
@@ -16,9 +16,9 @@ from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
16
16
  from phoenix.server.api.queries import Query
17
17
  from phoenix.server.api.types.Identifier import Identifier
18
18
  from phoenix.server.api.types.node import from_global_id_with_expected_type
19
- from phoenix.server.api.types.Prompt import Prompt, to_gql_prompt_from_orm
19
+ from phoenix.server.api.types.Prompt import Prompt
20
20
  from phoenix.server.api.types.PromptVersion import PromptVersion
21
- from phoenix.server.api.types.PromptVersionTag import PromptVersionTag, to_gql_prompt_version_tag
21
+ from phoenix.server.api.types.PromptVersionTag import PromptVersionTag
22
22
 
23
23
 
24
24
  @strawberry.input
@@ -75,7 +75,9 @@ class PromptVersionTagMutationMixin:
75
75
  await session.delete(prompt_version_tag)
76
76
  await session.commit()
77
77
  return PromptVersionTagMutationPayload(
78
- prompt_version_tag=None, query=Query(), prompt=to_gql_prompt_from_orm(prompt)
78
+ prompt_version_tag=None,
79
+ query=Query(),
80
+ prompt=Prompt(id=prompt.id, db_record=prompt),
79
81
  )
80
82
 
81
83
  @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
@@ -111,9 +113,10 @@ class PromptVersionTagMutationMixin:
111
113
  except (PostgreSQLIntegrityError, SQLiteIntegrityError):
112
114
  raise Conflict("Failed to update PromptVersionTag.")
113
115
 
114
- version_tag = to_gql_prompt_version_tag(updated_tag)
115
116
  return PromptVersionTagMutationPayload(
116
- prompt_version_tag=version_tag, prompt=to_gql_prompt_from_orm(prompt), query=Query()
117
+ prompt_version_tag=PromptVersionTag(id=updated_tag.id, db_record=updated_tag),
118
+ prompt=Prompt(id=prompt.id, db_record=prompt),
119
+ query=Query(),
117
120
  )
118
121
 
119
122
 
@@ -21,7 +21,7 @@ from phoenix.server.api.queries import Query
21
21
  from phoenix.server.api.types.AnnotationSource import AnnotationSource
22
22
  from phoenix.server.api.types.AnnotatorKind import AnnotatorKind
23
23
  from phoenix.server.api.types.node import from_global_id_with_expected_type
24
- from phoenix.server.api.types.SpanAnnotation import SpanAnnotation, to_gql_span_annotation
24
+ from phoenix.server.api.types.SpanAnnotation import SpanAnnotation
25
25
  from phoenix.server.bearer_auth import PhoenixUser
26
26
  from phoenix.server.dml_event import SpanAnnotationDeleteEvent, SpanAnnotationInsertEvent
27
27
 
@@ -138,7 +138,7 @@ class SpanAnnotationMutationMixin:
138
138
 
139
139
  # Convert the fully loaded annotations to GQL types
140
140
  returned_annotations = [
141
- to_gql_span_annotation(anno) for anno in ordered_final_annotations
141
+ SpanAnnotation(id=anno.id, db_record=anno) for anno in ordered_final_annotations
142
142
  ]
143
143
 
144
144
  await session.commit()
@@ -184,7 +184,9 @@ class SpanAnnotationMutationMixin:
184
184
  processed_annotation = result.one()
185
185
 
186
186
  info.context.event_queue.put(SpanAnnotationInsertEvent((processed_annotation.id,)))
187
- returned_annotation = to_gql_span_annotation(processed_annotation)
187
+ returned_annotation = SpanAnnotation(
188
+ id=processed_annotation.id, db_record=processed_annotation
189
+ )
188
190
  await session.commit()
189
191
  return SpanAnnotationMutationPayload(
190
192
  span_annotations=[returned_annotation],
@@ -256,7 +258,7 @@ class SpanAnnotationMutationMixin:
256
258
  session.add(span_annotation)
257
259
 
258
260
  patched_annotations = [
259
- to_gql_span_annotation(span_annotation)
261
+ SpanAnnotation(id=span_annotation.id, db_record=span_annotation)
260
262
  for span_annotation in span_annotations_by_id.values()
261
263
  ]
262
264
 
@@ -320,7 +322,10 @@ class SpanAnnotationMutationMixin:
320
322
  )
321
323
 
322
324
  deleted_annotations_gql = [
323
- to_gql_span_annotation(deleted_annotations_by_id[id]) for id in span_annotation_ids
325
+ SpanAnnotation(
326
+ id=deleted_annotations_by_id[id].id, db_record=deleted_annotations_by_id[id]
327
+ )
328
+ for id in span_annotation_ids
324
329
  ]
325
330
  info.context.event_queue.put(
326
331
  SpanAnnotationDeleteEvent(tuple(deleted_annotations_by_id.keys()))
@@ -16,7 +16,7 @@ from phoenix.server.api.input_types.PatchAnnotationInput import PatchAnnotationI
16
16
  from phoenix.server.api.queries import Query
17
17
  from phoenix.server.api.types.AnnotationSource import AnnotationSource
18
18
  from phoenix.server.api.types.node import from_global_id_with_expected_type
19
- from phoenix.server.api.types.TraceAnnotation import TraceAnnotation, to_gql_trace_annotation
19
+ from phoenix.server.api.types.TraceAnnotation import TraceAnnotation
20
20
  from phoenix.server.bearer_auth import PhoenixUser
21
21
  from phoenix.server.dml_event import TraceAnnotationDeleteEvent, TraceAnnotationInsertEvent
22
22
 
@@ -111,7 +111,9 @@ class TraceAnnotationMutationMixin:
111
111
  info.context.event_queue.put(TraceAnnotationInsertEvent(inserted_annotation_ids))
112
112
 
113
113
  returned_annotations = [
114
- to_gql_trace_annotation(processed_annotations_map[i])
114
+ TraceAnnotation(
115
+ id=processed_annotations_map[i].id, db_record=processed_annotations_map[i]
116
+ )
115
117
  for i in sorted(processed_annotations_map.keys())
116
118
  ]
117
119
 
@@ -186,7 +188,7 @@ class TraceAnnotationMutationMixin:
186
188
  await session.commit()
187
189
 
188
190
  patched_annotations = [
189
- to_gql_trace_annotation(trace_annotation)
191
+ TraceAnnotation(id=trace_annotation.id, db_record=trace_annotation)
190
192
  for trace_annotation in trace_annotations_by_id.values()
191
193
  ]
192
194
  info.context.event_queue.put(TraceAnnotationInsertEvent(tuple(patch_by_id.keys())))
@@ -245,7 +247,10 @@ class TraceAnnotationMutationMixin:
245
247
  )
246
248
 
247
249
  deleted_gql_annotations = [
248
- to_gql_trace_annotation(deleted_annotations_by_id[id]) for id in trace_annotation_ids
250
+ TraceAnnotation(
251
+ id=deleted_annotations_by_id[id].id, db_record=deleted_annotations_by_id[id]
252
+ )
253
+ for id in trace_annotation_ids
249
254
  ]
250
255
  info.context.event_queue.put(
251
256
  TraceAnnotationDeleteEvent(tuple(deleted_annotations_by_id.keys()))
@@ -33,7 +33,7 @@ from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound, Unauth
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
 
@@ -155,7 +155,7 @@ class UserMutationMixin:
155
155
  except Exception as error:
156
156
  # Log the error but do not raise it
157
157
  logger.error(f"Failed to send welcome email: {error}")
158
- return UserMutationPayload(user=to_gql_user(user))
158
+ return UserMutationPayload(user=User(id=user.id, db_record=user))
159
159
 
160
160
  @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin]) # type: ignore
161
161
  async def patch_user(
@@ -204,7 +204,7 @@ class UserMutationMixin:
204
204
  assert user
205
205
  if should_log_out:
206
206
  await info.context.log_out(user.id)
207
- return UserMutationPayload(user=to_gql_user(user))
207
+ return UserMutationPayload(user=User(id=user.id, db_record=user))
208
208
 
209
209
  @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
210
210
  async def patch_viewer(
@@ -246,7 +246,7 @@ class UserMutationMixin:
246
246
  response = info.context.get_response()
247
247
  response.delete_cookie(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)
248
248
  response.delete_cookie(PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
249
- return UserMutationPayload(user=to_gql_user(user))
249
+ return UserMutationPayload(user=User(id=user.id, db_record=user))
250
250
 
251
251
  @strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin, IsLocked]) # type: ignore
252
252
  async def delete_users(