arize-phoenix 9.6.1__py3-none-any.whl → 10.0.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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arize-phoenix
3
- Version: 9.6.1
3
+ Version: 10.0.0
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
@@ -1,12 +1,12 @@
1
1
  phoenix/__init__.py,sha256=X3eUEwd2rG8KKWWYVNNDJoqo08ihfjgHhlP29dcdNJE,5481
2
- phoenix/auth.py,sha256=VVMHrWN31tln3Zo4z6ofecrV4daiqJjLd8r85mqlxek,10939
3
- phoenix/config.py,sha256=p6uh69Tk0t1orLqrWrSoQ42xt6ceJyuR2BYSBYMlme0,51117
2
+ phoenix/auth.py,sha256=yW78f1xWNjTE30ACGUM14nOd5BzkukhlzA9B45kSUkM,11053
3
+ phoenix/config.py,sha256=J8fyMSM1sHegfIsUVPCUH58uew-CSSH52w9kyAIN0PE,57002
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=ngkyKGVatX3cO2WJdo2hKdaVKP-xJCMvqthvga6kJss,5196
8
8
  phoenix/settings.py,sha256=x87BX7hWGQQZbrW_vrYqFR_izCGfO9gFc--JXUG4Tdk,754
9
- phoenix/version.py,sha256=gkjhVoAFhlcpBLzRIiqhKP7hOWAIqvxp25eVs0y914g,22
9
+ phoenix/version.py,sha256=deXRkcNEpb32HB7j5yBmHLLQw46NO_uQtYe4EHMDyS4,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,11 +18,11 @@ phoenix/db/alembic.ini,sha256=GIS6HpHaKaJbbuahZg1Rc1D2_QqyCkV9r58wdARGf6w,3262
18
18
  phoenix/db/bulk_inserter.py,sha256=faNjuwLqqsw4ky8sa4D0h9u5TvEDTOjrccUW89L008E,12725
19
19
  phoenix/db/constants.py,sha256=-YE2rkzcROG06_rerfnX5hC7fLzOHx1Gjw4nXhX_um4,46
20
20
  phoenix/db/engines.py,sha256=tB_8iWMDz0folryVvw29sbBUxJOB2XZ-Xx0Uexj3uns,6889
21
- phoenix/db/enums.py,sha256=tt7iovXLhVTLZ3_LbHNGgcI44SnNjXfkKtLAZG57T54,428
22
- phoenix/db/facilitator.py,sha256=dznnV_kZNgpUe5D1BRpznfsClEkb1cgmd2QRMO4KyOM,9370
21
+ phoenix/db/enums.py,sha256=k8zkgDvJT-Fk4P8apvPjRdS2xlJWH8_YGN_W-wYm5tk,373
22
+ phoenix/db/facilitator.py,sha256=FBwGSRoxv_ZVIzC7HRVPSNy3c3q2Dj-vW_QmYYENSIE,9670
23
23
  phoenix/db/helpers.py,sha256=rbbHcl-STzcEpcXCYx6jbKzko7r3ggrWHHsXjZ48HsM,5352
24
24
  phoenix/db/migrate.py,sha256=oUrXH8yEbcpL4eh09aSCuUiSrhFli0eT5D_j4ZmYChY,2797
25
- phoenix/db/models.py,sha256=6a8-3Ciz2OylXlwQvWupODGcmHUNguolmJwk_PWa2QQ,51185
25
+ phoenix/db/models.py,sha256=UELkdw132bsF9d3g2Jh-lM2UCX2sA6A8osMRTo7wZAg,51939
26
26
  phoenix/db/pg_config.py,sha256=h6mB7qF7t4Zk6VGvAiyefHGVu74o-yJynaWzeE39k9Y,6001
27
27
  phoenix/db/insertion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  phoenix/db/insertion/constants.py,sha256=8wifm7X-1XvroZ__R2Gc96NsgLhTDn0zXl4lehlXtcA,70
@@ -43,6 +43,7 @@ phoenix/db/migrations/versions/10460e46d750_datasets.py,sha256=E2xC5NCmY6bEvJGf1
43
43
  phoenix/db/migrations/versions/2f9d1a65945f_annotation_config_migration.py,sha256=hkGeTYYw-2gZufjXD0OMhJiBrGepoCJ7hUc28IN3twY,10775
44
44
  phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py,sha256=RxE_1vrpSAy2GKwHvrU3cOLA_bxcRRDibAj87nJEDKE,3622
45
45
  phoenix/db/migrations/versions/4ded9e43755f_create_project_sessions_table.py,sha256=HDP3X5yxm5PwxMkODd-wrSwspQpFotTEz40jXRgq320,1787
46
+ phoenix/db/migrations/versions/6a88424799fe_update_users_with_auth_method.py,sha256=URZ1JYPHtP4atDGgqiQDpQ45feTFD19QqR4RHetEImY,8181
46
47
  phoenix/db/migrations/versions/8a3764fe7f1a_change_jsonb_to_json_for_prompts.py,sha256=VWluPffBH11NADJ1-CTzoFvbrqIFmEAOSKI21_jsLk8,1841
47
48
  phoenix/db/migrations/versions/bb8139330879_create_project_trace_retention_policies_table.py,sha256=MmkEZRfDPppX9iM9yxyDzmXlJXlGCEqhbN0nCT8aBF0,2044
