arize-phoenix 12.3.0__py3-none-any.whl → 12.4.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 (37) hide show
  1. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.4.0.dist-info}/METADATA +2 -1
  2. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.4.0.dist-info}/RECORD +37 -37
  3. phoenix/auth.py +19 -0
  4. phoenix/config.py +302 -53
  5. phoenix/db/README.md +546 -28
  6. phoenix/server/api/routers/auth.py +21 -30
  7. phoenix/server/api/routers/oauth2.py +213 -24
  8. phoenix/server/api/routers/v1/__init__.py +2 -3
  9. phoenix/server/api/routers/v1/annotation_configs.py +12 -29
  10. phoenix/server/api/routers/v1/annotations.py +21 -22
  11. phoenix/server/api/routers/v1/datasets.py +38 -56
  12. phoenix/server/api/routers/v1/documents.py +2 -3
  13. phoenix/server/api/routers/v1/evaluations.py +12 -24
  14. phoenix/server/api/routers/v1/experiment_evaluations.py +2 -3
  15. phoenix/server/api/routers/v1/experiment_runs.py +9 -10
  16. phoenix/server/api/routers/v1/experiments.py +16 -17
  17. phoenix/server/api/routers/v1/projects.py +15 -21
  18. phoenix/server/api/routers/v1/prompts.py +30 -31
  19. phoenix/server/api/routers/v1/sessions.py +2 -5
  20. phoenix/server/api/routers/v1/spans.py +35 -26
  21. phoenix/server/api/routers/v1/traces.py +11 -19
  22. phoenix/server/api/routers/v1/users.py +14 -23
  23. phoenix/server/api/routers/v1/utils.py +3 -7
  24. phoenix/server/app.py +1 -2
  25. phoenix/server/authorization.py +2 -3
  26. phoenix/server/bearer_auth.py +4 -5
  27. phoenix/server/oauth2.py +172 -5
  28. phoenix/server/static/.vite/manifest.json +9 -9
  29. phoenix/server/static/assets/{components-Bs8eJEpU.js → components-BvsExS75.js} +110 -120
  30. phoenix/server/static/assets/{index-C6WEu5UP.js → index-iq8WDxat.js} +1 -1
  31. phoenix/server/static/assets/{pages-D-n2pkoG.js → pages-Ckg4SLQ9.js} +4 -4
  32. phoenix/trace/attributes.py +80 -13
  33. phoenix/version.py +1 -1
  34. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.4.0.dist-info}/WHEEL +0 -0
  35. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.4.0.dist-info}/entry_points.txt +0 -0
  36. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.4.0.dist-info}/licenses/IP_NOTICE +0 -0
  37. {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,6 @@ from pydantic import ValidationError, model_validator
6
6
  from sqlalchemy import select
7
7
  from sqlalchemy.sql import Select
8
8
  from starlette.requests import Request
9
- from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
10
9
  from strawberry.relay import GlobalID
11
10
  from typing_extensions import Self, TypeAlias, assert_never
12
11
 
@@ -110,7 +109,7 @@ router = APIRouter(tags=["prompts"])
110
109
  response_description="A list of prompts with pagination information",
111
110
  responses=add_errors_to_responses(
112
111
  [
113
- HTTP_422_UNPROCESSABLE_ENTITY,
112
+ 422,
114
113
  ]
115
114
  ),
116
115
  )
