arize-phoenix 4.12.0rc1__py3-none-any.whl → 4.12.1rc1__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: arize-phoenix
3
- Version: 4.12.0rc1
3
+ Version: 4.12.1rc1
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -22,6 +22,7 @@ Requires-Dist: aiosqlite
22
22
  Requires-Dist: alembic<2,>=1.3.0
23
23
  Requires-Dist: arize-phoenix-evals>=0.13.1
24
24
  Requires-Dist: cachetools
25
+ Requires-Dist: fastapi
25
26
  Requires-Dist: grpcio
26
27
  Requires-Dist: hdbscan>=0.8.33
27
28
  Requires-Dist: httpx
@@ -40,8 +41,8 @@ Requires-Dist: pandas>=1.0
40
41
  Requires-Dist: protobuf<6.0,>=3.20
41
42
  Requires-Dist: psutil
42
43
  Requires-Dist: pyarrow
44
+ Requires-Dist: pydantic!=2.0.*,<3,>=1.0
43
45
  Requires-Dist: python-multipart
44
- Requires-Dist: pyyaml
45
46
  Requires-Dist: scikit-learn
46
47
  Requires-Dist: scipy
47
48
  Requires-Dist: sqlalchemy[asyncio]<3,>=2.0.4
@@ -56,9 +57,9 @@ Requires-Dist: uvicorn
56
57
  Requires-Dist: wrapt
57
58
  Provides-Extra: container
58
59
  Requires-Dist: opentelemetry-exporter-otlp; extra == 'container'
60
+ Requires-Dist: opentelemetry-instrumentation-fastapi; extra == 'container'
59
61
  Requires-Dist: opentelemetry-instrumentation-grpc; extra == 'container'
60
62
  Requires-Dist: opentelemetry-instrumentation-sqlalchemy; extra == 'container'
61
- Requires-Dist: opentelemetry-instrumentation-starlette; extra == 'container'
62
63
  Requires-Dist: opentelemetry-proto>=1.12.0; extra == 'container'
63
64
  Requires-Dist: opentelemetry-sdk; extra == 'container'
64
65
  Requires-Dist: opentelemetry-semantic-conventions; extra == 'container'
@@ -5,7 +5,7 @@ phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
5
5
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
6
  phoenix/services.py,sha256=aTxhcOA1pZHB6U-B3TEcp6fqDF5oT0xCUvEUNMZVTUQ,5175
7
7
  phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
8
- phoenix/version.py,sha256=vDTXhJ8GZFVcgTBE3Q26tNDA2kuyeZwK6HnXlO91H70,26
8
+ phoenix/version.py,sha256=C3HccMWHAx9lnG5eI9Z6SvQ8MEwqYKi1vrfgajsjMXM,26
9
9
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
11
11
  phoenix/core/model.py,sha256=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
@@ -60,14 +60,14 @@ phoenix/pointcloud/pointcloud.py,sha256=4zAIkKs2xOUbchpj4XDAV-iPMXrfAJ15TG6rlIYG
60
60
  phoenix/pointcloud/projectors.py,sha256=zO_RrtDYSv2rqVOfIP2_9Cv11Dc8EmcZR94xhFcBYPU,1057
61
61
  phoenix/pointcloud/umap_parameters.py,sha256=3UQSjrysVOvq2V4KNpTMqNqNiK0BsTZnPBHWZ4fyJtQ,1708
62
62
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- phoenix/server/app.py,sha256=u8s2AEUUDsY1uIpk3d8cMVHnAL6N57Ulb-k88kVvbMA,19673
63
+ phoenix/server/app.py,sha256=g-UDuF90bnE1nnW6peZY_-1_pplP4ERb2bnD2vERfRg,18758
64
64
  phoenix/server/grpc_server.py,sha256=faktLxEtWGlCB1bPR4QwwTsRoQloahKMx0hAWqRGI5s,3379
65
65
  phoenix/server/main.py,sha256=dRyODpwkNi_3as14fnZ8LWW_JLWtpXHldRy9SNjNtws,11251
66
66
  phoenix/server/prometheus.py,sha256=j9DHB2fERuq_ZKmwVaqR-9wx5WcPPuU1Cm5Bhg5241Y,2996
67
67
  phoenix/server/telemetry.py,sha256=T_2OKrxNViAeaANlNspEekg_Y5uZIFWvKAnpz8Aoqvk,2762