48
49
  phoenix/db/migrations/versions/bc8fea3c2bc8_add_prompt_tables.py,sha256=rq-bwg0grRVvbKvwE2ZBCFG_nQRmYNCd2PJeFd_5SDk,5527
@@ -88,15 +89,15 @@ phoenix/pointcloud/pointcloud.py,sha256=SN_1wXZcwKrtSnHGZLDZGx71orqE1WyVF7E-D58d
88
89
  phoenix/pointcloud/projectors.py,sha256=TQgwc9cJDjJkin1WZyZzgl3HsYrLLiyWD7Czy4jNW3U,1088
89
90
  phoenix/pointcloud/umap_parameters.py,sha256=db_WEPoamuWtopZx7tQfAXPnoE0MS8FkAV0_ThjEx_Q,1735
90
91
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
- phoenix/server/app.py,sha256=zDBaR8GPphzxV9HyVY8ktPovwxVcnKPXRIWnxKJMSGY,39961
92
+ phoenix/server/app.py,sha256=1N7OWYQ3obWhdb_Qbrg8mSuexvNk_oT1Taq8smfmMiU,41265
92
93
  phoenix/server/authorization.py,sha256=fofeRwuoodCUB3DQYPCuAgIyeiwopXW0tkH_KZvU0Rg,1848
93
- phoenix/server/bearer_auth.py,sha256=9AY0-aOSHm-B7OYB-20jFA7bETAA6S1_W1za42A8Yt8,6804
94
+ phoenix/server/bearer_auth.py,sha256=gh_DncYFcOcDzNO7V6zYAGDidSb_UKMGdcDUqV7gdhQ,6748
94
95
  phoenix/server/dml_event.py,sha256=MjJmVEKytq75chBOSyvYDusUnEbg1pHpIjR3pZkUaJA,2838
95
96
  phoenix/server/dml_event_handler.py,sha256=EZLXmCvx4pJrCkz29gxwKwmvmUkTtPCHw6klR-XM8qE,8258
96
97
  phoenix/server/grpc_server.py,sha256=dod29zE_Zlir7NyLcdVM8GH3P8sy-9ykzfaBfVifyE4,4656
97
- phoenix/server/jwt_store.py,sha256=asxzY4_ZBM2FWAMstHvhvnKUP_0AA3v3xPTL2IOgNqY,16831
98
- phoenix/server/main.py,sha256=8iOi3HhYjnpbAZLe7D0KxFOj2MyjTueaSeDI8BldHC8,18048
99
- phoenix/server/oauth2.py,sha256=EV4wcCwG0N7cJRcfGNURdP5rZgRVCeRDvXyle19A27Y,2064
98
+ phoenix/server/jwt_store.py,sha256=ZMNfRwFebRI2UDO3vmMxZF8qpZDT6LR8q-n0uoi1Y4g,16891
99
+ phoenix/server/main.py,sha256=FI7wZW8SYPvVLC6dUVpweFcHmKcLTaUxfGE0i_4lZNw,18252
100
+ phoenix/server/oauth2.py,sha256=GvUqZBoZ5dG-l2G1RMl1SUcN10jNAjaMXFznMSWz2Zs,3336
100
101
  phoenix/server/prometheus.py,sha256=1KjvSfjSa2-BPjDybVMM_Kag316CsN-Zwt64YNr_snc,7825
101
102
  phoenix/server/rate_limiters.py,sha256=cFc73D2NaxqNZZDbwfIDw4So-fRVOJPBtqxOZ8Qky_s,7155
102
103
  phoenix/server/retention.py,sha256=MQe1FWuc_NxhqgIq5q2hfFhWT8ddAmpppgI74xYEQ6c,3064
@@ -106,7 +107,7 @@ phoenix/server/types.py,sha256=gJJPBcDRkQ9VHZIt_aLqG_OBbGt1oWp4e3W3Jp61oKs,7409
106
107
  phoenix/server/api/README.md,sha256=Pyq1PLPgTzXAswrfIhGXrjI3Skq8it2jTVnanT6Ba4Q,1162
107
108
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
109
  phoenix/server/api/auth.py,sha256=cvKH2FQLL7PasiqZMHgu9P4qchhEG7a7P9Nxy35FoIQ,1551
109
- phoenix/server/api/context.py,sha256=O1-mnFWcxV69r0Uw4zz_630qEdRGHiZQCPicAbrZYxo,6898
110
+ phoenix/server/api/context.py,sha256=oxNmVIIyycl22iQZjv59lU1inwlo-Povxe2Ok7t54mw,6954
110
111
  phoenix/server/api/exceptions.py,sha256=TA0JuY2YRnj35qGuMSQ8d0ToHum9gWm9W--3fSKHrX0,1171
111
112
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
112
113
  phoenix/server/api/queries.py,sha256=LhJdTVYZX8xo0vuSzlKz5x5-I0le4byfUdCBMSSsgtw,40793
@@ -219,14 +220,14 @@ phoenix/server/api/mutations/prompt_version_tag_mutations.py,sha256=t77osYb5he2A
219
220
  phoenix/server/api/mutations/span_annotations_mutations.py,sha256=LQPcODp7-ZobXspjmtLaamyQa8UkTONC_va-ST9r-k8,15015
