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
@@ -8,7 +8,6 @@ from fastapi import APIRouter, HTTPException, Path, Query
8
8
  from pydantic import Field
9
9
  from sqlalchemy import exists, select
10
10
  from starlette.requests import Request
11
- from starlette.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
12
11
  from strawberry.relay import GlobalID
13
12
 
14
13
  from phoenix.db import models
@@ -198,11 +197,11 @@ class SessionAnnotationsResponseBody(PaginatedResponseBody[SessionAnnotation]):
198
197
  "/projects/{project_identifier}/span_annotations",
199
198
  operation_id="listSpanAnnotationsBySpanIds",
200
199
  summary="Get span annotations for a list of span_ids.",
201
- status_code=HTTP_200_OK,
200
+ status_code=200,
202
201
  responses=add_errors_to_responses(
203
202
  [
204
- {"status_code": HTTP_404_NOT_FOUND, "description": "Project or spans not found"},
205
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid parameters"},
203
+ {"status_code": 404, "description": "Project or spans not found"},
204
+ {"status_code": 422, "description": "Invalid parameters"},
206
205
  ]
207
206
  ),
208
207
  )
@@ -240,7 +239,7 @@ async def list_span_annotations(
240
239
  span_ids = list({*span_ids})
241
240
  if len(span_ids) > MAX_SPAN_IDS:
242
241
  raise HTTPException(
243
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
242
+ status_code=422,
244
243
  detail=f"Too many span_ids supplied: {len(span_ids)} (max {MAX_SPAN_IDS})",
245
244
  )
246
245
 
@@ -248,7 +247,7 @@ async def list_span_annotations(
248
247
  project = await _get_project_by_identifier(session, project_identifier)
249
248
  if not project:
250
249
  raise HTTPException(
251
- status_code=HTTP_404_NOT_FOUND,
250
+ status_code=404,
252
251
  detail=f"Project with identifier {project_identifier} not found",
253
252
  )
254
253
 
@@ -280,7 +279,7 @@ async def list_span_annotations(
280
279
  cursor_id = int(GlobalID.from_id(cursor).node_id)
281
280
  except ValueError:
282
281
  raise HTTPException(
283
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
282
+ status_code=422,
284
283
  detail="Invalid cursor value",
285
284
  )
286
285
  stmt = stmt.where(models.SpanAnnotation.id <= cursor_id)
@@ -310,7 +309,7 @@ async def list_span_annotations(
310
309
  if not spans_exist:
311
310
  raise HTTPException(
312
311
  detail="None of the supplied span_ids exist in this project",
313
- status_code=HTTP_404_NOT_FOUND,
312
+ status_code=404,
314
313
  )
315
314
 
316
315
  return SpanAnnotationsResponseBody(data=[], next_cursor=None)
@@ -343,11 +342,11 @@ async def list_span_annotations(
343
342
  "/projects/{project_identifier}/trace_annotations",
344
343
  operation_id="listTraceAnnotationsByTraceIds",
345
344
  summary="Get trace annotations for a list of trace_ids.",
346
- status_code=HTTP_200_OK,
345
+ status_code=200,
347
346
  responses=add_errors_to_responses(
348
347
  [
349
- {"status_code": HTTP_404_NOT_FOUND, "description": "Project or traces not found"},
350
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid parameters"},
348
+ {"status_code": 404, "description": "Project or traces not found"},
349
+ {"status_code": 422, "description": "Invalid parameters"},
351
350
  ]
352
351
  ),
353
352
  )
@@ -385,7 +384,7 @@ async def list_trace_annotations(
385
384
  trace_ids = list({*trace_ids})
386
385
  if len(trace_ids) > MAX_TRACE_IDS:
387
386
  raise HTTPException(
388
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
387
+ status_code=422,
389
388
  detail=f"Too many trace_ids supplied: {len(trace_ids)} (max {MAX_TRACE_IDS})",
390
389
  )
391
390
 
@@ -393,7 +392,7 @@ async def list_trace_annotations(
393
392
  project = await _get_project_by_identifier(session, project_identifier)
394
393
  if not project:
395
394
  raise HTTPException(
396
- status_code=HTTP_404_NOT_FOUND,
395
+ status_code=404,
397
396
  detail=f"Project with identifier {project_identifier} not found",
398
397
  )
399
398
 
@@ -424,7 +423,7 @@ async def list_trace_annotations(
424
423
  cursor_id = int(GlobalID.from_id(cursor).node_id)
425
424
  except ValueError:
426
425
  raise HTTPException(
427
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
426
+ status_code=422,
428
427
  detail="Invalid cursor value",
429
428
  )
430
429
  stmt = stmt.where(models.TraceAnnotation.id <= cursor_id)
@@ -450,7 +449,7 @@ async def list_trace_annotations(
450
449
  if not traces_exist:
451
450
  raise HTTPException(
452
451
  detail="None of the supplied trace_ids exist in this project",
453
- status_code=HTTP_404_NOT_FOUND,
452
+ status_code=404,
454
453
  )
455
454
 
456
455
  return TraceAnnotationsResponseBody(data=[], next_cursor=None)
@@ -483,11 +482,11 @@ async def list_trace_annotations(
483
482
  "/projects/{project_identifier}/session_annotations",
484
483
  operation_id="listSessionAnnotationsBySessionIds",
485
484
  summary="Get session annotations for a list of session_ids.",
486
- status_code=HTTP_200_OK,
485
+ status_code=200,
487
486
  responses=add_errors_to_responses(
488
487
  [
489
- {"status_code": HTTP_404_NOT_FOUND, "description": "Project or sessions not found"},
490
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid parameters"},
488
+ {"status_code": 404, "description": "Project or sessions not found"},
489
+ {"status_code": 422, "description": "Invalid parameters"},
491
490
  ]
492
491
  ),
493
492
  )
@@ -525,7 +524,7 @@ async def list_session_annotations(
525
524
  session_ids = list({*session_ids})
526
525
  if len(session_ids) > MAX_SESSION_IDS:
527
526
  raise HTTPException(
528
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
527
+ status_code=422,
529
528
  detail=f"Too many session_ids supplied: {len(session_ids)} (max {MAX_SESSION_IDS})",
530
529
  )
531
530
 
@@ -533,7 +532,7 @@ async def list_session_annotations(
533
532
  project = await _get_project_by_identifier(session, project_identifier)
534
533
  if not project:
535
534
  raise HTTPException(
536
- status_code=HTTP_404_NOT_FOUND,
535
+ status_code=404,
537
536
  detail=f"Project with identifier {project_identifier} not found",
538
537
  )
539
538
 
@@ -571,7 +570,7 @@ async def list_session_annotations(
571
570
  cursor_id = int(GlobalID.from_id(cursor).node_id)
572
571
  except ValueError:
573
572
  raise HTTPException(
574
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
573
+ status_code=422,
575
574
  detail="Invalid cursor value",
576
575
  )
577
576
  stmt = stmt.where(models.ProjectSessionAnnotation.id <= cursor_id)
@@ -597,7 +596,7 @@ async def list_session_annotations(
597
596
  if not sessions_exist:
598
597
  raise HTTPException(
599
598
  detail="None of the supplied session_ids exist in this project",
600
- status_code=HTTP_404_NOT_FOUND,
599
+ status_code=404,
601
600
  )
602
601
 
603
602
  return SessionAnnotationsResponseBody(data=[], next_cursor=None)
@@ -23,14 +23,6 @@ from starlette.concurrency import run_in_threadpool
23
23
  from starlette.datastructures import FormData, UploadFile
24
24
  from starlette.requests import Request
25
25
  from starlette.responses import Response
26
- from starlette.status import (
27
- HTTP_200_OK,
28
- HTTP_204_NO_CONTENT,
29
- HTTP_404_NOT_FOUND,
30
- HTTP_409_CONFLICT,
31
- HTTP_422_UNPROCESSABLE_ENTITY,
32
- HTTP_429_TOO_MANY_REQUESTS,
33
- )
34
26
  from strawberry.relay import GlobalID
35
27
  from typing_extensions import TypeAlias, assert_never
36
28
 
@@ -91,7 +83,7 @@ class ListDatasetsResponseBody(PaginatedResponseBody[Dataset]):
91
83
  "/datasets",
92
84
  operation_id="listDatasets",
93
85
  summary="List datasets",
94
- responses=add_errors_to_responses([HTTP_422_UNPROCESSABLE_ENTITY]),
86
+ responses=add_errors_to_responses([422]),
95
87
  )
96
88
  async def list_datasets(
97
89
  request: Request,
@@ -125,7 +117,7 @@ async def list_datasets(
125
117
  except ValueError:
126
118
  raise HTTPException(
127
119
  detail=f"Invalid cursor format: {cursor}",
128
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
120
+ status_code=422,
129
121
  )
130
122
  if name:
131
123
  query = query.filter(models.Dataset.name == name)
@@ -164,11 +156,11 @@ async def list_datasets(
164
156
  "/datasets/{id}",
165
157
  operation_id="deleteDatasetById",
166
158
  summary="Delete dataset by ID",
167
- status_code=HTTP_204_NO_CONTENT,
159
+ status_code=204,
168
160
  responses=add_errors_to_responses(
169
161
  [
170
- {"status_code": HTTP_404_NOT_FOUND, "description": "Dataset not found"},
171
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid dataset ID"},
162
+ {"status_code": 404, "description": "Dataset not found"},
163
+ {"status_code": 422, "description": "Invalid dataset ID"},
172
164
  ]
173
165
  ),
174
166
  )
@@ -182,11 +174,9 @@ async def delete_dataset(
182
174
  DATASET_NODE_NAME,
183
175
  )
184
176
  except ValueError:
185
- raise HTTPException(
186
- detail=f"Invalid Dataset ID: {id}", status_code=HTTP_422_UNPROCESSABLE_ENTITY
187
- )
177
+ raise HTTPException(detail=f"Invalid Dataset ID: {id}", status_code=422)
188
178
  else:
189
- raise HTTPException(detail="Missing Dataset ID", status_code=HTTP_422_UNPROCESSABLE_ENTITY)
179
+ raise HTTPException(detail="Missing Dataset ID", status_code=422)
190
180
  project_names_stmt = get_project_names_for_datasets(dataset_id)
191
181
  eval_trace_ids_stmt = get_eval_trace_ids_for_datasets(dataset_id)
192
182
  stmt = (
@@ -196,7 +186,7 @@ async def delete_dataset(
196
186
  project_names = await session.scalars(project_names_stmt)
197
187
  eval_trace_ids = await session.scalars(eval_trace_ids_stmt)
198
188
  if (await session.scalar(stmt)) is None:
199
- raise HTTPException(detail="Dataset does not exist", status_code=HTTP_404_NOT_FOUND)
189
+ raise HTTPException(detail="Dataset does not exist", status_code=404)
200
190
  tasks = BackgroundTasks()
201
191
  tasks.add_task(delete_projects, request.app.state.db, *project_names)
202
192
  tasks.add_task(delete_traces, request.app.state.db, *eval_trace_ids)
@@ -214,17 +204,21 @@ class GetDatasetResponseBody(ResponseBody[DatasetWithExampleCount]):
214
204
  "/datasets/{id}",
215
205
  operation_id="getDataset",
216
206
  summary="Get dataset by ID",
217
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND]),
207
+ responses=add_errors_to_responses([404]),
218
208
  )
219
209
  async def get_dataset(
220
210
  request: Request, id: str = Path(description="The ID of the dataset")
221
211
  ) -> GetDatasetResponseBody:
222
- dataset_id = GlobalID.from_id(id)
212
+ try:
213
+ dataset_id = GlobalID.from_id(id)
214
+ except Exception as e:
215
+ raise HTTPException(
216
+ detail=f"Invalid dataset ID format: {id}",
217
+ status_code=422,
218
+ ) from e
223
219
 
224
220
  if (type_name := dataset_id.type_name) != DATASET_NODE_NAME:
225
- raise HTTPException(
226
- detail=f"ID {dataset_id} refers to a f{type_name}", status_code=HTTP_404_NOT_FOUND
227
- )
221
+ raise HTTPException(detail=f"ID {dataset_id} refers to a f{type_name}", status_code=404)
228
222
  async with request.app.state.db() as session:
229
223
  result = await session.execute(
230
224
  select(models.Dataset, models.Dataset.example_count).filter(
@@ -235,9 +229,7 @@ async def get_dataset(
235
229
  dataset = dataset_query[0] if dataset_query else None
236
230
  example_count = dataset_query[1] if dataset_query else 0
237
231
  if dataset is None:
238
- raise HTTPException(
239
- detail=f"Dataset with ID {dataset_id} not found", status_code=HTTP_404_NOT_FOUND
240
- )
232
+ raise HTTPException(detail=f"Dataset with ID {dataset_id} not found", status_code=404)
241
233
 
242
234
  dataset = DatasetWithExampleCount(
243
235
  id=str(dataset_id),
@@ -266,7 +258,7 @@ class ListDatasetVersionsResponseBody(PaginatedResponseBody[DatasetVersion]):
266
258
  "/datasets/{id}/versions",
267
259
  operation_id="listDatasetVersionsByDatasetId",
268
260
  summary="List dataset versions",
269
- responses=add_errors_to_responses([HTTP_422_UNPROCESSABLE_ENTITY]),
261
+ responses=add_errors_to_responses([422]),
270
262
  )
271
263
  async def list_dataset_versions(
272
264
  request: Request,
@@ -288,12 +280,12 @@ async def list_dataset_versions(
288
280
  except ValueError:
289
281
  raise HTTPException(
290
282
  detail=f"Invalid Dataset ID: {id}",
291
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
283
+ status_code=422,
292
284
  )
293
285
  else:
294
286
  raise HTTPException(
295
287
  detail="Missing Dataset ID",
296
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
288
+ status_code=422,
297
289
  )
298
290
  stmt = (
299
291
  select(models.DatasetVersion)
@@ -309,7 +301,7 @@ async def list_dataset_versions(
309
301
  except ValueError:
310
302
  raise HTTPException(
311
303
  detail=f"Invalid cursor: {cursor}",
312
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
304
+ status_code=422,
313
305
  )
314
306
  max_dataset_version_id = (
315
307
  select(models.DatasetVersion.id)
@@ -348,10 +340,10 @@ class UploadDatasetResponseBody(ResponseBody[UploadDatasetData]):
348
340
  responses=add_errors_to_responses(
349
341
  [
350
342
  {
351
- "status_code": HTTP_409_CONFLICT,
343
+ "status_code": 409,
352
344
  "description": "Dataset of the same name already exists",
353
345
  },
354
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid request body"},
346
+ {"status_code": 422, "description": "Invalid request body"},
355
347
  ]
356
348
  ),
357
349
  # FastAPI cannot generate the request body portion of the OpenAPI schema for
@@ -414,7 +406,12 @@ async def upload_dataset(
414
406
  description="If true, fulfill request synchronously and return JSON containing dataset_id.",
415
407
  ),
416
408
  ) -> Optional[UploadDatasetResponseBody]:
417
- request_content_type = request.headers["content-type"]
409
+ request_content_type = request.headers.get("content-type")
410
+ if not request_content_type:
411
+ raise HTTPException(
412
+ detail="Missing content-type header",
413
+ status_code=400,
414
+ )
418
415
  examples: Union[Examples, Awaitable[Examples]]
419
416
  if request_content_type.startswith("application/json"):
420
417
  try:
@@ -424,14 +421,14 @@ async def upload_dataset(
424
421
  except ValueError as e:
425
422
  raise HTTPException(
426
423
  detail=str(e),
427
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
424
+ status_code=422,
428
425
  )
429
426
  if action is DatasetAction.CREATE:
430
427
  async with request.app.state.db() as session:
431
428
  if await _check_table_exists(session, name):
432
429
  raise HTTPException(
433
430
  detail=f"Dataset with the same name already exists: {name=}",
434
- status_code=HTTP_409_CONFLICT,
431
+ status_code=409,
435
432
  )
436
433
  elif request_content_type.startswith("multipart/form-data"):
437
434
  async with request.form() as form:
@@ -448,14 +445,14 @@ async def upload_dataset(
448
445
  except ValueError as e:
449
446
  raise HTTPException(
450
447
  detail=str(e),
451
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
448
+ status_code=422,
452
449
  )
453
450
  if action is DatasetAction.CREATE:
454
451
  async with request.app.state.db() as session:
455
452
  if await _check_table_exists(session, name):
456
453
  raise HTTPException(
457
454
  detail=f"Dataset with the same name already exists: {name=}",
458
- status_code=HTTP_409_CONFLICT,
455
+ status_code=409,
459
456
  )
460
457
  content = await file.read()
461
458
  try:
@@ -472,12 +469,12 @@ async def upload_dataset(
472
469
  except ValueError as e:
473
470
  raise HTTPException(
474
471
  detail=str(e),
475
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
472
+ status_code=422,
476
473
  )
477
474
  else:
478
475
  raise HTTPException(
479
476
  detail="Invalid request Content-Type",
480
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
477
+ status_code=422,
481
478
  )
482
479
  user_id: Optional[int] = None
483
480
  if request.app.state.authentication_enabled and isinstance(request.user, PhoenixUser):
@@ -510,7 +507,7 @@ async def upload_dataset(
510
507
  except QueueFull:
511
508
  if isinstance(examples, Coroutine):
512
509
  examples.close()
513
- raise HTTPException(detail="Too many requests.", status_code=HTTP_429_TOO_MANY_REQUESTS)
510
+ raise HTTPException(detail="Too many requests.", status_code=429)
514
511
  return None
515
512
 
516
513
 
@@ -711,7 +708,7 @@ class ListDatasetExamplesResponseBody(ResponseBody[ListDatasetExamplesData]):
711
708
  "/datasets/{id}/examples",
712
709
  operation_id="getDatasetExamples",
713
710
  summary="Get examples from a dataset",
714
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND]),
711
+ responses=add_errors_to_responses([404]),
715
712
  )
716
713
  async def get_dataset_examples(
717
714
  request: Request,
@@ -723,18 +720,30 @@ async def get_dataset_examples(
723
720
  ),
724
721
  ),
725
722
  ) -> ListDatasetExamplesResponseBody:
726
- dataset_gid = GlobalID.from_id(id)
727
- version_gid = GlobalID.from_id(version_id) if version_id else None
723
+ try:
724
+ dataset_gid = GlobalID.from_id(id)
725
+ except Exception as e:
726
+ raise HTTPException(
727
+ detail=f"Invalid dataset ID format: {id}",
728
+ status_code=422,
729
+ ) from e
730
+
731
+ if version_id:
732
+ try:
733
+ version_gid = GlobalID.from_id(version_id)
734
+ except Exception as e:
735
+ raise HTTPException(
736
+ detail=f"Invalid dataset version ID format: {version_id}",
737
+ status_code=422,
738
+ ) from e
739
+ else:
740
+ version_gid = None
728
741
 
729
742
  if (dataset_type := dataset_gid.type_name) != "Dataset":
730
- raise HTTPException(
731
- detail=f"ID {dataset_gid} refers to a {dataset_type}", status_code=HTTP_404_NOT_FOUND
732
- )
743
+ raise HTTPException(detail=f"ID {dataset_gid} refers to a {dataset_type}", status_code=404)
733
744
 
734
745
  if version_gid and (version_type := version_gid.type_name) != "DatasetVersion":
735
- raise HTTPException(
736
- detail=f"ID {version_gid} refers to a {version_type}", status_code=HTTP_404_NOT_FOUND
737
- )
746
+ raise HTTPException(detail=f"ID {version_gid} refers to a {version_type}", status_code=404)
738
747
 
739
748
  async with request.app.state.db() as session:
740
749
  if (
@@ -744,7 +753,7 @@ async def get_dataset_examples(
744
753
  ) is None:
745
754
  raise HTTPException(
746
755
  detail=f"No dataset with id {dataset_gid} can be found.",
747
- status_code=HTTP_404_NOT_FOUND,
756
+ status_code=404,
748
757
  )
749
758
 
750
759
  # Subquery to find the maximum created_at for each dataset_example_id
@@ -766,7 +775,7 @@ async def get_dataset_examples(
766
775
  ) is None:
767
776
  raise HTTPException(
768
777
  detail=f"No dataset version with id {version_id} can be found.",
769
- status_code=HTTP_404_NOT_FOUND,
778
+ status_code=404,
770
779
  )
771
780
  # if a version_id is provided, filter the subquery to only include revisions from that
772
781
  partial_subquery = partial_subquery.filter(
@@ -782,7 +791,7 @@ async def get_dataset_examples(
782
791
  ) is None:
783
792
  raise HTTPException(
784
793
  detail="Dataset has no versions.",
785
- status_code=HTTP_404_NOT_FOUND,
794
+ status_code=404,
786
795
  )
787
796
 
788
797
  subquery = partial_subquery.subquery()
@@ -825,10 +834,10 @@ async def get_dataset_examples(
825
834
  operation_id="getDatasetCsv",
826
835
  summary="Download dataset examples as CSV file",
827
836
  response_class=StreamingResponse,
828
- status_code=HTTP_200_OK,
837
+ status_code=200,
829
838
  responses={
830
- **add_errors_to_responses([HTTP_422_UNPROCESSABLE_ENTITY]),
831
- **add_text_csv_content_to_responses(HTTP_200_OK),
839
+ **add_errors_to_responses([422]),
840
+ **add_text_csv_content_to_responses(200),
832
841
  },
833
842
  )
834
843
  async def get_dataset_csv(
@@ -848,7 +857,7 @@ async def get_dataset_csv(
848
857
  session=session, id=id, version_id=version_id
849
858
  )
850
859
  except ValueError as e:
851
- raise HTTPException(detail=str(e), status_code=HTTP_422_UNPROCESSABLE_ENTITY)
860
+ raise HTTPException(detail=str(e), status_code=422)
852
861
  content = await run_in_threadpool(_get_content_csv, examples)
853
862
  encoded_dataset_name = urllib.parse.quote(dataset_name)
854
863
  return Response(
@@ -868,7 +877,7 @@ async def get_dataset_csv(
868
877
  responses=add_errors_to_responses(
869
878
  [
870
879
  {
871
- "status_code": HTTP_422_UNPROCESSABLE_ENTITY,
880
+ "status_code": 422,
872
881
  "description": "Invalid dataset or version ID",
873
882
  }
874
883
  ]
@@ -891,7 +900,7 @@ async def get_dataset_jsonl_openai_ft(
891
900
  session=session, id=id, version_id=version_id
892
901
  )
893
902
  except ValueError as e:
894
- raise HTTPException(detail=str(e), status_code=HTTP_422_UNPROCESSABLE_ENTITY)
903
+ raise HTTPException(detail=str(e), status_code=422)
895
904
  content = await run_in_threadpool(_get_content_jsonl_openai_ft, examples)
896
905
  encoded_dataset_name = urllib.parse.quote(dataset_name)
897
906
  response.headers["content-disposition"] = (
@@ -908,7 +917,7 @@ async def get_dataset_jsonl_openai_ft(
908
917
  responses=add_errors_to_responses(
909
918
  [
910
919
  {
911
- "status_code": HTTP_422_UNPROCESSABLE_ENTITY,
920
+ "status_code": 422,
912
921
  "description": "Invalid dataset or version ID",
913
922
  }
914
923
  ]
@@ -931,7 +940,7 @@ async def get_dataset_jsonl_openai_evals(
931
940
  session=session, id=id, version_id=version_id
932
941
  )
933
942
  except ValueError as e:
934
- raise HTTPException(detail=str(e), status_code=HTTP_422_UNPROCESSABLE_ENTITY)
943
+ raise HTTPException(detail=str(e), status_code=422)
935
944
  content = await run_in_threadpool(_get_content_jsonl_openai_evals, examples)
936
945
  encoded_dataset_name = urllib.parse.quote(dataset_name)
937
946
  response.headers["content-disposition"] = (
@@ -1010,12 +1019,25 @@ def _get_content_jsonl_openai_evals(examples: list[models.DatasetExampleRevision
1010
1019
  async def _get_db_examples(
1011
1020
  *, session: Any, id: str, version_id: Optional[str]
1012
1021
  ) -> tuple[str, list[models.DatasetExampleRevision]]:
1013
- dataset_id = from_global_id_with_expected_type(GlobalID.from_id(id), DATASET_NODE_NAME)
1022
+ try:
1023
+ dataset_id = from_global_id_with_expected_type(GlobalID.from_id(id), DATASET_NODE_NAME)
1024
+ except Exception as e:
1025
+ raise HTTPException(
1026
+ detail=f"Invalid dataset ID format: {id}",
1027
+ status_code=422,
1028
+ ) from e
1029
+
1014
1030
  dataset_version_id: Optional[int] = None
1015
1031
  if version_id:
1016
- dataset_version_id = from_global_id_with_expected_type(
1017
- GlobalID.from_id(version_id), DATASET_VERSION_NODE_NAME
1018
- )
1032
+ try:
1033
+ dataset_version_id = from_global_id_with_expected_type(
1034
+ GlobalID.from_id(version_id), DATASET_VERSION_NODE_NAME
1035
+ )
1036
+ except Exception as e:
1037
+ raise HTTPException(
1038
+ detail=f"Invalid dataset version ID format: {version_id}",
1039
+ status_code=422,
1040
+ ) from e
1019
1041
  latest_version = (
1020
1042
  select(
1021
1043
  models.DatasetExampleRevision.dataset_example_id,
@@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends, HTTPException, 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 HTTP_404_NOT_FOUND
8
7
  from strawberry.relay import GlobalID
9
8
 
10
9
  from phoenix.db import models
@@ -42,7 +41,7 @@ class AnnotateSpanDocumentsResponseBody(ResponseBody[list[InsertedSpanDocumentAn
42
41
  responses=add_errors_to_responses(
43
42
  [
44
43
  {
45
- "status_code": HTTP_404_NOT_FOUND,
44
+ "status_code": 404,
46
45
  "description": "Span not found",
47
46
  },
48
47
  {
@@ -102,7 +101,7 @@ async def annotate_span_documents(
102
101
  if missing_span_ids:
103
102
  raise HTTPException(
104
103
  detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
105
- status_code=HTTP_404_NOT_FOUND,
104
+ status_code=404,
106
105
  )
107
106
 
108
107
  # Validate that document positions are within bounds
@@ -15,12 +15,6 @@ from starlette.background import BackgroundTask
15
15
  from starlette.datastructures import State
16
16
  from starlette.requests import Request
17
17
  from starlette.responses import Response, StreamingResponse
18
- from starlette.status import (
19
- HTTP_204_NO_CONTENT,
20
- HTTP_404_NOT_FOUND,
21
- HTTP_415_UNSUPPORTED_MEDIA_TYPE,
22
- HTTP_422_UNPROCESSABLE_ENTITY,
23
- )
24
18
  from typing_extensions import TypeAlias
25
19
 
26
20
  import phoenix.trace.v1 as pb
@@ -50,16 +44,16 @@ router = APIRouter(tags=["traces"], include_in_schema=True)
50
44
  dependencies=[Depends(is_not_locked)],
51
45
  operation_id="addEvaluations",
52
46
  summary="Add span, trace, or document evaluations",
53
- status_code=HTTP_204_NO_CONTENT,
47
+ status_code=204,
54
48
  responses=add_errors_to_responses(
55
49
  [
56
50
  {
57
- "status_code": HTTP_415_UNSUPPORTED_MEDIA_TYPE,
51
+ "status_code": 415,
58
52
  "description": (
59
53
  "Unsupported content type, only gzipped protobuf and pandas-arrow are supported"
60
54
  ),
61
55
  },
62
- HTTP_422_UNPROCESSABLE_ENTITY,
56
+ 422,
63
57
  ]
64
58
  ),
65
59
  openapi_extra={
@@ -80,27 +74,21 @@ async def post_evaluations(
80
74
  if content_type == "application/x-pandas-arrow":
81
75
  return await _process_pyarrow(request)
82
76
  if content_type != "application/x-protobuf":
83
- raise HTTPException(
84
- detail="Unsupported content type", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
85
- )
77
+ raise HTTPException(detail="Unsupported content type", status_code=415)
86
78
  body = await request.body()
87
79
  if content_encoding == "gzip":
88
80
  body = gzip.decompress(body)
89
81
  elif content_encoding:
90
- raise HTTPException(
91
- detail="Unsupported content encoding", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
92
- )
82
+ raise HTTPException(detail="Unsupported content encoding", status_code=415)
93
83
  evaluation = pb.Evaluation()
94
84
  try:
95
85
  evaluation.ParseFromString(body)
96
86
  except DecodeError:
97
- raise HTTPException(
98
- detail="Request body is invalid", status_code=HTTP_422_UNPROCESSABLE_ENTITY
99
- )
87
+ raise HTTPException(detail="Request body is invalid", status_code=422)
100
88
  if not evaluation.name.strip():
101
89
  raise HTTPException(
102
90
  detail="Evaluation name must not be blank/empty",
103
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
91
+ status_code=422,
104
92
  )
105
93
  await request.state.enqueue_evaluation(evaluation)
106
94
  return Response()
@@ -110,7 +98,7 @@ async def post_evaluations(
110
98
  "/evaluations",
111
99
  operation_id="getEvaluations",
112
100
  summary="Get span, trace, or document evaluations from a project",
113
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND]),
101
+ responses=add_errors_to_responses([404]),
114
102
  )
115
103
  async def get_evaluations(
116
104
  request: Request,
@@ -149,7 +137,7 @@ async def get_evaluations(
149
137
  and span_evals_dataframe.empty
150
138
  and document_evals_dataframe.empty
151
139
  ):
152
- return Response(status_code=HTTP_404_NOT_FOUND)
140
+ return Response(status_code=404)
153
141
 
154
142
  evals = chain(
155
143
  map(
@@ -179,7 +167,7 @@ async def _process_pyarrow(request: Request) -> Response:
179
167
  except pa.ArrowInvalid:
180
168
  raise HTTPException(
181
169
  detail="Request body is not valid pyarrow",
182
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
170
+ status_code=422,
183
171
  )
184
172
  try:
185
173
  evaluations = Evaluations.from_pyarrow_reader(reader)
@@ -187,11 +175,11 @@ async def _process_pyarrow(request: Request) -> Response:
187
175
  if isinstance(e, PhoenixEvaluationNameIsMissing):
188
176
  raise HTTPException(
189
177
  detail="Evaluation name must not be blank/empty",
190
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
178
+ status_code=422,
191
179
  )
192
180
  raise HTTPException(
193
181
  detail="Invalid data in request body",
194
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
182
+ status_code=422,
195
183
  )
196
184
  return Response(background=BackgroundTask(_add_evaluations, request.state, evaluations))
197
185