68
- phoenix/server/thread_server.py,sha256=dP6cm6Cf08jNhDA1TRlVZpziu1YgtPDmaeIJMm725eI,2154
68
+ phoenix/server/thread_server.py,sha256=RwXQGP_QhGD7le6WB7xEygEEuwBl5Ck_Zo8xGIYGi9M,2135
69
69
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
- phoenix/server/api/context.py,sha256=DiK2IRMBbMvBF0uK20YBftApJXau4GSDHltVZX2yERQ,2957
70
+ phoenix/server/api/context.py,sha256=G9bmB3h-a4Top6wGAu91ymS-YE6D1DYQyWtPIlZnbgc,2811
71
71
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
72
72
  phoenix/server/api/queries.py,sha256=eq2xHaQF-x4k6AGSY6b6mU2pie9bj-AJML6P2Mr0_DM,19886
73
73
  phoenix/server/api/schema.py,sha256=BcxdqO5CSGqpKd-AAJHMjFlzaK9oJA8GJuxmMfcdjn4,434
@@ -134,19 +134,21 @@ phoenix/server/api/mutations/project_mutations.py,sha256=d_xtYkYfZ5flpVgEkGknKB8
134
134
  phoenix/server/api/mutations/span_annotations_mutations.py,sha256=Pfaq4y-FGskPit4z_9GvsyWeBwK1g3CDi2UhGKxyjFE,4973
135
135
  phoenix/server/api/mutations/trace_annotations_mutations.py,sha256=4xm-zg8PEjwjfVRsWzBD_iiOS94JI6UYtK3fV2dtA4M,5013
136
136
  phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
137
- phoenix/server/api/openapi/main.py,sha256=WY0pj3B7siQyyYqKyhqnzWC7P8MtEtiukOBUjGwLXfw,153
138
- phoenix/server/api/openapi/schema.py,sha256=uuSYe1Ecu72aXRgTNjyMu-9ZPE13DAHJPKtedS-MsSs,451
137
+ phoenix/server/api/openapi/main.py,sha256=KNutA_7AvV_WlGX8cOkvvDujcJKQ7AD1HT6rTpCpR8A,616
138
+ phoenix/server/api/openapi/schema.py,sha256=oVZoflWMfzOrLKMIrjr3iLnJ13rmN-t_DOe9g6KoN5s,471
139
139
  phoenix/server/api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
140
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
141
- phoenix/server/api/routers/v1/__init__.py,sha256=D1EFRWG4PcsTubeF3A3ENlNatCRq26AA52FxW11BGjM,3048
142
- phoenix/server/api/routers/v1/dataset_examples.py,sha256=XfqOvDKF1oxb0pkeYfBycwwGt3LnSyyGdMLKC5VKoGQ,6690
143
- phoenix/server/api/routers/v1/datasets.py,sha256=r0WcNxF8SKVa3-4rrTIg4Andwr4NmRmW1ybpKuxR9qw,33639
144
- phoenix/server/api/routers/v1/evaluations.py,sha256=8g6P_e2BweV3RDU0esFmpkb0L5fCwonQPXiJ0y6HLwg,9126
145
- phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=TE1GMSOLN_96uAaJpnRpIH2u9x6_ebtkECgZRHvqt-w,5098
146
- phoenix/server/api/routers/v1/experiment_runs.py,sha256=jy4SynmzdtQMoUzlowmG6wsVU14SsLAzfcW4JOhXjeQ,8154
147
- phoenix/server/api/routers/v1/experiments.py,sha256=uVdmhyJgYI-UqOiRSJ-8OcVpL8a6Z02B5H2Rt_7yboY,11829
148
- phoenix/server/api/routers/v1/spans.py,sha256=tryWFoJVFRLALzt6dfPmBBhKMS0s3hhlYdTathxVEU4,9638
149
- phoenix/server/api/routers/v1/traces.py,sha256=PBIrpdJHVJ9gyiukCy1Ck1w0xts0VEHtRKaF7Noa248,8434
141
+ phoenix/server/api/routers/v1/__init__.py,sha256=nb49zcOdAi3DSGuC9gUubN9Yri-o7-WFdlGak4jGuFw,1462
142
+ phoenix/server/api/routers/v1/dataset_examples.py,sha256=6ep0Trojax_NbwpCOlofNG9p4xMu6VJgZPMjkAUJQOA,5627
143
+ phoenix/server/api/routers/v1/datasets.py,sha256=-QzKYNV0DCWfw3D_k_KO1iF_PY57fNAvx7NGznNWt0g,32294
144
+ phoenix/server/api/routers/v1/evaluations.py,sha256=xNhS_VOA8S0BIijb1NSAOW677PzLOVl39Fl00g_T7Vo,9553
145
+ phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=xoVhU71U3c1QJSXiKsAa4yiH-UAqkyVc7uKPyIHjrYY,4682
146
+ phoenix/server/api/routers/v1/experiment_runs.py,sha256=7qvLYgqH58nxqhTnJ0hPf0PBfVmZnbxquodSnEGoQxk,6059
147
+ phoenix/server/api/routers/v1/experiments.py,sha256=iS2IBgR7WX1rgYlO90b2sleVU-C0y57RgI5TslCYMFw,9812
148
+ phoenix/server/api/routers/v1/pydantic_compat.py,sha256=FeK8oe2brqu-djsoqRxiKL4tw5cHmi89OHVfCFxYsAo,2890
149
+ phoenix/server/api/routers/v1/spans.py,sha256=yMs3Sm6s6JLV2nlmpnQ-ZJ_In0aTY5M3uOlLi-Wzb9c,8967
150
+ phoenix/server/api/routers/v1/traces.py,sha256=Zl_hGHd-4rA0tXegH_GVoN9Ij84vbPB8oHu28fzGHA8,8029
151
+ phoenix/server/api/routers/v1/utils.py,sha256=xvl2v-BKUkqmFVMmgmmWGFKuRBTrUdoiAeT3mCYEE68,3086
150
152
  phoenix/server/api/types/Annotation.py,sha256=7Ym7iuVcbwHlw2yIRylz4nATAF_Cm-Z17qcjiooj1cc,751