220
221
  phoenix/server/api/mutations/trace_annotations_mutations.py,sha256=PLNNzSlk3fyJHkAVaMGR8pbWB63nOos-cStUWbTi7f8,11995
221
222
  phoenix/server/api/mutations/trace_mutations.py,sha256=D5h2HYdlTo6yYZNq-O-PjaS9GeiZHxxVaOxDdh7fwjw,2957
222
- phoenix/server/api/mutations/user_mutations.py,sha256=7CSTuUENIfKkdFhwLguqPG28h3zO5KrhMhfQqSCjNIg,14164
223
+ phoenix/server/api/mutations/user_mutations.py,sha256=wrYhO6s-pFQvH_1dTBLZafcIWnxdE6HU6EqxdUZDSPI,15568
223
224
  phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
224
225
  phoenix/server/api/openapi/main.py,sha256=yKdzJYI4cxy_1mFcK4_7YObIcuRviBIfwNjB23RG14k,461
225
226
  phoenix/server/api/openapi/schema.py,sha256=WGmHWSIyJhtc5EIh_M3vlXU-EgHkFuTlyVofgS0kj1I,529
226
227
  phoenix/server/api/routers/__init__.py,sha256=YIzHsIFOOXuCRbDkMUHx-McrANFJK5UfUn6a4BNIzmo,277
227
- phoenix/server/api/routers/auth.py,sha256=T774FE5mqrfRSSYo1snpR5NIp3YzAJnsLsY9FJB9GCA,11164
228
+ phoenix/server/api/routers/auth.py,sha256=bP2ptHe53BxlG10w-8ul6fYd1ZpYW3iscV3mlMlFGp8,11479
228
229
  phoenix/server/api/routers/embeddings.py,sha256=BpZGJee0pdL0W5Rp1L0b30dEtZTgJeVqXky8LgZ0ZXw,898
229
- phoenix/server/api/routers/oauth2.py,sha256=qXSUmnHPoRS8extKB8qUD8zbnUerAHeoEyEiVPiCyek,17285
230
+ phoenix/server/api/routers/oauth2.py,sha256=ByE3QImKb7FWQKK2RJTzQ1J7WiLlSw_5WnksRtSstIw,24120
230
231
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
231
232
  phoenix/server/api/routers/v1/__init__.py,sha256=0oOcsKJkQtBXAjZAo3AMtfjyW3OGCU4MI4TGW5nV6lo,2614
232
233
  phoenix/server/api/routers/v1/annotation_configs.py,sha256=rZ3yJm7m75BVegSjSHqsdqf7n26roGg7vYYiiKfWA3A,15898
@@ -346,10 +347,10 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
346
347
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
347
348
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
348
349
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
349
- phoenix/server/static/.vite/manifest.json,sha256=hW3yshzfVwBhZmcRVFOv6lgVC5qy7v5U59K207nsiVI,2165
350
- phoenix/server/static/assets/components-CDvTuTqd.js,sha256=lJUX_imM4QeN2DzlgfWXoC-tJ3eci8aeS-YaU6Bgy1Y,535701
351
- phoenix/server/static/assets/index-DpcxdHu4.js,sha256=qvTZErnPG4_mjpaNZbTi53rL_9s9k25udpb0AElehBM,60240
352
- phoenix/server/static/assets/pages-Bcs41-Zv.js,sha256=o5FR83BYGFyox-6dXabSgsHCGxVGblMzqnlKVpCsRjY,1038496
350
+ phoenix/server/static/.vite/manifest.json,sha256=zbBBOCJNmoQbDvnJ_wMeXJnqHXKbWhZWP34KtGWbcjo,2165
351
+ phoenix/server/static/assets/components-CjGpmneV.js,sha256=MCQOQA1sK05pmqerhgP0WQ-YFKLEPqO9USHapgcbS4A,537618
352
+ phoenix/server/static/assets/index-C57g4e_o.js,sha256=82Unl1FqR2QKeZdWPy8OYFT2s_D8vIcxSYUzQ-oVJmg,60283
353
+ phoenix/server/static/assets/pages-fQ2s7TFY.js,sha256=3g7A2Vi2f4N1qj_S5Uw0GoI_B6DTEEeaFfdHt1v3DPk,1039682
353
354
  phoenix/server/static/assets/vendor-CToBXdDM.js,sha256=q_UwZrhCRrNhrvFyv3OO6bW52jM1TDiYk3aTj-NgdLU,2744392
354
355
  phoenix/server/static/assets/vendor-WIZid84E.css,sha256=spZD2r7XL5GfLO13ln-IuXfnjAref8l6g_n_AvxxOlI,5517
355
356
  phoenix/server/static/assets/vendor-arizeai-BhbMHqQs.js,sha256=l3G1o-P_IYcqQWOHBcSpT5RextOH2myGl58ZSN7NvcQ,193248
@@ -358,7 +359,7 @@ phoenix/server/static/assets/vendor-recharts-PlWJHgM9.js,sha256=2F5ko_UqDn7-C7v7
358
359
  phoenix/server/static/assets/vendor-shiki-CPwL2jwA.js,sha256=goWi7CncK66zsIjhFTExUD-0cWQSFSGtpXjb1GYflcw,8980312
