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
|
@@ -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
|
-
|
|
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=
|
|
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([
|
|
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=
|
|
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
|
-
|
|
259
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
308
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
361
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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(
|
|
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=
|
|
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(
|
|
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=
|
|
612
|
+
status_code=204,
|
|
614
613
|
responses=add_errors_to_responses(
|
|
615
614
|
[
|
|
616
|
-
|
|
617
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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=
|
|
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([
|
|
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=
|
|
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=
|
|
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([
|
|
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=
|
|
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([
|
|
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=
|
|
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=
|
|
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([
|
|
961
|
-
status_code=
|
|
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
|
-
|
|
1019
|
-
|
|
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,
|
|
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=
|
|
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([
|
|
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=
|
|
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=
|
|
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":
|
|
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":
|
|
63
|
+
{"status_code": 422, "description": "Invalid request body"},
|
|
70
64
|
{
|
|
71
|
-
"status_code":
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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([
|
|
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=
|
|
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
|
-
|
|
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=
|
|
181
|
+
status_code=201,
|
|
191
182
|
responses=add_errors_to_responses(
|
|
192
183
|
[
|
|
193
|
-
{"status_code":
|
|
194
|
-
{"status_code":
|
|
195
|
-
|
|
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,17 +204,10 @@ async def create_user(
|
|
|
213
204
|
# Prevent creation of SYSTEM users
|
|
214
205
|
if role == "SYSTEM":
|
|
215
206
|
raise HTTPException(
|
|
216
|
-
status_code=
|
|
207
|
+
status_code=400,
|
|
217
208
|
detail="Cannot create users with SYSTEM role",
|
|
218
209
|
)
|
|
219
210
|
|
|
220
|
-
# TODO: Implement VIEWER role
|
|
221
|
-
if role == "VIEWER":
|
|
222
|
-
raise HTTPException(
|
|
223
|
-
status_code=HTTP_400_BAD_REQUEST,
|
|
224
|
-
detail="VIEWER role not yet implemented",
|
|
225
|
-
)
|
|
226
|
-
|
|
227
211
|
user: models.User
|
|
228
212
|
if isinstance(user_data, LocalUserData):
|
|
229
213
|
password = (user_data.password or secrets.token_hex()).strip()
|
|
@@ -259,12 +243,12 @@ async def create_user(
|
|
|
259
243
|
session.add(user)
|
|
260
244
|
except (PostgreSQLIntegrityError, SQLiteIntegrityError) as e:
|
|
261
245
|
if "users.username" in str(e):
|
|
262
|
-
raise HTTPException(status_code=
|
|
246
|
+
raise HTTPException(status_code=409, detail="Username already exists")
|
|
263
247
|
elif "users.email" in str(e):
|
|
264
|
-
raise HTTPException(status_code=
|
|
248
|
+
raise HTTPException(status_code=409, detail="Email already exists")
|
|
265
249
|
else:
|
|
266
250
|
raise HTTPException(
|
|
267
|
-
status_code=
|
|
251
|
+
status_code=409,
|
|
268
252
|
detail="Failed to create user due to a conflict with existing data",
|
|
269
253
|
)
|
|
270
254
|
id_ = str(GlobalID("User", str(user.id)))
|
|
@@ -314,13 +298,13 @@ async def create_user(
|
|
|
314
298
|
summary="Delete a user by ID",
|
|
315
299
|
description="Delete an existing user by their unique GlobalID.",
|
|
316
300
|
response_description="No content returned on successful deletion.",
|
|
317
|
-
status_code=
|
|
301
|
+
status_code=204,
|
|
318
302
|
responses=add_errors_to_responses(
|
|
319
303
|
[
|
|
320
|
-
{"status_code":
|
|
321
|
-
|
|
304
|
+
{"status_code": 404, "description": "User not found."},
|
|
305
|
+
422,
|
|
322
306
|
{
|
|
323
|
-
"status_code":
|
|
307
|
+
"status_code": 403,
|
|
324
308
|
"description": "Cannot delete the default admin or system user",
|
|
325
309
|
},
|
|
326
310
|
]
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
148
|
+
status_code=404,
|
|
153
149
|
detail=f"Project with ID {project_identifier} not found",
|
|
154
150
|
)
|
|
155
151
|
return project
|