151
153
  phoenix/server/api/types/AnnotatorKind.py,sha256=rPgGdbN1Gvc109sGQ_ZH-gfJbp93V9wlarzTEJNtUwI,236
152
154
  phoenix/server/api/types/Cluster.py,sha256=ac4YfT1OH3xLVmex7EUmB6b9IpULnhLTt554LR0jglE,5689
@@ -202,7 +204,6 @@ phoenix/server/api/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
202
204
  phoenix/server/api/types/node.py,sha256=V0Xh9U4cGkz3iMg-vzEXtcs6cumU29JFPiU-JuGzjWI,848
203
205
  phoenix/server/api/types/pagination.py,sha256=PcaJ0s4exsTKgCZC4aFm1cgZNrGpHSdo6PbkWzPcweg,9077
204
206
  phoenix/server/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
205
- phoenix/server/openapi/docs.py,sha256=fTb9q2oOSKC5bLVQy2Dsg3Bs0mGkCOKX1ypX7731sE0,7044
206
207
  phoenix/server/static/apple-touch-icon-114x114.png,sha256=xtFVXAYQnJkpUApg2D1hltSTuyO4Is4sD4A0ZkikiVU,9486
207
208
  phoenix/server/static/apple-touch-icon-120x120.png,sha256=iqZVAk634BbjJMozA8aHYyw15JjhIlIrG41FA2DFFaE,9957
208
209
  phoenix/server/static/apple-touch-icon-144x144.png,sha256=VgARtkHKoU8zikb3_G83h_cb02kpPcoJqO78yRh1AfU,10047
@@ -224,9 +225,9 @@ phoenix/server/static/assets/vendor-codemirror-CrdxOlMs.js,sha256=KMJHeNKQyBjzBz
224
225
  phoenix/server/static/assets/vendor-recharts-PKRvByVe.js,sha256=Uul1I5FtZKipkx1ku4y2OqWd86GiO4aB4dA9o_LJbvM,282859
225
226
  phoenix/server/static/assets/vendor-three-DwGkEfCM.js,sha256=0D12ZgKzfKCTSdSTKJBFR2RZO_xxeMXrqDp0AszZqHY,620972
226
227
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
227
- phoenix/server/templates/index.html,sha256=lgWe7Smx5KT6XYqnLC2-ZJYtxCJIC-if0-_6TesO1_Q,3884
228
+ phoenix/server/templates/index.html,sha256=gVpjB8pCMiubdMh2DA9mTCtV5AVTXJH_9u5PmG2t7Vk,4238
228
229
  phoenix/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