359
360
  phoenix/server/static/assets/vendor-three-C5WAXd5r.js,sha256=ELkg06u70N7h8oFmvqdoHyPuUf9VgGEWeT4LKFx4VWo,620975
360
361
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
361
- phoenix/server/templates/index.html,sha256=e8_jdi7Eo19SK7DI_gglkTW094D17E0VAegoMmmmvIc,4330
362
+ phoenix/server/templates/index.html,sha256=NpJ83DULqcStXFbShNamX4_NPDtnnucuBxppvUYjJa8,4409
362
363
  phoenix/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
363
364
  phoenix/session/client.py,sha256=uw5WlCuFcN_eEj7Ko2bhJVcaihEIp7Evy50KnL6Sq-k,35602
364
365
  phoenix/session/data_extractor.py,sha256=Y0RzYFaNy9fQj8PEIeQ76TBZ90_E1FW7bXu3K5x0EZY,2782
@@ -396,9 +397,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
396
397
  phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
397
398
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
398
399
  phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
399
- arize_phoenix-9.6.1.dist-info/METADATA,sha256=WERys-C4em-Qhasa3VK1FGQoe33vfqXhySCYQ3mvX5A,25590
400
- arize_phoenix-9.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
401
- arize_phoenix-9.6.1.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
402
- arize_phoenix-9.6.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
403
- arize_phoenix-9.6.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
404
- arize_phoenix-9.6.1.dist-info/RECORD,,
400
+ arize_phoenix-10.0.0.dist-info/METADATA,sha256=3eyiDeDkyez7xfhrJyoXtiBGnbpNTvIUtE2L_Q27Rz4,25591
401
+ arize_phoenix-10.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
402
+ arize_phoenix-10.0.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
403
+ arize_phoenix-10.0.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
404
+ arize_phoenix-10.0.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
405
+ arize_phoenix-10.0.0.dist-info/RECORD,,
phoenix/auth.py CHANGED
@@ -7,15 +7,16 @@ from enum import Enum, auto
7
7
  from hashlib import pbkdf2_hmac
8
8
  from typing import Any, Literal, Optional, Protocol
9
9
 
10
+ from starlette.datastructures import Secret
10
11
  from starlette.responses import Response
11
12
  from typing_extensions import TypeVar
12
13
 
13
- from phoenix.config import get_env_phoenix_use_secure_cookies
14
+ from phoenix.config import get_env_cookies_path, get_env_phoenix_use_secure_cookies
14
15
 
15
16
  ResponseType = TypeVar("ResponseType", bound=Response)
16
17
 
17
18
 
18
- def compute_password_hash(*, password: str, salt: bytes) -> bytes:
19
+ def compute_password_hash(*, password: Secret, salt: bytes) -> bytes:
19
20
  """
20
21
  Salts and hashes a password using PBKDF2, HMAC and SHA256.
21
22
 
@@ -26,11 +27,11 @@ def compute_password_hash(*, password: str, salt: bytes) -> bytes:
26
27
  bytes: the hashed password
27
28
  """
28
29
  assert salt
29
- password_bytes = password.encode("utf-8")
30
+ password_bytes = str(password).encode("utf-8")
30
31
  return pbkdf2_hmac("sha256", password_bytes, salt, NUM_ITERATIONS)
31
32
 
32
33
 
33
- def is_valid_password(*, password: str, salt: bytes, password_hash: bytes) -> bool:
34
+ def is_valid_password(*, password: Secret, salt: bytes, password_hash: bytes) -> bool:
34
35
  """
35
36
  Determines whether the password is valid by salting and hashing the password
36
37
  and comparing against the existing hash value.
@@ -131,6 +132,7 @@ def _set_cookie(
131
132
  httponly=True,
132
133
  samesite=samesite,
133
134
  max_age=int(cookie_max_age.total_seconds()),
135
+ path=get_env_cookies_path(),
134
136
  )
135
137
  return response
136
138
 
phoenix/config.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import os
3
5
  import re
@@ -7,16 +9,18 @@ from datetime import timedelta
7
9
  from enum import Enum
8
10
  from importlib.metadata import version
9
11
  from pathlib import Path
10
- from typing import Any, Optional, Union, cast, overload
12
+ from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Union, cast, overload
11
13
  from urllib.parse import quote_plus, urljoin, urlparse
12
14
 
13
15
  import wrapt
14
16
  from email_validator import EmailNotValidError, validate_email
15
- from starlette.datastructures import URL
17
+ from starlette.datastructures import URL, Secret
16
18
 
17
19
  from phoenix.utilities.logging import log_a_list
20
+ from phoenix.utilities.re import parse_env_headers
18
21
 
19
- from .utilities.re import parse_env_headers
22
+ if TYPE_CHECKING:
23
+ from phoenix.server.oauth2 import OAuth2Clients
20
24
 
21
25
  logger = logging.getLogger(__name__)
22
26
 
@@ -138,6 +142,11 @@ ENV_PHOENIX_SERVER_INSTRUMENTATION_OTLP_TRACE_COLLECTOR_GRPC_ENDPOINT = (
138
142
 
139
143
  # Authentication settings
140
144
  ENV_PHOENIX_ENABLE_AUTH = "PHOENIX_ENABLE_AUTH"
145
+ ENV_PHOENIX_DISABLE_BASIC_AUTH = "PHOENIX_DISABLE_BASIC_AUTH"
146
+ """
147
+ Forbid login via password and disable the creation of local users, which log in via passwords.
148
+ This can be helpful in setups where authentication is handled entirely through OAUTH2.
149
+ """
141
150
  ENV_PHOENIX_DISABLE_RATE_LIMIT = "PHOENIX_DISABLE_RATE_LIMIT"
142
151
  ENV_PHOENIX_SECRET = "PHOENIX_SECRET"
143
152
  """
