arize-phoenix 3.16.0__py3-none-any.whl → 7.7.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.
- arize_phoenix-7.7.0.dist-info/METADATA +261 -0
- arize_phoenix-7.7.0.dist-info/RECORD +345 -0
- {arize_phoenix-3.16.0.dist-info → arize_phoenix-7.7.0.dist-info}/WHEEL +1 -1
- arize_phoenix-7.7.0.dist-info/entry_points.txt +3 -0
- phoenix/__init__.py +86 -14
- phoenix/auth.py +309 -0
- phoenix/config.py +675 -45
- phoenix/core/model.py +32 -30
- phoenix/core/model_schema.py +102 -109
- phoenix/core/model_schema_adapter.py +48 -45
- phoenix/datetime_utils.py +24 -3
- phoenix/db/README.md +54 -0
- phoenix/db/__init__.py +4 -0
- phoenix/db/alembic.ini +85 -0
- phoenix/db/bulk_inserter.py +294 -0
- phoenix/db/engines.py +208 -0
- phoenix/db/enums.py +20 -0
- phoenix/db/facilitator.py +113 -0
- phoenix/db/helpers.py +159 -0
- phoenix/db/insertion/constants.py +2 -0
- phoenix/db/insertion/dataset.py +227 -0
- phoenix/db/insertion/document_annotation.py +171 -0
- phoenix/db/insertion/evaluation.py +191 -0
- phoenix/db/insertion/helpers.py +98 -0
- phoenix/db/insertion/span.py +193 -0
- phoenix/db/insertion/span_annotation.py +158 -0
- phoenix/db/insertion/trace_annotation.py +158 -0
- phoenix/db/insertion/types.py +256 -0
- phoenix/db/migrate.py +86 -0
- phoenix/db/migrations/data_migration_scripts/populate_project_sessions.py +199 -0
- phoenix/db/migrations/env.py +114 -0
- phoenix/db/migrations/script.py.mako +26 -0
- phoenix/db/migrations/versions/10460e46d750_datasets.py +317 -0
- phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py +126 -0
- phoenix/db/migrations/versions/4ded9e43755f_create_project_sessions_table.py +66 -0
- phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
- phoenix/db/migrations/versions/cf03bd6bae1d_init.py +280 -0
- phoenix/db/models.py +807 -0
- phoenix/exceptions.py +5 -1
- phoenix/experiments/__init__.py +6 -0
- phoenix/experiments/evaluators/__init__.py +29 -0
- phoenix/experiments/evaluators/base.py +158 -0
- phoenix/experiments/evaluators/code_evaluators.py +184 -0
- phoenix/experiments/evaluators/llm_evaluators.py +473 -0
- phoenix/experiments/evaluators/utils.py +236 -0
- phoenix/experiments/functions.py +772 -0
- phoenix/experiments/tracing.py +86 -0
- phoenix/experiments/types.py +726 -0
- phoenix/experiments/utils.py +25 -0
- phoenix/inferences/__init__.py +0 -0
- phoenix/{datasets → inferences}/errors.py +6 -5
- phoenix/{datasets → inferences}/fixtures.py +49 -42
- phoenix/{datasets/dataset.py → inferences/inferences.py} +121 -105
- phoenix/{datasets → inferences}/schema.py +11 -11
- phoenix/{datasets → inferences}/validation.py +13 -14
- phoenix/logging/__init__.py +3 -0
- phoenix/logging/_config.py +90 -0
- phoenix/logging/_filter.py +6 -0
- phoenix/logging/_formatter.py +69 -0
- phoenix/metrics/__init__.py +5 -4
- phoenix/metrics/binning.py +4 -3
- phoenix/metrics/metrics.py +2 -1
- phoenix/metrics/mixins.py +7 -6
- phoenix/metrics/retrieval_metrics.py +2 -1
- phoenix/metrics/timeseries.py +5 -4
- phoenix/metrics/wrappers.py +9 -3
- phoenix/pointcloud/clustering.py +5 -5
- phoenix/pointcloud/pointcloud.py +7 -5
- phoenix/pointcloud/projectors.py +5 -6
- phoenix/pointcloud/umap_parameters.py +53 -52
- phoenix/server/api/README.md +28 -0
- phoenix/server/api/auth.py +44 -0
- phoenix/server/api/context.py +152 -9
- phoenix/server/api/dataloaders/__init__.py +91 -0
- phoenix/server/api/dataloaders/annotation_summaries.py +139 -0
- phoenix/server/api/dataloaders/average_experiment_run_latency.py +54 -0
- phoenix/server/api/dataloaders/cache/__init__.py +3 -0
- phoenix/server/api/dataloaders/cache/two_tier_cache.py +68 -0
- phoenix/server/api/dataloaders/dataset_example_revisions.py +131 -0
- phoenix/server/api/dataloaders/dataset_example_spans.py +38 -0
- phoenix/server/api/dataloaders/document_evaluation_summaries.py +144 -0
- phoenix/server/api/dataloaders/document_evaluations.py +31 -0
- phoenix/server/api/dataloaders/document_retrieval_metrics.py +89 -0
- phoenix/server/api/dataloaders/experiment_annotation_summaries.py +79 -0
- phoenix/server/api/dataloaders/experiment_error_rates.py +58 -0
- phoenix/server/api/dataloaders/experiment_run_annotations.py +36 -0
- phoenix/server/api/dataloaders/experiment_run_counts.py +49 -0
- phoenix/server/api/dataloaders/experiment_sequence_number.py +44 -0
- phoenix/server/api/dataloaders/latency_ms_quantile.py +188 -0
- phoenix/server/api/dataloaders/min_start_or_max_end_times.py +85 -0
- phoenix/server/api/dataloaders/project_by_name.py +31 -0
- phoenix/server/api/dataloaders/record_counts.py +116 -0
- phoenix/server/api/dataloaders/session_io.py +79 -0
- phoenix/server/api/dataloaders/session_num_traces.py +30 -0
- phoenix/server/api/dataloaders/session_num_traces_with_error.py +32 -0
- phoenix/server/api/dataloaders/session_token_usages.py +41 -0
- phoenix/server/api/dataloaders/session_trace_latency_ms_quantile.py +55 -0
- phoenix/server/api/dataloaders/span_annotations.py +26 -0
- phoenix/server/api/dataloaders/span_dataset_examples.py +31 -0
- phoenix/server/api/dataloaders/span_descendants.py +57 -0
- phoenix/server/api/dataloaders/span_projects.py +33 -0
- phoenix/server/api/dataloaders/token_counts.py +124 -0
- phoenix/server/api/dataloaders/trace_by_trace_ids.py +25 -0
- phoenix/server/api/dataloaders/trace_root_spans.py +32 -0
- phoenix/server/api/dataloaders/user_roles.py +30 -0
- phoenix/server/api/dataloaders/users.py +33 -0
- phoenix/server/api/exceptions.py +48 -0
- phoenix/server/api/helpers/__init__.py +12 -0
- phoenix/server/api/helpers/dataset_helpers.py +217 -0
- phoenix/server/api/helpers/experiment_run_filters.py +763 -0
- phoenix/server/api/helpers/playground_clients.py +948 -0
- phoenix/server/api/helpers/playground_registry.py +70 -0
- phoenix/server/api/helpers/playground_spans.py +455 -0
- phoenix/server/api/input_types/AddExamplesToDatasetInput.py +16 -0
- phoenix/server/api/input_types/AddSpansToDatasetInput.py +14 -0
- phoenix/server/api/input_types/ChatCompletionInput.py +38 -0
- phoenix/server/api/input_types/ChatCompletionMessageInput.py +24 -0
- phoenix/server/api/input_types/ClearProjectInput.py +15 -0
- phoenix/server/api/input_types/ClusterInput.py +2 -2
- phoenix/server/api/input_types/CreateDatasetInput.py +12 -0
- phoenix/server/api/input_types/CreateSpanAnnotationInput.py +18 -0
- phoenix/server/api/input_types/CreateTraceAnnotationInput.py +18 -0
- phoenix/server/api/input_types/DataQualityMetricInput.py +5 -2
- phoenix/server/api/input_types/DatasetExampleInput.py +14 -0
- phoenix/server/api/input_types/DatasetSort.py +17 -0
- phoenix/server/api/input_types/DatasetVersionSort.py +16 -0
- phoenix/server/api/input_types/DeleteAnnotationsInput.py +7 -0
- phoenix/server/api/input_types/DeleteDatasetExamplesInput.py +13 -0
- phoenix/server/api/input_types/DeleteDatasetInput.py +7 -0
- phoenix/server/api/input_types/DeleteExperimentsInput.py +7 -0
- phoenix/server/api/input_types/DimensionFilter.py +4 -4
- phoenix/server/api/input_types/GenerativeModelInput.py +17 -0
- phoenix/server/api/input_types/Granularity.py +1 -1
- phoenix/server/api/input_types/InvocationParameters.py +162 -0
- phoenix/server/api/input_types/PatchAnnotationInput.py +19 -0
- phoenix/server/api/input_types/PatchDatasetExamplesInput.py +35 -0
- phoenix/server/api/input_types/PatchDatasetInput.py +14 -0
- phoenix/server/api/input_types/PerformanceMetricInput.py +5 -2
- phoenix/server/api/input_types/ProjectSessionSort.py +29 -0
- phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
- phoenix/server/api/input_types/SpanSort.py +134 -69
- phoenix/server/api/input_types/TemplateOptions.py +10 -0
- phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
- phoenix/server/api/input_types/UserRoleInput.py +9 -0
- phoenix/server/api/mutations/__init__.py +28 -0
- phoenix/server/api/mutations/api_key_mutations.py +167 -0
- phoenix/server/api/mutations/chat_mutations.py +593 -0
- phoenix/server/api/mutations/dataset_mutations.py +591 -0
- phoenix/server/api/mutations/experiment_mutations.py +75 -0
- phoenix/server/api/{types/ExportEventsMutation.py → mutations/export_events_mutations.py} +21 -18
- phoenix/server/api/mutations/project_mutations.py +57 -0
- phoenix/server/api/mutations/span_annotations_mutations.py +128 -0
- phoenix/server/api/mutations/trace_annotations_mutations.py +127 -0
- phoenix/server/api/mutations/user_mutations.py +329 -0
- phoenix/server/api/openapi/__init__.py +0 -0
- phoenix/server/api/openapi/main.py +17 -0
- phoenix/server/api/openapi/schema.py +16 -0
- phoenix/server/api/queries.py +738 -0
- phoenix/server/api/routers/__init__.py +11 -0
- phoenix/server/api/routers/auth.py +284 -0
- phoenix/server/api/routers/embeddings.py +26 -0
- phoenix/server/api/routers/oauth2.py +488 -0
- phoenix/server/api/routers/v1/__init__.py +64 -0
- phoenix/server/api/routers/v1/datasets.py +1017 -0
- phoenix/server/api/routers/v1/evaluations.py +362 -0
- phoenix/server/api/routers/v1/experiment_evaluations.py +115 -0
- phoenix/server/api/routers/v1/experiment_runs.py +167 -0
- phoenix/server/api/routers/v1/experiments.py +308 -0
- phoenix/server/api/routers/v1/pydantic_compat.py +78 -0
- phoenix/server/api/routers/v1/spans.py +267 -0
- phoenix/server/api/routers/v1/traces.py +208 -0
- phoenix/server/api/routers/v1/utils.py +95 -0
- phoenix/server/api/schema.py +44 -247
- phoenix/server/api/subscriptions.py +597 -0
- phoenix/server/api/types/Annotation.py +21 -0
- phoenix/server/api/types/AnnotationSummary.py +55 -0
- phoenix/server/api/types/AnnotatorKind.py +16 -0
- phoenix/server/api/types/ApiKey.py +27 -0
- phoenix/server/api/types/AuthMethod.py +9 -0
- phoenix/server/api/types/ChatCompletionMessageRole.py +11 -0
- phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +46 -0
- phoenix/server/api/types/Cluster.py +25 -24
- phoenix/server/api/types/CreateDatasetPayload.py +8 -0
- phoenix/server/api/types/DataQualityMetric.py +31 -13
- phoenix/server/api/types/Dataset.py +288 -63
- phoenix/server/api/types/DatasetExample.py +85 -0
- phoenix/server/api/types/DatasetExampleRevision.py +34 -0
- phoenix/server/api/types/DatasetVersion.py +14 -0
- phoenix/server/api/types/Dimension.py +32 -31
- phoenix/server/api/types/DocumentEvaluationSummary.py +9 -8
- phoenix/server/api/types/EmbeddingDimension.py +56 -49
- phoenix/server/api/types/Evaluation.py +25 -31
- phoenix/server/api/types/EvaluationSummary.py +30 -50
- phoenix/server/api/types/Event.py +20 -20
- phoenix/server/api/types/ExampleRevisionInterface.py +14 -0
- phoenix/server/api/types/Experiment.py +152 -0
- phoenix/server/api/types/ExperimentAnnotationSummary.py +13 -0
- phoenix/server/api/types/ExperimentComparison.py +17 -0
- phoenix/server/api/types/ExperimentRun.py +119 -0
- phoenix/server/api/types/ExperimentRunAnnotation.py +56 -0
- phoenix/server/api/types/GenerativeModel.py +9 -0
- phoenix/server/api/types/GenerativeProvider.py +85 -0
- phoenix/server/api/types/Inferences.py +80 -0
- phoenix/server/api/types/InferencesRole.py +23 -0
- phoenix/server/api/types/LabelFraction.py +7 -0
- phoenix/server/api/types/MimeType.py +2 -2
- phoenix/server/api/types/Model.py +54 -54
- phoenix/server/api/types/PerformanceMetric.py +8 -5
- phoenix/server/api/types/Project.py +407 -142
- phoenix/server/api/types/ProjectSession.py +139 -0
- phoenix/server/api/types/Segments.py +4 -4
- phoenix/server/api/types/Span.py +221 -176
- phoenix/server/api/types/SpanAnnotation.py +43 -0
- phoenix/server/api/types/SpanIOValue.py +15 -0
- phoenix/server/api/types/SystemApiKey.py +9 -0
- phoenix/server/api/types/TemplateLanguage.py +10 -0
- phoenix/server/api/types/TimeSeries.py +19 -15
- phoenix/server/api/types/TokenUsage.py +11 -0
- phoenix/server/api/types/Trace.py +154 -0
- phoenix/server/api/types/TraceAnnotation.py +45 -0
- phoenix/server/api/types/UMAPPoints.py +7 -7
- phoenix/server/api/types/User.py +60 -0
- phoenix/server/api/types/UserApiKey.py +45 -0
- phoenix/server/api/types/UserRole.py +15 -0
- phoenix/server/api/types/node.py +13 -107
- phoenix/server/api/types/pagination.py +156 -57
- phoenix/server/api/utils.py +34 -0
- phoenix/server/app.py +864 -115
- phoenix/server/bearer_auth.py +163 -0
- phoenix/server/dml_event.py +136 -0
- phoenix/server/dml_event_handler.py +256 -0
- phoenix/server/email/__init__.py +0 -0
- phoenix/server/email/sender.py +97 -0
- phoenix/server/email/templates/__init__.py +0 -0
- phoenix/server/email/templates/password_reset.html +19 -0
- phoenix/server/email/types.py +11 -0
- phoenix/server/grpc_server.py +102 -0
- phoenix/server/jwt_store.py +505 -0
- phoenix/server/main.py +305 -116
- phoenix/server/oauth2.py +52 -0
- phoenix/server/openapi/__init__.py +0 -0
- phoenix/server/prometheus.py +111 -0
- phoenix/server/rate_limiters.py +188 -0
- phoenix/server/static/.vite/manifest.json +87 -0
- phoenix/server/static/assets/components-Cy9nwIvF.js +2125 -0
- phoenix/server/static/assets/index-BKvHIxkk.js +113 -0
- phoenix/server/static/assets/pages-CUi2xCVQ.js +4449 -0
- phoenix/server/static/assets/vendor-DvC8cT4X.js +894 -0
- phoenix/server/static/assets/vendor-DxkFTwjz.css +1 -0
- phoenix/server/static/assets/vendor-arizeai-Do1793cv.js +662 -0
- phoenix/server/static/assets/vendor-codemirror-BzwZPyJM.js +24 -0
- phoenix/server/static/assets/vendor-recharts-_Jb7JjhG.js +59 -0
- phoenix/server/static/assets/vendor-shiki-Cl9QBraO.js +5 -0
- phoenix/server/static/assets/vendor-three-DwGkEfCM.js +2998 -0
- phoenix/server/telemetry.py +68 -0
- phoenix/server/templates/index.html +82 -23
- phoenix/server/thread_server.py +3 -3
- phoenix/server/types.py +275 -0
- phoenix/services.py +27 -18
- phoenix/session/client.py +743 -68
- phoenix/session/data_extractor.py +31 -7
- phoenix/session/evaluation.py +3 -9
- phoenix/session/session.py +263 -219
- phoenix/settings.py +22 -0
- phoenix/trace/__init__.py +2 -22
- phoenix/trace/attributes.py +338 -0
- phoenix/trace/dsl/README.md +116 -0
- phoenix/trace/dsl/filter.py +663 -213
- phoenix/trace/dsl/helpers.py +73 -21
- phoenix/trace/dsl/query.py +574 -201
- phoenix/trace/exporter.py +24 -19
- phoenix/trace/fixtures.py +368 -32
- phoenix/trace/otel.py +71 -219
- phoenix/trace/projects.py +3 -2
- phoenix/trace/schemas.py +33 -11
- phoenix/trace/span_evaluations.py +21 -16
- phoenix/trace/span_json_decoder.py +6 -4
- phoenix/trace/span_json_encoder.py +2 -2
- phoenix/trace/trace_dataset.py +47 -32
- phoenix/trace/utils.py +21 -4
- phoenix/utilities/__init__.py +0 -26
- phoenix/utilities/client.py +132 -0
- phoenix/utilities/deprecation.py +31 -0
- phoenix/utilities/error_handling.py +3 -2
- phoenix/utilities/json.py +109 -0
- phoenix/utilities/logging.py +8 -0
- phoenix/utilities/project.py +2 -2
- phoenix/utilities/re.py +49 -0
- phoenix/utilities/span_store.py +0 -23
- phoenix/utilities/template_formatters.py +99 -0
- phoenix/version.py +1 -1
- arize_phoenix-3.16.0.dist-info/METADATA +0 -495
- arize_phoenix-3.16.0.dist-info/RECORD +0 -178
- phoenix/core/project.py +0 -617
- phoenix/core/traces.py +0 -100
- phoenix/experimental/evals/__init__.py +0 -73
- phoenix/experimental/evals/evaluators.py +0 -413
- phoenix/experimental/evals/functions/__init__.py +0 -4
- phoenix/experimental/evals/functions/classify.py +0 -453
- phoenix/experimental/evals/functions/executor.py +0 -353
- phoenix/experimental/evals/functions/generate.py +0 -138
- phoenix/experimental/evals/functions/processing.py +0 -76
- phoenix/experimental/evals/models/__init__.py +0 -14
- phoenix/experimental/evals/models/anthropic.py +0 -175
- phoenix/experimental/evals/models/base.py +0 -170
- phoenix/experimental/evals/models/bedrock.py +0 -221
- phoenix/experimental/evals/models/litellm.py +0 -134
- phoenix/experimental/evals/models/openai.py +0 -448
- phoenix/experimental/evals/models/rate_limiters.py +0 -246
- phoenix/experimental/evals/models/vertex.py +0 -173
- phoenix/experimental/evals/models/vertexai.py +0 -186
- phoenix/experimental/evals/retrievals.py +0 -96
- phoenix/experimental/evals/templates/__init__.py +0 -50
- phoenix/experimental/evals/templates/default_templates.py +0 -472
- phoenix/experimental/evals/templates/template.py +0 -195
- phoenix/experimental/evals/utils/__init__.py +0 -172
- phoenix/experimental/evals/utils/threads.py +0 -27
- phoenix/server/api/helpers.py +0 -11
- phoenix/server/api/routers/evaluation_handler.py +0 -109
- phoenix/server/api/routers/span_handler.py +0 -70
- phoenix/server/api/routers/trace_handler.py +0 -60
- phoenix/server/api/types/DatasetRole.py +0 -23
- phoenix/server/static/index.css +0 -6
- phoenix/server/static/index.js +0 -7447
- phoenix/storage/span_store/__init__.py +0 -23
- phoenix/storage/span_store/text_file.py +0 -85
- phoenix/trace/dsl/missing.py +0 -60
- phoenix/trace/langchain/__init__.py +0 -3
- phoenix/trace/langchain/instrumentor.py +0 -35
- phoenix/trace/llama_index/__init__.py +0 -3
- phoenix/trace/llama_index/callback.py +0 -102
- phoenix/trace/openai/__init__.py +0 -3
- phoenix/trace/openai/instrumentor.py +0 -30
- {arize_phoenix-3.16.0.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-3.16.0.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/LICENSE +0 -0
- /phoenix/{datasets → db/insertion}/__init__.py +0 -0
- /phoenix/{experimental → db/migrations}/__init__.py +0 -0
- /phoenix/{storage → db/migrations/data_migration_scripts}/__init__.py +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
from contextlib import AsyncExitStack
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Literal, Optional
|
|
5
|
+
|
|
6
|
+
import strawberry
|
|
7
|
+
from sqlalchemy import Boolean, Select, and_, case, cast, delete, distinct, func, select
|
|
8
|
+
from sqlalchemy.orm import joinedload
|
|
9
|
+
from sqlean.dbapi2 import IntegrityError # type: ignore[import-untyped]
|
|
10
|
+
from strawberry import UNSET
|
|
11
|
+
from strawberry.relay import GlobalID
|
|
12
|
+
from strawberry.types import Info
|
|
13
|
+
|
|
14
|
+
from phoenix.auth import (
|
|
15
|
+
DEFAULT_ADMIN_EMAIL,
|
|
16
|
+
DEFAULT_ADMIN_USERNAME,
|
|
17
|
+
DEFAULT_SECRET_LENGTH,
|
|
18
|
+
PASSWORD_REQUIREMENTS,
|
|
19
|
+
PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
|
|
20
|
+
PHOENIX_REFRESH_TOKEN_COOKIE_NAME,
|
|
21
|
+
validate_email_format,
|
|
22
|
+
validate_password_format,
|
|
23
|
+
)
|
|
24
|
+
from phoenix.db import enums, models
|
|
25
|
+
from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly
|
|
26
|
+
from phoenix.server.api.context import Context
|
|
27
|
+
from phoenix.server.api.exceptions import Conflict, NotFound, Unauthorized
|
|
28
|
+
from phoenix.server.api.input_types.UserRoleInput import UserRoleInput
|
|
29
|
+
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
30
|
+
from phoenix.server.api.types.User import User, to_gql_user
|
|
31
|
+
from phoenix.server.bearer_auth import PhoenixUser
|
|
32
|
+
from phoenix.server.types import AccessTokenId, ApiKeyId, PasswordResetTokenId, RefreshTokenId
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@strawberry.input
|
|
36
|
+
class CreateUserInput:
|
|
37
|
+
email: str
|
|
38
|
+
username: str
|
|
39
|
+
password: str
|
|
40
|
+
role: UserRoleInput
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@strawberry.input
|
|
44
|
+
class PatchViewerInput:
|
|
45
|
+
new_username: Optional[str] = UNSET
|
|
46
|
+
new_password: Optional[str] = UNSET
|
|
47
|
+
current_password: Optional[str] = UNSET
|
|
48
|
+
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
if not self.new_username and not self.new_password:
|
|
51
|
+
raise ValueError("At least one field must be set")
|
|
52
|
+
if self.new_password and not self.current_password:
|
|
53
|
+
raise ValueError("current_password is required when modifying password")
|
|
54
|
+
if self.new_password:
|
|
55
|
+
PASSWORD_REQUIREMENTS.validate(self.new_password)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@strawberry.input
|
|
59
|
+
class PatchUserInput:
|
|
60
|
+
user_id: GlobalID
|
|
61
|
+
new_role: Optional[UserRoleInput] = UNSET
|
|
62
|
+
new_username: Optional[str] = UNSET
|
|
63
|
+
new_password: Optional[str] = UNSET
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
if not self.new_role and not self.new_username and not self.new_password:
|
|
67
|
+
raise ValueError("At least one field must be set")
|
|
68
|
+
if self.new_password:
|
|
69
|
+
PASSWORD_REQUIREMENTS.validate(self.new_password)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@strawberry.input
|
|
73
|
+
class DeleteUsersInput:
|
|
74
|
+
user_ids: list[GlobalID]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@strawberry.type
|
|
78
|
+
class UserMutationPayload:
|
|
79
|
+
user: User
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@strawberry.type
|
|
83
|
+
class UserMutationMixin:
|
|
84
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
|
85
|
+
async def create_user(
|
|
86
|
+
self,
|
|
87
|
+
info: Info[Context, None],
|
|
88
|
+
input: CreateUserInput,
|
|
89
|
+
) -> UserMutationPayload:
|
|
90
|
+
validate_email_format(email := input.email)
|
|
91
|
+
validate_password_format(password := input.password)
|
|
92
|
+
salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
|
|
93
|
+
password_hash = await info.context.hash_password(password, salt)
|
|
94
|
+
user = models.User(
|
|
95
|
+
reset_password=True,
|
|
96
|
+
username=input.username,
|
|
97
|
+
email=email,
|
|
98
|
+
password_hash=password_hash,
|
|
99
|
+
password_salt=salt,
|
|
100
|
+
)
|
|
101
|
+
async with AsyncExitStack() as stack:
|
|
102
|
+
session = await stack.enter_async_context(info.context.db())
|
|
103
|
+
user_role_id = await session.scalar(_select_role_id_by_name(input.role.value))
|
|
104
|
+
if user_role_id is None:
|
|
105
|
+
raise NotFound(f"Role {input.role.value} not found")
|
|
106
|
+
stack.enter_context(session.no_autoflush)
|
|
107
|
+
user.user_role_id = user_role_id
|
|
108
|
+
session.add(user)
|
|
109
|
+
try:
|
|
110
|
+
await session.flush()
|
|
111
|
+
except IntegrityError as error:
|
|
112
|
+
raise Conflict(_user_operation_error_message(error))
|
|
113
|
+
return UserMutationPayload(user=to_gql_user(user))
|
|
114
|
+
|
|
115
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
|
116
|
+
async def patch_user(
|
|
117
|
+
self,
|
|
118
|
+
info: Info[Context, None],
|
|
119
|
+
input: PatchUserInput,
|
|
120
|
+
) -> UserMutationPayload:
|
|
121
|
+
assert (request := info.context.request)
|
|
122
|
+
assert isinstance(request.user, PhoenixUser)
|
|
123
|
+
assert (requester_id := int(request.user.identity))
|
|
124
|
+
user_id = from_global_id_with_expected_type(input.user_id, expected_type_name=User.__name__)
|
|
125
|
+
async with AsyncExitStack() as stack:
|
|
126
|
+
session = await stack.enter_async_context(info.context.db())
|
|
127
|
+
requester = await session.scalar(_select_user_by_id(requester_id))
|
|
128
|
+
assert requester
|
|
129
|
+
if not (user := await session.scalar(_select_user_by_id(user_id))):
|
|
130
|
+
raise NotFound("User not found")
|
|
131
|
+
stack.enter_context(session.no_autoflush)
|
|
132
|
+
if input.new_role:
|
|
133
|
+
if user.email == DEFAULT_ADMIN_EMAIL:
|
|
134
|
+
raise Unauthorized("Cannot modify role for the default admin user")
|
|
135
|
+
user_role_id = await session.scalar(_select_role_id_by_name(input.new_role.value))
|
|
136
|
+
if user_role_id is None:
|
|
137
|
+
raise NotFound(f"Role {input.new_role.value} not found")
|
|
138
|
+
user.user_role_id = user_role_id
|
|
139
|
+
if password := input.new_password:
|
|
140
|
+
if user.auth_method != enums.AuthMethod.LOCAL.value:
|
|
141
|
+
raise Conflict("Cannot modify password for non-local user")
|
|
142
|
+
validate_password_format(password)
|
|
143
|
+
user.password_salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
|
|
144
|
+
user.password_hash = await info.context.hash_password(password, user.password_salt)
|
|
145
|
+
user.reset_password = True
|
|
146
|
+
if username := input.new_username:
|
|
147
|
+
user.username = username
|
|
148
|
+
assert user in session.dirty
|
|
149
|
+
try:
|
|
150
|
+
await session.flush()
|
|
151
|
+
except IntegrityError as error:
|
|
152
|
+
raise Conflict(_user_operation_error_message(error, "modify"))
|
|
153
|
+
assert user
|
|
154
|
+
if input.new_password:
|
|
155
|
+
await info.context.log_out(user.id)
|
|
156
|
+
return UserMutationPayload(user=to_gql_user(user))
|
|
157
|
+
|
|
158
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
159
|
+
async def patch_viewer(
|
|
160
|
+
self,
|
|
161
|
+
info: Info[Context, None],
|
|
162
|
+
input: PatchViewerInput,
|
|
163
|
+
) -> UserMutationPayload:
|
|
164
|
+
assert (request := info.context.request)
|
|
165
|
+
assert isinstance(user := request.user, PhoenixUser)
|
|
166
|
+
user_id = int(user.identity)
|
|
167
|
+
async with AsyncExitStack() as stack:
|
|
168
|
+
session = await stack.enter_async_context(info.context.db())
|
|
169
|
+
if not (user := await session.scalar(_select_user_by_id(user_id))):
|
|
170
|
+
raise NotFound("User not found")
|
|
171
|
+
stack.enter_context(session.no_autoflush)
|
|
172
|
+
if password := input.new_password:
|
|
173
|
+
if user.auth_method != enums.AuthMethod.LOCAL.value:
|
|
174
|
+
raise Conflict("Cannot modify password for non-local user")
|
|
175
|
+
if not (
|
|
176
|
+
current_password := input.current_password
|
|
177
|
+
) or not await info.context.is_valid_password(current_password, user):
|
|
178
|
+
raise Conflict("Valid current password is required to modify password")
|
|
179
|
+
validate_password_format(password)
|
|
180
|
+
user.password_salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
|
|
181
|
+
user.password_hash = await info.context.hash_password(password, user.password_salt)
|
|
182
|
+
user.reset_password = False
|
|
183
|
+
if username := input.new_username:
|
|
184
|
+
user.username = username
|
|
185
|
+
assert user in session.dirty
|
|
186
|
+
user.updated_at = datetime.now(timezone.utc)
|
|
187
|
+
try:
|
|
188
|
+
await session.flush()
|
|
189
|
+
except IntegrityError as error:
|
|
190
|
+
raise Conflict(_user_operation_error_message(error, "modify"))
|
|
191
|
+
assert user
|
|
192
|
+
if input.new_password:
|
|
193
|
+
await info.context.log_out(user.id)
|
|
194
|
+
response = info.context.get_response()
|
|
195
|
+
response.delete_cookie(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)
|
|
196
|
+
response.delete_cookie(PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
|
|
197
|
+
return UserMutationPayload(user=to_gql_user(user))
|
|
198
|
+
|
|
199
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
|
200
|
+
async def delete_users(
|
|
201
|
+
self,
|
|
202
|
+
info: Info[Context, None],
|
|
203
|
+
input: DeleteUsersInput,
|
|
204
|
+
) -> None:
|
|
205
|
+
assert (token_store := info.context.token_store) is not None
|
|
206
|
+
if not input.user_ids:
|
|
207
|
+
return
|
|
208
|
+
user_ids = tuple(
|
|
209
|
+
map(
|
|
210
|
+
lambda gid: from_global_id_with_expected_type(gid, User.__name__),
|
|
211
|
+
set(input.user_ids),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
system_user_role_id = (
|
|
215
|
+
select(models.UserRole.id)
|
|
216
|
+
.where(models.UserRole.name == enums.UserRole.SYSTEM.value)
|
|
217
|
+
.scalar_subquery()
|
|
218
|
+
)
|
|
219
|
+
admin_user_role_id = (
|
|
220
|
+
select(models.UserRole.id)
|
|
221
|
+
.where(models.UserRole.name == enums.UserRole.ADMIN.value)
|
|
222
|
+
.scalar_subquery()
|
|
223
|
+
)
|
|
224
|
+
default_admin_user_id = (
|
|
225
|
+
select(models.User.id)
|
|
226
|
+
.where(
|
|
227
|
+
(
|
|
228
|
+
and_(
|
|
229
|
+
models.User.user_role_id == admin_user_role_id,
|
|
230
|
+
models.User.username == DEFAULT_ADMIN_USERNAME,
|
|
231
|
+
models.User.email == DEFAULT_ADMIN_EMAIL,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
.scalar_subquery()
|
|
236
|
+
)
|
|
237
|
+
async with info.context.db() as session:
|
|
238
|
+
[
|
|
239
|
+
(
|
|
240
|
+
deletes_default_admin,
|
|
241
|
+
num_resolved_user_ids,
|
|
242
|
+
)
|
|
243
|
+
] = (
|
|
244
|
+
await session.execute(
|
|
245
|
+
select(
|
|
246
|
+
cast(
|
|
247
|
+
func.coalesce(
|
|
248
|
+
func.max(
|
|
249
|
+
case((models.User.id == default_admin_user_id, 1), else_=0)
|
|
250
|
+
),
|
|
251
|
+
0,
|
|
252
|
+
),
|
|
253
|
+
Boolean,
|
|
254
|
+
).label("deletes_default_admin"),
|
|
255
|
+
func.count(distinct(models.User.id)).label("num_resolved_user_ids"),
|
|
256
|
+
)
|
|
257
|
+
.select_from(models.User)
|
|
258
|
+
.where(
|
|
259
|
+
and_(
|
|
260
|
+
models.User.id.in_(user_ids),
|
|
261
|
+
models.User.user_role_id != system_user_role_id,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
).all()
|
|
266
|
+
if deletes_default_admin:
|
|
267
|
+
raise Conflict("Cannot delete the default admin user")
|
|
268
|
+
if num_resolved_user_ids < len(user_ids):
|
|
269
|
+
raise NotFound("Some user IDs could not be found")
|
|
270
|
+
password_reset_token_ids = [
|
|
271
|
+
PasswordResetTokenId(id_)
|
|
272
|
+
async for id_ in await session.stream_scalars(
|
|
273
|
+
select(models.PasswordResetToken.id).where(
|
|
274
|
+
models.PasswordResetToken.user_id.in_(user_ids)
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
]
|
|
278
|
+
access_token_ids = [
|
|
279
|
+
AccessTokenId(id_)
|
|
280
|
+
async for id_ in await session.stream_scalars(
|
|
281
|
+
select(models.AccessToken.id).where(models.AccessToken.user_id.in_(user_ids))
|
|
282
|
+
)
|
|
283
|
+
]
|
|
284
|
+
refresh_token_ids = [
|
|
285
|
+
RefreshTokenId(id_)
|
|
286
|
+
async for id_ in await session.stream_scalars(
|
|
287
|
+
select(models.RefreshToken.id).where(models.RefreshToken.user_id.in_(user_ids))
|
|
288
|
+
)
|
|
289
|
+
]
|
|
290
|
+
api_key_ids = [
|
|
291
|
+
ApiKeyId(id_)
|
|
292
|
+
async for id_ in await session.stream_scalars(
|
|
293
|
+
select(models.ApiKey.id).where(models.ApiKey.user_id.in_(user_ids))
|
|
294
|
+
)
|
|
295
|
+
]
|
|
296
|
+
await session.execute(delete(models.User).where(models.User.id.in_(user_ids)))
|
|
297
|
+
await token_store.revoke(
|
|
298
|
+
*password_reset_token_ids,
|
|
299
|
+
*access_token_ids,
|
|
300
|
+
*refresh_token_ids,
|
|
301
|
+
*api_key_ids,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _select_role_id_by_name(role_name: str) -> Select[tuple[int]]:
|
|
306
|
+
return select(models.UserRole.id).where(models.UserRole.name == role_name)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _select_user_by_id(user_id: int) -> Select[tuple[models.User]]:
|
|
310
|
+
return (
|
|
311
|
+
select(models.User).where(models.User.id == user_id).options(joinedload(models.User.role))
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _user_operation_error_message(
|
|
316
|
+
error: IntegrityError,
|
|
317
|
+
operation: Literal["create", "modify"] = "create",
|
|
318
|
+
) -> str:
|
|
319
|
+
"""
|
|
320
|
+
User-facing error message to explain why user creation/modification failed.
|
|
321
|
+
"""
|
|
322
|
+
original_error_message = str(error)
|
|
323
|
+
username_already_exists = "users.username" in original_error_message
|
|
324
|
+
email_already_exists = "users.email" in original_error_message
|
|
325
|
+
if username_already_exists:
|
|
326
|
+
return "Username already exists"
|
|
327
|
+
elif email_already_exists:
|
|
328
|
+
return "Email already exists"
|
|
329
|
+
return f"Failed to {operation} user"
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from argparse import ArgumentParser
|
|
3
|
+
|
|
4
|
+
from phoenix.server.api.openapi.schema import get_openapi_schema
|
|
5
|
+
|
|
6
|
+
if __name__ == "__main__":
|
|
7
|
+
parser = ArgumentParser()
|
|
8
|
+
parser.add_argument(
|
|
9
|
+
"-o",
|
|
10
|
+
"--output",
|
|
11
|
+
type=str,
|
|
12
|
+
required=True,
|
|
13
|
+
help="Path to the output file (e.g., openapi.json)",
|
|
14
|
+
)
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
with open(args.output, "w") as f:
|
|
17
|
+
json.dump(get_openapi_schema(), f, indent=2)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi.openapi.utils import get_openapi
|
|
4
|
+
|
|
5
|
+
from phoenix.server.api.routers.v1 import REST_API_VERSION, create_v1_router
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_openapi_schema() -> dict[str, Any]:
|
|
9
|
+
v1_router = create_v1_router(authentication_enabled=False)
|
|
10
|
+
return get_openapi(
|
|
11
|
+
title="Arize-Phoenix REST API",
|
|
12
|
+
version=REST_API_VERSION,
|
|
13
|
+
openapi_version="3.1.0",
|
|
14
|
+
description="Schema for Arize-Phoenix REST API",
|
|
15
|
+
routes=v1_router.routes,
|
|
16
|
+
)
|