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,163 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from collections.abc import Awaitable, Callable
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import Any, Optional, cast
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
from fastapi import HTTPException, Request, WebSocket, WebSocketException
|
|
9
|
+
from grpc_interceptor import AsyncServerInterceptor
|
|
10
|
+
from grpc_interceptor.exceptions import Unauthenticated
|
|
11
|
+
from starlette.authentication import AuthCredentials, AuthenticationBackend, BaseUser
|
|
12
|
+
from starlette.requests import HTTPConnection
|
|
13
|
+
from starlette.status import HTTP_401_UNAUTHORIZED
|
|
14
|
+
|
|
15
|
+
from phoenix.auth import (
|
|
16
|
+
PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
|
|
17
|
+
CanReadToken,
|
|
18
|
+
ClaimSetStatus,
|
|
19
|
+
Token,
|
|
20
|
+
)
|
|
21
|
+
from phoenix.db import enums
|
|
22
|
+
from phoenix.db.enums import UserRole
|
|
23
|
+
from phoenix.db.models import User as OrmUser
|
|
24
|
+
from phoenix.server.types import (
|
|
25
|
+
AccessToken,
|
|
26
|
+
AccessTokenAttributes,
|
|
27
|
+
AccessTokenClaims,
|
|
28
|
+
ApiKeyClaims,
|
|
29
|
+
RefreshToken,
|
|
30
|
+
RefreshTokenAttributes,
|
|
31
|
+
RefreshTokenClaims,
|
|
32
|
+
TokenStore,
|
|
33
|
+
UserClaimSet,
|
|
34
|
+
UserId,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HasTokenStore(ABC):
|
|
39
|
+
def __init__(self, token_store: CanReadToken) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
self._token_store = token_store
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BearerTokenAuthBackend(HasTokenStore, AuthenticationBackend):
|
|
45
|
+
async def authenticate(
|
|
46
|
+
self,
|
|
47
|
+
conn: HTTPConnection,
|
|
48
|
+
) -> Optional[tuple[AuthCredentials, BaseUser]]:
|
|
49
|
+
if header := conn.headers.get("Authorization"):
|
|
50
|
+
scheme, _, token = header.partition(" ")
|
|
51
|
+
if scheme.lower() != "bearer" or not token:
|
|
52
|
+
return None
|
|
53
|
+
elif access_token := conn.cookies.get(PHOENIX_ACCESS_TOKEN_COOKIE_NAME):
|
|
54
|
+
token = access_token
|
|
55
|
+
else:
|
|
56
|
+
return None
|
|
57
|
+
claims = await self._token_store.read(Token(token))
|
|
58
|
+
if not (isinstance(claims, UserClaimSet) and isinstance(claims.subject, UserId)):
|
|
59
|
+
return None
|
|
60
|
+
if not isinstance(claims, (ApiKeyClaims, AccessTokenClaims)):
|
|
61
|
+
return None
|
|
62
|
+
return AuthCredentials(), PhoenixUser(claims.subject, claims)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PhoenixUser(BaseUser):
|
|
66
|
+
def __init__(self, user_id: UserId, claims: UserClaimSet) -> None:
|
|
67
|
+
self._user_id = user_id
|
|
68
|
+
self.claims = claims
|
|
69
|
+
assert claims.attributes
|
|
70
|
+
self._is_admin = (
|
|
71
|
+
claims.status is ClaimSetStatus.VALID
|
|
72
|
+
and claims.attributes.user_role == enums.UserRole.ADMIN
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@cached_property
|
|
76
|
+
def is_admin(self) -> bool:
|
|
77
|
+
return self._is_admin
|
|
78
|
+
|
|
79
|
+
@cached_property
|
|
80
|
+
def identity(self) -> UserId:
|
|
81
|
+
return self._user_id
|
|
82
|
+
|
|
83
|
+
@cached_property
|
|
84
|
+
def is_authenticated(self) -> bool:
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ApiKeyInterceptor(HasTokenStore, AsyncServerInterceptor):
|
|
89
|
+
async def intercept(
|
|
90
|
+
self,
|
|
91
|
+
method: Callable[[Any, grpc.ServicerContext], Awaitable[Any]],
|
|
92
|
+
request_or_iterator: Any,
|
|
93
|
+
context: grpc.ServicerContext,
|
|
94
|
+
method_name: str,
|
|
95
|
+
) -> Any:
|
|
96
|
+
for datum in context.invocation_metadata():
|
|
97
|
+
if datum.key.lower() == "authorization":
|
|
98
|
+
scheme, _, token = datum.value.partition(" ")
|
|
99
|
+
if scheme.lower() != "bearer" or not token:
|
|
100
|
+
break
|
|
101
|
+
claims = await self._token_store.read(Token(token))
|
|
102
|
+
if not (isinstance(claims, UserClaimSet) and isinstance(claims.subject, UserId)):
|
|
103
|
+
break
|
|
104
|
+
if not isinstance(claims, (ApiKeyClaims, AccessTokenClaims)):
|
|
105
|
+
raise Unauthenticated(details="Invalid token")
|
|
106
|
+
if claims.status is ClaimSetStatus.EXPIRED:
|
|
107
|
+
raise Unauthenticated(details="Expired token")
|
|
108
|
+
if claims.status is ClaimSetStatus.VALID:
|
|
109
|
+
return await method(request_or_iterator, context)
|
|
110
|
+
raise Unauthenticated()
|
|
111
|
+
raise Unauthenticated()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def is_authenticated(
|
|
115
|
+
# fastapi dependencies require non-optional types
|
|
116
|
+
request: Request = cast(Request, None),
|
|
117
|
+
websocket: WebSocket = cast(WebSocket, None),
|
|
118
|
+
) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Raises a 401 if the request or websocket connection is not authenticated.
|
|
121
|
+
"""
|
|
122
|
+
assert request or websocket
|
|
123
|
+
if request and not isinstance((user := request.user), PhoenixUser):
|
|
124
|
+
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
|
125
|
+
if websocket and not isinstance((user := websocket.user), PhoenixUser):
|
|
126
|
+
raise WebSocketException(code=HTTP_401_UNAUTHORIZED, reason="Invalid token")
|
|
127
|
+
claims = user.claims
|
|
128
|
+
if claims.status is ClaimSetStatus.EXPIRED:
|
|
129
|
+
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Expired token")
|
|
130
|
+
if claims.status is not ClaimSetStatus.VALID:
|
|
131
|
+
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def create_access_and_refresh_tokens(
|
|
135
|
+
*,
|
|
136
|
+
token_store: TokenStore,
|
|
137
|
+
user: OrmUser,
|
|
138
|
+
access_token_expiry: timedelta,
|
|
139
|
+
refresh_token_expiry: timedelta,
|
|
140
|
+
) -> tuple[AccessToken, RefreshToken]:
|
|
141
|
+
issued_at = datetime.now(timezone.utc)
|
|
142
|
+
user_id = UserId(user.id)
|
|
143
|
+
user_role = UserRole(user.role.name)
|
|
144
|
+
refresh_token_claims = RefreshTokenClaims(
|
|
145
|
+
subject=user_id,
|
|
146
|
+
issued_at=issued_at,
|
|
147
|
+
expiration_time=issued_at + refresh_token_expiry,
|
|
148
|
+
attributes=RefreshTokenAttributes(
|
|
149
|
+
user_role=user_role,
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
refresh_token, refresh_token_id = await token_store.create_refresh_token(refresh_token_claims)
|
|
153
|
+
access_token_claims = AccessTokenClaims(
|
|
154
|
+
subject=user_id,
|
|
155
|
+
issued_at=issued_at,
|
|
156
|
+
expiration_time=issued_at + access_token_expiry,
|
|
157
|
+
attributes=AccessTokenAttributes(
|
|
158
|
+
user_role=user_role,
|
|
159
|
+
refresh_token_id=refresh_token_id,
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
access_token, _ = await token_store.create_access_token(access_token_claims)
|
|
163
|
+
return access_token, refresh_token
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from phoenix.db import models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class DmlEvent(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Event corresponding to a Data Manipulation Language (DML)
|
|
14
|
+
operation, e.g. insertion, update, or deletion.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
table: ClassVar[type[models.Base]]
|
|
18
|
+
ids: tuple[int, ...] = field(default_factory=tuple)
|
|
19
|
+
|
|
20
|
+
def __bool__(self) -> bool:
|
|
21
|
+
return bool(self.ids)
|
|
22
|
+
|
|
23
|
+
def __hash__(self) -> int:
|
|
24
|
+
return id(self)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ProjectDmlEvent(DmlEvent):
|
|
29
|
+
table = models.Project
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ProjectDeleteEvent(ProjectDmlEvent): ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class SpanDmlEvent(ProjectDmlEvent): ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class SpanInsertEvent(SpanDmlEvent): ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class SpanDeleteEvent(SpanDmlEvent): ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class DatasetDmlEvent(DmlEvent):
|
|
50
|
+
table = models.Dataset
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class DatasetInsertEvent(DatasetDmlEvent): ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class DatasetDeleteEvent(DatasetDmlEvent): ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class ExperimentDmlEvent(DmlEvent):
|
|
63
|
+
table = models.Experiment
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class ExperimentInsertEvent(ExperimentDmlEvent): ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class ExperimentDeleteEvent(ExperimentDmlEvent): ...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class ExperimentRunDmlEvent(DmlEvent):
|
|
76
|
+
table = models.ExperimentRun
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ExperimentRunInsertEvent(ExperimentRunDmlEvent): ...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class ExperimentRunDeleteEvent(ExperimentRunDmlEvent): ...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class ExperimentRunAnnotationDmlEvent(DmlEvent):
|
|
89
|
+
table = models.ExperimentRunAnnotation
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class ExperimentRunAnnotationInsertEvent(ExperimentRunAnnotationDmlEvent): ...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class ExperimentRunAnnotationDeleteEvent(ExperimentRunAnnotationDmlEvent): ...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class SpanAnnotationDmlEvent(DmlEvent):
|
|
102
|
+
table = models.SpanAnnotation
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class SpanAnnotationInsertEvent(SpanAnnotationDmlEvent): ...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class SpanAnnotationDeleteEvent(SpanAnnotationDmlEvent): ...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class TraceAnnotationDmlEvent(DmlEvent):
|
|
115
|
+
table = models.TraceAnnotation
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class TraceAnnotationInsertEvent(TraceAnnotationDmlEvent): ...
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class TraceAnnotationDeleteEvent(TraceAnnotationDmlEvent): ...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class DocumentAnnotationDmlEvent(DmlEvent):
|
|
128
|
+
table = models.DocumentAnnotation
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class DocumentAnnotationInsertEvent(DocumentAnnotationDmlEvent): ...
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class DocumentAnnotationDeleteEvent(DocumentAnnotationDmlEvent): ...
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from asyncio import gather
|
|
5
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
6
|
+
from inspect import getmro
|
|
7
|
+
from itertools import chain
|
|
8
|
+
from typing import Any, Generic, Optional, TypedDict, TypeVar, Union, cast
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import Select, select
|
|
11
|
+
from typing_extensions import TypeAlias, Unpack
|
|
12
|
+
|
|
13
|
+
from phoenix.db.models import (
|
|
14
|
+
Base,
|
|
15
|
+
DocumentAnnotation,
|
|
16
|
+
Project,
|
|
17
|
+
Span,
|
|
18
|
+
SpanAnnotation,
|
|
19
|
+
Trace,
|
|
20
|
+
TraceAnnotation,
|
|
21
|
+
)
|
|
22
|
+
from phoenix.server.api.dataloaders import CacheForDataLoaders
|
|
23
|
+
from phoenix.server.dml_event import (
|
|
24
|
+
DmlEvent,
|
|
25
|
+
DocumentAnnotationDmlEvent,
|
|
26
|
+
SpanAnnotationDmlEvent,
|
|
27
|
+
SpanDeleteEvent,
|
|
28
|
+
SpanDmlEvent,
|
|
29
|
+
TraceAnnotationDmlEvent,
|
|
30
|
+
)
|
|
31
|
+
from phoenix.server.types import (
|
|
32
|
+
BatchedCaller,
|
|
33
|
+
CanSetLastUpdatedAt,
|
|
34
|
+
DbSessionFactory,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_DmlEventT = TypeVar("_DmlEventT", bound=DmlEvent)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _DmlEventQueue(Generic[_DmlEventT]):
|
|
41
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
42
|
+
super().__init__(**kwargs)
|
|
43
|
+
self._events: set[_DmlEventT] = set()
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def empty(self) -> bool:
|
|
47
|
+
return not self._events
|
|
48
|
+
|
|
49
|
+
def put(self, event: _DmlEventT) -> None:
|
|
50
|
+
self._events.add(event)
|
|
51
|
+
|
|
52
|
+
def clear(self) -> None:
|
|
53
|
+
self._events.clear()
|
|
54
|
+
|
|
55
|
+
def __iter__(self) -> Iterator[_DmlEventT]:
|
|
56
|
+
yield from self._events
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _HandlerParams(TypedDict):
|
|
60
|
+
db: DbSessionFactory
|
|
61
|
+
last_updated_at: CanSetLastUpdatedAt
|
|
62
|
+
cache_for_dataloaders: Optional[CacheForDataLoaders]
|
|
63
|
+
sleep_seconds: float
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _HasLastUpdatedAt(ABC):
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
last_updated_at: CanSetLastUpdatedAt,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> None:
|
|
72
|
+
super().__init__(**kwargs)
|
|
73
|
+
self._last_updated_at = last_updated_at
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _HasCacheForDataLoaders(ABC):
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
|
|
80
|
+
**kwargs: Any,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(**kwargs)
|
|
83
|
+
self._cache_for_dataloaders = cache_for_dataloaders
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _DmlEventHandler(
|
|
87
|
+
_HasLastUpdatedAt,
|
|
88
|
+
_HasCacheForDataLoaders,
|
|
89
|
+
BatchedCaller[_DmlEventT],
|
|
90
|
+
Generic[_DmlEventT],
|
|
91
|
+
ABC,
|
|
92
|
+
):
|
|
93
|
+
_batch_factory = cast(Callable[[], _DmlEventQueue[_DmlEventT]], _DmlEventQueue)
|
|
94
|
+
|
|
95
|
+
def __init__(self, *, db: DbSessionFactory, **kwargs: Any) -> None:
|
|
96
|
+
super().__init__(**kwargs)
|
|
97
|
+
self._db = db
|
|
98
|
+
|
|
99
|
+
def __hash__(self) -> int:
|
|
100
|
+
return id(self)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _GenericDmlEventHandler(_DmlEventHandler[DmlEvent]):
|
|
104
|
+
async def __call__(self) -> None:
|
|
105
|
+
for e in self._batch:
|
|
106
|
+
for id_ in e.ids:
|
|
107
|
+
self._update(e.table, id_)
|
|
108
|
+
|
|
109
|
+
def _update(self, table: type[Base], id_: int) -> None:
|
|
110
|
+
self._last_updated_at.set(table, id_)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _SpanDmlEventHandler(_DmlEventHandler[SpanDmlEvent]):
|
|
114
|
+
async def __call__(self) -> None:
|
|
115
|
+
if cache := self._cache_for_dataloaders:
|
|
116
|
+
for id_ in set(chain.from_iterable(e.ids for e in self._batch)):
|
|
117
|
+
self._clear(cache, id_)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _clear(cache: CacheForDataLoaders, project_id: int) -> None:
|
|
121
|
+
cache.latency_ms_quantile.invalidate(project_id)
|
|
122
|
+
cache.token_count.invalidate(project_id)
|
|
123
|
+
cache.record_count.invalidate(project_id)
|
|
124
|
+
cache.min_start_or_max_end_time.invalidate(project_id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _SpanDeleteEventHandler(_SpanDmlEventHandler):
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _clear(cache: CacheForDataLoaders, project_id: int) -> None:
|
|
130
|
+
cache.annotation_summary.invalidate_project(project_id)
|
|
131
|
+
cache.document_evaluation_summary.invalidate_project(project_id)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_AnnotationTable: TypeAlias = Union[
|
|
135
|
+
type[SpanAnnotation],
|
|
136
|
+
type[TraceAnnotation],
|
|
137
|
+
type[DocumentAnnotation],
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
_AnnotationDmlEventT = TypeVar(
|
|
141
|
+
"_AnnotationDmlEventT",
|
|
142
|
+
SpanAnnotationDmlEvent,
|
|
143
|
+
TraceAnnotationDmlEvent,
|
|
144
|
+
DocumentAnnotationDmlEvent,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class _AnnotationDmlEventHandler(
|
|
149
|
+
_DmlEventHandler[_AnnotationDmlEventT],
|
|
150
|
+
Generic[_AnnotationDmlEventT],
|
|
151
|
+
ABC,
|
|
152
|
+
):
|
|
153
|
+
_table: _AnnotationTable
|
|
154
|
+
_base_stmt: Union[Select[tuple[int, str]], Select[tuple[int]]] = (
|
|
155
|
+
select(Project.id).join_from(Project, Trace).distinct()
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def __init__(self, **kwargs: Unpack[_HandlerParams]) -> None:
|
|
159
|
+
super().__init__(**kwargs)
|
|
160
|
+
self._stmt = self._base_stmt
|
|
161
|
+
if self._cache_for_dataloaders:
|
|
162
|
+
self._stmt = self._stmt.add_columns(self._table.name)
|
|
163
|
+
|
|
164
|
+
def _get_stmt(self) -> Union[Select[tuple[int, str]], Select[tuple[int]]]:
|
|
165
|
+
ids = set(chain.from_iterable(e.ids for e in self._batch))
|
|
166
|
+
return self._stmt.where(self._table.id.in_(ids))
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
@abstractmethod
|
|
170
|
+
def _clear(cache: CacheForDataLoaders, project_id: int, name: str) -> None: ...
|
|
171
|
+
|
|
172
|
+
async def __call__(self) -> None:
|
|
173
|
+
async with self._db() as session:
|
|
174
|
+
async for row in await session.stream(self._get_stmt()):
|
|
175
|
+
self._last_updated_at.set(Project, row.id)
|
|
176
|
+
if cache := self._cache_for_dataloaders:
|
|
177
|
+
self._clear(cache, row.id, row.name)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class _SpanAnnotationDmlEventHandler(_AnnotationDmlEventHandler[SpanAnnotationDmlEvent]):
|
|
181
|
+
_table = SpanAnnotation
|
|
182
|
+
|
|
183
|
+
def __init__(self, **kwargs: Unpack[_HandlerParams]) -> None:
|
|
184
|
+
super().__init__(**kwargs)
|
|
185
|
+
self._stmt = self._stmt.join_from(Trace, Span).join_from(Span, self._table)
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _clear(cache: CacheForDataLoaders, project_id: int, name: str) -> None:
|
|
189
|
+
cache.annotation_summary.invalidate((project_id, name, "span"))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class _TraceAnnotationDmlEventHandler(_AnnotationDmlEventHandler[TraceAnnotationDmlEvent]):
|
|
193
|
+
_table = TraceAnnotation
|
|
194
|
+
|
|
195
|
+
def __init__(self, **kwargs: Unpack[_HandlerParams]) -> None:
|
|
196
|
+
super().__init__(**kwargs)
|
|
197
|
+
self._stmt = self._stmt.join_from(Trace, self._table)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _clear(cache: CacheForDataLoaders, project_id: int, name: str) -> None:
|
|
201
|
+
cache.annotation_summary.invalidate((project_id, name, "trace"))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class _DocumentAnnotationDmlEventHandler(_AnnotationDmlEventHandler[DocumentAnnotationDmlEvent]):
|
|
205
|
+
_table = DocumentAnnotation
|
|
206
|
+
|
|
207
|
+
def __init__(self, **kwargs: Unpack[_HandlerParams]) -> None:
|
|
208
|
+
super().__init__(**kwargs)
|
|
209
|
+
self._stmt = self._stmt.join_from(Trace, Span).join_from(Span, self._table)
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _clear(cache: CacheForDataLoaders, project_id: int, name: str) -> None:
|
|
213
|
+
cache.document_evaluation_summary.invalidate((project_id, name))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class DmlEventHandler:
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
*,
|
|
220
|
+
db: DbSessionFactory,
|
|
221
|
+
last_updated_at: CanSetLastUpdatedAt,
|
|
222
|
+
cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
|
|
223
|
+
sleep_seconds: float = 0.1,
|
|
224
|
+
) -> None:
|
|
225
|
+
kwargs = _HandlerParams(
|
|
226
|
+
db=db,
|
|
227
|
+
last_updated_at=last_updated_at,
|
|
228
|
+
cache_for_dataloaders=cache_for_dataloaders,
|
|
229
|
+
sleep_seconds=sleep_seconds,
|
|
230
|
+
)
|
|
231
|
+
self._handlers: Mapping[type[DmlEvent], Iterable[_DmlEventHandler[Any]]] = {
|
|
232
|
+
DmlEvent: [_GenericDmlEventHandler(**kwargs)],
|
|
233
|
+
SpanDmlEvent: [_SpanDmlEventHandler(**kwargs)],
|
|
234
|
+
SpanDeleteEvent: [_SpanDeleteEventHandler(**kwargs)],
|
|
235
|
+
SpanAnnotationDmlEvent: [_SpanAnnotationDmlEventHandler(**kwargs)],
|
|
236
|
+
TraceAnnotationDmlEvent: [_TraceAnnotationDmlEventHandler(**kwargs)],
|
|
237
|
+
DocumentAnnotationDmlEvent: [_DocumentAnnotationDmlEventHandler(**kwargs)],
|
|
238
|
+
}
|
|
239
|
+
self._all_handlers = frozenset(chain.from_iterable(self._handlers.values()))
|
|
240
|
+
|
|
241
|
+
async def __aenter__(self) -> None:
|
|
242
|
+
await gather(*(h.start() for h in self._all_handlers))
|
|
243
|
+
|
|
244
|
+
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
|
245
|
+
await gather(*(h.stop() for h in self._all_handlers))
|
|
246
|
+
|
|
247
|
+
def put(self, event: DmlEvent) -> None:
|
|
248
|
+
if not (isinstance(event, DmlEvent) and event):
|
|
249
|
+
return
|
|
250
|
+
for cls in getmro(type(event)):
|
|
251
|
+
if not (issubclass(cls, DmlEvent) and (handlers := self._handlers.get(cls))):
|
|
252
|
+
continue
|
|
253
|
+
for h in handlers:
|
|
254
|
+
h.put(event)
|
|
255
|
+
if cls is DmlEvent:
|
|
256
|
+
break
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import smtplib
|
|
3
|
+
import ssl
|
|
4
|
+
from email.message import EmailMessage
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
9
|
+
|
|
10
|
+
EMAIL_TEMPLATE_FOLDER = Path(__file__).parent / "templates"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SimpleEmailSender:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
smtp_server: str,
|
|
17
|
+
smtp_port: int,
|
|
18
|
+
username: str,
|
|
19
|
+
password: str,
|
|
20
|
+
sender_email: str,
|
|
21
|
+
connection_method: Literal["STARTTLS", "SSL", "PLAIN"] = "STARTTLS",
|
|
22
|
+
validate_certs: bool = True,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.smtp_server = smtp_server
|
|
25
|
+
self.smtp_port = smtp_port
|
|
26
|
+
self.username = username
|
|
27
|
+
self.password = password
|
|
28
|
+
self.sender_email = sender_email
|
|
29
|
+
self.connection_method = connection_method.upper()
|
|
30
|
+
self.validate_certs = validate_certs
|
|
31
|
+
|
|
32
|
+
self.env = Environment(
|
|
33
|
+
loader=FileSystemLoader(EMAIL_TEMPLATE_FOLDER),
|
|
34
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
async def send_password_reset_email(
|
|
38
|
+
self,
|
|
39
|
+
email: str,
|
|
40
|
+
reset_url: str,
|
|
41
|
+
) -> None:
|
|
42
|
+
subject = "[Phoenix] Password Reset Request"
|
|
43
|
+
template_name = "password_reset.html"
|
|
44
|
+
|
|
45
|
+
template = self.env.get_template(template_name)
|
|
46
|
+
html_content = template.render(reset_url=reset_url)
|
|
47
|
+
|
|
48
|
+
msg = EmailMessage()
|
|
49
|
+
msg["Subject"] = subject
|
|
50
|
+
msg["From"] = self.sender_email
|
|
51
|
+
msg["To"] = email
|
|
52
|
+
msg.set_content(html_content, subtype="html")
|
|
53
|
+
|
|
54
|
+
def send_email() -> None:
|
|
55
|
+
context: ssl.SSLContext
|
|
56
|
+
if self.validate_certs:
|
|
57
|
+
context = ssl.create_default_context()
|
|
58
|
+
else:
|
|
59
|
+
context = ssl._create_unverified_context()
|
|
60
|
+
|
|
61
|
+
methods_to_try = [self.connection_method]
|
|
62
|
+
# add secure method fallbacks
|
|
63
|
+
if self.connection_method != "PLAIN":
|
|
64
|
+
if self.connection_method != "STARTTLS":
|
|
65
|
+
methods_to_try.append("STARTTLS")
|
|
66
|
+
elif self.connection_method != "SSL":
|
|
67
|
+
methods_to_try.append("SSL")
|
|
68
|
+
|
|
69
|
+
for method in methods_to_try:
|
|
70
|
+
try:
|
|
71
|
+
if method == "STARTTLS":
|
|
72
|
+
server = smtplib.SMTP(self.smtp_server, self.smtp_port)
|
|
73
|
+
server.ehlo()
|
|
74
|
+
server.starttls(context=context)
|
|
75
|
+
server.ehlo()
|
|
76
|
+
elif method == "SSL":
|
|
77
|
+
server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port, context=context)
|
|
78
|
+
server.ehlo()
|
|
79
|
+
elif method == "PLAIN":
|
|
80
|
+
server = smtplib.SMTP(self.smtp_server, self.smtp_port)
|
|
81
|
+
server.ehlo()
|
|
82
|
+
else:
|
|
83
|
+
continue # Unsupported method
|
|
84
|
+
|
|
85
|
+
if self.username and self.password:
|
|
86
|
+
server.login(self.username, self.password)
|
|
87
|
+
|
|
88
|
+
server.send_message(msg)
|
|
89
|
+
server.quit()
|
|
90
|
+
break # Success
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"Failed to send email using {method}: {e}")
|
|
93
|
+
continue
|
|
94
|
+
else:
|
|
95
|
+
raise Exception("All connection methods failed")
|
|
96
|
+
|
|
97
|
+
await asyncio.to_thread(send_email)
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Password Reset</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<p>Hello.</p>
|
|
9
|
+
<p>
|
|
10
|
+
You have requested a password reset. Please click on the link below to
|
|
11
|
+
reset your password:
|
|
12
|
+
</p>
|
|
13
|
+
<p>
|
|
14
|
+
<a id="reset-url" href="{{ reset_url }}">Reset Password</a
|
|
15
|
+
>
|
|
16
|
+
</p>
|
|
17
|
+
<p>If you did not make this request, please contact your administrator.</p>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|