@@ -160,6 +169,7 @@ be updated manually in the application.
160
169
  """
161
170
  ENV_PHOENIX_API_KEY = "PHOENIX_API_KEY"
162
171
  ENV_PHOENIX_USE_SECURE_COOKIES = "PHOENIX_USE_SECURE_COOKIES"
172
+ ENV_PHOENIX_COOKIES_PATH = "PHOENIX_COOKIES_PATH"
163
173
  ENV_PHOENIX_ACCESS_TOKEN_EXPIRY_MINUTES = "PHOENIX_ACCESS_TOKEN_EXPIRY_MINUTES"
164
174
  """
165
175
  The duration, in minutes, before access tokens expire.
@@ -555,7 +565,7 @@ def _bool_val(env_var: str, default: Optional[bool] = None) -> Optional[bool]:
555
565
  assert (lower := value.lower()) in (
556
566
  "true",
557
567
  "false",
558
- ), f"{env_var} must be set to TRUE or FALSE (case-insensitive)"
568
+ ), f"{env_var} must be set to TRUE or FALSE (case-insensitive). Got: {value}"
559
569
  return lower == "true"
560
570
 
561
571
 
@@ -573,8 +583,7 @@ def _float_val(env_var: str, default: Optional[float] = None) -> Optional[float]
573
583
  return float(value)
574
584
  except ValueError:
575
585
  raise ValueError(
576
- f"Invalid value for environment variable {env_var}: {value}. "
577
- f"Value must be a number."
586
+ f"Invalid value for environment variable {env_var}: {value}. Value must be a number."
578
587
  )
579
588
 
580
589
 
@@ -592,8 +601,7 @@ def _int_val(env_var: str, default: Optional[int] = None) -> Optional[int]:
592
601
  return int(value)
593
602
  except ValueError:
594
603
  raise ValueError(
595
- f"Invalid value for environment variable {env_var}: {value}. "
596
- f"Value must be an integer."
604
+ f"Invalid value for environment variable {env_var}: {value}. Value must be an integer."
597
605
  )
598
606
 
599
607
 
@@ -631,6 +639,13 @@ def get_env_enable_auth() -> bool:
631
639
  return _bool_val(ENV_PHOENIX_ENABLE_AUTH, False)
632
640
 
633
641
 
642
+ def get_env_disable_basic_auth() -> bool:
643
+ """
644
+ Gets the value of the ENV_PHOENIX_DISABLE_BASIC_AUTH environment variable.
645
+ """
646
+ return _bool_val(ENV_PHOENIX_DISABLE_BASIC_AUTH, False)
647
+
648
+
634
649
  def get_env_disable_rate_limit() -> bool:
635
650
  """
636
651
  Gets the value of the PHOENIX_DISABLE_RATE_LIMIT environment variable.
@@ -638,29 +653,29 @@ def get_env_disable_rate_limit() -> bool:
638
653
  return _bool_val(ENV_PHOENIX_DISABLE_RATE_LIMIT, False)
639
654
 
640
655
 
641
- def get_env_phoenix_secret() -> Optional[str]:
656
+ def get_env_phoenix_secret() -> Secret:
642
657
  """
643
658
  Gets the value of the PHOENIX_SECRET environment variable
644
659
  and performs validation.
645
660
  """
646
661
  phoenix_secret = getenv(ENV_PHOENIX_SECRET)
647
662
  if phoenix_secret is None:
648
- return None
663
+ return Secret("")
649
664
  from phoenix.auth import REQUIREMENTS_FOR_PHOENIX_SECRET
650
665
 
651
666
  REQUIREMENTS_FOR_PHOENIX_SECRET.validate(phoenix_secret, "Phoenix secret")
652
- return phoenix_secret
667
+ return Secret(phoenix_secret)
653
668
 
654
669
 
655
- def get_env_phoenix_admin_secret() -> Optional[str]:
670
+ def get_env_phoenix_admin_secret() -> Secret:
656
671
  """
657
672
  Gets the value of the PHOENIX_ADMIN_SECRET environment variable
658
673
  and performs validation.
