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.

Files changed (73) hide show
  1. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/METADATA +2 -1
  2. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/RECORD +73 -72
  3. phoenix/auth.py +27 -2
  4. phoenix/config.py +302 -53
  5. phoenix/db/README.md +546 -28
  6. phoenix/db/models.py +3 -3
  7. phoenix/server/api/auth.py +9 -0
  8. phoenix/server/api/context.py +2 -0
  9. phoenix/server/api/dataloaders/__init__.py +2 -0
  10. phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
  11. phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
  12. phoenix/server/api/input_types/SpanSort.py +2 -1
  13. phoenix/server/api/input_types/UserRoleInput.py +1 -0
  14. phoenix/server/api/mutations/annotation_config_mutations.py +6 -6
  15. phoenix/server/api/mutations/api_key_mutations.py +13 -5
  16. phoenix/server/api/mutations/chat_mutations.py +3 -3
  17. phoenix/server/api/mutations/dataset_label_mutations.py +6 -6
  18. phoenix/server/api/mutations/dataset_mutations.py +8 -8
  19. phoenix/server/api/mutations/dataset_split_mutations.py +7 -7
  20. phoenix/server/api/mutations/experiment_mutations.py +2 -2
  21. phoenix/server/api/mutations/export_events_mutations.py +3 -3
  22. phoenix/server/api/mutations/model_mutations.py +4 -4
  23. phoenix/server/api/mutations/project_mutations.py +4 -4
  24. phoenix/server/api/mutations/project_session_annotations_mutations.py +4 -4
  25. phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
  26. phoenix/server/api/mutations/prompt_label_mutations.py +7 -7
  27. phoenix/server/api/mutations/prompt_mutations.py +7 -7
  28. phoenix/server/api/mutations/prompt_version_tag_mutations.py +3 -3
  29. phoenix/server/api/mutations/span_annotations_mutations.py +5 -5
  30. phoenix/server/api/mutations/trace_annotations_mutations.py +4 -4
  31. phoenix/server/api/mutations/trace_mutations.py +3 -3
  32. phoenix/server/api/mutations/user_mutations.py +8 -5
  33. phoenix/server/api/routers/auth.py +23 -32
  34. phoenix/server/api/routers/oauth2.py +213 -24
  35. phoenix/server/api/routers/v1/__init__.py +18 -4
  36. phoenix/server/api/routers/v1/annotation_configs.py +19 -30
  37. phoenix/server/api/routers/v1/annotations.py +21 -22
  38. phoenix/server/api/routers/v1/datasets.py +86 -64
  39. phoenix/server/api/routers/v1/documents.py +2 -3
  40. phoenix/server/api/routers/v1/evaluations.py +12 -24
  41. phoenix/server/api/routers/v1/experiment_evaluations.py +2 -3
  42. phoenix/server/api/routers/v1/experiment_runs.py +16 -11
  43. phoenix/server/api/routers/v1/experiments.py +57 -22
  44. phoenix/server/api/routers/v1/projects.py +16 -50
  45. phoenix/server/api/routers/v1/prompts.py +30 -31
  46. phoenix/server/api/routers/v1/sessions.py +2 -5
  47. phoenix/server/api/routers/v1/spans.py +35 -26
  48. phoenix/server/api/routers/v1/traces.py +11 -19
  49. phoenix/server/api/routers/v1/users.py +13 -29
  50. phoenix/server/api/routers/v1/utils.py +3 -7
  51. phoenix/server/api/subscriptions.py +3 -3
  52. phoenix/server/api/types/Dataset.py +95 -6
  53. phoenix/server/api/types/Project.py +24 -68
  54. phoenix/server/app.py +3 -2
  55. phoenix/server/authorization.py +5 -4
  56. phoenix/server/bearer_auth.py +13 -5
  57. phoenix/server/jwt_store.py +8 -6
  58. phoenix/server/oauth2.py +172 -5
  59. phoenix/server/static/.vite/manifest.json +39 -39
  60. phoenix/server/static/assets/{components-Bs8eJEpU.js → components-cwdYEs7B.js} +501 -404
  61. phoenix/server/static/assets/{index-C6WEu5UP.js → index-Dc0vD1Rn.js} +1 -1
  62. phoenix/server/static/assets/{pages-D-n2pkoG.js → pages-BDkB3a_a.js} +577 -533
  63. phoenix/server/static/assets/{vendor-D2eEI-6h.js → vendor-Ce6GTAin.js} +1 -1
  64. phoenix/server/static/assets/{vendor-arizeai-kfOei7nf.js → vendor-arizeai-CSF-1Kc5.js} +1 -1
  65. phoenix/server/static/assets/{vendor-codemirror-1bq_t1Ec.js → vendor-codemirror-Bv8J_7an.js} +3 -3
  66. phoenix/server/static/assets/{vendor-recharts-DQ4xfrf4.js → vendor-recharts-DcLgzI7g.js} +1 -1
  67. phoenix/server/static/assets/{vendor-shiki-GGmcIQxA.js → vendor-shiki-BF8rh_7m.js} +1 -1
  68. phoenix/trace/attributes.py +80 -13
  69. phoenix/version.py +1 -1
  70. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/WHEEL +0 -0
  71. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/entry_points.txt +0 -0
  72. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/licenses/IP_NOTICE +0 -0
  73. {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": HTTP_404_NOT_FOUND, "description": "Experiment run not found"}]
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=HTTP_404_NOT_FOUND,
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": HTTP_404_NOT_FOUND,
62
+ "status_code": 404,
64
63
  "description": "Experiment or dataset example not found",
65
64
  },