229
- phoenix/session/client.py,sha256=aq5AhgyoI-rJU5eTNFAk8SzdBl6hiZvj7fWrC09s4aI,32523
230
+ phoenix/session/client.py,sha256=niTRnsmLb6OdrwNvFNgVqrkft5ZRpYYZoWq4WhjS_AI,32555
230
231
  phoenix/session/data_extractor.py,sha256=gkEM3WWZAlWGMfRgQopAQlid4cSi6GNco-sdrGir0qc,2788
231
232
  phoenix/session/evaluation.py,sha256=aKeV8UVOyq3b7CYOwt3cWuLz0xzvMjX7vlEPILJ_fcs,5311
232
233
  phoenix/session/session.py,sha256=1ZGR0pBmah8bqX353MDf4sq7XuK904EfxNLo0B9z_sU,26714
@@ -266,8 +267,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
266
267
  phoenix/utilities/project.py,sha256=qWsvKnG1oKhOFUowXf9qiOL2ia7jaFe_ijFFHEt8GJo,431
267
268
  phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
268
269
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
269
- arize_phoenix-4.12.0rc1.dist-info/METADATA,sha256=RrJgXQpDjiI30wmDKUZrcbBoM6vb3f2suCGJC4mtuQ0,11455
270
- arize_phoenix-4.12.0rc1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
271
- arize_phoenix-4.12.0rc1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
272
- arize_phoenix-4.12.0rc1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
273
- arize_phoenix-4.12.0rc1.dist-info/RECORD,,
270
+ arize_phoenix-4.12.1rc1.dist-info/METADATA,sha256=9EuJLBvIpbylubhJ6sCVH2zk-_klCyHiBYHqyGdAB5M,11494
271
+ arize_phoenix-4.12.1rc1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
272
+ arize_phoenix-4.12.1rc1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
273
+ arize_phoenix-4.12.1rc1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
274
+ arize_phoenix-4.12.1rc1.dist-info/RECORD,,
@@ -1,12 +1,10 @@
1
1
  from dataclasses import dataclass
2
2
  from datetime import datetime
3
3
  from pathlib import Path
4
- from typing import AsyncContextManager, Callable, Optional, Union
4
+ from typing import AsyncContextManager, Callable, Optional
5
5
 
6
6
  from sqlalchemy.ext.asyncio import AsyncSession
7
- from starlette.requests import Request
8
- from starlette.responses import Response
9
- from starlette.websockets import WebSocket
7
+ from strawberry.fastapi import BaseContext
10
8
  from typing_extensions import TypeAlias
11
9
 
12
10
  from phoenix.core.model_schema import Model
@@ -67,9 +65,7 @@ ProjectRowId: TypeAlias = int
67
65
 
68
66
 
69
67
  @dataclass
70
- class Context:
71
- request: Union[Request, WebSocket]
72
- response: Optional[Response]
68
+ class Context(BaseContext):
73
69
  db: Callable[[], AsyncContextManager[AsyncSession]]
74
70
  data_loaders: DataLoaders
75
71
  cache_for_dataloaders: Optional[CacheForDataLoaders]
@@ -1,6 +1,22 @@
1
+ import json
2
+ from argparse import ArgumentParser
3
+ from typing import Optional, Tuple
4
+
1
5
  from .schema import get_openapi_schema
2
6
 
3
7
  if __name__ == "__main__":
4
- import yaml # type: ignore
8
+ parser = ArgumentParser()
9
+ parser.add_argument(
10
+ "--compress",
11
+ action="store_true",
12
+ help="Whether to output a compressed version of the OpenAPI schema",
13
+ )
14
+ args = parser.parse_args()
5
15
 
6
- print(yaml.dump(get_openapi_schema(), indent=2))
16
+ indent: Optional[int] = None
17
+ separator: Optional[Tuple[str, str]] = None
18
+ if args.compress:
19
+ separator = (",", ":")
20
+ else:
21
+ indent = 2
22
+ print(json.dumps(get_openapi_schema(), indent=indent, separators=separator))
@@ -1,16 +1,16 @@
1
- from typing import Any
1
+ from typing import Any, Dict
2
2
 
3
- from starlette.schemas import SchemaGenerator
3
+ from fastapi.openapi.utils import get_openapi
4
4
 
5
- from phoenix.server.api.routers.v1 import V1_ROUTES
5
+ from phoenix.server.api.routers.v1 import REST_API_VERSION
6
+ from phoenix.server.api.routers.v1 import router as v1_router
6
7
 