659
674
  """
660
675
  phoenix_admin_secret = getenv(ENV_PHOENIX_ADMIN_SECRET)
661
676
  if phoenix_admin_secret is None:
662
- return None
663
- if (phoenix_secret := get_env_phoenix_secret()) is None:
677
+ return Secret("")
678
+ if not (phoenix_secret := get_env_phoenix_secret()):
664
679
  raise ValueError(
665
680
  f"`{ENV_PHOENIX_ADMIN_SECRET}` must be not be set without "
666
681
  f"setting `{ENV_PHOENIX_SECRET}`."
@@ -668,17 +683,24 @@ def get_env_phoenix_admin_secret() -> Optional[str]:
668
683
  from phoenix.auth import REQUIREMENTS_FOR_PHOENIX_SECRET
669
684
 
670
685
  REQUIREMENTS_FOR_PHOENIX_SECRET.validate(phoenix_admin_secret, "Phoenix secret")
671
- if phoenix_admin_secret == phoenix_secret:
686
+ if phoenix_admin_secret == str(phoenix_secret):
672
687
  raise ValueError(
673
688
  f"`{ENV_PHOENIX_ADMIN_SECRET}` must be different from `{ENV_PHOENIX_SECRET}`"
674
689
  )
675
- return phoenix_admin_secret
690
+ return Secret(phoenix_admin_secret)
676
691
 
677
692
 
678
- def get_env_default_admin_initial_password() -> str:
693
+ def get_env_default_admin_initial_password() -> Secret:
679
694
  from phoenix.auth import DEFAULT_ADMIN_PASSWORD
680
695
 
681
- return getenv(ENV_PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD) or DEFAULT_ADMIN_PASSWORD
696
+ return Secret(getenv(ENV_PHOENIX_DEFAULT_ADMIN_INITIAL_PASSWORD) or DEFAULT_ADMIN_PASSWORD)
697
+
698
+
699
+ def get_env_cookies_path() -> str:
700
+ """
701
+ Gets the value of the PHOENIX_COOKIE_PATH environment variable.
702
+ """
703
+ return getenv(ENV_PHOENIX_COOKIES_PATH, "/")
682
704
 
683
705
 
684
706
  def get_env_phoenix_use_secure_cookies() -> bool:
@@ -689,7 +711,15 @@ def get_env_phoenix_api_key() -> Optional[str]:
689
711
  return getenv(ENV_PHOENIX_API_KEY)
690
712
 
691
713
 
692
- def get_env_auth_settings() -> tuple[bool, Optional[str]]:
714
+ class AuthSettings(NamedTuple):
715
+ enable_auth: bool
716
+ disable_basic_auth: bool
717
+ phoenix_secret: Secret
718
+ phoenix_admin_secret: Secret
719
+ oauth2_clients: OAuth2Clients
720
+
721
+
722
+ def get_env_auth_settings() -> AuthSettings:
693
723
  """
694
724
  Gets auth settings and performs validation.
695
725
  """
@@ -700,7 +730,22 @@ def get_env_auth_settings() -> tuple[bool, Optional[str]]:
700
730
  f"`{ENV_PHOENIX_SECRET}` must be set when "
701
731
  f"auth is enabled with `{ENV_PHOENIX_ENABLE_AUTH}`"
702
732
  )
703
- return enable_auth, phoenix_secret
733
+ phoenix_admin_secret = get_env_phoenix_admin_secret()
734
+ disable_basic_auth = get_env_disable_basic_auth()
735
+ from phoenix.server.oauth2 import OAuth2Clients
736
+
737
+ oauth2_clients = OAuth2Clients.from_configs(get_env_oauth2_settings())
738
+ if enable_auth and disable_basic_auth and not oauth2_clients:
739
+ raise ValueError(
740
+ "OAuth2 is the only supported auth method but no OAuth2 client configs are provided."
741
+ )
742
+ return AuthSettings(
743
+ enable_auth=enable_auth,
744
+ disable_basic_auth=disable_basic_auth,
745
+ phoenix_secret=phoenix_secret,
746
+ phoenix_admin_secret=phoenix_admin_secret,
747
+ oauth2_clients=oauth2_clients,
748
+ )
704
749
 
705
750
 
706
751
  def get_env_password_reset_token_expiry() -> timedelta:
@@ -839,6 +884,8 @@ class OAuth2ClientConfig:
839
884
  client_id: str
840
885
  client_secret: str
841
886
  oidc_config_url: str
887
+ allow_sign_up: bool
888
+ auto_login: bool
842
889
 
843
890
  @classmethod
844
891
  def from_env(cls, idp_name: str) -> "OAuth2ClientConfig":
@@ -870,6 +917,8 @@ class OAuth2ClientConfig:
870
917
  f"An OpenID Connect configuration URL must be set for the {idp_name} OAuth2 IDP "
871
918
  f"via the {oidc_config_url_env_var} environment variable"
872
919
  )
920
+ allow_sign_up = get_env_oauth2_allow_sign_up(idp_name)
921
+ auto_login = get_env_oauth2_auto_login(idp_name)
873
922
  parsed_oidc_config_url = urlparse(oidc_config_url)
874
923
  is_local_oidc_config_url = parsed_oidc_config_url.hostname in ("localhost", "127.0.0.1")
875
924
  if parsed_oidc_config_url.scheme != "https" and not is_local_oidc_config_url:
@@ -886,17 +935,52 @@ class OAuth2ClientConfig:
886
935
  client_id=client_id,
887
936
  client_secret=client_secret,
888
937
  oidc_config_url=oidc_config_url,
938
+ allow_sign_up=allow_sign_up,
939
+ auto_login=auto_login,
889
940
  )
890
941
 
891
942
 
892
943
  def get_env_oauth2_settings() -> list[OAuth2ClientConfig]:
893
944
  """