66
65
  {
67
- "status_code": HTTP_409_CONFLICT,
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=HTTP_404_NOT_FOUND,
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=HTTP_404_NOT_FOUND,
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=HTTP_409_CONFLICT,
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": HTTP_404_NOT_FOUND, "description": "Experiment not found"},
145
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid cursor format"},
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
- experiment_gid = GlobalID.from_id(experiment_id)
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=HTTP_404_NOT_FOUND,
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=HTTP_422_UNPROCESSABLE_ENTITY,
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": HTTP_404_NOT_FOUND, "description": "Dataset or DatasetVersion not found"}]
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
- dataset_globalid = GlobalID.from_id(dataset_id)
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=HTTP_404_NOT_FOUND,
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=HTTP_404_NOT_FOUND,
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=HTTP_404_NOT_FOUND,
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=HTTP_404_NOT_FOUND,
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=HTTP_404_NOT_FOUND,
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": HTTP_404_NOT_FOUND, "description": "Experiment not found"}]
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
- experiment_globalid = GlobalID.from_id(experiment_id)
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=HTTP_404_NOT_FOUND,
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=HTTP_404_NOT_FOUND,
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
- dataset_gid = GlobalID.from_id(dataset_id)
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=HTTP_404_NOT_FOUND,
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=HTTP_404_NOT_FOUND)
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=HTTP_404_NOT_FOUND,
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": HTTP_404_NOT_FOUND, "description": "Experiment not found"},
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
- experiment_globalid = GlobalID.from_id(experiment_id)
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=HTTP_422_UNPROCESSABLE_ENTITY,
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(HTTP_200_OK)},
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
- experiment_globalid = GlobalID.from_id(experiment_id)
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=HTTP_422_UNPROCESSABLE_ENTITY,
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
- HTTP_422_UNPROCESSABLE_ENTITY,
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=HTTP_422_UNPROCESSABLE_ENTITY,
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
- HTTP_404_NOT_FOUND,
146
- HTTP_422_UNPROCESSABLE_ENTITY,
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
- HTTP_422_UNPROCESSABLE_ENTITY,
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
- HTTP_403_FORBIDDEN,
227
- HTTP_404_NOT_FOUND,
228
- HTTP_422_UNPROCESSABLE_ENTITY,
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=HTTP_204_NO_CONTENT,
265
+ status_code=204,
286
266
  responses=add_errors_to_responses(
287
267
  [
288
- HTTP_403_FORBIDDEN,
289
- HTTP_404_NOT_FOUND,
290
- HTTP_422_UNPROCESSABLE_ENTITY,
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=HTTP_403_FORBIDDEN,
300
+ status_code=403,
335
301
  detail="The default project cannot be deleted",
336
302
  )
337
303