@@ -154,7 +153,7 @@ async def get_prompts(
154
153
  except ValueError:
155
154
  raise HTTPException(
156
155
  detail=f"Invalid cursor format: {cursor}",
157
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
156
+ status_code=422,
158
157
  )
159
158
 
160
159
  query = query.limit(limit + 1)
@@ -181,7 +180,7 @@ async def get_prompts(
181
180
  description="Retrieve all versions of a specific prompt with pagination support. Each prompt "
182
181
  "can have multiple versions with different configurations.",
183
182
  response_description="A list of prompt versions with pagination information",
184
- responses=add_errors_to_responses([HTTP_422_UNPROCESSABLE_ENTITY, HTTP_404_NOT_FOUND]),
183
+ responses=add_errors_to_responses([422, 404]),
185
184
  response_model_by_alias=True,
186
185
  response_model_exclude_defaults=True,
187
186
  response_model_exclude_unset=True,
@@ -226,7 +225,7 @@ async def list_prompt_versions(
226
225
  except ValueError:
227
226
  raise HTTPException(
228
227
  detail=f"Invalid cursor format: {cursor}",
229
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
228
+ status_code=422,
230
229
  )
231
230
 
232
231
  query = query.limit(limit + 1)
@@ -255,8 +254,8 @@ async def list_prompt_versions(
255
254
  response_description="The requested prompt version",
256
255
  responses=add_errors_to_responses(
257
256
  [
258
- HTTP_404_NOT_FOUND,
259
- HTTP_422_UNPROCESSABLE_ENTITY,
257
+ 404,
258
+ 422,
260
259
  ]
261
260
  ),
262
261
  response_model_by_alias=True,
@@ -286,11 +285,11 @@ async def get_prompt_version_by_prompt_version_id(
286
285
  PromptVersionNodeType.__name__,
287
286
  )
288
287
  except ValueError:
289
- raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, "Invalid prompt version ID")
288
+ raise HTTPException(422, "Invalid prompt version ID")
290
289
  async with request.app.state.db() as session:
291
290
  prompt_version = await session.get(models.PromptVersion, id_)
292
291
  if prompt_version is None:
293
- raise HTTPException(HTTP_404_NOT_FOUND)
292
+ raise HTTPException(404)
294
293
  data = _prompt_version_from_orm_version(prompt_version)
295
294
  return GetPromptResponseBody(data=data)
296
295
 
@@ -304,8 +303,8 @@ async def get_prompt_version_by_prompt_version_id(
304
303
  response_description="The prompt version with the specified tag",
305
304
  responses=add_errors_to_responses(
306
305
  [
307
- HTTP_404_NOT_FOUND,
308
- HTTP_422_UNPROCESSABLE_ENTITY,
306
+ 404,
307
+ 422,
309
308
  ]
310
309
  ),
311
310
  response_model_by_alias=True,
@@ -334,7 +333,7 @@ async def get_prompt_version_by_tag_name(
334
333
  try:
335
334
  name = Identifier.model_validate(tag_name)
336
335
  except ValidationError:
337
- raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, "Invalid tag name")
336
+ raise HTTPException(422, "Invalid tag name")
338
337
  stmt = (
339
338
  select(models.PromptVersion)
340
339
  .join_from(models.PromptVersion, models.PromptVersionTag)
@@ -344,7 +343,7 @@ async def get_prompt_version_by_tag_name(
344
343
  async with request.app.state.db() as session:
345
344
  prompt_version: models.PromptVersion = await session.scalar(stmt)
346
345
  if prompt_version is None:
347
- raise HTTPException(HTTP_404_NOT_FOUND)
346
+ raise HTTPException(404)
348
347
  data = _prompt_version_from_orm_version(prompt_version)
349
348
  return GetPromptResponseBody(data=data)
350
349
 
@@ -357,8 +356,8 @@ async def get_prompt_version_by_tag_name(
357
356
  response_description="The latest version of the specified prompt",
358
357
  responses=add_errors_to_responses(
359
358
  [
360
- HTTP_404_NOT_FOUND,
361
- HTTP_422_UNPROCESSABLE_ENTITY,
359
+ 404,
360
+ 422,
362
361
  ]
363
362
  ),
364
363
  response_model_by_alias=True,
@@ -387,7 +386,7 @@ async def get_prompt_version_by_latest(
387
386
  async with request.app.state.db() as session:
388
387
  prompt_version: models.PromptVersion = await session.scalar(stmt)
389
388
  if prompt_version is None:
390
- raise HTTPException(HTTP_404_NOT_FOUND)
389
+ raise HTTPException(404)
391
390
  data = _prompt_version_from_orm_version(prompt_version)
392
391
  return GetPromptResponseBody(data=data)
393
392
 
@@ -401,7 +400,7 @@ async def get_prompt_version_by_latest(
401
400
  response_description="The newly created prompt version",
402
401
  responses=add_errors_to_responses(
403
402
  [
404
- HTTP_422_UNPROCESSABLE_ENTITY,
403
+ 422,
405
404
  ]
406
405
  ),
407
406
  response_model_by_alias=True,
@@ -431,7 +430,7 @@ async def create_prompt(
431
430
  or request_body.version.template_type != PromptTemplateType.CHAT
432
431
  ):
433
432
  raise HTTPException(
434
- HTTP_422_UNPROCESSABLE_ENTITY,
433
+ 422,
435
434
  "Only CHAT template type is supported for prompts",
436
435
  )
437
436
  prompt = request_body.prompt
@@ -439,7 +438,7 @@ async def create_prompt(
439
438
  name = Identifier.model_validate(prompt.name)
440
439
  except ValidationError as e:
441
440
  raise HTTPException(
442
- HTTP_422_UNPROCESSABLE_ENTITY,
441
+ 422,
443
442
  "Invalid name identifier for prompt: " + e.errors()[0]["msg"],
444
443
  )
445
444
  version = request_body.version
@@ -496,8 +495,8 @@ class GetPromptVersionTagsResponseBody(PaginatedResponseBody[PromptVersionTag]):
496
495
  response_description="A list of tags associated with the prompt version",
497
496
  responses=add_errors_to_responses(
498
497
  [
499
- HTTP_404_NOT_FOUND,
500
- HTTP_422_UNPROCESSABLE_ENTITY,
498
+ 404,
499
+ 422,
501
500
  ]
502
501
  ),
503
502
  response_model_by_alias=True,
@@ -537,7 +536,7 @@ async def list_prompt_version_tags(
537
536
  PromptVersionNodeType.__name__,
538
537
  )
539
538
  except ValueError:
540
- raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, "Invalid prompt version ID")
539
+ raise HTTPException(422, "Invalid prompt version ID")
541
540
 
542
541
  # Build the query for tags
543
542
  stmt = (
@@ -560,7 +559,7 @@ async def list_prompt_version_tags(
560
559
  except ValueError:
561
560
  raise HTTPException(
562
561
  detail=f"Invalid cursor format: {cursor}",
563
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
562
+ status_code=422,
564
563
  )
565
564
 
566
565
  # Apply limit
@@ -571,7 +570,7 @@ async def list_prompt_version_tags(
571
570
 
572
571
  # Check if prompt version exists
573
572
  if not result:
574
- raise HTTPException(HTTP_404_NOT_FOUND, "Prompt version not found")
573
+ raise HTTPException(404, "Prompt version not found")
575
574
 
576
575
  # Check if there are any tags
577
576
  has_tags = any(id_ is not None for _, id_, _, _ in result)
@@ -610,11 +609,11 @@ async def list_prompt_version_tags(
610
609
  description="Add a new tag to a specific prompt version. Tags help identify and categorize "
611
610
  "different versions of a prompt.",
612
611
  response_description="No content returned on successful tag creation",
613
- status_code=HTTP_204_NO_CONTENT,
612
+ status_code=204,
614
613
  responses=add_errors_to_responses(
615
614
  [
616
- HTTP_404_NOT_FOUND,
617
- HTTP_422_UNPROCESSABLE_ENTITY,
615
+ 404,
616
+ 422,
618
617
  ]
619
618
  ),
620
619
  response_model_by_alias=True,
@@ -647,7 +646,7 @@ async def create_prompt_version_tag(
647
646
  PromptVersionNodeType.__name__,
648
647
  )
649
648
  except ValueError:
650
- raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, "Invalid prompt version ID")
649
+ raise HTTPException(422, "Invalid prompt version ID")
651
650
  user_id: Optional[int] = None
652
651
  if request.app.state.authentication_enabled:
653
652
  assert isinstance(user := request.user, PhoenixUser)
@@ -655,7 +654,7 @@ async def create_prompt_version_tag(
655
654
  async with request.app.state.db() as session:
656
655
  prompt_id = await session.scalar(select(models.PromptVersion.prompt_id).filter_by(id=id_))
657
656
  if prompt_id is None:
658
- raise HTTPException(HTTP_404_NOT_FOUND)
657
+ raise HTTPException(404)
659
658
  dialect = SupportedSQLDialect(session.bind.dialect.name)
660
659
  values = dict(
661
660
  name=request_body.name,
@@ -686,7 +685,7 @@ def _parse_prompt_identifier(
686
685
  prompt_identifier: str,
687
686
  ) -> _PromptIdentifier:
688
687
  if not prompt_identifier:
689
- raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, "Invalid prompt identifier")
688
+ raise HTTPException(422, "Invalid prompt identifier")
690
689
  try:
691
690
  prompt_id = from_global_id_with_expected_type(
692
691
  GlobalID.from_id(prompt_identifier),
@@ -696,7 +695,7 @@ def _parse_prompt_identifier(
696
695
  try:
697
696
  return Identifier.model_validate(prompt_identifier)
698
697
  except ValidationError:
699
- raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY, "Invalid prompt name")
698
+ raise HTTPException(422, "Invalid prompt name")
700
699
  return _PromptId(prompt_id)
701
700
 
702
701
 
@@ -7,7 +7,6 @@ from fastapi import APIRouter, Depends, HTTPException, Query
7
7
  from pydantic import Field
8
8
  from sqlalchemy import select
9
9
  from starlette.requests import Request
10
- from starlette.status import HTTP_404_NOT_FOUND
11
10
 
12
11
  from phoenix.db import models
13
12
  from phoenix.db.helpers import SupportedSQLDialect
@@ -39,9 +38,7 @@ class AnnotateSessionsResponseBody(ResponseBody[list[InsertedSessionAnnotation]]
39
38
  dependencies=[Depends(is_not_locked)],
40
39
  operation_id="annotateSessions",
41
40
  summary="Create session annotations",
42
- responses=add_errors_to_responses(
43
- [{"status_code": HTTP_404_NOT_FOUND, "description": "Session not found"}]
44
- ),
41
+ responses=add_errors_to_responses([{"status_code": 404, "description": "Session not found"}]),
45
42
  response_description="Session annotations inserted successfully",
46
43
  include_in_schema=True,
47
44
  )
@@ -88,7 +85,7 @@ async def annotate_sessions(
88
85
  if missing_session_ids:
89
86
  raise HTTPException(
90
87
  detail=f"Sessions with IDs {', '.join(missing_session_ids)} do not exist.",
91
- status_code=HTTP_404_NOT_FOUND,
88
+ status_code=404,
92
89
  )
93
90
 
94
91
  async with request.app.state.db() as session:
@@ -14,12 +14,7 @@ from pydantic import BaseModel, Field
14
14
  from sqlalchemy import exists, select, update
15
15
  from starlette.requests import Request
16
16
  from starlette.responses import Response, StreamingResponse
17
- from starlette.status import (
18
- HTTP_202_ACCEPTED,
19
- HTTP_400_BAD_REQUEST,
20
- HTTP_404_NOT_FOUND,
21
- HTTP_422_UNPROCESSABLE_ENTITY,
22
- )
17
+ from starlette.status import HTTP_404_NOT_FOUND
23
18
  from strawberry.relay import GlobalID
24
19
 
25
20
  from phoenix.config import DEFAULT_PROJECT_NAME
@@ -33,7 +28,7 @@ from phoenix.server.api.types.node import from_global_id_with_expected_type
33
28
  from phoenix.server.authorization import is_not_locked
34
29
  from phoenix.server.bearer_auth import PhoenixUser
35
30
  from phoenix.server.dml_event import SpanAnnotationInsertEvent, SpanDeleteEvent
36
- from phoenix.trace.attributes import flatten
31
+ from phoenix.trace.attributes import flatten, unflatten
37
32
  from phoenix.trace.dsl import SpanQuery as SpanQuery_
38
33
  from phoenix.trace.schemas import (
39
34
  Span as SpanForInsertion,
@@ -440,7 +435,7 @@ class SpansResponseBody(PaginatedResponseBody[Span]):
440
435
  "/spans",
441
436
  operation_id="querySpans",
442
437
  summary="Query spans with query DSL",
443
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
438
+ responses=add_errors_to_responses([404, 422]),
444
439
  include_in_schema=False,
445
440
  )
446
441
  async def query_spans_handler(
@@ -467,7 +462,7 @@ async def query_spans_handler(
467
462
  except Exception as e:
468
463
  raise HTTPException(
469
464
  detail=f"Invalid query: {e}",
470
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
465
+ status_code=422,
471
466
  )
472
467
 
473
468
  async with request.app.state.db() as session:
@@ -490,7 +485,7 @@ async def query_spans_handler(
490
485
  )
491
486
  results.append(df)
492
487
  if not results:
493
- raise HTTPException(status_code=HTTP_404_NOT_FOUND)
488
+ raise HTTPException(status_code=404)
494
489
 
495
490
  if accept == "application/json":
496
491
  boundary_token = token_urlsafe(64)
@@ -574,7 +569,7 @@ def _to_any_value(value: Any) -> OtlpAnyValue:
574
569
  summary="Search spans with simple filters (no DSL)",
575
570
  description="Return spans within a project filtered by time range. "
576
571
  "Supports cursor-based pagination.",
577
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
572
+ responses=add_errors_to_responses([404, 422]),
578
573
  )
579
574
  async def span_search_otlpv1(
580
575
  request: Request,
@@ -617,7 +612,7 @@ async def span_search_otlpv1(
617
612
  cursor_rowid = int(GlobalID.from_id(cursor).node_id)
618
613
  stmt = stmt.where(models.Span.id <= cursor_rowid)
619
614
  except Exception:
620
- raise HTTPException(status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid cursor")
615
+ raise HTTPException(status_code=422, detail="Invalid cursor")
621
616
 
622
617
  stmt = stmt.limit(limit + 1)
623
618
 
@@ -711,7 +706,7 @@ async def span_search_otlpv1(
711
706
  summary="List spans with simple filters (no DSL)",
712
707
  description="Return spans within a project filtered by time range. "
713
708
  "Supports cursor-based pagination.",
714
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
709
+ responses=add_errors_to_responses([404, 422]),
715
710
  )
716
711
  async def span_search(
717
712
  request: Request,
@@ -751,7 +746,7 @@ async def span_search(
751
746
  try:
752
747
  cursor_rowid = int(GlobalID.from_id(cursor).node_id)
753
748
  except Exception:
754
- raise HTTPException(status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid cursor")
749
+ raise HTTPException(status_code=422, detail="Invalid cursor")
755
750
  stmt = stmt.where(models.Span.id <= cursor_rowid)
756
751
 
757
752
  stmt = stmt.limit(limit + 1)
@@ -867,9 +862,7 @@ class AnnotateSpansResponseBody(ResponseBody[list[InsertedSpanAnnotation]]):
867
862
  dependencies=[Depends(is_not_locked)],
868
863
  operation_id="annotateSpans",
869
864
  summary="Create span annotations",
870
- responses=add_errors_to_responses(
871
- [{"status_code": HTTP_404_NOT_FOUND, "description": "Span not found"}]
872
- ),
865
+ responses=add_errors_to_responses([{"status_code": 404, "description": "Span not found"}]),
873
866
  response_description="Span annotations inserted successfully",
874
867
  include_in_schema=True,
875
868
  )
@@ -915,7 +908,7 @@ async def annotate_spans(
915
908
  if missing_span_ids:
916
909
  raise HTTPException(
917
910
  detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
918
- status_code=HTTP_404_NOT_FOUND,
911
+ status_code=404,
919
912
  )
920
913
  inserted_ids = []
921
914
  dialect = SupportedSQLDialect(session.bind.dialect.name)
@@ -957,8 +950,8 @@ class CreateSpansResponseBody(V1RoutesBaseModel):
957
950
  "Submit spans to be inserted into a project. If any spans are invalid or "
958
951
  "duplicates, no spans will be inserted."
959
952
  ),
960
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_400_BAD_REQUEST]),
961
- status_code=HTTP_202_ACCEPTED,
953
+ responses=add_errors_to_responses([404, 400]),
954
+ status_code=202,
962
955
  )
963
956
  async def create_spans(
964
957
  request: Request,
@@ -997,6 +990,7 @@ async def create_spans(
997
990
  # Add back the openinference.span.kind attribute since it's stored separately in the API
998
991
  attributes = dict(api_span.attributes)
999
992
  attributes["openinference.span.kind"] = api_span.span_kind
993
+ attributes = unflatten(attributes.items())
1000
994
 
1001
995
  # Create span for insertion - note we ignore the 'id' field as it's server-generated
1002
996
  return SpanForInsertion(
@@ -1015,8 +1009,23 @@ async def create_spans(
1015
1009
  conversation=None, # Unused
1016
1010
  )
1017
1011
 
1018
- async with request.app.state.db() as session:
1019
- project = await _get_project_by_identifier(session, project_identifier)
1012
+ try:
1013
+ id_ = from_global_id_with_expected_type(
1014
+ GlobalID.from_id(project_identifier),
1015
+ "Project",
1016
+ )
1017
+ except Exception:
1018
+ project_name = project_identifier
1019
+ else:
1020
+ stmt = select(models.Project).filter_by(id=id_)
1021
+ async with request.app.state.db() as session:
1022
+ project = await session.scalar(stmt)
1023
+ if project is None:
1024
+ raise HTTPException(
1025
+ status_code=HTTP_404_NOT_FOUND,
1026
+ detail=f"Project with ID {project_identifier} not found",
1027
+ )
1028
+ project_name = project.name
1020
1029
 
1021
1030
  total_received = len(request_body.data)
1022
1031
  duplicate_spans: list[dict[str, str]] = []
@@ -1044,7 +1053,7 @@ async def create_spans(
1044
1053
 
1045
1054
  try:
1046
1055
  span_for_insertion = convert_api_span_for_insertion(api_span)
1047
- spans_to_queue.append((span_for_insertion, project.name))
1056
+ spans_to_queue.append((span_for_insertion, project_name))
1048
1057
  except Exception as e:
1049
1058
  invalid_spans.append(
1050
1059
  {
@@ -1066,7 +1075,7 @@ async def create_spans(
1066
1075
  "invalid_spans": invalid_spans,
1067
1076
  }
1068
1077
  raise HTTPException(
1069
- status_code=HTTP_400_BAD_REQUEST,
1078
+ status_code=400,
1070
1079
  detail=json.dumps(error_detail),
1071
1080
  )
1072
1081
 
@@ -1102,7 +1111,7 @@ async def create_spans(
1102
1111
  **Note**: This operation is irreversible and may create orphaned spans.
1103
1112
  """
1104
1113
  ),
1105
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND]),
1114
+ responses=add_errors_to_responses([404]),
1106
1115
  status_code=204, # No Content for successful deletion
1107
1116
  )
1108
1117
  async def delete_span(
@@ -1154,7 +1163,7 @@ async def delete_span(
1154
1163
 
1155
1164
  if target_span is None:
1156
1165
  raise HTTPException(
1157
- status_code=HTTP_404_NOT_FOUND,
1166
+ status_code=404,
1158
1167
  detail=error_detail,
1159
1168
  )
1160
1169
 
@@ -14,12 +14,6 @@ from starlette.concurrency import run_in_threadpool
14
14
  from starlette.datastructures import State
15
15
  from starlette.requests import Request
16
16
  from starlette.responses import Response
17
- from starlette.status import (
18
- HTTP_404_NOT_FOUND,
19
- HTTP_415_UNSUPPORTED_MEDIA_TYPE,
20
- HTTP_422_UNPROCESSABLE_ENTITY,
21
- HTTP_503_SERVICE_UNAVAILABLE,
22
- )
23
17
  from strawberry.relay import GlobalID
24
18
 
25
19
  from phoenix.db import models
@@ -49,7 +43,7 @@ def is_not_at_capacity(request: Request) -> None:
49
43
  SPAN_QUEUE_REJECTIONS.inc()
50
44
  raise HTTPException(
51
45
  detail="Server is at capacity and cannot process more requests",
52
- status_code=HTTP_503_SERVICE_UNAVAILABLE,
46
+ status_code=503,
53
47
  )
54
48
 
55
49
 
@@ -61,14 +55,14 @@ def is_not_at_capacity(request: Request) -> None:
61
55
  responses=add_errors_to_responses(
62
56
  [
63
57
  {
64
- "status_code": HTTP_415_UNSUPPORTED_MEDIA_TYPE,
58
+ "status_code": 415,
65
59
  "description": (
66
60
  "Unsupported content type (only `application/x-protobuf` is supported)"
67
61
  ),
68
62
  },
69
- {"status_code": HTTP_422_UNPROCESSABLE_ENTITY, "description": "Invalid request body"},
63
+ {"status_code": 422, "description": "Invalid request body"},
70
64
  {
71
- "status_code": HTTP_503_SERVICE_UNAVAILABLE,
65
+ "status_code": 503,
72
66
  "description": "Server is at capacity and cannot process more requests",
73
67
  },
74
68
  ]
@@ -92,12 +86,12 @@ async def post_traces(
92
86
  if content_type != "application/x-protobuf":
93
87
  raise HTTPException(
94
88
  detail=f"Unsupported content type: {content_type}",
95
- status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE,
89
+ status_code=415,
96
90
  )
97
91
  if content_encoding and content_encoding not in ("gzip", "deflate"):
98
92
  raise HTTPException(
99
93
  detail=f"Unsupported content encoding: {content_encoding}",
100
- status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE,
94
+ status_code=415,
101
95
  )
102
96
  body = await request.body()
103
97
  if content_encoding == "gzip":
@@ -110,7 +104,7 @@ async def post_traces(
110
104
  except DecodeError:
111
105
  raise HTTPException(
112
106
  detail="Request body is invalid ExportTraceServiceRequest",
113
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
107
+ status_code=422,
114
108
  )
115
109
  background_tasks.add_task(_add_spans, req, request.state)
116
110
 
@@ -141,9 +135,7 @@ class AnnotateTracesResponseBody(ResponseBody[list[InsertedTraceAnnotation]]):
141
135
  dependencies=[Depends(is_not_locked)],
142
136
  operation_id="annotateTraces",
143
137
  summary="Create trace annotations",
144
- responses=add_errors_to_responses(
145
- [{"status_code": HTTP_404_NOT_FOUND, "description": "Trace not found"}]
146
- ),
138
+ responses=add_errors_to_responses([{"status_code": 404, "description": "Trace not found"}]),
147
139
  )
148
140
  async def annotate_traces(
149
141
  request: Request,
@@ -177,7 +169,7 @@ async def annotate_traces(
177
169
  if missing_trace_ids:
178
170
  raise HTTPException(
179
171
  detail=f"Traces with IDs {', '.join(missing_trace_ids)} do not exist.",
180
- status_code=HTTP_404_NOT_FOUND,
172
+ status_code=404,
181
173
  )
182
174
  inserted_ids = []
183
175
  dialect = SupportedSQLDialect(session.bind.dialect.name)
@@ -220,7 +212,7 @@ async def _add_spans(req: ExportTraceServiceRequest, state: State) -> None:
220
212
  "2. An OpenTelemetry trace_id (hex string)\n\n"
221
213
  "This will permanently remove all spans in the trace and their associated data."
222
214
  ),
223
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND]),
215
+ responses=add_errors_to_responses([404]),
224
216
  status_code=204, # No Content for successful deletion
225
217
  )
226
218
  async def delete_trace(
@@ -268,7 +260,7 @@ async def delete_trace(
268
260
 
269
261
  if project_id is None:
270
262
  raise HTTPException(
271
- status_code=HTTP_404_NOT_FOUND,
263
+ status_code=404,
272
264
  detail=error_detail,
273
265
  )
274
266
 
@@ -12,15 +12,6 @@ from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
12
12
  from sqlalchemy.orm import joinedload
13
13
  from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
14
14
  from starlette.datastructures import Secret
15
- from starlette.status import (
16
- HTTP_201_CREATED,
17
- HTTP_204_NO_CONTENT,
18
- HTTP_400_BAD_REQUEST,
19
- HTTP_403_FORBIDDEN,
20
- HTTP_404_NOT_FOUND,
21
- HTTP_409_CONFLICT,
22
- HTTP_422_UNPROCESSABLE_ENTITY,
23
- )
24
15
  from strawberry.relay import GlobalID
25
16
  from typing_extensions import TypeAlias, assert_never
26
17
 
@@ -113,7 +104,7 @@ DEFAULT_PAGINATION_PAGE_LIMIT = 100
113
104
  response_description="A list of users.",
114
105
  responses=add_errors_to_responses(
115
106
  [
116
- HTTP_422_UNPROCESSABLE_ENTITY,
107
+ 422,
117
108
  ],
118
109
  ),
119
110
  dependencies=[Depends(require_admin)],
@@ -187,12 +178,12 @@ async def list_users(
187
178
  summary="Create a new user",
188
179
  description="Create a new user with the specified configuration.",
189
180
  response_description="The newly created user.",
190
- status_code=HTTP_201_CREATED,
181
+ status_code=201,
191
182
  responses=add_errors_to_responses(
192
183
  [
193
- {"status_code": HTTP_400_BAD_REQUEST, "description": "Role not found."},
194
- {"status_code": HTTP_409_CONFLICT, "description": "Username or email already exists."},
195
- HTTP_422_UNPROCESSABLE_ENTITY,
184
+ {"status_code": 400, "description": "Role not found."},
185
+ {"status_code": 409, "description": "Username or email already exists."},
186
+ 422,
196
187
  ]
197
188
  ),
198
189
  dependencies=[Depends(require_admin), Depends(is_not_locked)],
@@ -213,14 +204,14 @@ async def create_user(
213
204
  # Prevent creation of SYSTEM users
214
205
  if role == "SYSTEM":
215
206
  raise HTTPException(
216
- status_code=HTTP_400_BAD_REQUEST,
207
+ status_code=400,
217
208
  detail="Cannot create users with SYSTEM role",
218
209
  )
219
210
 
220
211
  # TODO: Implement VIEWER role
221
212
  if role == "VIEWER":
222
213
  raise HTTPException(
223
- status_code=HTTP_400_BAD_REQUEST,
214
+ status_code=400,
224
215
  detail="VIEWER role not yet implemented",
225
216
  )
226
217
 
@@ -259,12 +250,12 @@ async def create_user(
259
250
  session.add(user)
260
251
  except (PostgreSQLIntegrityError, SQLiteIntegrityError) as e:
261
252
  if "users.username" in str(e):
262
- raise HTTPException(status_code=HTTP_409_CONFLICT, detail="Username already exists")
253
+ raise HTTPException(status_code=409, detail="Username already exists")
263
254
  elif "users.email" in str(e):
264
- raise HTTPException(status_code=HTTP_409_CONFLICT, detail="Email already exists")
255
+ raise HTTPException(status_code=409, detail="Email already exists")
265
256
  else:
266
257
  raise HTTPException(
267
- status_code=HTTP_409_CONFLICT,
258
+ status_code=409,
268
259
  detail="Failed to create user due to a conflict with existing data",
269
260
  )
270
261
  id_ = str(GlobalID("User", str(user.id)))
@@ -314,13 +305,13 @@ async def create_user(
314
305
  summary="Delete a user by ID",
315
306
  description="Delete an existing user by their unique GlobalID.",
316
307
  response_description="No content returned on successful deletion.",
317
- status_code=HTTP_204_NO_CONTENT,
308
+ status_code=204,
318
309
  responses=add_errors_to_responses(
319
310
  [
320
- {"status_code": HTTP_404_NOT_FOUND, "description": "User not found."},
321
- HTTP_422_UNPROCESSABLE_ENTITY,
311
+ {"status_code": 404, "description": "User not found."},
312
+ 422,
322
313
  {
323
- "status_code": HTTP_403_FORBIDDEN,
314
+ "status_code": 403,
324
315
  "description": "Cannot delete the default admin or system user",
325
316
  },
326
317
  ]
@@ -3,10 +3,6 @@ from typing import Any, Generic, Optional, TypedDict, TypeVar, Union
3
3
  from fastapi import HTTPException
4
4
  from sqlalchemy import select
5
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
- from starlette.status import (
7
- HTTP_404_NOT_FOUND,
8
- HTTP_422_UNPROCESSABLE_ENTITY,
9
- )
10
6
  from strawberry.relay import GlobalID
11
7
  from typing_extensions import TypeAlias, assert_never
12
8
 
@@ -135,21 +131,21 @@ async def _get_project_by_identifier(
135
131
  name = project_identifier
136
132
  except HTTPException:
137
133
  raise HTTPException(
138
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
134
+ status_code=422,
139
135
  detail=f"Invalid project identifier format: {project_identifier}",
140
136
  )
141
137
  stmt = select(models.Project).filter_by(name=name)
142
138
  project = await session.scalar(stmt)
143
139
  if project is None:
144
140
  raise HTTPException(
145
- status_code=HTTP_404_NOT_FOUND,
141
+ status_code=404,
146
142
  detail=f"Project with name {name} not found",
147
143
  )
148
144
  else:
149
145
  project = await session.get(models.Project, id_)
150
146
  if project is None:
151
147
  raise HTTPException(
152
- status_code=HTTP_404_NOT_FOUND,
148
+ status_code=404,
153
149
  detail=f"Project with ID {project_identifier} not found",
154
150
  )
155
151
  return project
phoenix/server/app.py CHANGED
@@ -45,7 +45,6 @@ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoin
45
45
  from starlette.requests import Request
46
46
  from starlette.responses import JSONResponse, PlainTextResponse, RedirectResponse, Response
47
47
  from starlette.staticfiles import StaticFiles
48
- from starlette.status import HTTP_401_UNAUTHORIZED
49
48
  from starlette.templating import Jinja2Templates
50
49
  from starlette.types import Scope, StatefulLifespan
51
50
  from strawberry.extensions import SchemaExtension
@@ -352,7 +351,7 @@ class RequestOriginHostnameValidator(BaseHTTPMiddleware):
352
351
  if not (url := headers.get(key)):
353
352
  continue
354
353
  if urlparse(url).hostname not in self._trusted_hostnames:
355
- return Response(f"untrusted {key}", status_code=HTTP_401_UNAUTHORIZED)
354
+ return Response(f"untrusted {key}", status_code=401)
356
355
  return await call_next(request)
357
356
 
358
357