894
- Get OAuth2 settings from environment variables.
895
- """
945
+ Retrieves and validates OAuth2/OpenID Connect (OIDC) identity provider configurations from environment variables.
946
+
947
+ This function scans the environment for OAuth2 configuration variables and returns a list of
948
+ configured identity providers. It supports multiple identity providers simultaneously.
949
+
950
+ Environment Variable Pattern:
951
+ PHOENIX_OAUTH2_{IDP_NAME}_{CONFIG_TYPE}
896
952
 
953
+ Required Environment Variables for each IDP:
954
+ - PHOENIX_OAUTH2_{IDP_NAME}_CLIENT_ID: The OAuth2 client ID issued by the identity provider
955
+ - PHOENIX_OAUTH2_{IDP_NAME}_CLIENT_SECRET: The OAuth2 client secret issued by the identity provider
956
+ - PHOENIX_OAUTH2_{IDP_NAME}_OIDC_CONFIG_URL: The OpenID Connect configuration URL (must be HTTPS)
957
+
958
+ Optional Environment Variables:
959
+ - PHOENIX_OAUTH2_{IDP_NAME}_DISPLAY_NAME: A user-friendly name for the identity provider
960
+ - PHOENIX_OAUTH2_{IDP_NAME}_ALLOW_SIGN_UP: Whether to allow new user registration (defaults to True)
961
+ When set to False, the system will check if the user exists in the database by their email address.
962
+ If the user does not exist or has a password set, they will be redirected to the login page with
963
+ an error message.
964
+
965
+ Returns:
966
+ list[OAuth2ClientConfig]: A list of configured OAuth2 identity providers, sorted alphabetically by IDP name.
967
+ Each OAuth2ClientConfig contains the validated configuration for one identity provider.
968
+
969
+ Raises:
970
+ ValueError: If required environment variables are missing or invalid.
971
+ Specifically, if the OIDC configuration URL is not HTTPS (except for localhost).
972
+
973
+ Example:
974
+ To configure Google as an identity provider, set these environment variables:
975
+ PHOENIX_OAUTH2_GOOGLE_CLIENT_ID=your_client_id
976
+ PHOENIX_OAUTH2_GOOGLE_CLIENT_SECRET=your_client_secret
977
+ PHOENIX_OAUTH2_GOOGLE_OIDC_CONFIG_URL=https://accounts.google.com/.well-known/openid-configuration
978
+ PHOENIX_OAUTH2_GOOGLE_DISPLAY_NAME=Google (optional)
979
+ PHOENIX_OAUTH2_GOOGLE_ALLOW_SIGN_UP=true (optional, defaults to true)
980
+ """ # noqa: E501
897
981
  idp_names = set()
898
982
  pattern = re.compile(
899
- r"^PHOENIX_OAUTH2_(\w+)_(DISPLAY_NAME|CLIENT_ID|CLIENT_SECRET|OIDC_CONFIG_URL)$"
983
+ r"^PHOENIX_OAUTH2_(\w+)_(DISPLAY_NAME|CLIENT_ID|CLIENT_SECRET|OIDC_CONFIG_URL|ALLOW_SIGN_UP|AUTO_LOGIN)$" # noqa: E501
900
984
  )
901
985
  for env_var in os.environ:
902
986
  if (match := pattern.match(env_var)) is not None and (idp_name := match.group(1).lower()):
@@ -904,6 +988,47 @@ def get_env_oauth2_settings() -> list[OAuth2ClientConfig]:
904
988
  return [OAuth2ClientConfig.from_env(idp_name) for idp_name in sorted(idp_names)]
905
989
 
906
990
 
991
+ def get_env_oauth2_allow_sign_up(idp_name: str) -> bool:
992
+ """Retrieves the allow_sign_up setting for a specific OAuth2 identity provider.
993
+
994
+ This function determines whether new user registration is allowed for the specified identity provider.
995
+ When set to False, the system will check if the user exists in the database by their email address.
996
+ If the user does not exist or has a password set, they will be redirected to the login page with
997
+ an error message.
998
+
999
+ Parameters:
1000
+ idp_name (str): The name of the identity provider (e.g., 'google', 'aws_cognito', 'microsoft_entra_id')
1001
+
1002
+ Returns:
1003
+ bool: True if new user registration is allowed (default), False otherwise
1004
+
1005
+ Environment Variable:
1006
+ PHOENIX_OAUTH2_{IDP_NAME}_ALLOW_SIGN_UP: Controls whether new user registration is allowed (defaults to True if not set)
1007
+ """ # noqa: E501
1008
+ env_var = f"PHOENIX_OAUTH2_{idp_name}_ALLOW_SIGN_UP".upper()
1009
+ return _bool_val(env_var, True)
1010
+
1011
+
1012
+ def get_env_oauth2_auto_login(idp_name: str) -> bool:
1013
+ """Retrieves the auto_login setting for a specific OAuth2 identity provider.
1014
+
1015
+ This function determines whether users should be automatically logged in when accessing the OAuth2
1016
+ identity provider's login page. When set to True, users will be redirected to the identity provider's
1017
+ login page without first seeing the application's login page.
1018
+
1019
+ Parameters:
1020
+ idp_name (str): The name of the identity provider (e.g., 'google', 'aws_cognito', 'microsoft_entra_id')
1021
+
1022
+ Returns:
1023
+ bool: True if auto-login is enabled, False otherwise (defaults to False if not set)
1024
+
1025
+ Environment Variable:
1026
+ PHOENIX_OAUTH2_{IDP_NAME}_AUTO_LOGIN: Controls whether auto-login is enabled (defaults to False if not set)
1027
+ """ # noqa: E501
1028
+ env_var = f"PHOENIX_OAUTH2_{idp_name}_AUTO_LOGIN".upper()
1029
+ return _bool_val(env_var, False)
1030
+
1031
+
907
1032
  PHOENIX_DIR = Path(__file__).resolve().parent
