arize-phoenix 11.23.1__py3-none-any.whl → 12.28.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +61 -36
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/RECORD +212 -162
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
- {arize_phoenix-11.23.1.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 +2 -1
- phoenix/auth.py +27 -2
- phoenix/config.py +1594 -81
- phoenix/db/README.md +546 -28
- phoenix/db/bulk_inserter.py +119 -116
- phoenix/db/engines.py +140 -33
- phoenix/db/facilitator.py +22 -1
- phoenix/db/helpers.py +818 -65
- phoenix/db/iam_auth.py +64 -0
- phoenix/db/insertion/dataset.py +133 -1
- phoenix/db/insertion/document_annotation.py +9 -6
- phoenix/db/insertion/evaluation.py +2 -3
- phoenix/db/insertion/helpers.py +2 -2
- phoenix/db/insertion/session_annotation.py +176 -0
- phoenix/db/insertion/span_annotation.py +3 -4
- phoenix/db/insertion/trace_annotation.py +3 -4
- phoenix/db/insertion/types.py +41 -18
- 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/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 +364 -56
- phoenix/db/pg_config.py +10 -0
- phoenix/db/types/trace_retention.py +7 -6
- phoenix/experiments/functions.py +69 -19
- phoenix/inferences/inferences.py +1 -2
- phoenix/server/api/auth.py +9 -0
- phoenix/server/api/auth_messages.py +46 -0
- phoenix/server/api/context.py +60 -0
- phoenix/server/api/dataloaders/__init__.py +36 -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/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_summary_by_experiment_repeated_run_group.py +64 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project.py +28 -14
- phoenix/server/api/dataloaders/span_costs.py +3 -9
- 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/exceptions.py +5 -1
- phoenix/server/api/helpers/playground_clients.py +263 -83
- phoenix/server/api/helpers/playground_spans.py +2 -1
- phoenix/server/api/helpers/playground_users.py +26 -0
- phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
- phoenix/server/api/helpers/prompts/models.py +61 -19
- phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
- phoenix/server/api/input_types/ChatCompletionInput.py +3 -0
- phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
- phoenix/server/api/input_types/DatasetFilter.py +5 -2
- phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
- phoenix/server/api/input_types/GenerativeModelInput.py +3 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
- phoenix/server/api/input_types/PromptVersionInput.py +47 -1
- phoenix/server/api/input_types/SpanSort.py +3 -2
- phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/__init__.py +8 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
- phoenix/server/api/mutations/api_key_mutations.py +15 -20
- phoenix/server/api/mutations/chat_mutations.py +106 -37
- 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 +11 -9
- phoenix/server/api/mutations/project_mutations.py +4 -4
- 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 +13 -8
- phoenix/server/api/mutations/trace_mutations.py +3 -3
- phoenix/server/api/mutations/user_mutations.py +55 -26
- phoenix/server/api/queries.py +501 -617
- phoenix/server/api/routers/__init__.py +2 -2
- phoenix/server/api/routers/auth.py +141 -87
- phoenix/server/api/routers/ldap.py +229 -0
- phoenix/server/api/routers/oauth2.py +349 -101
- phoenix/server/api/routers/v1/__init__.py +22 -4
- phoenix/server/api/routers/v1/annotation_configs.py +19 -30
- phoenix/server/api/routers/v1/annotations.py +455 -13
- phoenix/server/api/routers/v1/datasets.py +355 -68
- phoenix/server/api/routers/v1/documents.py +142 -0
- phoenix/server/api/routers/v1/evaluations.py +20 -28
- phoenix/server/api/routers/v1/experiment_evaluations.py +16 -6
- phoenix/server/api/routers/v1/experiment_runs.py +335 -59
- phoenix/server/api/routers/v1/experiments.py +475 -47
- phoenix/server/api/routers/v1/projects.py +16 -50
- phoenix/server/api/routers/v1/prompts.py +50 -39
- phoenix/server/api/routers/v1/sessions.py +108 -0
- phoenix/server/api/routers/v1/spans.py +156 -96
- phoenix/server/api/routers/v1/traces.py +51 -77
- phoenix/server/api/routers/v1/users.py +64 -24
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +257 -93
- 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/Dataset.py +199 -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 +215 -68
- phoenix/server/api/types/ExperimentComparison.py +3 -9
- phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
- phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
- phoenix/server/api/types/ExperimentRun.py +120 -70
- phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
- phoenix/server/api/types/GenerativeModel.py +95 -42
- phoenix/server/api/types/GenerativeProvider.py +1 -1
- phoenix/server/api/types/ModelInterface.py +7 -2
- phoenix/server/api/types/PlaygroundModel.py +12 -2
- phoenix/server/api/types/Project.py +218 -185
- phoenix/server/api/types/ProjectSession.py +146 -29
- 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/Span.py +130 -123
- phoenix/server/api/types/SpanAnnotation.py +189 -42
- phoenix/server/api/types/SystemApiKey.py +65 -1
- phoenix/server/api/types/Trace.py +184 -53
- phoenix/server/api/types/TraceAnnotation.py +149 -50
- phoenix/server/api/types/User.py +128 -33
- 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 +154 -36
- phoenix/server/authorization.py +5 -4
- phoenix/server/bearer_auth.py +13 -5
- phoenix/server/cost_tracking/cost_model_lookup.py +42 -14
- phoenix/server/cost_tracking/model_cost_manifest.json +1085 -194
- phoenix/server/daemons/generative_model_store.py +61 -9
- phoenix/server/daemons/span_cost_calculator.py +10 -8
- phoenix/server/dml_event.py +13 -0
- phoenix/server/email/sender.py +29 -2
- phoenix/server/grpc_server.py +9 -9
- phoenix/server/jwt_store.py +8 -6
- phoenix/server/ldap.py +1449 -0
- phoenix/server/main.py +9 -3
- phoenix/server/oauth2.py +330 -12
- phoenix/server/prometheus.py +43 -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 +51 -53
- phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
- phoenix/server/static/assets/{index-BPCwGQr8.js → index-CTQoemZv.js} +42 -35
- 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-Bw30oz1A.js → vendor-recharts-V9cwpXsm.js} +7 -7
- phoenix/server/static/assets/{vendor-shiki-DZajAPeq.js → vendor-shiki-Do--csgv.js} +1 -1
- phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
- phoenix/server/templates/index.html +7 -1
- phoenix/server/thread_server.py +1 -2
- phoenix/server/utils.py +74 -0
- phoenix/session/client.py +55 -1
- phoenix/session/data_extractor.py +5 -0
- phoenix/session/evaluation.py +8 -4
- phoenix/session/session.py +44 -8
- phoenix/settings.py +2 -0
- phoenix/trace/attributes.py +80 -13
- phoenix/trace/dsl/query.py +2 -0
- phoenix/trace/projects.py +5 -0
- phoenix/utilities/template_formatters.py +1 -1
- phoenix/version.py +1 -1
- phoenix/server/api/types/Evaluation.py +0 -39
- phoenix/server/static/assets/components-D0DWAf0l.js +0 -5650
- phoenix/server/static/assets/pages-Creyamao.js +0 -8612
- phoenix/server/static/assets/vendor-CU36oj8y.js +0 -905
- phoenix/server/static/assets/vendor-CqDb5u4o.css +0 -1
- phoenix/server/static/assets/vendor-arizeai-Ctgw0e1G.js +0 -168
- phoenix/server/static/assets/vendor-codemirror-Cojjzqb9.js +0 -25
- phoenix/server/static/assets/vendor-three-BLWp5bic.js +0 -2998
- phoenix/utilities/deprecation.py +0 -31
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
phoenix/server/api/types/User.py
CHANGED
|
@@ -3,7 +3,6 @@ from typing import Optional
|
|
|
3
3
|
|
|
4
4
|
import strawberry
|
|
5
5
|
from sqlalchemy import select
|
|
6
|
-
from strawberry import Private
|
|
7
6
|
from strawberry.relay import Node, NodeID
|
|
8
7
|
from strawberry.types import Info
|
|
9
8
|
|
|
@@ -12,59 +11,155 @@ from phoenix.db import models
|
|
|
12
11
|
from phoenix.server.api.context import Context
|
|
13
12
|
from phoenix.server.api.exceptions import NotFound
|
|
14
13
|
from phoenix.server.api.types.AuthMethod import AuthMethod
|
|
15
|
-
from phoenix.server.api.types.UserApiKey import UserApiKey
|
|
14
|
+
from phoenix.server.api.types.UserApiKey import UserApiKey
|
|
15
|
+
from phoenix.server.ldap import is_null_email_marker
|
|
16
16
|
|
|
17
17
|
from .UserRole import UserRole, to_gql_user_role
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@strawberry.type
|
|
21
21
|
class User(Node):
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
id: NodeID[int]
|
|
23
|
+
db_record: strawberry.Private[Optional[models.User]] = None
|
|
24
|
+
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
if self.db_record and self.id != self.db_record.id:
|
|
27
|
+
raise ValueError("User ID mismatch")
|
|
28
|
+
|
|
29
|
+
@strawberry.field
|
|
30
|
+
async def password_needs_reset(
|
|
31
|
+
self,
|
|
32
|
+
info: Info[Context, None],
|
|
33
|
+
) -> bool:
|
|
34
|
+
if self.db_record:
|
|
35
|
+
val = self.db_record.reset_password
|
|
36
|
+
else:
|
|
37
|
+
val = await info.context.data_loaders.user_fields.load(
|
|
38
|
+
(self.id, models.User.reset_password),
|
|
39
|
+
)
|
|
40
|
+
return val
|
|
41
|
+
|
|
42
|
+
@strawberry.field
|
|
43
|
+
async def email(
|
|
44
|
+
self,
|
|
45
|
+
info: Info[Context, None],
|
|
46
|
+
) -> str | None:
|
|
47
|
+
if self.db_record:
|
|
48
|
+
val = self.db_record.email
|
|
49
|
+
else:
|
|
50
|
+
val = await info.context.data_loaders.user_fields.load(
|
|
51
|
+
(self.id, models.User.email),
|
|
52
|
+
)
|
|
53
|
+
if is_null_email_marker(val):
|
|
54
|
+
return None
|
|
55
|
+
return val
|
|
56
|
+
|
|
57
|
+
@strawberry.field
|
|
58
|
+
async def username(
|
|
59
|
+
self,
|
|
60
|
+
info: Info[Context, None],
|
|
61
|
+
) -> str:
|
|
62
|
+
if self.db_record:
|
|
63
|
+
val = self.db_record.username
|
|
64
|
+
else:
|
|
65
|
+
val = await info.context.data_loaders.user_fields.load(
|
|
66
|
+
(self.id, models.User.username),
|
|
67
|
+
)
|
|
68
|
+
return val
|
|
69
|
+
|
|
70
|
+
@strawberry.field
|
|
71
|
+
async def profile_picture_url(
|
|
72
|
+
self,
|
|
73
|
+
info: Info[Context, None],
|
|
74
|
+
) -> Optional[str]:
|
|
75
|
+
if self.db_record:
|
|
76
|
+
val = self.db_record.profile_picture_url
|
|
77
|
+
else:
|
|
78
|
+
val = await info.context.data_loaders.user_fields.load(
|
|
79
|
+
(self.id, models.User.profile_picture_url),
|
|
80
|
+
)
|
|
81
|
+
return val
|
|
82
|
+
|
|
83
|
+
@strawberry.field
|
|
84
|
+
async def created_at(
|
|
85
|
+
self,
|
|
86
|
+
info: Info[Context, None],
|
|
87
|
+
) -> datetime:
|
|
88
|
+
if self.db_record:
|
|
89
|
+
val = self.db_record.created_at
|
|
90
|
+
else:
|
|
91
|
+
val = await info.context.data_loaders.user_fields.load(
|
|
92
|
+
(self.id, models.User.created_at),
|
|
93
|
+
)
|
|
94
|
+
return val
|
|
95
|
+
|
|
96
|
+
@strawberry.field
|
|
97
|
+
async def auth_method(
|
|
98
|
+
self,
|
|
99
|
+
info: Info[Context, None],
|
|
100
|
+
) -> AuthMethod:
|
|
101
|
+
"""Return semantic auth method (translated from database for LDAP users).
|
|
102
|
+
|
|
103
|
+
For Approach 1 (zero-migration), LDAP users are stored with auth_method='OAUTH2'
|
|
104
|
+
but have a special Unicode marker in oauth2_client_id. This resolver translates
|
|
105
|
+
that storage convention to the correct semantic AuthMethod.LDAP for the frontend.
|
|
106
|
+
"""
|
|
107
|
+
if self.db_record:
|
|
108
|
+
auth_method_val = self.db_record.auth_method
|
|
109
|
+
oauth2_client_id = self.db_record.oauth2_client_id
|
|
110
|
+
else:
|
|
111
|
+
(
|
|
112
|
+
auth_method_val,
|
|
113
|
+
oauth2_client_id,
|
|
114
|
+
) = await info.context.data_loaders.user_fields.load_many(
|
|
115
|
+
(
|
|
116
|
+
(self.id, models.User.auth_method),
|
|
117
|
+
(self.id, models.User.oauth2_client_id),
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Translate LDAP users from database storage to semantic type
|
|
122
|
+
if auth_method_val == "OAUTH2":
|
|
123
|
+
# Import here to avoid circular dependency
|
|
124
|
+
from phoenix.server.ldap import is_ldap_user
|
|
125
|
+
|
|
126
|
+
if is_ldap_user(oauth2_client_id):
|
|
127
|
+
return AuthMethod.LDAP
|
|
128
|
+
|
|
129
|
+
return AuthMethod(auth_method_val)
|
|
30
130
|
|
|
31
131
|
@strawberry.field
|
|
32
132
|
async def role(self, info: Info[Context, None]) -> UserRole:
|
|
33
|
-
|
|
133
|
+
if self.db_record:
|
|
134
|
+
user_role_id = self.db_record.user_role_id
|
|
135
|
+
else:
|
|
136
|
+
user_role_id = await info.context.data_loaders.user_fields.load(
|
|
137
|
+
(self.id, models.User.user_role_id),
|
|
138
|
+
)
|
|
139
|
+
role = await info.context.data_loaders.user_roles.load(user_role_id)
|
|
34
140
|
if role is None:
|
|
35
|
-
raise NotFound(f"User role with id {
|
|
141
|
+
raise NotFound(f"User role with id {user_role_id} not found")
|
|
36
142
|
return to_gql_user_role(role)
|
|
37
143
|
|
|
38
144
|
@strawberry.field
|
|
39
145
|
async def api_keys(self, info: Info[Context, None]) -> list[UserApiKey]:
|
|
40
146
|
async with info.context.db() as session:
|
|
41
147
|
api_keys = await session.scalars(
|
|
42
|
-
select(models.ApiKey).where(models.ApiKey.user_id == self.
|
|
148
|
+
select(models.ApiKey).where(models.ApiKey.user_id == self.id)
|
|
43
149
|
)
|
|
44
|
-
return [
|
|
150
|
+
return [UserApiKey(id=api_key.id, db_record=api_key) for api_key in api_keys]
|
|
45
151
|
|
|
46
152
|
@strawberry.field
|
|
47
|
-
async def is_management_user(self) -> bool:
|
|
153
|
+
async def is_management_user(self, info: Info[Context, None]) -> bool:
|
|
48
154
|
initial_admins = get_env_admins()
|
|
49
155
|
# this field is only visible to initial admins as they are the ones likely to have access to
|
|
50
156
|
# a management interface / the phoenix environment.
|
|
51
|
-
if self.
|
|
157
|
+
if self.db_record:
|
|
158
|
+
email = self.db_record.email
|
|
159
|
+
else:
|
|
160
|
+
email = await info.context.data_loaders.user_fields.load(
|
|
161
|
+
(self.id, models.User.email),
|
|
162
|
+
)
|
|
163
|
+
if email in initial_admins or email == "admin@localhost":
|
|
52
164
|
return True
|
|
53
165
|
return False
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def to_gql_user(user: models.User, api_keys: Optional[list[models.ApiKey]] = None) -> User:
|
|
57
|
-
"""
|
|
58
|
-
Converts an ORM user to a GraphQL user.
|
|
59
|
-
"""
|
|
60
|
-
assert user.auth_method is not None
|
|
61
|
-
return User(
|
|
62
|
-
id_attr=user.id,
|
|
63
|
-
password_needs_reset=user.reset_password,
|
|
64
|
-
username=user.username,
|
|
65
|
-
email=user.email,
|
|
66
|
-
profile_picture_url=user.profile_picture_url,
|
|
67
|
-
created_at=user.created_at,
|
|
68
|
-
user_role_id=user.user_role_id,
|
|
69
|
-
auth_method=AuthMethod(user.auth_method),
|
|
70
|
-
)
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
|
2
3
|
|
|
3
4
|
import strawberry
|
|
4
|
-
from strawberry import Private
|
|
5
5
|
from strawberry.relay import Node, NodeID
|
|
6
6
|
from strawberry.types import Info
|
|
7
7
|
from typing_extensions import Annotated
|
|
8
8
|
|
|
9
9
|
from phoenix.db.models import ApiKey as OrmApiKey
|
|
10
10
|
from phoenix.server.api.context import Context
|
|
11
|
-
from phoenix.server.api.exceptions import NotFound
|
|
12
11
|
|
|
13
12
|
from .ApiKey import ApiKey
|
|
14
13
|
|
|
@@ -18,28 +17,76 @@ if TYPE_CHECKING:
|
|
|
18
17
|
|
|
19
18
|
@strawberry.type
|
|
20
19
|
class UserApiKey(ApiKey, Node):
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
id: NodeID[int]
|
|
21
|
+
db_record: strawberry.Private[Optional[OrmApiKey]] = None
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if self.db_record and self.id != self.db_record.id:
|
|
25
|
+
raise ValueError("UserApiKey ID mismatch")
|
|
26
|
+
|
|
27
|
+
@strawberry.field(description="Name of the API key.") # type: ignore
|
|
28
|
+
async def name(
|
|
29
|
+
self,
|
|
30
|
+
info: Info[Context, None],
|
|
31
|
+
) -> str:
|
|
32
|
+
if self.db_record:
|
|
33
|
+
val = self.db_record.name
|
|
34
|
+
else:
|
|
35
|
+
val = await info.context.data_loaders.user_api_key_fields.load(
|
|
36
|
+
(self.id, OrmApiKey.name),
|
|
37
|
+
)
|
|
38
|
+
return val
|
|
39
|
+
|
|
40
|
+
@strawberry.field(description="Description of the API key.") # type: ignore
|
|
41
|
+
async def description(
|
|
42
|
+
self,
|
|
43
|
+
info: Info[Context, None],
|
|
44
|
+
) -> Optional[str]:
|
|
45
|
+
if self.db_record:
|
|
46
|
+
val = self.db_record.description
|
|
47
|
+
else:
|
|
48
|
+
val = await info.context.data_loaders.user_api_key_fields.load(
|
|
49
|
+
(self.id, OrmApiKey.description),
|
|
50
|
+
)
|
|
51
|
+
return val
|
|
52
|
+
|
|
53
|
+
@strawberry.field(description="The date and time the API key was created.") # type: ignore
|
|
54
|
+
async def created_at(
|
|
55
|
+
self,
|
|
56
|
+
info: Info[Context, None],
|
|
57
|
+
) -> datetime:
|
|
58
|
+
if self.db_record:
|
|
59
|
+
val = self.db_record.created_at
|
|
60
|
+
else:
|
|
61
|
+
val = await info.context.data_loaders.user_api_key_fields.load(
|
|
62
|
+
(self.id, OrmApiKey.created_at),
|
|
63
|
+
)
|
|
64
|
+
return val
|
|
65
|
+
|
|
66
|
+
@strawberry.field(description="The date and time the API key will expire.") # type: ignore
|
|
67
|
+
async def expires_at(
|
|
68
|
+
self,
|
|
69
|
+
info: Info[Context, None],
|
|
70
|
+
) -> Optional[datetime]:
|
|
71
|
+
if self.db_record:
|
|
72
|
+
val = self.db_record.expires_at
|
|
73
|
+
else:
|
|
74
|
+
val = await info.context.data_loaders.user_api_key_fields.load(
|
|
75
|
+
(self.id, OrmApiKey.expires_at),
|
|
76
|
+
)
|
|
77
|
+
return val
|
|
23
78
|
|
|
24
79
|
@strawberry.field
|
|
25
|
-
async def user(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return UserApiKey(
|
|
39
|
-
id_attr=api_key.id,
|
|
40
|
-
user_id=api_key.user_id,
|
|
41
|
-
name=api_key.name,
|
|
42
|
-
description=api_key.description,
|
|
43
|
-
created_at=api_key.created_at,
|
|
44
|
-
expires_at=api_key.expires_at,
|
|
45
|
-
)
|
|
80
|
+
async def user(
|
|
81
|
+
self,
|
|
82
|
+
info: Info[Context, None],
|
|
83
|
+
) -> Annotated["User", strawberry.lazy(".User")]:
|
|
84
|
+
if self.db_record:
|
|
85
|
+
user_id = self.db_record.user_id
|
|
86
|
+
else:
|
|
87
|
+
user_id = await info.context.data_loaders.user_api_key_fields.load(
|
|
88
|
+
(self.id, OrmApiKey.user_id),
|
|
89
|
+
)
|
|
90
|
+
from .User import User
|
|
91
|
+
|
|
92
|
+
return User(id=user_id)
|
phoenix/server/api/types/node.py
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from base64 import b64decode
|
|
3
|
+
|
|
1
4
|
from strawberry.relay import GlobalID
|
|
2
5
|
|
|
6
|
+
_GLOBAL_ID_PATTERN = re.compile(r"[a-zA-Z]+:[0-9]+")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_global_id(node_id: str) -> bool:
|
|
10
|
+
decoded_node_id = b64decode(node_id).decode()
|
|
11
|
+
return _GLOBAL_ID_PATTERN.match(decoded_node_id) is not None
|
|
12
|
+
|
|
3
13
|
|
|
4
14
|
def from_global_id(global_id: GlobalID) -> tuple[str, int]:
|
|
5
15
|
"""
|
|
@@ -9,7 +9,7 @@ from strawberry.relay.types import Connection, Edge, NodeType, PageInfo
|
|
|
9
9
|
from typing_extensions import TypeAlias, assert_never
|
|
10
10
|
|
|
11
11
|
ID: TypeAlias = int
|
|
12
|
-
CursorSortColumnValue: TypeAlias = Union[str, int, float, datetime]
|
|
12
|
+
CursorSortColumnValue: TypeAlias = Union[str, int, float, datetime, None]
|
|
13
13
|
|
|
14
14
|
# A type alias for the connection cursor implementation
|
|
15
15
|
CursorString = str
|
|
@@ -19,6 +19,7 @@ CURSOR_PREFIX = "connection:"
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class CursorSortColumnDataType(Enum):
|
|
22
|
+
NULL = auto()
|
|
22
23
|
STRING = auto()
|
|
23
24
|
INT = auto()
|
|
24
25
|
FLOAT = auto()
|
|
@@ -30,6 +31,10 @@ class CursorSortColumn:
|
|
|
30
31
|
type: CursorSortColumnDataType
|
|
31
32
|
value: CursorSortColumnValue
|
|
32
33
|
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if self.value is None:
|
|
36
|
+
self.type = CursorSortColumnDataType.NULL
|
|
37
|
+
|
|
33
38
|
def __str__(self) -> str:
|
|
34
39
|
if isinstance(self.value, str):
|
|
35
40
|
return self.value
|
|
@@ -37,7 +42,9 @@ class CursorSortColumn:
|
|
|
37
42
|
return str(self.value)
|
|
38
43
|
if isinstance(self.value, datetime):
|
|
39
44
|
return self.value.isoformat()
|
|
40
|
-
|
|
45
|
+
if self.value is None:
|
|
46
|
+
return ""
|
|
47
|
+
assert_never(self.value)
|
|
41
48
|
|
|
42
49
|
@classmethod
|
|
43
50
|
def from_string(cls, type: CursorSortColumnDataType, cursor_string: str) -> "CursorSortColumn":
|
|
@@ -50,6 +57,8 @@ class CursorSortColumn:
|
|
|
50
57
|
value = float(cursor_string)
|
|
51
58
|
elif type is CursorSortColumnDataType.DATETIME:
|
|
52
59
|
value = datetime.fromisoformat(cursor_string)
|
|
60
|
+
elif type is CursorSortColumnDataType.NULL:
|
|
61
|
+
value = None
|
|
53
62
|
else:
|
|
54
63
|
assert_never(type)
|
|
55
64
|
return cls(type=type, value=value)
|