arize-phoenix 12.3.0__py3-none-any.whl → 12.5.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-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/METADATA +2 -1
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/RECORD +73 -72
- phoenix/auth.py +27 -2
- phoenix/config.py +302 -53
- phoenix/db/README.md +546 -28
- phoenix/db/models.py +3 -3
- phoenix/server/api/auth.py +9 -0
- phoenix/server/api/context.py +2 -0
- phoenix/server/api/dataloaders/__init__.py +2 -0
- phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
- phoenix/server/api/input_types/SpanSort.py +2 -1
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +6 -6
- phoenix/server/api/mutations/api_key_mutations.py +13 -5
- phoenix/server/api/mutations/chat_mutations.py +3 -3
- phoenix/server/api/mutations/dataset_label_mutations.py +6 -6
- phoenix/server/api/mutations/dataset_mutations.py +8 -8
- phoenix/server/api/mutations/dataset_split_mutations.py +7 -7
- 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 +4 -4
- phoenix/server/api/mutations/project_mutations.py +4 -4
- phoenix/server/api/mutations/project_session_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
- phoenix/server/api/mutations/prompt_label_mutations.py +7 -7
- phoenix/server/api/mutations/prompt_mutations.py +7 -7
- phoenix/server/api/mutations/prompt_version_tag_mutations.py +3 -3
- phoenix/server/api/mutations/span_annotations_mutations.py +5 -5
- phoenix/server/api/mutations/trace_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/trace_mutations.py +3 -3
- phoenix/server/api/mutations/user_mutations.py +8 -5
- phoenix/server/api/routers/auth.py +23 -32
- phoenix/server/api/routers/oauth2.py +213 -24
- phoenix/server/api/routers/v1/__init__.py +18 -4
- phoenix/server/api/routers/v1/annotation_configs.py +19 -30
- phoenix/server/api/routers/v1/annotations.py +21 -22
- phoenix/server/api/routers/v1/datasets.py +86 -64
- phoenix/server/api/routers/v1/documents.py +2 -3
- phoenix/server/api/routers/v1/evaluations.py +12 -24
- phoenix/server/api/routers/v1/experiment_evaluations.py +2 -3
- phoenix/server/api/routers/v1/experiment_runs.py +16 -11
- phoenix/server/api/routers/v1/experiments.py +57 -22
- phoenix/server/api/routers/v1/projects.py +16 -50
- phoenix/server/api/routers/v1/prompts.py +30 -31
- phoenix/server/api/routers/v1/sessions.py +2 -5
- phoenix/server/api/routers/v1/spans.py +35 -26
- phoenix/server/api/routers/v1/traces.py +11 -19
- phoenix/server/api/routers/v1/users.py +13 -29
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +3 -3
- phoenix/server/api/types/Dataset.py +95 -6
- phoenix/server/api/types/Project.py +24 -68
- phoenix/server/app.py +3 -2
- phoenix/server/authorization.py +5 -4
- phoenix/server/bearer_auth.py +13 -5
- phoenix/server/jwt_store.py +8 -6
- phoenix/server/oauth2.py +172 -5
- phoenix/server/static/.vite/manifest.json +39 -39
- phoenix/server/static/assets/{components-Bs8eJEpU.js → components-cwdYEs7B.js} +501 -404
- phoenix/server/static/assets/{index-C6WEu5UP.js → index-Dc0vD1Rn.js} +1 -1
- phoenix/server/static/assets/{pages-D-n2pkoG.js → pages-BDkB3a_a.js} +577 -533
- phoenix/server/static/assets/{vendor-D2eEI-6h.js → vendor-Ce6GTAin.js} +1 -1
- phoenix/server/static/assets/{vendor-arizeai-kfOei7nf.js → vendor-arizeai-CSF-1Kc5.js} +1 -1
- phoenix/server/static/assets/{vendor-codemirror-1bq_t1Ec.js → vendor-codemirror-Bv8J_7an.js} +3 -3
- phoenix/server/static/assets/{vendor-recharts-DQ4xfrf4.js → vendor-recharts-DcLgzI7g.js} +1 -1
- phoenix/server/static/assets/{vendor-shiki-GGmcIQxA.js → vendor-shiki-BF8rh_7m.js} +1 -1
- phoenix/trace/attributes.py +80 -13
- phoenix/version.py +1 -1
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,6 @@ from dateutil.parser import isoparse
|
|
|
5
5
|
from fastapi import APIRouter, HTTPException
|
|
6
6
|
from pydantic import Field, model_validator
|
|
7
7
|
from starlette.requests import Request
|
|
8
|
-
from starlette.status import HTTP_404_NOT_FOUND
|
|
9
8
|
from strawberry.relay import GlobalID
|
|
10
9
|
from typing_extensions import Self
|
|
11
10
|
|
|
@@ -72,7 +71,7 @@ class UpsertExperimentEvaluationResponseBody(
|
|
|
72
71
|
operation_id="upsertExperimentEvaluation",
|
|
73
72
|
summary="Create or update evaluation for an experiment run",
|
|
74
73
|
responses=add_errors_to_responses(
|
|
75
|
-
[{"status_code":
|
|
74
|
+
[{"status_code": 404, "description": "Experiment run not found"}]
|
|
76
75
|
),
|
|
77
76
|
)
|
|
78
77
|
async def upsert_experiment_evaluation(
|
|
@@ -85,7 +84,7 @@ async def upsert_experiment_evaluation(
|
|
|
85
84
|
except ValueError:
|
|
86
85
|
raise HTTPException(
|
|
87
86
|
detail=f"ExperimentRun with ID {experiment_run_gid} does not exist",
|
|
88
|
-
status_code=
|
|
87
|
+
status_code=404,
|
|
89
88
|
)
|
|
90
89
|
name = request_body.name
|
|
91
90
|
annotator_kind = request_body.annotator_kind
|
|
@@ -7,7 +7,6 @@ from sqlalchemy import select
|
|
|
7
7
|
from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
|
|
8
8
|
from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
|
|
9
9
|
from starlette.requests import Request
|
|
10
|
-
from starlette.status import HTTP_404_NOT_FOUND, HTTP_409_CONFLICT, HTTP_422_UNPROCESSABLE_ENTITY
|
|
11
10
|
from strawberry.relay import GlobalID
|
|
12
11
|
|
|
13
12
|
from phoenix.db import models
|
|
@@ -60,11 +59,11 @@ class CreateExperimentRunResponseBody(ResponseBody[CreateExperimentRunResponseBo
|
|
|
60
59
|
responses=add_errors_to_responses(
|
|
61
60
|
[
|
|
62
61
|
{
|
|
63
|
-
"status_code":
|
|
62
|
+
"status_code": 404,
|
|
64
63
|
"description": "Experiment or dataset example not found",
|
|
65
64
|
},
|
|
66
65
|
{
|
|
67
|
-
"status_code":
|
|
66
|
+
"status_code": 409,
|
|
68
67
|
"description": "This experiment run has already been submitted",
|
|
69
68
|
},
|
|
70
69
|
]
|
|
@@ -79,7 +78,7 @@ async def create_experiment_run(
|
|
|
79
78
|
except ValueError:
|
|
80
79
|
raise HTTPException(
|
|
81
80
|
detail=f"Experiment with ID {experiment_gid} does not exist",
|
|
82
|
-
status_code=
|
|
81
|
+
status_code=404,
|
|
83
82
|
)
|
|
84
83
|
|
|
85
84
|
example_gid = GlobalID.from_id(request_body.dataset_example_id)
|
|
@@ -88,7 +87,7 @@ async def create_experiment_run(
|
|
|
88
87
|
except ValueError:
|
|
89
88
|
raise HTTPException(
|
|
90
89
|
detail=f"DatasetExample with ID {example_gid} does not exist",
|
|
91
|
-
status_code=
|
|
90
|
+
status_code=404,
|
|
92
91
|
)
|
|
93
92
|
|
|
94
93
|
trace_id = request_body.trace_id
|
|
@@ -115,7 +114,7 @@ async def create_experiment_run(
|
|
|
115
114
|
except (PostgreSQLIntegrityError, SQLiteIntegrityError):
|
|
116
115
|
raise HTTPException(
|
|
117
116
|
detail="This experiment run has already been submitted",
|
|
118
|
-
status_code=
|
|
117
|
+
status_code=409,
|
|
119
118
|
)
|
|
120
119
|
request.state.event_queue.put(ExperimentRunInsertEvent((exp_run.id,)))
|
|
121
120
|
run_gid = GlobalID("ExperimentRun", str(exp_run.id))
|
|
@@ -141,8 +140,8 @@ class ListExperimentRunsResponseBody(PaginatedResponseBody[ExperimentRunResponse
|
|
|
141
140
|
response_description="Experiment runs retrieved successfully",
|
|
142
141
|
responses=add_errors_to_responses(
|
|
143
142
|
[
|
|
144
|
-
{"status_code":
|
|
145
|
-
{"status_code":
|
|
143
|
+
{"status_code": 404, "description": "Experiment not found"},
|
|
144
|
+
{"status_code": 422, "description": "Invalid cursor format"},
|
|
146
145
|
]
|
|
147
146
|
),
|
|
148
147
|
)
|
|
@@ -160,13 +159,19 @@ async def list_experiment_runs(
|
|
|
160
159
|
gt=0,
|
|
161
160
|
),
|
|
162
161
|
) -> ListExperimentRunsResponseBody:
|
|
163
|
-
|
|
162
|
+
try:
|
|
163
|
+
experiment_gid = GlobalID.from_id(experiment_id)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise HTTPException(
|
|
166
|
+
detail=f"Invalid experiment ID format: {experiment_id}",
|
|
167
|
+
status_code=422,
|
|
168
|
+
) from e
|
|
164
169
|
try:
|
|
165
170
|
experiment_rowid = from_global_id_with_expected_type(experiment_gid, "Experiment")
|
|
166
171
|
except ValueError:
|
|
167
172
|
raise HTTPException(
|
|
168
173
|
detail=f"Experiment with ID {experiment_gid} does not exist",
|
|
169
|
-
status_code=
|
|
174
|
+
status_code=404,
|
|
170
175
|
)
|
|
171
176
|
|
|
172
177
|
stmt = (
|
|
@@ -182,7 +187,7 @@ async def list_experiment_runs(
|
|
|
182
187
|
except ValueError:
|
|
183
188
|
raise HTTPException(
|
|
184
189
|
detail=f"Invalid cursor format: {cursor}",
|
|
185
|
-
status_code=
|
|
190
|
+
status_code=422,
|
|
186
191
|
)
|
|
187
192
|
|
|
188
193
|
# Apply limit only if specified for pagination
|
|
@@ -11,7 +11,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
11
11
|
from sqlalchemy.orm import joinedload
|
|
12
12
|
from starlette.requests import Request
|
|
13
13
|
from starlette.responses import PlainTextResponse
|
|
14
|
-
from starlette.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
|
|
15
14
|
from strawberry.relay import GlobalID
|
|
16
15
|
|
|
17
16
|
from phoenix.db import models
|
|
@@ -96,7 +95,7 @@ class CreateExperimentResponseBody(ResponseBody[Experiment]):
|
|
|
96
95
|
operation_id="createExperiment",
|
|
97
96
|
summary="Create experiment on a dataset",
|
|
98
97
|
responses=add_errors_to_responses(
|
|
99
|
-
[{"status_code":
|
|
98
|
+
[{"status_code": 404, "description": "Dataset or DatasetVersion not found"}]
|
|
100
99
|
),
|
|
101
100
|
response_description="Experiment retrieved successfully",
|
|
102
101
|
)
|
|
@@ -105,26 +104,38 @@ async def create_experiment(
|
|
|
105
104
|
request_body: CreateExperimentRequestBody,
|
|
106
105
|
dataset_id: str = Path(..., title="Dataset ID"),
|
|
107
106
|
) -> CreateExperimentResponseBody:
|
|
108
|
-
|
|
107
|
+
try:
|
|
108
|
+
dataset_globalid = GlobalID.from_id(dataset_id)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise HTTPException(
|
|
111
|
+
detail=f"Invalid dataset ID format: {dataset_id}",
|
|
112
|
+
status_code=422,
|
|
113
|
+
) from e
|
|
109
114
|
try:
|
|
110
115
|
dataset_rowid = from_global_id_with_expected_type(dataset_globalid, "Dataset")
|
|
111
116
|
except ValueError:
|
|
112
117
|
raise HTTPException(
|
|
113
118
|
detail="Dataset with ID {dataset_globalid} does not exist",
|
|
114
|
-
status_code=
|
|
119
|
+
status_code=404,
|
|
115
120
|
)
|
|
116
121
|
|
|
117
122
|
dataset_version_globalid_str = request_body.version_id
|
|
118
123
|
if dataset_version_globalid_str is not None:
|
|
119
124
|
try:
|
|
120
125
|
dataset_version_globalid = GlobalID.from_id(dataset_version_globalid_str)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise HTTPException(
|
|
128
|
+
detail=f"Invalid dataset version ID format: {dataset_version_globalid_str}",
|
|
129
|
+
status_code=422,
|
|
130
|
+
) from e
|
|
131
|
+
try:
|
|
121
132
|
dataset_version_id = from_global_id_with_expected_type(
|
|
122
133
|
dataset_version_globalid, "DatasetVersion"
|
|
123
134
|
)
|
|
124
135
|
except ValueError:
|
|
125
136
|
raise HTTPException(
|
|
126
137
|
detail=f"DatasetVersion with ID {dataset_version_globalid_str} does not exist",
|
|
127
|
-
status_code=
|
|
138
|
+
status_code=404,
|
|
128
139
|
)
|
|
129
140
|
|
|
130
141
|
async with request.app.state.db() as session:
|
|
@@ -134,7 +145,7 @@ async def create_experiment(
|
|
|
134
145
|
if result is None:
|
|
135
146
|
raise HTTPException(
|
|
136
147
|
detail=f"Dataset with ID {dataset_globalid} does not exist",
|
|
137
|
-
status_code=
|
|
148
|
+
status_code=404,
|
|
138
149
|
)
|
|
139
150
|
dataset_name = result.name
|
|
140
151
|
if dataset_version_globalid_str is None:
|
|
@@ -147,7 +158,7 @@ async def create_experiment(
|
|
|
147
158
|
if not dataset_version:
|
|
148
159
|
raise HTTPException(
|
|
149
160
|
detail=f"Dataset {dataset_globalid} does not have any versions",
|
|
150
|
-
status_code=
|
|
161
|
+
status_code=404,
|
|
151
162
|
)
|
|
152
163
|
dataset_version_id = dataset_version.id
|
|
153
164
|
dataset_version_globalid = GlobalID("DatasetVersion", str(dataset_version_id))
|
|
@@ -159,7 +170,7 @@ async def create_experiment(
|
|
|
159
170
|
if not dataset_version:
|
|
160
171
|
raise HTTPException(
|
|
161
172
|
detail=f"DatasetVersion with ID {dataset_version_globalid} does not exist",
|
|
162
|
-
status_code=
|
|
173
|
+
status_code=404,
|
|
163
174
|
)
|
|
164
175
|
user_id: Optional[int] = None
|
|
165
176
|
if request.app.state.authentication_enabled and isinstance(request.user, PhoenixUser):
|
|
@@ -228,18 +239,24 @@ class GetExperimentResponseBody(ResponseBody[Experiment]):
|
|
|
228
239
|
operation_id="getExperiment",
|
|
229
240
|
summary="Get experiment by ID",
|
|
230
241
|
responses=add_errors_to_responses(
|
|
231
|
-
[{"status_code":
|
|
242
|
+
[{"status_code": 404, "description": "Experiment not found"}]
|
|
232
243
|
),
|
|
233
244
|
response_description="Experiment retrieved successfully",
|
|
234
245
|
)
|
|
235
246
|
async def get_experiment(request: Request, experiment_id: str) -> GetExperimentResponseBody:
|
|
236
|
-
|
|
247
|
+
try:
|
|
248
|
+
experiment_globalid = GlobalID.from_id(experiment_id)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
raise HTTPException(
|
|
251
|
+
detail=f"Invalid experiment ID format: {experiment_id}",
|
|
252
|
+
status_code=422,
|
|
253
|
+
) from e
|
|
237
254
|
try:
|
|
238
255
|
experiment_rowid = from_global_id_with_expected_type(experiment_globalid, "Experiment")
|
|
239
256
|
except ValueError:
|
|
240
257
|
raise HTTPException(
|
|
241
258
|
detail="Experiment with ID {experiment_globalid} does not exist",
|
|
242
|
-
status_code=
|
|
259
|
+
status_code=404,
|
|
243
260
|
)
|
|
244
261
|
|
|
245
262
|
async with request.app.state.db() as session:
|
|
@@ -250,7 +267,7 @@ async def get_experiment(request: Request, experiment_id: str) -> GetExperimentR
|
|
|
250
267
|
if not experiment:
|
|
251
268
|
raise HTTPException(
|
|
252
269
|
detail=f"Experiment with ID {experiment_globalid} does not exist",
|
|
253
|
-
status_code=
|
|
270
|
+
status_code=404,
|
|
254
271
|
)
|
|
255
272
|
|
|
256
273
|
dataset_globalid = GlobalID("Dataset", str(experiment.dataset_id))
|
|
@@ -283,13 +300,19 @@ async def list_experiments(
|
|
|
283
300
|
request: Request,
|
|
284
301
|
dataset_id: str = Path(..., title="Dataset ID"),
|
|
285
302
|
) -> ListExperimentsResponseBody:
|
|
286
|
-
|
|
303
|
+
try:
|
|
304
|
+
dataset_gid = GlobalID.from_id(dataset_id)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
raise HTTPException(
|
|
307
|
+
detail=f"Invalid dataset ID format: {dataset_id}",
|
|
308
|
+
status_code=422,
|
|
309
|
+
) from e
|
|
287
310
|
try:
|
|
288
311
|
dataset_rowid = from_global_id_with_expected_type(dataset_gid, "Dataset")
|
|
289
312
|
except ValueError:
|
|
290
313
|
raise HTTPException(
|
|
291
314
|
detail=f"Dataset with ID {dataset_gid} does not exist",
|
|
292
|
-
status_code=
|
|
315
|
+
status_code=404,
|
|
293
316
|
)
|
|
294
317
|
async with request.app.state.db() as session:
|
|
295
318
|
query = (
|
|
@@ -328,7 +351,7 @@ async def _get_experiment_runs_and_revisions(
|
|
|
328
351
|
) -> tuple[models.Experiment, tuple[models.ExperimentRun], tuple[models.DatasetExampleRevision]]:
|
|
329
352
|
experiment = await session.get(models.Experiment, experiment_rowid)
|
|
330
353
|
if not experiment:
|
|
331
|
-
raise HTTPException(detail="Experiment not found", status_code=
|
|
354
|
+
raise HTTPException(detail="Experiment not found", status_code=404)
|
|
332
355
|
revision_ids = (
|
|
333
356
|
select(func.max(models.DatasetExampleRevision.id))
|
|
334
357
|
.join(
|
|
@@ -377,7 +400,7 @@ async def _get_experiment_runs_and_revisions(
|
|
|
377
400
|
if not runs_and_revisions:
|
|
378
401
|
raise HTTPException(
|
|
379
402
|
detail="Experiment has no runs",
|
|
380
|
-
status_code=
|
|
403
|
+
status_code=404,
|
|
381
404
|
)
|
|
382
405
|
runs, revisions = zip(*runs_and_revisions)
|
|
383
406
|
return experiment, runs, revisions
|
|
@@ -390,7 +413,7 @@ async def _get_experiment_runs_and_revisions(
|
|
|
390
413
|
response_class=PlainTextResponse,
|
|
391
414
|
responses=add_errors_to_responses(
|
|
392
415
|
[
|
|
393
|
-
{"status_code":
|
|
416
|
+
{"status_code": 404, "description": "Experiment not found"},
|
|
394
417
|
]
|
|
395
418
|
),
|
|
396
419
|
)
|
|
@@ -398,13 +421,19 @@ async def get_experiment_json(
|
|
|
398
421
|
request: Request,
|
|
399
422
|
experiment_id: str = Path(..., title="Experiment ID"),
|
|
400
423
|
) -> Response:
|
|
401
|
-
|
|
424
|
+
try:
|
|
425
|
+
experiment_globalid = GlobalID.from_id(experiment_id)
|
|
426
|
+
except Exception as e:
|
|
427
|
+
raise HTTPException(
|
|
428
|
+
detail=f"Invalid experiment ID format: {experiment_id}",
|
|
429
|
+
status_code=422,
|
|
430
|
+
) from e
|
|
402
431
|
try:
|
|
403
432
|
experiment_rowid = from_global_id_with_expected_type(experiment_globalid, "Experiment")
|
|
404
433
|
except ValueError:
|
|
405
434
|
raise HTTPException(
|
|
406
435
|
detail=f"Invalid experiment ID: {experiment_globalid}",
|
|
407
|
-
status_code=
|
|
436
|
+
status_code=422,
|
|
408
437
|
)
|
|
409
438
|
|
|
410
439
|
async with request.app.state.db() as session:
|
|
@@ -459,19 +488,25 @@ async def get_experiment_json(
|
|
|
459
488
|
"/experiments/{experiment_id}/csv",
|
|
460
489
|
operation_id="getExperimentCSV",
|
|
461
490
|
summary="Download experiment runs as a CSV file",
|
|
462
|
-
responses={**add_text_csv_content_to_responses(
|
|
491
|
+
responses={**add_text_csv_content_to_responses(200)},
|
|
463
492
|
)
|
|
464
493
|
async def get_experiment_csv(
|
|
465
494
|
request: Request,
|
|
466
495
|
experiment_id: str = Path(..., title="Experiment ID"),
|
|
467
496
|
) -> Response:
|
|
468
|
-
|
|
497
|
+
try:
|
|
498
|
+
experiment_globalid = GlobalID.from_id(experiment_id)
|
|
499
|
+
except Exception as e:
|
|
500
|
+
raise HTTPException(
|
|
501
|
+
detail=f"Invalid experiment ID format: {experiment_id}",
|
|
502
|
+
status_code=422,
|
|
503
|
+
) from e
|
|
469
504
|
try:
|
|
470
505
|
experiment_rowid = from_global_id_with_expected_type(experiment_globalid, "Experiment")
|
|
471
506
|
except ValueError:
|
|
472
507
|
raise HTTPException(
|
|
473
508
|
detail=f"Invalid experiment ID: {experiment_globalid}",
|
|
474
|
-
status_code=
|
|
509
|
+
status_code=422,
|
|
475
510
|
)
|
|
476
511
|
|
|
477
512
|
async with request.app.state.db() as session:
|
|
@@ -4,18 +4,11 @@ from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
from sqlalchemy import select
|
|
6
6
|
from starlette.requests import Request
|
|
7
|
-
from starlette.status import (
|
|
8
|
-
HTTP_204_NO_CONTENT,
|
|
9
|
-
HTTP_403_FORBIDDEN,
|
|
10
|
-
HTTP_404_NOT_FOUND,
|
|
11
|
-
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
12
|
-
)
|
|
13
7
|
from strawberry.relay import GlobalID
|
|
14
8
|
|
|
15
9
|
from phoenix.config import DEFAULT_PROJECT_NAME
|
|
16
10
|
from phoenix.db import models
|
|
17
11
|
from phoenix.db.helpers import exclude_experiment_projects
|
|
18
|
-
from phoenix.db.models import UserRoleName
|
|
19
12
|
from phoenix.server.api.routers.v1.models import V1RoutesBaseModel
|
|
20
13
|
from phoenix.server.api.routers.v1.utils import (
|
|
21
14
|
PaginatedResponseBody,
|
|
@@ -24,7 +17,7 @@ from phoenix.server.api.routers.v1.utils import (
|
|
|
24
17
|
add_errors_to_responses,
|
|
25
18
|
)
|
|
26
19
|
from phoenix.server.api.types.Project import Project as ProjectNodeType
|
|
27
|
-
from phoenix.server.authorization import is_not_locked
|
|
20
|
+
from phoenix.server.authorization import is_not_locked, require_admin
|
|
28
21
|
|
|
29
22
|
router = APIRouter(tags=["projects"])
|
|
30
23
|
|
|
@@ -70,7 +63,7 @@ class UpdateProjectResponseBody(ResponseBody[Project]):
|
|
|
70
63
|
response_description="A list of projects with pagination information", # noqa: E501
|
|
71
64
|
responses=add_errors_to_responses(
|
|
72
65
|
[
|
|
73
|
-
|
|
66
|
+
422,
|
|
74
67
|
]
|
|
75
68
|
),
|
|
76
69
|
)
|
|
@@ -115,7 +108,7 @@ async def get_projects(
|
|
|
115
108
|
except ValueError:
|
|
116
109
|
raise HTTPException(
|
|
117
110
|
detail=f"Invalid cursor format: {cursor}",
|
|
118
|
-
status_code=
|
|
111
|
+
status_code=422,
|
|
119
112
|
)
|
|
120
113
|
|
|
121
114
|
stmt = stmt.limit(limit + 1)
|
|
@@ -142,8 +135,8 @@ async def get_projects(
|
|
|
142
135
|
response_description="The requested project", # noqa: E501
|
|
143
136
|
responses=add_errors_to_responses(
|
|
144
137
|
[
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
404,
|
|
139
|
+
422,
|
|
147
140
|
]
|
|
148
141
|
),
|
|
149
142
|
)
|
|
@@ -182,7 +175,7 @@ async def get_project(
|
|
|
182
175
|
response_description="The newly created project", # noqa: E501
|
|
183
176
|
responses=add_errors_to_responses(
|
|
184
177
|
[
|
|
185
|
-
|
|
178
|
+
422,
|
|
186
179
|
]
|
|
187
180
|
),
|
|
188
181
|
)
|
|
@@ -216,16 +209,16 @@ async def create_project(
|
|
|
216
209
|
|
|
217
210
|
@router.put(
|
|
218
211
|
"/projects/{project_identifier}",
|
|
219
|
-
dependencies=[Depends(is_not_locked)],
|
|
212
|
+
dependencies=[Depends(require_admin), Depends(is_not_locked)],
|
|
220
213
|
operation_id="updateProject",
|
|
221
214
|
summary="Update a project by ID or name", # noqa: E501
|
|
222
215
|
description="Update an existing project with new configuration. Project names cannot be changed. The project identifier is either project ID or project name. Note: When using a project name as the identifier, it cannot contain slash (/), question mark (?), or pound sign (#) characters.", # noqa: E501
|
|
223
216
|
response_description="The updated project", # noqa: E501
|
|
224
217
|
responses=add_errors_to_responses(
|
|
225
218
|
[
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
219
|
+
403,
|
|
220
|
+
404,
|
|
221
|
+
422,
|
|
229
222
|
]
|
|
230
223
|
),
|
|
231
224
|
)
|
|
@@ -251,20 +244,6 @@ async def update_project(
|
|
|
251
244
|
Raises:
|
|
252
245
|
HTTPException: If the project identifier format is invalid or the project is not found.
|
|
253
246
|
""" # noqa: E501
|
|
254
|
-
if request.app.state.authentication_enabled:
|
|
255
|
-
async with request.app.state.db() as session:
|
|
256
|
-
# Check if the user is an admin
|
|
257
|
-
stmt = (
|
|
258
|
-
select(models.UserRole.name)
|
|
259
|
-
.join(models.User)
|
|
260
|
-
.where(models.User.id == int(request.user.identity))
|
|
261
|
-
)
|
|
262
|
-
role_name: UserRoleName = await session.scalar(stmt)
|
|
263
|
-
if role_name != "ADMIN" and role_name != "SYSTEM":
|
|
264
|
-
raise HTTPException(
|
|
265
|
-
status_code=HTTP_403_FORBIDDEN,
|
|
266
|
-
detail="Only admins can update projects",
|
|
267
|
-
)
|
|
268
247
|
async with request.app.state.db() as session:
|
|
269
248
|
project = await _get_project_by_identifier(session, project_identifier)
|
|
270
249
|
|
|
@@ -278,16 +257,17 @@ async def update_project(
|
|
|
278
257
|
|
|
279
258
|
@router.delete(
|
|
280
259
|
"/projects/{project_identifier}",
|
|
260
|
+
dependencies=[Depends(require_admin)],
|
|
281
261
|
operation_id="deleteProject",
|
|
282
262
|
summary="Delete a project by ID or name", # noqa: E501
|
|
283
263
|
description="Delete an existing project and all its associated data. The project identifier is either project ID or project name. The default project cannot be deleted. Note: When using a project name as the identifier, it cannot contain slash (/), question mark (?), or pound sign (#) characters.", # noqa: E501
|
|
284
264
|
response_description="No content returned on successful deletion", # noqa: E501
|
|
285
|
-
status_code=
|
|
265
|
+
status_code=204,
|
|
286
266
|
responses=add_errors_to_responses(
|
|
287
267
|
[
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
268
|
+
403,
|
|
269
|
+
404,
|
|
270
|
+
422,
|
|
291
271
|
]
|
|
292
272
|
),
|
|
293
273
|
)
|
|
@@ -311,27 +291,13 @@ async def delete_project(
|
|
|
311
291
|
Raises:
|
|
312
292
|
HTTPException: If the project identifier format is invalid, the project is not found, or it's the default project.
|
|
313
293
|
""" # noqa: E501
|
|
314
|
-
if request.app.state.authentication_enabled:
|
|
315
|
-
async with request.app.state.db() as session:
|
|
316
|
-
# Check if the user is an admin
|
|
317
|
-
stmt = (
|
|
318
|
-
select(models.UserRole.name)
|
|
319
|
-
.join(models.User)
|
|
320
|
-
.where(models.User.id == int(request.user.identity))
|
|
321
|
-
)
|
|
322
|
-
role_name: UserRoleName = await session.scalar(stmt)
|
|
323
|
-
if role_name != "ADMIN" and role_name != "SYSTEM":
|
|
324
|
-
raise HTTPException(
|
|
325
|
-
status_code=HTTP_403_FORBIDDEN,
|
|
326
|
-
detail="Only admins can delete projects",
|
|
327
|
-
)
|
|
328
294
|
async with request.app.state.db() as session:
|
|
329
295
|
project = await _get_project_by_identifier(session, project_identifier)
|
|
330
296
|
|
|
331
297
|
# The default project must not be deleted - it's forbidden
|
|
332
298
|
if project.name == DEFAULT_PROJECT_NAME:
|
|
333
299
|
raise HTTPException(
|
|
334
|
-
status_code=
|
|
300
|
+
status_code=403,
|
|
335
301
|
detail="The default project cannot be deleted",
|
|
336
302
|
)
|
|
337
303
|
|