908
1033
  # Server config
909
1034
  SERVER_DIR = PHOENIX_DIR / "server"
@@ -1258,7 +1383,7 @@ def get_env_logging_mode() -> LoggingMode:
1258
1383
  except ValueError:
1259
1384
  raise ValueError(
1260
1385
  f"Invalid value `{logging_mode}` for env var `{ENV_LOGGING_MODE}`. "
1261
- f"Valid values are: {log_a_list([mode.value for mode in LoggingMode],'and')} "
1386
+ f"Valid values are: {log_a_list([mode.value for mode in LoggingMode], 'and')} "
1262
1387
  "(case-insensitive)."
1263
1388
  )
1264
1389
 
@@ -1337,7 +1462,7 @@ def _get_logging_level(env_var: str, default_level: int) -> int:
1337
1462
  if logging_level.upper() not in valid_values:
1338
1463
  raise ValueError(
1339
1464
  f"Invalid value `{logging_level}` for env var `{env_var}`. "
1340
- f"Valid values are: {log_a_list(valid_values,'and')} (case-insensitive)."
1465
+ f"Valid values are: {log_a_list(valid_values, 'and')} (case-insensitive)."
1341
1466
  )
1342
1467
  return levelNamesMapping[logging_level.upper()]
1343
1468
 
phoenix/db/enums.py CHANGED
@@ -4,9 +4,8 @@ from enum import Enum
4
4
  from sqlalchemy.orm import InstrumentedAttribute
5
5
 
6
6
  from phoenix.db import models
7
- from phoenix.db.models import AuthMethod
8
7
 
9
- __all__ = ["AuthMethod", "UserRole", "COLUMN_ENUMS"]
8
+ __all__ = ["UserRole", "COLUMN_ENUMS"]
10
9
 
11
10
 
12
11
  class UserRole(Enum):
phoenix/db/facilitator.py CHANGED
@@ -26,6 +26,7 @@ from phoenix.auth import (
26
26
  from phoenix.config import (
27
27
  get_env_admins,
28
28
  get_env_default_admin_initial_password,
29
+ get_env_disable_basic_auth,
29
30
  )
30
31
  from phoenix.db import models
31
32
  from phoenix.db.constants import DEFAULT_PROJECT_TRACE_RETENTION_POLICY_ID
@@ -111,7 +112,7 @@ async def _ensure_user_roles(db: DbSessionFactory) -> None:
111
112
  if (system_role := UserRole.SYSTEM.value) not in existing_roles and (
112
113
  system_role_id := role_ids.get(system_role)
113
114
  ) is not None:
114
- system_user = models.User(
115
+ system_user = models.LocalUser(
115
116
  user_role_id=system_role_id,
116
117
  username=DEFAULT_SYSTEM_USERNAME,
117
118
  email=DEFAULT_SYSTEM_EMAIL,
@@ -128,7 +129,7 @@ async def _ensure_user_roles(db: DbSessionFactory) -> None:
128
129
  compute = partial(compute_password_hash, password=password, salt=salt)
129
130
  loop = asyncio.get_running_loop()
130
131
  hash_ = await loop.run_in_executor(None, compute)
131
- admin_user = models.User(
132
+ admin_user = models.LocalUser(
132
133
  user_role_id=admin_role_id,
133
134
  username=DEFAULT_ADMIN_USERNAME,
134
135
  email=DEFAULT_ADMIN_EMAIL,
@@ -168,6 +169,7 @@ async def _ensure_admins(
168
169
  """
169
170
  if not (admins := get_env_admins()):
170
171
  return
172
+ disable_basic_auth = get_env_disable_basic_auth()
171
173
  async with db() as session:
172
174
  existing_emails = set(
173
175
  await session.scalars(
@@ -195,16 +197,23 @@ async def _ensure_admins(
195
197
  select(models.UserRole.id).filter_by(name=UserRole.ADMIN.value)
196
198
  )
197
199
  assert admin_role_id is not None, "Admin role not found in database"
200
+ user: models.User
198
201
  for email, username in admins.items():
199
- values = dict(
200
- user_role_id=admin_role_id,
201
- username=username,
202
- email=email,
203
- password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
204
- password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
205
- reset_password=True,
206
- )
207
- await session.execute(insert(models.User).values(values))
202
+ if not disable_basic_auth:
203
+ user = models.LocalUser(
204
+ email=email,
205
+ username=username,
206
+ password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
207
+ password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
208
+ )
209
+ else:
210
+ user = models.OAuth2User(
211
+ email=email,
212
+ username=username,
213
+ )
214
+ user.user_role_id = admin_role_id
215
+ session.add(user)
216
+ await session.flush()
208
217
  if email_sender is None:
209
218
  return
210
219
  for exc in await gather(