arize-phoenix 10.0.4__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.
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +124 -72
- arize_phoenix-12.28.1.dist-info/RECORD +499 -0
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/IP_NOTICE +1 -1
- phoenix/__generated__/__init__.py +0 -0
- phoenix/__generated__/classification_evaluator_configs/__init__.py +20 -0
- phoenix/__generated__/classification_evaluator_configs/_document_relevance_classification_evaluator_config.py +17 -0
- phoenix/__generated__/classification_evaluator_configs/_hallucination_classification_evaluator_config.py +17 -0
- phoenix/__generated__/classification_evaluator_configs/_models.py +18 -0
- phoenix/__generated__/classification_evaluator_configs/_tool_selection_classification_evaluator_config.py +17 -0
- phoenix/__init__.py +5 -4
- phoenix/auth.py +39 -2
- phoenix/config.py +1763 -91
- phoenix/datetime_utils.py +120 -2
- phoenix/db/README.md +595 -25
- phoenix/db/bulk_inserter.py +145 -103
- phoenix/db/engines.py +140 -33
- phoenix/db/enums.py +3 -12
- phoenix/db/facilitator.py +302 -35
- phoenix/db/helpers.py +1000 -65
- phoenix/db/iam_auth.py +64 -0
- phoenix/db/insertion/dataset.py +135 -2
- phoenix/db/insertion/document_annotation.py +9 -6
- phoenix/db/insertion/evaluation.py +2 -3
- phoenix/db/insertion/helpers.py +17 -2
- phoenix/db/insertion/session_annotation.py +176 -0
- phoenix/db/insertion/span.py +15 -11
- phoenix/db/insertion/span_annotation.py +3 -4
- phoenix/db/insertion/trace_annotation.py +3 -4
- phoenix/db/insertion/types.py +50 -20
- phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
- phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
- phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
- phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
- phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
- phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
- phoenix/db/migrations/versions/a20694b15f82_cost.py +196 -0
- phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
- phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
- phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
- phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
- phoenix/db/models.py +669 -56
- phoenix/db/pg_config.py +10 -0
- phoenix/db/types/model_provider.py +4 -0
- phoenix/db/types/token_price_customization.py +29 -0
- phoenix/db/types/trace_retention.py +23 -15
- phoenix/experiments/evaluators/utils.py +3 -3
- phoenix/experiments/functions.py +160 -52
- phoenix/experiments/tracing.py +2 -2
- phoenix/experiments/types.py +1 -1
- phoenix/inferences/inferences.py +1 -2
- phoenix/server/api/auth.py +38 -7
- phoenix/server/api/auth_messages.py +46 -0
- phoenix/server/api/context.py +100 -4
- phoenix/server/api/dataloaders/__init__.py +79 -5
- phoenix/server/api/dataloaders/annotation_configs_by_project.py +31 -0
- phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
- phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
- phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
- phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
- phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
- phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
- phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
- phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
- phoenix/server/api/dataloaders/dataset_labels.py +36 -0
- phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
- phoenix/server/api/dataloaders/document_evaluations.py +6 -9
- phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
- phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
- phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
- phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
- phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
- phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
- phoenix/server/api/dataloaders/last_used_times_by_generative_model_id.py +35 -0
- phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
- phoenix/server/api/dataloaders/record_counts.py +37 -10
- phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
- phoenix/server/api/dataloaders/span_cost_by_span.py +24 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_generative_model.py +56 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_project_session.py +57 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_span.py +43 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_trace.py +56 -0
- phoenix/server/api/dataloaders/span_cost_details_by_span_cost.py +27 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +57 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment_run.py +58 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_generative_model.py +55 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project.py +152 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project_session.py +56 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_trace.py +55 -0
- phoenix/server/api/dataloaders/span_costs.py +29 -0
- phoenix/server/api/dataloaders/table_fields.py +2 -2
- phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
- phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
- phoenix/server/api/dataloaders/types.py +29 -0
- phoenix/server/api/exceptions.py +11 -1
- phoenix/server/api/helpers/dataset_helpers.py +5 -1
- phoenix/server/api/helpers/playground_clients.py +1243 -292
- phoenix/server/api/helpers/playground_registry.py +2 -2
- phoenix/server/api/helpers/playground_spans.py +8 -4
- phoenix/server/api/helpers/playground_users.py +26 -0
- phoenix/server/api/helpers/prompts/conversions/aws.py +83 -0
- phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
- phoenix/server/api/helpers/prompts/models.py +205 -22
- phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
- phoenix/server/api/input_types/ChatCompletionInput.py +6 -2
- phoenix/server/api/input_types/CreateProjectInput.py +27 -0
- phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
- phoenix/server/api/input_types/DatasetFilter.py +17 -0
- phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
- phoenix/server/api/input_types/GenerativeCredentialInput.py +9 -0
- phoenix/server/api/input_types/GenerativeModelInput.py +5 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +161 -1
- phoenix/server/api/input_types/PromptFilter.py +14 -0
- phoenix/server/api/input_types/PromptVersionInput.py +52 -1
- phoenix/server/api/input_types/SpanSort.py +44 -7
- phoenix/server/api/input_types/TimeBinConfig.py +23 -0
- phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/__init__.py +10 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
- phoenix/server/api/mutations/api_key_mutations.py +19 -23
- phoenix/server/api/mutations/chat_mutations.py +154 -47
- phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
- phoenix/server/api/mutations/dataset_mutations.py +21 -16
- phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
- phoenix/server/api/mutations/experiment_mutations.py +2 -2
- phoenix/server/api/mutations/export_events_mutations.py +3 -3
- phoenix/server/api/mutations/model_mutations.py +210 -0
- phoenix/server/api/mutations/project_mutations.py +49 -10
- phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
- phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
- phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
- phoenix/server/api/mutations/prompt_mutations.py +65 -129
- phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
- phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
- phoenix/server/api/mutations/trace_annotations_mutations.py +14 -10
- phoenix/server/api/mutations/trace_mutations.py +47 -3
- phoenix/server/api/mutations/user_mutations.py +66 -41
- phoenix/server/api/queries.py +768 -293
- phoenix/server/api/routers/__init__.py +2 -2
- phoenix/server/api/routers/auth.py +154 -88
- phoenix/server/api/routers/ldap.py +229 -0
- phoenix/server/api/routers/oauth2.py +369 -106
- phoenix/server/api/routers/v1/__init__.py +24 -4
- phoenix/server/api/routers/v1/annotation_configs.py +23 -31
- phoenix/server/api/routers/v1/annotations.py +481 -17
- phoenix/server/api/routers/v1/datasets.py +395 -81
- phoenix/server/api/routers/v1/documents.py +142 -0
- phoenix/server/api/routers/v1/evaluations.py +24 -31
- phoenix/server/api/routers/v1/experiment_evaluations.py +19 -8
- phoenix/server/api/routers/v1/experiment_runs.py +337 -59
- phoenix/server/api/routers/v1/experiments.py +479 -48
- phoenix/server/api/routers/v1/models.py +7 -0
- phoenix/server/api/routers/v1/projects.py +18 -49
- phoenix/server/api/routers/v1/prompts.py +54 -40
- phoenix/server/api/routers/v1/sessions.py +108 -0
- phoenix/server/api/routers/v1/spans.py +1091 -81
- phoenix/server/api/routers/v1/traces.py +132 -78
- phoenix/server/api/routers/v1/users.py +389 -0
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +305 -88
- phoenix/server/api/types/Annotation.py +90 -23
- phoenix/server/api/types/ApiKey.py +13 -17
- phoenix/server/api/types/AuthMethod.py +1 -0
- phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
- phoenix/server/api/types/CostBreakdown.py +12 -0
- phoenix/server/api/types/Dataset.py +226 -72
- phoenix/server/api/types/DatasetExample.py +88 -18
- phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
- phoenix/server/api/types/DatasetLabel.py +57 -0
- phoenix/server/api/types/DatasetSplit.py +98 -0
- phoenix/server/api/types/DatasetVersion.py +49 -4
- phoenix/server/api/types/DocumentAnnotation.py +212 -0
- phoenix/server/api/types/Experiment.py +264 -59
- phoenix/server/api/types/ExperimentComparison.py +5 -10
- phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
- phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
- phoenix/server/api/types/ExperimentRun.py +169 -65
- phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
- phoenix/server/api/types/GenerativeModel.py +245 -3
- phoenix/server/api/types/GenerativeProvider.py +70 -11
- phoenix/server/api/types/{Model.py → InferenceModel.py} +1 -1
- phoenix/server/api/types/ModelInterface.py +16 -0
- phoenix/server/api/types/PlaygroundModel.py +20 -0
- phoenix/server/api/types/Project.py +1278 -216
- phoenix/server/api/types/ProjectSession.py +188 -28
- phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
- phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
- phoenix/server/api/types/Prompt.py +119 -39
- phoenix/server/api/types/PromptLabel.py +42 -25
- phoenix/server/api/types/PromptVersion.py +11 -8
- phoenix/server/api/types/PromptVersionTag.py +65 -25
- phoenix/server/api/types/ServerStatus.py +6 -0
- phoenix/server/api/types/Span.py +167 -123
- phoenix/server/api/types/SpanAnnotation.py +189 -42
- phoenix/server/api/types/SpanCostDetailSummaryEntry.py +10 -0
- phoenix/server/api/types/SpanCostSummary.py +10 -0
- phoenix/server/api/types/SystemApiKey.py +65 -1
- phoenix/server/api/types/TokenPrice.py +16 -0
- phoenix/server/api/types/TokenUsage.py +3 -3
- phoenix/server/api/types/Trace.py +223 -51
- phoenix/server/api/types/TraceAnnotation.py +149 -50
- phoenix/server/api/types/User.py +137 -32
- phoenix/server/api/types/UserApiKey.py +73 -26
- phoenix/server/api/types/node.py +10 -0
- phoenix/server/api/types/pagination.py +11 -2
- phoenix/server/app.py +290 -45
- phoenix/server/authorization.py +38 -3
- phoenix/server/bearer_auth.py +34 -24
- phoenix/server/cost_tracking/cost_details_calculator.py +196 -0
- phoenix/server/cost_tracking/cost_model_lookup.py +179 -0
- phoenix/server/cost_tracking/helpers.py +68 -0
- phoenix/server/cost_tracking/model_cost_manifest.json +3657 -830
- phoenix/server/cost_tracking/regex_specificity.py +397 -0
- phoenix/server/cost_tracking/token_cost_calculator.py +57 -0
- phoenix/server/daemons/__init__.py +0 -0
- phoenix/server/daemons/db_disk_usage_monitor.py +214 -0
- phoenix/server/daemons/generative_model_store.py +103 -0
- phoenix/server/daemons/span_cost_calculator.py +99 -0
- phoenix/server/dml_event.py +17 -0
- phoenix/server/dml_event_handler.py +5 -0
- phoenix/server/email/sender.py +56 -3
- phoenix/server/email/templates/db_disk_usage_notification.html +19 -0
- phoenix/server/email/types.py +11 -0
- phoenix/server/experiments/__init__.py +0 -0
- phoenix/server/experiments/utils.py +14 -0
- phoenix/server/grpc_server.py +11 -11
- phoenix/server/jwt_store.py +17 -15
- phoenix/server/ldap.py +1449 -0
- phoenix/server/main.py +26 -10
- phoenix/server/oauth2.py +330 -12
- phoenix/server/prometheus.py +66 -6
- phoenix/server/rate_limiters.py +4 -9
- phoenix/server/retention.py +33 -20
- phoenix/server/session_filters.py +49 -0
- phoenix/server/static/.vite/manifest.json +55 -51
- phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
- phoenix/server/static/assets/{index-E0M82BdE.js → index-CTQoemZv.js} +140 -56
- phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
- phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
- phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
- phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
- phoenix/server/static/assets/vendor-recharts-V9cwpXsm.js +37 -0
- phoenix/server/static/assets/vendor-shiki-Do--csgv.js +5 -0
- phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
- phoenix/server/templates/index.html +40 -6
- phoenix/server/thread_server.py +1 -2
- phoenix/server/types.py +14 -4
- phoenix/server/utils.py +74 -0
- phoenix/session/client.py +56 -3
- phoenix/session/data_extractor.py +5 -0
- phoenix/session/evaluation.py +14 -5
- phoenix/session/session.py +45 -9
- phoenix/settings.py +5 -0
- phoenix/trace/attributes.py +80 -13
- phoenix/trace/dsl/helpers.py +90 -1
- phoenix/trace/dsl/query.py +8 -6
- phoenix/trace/projects.py +5 -0
- phoenix/utilities/template_formatters.py +1 -1
- phoenix/version.py +1 -1
- arize_phoenix-10.0.4.dist-info/RECORD +0 -405
- phoenix/server/api/types/Evaluation.py +0 -39
- phoenix/server/cost_tracking/cost_lookup.py +0 -255
- phoenix/server/static/assets/components-DULKeDfL.js +0 -4365
- phoenix/server/static/assets/pages-Cl0A-0U2.js +0 -7430
- phoenix/server/static/assets/vendor-WIZid84E.css +0 -1
- phoenix/server/static/assets/vendor-arizeai-Dy-0mSNw.js +0 -649
- phoenix/server/static/assets/vendor-codemirror-DBtifKNr.js +0 -33
- phoenix/server/static/assets/vendor-oB4u9zuV.js +0 -905
- phoenix/server/static/assets/vendor-recharts-D-T4KPz2.js +0 -59
- phoenix/server/static/assets/vendor-shiki-BMn4O_9F.js +0 -5
- phoenix/server/static/assets/vendor-three-C5WAXd5r.js +0 -2998
- phoenix/utilities/deprecation.py +0 -31
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,7 +7,7 @@ from starlette.requests import Request
|
|
|
7
7
|
from strawberry import UNSET, Info
|
|
8
8
|
|
|
9
9
|
from phoenix.db import models
|
|
10
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
10
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
11
11
|
from phoenix.server.api.context import Context
|
|
12
12
|
from phoenix.server.api.exceptions import BadRequest, NotFound, Unauthorized
|
|
13
13
|
from phoenix.server.api.helpers.annotations import get_user_identifier
|
|
@@ -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
|
|
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
|
|
|
@@ -34,7 +34,7 @@ class SpanAnnotationMutationPayload:
|
|
|
34
34
|
|
|
35
35
|
@strawberry.type
|
|
36
36
|
class SpanAnnotationMutationMixin:
|
|
37
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
37
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
38
38
|
async def create_span_annotations(
|
|
39
39
|
self, info: Info[Context, None], input: list[CreateSpanAnnotationInput]
|
|
40
40
|
) -> SpanAnnotationMutationPayload:
|
|
@@ -138,7 +138,7 @@ class SpanAnnotationMutationMixin:
|
|
|
138
138
|
|
|
139
139
|
# Convert the fully loaded annotations to GQL types
|
|
140
140
|
returned_annotations = [
|
|
141
|
-
|
|
141
|
+
SpanAnnotation(id=anno.id, db_record=anno) for anno in ordered_final_annotations
|
|
142
142
|
]
|
|
143
143
|
|
|
144
144
|
await session.commit()
|
|
@@ -148,7 +148,7 @@ class SpanAnnotationMutationMixin:
|
|
|
148
148
|
query=Query(),
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
151
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
152
152
|
async def create_span_note(
|
|
153
153
|
self, info: Info[Context, None], annotation_input: CreateSpanNoteInput
|
|
154
154
|
) -> SpanAnnotationMutationPayload:
|
|
@@ -184,14 +184,16 @@ class SpanAnnotationMutationMixin:
|
|
|
184
184
|
processed_annotation = result.one()
|
|
185
185
|
|
|
186
186
|
info.context.event_queue.put(SpanAnnotationInsertEvent((processed_annotation.id,)))
|
|
187
|
-
returned_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],
|
|
191
193
|
query=Query(),
|
|
192
194
|
)
|
|
193
195
|
|
|
194
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
196
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
195
197
|
async def patch_span_annotations(
|
|
196
198
|
self, info: Info[Context, None], input: list[PatchAnnotationInput]
|
|
197
199
|
) -> SpanAnnotationMutationPayload:
|
|
@@ -256,7 +258,7 @@ class SpanAnnotationMutationMixin:
|
|
|
256
258
|
session.add(span_annotation)
|
|
257
259
|
|
|
258
260
|
patched_annotations = [
|
|
259
|
-
|
|
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
|
|
|
@@ -268,7 +270,7 @@ class SpanAnnotationMutationMixin:
|
|
|
268
270
|
query=Query(),
|
|
269
271
|
)
|
|
270
272
|
|
|
271
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
273
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
272
274
|
async def delete_span_annotations(
|
|
273
275
|
self, info: Info[Context, None], input: DeleteAnnotationsInput
|
|
274
276
|
) -> SpanAnnotationMutationPayload:
|
|
@@ -320,7 +322,10 @@ class SpanAnnotationMutationMixin:
|
|
|
320
322
|
)
|
|
321
323
|
|
|
322
324
|
deleted_annotations_gql = [
|
|
323
|
-
|
|
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()))
|
|
@@ -6,7 +6,7 @@ from starlette.requests import Request
|
|
|
6
6
|
from strawberry import UNSET, Info
|
|
7
7
|
|
|
8
8
|
from phoenix.db import models
|
|
9
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
9
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
10
10
|
from phoenix.server.api.context import Context
|
|
11
11
|
from phoenix.server.api.exceptions import BadRequest, NotFound, Unauthorized
|
|
12
12
|
from phoenix.server.api.helpers.annotations import get_user_identifier
|
|
@@ -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
|
|
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
|
|
|
@@ -29,7 +29,7 @@ class TraceAnnotationMutationPayload:
|
|
|
29
29
|
|
|
30
30
|
@strawberry.type
|
|
31
31
|
class TraceAnnotationMutationMixin:
|
|
32
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
32
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
33
33
|
async def create_trace_annotations(
|
|
34
34
|
self, info: Info[Context, None], input: list[CreateTraceAnnotationInput]
|
|
35
35
|
) -> TraceAnnotationMutationPayload:
|
|
@@ -49,8 +49,7 @@ class TraceAnnotationMutationMixin:
|
|
|
49
49
|
trace_rowid = from_global_id_with_expected_type(annotation_input.trace_id, "Trace")
|
|
50
50
|
except ValueError:
|
|
51
51
|
raise BadRequest(
|
|
52
|
-
f"Invalid trace ID for annotation at index {idx}: "
|
|
53
|
-
f"{annotation_input.trace_id}"
|
|
52
|
+
f"Invalid trace ID for annotation at index {idx}: {annotation_input.trace_id}"
|
|
54
53
|
)
|
|
55
54
|
trace_rowids.append(trace_rowid)
|
|
56
55
|
|
|
@@ -112,7 +111,9 @@ class TraceAnnotationMutationMixin:
|
|
|
112
111
|
info.context.event_queue.put(TraceAnnotationInsertEvent(inserted_annotation_ids))
|
|
113
112
|
|
|
114
113
|
returned_annotations = [
|
|
115
|
-
|
|
114
|
+
TraceAnnotation(
|
|
115
|
+
id=processed_annotations_map[i].id, db_record=processed_annotations_map[i]
|
|
116
|
+
)
|
|
116
117
|
for i in sorted(processed_annotations_map.keys())
|
|
117
118
|
]
|
|
118
119
|
|
|
@@ -121,7 +122,7 @@ class TraceAnnotationMutationMixin:
|
|
|
121
122
|
query=Query(),
|
|
122
123
|
)
|
|
123
124
|
|
|
124
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
125
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
125
126
|
async def patch_trace_annotations(
|
|
126
127
|
self, info: Info[Context, None], input: list[PatchAnnotationInput]
|
|
127
128
|
) -> TraceAnnotationMutationPayload:
|
|
@@ -187,7 +188,7 @@ class TraceAnnotationMutationMixin:
|
|
|
187
188
|
await session.commit()
|
|
188
189
|
|
|
189
190
|
patched_annotations = [
|
|
190
|
-
|
|
191
|
+
TraceAnnotation(id=trace_annotation.id, db_record=trace_annotation)
|
|
191
192
|
for trace_annotation in trace_annotations_by_id.values()
|
|
192
193
|
]
|
|
193
194
|
info.context.event_queue.put(TraceAnnotationInsertEvent(tuple(patch_by_id.keys())))
|
|
@@ -196,7 +197,7 @@ class TraceAnnotationMutationMixin:
|
|
|
196
197
|
query=Query(),
|
|
197
198
|
)
|
|
198
199
|
|
|
199
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
200
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
200
201
|
async def delete_trace_annotations(
|
|
201
202
|
self, info: Info[Context, None], input: DeleteAnnotationsInput
|
|
202
203
|
) -> TraceAnnotationMutationPayload:
|
|
@@ -246,7 +247,10 @@ class TraceAnnotationMutationMixin:
|
|
|
246
247
|
)
|
|
247
248
|
|
|
248
249
|
deleted_gql_annotations = [
|
|
249
|
-
|
|
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
|
|
250
254
|
]
|
|
251
255
|
info.context.event_queue.put(
|
|
252
256
|
TraceAnnotationDeleteEvent(tuple(deleted_annotations_by_id.keys()))
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import strawberry
|
|
2
|
-
from sqlalchemy import and_, delete, not_, select
|
|
2
|
+
from sqlalchemy import and_, delete, not_, select, update
|
|
3
3
|
from sqlalchemy.orm import load_only
|
|
4
4
|
from sqlalchemy.sql import literal
|
|
5
5
|
from strawberry.relay import GlobalID
|
|
6
6
|
from strawberry.types import Info
|
|
7
7
|
|
|
8
8
|
from phoenix.db import models
|
|
9
|
-
from phoenix.server.api.auth import IsNotReadOnly
|
|
9
|
+
from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
|
|
10
10
|
from phoenix.server.api.context import Context
|
|
11
11
|
from phoenix.server.api.exceptions import BadRequest
|
|
12
12
|
from phoenix.server.api.queries import Query
|
|
@@ -16,7 +16,7 @@ from phoenix.server.dml_event import SpanDeleteEvent
|
|
|
16
16
|
|
|
17
17
|
@strawberry.type
|
|
18
18
|
class TraceMutationMixin:
|
|
19
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
19
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
20
20
|
async def delete_traces(
|
|
21
21
|
self,
|
|
22
22
|
info: Info[Context, None],
|
|
@@ -72,3 +72,47 @@ class TraceMutationMixin:
|
|
|
72
72
|
)
|
|
73
73
|
info.context.event_queue.put(SpanDeleteEvent(project_ids))
|
|
74
74
|
return Query()
|
|
75
|
+
|
|
76
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
77
|
+
async def transfer_traces_to_project(
|
|
78
|
+
self,
|
|
79
|
+
info: Info[Context, None],
|
|
80
|
+
trace_ids: list[GlobalID],
|
|
81
|
+
project_id: GlobalID,
|
|
82
|
+
) -> Query:
|
|
83
|
+
if not trace_ids:
|
|
84
|
+
raise BadRequest("Must provide at least one trace ID to transfer")
|
|
85
|
+
trace_ids = list(set(trace_ids))
|
|
86
|
+
try:
|
|
87
|
+
trace_rowids = [
|
|
88
|
+
from_global_id_with_expected_type(global_id=id, expected_type_name="Trace")
|
|
89
|
+
for id in trace_ids
|
|
90
|
+
]
|
|
91
|
+
dest_project_rowid = from_global_id_with_expected_type(
|
|
92
|
+
global_id=project_id, expected_type_name="Project"
|
|
93
|
+
)
|
|
94
|
+
except ValueError as error:
|
|
95
|
+
raise BadRequest(str(error))
|
|
96
|
+
|
|
97
|
+
async with info.context.db() as session:
|
|
98
|
+
dest_project = await session.get(models.Project, dest_project_rowid)
|
|
99
|
+
if dest_project is None:
|
|
100
|
+
raise BadRequest("Destination project does not exist")
|
|
101
|
+
|
|
102
|
+
traces = (
|
|
103
|
+
await session.scalars(select(models.Trace).where(models.Trace.id.in_(trace_rowids)))
|
|
104
|
+
).all()
|
|
105
|
+
if len(traces) < len(trace_rowids):
|
|
106
|
+
raise BadRequest("Invalid trace IDs provided")
|
|
107
|
+
|
|
108
|
+
source_project_ids = set(trace.project_rowid for trace in traces)
|
|
109
|
+
if len(source_project_ids) > 1:
|
|
110
|
+
raise BadRequest("Cannot transfer traces from multiple projects")
|
|
111
|
+
|
|
112
|
+
await session.execute(
|
|
113
|
+
update(models.Trace)
|
|
114
|
+
.where(models.Trace.id.in_(trace_rowids))
|
|
115
|
+
.values(project_rowid=dest_project_rowid)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return Query()
|
|
@@ -21,18 +21,19 @@ from phoenix.auth import (
|
|
|
21
21
|
PASSWORD_REQUIREMENTS,
|
|
22
22
|
PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
|
|
23
23
|
PHOENIX_REFRESH_TOKEN_COOKIE_NAME,
|
|
24
|
+
sanitize_email,
|
|
24
25
|
validate_email_format,
|
|
25
26
|
validate_password_format,
|
|
26
27
|
)
|
|
27
28
|
from phoenix.config import get_env_disable_basic_auth
|
|
28
|
-
from phoenix.db import
|
|
29
|
-
from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly
|
|
29
|
+
from phoenix.db import models
|
|
30
|
+
from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly, IsNotViewer
|
|
30
31
|
from phoenix.server.api.context import Context
|
|
31
32
|
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound, Unauthorized
|
|
32
33
|
from phoenix.server.api.input_types.UserRoleInput import UserRoleInput
|
|
33
34
|
from phoenix.server.api.types.AuthMethod import AuthMethod
|
|
34
35
|
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
35
|
-
from phoenix.server.api.types.User import User
|
|
36
|
+
from phoenix.server.api.types.User import User
|
|
36
37
|
from phoenix.server.bearer_auth import PhoenixUser
|
|
37
38
|
from phoenix.server.types import AccessTokenId, ApiKeyId, PasswordResetTokenId, RefreshTokenId
|
|
38
39
|
|
|
@@ -49,7 +50,10 @@ class CreateUserInput:
|
|
|
49
50
|
auth_method: Optional[AuthMethod] = AuthMethod.LOCAL
|
|
50
51
|
|
|
51
52
|
def __post_init__(self) -> None:
|
|
52
|
-
if self.auth_method is AuthMethod.
|
|
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:
|
|
53
57
|
if self.password:
|
|
54
58
|
raise BadRequest("Password is not allowed for OAuth2 authentication")
|
|
55
59
|
elif get_env_disable_basic_auth():
|
|
@@ -102,6 +106,11 @@ class DeleteUsersInput:
|
|
|
102
106
|
user_ids: list[GlobalID]
|
|
103
107
|
|
|
104
108
|
|
|
109
|
+
@strawberry.type
|
|
110
|
+
class DeleteUsersPayload:
|
|
111
|
+
user_ids: list[GlobalID]
|
|
112
|
+
|
|
113
|
+
|
|
105
114
|
@strawberry.type
|
|
106
115
|
class UserMutationPayload:
|
|
107
116
|
user: User
|
|
@@ -109,26 +118,34 @@ class UserMutationPayload:
|
|
|
109
118
|
|
|
110
119
|
@strawberry.type
|
|
111
120
|
class UserMutationMixin:
|
|
112
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
|
121
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin, IsLocked]) # type: ignore
|
|
113
122
|
async def create_user(
|
|
114
123
|
self,
|
|
115
124
|
info: Info[Context, None],
|
|
116
125
|
input: CreateUserInput,
|
|
117
126
|
) -> UserMutationPayload:
|
|
127
|
+
# Sanitize email by trimming and lowercasing
|
|
128
|
+
email = sanitize_email(input.email)
|
|
129
|
+
|
|
118
130
|
user: models.User
|
|
119
|
-
if input.auth_method is AuthMethod.
|
|
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:
|
|
120
137
|
user = models.OAuth2User(
|
|
121
|
-
email=
|
|
138
|
+
email=email,
|
|
122
139
|
username=input.username,
|
|
123
140
|
)
|
|
124
141
|
else:
|
|
125
142
|
assert input.password
|
|
126
|
-
validate_email_format(
|
|
143
|
+
validate_email_format(email)
|
|
127
144
|
validate_password_format(input.password)
|
|
128
145
|
salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
|
|
129
146
|
password_hash = await info.context.hash_password(Secret(input.password), salt)
|
|
130
147
|
user = models.LocalUser(
|
|
131
|
-
email=
|
|
148
|
+
email=email,
|
|
132
149
|
username=input.username,
|
|
133
150
|
password_hash=password_hash,
|
|
134
151
|
password_salt=salt,
|
|
@@ -151,9 +168,9 @@ class UserMutationMixin:
|
|
|
151
168
|
except Exception as error:
|
|
152
169
|
# Log the error but do not raise it
|
|
153
170
|
logger.error(f"Failed to send welcome email: {error}")
|
|
154
|
-
return UserMutationPayload(user=
|
|
171
|
+
return UserMutationPayload(user=User(id=user.id, db_record=user))
|
|
155
172
|
|
|
156
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly,
|
|
173
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin]) # type: ignore
|
|
157
174
|
async def patch_user(
|
|
158
175
|
self,
|
|
159
176
|
info: Info[Context, None],
|
|
@@ -170,6 +187,7 @@ class UserMutationMixin:
|
|
|
170
187
|
if not (user := await session.scalar(_select_user_by_id(user_id))):
|
|
171
188
|
raise NotFound("User not found")
|
|
172
189
|
stack.enter_context(session.no_autoflush)
|
|
190
|
+
should_log_out = False
|
|
173
191
|
if input.new_role:
|
|
174
192
|
if user.email == DEFAULT_ADMIN_EMAIL:
|
|
175
193
|
raise Unauthorized("Cannot modify role for the default admin user")
|
|
@@ -179,6 +197,7 @@ class UserMutationMixin:
|
|
|
179
197
|
if user_role_id is None:
|
|
180
198
|
raise NotFound(f"Role {input.new_role.value} not found")
|
|
181
199
|
user.user_role_id = user_role_id
|
|
200
|
+
should_log_out = True
|
|
182
201
|
if password := input.new_password:
|
|
183
202
|
if user.auth_method != "LOCAL":
|
|
184
203
|
raise Conflict("Cannot modify password for non-local user")
|
|
@@ -187,6 +206,7 @@ class UserMutationMixin:
|
|
|
187
206
|
user.password_salt = salt
|
|
188
207
|
user.password_hash = await info.context.hash_password(Secret(password), salt)
|
|
189
208
|
user.reset_password = True
|
|
209
|
+
should_log_out = True
|
|
190
210
|
if username := input.new_username:
|
|
191
211
|
user.username = username
|
|
192
212
|
assert user in session.dirty
|
|
@@ -195,11 +215,11 @@ class UserMutationMixin:
|
|
|
195
215
|
except (PostgreSQLIntegrityError, SQLiteIntegrityError) as error:
|
|
196
216
|
raise Conflict(_user_operation_error_message(error, "modify"))
|
|
197
217
|
assert user
|
|
198
|
-
if
|
|
218
|
+
if should_log_out:
|
|
199
219
|
await info.context.log_out(user.id)
|
|
200
|
-
return UserMutationPayload(user=
|
|
220
|
+
return UserMutationPayload(user=User(id=user.id, db_record=user))
|
|
201
221
|
|
|
202
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly
|
|
222
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
203
223
|
async def patch_viewer(
|
|
204
224
|
self,
|
|
205
225
|
info: Info[Context, None],
|
|
@@ -239,33 +259,28 @@ class UserMutationMixin:
|
|
|
239
259
|
response = info.context.get_response()
|
|
240
260
|
response.delete_cookie(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)
|
|
241
261
|
response.delete_cookie(PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
|
|
242
|
-
return UserMutationPayload(user=
|
|
262
|
+
return UserMutationPayload(user=User(id=user.id, db_record=user))
|
|
243
263
|
|
|
244
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
|
264
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin, IsLocked]) # type: ignore
|
|
245
265
|
async def delete_users(
|
|
246
266
|
self,
|
|
247
267
|
info: Info[Context, None],
|
|
248
268
|
input: DeleteUsersInput,
|
|
249
|
-
) ->
|
|
269
|
+
) -> DeleteUsersPayload:
|
|
250
270
|
assert (token_store := info.context.token_store) is not None
|
|
251
271
|
if not input.user_ids:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
admin_user_role_id = (
|
|
265
|
-
select(models.UserRole.id)
|
|
266
|
-
.where(models.UserRole.name == enums.UserRole.ADMIN.value)
|
|
267
|
-
.scalar_subquery()
|
|
268
|
-
)
|
|
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())
|
|
282
|
+
system_user_role_id = select(models.UserRole.id).filter_by(name="SYSTEM").scalar_subquery()
|
|
283
|
+
admin_user_role_id = select(models.UserRole.id).filter_by(name="ADMIN").scalar_subquery()
|
|
269
284
|
default_admin_user_id = (
|
|
270
285
|
select(models.User.id)
|
|
271
286
|
.where(
|
|
@@ -302,7 +317,7 @@ class UserMutationMixin:
|
|
|
302
317
|
.select_from(models.User)
|
|
303
318
|
.where(
|
|
304
319
|
and_(
|
|
305
|
-
models.User.id.in_(
|
|
320
|
+
models.User.id.in_(user_rowids),
|
|
306
321
|
models.User.user_role_id != system_user_role_id,
|
|
307
322
|
)
|
|
308
323
|
)
|
|
@@ -310,41 +325,51 @@ class UserMutationMixin:
|
|
|
310
325
|
).all()
|
|
311
326
|
if deletes_default_admin:
|
|
312
327
|
raise Conflict("Cannot delete the default admin user")
|
|
313
|
-
if num_resolved_user_ids < len(
|
|
328
|
+
if num_resolved_user_ids < len(user_rowids):
|
|
314
329
|
raise NotFound("Some user IDs could not be found")
|
|
315
330
|
password_reset_token_ids = [
|
|
316
331
|
PasswordResetTokenId(id_)
|
|
317
332
|
async for id_ in await session.stream_scalars(
|
|
318
333
|
select(models.PasswordResetToken.id).where(
|
|
319
|
-
models.PasswordResetToken.user_id.in_(
|
|
334
|
+
models.PasswordResetToken.user_id.in_(user_rowids)
|
|
320
335
|
)
|
|
321
336
|
)
|
|
322
337
|
]
|
|
323
338
|
access_token_ids = [
|
|
324
339
|
AccessTokenId(id_)
|
|
325
340
|
async for id_ in await session.stream_scalars(
|
|
326
|
-
select(models.AccessToken.id).where(models.AccessToken.user_id.in_(
|
|
341
|
+
select(models.AccessToken.id).where(models.AccessToken.user_id.in_(user_rowids))
|
|
327
342
|
)
|
|
328
343
|
]
|
|
329
344
|
refresh_token_ids = [
|
|
330
345
|
RefreshTokenId(id_)
|
|
331
346
|
async for id_ in await session.stream_scalars(
|
|
332
|
-
select(models.RefreshToken.id).where(
|
|
347
|
+
select(models.RefreshToken.id).where(
|
|
348
|
+
models.RefreshToken.user_id.in_(user_rowids)
|
|
349
|
+
)
|
|
333
350
|
)
|
|
334
351
|
]
|
|
335
352
|
api_key_ids = [
|
|
336
353
|
ApiKeyId(id_)
|
|
337
354
|
async for id_ in await session.stream_scalars(
|
|
338
|
-
select(models.ApiKey.id).where(models.ApiKey.user_id.in_(
|
|
355
|
+
select(models.ApiKey.id).where(models.ApiKey.user_id.in_(user_rowids))
|
|
339
356
|
)
|
|
340
357
|
]
|
|
341
|
-
await session.
|
|
358
|
+
deleted_user_ids = await session.scalars(
|
|
359
|
+
delete(models.User).where(models.User.id.in_(user_rowids)).returning(models.User.id)
|
|
360
|
+
)
|
|
342
361
|
await token_store.revoke(
|
|
343
362
|
*password_reset_token_ids,
|
|
344
363
|
*access_token_ids,
|
|
345
364
|
*refresh_token_ids,
|
|
346
365
|
*api_key_ids,
|
|
347
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)
|
|
348
373
|
|
|
349
374
|
|
|
350
375
|
def _select_role_id_by_name(role_name: str) -> Select[tuple[int]]:
|