7
- OPENAPI_SCHEMA_GENERATOR = SchemaGenerator(
8
- {"openapi": "3.0.0", "info": {"title": "Arize-Phoenix API", "version": "1.0"}}
9
- )
10
8
 
11
-
12
- def get_openapi_schema() -> Any:
13
- """
14
- Exports an OpenAPI schema for the Phoenix REST API as a JSON object.
15
- """
16
- return OPENAPI_SCHEMA_GENERATOR.get_schema(V1_ROUTES) # type: ignore
9
+ def get_openapi_schema() -> Dict[str, Any]:
10
+ return get_openapi(
11
+ title="Arize-Phoenix REST API",
12
+ version=REST_API_VERSION,
13
+ openapi_version="3.1.0",
14
+ description="Schema for Arize-Phoenix REST API",
15
+ routes=v1_router.routes,
16
+ )
@@ -1,89 +1,42 @@
1
- from typing import Any, Awaitable, Callable, Mapping, Tuple
2
-
3
- import wrapt
4
- from starlette import routing
5
- from starlette.requests import Request
6
- from starlette.responses import Response
1
+ from fastapi import APIRouter, Depends, HTTPException, Request
7
2
  from starlette.status import HTTP_403_FORBIDDEN
8
3
 
9
- from . import (
10
- datasets,
11
- evaluations,
12
- experiment_evaluations,
13
- experiment_runs,
14
- experiments,
15
- spans,
16
- traces,
17
- )
18
- from .dataset_examples import list_dataset_examples
19
-
4
+ from .datasets import router as datasets_router
5
+ from .evaluations import router as evaluations_router
6
+ from .experiment_evaluations import router as experiment_evaluations_router
7
+ from .experiment_runs import router as experiment_runs_router
8
+ from .experiments import router as experiments_router
9
+ from .spans import router as spans_router
10
+ from .traces import router as traces_router
11
+ from .utils import add_errors_to_responses
20
12
 
21
- @wrapt.decorator # type: ignore
22
- async def forbid_if_readonly(
23
- wrapped: Callable[[Request], Awaitable[Response]],
24
- _: Any,
25
- args: Tuple[Request],
26
- kwargs: Mapping[str, Any],
27
- ) -> Response:
28
- request, *_ = args
29
- if request.app.state.read_only:
30
- return Response(status_code=HTTP_403_FORBIDDEN)
31
- return await wrapped(*args, **kwargs)
13
+ REST_API_VERSION = "1.0"
32
14
 
33
15
 
