arize-phoenix 8.23.0__py3-none-any.whl → 8.24.1__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-8.23.0.dist-info → arize_phoenix-8.24.1.dist-info}/METADATA +1 -1
- {arize_phoenix-8.23.0.dist-info → arize_phoenix-8.24.1.dist-info}/RECORD +24 -23
- phoenix/config.py +48 -2
- phoenix/db/facilitator.py +118 -85
- phoenix/db/helpers.py +16 -0
- phoenix/server/api/context.py +2 -0
- phoenix/server/api/mutations/user_mutations.py +10 -0
- phoenix/server/api/queries.py +3 -14
- phoenix/server/api/routers/v1/projects.py +119 -109
- phoenix/server/api/subscriptions.py +1 -1
- phoenix/server/app.py +7 -0
- phoenix/server/email/sender.py +75 -47
- phoenix/server/email/templates/welcome.html +12 -0
- phoenix/server/email/types.py +16 -1
- phoenix/server/main.py +2 -1
- phoenix/server/static/.vite/manifest.json +9 -9
- phoenix/server/static/assets/{components-D8xtf7Q6.js → components-B6cljCxu.js} +1 -1
- phoenix/server/static/assets/{index-CGfsCafL.js → index-DfHKoAV9.js} +1 -1
- phoenix/server/static/assets/{pages-BiUq97Df.js → pages-Dhitcl5V.js} +2 -2
- phoenix/version.py +1 -1
- {arize_phoenix-8.23.0.dist-info → arize_phoenix-8.24.1.dist-info}/WHEEL +0 -0
- {arize_phoenix-8.23.0.dist-info → arize_phoenix-8.24.1.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-8.23.0.dist-info → arize_phoenix-8.24.1.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-8.23.0.dist-info → arize_phoenix-8.24.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
phoenix/__init__.py,sha256=X3eUEwd2rG8KKWWYVNNDJoqo08ihfjgHhlP29dcdNJE,5481
|
|
2
2
|
phoenix/auth.py,sha256=VVMHrWN31tln3Zo4z6ofecrV4daiqJjLd8r85mqlxek,10939
|
|
3
|
-
phoenix/config.py,sha256=
|
|
3
|
+
phoenix/config.py,sha256=SNBMUEo6lNn5WXP-x6B85UmADZfGEkhfeHDDfW3Af5Y,38090
|
|
4
4
|
phoenix/datetime_utils.py,sha256=iJzNG6YJ6V7_u8B2iA7P2Z26FyxYbOPtx0dhJ7kNDHA,3398
|
|
5
5
|
phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
|
|
6
6
|
phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
7
7
|
phoenix/services.py,sha256=kpW1WL0kiB8XJsO6XycvZVJ-lBkNoenhQ7atCvBoSe8,5365
|
|
8
8
|
phoenix/settings.py,sha256=x87BX7hWGQQZbrW_vrYqFR_izCGfO9gFc--JXUG4Tdk,754
|
|
9
|
-
phoenix/version.py,sha256=
|
|
9
|
+
phoenix/version.py,sha256=Ay9oO96i2YGmx2lcWChBnshhqL44Hxy1NdOIEHzoMOw,23
|
|
10
10
|
phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
|
|
12
12
|
phoenix/core/model.py,sha256=qBFraOtmwCCnWJltKNP18DDG0mULXigytlFsa6YOz6k,4837
|
|
@@ -18,8 +18,8 @@ phoenix/db/alembic.ini,sha256=GIS6HpHaKaJbbuahZg1Rc1D2_QqyCkV9r58wdARGf6w,3262
|
|
|
18
18
|
phoenix/db/bulk_inserter.py,sha256=faNjuwLqqsw4ky8sa4D0h9u5TvEDTOjrccUW89L008E,12725
|
|
19
19
|
phoenix/db/engines.py,sha256=gxcP2aNy_JyKHv1MO4d2UM47GTMy1jDcN-FQETZ3iNA,7348
|
|
20
20
|
phoenix/db/enums.py,sha256=tt7iovXLhVTLZ3_LbHNGgcI44SnNjXfkKtLAZG57T54,428
|
|
21
|
-
phoenix/db/facilitator.py,sha256
|
|
22
|
-
phoenix/db/helpers.py,sha256=
|
|
21
|
+
phoenix/db/facilitator.py,sha256=-uzv3Wr4YuUtVff4_0bLTcIFvzWzWOA3IPq8s2RA_bc,6821
|
|
22
|
+
phoenix/db/helpers.py,sha256=rbbHcl-STzcEpcXCYx6jbKzko7r3ggrWHHsXjZ48HsM,5352
|
|
23
23
|
phoenix/db/migrate.py,sha256=oUrXH8yEbcpL4eh09aSCuUiSrhFli0eT5D_j4ZmYChY,2797
|
|
24
24
|
phoenix/db/models.py,sha256=TbHgtT7WWdkQK-OtLsUqp3MwP23HGV1IaSAWTqCf5ac,45707
|
|
25
25
|
phoenix/db/insertion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -80,13 +80,13 @@ phoenix/pointcloud/pointcloud.py,sha256=SN_1wXZcwKrtSnHGZLDZGx71orqE1WyVF7E-D58d
|
|
|
80
80
|
phoenix/pointcloud/projectors.py,sha256=TQgwc9cJDjJkin1WZyZzgl3HsYrLLiyWD7Czy4jNW3U,1088
|
|
81
81
|
phoenix/pointcloud/umap_parameters.py,sha256=db_WEPoamuWtopZx7tQfAXPnoE0MS8FkAV0_ThjEx_Q,1735
|
|
82
82
|
phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
83
|
-
phoenix/server/app.py,sha256=
|
|
83
|
+
phoenix/server/app.py,sha256=f9H8ekaGiMwCMbSf2kbQIUFKywY2VmaIuJgPlrC7obU,40221
|
|
84
84
|
phoenix/server/bearer_auth.py,sha256=0UudvkAS_dxna5JEJJhGUYwB6Ny-e22ssX5Mm79QwCk,5907
|
|
85
85
|
phoenix/server/dml_event.py,sha256=MjJmVEKytq75chBOSyvYDusUnEbg1pHpIjR3pZkUaJA,2838
|
|
86
86
|
phoenix/server/dml_event_handler.py,sha256=EZLXmCvx4pJrCkz29gxwKwmvmUkTtPCHw6klR-XM8qE,8258
|
|
87
87
|
phoenix/server/grpc_server.py,sha256=SknR-iLIUqU9swiAyLhbCUREA1obOM6w49xgdK1AQDs,3839
|
|
88
88
|
phoenix/server/jwt_store.py,sha256=asxzY4_ZBM2FWAMstHvhvnKUP_0AA3v3xPTL2IOgNqY,16831
|
|
89
|
-
phoenix/server/main.py,sha256=
|
|
89
|
+
phoenix/server/main.py,sha256=qJcqXudiY65UeOWOaxP1-dqzrV-BBnbZu_IDd-qMKMk,16720
|
|
90
90
|
phoenix/server/oauth2.py,sha256=EV4wcCwG0N7cJRcfGNURdP5rZgRVCeRDvXyle19A27Y,2064
|
|
91
91
|
phoenix/server/prometheus.py,sha256=1KjvSfjSa2-BPjDybVMM_Kag316CsN-Zwt64YNr_snc,7825
|
|
92
92
|
phoenix/server/rate_limiters.py,sha256=cFc73D2NaxqNZZDbwfIDw4So-fRVOJPBtqxOZ8Qky_s,7155
|
|
@@ -96,12 +96,12 @@ phoenix/server/types.py,sha256=gJJPBcDRkQ9VHZIt_aLqG_OBbGt1oWp4e3W3Jp61oKs,7409
|
|
|
96
96
|
phoenix/server/api/README.md,sha256=Pyq1PLPgTzXAswrfIhGXrjI3Skq8it2jTVnanT6Ba4Q,1162
|
|
97
97
|
phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
98
|
phoenix/server/api/auth.py,sha256=nywpmfMI1trZTbZRD3oBj4kFjzg_vnxDljcM431T1eY,1246
|
|
99
|
-
phoenix/server/api/context.py,sha256=
|
|
99
|
+
phoenix/server/api/context.py,sha256=gB9bNgdqqD4Bn-6V4zlKeILl6L76jynPtOuM_5UZBNw,6557
|
|
100
100
|
phoenix/server/api/exceptions.py,sha256=TA0JuY2YRnj35qGuMSQ8d0ToHum9gWm9W--3fSKHrX0,1171
|
|
101
101
|
phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
|
|
102
|
-
phoenix/server/api/queries.py,sha256=
|
|
102
|
+
phoenix/server/api/queries.py,sha256=WNH1LxB7bobfOMdBJts-9_ncfVhv2LThAKu14MRPMpk,35810
|
|
103
103
|
phoenix/server/api/schema.py,sha256=fcs36xQwFF_Qe41_5cWR8wYpDvOrnbcyTeo5WNMbDsA,1702
|
|
104
|
-
phoenix/server/api/subscriptions.py,sha256=
|
|
104
|
+
phoenix/server/api/subscriptions.py,sha256=TnZhdoNHMXp1NkUVLA-eB54woll7FvxtsB2pLt1dO0w,23001
|
|
105
105
|
phoenix/server/api/utils.py,sha256=quCBRcusc6PUq9tJq7M8PgwFZp7nXgVAxtbw8feribY,833
|
|
106
106
|
phoenix/server/api/dataloaders/__init__.py,sha256=LiOF9d95TPOD3pYuE906trLq-_q1VP8JQ9B6SHicbbM,4360
|
|
107
107
|
phoenix/server/api/dataloaders/annotation_summaries.py,sha256=2sHmIDX7n8tuPeBTs9bMKtlMKWn_Ph9awTZqmwn2Owc,5505
|
|
@@ -201,7 +201,7 @@ phoenix/server/api/mutations/prompt_version_tag_mutations.py,sha256=t77osYb5he2A
|
|
|
201
201
|
phoenix/server/api/mutations/span_annotations_mutations.py,sha256=sumBLUqRKlgMASWdWwYItmIJ2l7AyAp_PlIYeXYfguc,5970
|
|
202
202
|
phoenix/server/api/mutations/trace_annotations_mutations.py,sha256=sEcEt8hbMt8YMiRX5o3xcJ5rWWZbDeBPMNz2teZoi3U,5945
|
|
203
203
|
phoenix/server/api/mutations/trace_mutations.py,sha256=D5h2HYdlTo6yYZNq-O-PjaS9GeiZHxxVaOxDdh7fwjw,2957
|
|
204
|
-
phoenix/server/api/mutations/user_mutations.py,sha256=
|
|
204
|
+
phoenix/server/api/mutations/user_mutations.py,sha256=5pgtNKZwOnP1pbuegnNMwPn5u4teeWJ6vZSIcjDSCUg,14055
|
|
205
205
|
phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
206
206
|
phoenix/server/api/openapi/main.py,sha256=yKdzJYI4cxy_1mFcK4_7YObIcuRviBIfwNjB23RG14k,461
|
|
207
207
|
phoenix/server/api/openapi/schema.py,sha256=WGmHWSIyJhtc5EIh_M3vlXU-EgHkFuTlyVofgS0kj1I,529
|
|
@@ -217,7 +217,7 @@ phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=vx4CKlE84sAL1vtPi
|
|
|
217
217
|
phoenix/server/api/routers/v1/experiment_runs.py,sha256=bInuasRv7ogiYf8fq-LwpJ5tptmMQsBNDlJAqwdymko,6378
|
|
218
218
|
phoenix/server/api/routers/v1/experiments.py,sha256=V9_sxqLTE1MKGFu9H3FEdGKr70lYMbGZx813MGaavfQ,20430
|
|
219
219
|
phoenix/server/api/routers/v1/models.py,sha256=r0nM2kFJ3mxDqgc5vFr1cjNuyOPs3RIKE_DS2VMdF48,1749
|
|
220
|
-
phoenix/server/api/routers/v1/projects.py,sha256=
|
|
220
|
+
phoenix/server/api/routers/v1/projects.py,sha256=qv4RffYVGCJQuJ3FzaTVkueY8qKY0-GUbG9eSdm7oRY,14181
|
|
221
221
|
phoenix/server/api/routers/v1/prompts.py,sha256=aBOUBwLDzZDIzJQkxJcR8ZKnakNJOLMwzsLKINSs1mA,26545
|
|
222
222
|
phoenix/server/api/routers/v1/spans.py,sha256=uoU_bwIgz86fuvPjP5sX8goDyuCcnsTig-x3f17p60U,9625
|
|
223
223
|
phoenix/server/api/routers/v1/traces.py,sha256=hSv35QIB4mwFgp53rOpz3zWIiSwbZzQnjafD790QuJU,7908
|
|
@@ -300,10 +300,11 @@ phoenix/server/api/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
300
300
|
phoenix/server/api/types/node.py,sha256=BLl_IOFr0zrqUxaAtGLGui5aeM5VNVXFTzGeAKrztr0,822
|
|
301
301
|
phoenix/server/api/types/pagination.py,sha256=BXm46gXZfrBS4hpiLvVSEdsbb29ctUMVJYjKXlOLxUA,9064
|
|
302
302
|
phoenix/server/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
303
|
-
phoenix/server/email/sender.py,sha256=
|
|
304
|
-
phoenix/server/email/types.py,sha256=
|
|
303
|
+
phoenix/server/email/sender.py,sha256=eC6RcLANVJH0mh20mGZ2qr-bU-OWo9po2e5og2tMzJw,4127
|
|
304
|
+
phoenix/server/email/types.py,sha256=IO2bTtCh-1cve-xiM4MWnunCCVNOQ3Z2cqTqF7vH-do,466
|
|
305
305
|
phoenix/server/email/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
306
306
|
phoenix/server/email/templates/password_reset.html,sha256=jv0Pe-06JloPZcubRWxPdAFHYEn9eDj_4SjmuoIwshI,441
|
|
307
|
+
phoenix/server/email/templates/welcome.html,sha256=AyVsIOtpmyYwWmmkXjuEgXwkbsag4YHHKfkmOTiNo-M,316
|
|
307
308
|
phoenix/server/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
308
309
|
phoenix/server/static/apple-touch-icon-114x114.png,sha256=xtFVXAYQnJkpUApg2D1hltSTuyO4Is4sD4A0ZkikiVU,9486
|
|
309
310
|
phoenix/server/static/apple-touch-icon-120x120.png,sha256=iqZVAk634BbjJMozA8aHYyw15JjhIlIrG41FA2DFFaE,9957
|
|
@@ -315,10 +316,10 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
|
|
|
315
316
|
phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
|
|
316
317
|
phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
|
|
317
318
|
phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
|
|
318
|
-
phoenix/server/static/.vite/manifest.json,sha256=
|
|
319
|
-
phoenix/server/static/assets/components-
|
|
320
|
-
phoenix/server/static/assets/index-
|
|
321
|
-
phoenix/server/static/assets/pages-
|
|
319
|
+
phoenix/server/static/.vite/manifest.json,sha256=HoxYrYg2l2nq5chl9GKo0pnga42JFMShSC0Kr4wLX5o,2165
|
|
320
|
+
phoenix/server/static/assets/components-B6cljCxu.js,sha256=VQZ_xj8sB55iva4mG-oFIPElEaArjo0-GF8hWOQ7L8c,455620
|
|
321
|
+
phoenix/server/static/assets/index-DfHKoAV9.js,sha256=OmqicccaM0lPOLABn3D6U-igVTGSREXbM4ZFiKWYGMI,60397
|
|
322
|
+
phoenix/server/static/assets/pages-Dhitcl5V.js,sha256=CdjHOTXth5jc1mqFO-ofYgFTglKUtVlBwiYEK8SBRJE,863027
|
|
322
323
|
phoenix/server/static/assets/vendor-C3H3sezv.js,sha256=Ask2DJPiTV2k5gNIopK_nOQFKCysUP5Z7h1gWVsFB3c,2510162
|
|
323
324
|
phoenix/server/static/assets/vendor-Cg6lcjUC.css,sha256=nZrkr0u6NNElFGvpWHk9GTHeGoibCXCli1bE7mXZGZg,1816
|
|
324
325
|
phoenix/server/static/assets/vendor-arizeai-DT8pwHfH.js,sha256=JraS2i2tsj4ToS_KGjq9BWS5zv4_FohPgF-vPV3MxEE,193248
|
|
@@ -365,9 +366,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
|
|
|
365
366
|
phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
|
|
366
367
|
phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
367
368
|
phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
|
|
368
|
-
arize_phoenix-8.
|
|
369
|
-
arize_phoenix-8.
|
|
370
|
-
arize_phoenix-8.
|
|
371
|
-
arize_phoenix-8.
|
|
372
|
-
arize_phoenix-8.
|
|
373
|
-
arize_phoenix-8.
|
|
369
|
+
arize_phoenix-8.24.1.dist-info/METADATA,sha256=u080-Lqu15myFGIszydtax26yxvITRhiE_WXHsaQTtY,24495
|
|
370
|
+
arize_phoenix-8.24.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
371
|
+
arize_phoenix-8.24.1.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
|
|
372
|
+
arize_phoenix-8.24.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
|
|
373
|
+
arize_phoenix-8.24.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
|
|
374
|
+
arize_phoenix-8.24.1.dist-info/RECORD,,
|
phoenix/config.py
CHANGED
|
@@ -8,10 +8,11 @@ from enum import Enum
|
|
|
8
8
|
from importlib.metadata import version
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Optional, Union, cast, overload
|
|
11
|
-
from urllib.parse import quote_plus, urlparse
|
|
11
|
+
from urllib.parse import quote_plus, urljoin, urlparse
|
|
12
12
|
|
|
13
13
|
import wrapt
|
|
14
14
|
from email_validator import EmailNotValidError, validate_email
|
|
15
|
+
from starlette.datastructures import URL
|
|
15
16
|
|
|
16
17
|
from phoenix.utilities.logging import log_a_list
|
|
17
18
|
|
|
@@ -182,6 +183,19 @@ If the username or email address already exists in the database, the user record
|
|
|
182
183
|
modified, e.g., changed from non-admin to admin. Changing this environment variable for the next
|
|
183
184
|
startup will not undo any records created in previous startups.
|
|
184
185
|
"""
|
|
186
|
+
ENV_PHOENIX_ROOT_URL = "PHOENIX_ROOT_URL"
|
|
187
|
+
"""
|
|
188
|
+
This is the full URL used to access Phoenix from a web browser. This setting is important when
|
|
189
|
+
you have a reverse proxy in front of Phoenix. If the reverse proxy exposes Phoenix through a
|
|
190
|
+
sub-path, add that sub-path to the end of this URL setting.
|
|
191
|
+
|
|
192
|
+
WARNING: When a sub-path is needed, you must also specify the sub-path via the environment
|
|
193
|
+
variable PHOENIX_HOST_ROOT_PATH. Setting just this URL setting is not enough.
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
- With a sub-path: "https://example.com/phoenix"
|
|
197
|
+
- Without a sub-path: "https://phoenix.example.com"
|
|
198
|
+
"""
|
|
185
199
|
|
|
186
200
|
|
|
187
201
|
# SMTP settings
|
|
@@ -828,7 +842,7 @@ def get_env_host() -> str:
|
|
|
828
842
|
|
|
829
843
|
|
|
830
844
|
def get_env_host_root_path() -> str:
|
|
831
|
-
if (host_root_path := getenv(ENV_PHOENIX_HOST_ROOT_PATH))
|
|
845
|
+
if not (host_root_path := getenv(ENV_PHOENIX_HOST_ROOT_PATH)):
|
|
832
846
|
return HOST_ROOT_PATH
|
|
833
847
|
if not host_root_path.startswith("/"):
|
|
834
848
|
raise ValueError(
|
|
@@ -894,7 +908,33 @@ def get_env_client_headers() -> dict[str, str]:
|
|
|
894
908
|
return headers
|
|
895
909
|
|
|
896
910
|
|
|
911
|
+
def get_env_root_url() -> URL:
|
|
912
|
+
"""
|
|
913
|
+
Get the URL used to access Phoenix from a web browser
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
URL: The root URL of the Phoenix server
|
|
917
|
+
|
|
918
|
+
Note:
|
|
919
|
+
This is intended to replace the legacy `get_base_url()` helper function. In
|
|
920
|
+
particular, `get_env_collector_endpoint()` is really for the client and should be
|
|
921
|
+
deprecated on the server side.
|
|
922
|
+
"""
|
|
923
|
+
if root_url := getenv(ENV_PHOENIX_ROOT_URL):
|
|
924
|
+
result = urlparse(root_url)
|
|
925
|
+
if not result.scheme or not result.netloc:
|
|
926
|
+
raise ValueError(
|
|
927
|
+
f"The environment variable `{ENV_PHOENIX_ROOT_URL}` must be a valid URL."
|
|
928
|
+
)
|
|
929
|
+
return URL(root_url)
|
|
930
|
+
host = get_env_host()
|
|
931
|
+
if host == "0.0.0.0":
|
|
932
|
+
host = "127.0.0.1"
|
|
933
|
+
return URL(urljoin(f"http://{host}:{get_env_port()}", get_env_host_root_path()))
|
|
934
|
+
|
|
935
|
+
|
|
897
936
|
def get_base_url() -> str:
|
|
937
|
+
"""Deprecated: Use get_env_root_url() instead, but note the difference in behavior."""
|
|
898
938
|
host = get_env_host()
|
|
899
939
|
if host == "0.0.0.0":
|
|
900
940
|
host = "127.0.0.1"
|
|
@@ -1065,4 +1105,10 @@ def get_env_allowed_origins() -> Optional[list[str]]:
|
|
|
1065
1105
|
return allowed_origins.split(",")
|
|
1066
1106
|
|
|
1067
1107
|
|
|
1108
|
+
def verify_server_environment_variables() -> None:
|
|
1109
|
+
"""Verify that the environment variables are set correctly. Raises an error otherwise."""
|
|
1110
|
+
get_env_root_url()
|
|
1111
|
+
|
|
1112
|
+
|
|
1068
1113
|
SKLEARN_VERSION = cast(tuple[int, int], tuple(map(int, version("scikit-learn").split(".", 2)[:2])))
|
|
1114
|
+
PLAYGROUND_PROJECT_NAME = "playground"
|
phoenix/db/facilitator.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import logging
|
|
4
5
|
import secrets
|
|
6
|
+
from asyncio import gather
|
|
5
7
|
from functools import partial
|
|
8
|
+
from typing import Optional
|
|
6
9
|
|
|
7
10
|
from sqlalchemy import (
|
|
8
11
|
distinct,
|
|
9
12
|
insert,
|
|
10
13
|
select,
|
|
11
14
|
)
|
|
12
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
13
15
|
|
|
14
16
|
from phoenix.auth import (
|
|
15
17
|
DEFAULT_ADMIN_EMAIL,
|
|
@@ -25,8 +27,11 @@ from phoenix.config import (
|
|
|
25
27
|
)
|
|
26
28
|
from phoenix.db import models
|
|
27
29
|
from phoenix.db.enums import COLUMN_ENUMS, UserRole
|
|
30
|
+
from phoenix.server.email.types import WelcomeEmailSender
|
|
28
31
|
from phoenix.server.types import DbSessionFactory
|
|
29
32
|
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
30
35
|
|
|
31
36
|
class Facilitator:
|
|
32
37
|
"""
|
|
@@ -36,20 +41,25 @@ class Facilitator:
|
|
|
36
41
|
carried out as callbacks at the very beginning of Starlette's lifespan process.
|
|
37
42
|
"""
|
|
38
43
|
|
|
39
|
-
def __init__(
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
db: DbSessionFactory,
|
|
48
|
+
email_sender: Optional[WelcomeEmailSender] = None,
|
|
49
|
+
) -> None:
|
|
40
50
|
self._db = db
|
|
51
|
+
self._email_sender = email_sender
|
|
41
52
|
|
|
42
53
|
async def __call__(self) -> None:
|
|
43
54
|
for fn in (
|
|
44
55
|
_ensure_enums,
|
|
45
56
|
_ensure_user_roles,
|
|
46
|
-
_ensure_admins,
|
|
57
|
+
partial(_ensure_admins, email_sender=self._email_sender),
|
|
47
58
|
):
|
|
48
|
-
|
|
49
|
-
await fn(session)
|
|
59
|
+
await fn(self._db)
|
|
50
60
|
|
|
51
61
|
|
|
52
|
-
async def _ensure_enums(
|
|
62
|
+
async def _ensure_enums(db: DbSessionFactory) -> None:
|
|
53
63
|
"""
|
|
54
64
|
Ensure that all enum values are present in their respective tables. If any values are missing,
|
|
55
65
|
they will be added. If any values are present in the database but not in the enum, an error will
|
|
@@ -57,99 +67,122 @@ async def _ensure_enums(session: AsyncSession) -> None:
|
|
|
57
67
|
"""
|
|
58
68
|
for column, enum in COLUMN_ENUMS.items():
|
|
59
69
|
table = column.class_
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
async with db() as session:
|
|
71
|
+
existing = set(
|
|
72
|
+
[_ async for _ in await session.stream_scalars(select(distinct(column)))]
|
|
73
|
+
)
|
|
74
|
+
expected = set(e.value for e in enum)
|
|
75
|
+
if unexpected := existing - expected:
|
|
76
|
+
raise ValueError(f"Unexpected values in {table.name}.{column.key}: {unexpected}")
|
|
77
|
+
if not (missing := expected - existing):
|
|
78
|
+
continue
|
|
79
|
+
await session.execute(insert(table), [{column.key: v} for v in missing])
|
|
67
80
|
|
|
68
81
|
|
|
69
|
-
async def _ensure_user_roles(
|
|
82
|
+
async def _ensure_user_roles(db: DbSessionFactory) -> None:
|
|
70
83
|
"""
|
|
71
84
|
Ensure that the system and admin roles are present in the database. If they are not, they will
|
|
72
85
|
be added. The system user will have the email "system@localhost" and the admin user will have
|
|
73
86
|
the email "admin@localhost".
|
|
74
87
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
88
|
+
async with db() as session:
|
|
89
|
+
role_ids = {
|
|
90
|
+
name: id_
|
|
91
|
+
async for name, id_ in await session.stream(
|
|
92
|
+
select(models.UserRole.name, models.UserRole.id)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
existing_roles = [
|
|
96
|
+
name
|
|
97
|
+
async for name in await session.stream_scalars(
|
|
98
|
+
select(distinct(models.UserRole.name)).join_from(models.User, models.UserRole)
|
|
99
|
+
)
|
|
100
|
+
]
|
|
101
|
+
if (system_role := UserRole.SYSTEM.value) not in existing_roles and (
|
|
102
|
+
system_role_id := role_ids.get(system_role)
|
|
103
|
+
) is not None:
|
|
104
|
+
system_user = models.User(
|
|
105
|
+
user_role_id=system_role_id,
|
|
106
|
+
username=DEFAULT_SYSTEM_USERNAME,
|
|
107
|
+
email=DEFAULT_SYSTEM_EMAIL,
|
|
108
|
+
reset_password=False,
|
|
109
|
+
password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
|
|
110
|
+
password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
|
|
111
|
+
)
|
|
112
|
+
session.add(system_user)
|
|
113
|
+
if (admin_role := UserRole.ADMIN.value) not in existing_roles and (
|
|
114
|
+
admin_role_id := role_ids.get(admin_role)
|
|
115
|
+
) is not None:
|
|
116
|
+
salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
|
|
117
|
+
password = get_env_default_admin_initial_password()
|
|
118
|
+
compute = partial(compute_password_hash, password=password, salt=salt)
|
|
119
|
+
loop = asyncio.get_running_loop()
|
|
120
|
+
hash_ = await loop.run_in_executor(None, compute)
|
|
121
|
+
admin_user = models.User(
|
|
122
|
+
user_role_id=admin_role_id,
|
|
123
|
+
username=DEFAULT_ADMIN_USERNAME,
|
|
124
|
+
email=DEFAULT_ADMIN_EMAIL,
|
|
125
|
+
password_salt=salt,
|
|
126
|
+
password_hash=hash_,
|
|
127
|
+
reset_password=True,
|
|
128
|
+
)
|
|
129
|
+
session.add(admin_user)
|
|
130
|
+
await session.flush()
|
|
117
131
|
|
|
118
132
|
|
|
119
|
-
async def _ensure_admins(
|
|
133
|
+
async def _ensure_admins(
|
|
134
|
+
db: DbSessionFactory,
|
|
135
|
+
*,
|
|
136
|
+
email_sender: Optional[WelcomeEmailSender] = None,
|
|
137
|
+
) -> None:
|
|
120
138
|
"""
|
|
121
139
|
Ensure that all startup admin users are present in the database. If any are missing, they will
|
|
122
140
|
be added. Existing records will not be modified.
|
|
123
141
|
"""
|
|
124
142
|
if not (admins := get_env_admins()):
|
|
125
143
|
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return
|
|
132
|
-
existing_usernames = set(
|
|
133
|
-
await session.scalars(
|
|
134
|
-
select(models.User.username).where(models.User.username.in_(admins.values()))
|
|
144
|
+
async with db() as session:
|
|
145
|
+
existing_emails = set(
|
|
146
|
+
await session.scalars(
|
|
147
|
+
select(models.User.email).where(models.User.email.in_(admins.keys()))
|
|
148
|
+
)
|
|
135
149
|
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
assert admin_role_id is not None, "Admin role not found in database"
|
|
146
|
-
for email, username in admins.items():
|
|
147
|
-
values = dict(
|
|
148
|
-
user_role_id=admin_role_id,
|
|
149
|
-
username=username,
|
|
150
|
-
email=email,
|
|
151
|
-
password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
|
|
152
|
-
password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
|
|
153
|
-
reset_password=True,
|
|
150
|
+
admins = {
|
|
151
|
+
email: username for email, username in admins.items() if email not in existing_emails
|
|
152
|
+
}
|
|
153
|
+
if not admins:
|
|
154
|
+
return
|
|
155
|
+
existing_usernames = set(
|
|
156
|
+
await session.scalars(
|
|
157
|
+
select(models.User.username).where(models.User.username.in_(admins.values()))
|
|
158
|
+
)
|
|
154
159
|
)
|
|
155
|
-
|
|
160
|
+
admins = {
|
|
161
|
+
email: username
|
|
162
|
+
for email, username in admins.items()
|
|
163
|
+
if username not in existing_usernames
|
|
164
|
+
}
|
|
165
|
+
if not admins:
|
|
166
|
+
return
|
|
167
|
+
admin_role_id = await session.scalar(
|
|
168
|
+
select(models.UserRole.id).filter_by(name=UserRole.ADMIN.value)
|
|
169
|
+
)
|
|
170
|
+
assert admin_role_id is not None, "Admin role not found in database"
|
|
171
|
+
for email, username in admins.items():
|
|
172
|
+
values = dict(
|
|
173
|
+
user_role_id=admin_role_id,
|
|
174
|
+
username=username,
|
|
175
|
+
email=email,
|
|
176
|
+
password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
|
|
177
|
+
password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
|
|
178
|
+
reset_password=True,
|
|
179
|
+
)
|
|
180
|
+
await session.execute(insert(models.User).values(values))
|
|
181
|
+
if email_sender is None:
|
|
182
|
+
return
|
|
183
|
+
for exc in await gather(
|
|
184
|
+
*(email_sender.send_welcome_email(email, username) for email, username in admins.items()),
|
|
185
|
+
return_exceptions=True,
|
|
186
|
+
):
|
|
187
|
+
if isinstance(exc, Exception):
|
|
188
|
+
logger.error(f"Failed to send welcome email: {exc}")
|
phoenix/db/helpers.py
CHANGED
|
@@ -19,6 +19,7 @@ from sqlalchemy import (
|
|
|
19
19
|
)
|
|
20
20
|
from typing_extensions import assert_never
|
|
21
21
|
|
|
22
|
+
from phoenix.config import PLAYGROUND_PROJECT_NAME
|
|
22
23
|
from phoenix.db import models
|
|
23
24
|
|
|
24
25
|
|
|
@@ -157,3 +158,18 @@ def get_dataset_example_revisions(
|
|
|
157
158
|
),
|
|
158
159
|
)
|
|
159
160
|
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
_AnyTuple = TypeVar("_AnyTuple", bound=tuple[Any, ...])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def exclude_experiment_projects(
|
|
167
|
+
stmt: Select[_AnyTuple],
|
|
168
|
+
) -> Select[_AnyTuple]:
|
|
169
|
+
return stmt.outerjoin(
|
|
170
|
+
models.Experiment,
|
|
171
|
+
and_(
|
|
172
|
+
models.Project.name == models.Experiment.project_name,
|
|
173
|
+
models.Experiment.project_name != PLAYGROUND_PROJECT_NAME,
|
|
174
|
+
),
|
|
175
|
+
).where(models.Experiment.project_name.is_(None))
|
phoenix/server/api/context.py
CHANGED
|
@@ -53,6 +53,7 @@ from phoenix.server.api.dataloaders import (
|
|
|
53
53
|
)
|
|
54
54
|
from phoenix.server.bearer_auth import PhoenixUser
|
|
55
55
|
from phoenix.server.dml_event import DmlEvent
|
|
56
|
+
from phoenix.server.email.types import EmailSender
|
|
56
57
|
from phoenix.server.types import (
|
|
57
58
|
CanGetLastUpdatedAt,
|
|
58
59
|
CanPutItem,
|
|
@@ -124,6 +125,7 @@ class Context(BaseContext):
|
|
|
124
125
|
auth_enabled: bool = False
|
|
125
126
|
secret: Optional[str] = None
|
|
126
127
|
token_store: Optional[TokenStore] = None
|
|
128
|
+
email_sender: Optional[EmailSender] = None
|
|
127
129
|
|
|
128
130
|
def get_secret(self) -> str:
|
|
129
131
|
"""A type-safe way to get the application secret. Throws an error if the secret is not set.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import secrets
|
|
2
3
|
from contextlib import AsyncExitStack
|
|
3
4
|
from datetime import datetime, timezone
|
|
@@ -32,6 +33,8 @@ from phoenix.server.api.types.User import User, to_gql_user
|
|
|
32
33
|
from phoenix.server.bearer_auth import PhoenixUser
|
|
33
34
|
from phoenix.server.types import AccessTokenId, ApiKeyId, PasswordResetTokenId, RefreshTokenId
|
|
34
35
|
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
35
38
|
|
|
36
39
|
@strawberry.input
|
|
37
40
|
class CreateUserInput:
|
|
@@ -39,6 +42,7 @@ class CreateUserInput:
|
|
|
39
42
|
username: str
|
|
40
43
|
password: str
|
|
41
44
|
role: UserRoleInput
|
|
45
|
+
send_welcome_email: Optional[bool] = False
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
@strawberry.input
|
|
@@ -111,6 +115,12 @@ class UserMutationMixin:
|
|
|
111
115
|
await session.flush()
|
|
112
116
|
except (PostgreSQLIntegrityError, SQLiteIntegrityError) as error:
|
|
113
117
|
raise Conflict(_user_operation_error_message(error))
|
|
118
|
+
if input.send_welcome_email and info.context.email_sender is not None:
|
|
119
|
+
try:
|
|
120
|
+
await info.context.email_sender.send_welcome_email(user.email, user.username)
|
|
121
|
+
except Exception as error:
|
|
122
|
+
# Log the error but do not raise it
|
|
123
|
+
logger.error(f"Failed to send welcome email: {error}")
|
|
114
124
|
return UserMutationPayload(user=to_gql_user(user))
|
|
115
125
|
|
|
116
126
|
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
phoenix/server/api/queries.py
CHANGED
|
@@ -19,7 +19,7 @@ from phoenix.config import (
|
|
|
19
19
|
getenv,
|
|
20
20
|
)
|
|
21
21
|
from phoenix.db import enums, models
|
|
22
|
-
from phoenix.db.helpers import SupportedSQLDialect
|
|
22
|
+
from phoenix.db.helpers import SupportedSQLDialect, exclude_experiment_projects
|
|
23
23
|
from phoenix.db.models import DatasetExample as OrmExample
|
|
24
24
|
from phoenix.db.models import DatasetExampleRevision as OrmRevision
|
|
25
25
|
from phoenix.db.models import DatasetVersion as OrmVersion
|
|
@@ -42,7 +42,6 @@ from phoenix.server.api.input_types.ClusterInput import ClusterInput
|
|
|
42
42
|
from phoenix.server.api.input_types.Coordinates import InputCoordinate2D, InputCoordinate3D
|
|
43
43
|
from phoenix.server.api.input_types.DatasetSort import DatasetSort
|
|
44
44
|
from phoenix.server.api.input_types.InvocationParameters import InvocationParameter
|
|
45
|
-
from phoenix.server.api.subscriptions import PLAYGROUND_PROJECT_NAME
|
|
46
45
|
from phoenix.server.api.types.Cluster import Cluster, to_gql_clusters
|
|
47
46
|
from phoenix.server.api.types.Dataset import Dataset, to_gql_dataset
|
|
48
47
|
from phoenix.server.api.types.DatasetExample import DatasetExample
|
|
@@ -233,18 +232,8 @@ class Query:
|
|
|
233
232
|
last=last,
|
|
234
233
|
before=before if isinstance(before, CursorString) else None,
|
|
235
234
|
)
|
|
236
|
-
stmt = (
|
|
237
|
-
|
|
238
|
-
.outerjoin(
|
|
239
|
-
models.Experiment,
|
|
240
|
-
and_(
|
|
241
|
-
models.Project.name == models.Experiment.project_name,
|
|
242
|
-
models.Experiment.project_name != PLAYGROUND_PROJECT_NAME,
|
|
243
|
-
),
|
|
244
|
-
)
|
|
245
|
-
.where(models.Experiment.project_name.is_(None))
|
|
246
|
-
.order_by(models.Project.id)
|
|
247
|
-
)
|
|
235
|
+
stmt = select(models.Project).order_by(models.Project.id)
|
|
236
|
+
stmt = exclude_experiment_projects(stmt)
|
|
248
237
|
async with info.context.db() as session:
|
|
249
238
|
projects = await session.stream_scalars(stmt)
|
|
250
239
|
data = [
|