34
- class Route(routing.Route):
35
- def __init__(self, path: str, endpoint: Callable[..., Any], **kwargs: Any) -> None:
36
- super().__init__(path, forbid_if_readonly(endpoint), **kwargs)
37
-
38
-
39
- V1_ROUTES = [
40
- Route("/v1/evaluations", evaluations.post_evaluations, methods=["POST"]),
41
- Route("/v1/evaluations", evaluations.get_evaluations, methods=["GET"]),
42
- Route("/v1/traces", traces.post_traces, methods=["POST"]),
43
- Route("/v1/trace_annotations", traces.annotate_traces, methods=["POST"]),
44
- Route("/v1/spans", spans.query_spans_handler, methods=["POST"]),
45
- Route("/v1/spans", spans.get_spans_handler, methods=["GET"]),
46
- Route("/v1/span_annotations", spans.annotate_spans, methods=["POST"]),
47
- Route("/v1/datasets/upload", datasets.post_datasets_upload, methods=["POST"]),
48
- Route("/v1/datasets", datasets.list_datasets, methods=["GET"]),
49
- Route("/v1/datasets/{id:str}", datasets.delete_dataset_by_id, methods=["DELETE"]),
50
- Route("/v1/datasets/{id:str}", datasets.get_dataset_by_id, methods=["GET"]),
51
- Route("/v1/datasets/{id:str}/csv", datasets.get_dataset_csv, methods=["GET"]),
52
- Route(
53
- "/v1/datasets/{id:str}/jsonl/openai_ft",
54
- datasets.get_dataset_jsonl_openai_ft,
55
- methods=["GET"],
56
- ),
57
- Route(
58
- "/v1/datasets/{id:str}/jsonl/openai_evals",
59
- datasets.get_dataset_jsonl_openai_evals,
60
- methods=["GET"],
61
- ),
62
- Route("/v1/datasets/{id:str}/examples", list_dataset_examples, methods=["GET"]),
63
- Route("/v1/datasets/{id:str}/versions", datasets.get_dataset_versions, methods=["GET"]),
64
- Route(
65
- "/v1/datasets/{dataset_id:str}/experiments",
66
- experiments.create_experiment,
67
- methods=["POST"],
68
- ),
69
- Route(
70
- "/v1/experiments/{experiment_id:str}",
71
- experiments.read_experiment,
72
- methods=["GET"],
73
- ),
74
- Route(
75
- "/v1/experiments/{experiment_id:str}/runs",
76
- experiment_runs.create_experiment_run,
77
- methods=["POST"],
78
- ),
79
- Route(
80
- "/v1/experiments/{experiment_id:str}/runs",
81
- experiment_runs.list_experiment_runs,
82
- methods=["GET"],
83
- ),
84
- Route(
85
- "/v1/experiment_evaluations",
86
- experiment_evaluations.upsert_experiment_evaluation,
87
- methods=["POST"],
16
+ async def prevent_access_in_read_only_mode(request: Request) -> None:
17
+ """
18
+ Prevents access to the REST API in read-only mode.
19
+ """
20
+ if request.app.state.read_only:
21
+ raise HTTPException(
22
+ detail="The Phoenix REST API is disabled in read-only mode.",
23
+ status_code=HTTP_403_FORBIDDEN,
24
+ )
25
+
26
+
27
+ router = APIRouter(
28
+ prefix="/v1",
29
+ dependencies=[Depends(prevent_access_in_read_only_mode)],
30
+ responses=add_errors_to_responses(
31
+ [
32
+ HTTP_403_FORBIDDEN # adds a 403 response to each route in the generated OpenAPI schema
33
+ ]
88
34
  ),
89
- ]
35
+ )
36
+ router.include_router(datasets_router)
37
+ router.include_router(experiments_router)
38
+ router.include_router(experiment_runs_router)
39
+ router.include_router(experiment_evaluations_router)
40
+ router.include_router(traces_router)
41
+ router.include_router(spans_router)
42
+ router.include_router(evaluations_router)
@@ -1,178 +1,157 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from fastapi import APIRouter, HTTPException, Path, Query
1
5
  from sqlalchemy import and_, func, select
2
6
  from starlette.requests import Request
3
- from starlette.responses import JSONResponse, Response
4
7
  from starlette.status import HTTP_404_NOT_FOUND
5
8
  from strawberry.relay import GlobalID
6
9
 
7
- from phoenix.db.models import Dataset, DatasetExample, DatasetExampleRevision, DatasetVersion
8
-
9
-
10
- async def list_dataset_examples(request: Request) -> Response:
11
- """
12
- summary: Get dataset examples by dataset ID
13
- operationId: getDatasetExamples
14
- tags:
15
- - datasets
16
- parameters:
17
- - in: path
18
- name: id
19
- required: true
20
- schema:
21
- type: string
22
- description: Dataset ID
23
- - in: query
24
- name: version_id
25
- schema:
26
- type: string
27
- description: Dataset version ID. If omitted, returns the latest version.
28
- responses:
29
- 200:
30
- description: Success
31
- content:
32
- application/json:
33
- schema:
34
- type: object
35
- properties:
36
- data:
37
- type: object
38
- properties:
39
- dataset_id:
40
- type: string
41
- description: ID of the dataset
42
- version_id:
43
- type: string
44
- description: ID of the version
45
- examples:
46
- type: array
47
- items:
48
- type: object
49
- properties:
50
- id:
51
- type: string
52
- description: ID of the dataset example
53
- input:
54
- type: object
55
- description: Input data of the example
56
- output:
57
- type: object
58
- description: Output data of the example
59
- metadata:
60
- type: object
61
- description: Metadata of the example
62
- updated_at:
63
- type: string
64
- format: date-time
65
- description: ISO formatted timestamp of when the example was updated
66
- required:
67
- - id
68
- - input
69
- - output
70
- - metadata
71
- - updated_at
72
- required:
73
- - dataset_id
74
- - version_id
75
- - examples
76
- 403:
77
- description: Forbidden
78
- 404:
79
- description: Dataset does not exist.
80
- """
81
- dataset_id = GlobalID.from_id(request.path_params["id"])
82
- raw_version_id = request.query_params.get("version_id")
83
- version_id = GlobalID.from_id(raw_version_id) if raw_version_id else None
84
-
85
- if (dataset_type := dataset_id.type_name) != "Dataset":
86
- return Response(
87
- content=f"ID {dataset_id} refers to a {dataset_type}", status_code=HTTP_404_NOT_FOUND
10
+ from phoenix.db.models import (
11
+ Dataset as ORMDataset,
12
+ )
13
+ from phoenix.db.models import (
14
+ DatasetExample as ORMDatasetExample,
15
+ )
16
+ from phoenix.db.models import (
17
+ DatasetExampleRevision as ORMDatasetExampleRevision,
18
+ )
19
+ from phoenix.db.models import (
20
+ DatasetVersion as ORMDatasetVersion,
21
+ )
22
+
23
+ from .pydantic_compat import V1RoutesBaseModel
24
+ from .utils import ResponseBody, add_errors_to_responses
25
+
26
+ router = APIRouter(tags=["datasets"])
27
+
28
+
29
+ class DatasetExample(V1RoutesBaseModel):
30
+ id: str
31
+ input: Dict[str, Any]
32
+ output: Dict[str, Any]
33
+ metadata: Dict[str, Any]
34
+ updated_at: datetime
35
+
36
+
37
+ class ListDatasetExamplesData(V1RoutesBaseModel):
38
+ dataset_id: str
39
+ version_id: str
40
+ examples: List[DatasetExample]
41
+
42
+
43
+ class ListDatasetExamplesResponseBody(ResponseBody[ListDatasetExamplesData]):
44
+ pass
45
+
46
+
47
+ @router.get(
48
+ "/datasets/{id}/examples",
49
+ operation_id="getDatasetExamples",
50
+ summary="Get examples from a dataset",
51
+ responses=add_errors_to_responses([HTTP_404_NOT_FOUND]),
52
+ )
53
+ async def get_dataset_examples(
54
+ request: Request,
55
+ id: str = Path(description="The ID of the dataset"),
56
+ version_id: Optional[str] = Query(
57
+ default=None,
58
+ description=(
59
+ "The ID of the dataset version " "(if omitted, returns data from the latest version)"
60
+ ),
61
+ ),
62
+ ) -> ListDatasetExamplesResponseBody:
63
+ dataset_gid = GlobalID.from_id(id)
64
+ version_gid = GlobalID.from_id(version_id) if version_id else None
65
+
66
+ if (dataset_type := dataset_gid.type_name) != "Dataset":
67
+ raise HTTPException(
68
+ detail=f"ID {dataset_gid} refers to a {dataset_type}", status_code=HTTP_404_NOT_FOUND
88
69
  )
89
70
 
90
- if version_id and (version_type := version_id.type_name) != "DatasetVersion":
91
- return Response(
92
- content=f"ID {version_id} refers to a {version_type}", status_code=HTTP_404_NOT_FOUND
71
+ if version_gid and (version_type := version_gid.type_name) != "DatasetVersion":
72
+ raise HTTPException(
73
+ detail=f"ID {version_gid} refers to a {version_type}", status_code=HTTP_404_NOT_FOUND
93
74
  )
94
75
 
95
76
  async with request.app.state.db() as session:
96
77
  if (
97
78
  resolved_dataset_id := await session.scalar(
98
- select(Dataset.id).where(Dataset.id == int(dataset_id.node_id))
79
+ select(ORMDataset.id).where(ORMDataset.id == int(dataset_gid.node_id))
99
80
  )
100
81
  ) is None:
101
- return Response(
102
- content=f"No dataset with id {dataset_id} can be found.",
82
+ raise HTTPException(
83
+ detail=f"No dataset with id {dataset_gid} can be found.",
103
84
  status_code=HTTP_404_NOT_FOUND,
104
85
  )
105
86
 
106
87
  # Subquery to find the maximum created_at for each dataset_example_id
107
88
  # timestamp tiebreaks are resolved by the largest id
108
89
  partial_subquery = select(
109
- func.max(DatasetExampleRevision.id).label("max_id"),
110
- ).group_by(DatasetExampleRevision.dataset_example_id)
90
+ func.max(ORMDatasetExampleRevision.id).label("max_id"),
91
+ ).group_by(ORMDatasetExampleRevision.dataset_example_id)
111
92
 
112
- if version_id:
93
+ if version_gid:
113
94
  if (
114
95
  resolved_version_id := await session.scalar(
115
- select(DatasetVersion.id).where(
96
+ select(ORMDatasetVersion.id).where(
116
97
  and_(
117
- DatasetVersion.dataset_id == resolved_dataset_id,
118
- DatasetVersion.id == int(version_id.node_id),
98
+ ORMDatasetVersion.dataset_id == resolved_dataset_id,
99
+ ORMDatasetVersion.id == int(version_gid.node_id),
119
100
  )
120
101
  )
121
102
  )
122
103
  ) is None:
123
- return Response(
124
- content=f"No dataset version with id {version_id} can be found.",
104
+ raise HTTPException(
105
+ detail=f"No dataset version with id {version_id} can be found.",
125
106
  status_code=HTTP_404_NOT_FOUND,
126
107
  )
127
108
  # if a version_id is provided, filter the subquery to only include revisions from that
128
109
  partial_subquery = partial_subquery.filter(
129
- DatasetExampleRevision.dataset_version_id <= resolved_version_id
110
+ ORMDatasetExampleRevision.dataset_version_id <= resolved_version_id
130
111
  )
131
112
  else:
132
113
  if (
133
114
  resolved_version_id := await session.scalar(
134
- select(func.max(DatasetVersion.id)).where(
135
- DatasetVersion.dataset_id == resolved_dataset_id
115
+ select(func.max(ORMDatasetVersion.id)).where(
116
+ ORMDatasetVersion.dataset_id == resolved_dataset_id
136
117
  )
137
118
  )
138
119
  ) is None:
139
- return Response(
140
- content="Dataset has no versions.",
120
+ raise HTTPException(
121
+ detail="Dataset has no versions.",
141
122
  status_code=HTTP_404_NOT_FOUND,
142
123
  )
143
124
 
144
125
  subquery = partial_subquery.subquery()
145
126
  # Query for the most recent example revisions that are not deleted
146
127
  query = (
147
- select(DatasetExample, DatasetExampleRevision)
128
+ select(ORMDatasetExample, ORMDatasetExampleRevision)
148
129
  .join(
149
- DatasetExampleRevision,
150
- DatasetExample.id == DatasetExampleRevision.dataset_example_id,
130
+ ORMDatasetExampleRevision,
131
+ ORMDatasetExample.id == ORMDatasetExampleRevision.dataset_example_id,
151
132
  )
152
133
  .join(
153
134
  subquery,
154
- (subquery.c.max_id == DatasetExampleRevision.id),
135
+ (subquery.c.max_id == ORMDatasetExampleRevision.id),
155
136
  )
156
- .filter(DatasetExample.dataset_id == resolved_dataset_id)
157
- .filter(DatasetExampleRevision.revision_kind != "DELETE")
158
- .order_by(DatasetExample.id.asc())
137
+ .filter(ORMDatasetExample.dataset_id == resolved_dataset_id)
138
+ .filter(ORMDatasetExampleRevision.revision_kind != "DELETE")
139
+ .order_by(ORMDatasetExample.id.asc())
159
140
  )
160
141
  examples = [
161
- {
162
- "id": str(GlobalID("DatasetExample", str(example.id))),
163
- "input": revision.input,
164
- "output": revision.output,
165
- "metadata": revision.metadata_,
166
- "updated_at": revision.created_at.isoformat(),
167
- }
142
+ DatasetExample(
143
+ id=str(GlobalID("DatasetExample", str(example.id))),
144
+ input=revision.input,
145
+ output=revision.output,
146
+ metadata=revision.metadata_,
147
+ updated_at=revision.created_at,
148
+ )
168
149
  async for example, revision in await session.stream(query)
169
150
  ]
170
- return JSONResponse(
171
- {
172
- "data": {
173
- "dataset_id": str(GlobalID("Dataset", str(resolved_dataset_id))),
174
- "version_id": str(GlobalID("DatasetVersion", str(resolved_version_id))),
175
- "examples": examples,
176
- }
177
- }
151
+ return ListDatasetExamplesResponseBody(
152
+ data=ListDatasetExamplesData(
153
+ dataset_id=str(GlobalID("Dataset", str(resolved_dataset_id))),
154
+ version_id=str(GlobalID("DatasetVersion", str(resolved_version_id))),
155
+ examples=examples,
156
+ )
178
157
  )