arize-phoenix 11.16.0__py3-none-any.whl → 11.17.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: 11.16.0
3
+ Version: 11.17.0
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://arize.com/docs/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -1,12 +1,12 @@
1
1
  phoenix/__init__.py,sha256=xkpXH76HFbEDCq8IhiFp-2GnEHx39xPMdOpV5Skew1w,5481
2
- phoenix/auth.py,sha256=yW78f1xWNjTE30ACGUM14nOd5BzkukhlzA9B45kSUkM,11053
3
- phoenix/config.py,sha256=VWYsWA9yzL0ml_1dQ9Q-vnEZxBri1TO8zJocBBUVeZc,61985
2
+ phoenix/auth.py,sha256=9rscOefuxy5VPcb6MlOYenuy3gDd5l7RxknONovGslE,11342
3
+ phoenix/config.py,sha256=1Wb3pToeubtw1OFXsLct9HVYxQBsvjTqtPxkCT3q4OA,62749
4
4
  phoenix/datetime_utils.py,sha256=pRD-nzxXYKlMWNtd3r2tKGKfPFhwuJhfOAtlGLVAO60,8784
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=2kHfT3BNOVd4dAO1bq-syEQbHSG8oX2-7NhOwK2QREk,896
9
- phoenix/version.py,sha256=1jPwVRFknFjr1Z2-4KMQGv_ONw2Eybch5xZM1zNP1rg,24
9
+ phoenix/version.py,sha256=HjDQzFRz8KWfrwF_W0ajYBKE8yZf-yeJHl8aLSu_Zis,24
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
@@ -19,7 +19,7 @@ phoenix/db/bulk_inserter.py,sha256=MbbmKip6gFtnLP1ZA3Z-mP-zy-Qnb9bPD8mRXJQOW2c,1
19
19
  phoenix/db/constants.py,sha256=-YE2rkzcROG06_rerfnX5hC7fLzOHx1Gjw4nXhX_um4,46
20
20
  phoenix/db/engines.py,sha256=tB_8iWMDz0folryVvw29sbBUxJOB2XZ-Xx0Uexj3uns,6889
21
21
  phoenix/db/enums.py,sha256=w3O5YuJEEzVTwVDZb8b2UUFhU8yK_GosF081VVrrno0,188
22
- phoenix/db/facilitator.py,sha256=aRbkIJkIDP2zMsLKbO7Y8jJq4U2HbV7Lf6GYVWXVImU,20151
22
+ phoenix/db/facilitator.py,sha256=UIC-l14p3R8GFVWPmz04NY-CDm_zAynXCAuIYpj_W_g,20254
23
23
  phoenix/db/helpers.py,sha256=dsGONSgkhmVtjMpJh-84KRVTf5uPdQ5c8O2AhUgHkRg,14150
24
24
  phoenix/db/migrate.py,sha256=oUrXH8yEbcpL4eh09aSCuUiSrhFli0eT5D_j4ZmYChY,2797
25
25
  phoenix/db/models.py,sha256=bxyBRSST8rqBKcAyPyyDHmkv9AadaE3XmQnpcaMvvnk,61588
@@ -114,7 +114,7 @@ phoenix/server/api/exceptions.py,sha256=E2W0x63CBzc0CoQPptrLr9nZxPF9zIP8MCJ3RuJM
114
114
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
115
115
  phoenix/server/api/queries.py,sha256=--ycVajCdDuN4Y-7bnD9py3BIXVWnZEwrpwDEW2Y8i4,46498
116
116
  phoenix/server/api/schema.py,sha256=fcs36xQwFF_Qe41_5cWR8wYpDvOrnbcyTeo5WNMbDsA,1702
117
- phoenix/server/api/subscriptions.py,sha256=ZOGNsLVr5TNjCWgbzO7Eq6Ls_NRdJH9AxC0cW_v0vhM,25332
117
+ phoenix/server/api/subscriptions.py,sha256=U7JZl-FGfsaIhRkIFdeSQLqR7xCS7CY1h-21BOAcaqY,25439
118
118
  phoenix/server/api/utils.py,sha256=quCBRcusc6PUq9tJq7M8PgwFZp7nXgVAxtbw8feribY,833
119
119
  phoenix/server/api/dataloaders/__init__.py,sha256=ddiX1BdbyGkPTzMZNo-hkF_2kqIquelBUFvQejnAJYk,6834
120
120
  phoenix/server/api/dataloaders/annotation_configs_by_project.py,sha256=_Nfiug9o01JimU3Z0LpZJ0uaMCjchXomyt_dYAxPFRY,1178
@@ -232,7 +232,7 @@ phoenix/server/api/input_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
232
232
  phoenix/server/api/mutations/__init__.py,sha256=DlAwXDKQzOpY4vFiDUR9Cu2kmncFr4m-RPRyQABBPIE,1972
233
233
  phoenix/server/api/mutations/annotation_config_mutations.py,sha256=i7NsQhYICcQ-I-tnFjGtVAYc8WVmMBacmRaqHWJ25t4,15433
234
234
  phoenix/server/api/mutations/api_key_mutations.py,sha256=nfnRjALCaQMi_jIbEPW4G3Dn3tPnmZVU11tpBbBijGA,6242
235
- phoenix/server/api/mutations/chat_mutations.py,sha256=97g5OEpFklS2hI73ACCzpRBq5s_h9-nxzbUr82XmU-8,24373
235
+ phoenix/server/api/mutations/chat_mutations.py,sha256=KB9_39rcGWI8x687ktAiijgww-P8B9mVJKnyL8NCmKQ,24647
236
236
  phoenix/server/api/mutations/dataset_mutations.py,sha256=KRlF-Ag3twqaBpLR_6WYxf57DffaGuFBm-soaBPStbI,27787
237
237
  phoenix/server/api/mutations/experiment_mutations.py,sha256=p3CoLAa8nFPa3D759Y2A7De_PVJNGOL98mA3HoZBrRQ,3188
238
238
  phoenix/server/api/mutations/export_events_mutations.py,sha256=xoDnVWC7eA_8wNQP0-oyiHojyUZ0EhVVSrsAnztetC0,3993
@@ -245,14 +245,14 @@ phoenix/server/api/mutations/prompt_version_tag_mutations.py,sha256=t77osYb5he2A
245
245
  phoenix/server/api/mutations/span_annotations_mutations.py,sha256=LQPcODp7-ZobXspjmtLaamyQa8UkTONC_va-ST9r-k8,15015
246
246
  phoenix/server/api/mutations/trace_annotations_mutations.py,sha256=zWoMfOMSQZqw7gZl7Le2PRojkDcG_KOiP1iIuqZpZ8Q,11971
247
247
  phoenix/server/api/mutations/trace_mutations.py,sha256=AvtQAfqNWBQpJOZm4e0DZFimhVJ6HQHtSSZtezRadCo,4698
248
- phoenix/server/api/mutations/user_mutations.py,sha256=9vFWfvfhb6RUdxL8xajV2-P2kVDtFRijRD58OFeBY4M,15373
248
+ phoenix/server/api/mutations/user_mutations.py,sha256=6mwMx5I5c_VgUutOLrN9LSMcyPLgXzNaT0XT24ZwNWM,15473
249
249
  phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
250
250
  phoenix/server/api/openapi/main.py,sha256=yKdzJYI4cxy_1mFcK4_7YObIcuRviBIfwNjB23RG14k,461
251
251
  phoenix/server/api/openapi/schema.py,sha256=WGmHWSIyJhtc5EIh_M3vlXU-EgHkFuTlyVofgS0kj1I,529
252
252
  phoenix/server/api/routers/__init__.py,sha256=YIzHsIFOOXuCRbDkMUHx-McrANFJK5UfUn6a4BNIzmo,277
253
- phoenix/server/api/routers/auth.py,sha256=nwoum3HKlrSkNF0MWN91rrsTcWMVKefGI9xPz9s0DGw,11604
253
+ phoenix/server/api/routers/auth.py,sha256=PKGwWdw7O015KmjMY1mIxlvXeU7OrmqIPF5TWTOGmp4,11871
254
254
  phoenix/server/api/routers/embeddings.py,sha256=BpZGJee0pdL0W5Rp1L0b30dEtZTgJeVqXky8LgZ0ZXw,898
255
- phoenix/server/api/routers/oauth2.py,sha256=rwIqeJe4T5jK5NeuPAmrfNsbBqUZhMSFRNJdOJYyFPc,24445
255
+ phoenix/server/api/routers/oauth2.py,sha256=rPcKFvfijzBYLjfwbCNzCn0ihn4wGWh4xh6BRqg9Ay4,24524
256
256
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
257
257
  phoenix/server/api/routers/v1/__init__.py,sha256=ngLMPjC7lgZxgKy_Is33KxTRnMzSqy25qTTChCVx_Mo,2696
258
258
  phoenix/server/api/routers/v1/annotation_configs.py,sha256=xp5lJmKYlRsINCUrRD9-lTAElw2v4hdFndS5BWrxICA,16048
@@ -261,13 +261,13 @@ phoenix/server/api/routers/v1/datasets.py,sha256=9iPORLmbOrPKgUUcRDMs6ZczSIz7hvc
261
261
  phoenix/server/api/routers/v1/evaluations.py,sha256=_I0X01J8EZmTlfAGkvMkZ05IB3x691xzUmHV05ov26Y,12914
262
262
  phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=_xnVqFCwZoOUPravdPixZLSQiU1H3sB5EzMknB7E1CI,4837
263
263
  phoenix/server/api/routers/v1/experiment_runs.py,sha256=LZeCQWQIEOZ9jK5Gp_C4JbiYY6AmnnWe85cVcvdkCLE,7107
264
- phoenix/server/api/routers/v1/experiments.py,sha256=osy0JG_iVSCkoRi__0tjldfS5zzBDvNfv-GimbP4ShA,20537
264
+ phoenix/server/api/routers/v1/experiments.py,sha256=wML-cad0NiflFGIysaJECFulXLqj5rjwMmNmKQVaGzw,20592
265
265
  phoenix/server/api/routers/v1/models.py,sha256=p3gJN-9SWiUYTUTft4bZMsZVCBNTb4nN1Foy68eRZzQ,1997
266
266
  phoenix/server/api/routers/v1/projects.py,sha256=32GwTLsaFgQLVNdjrlrGe90XT3pIX1N7-zX9D9_J_4w,12701
267
267
  phoenix/server/api/routers/v1/prompts.py,sha256=chRYcLkOYDJdJfVZVukVTUyIRnLPvsJCg41CuPxOIU8,26695
268
268
  phoenix/server/api/routers/v1/spans.py,sha256=ETH6I14O_zY9IW69Fo-LxL796BR3xgt8qdzwqzYAvbE,44208
269
269
  phoenix/server/api/routers/v1/traces.py,sha256=uLASCHMgU13tUhuWXnXqaom1crrQVpXi9PUtsyDXU9Y,10318
270
- phoenix/server/api/routers/v1/users.py,sha256=hUZCe7ctJqEkSJBe046a0OAFMLZodtyO7NLP7U6S8Pg,11986
270
+ phoenix/server/api/routers/v1/users.py,sha256=eO8zMtGU33Td2_G1l9D7Z0a4CG1CwBUCj_Z9z2uk7wg,12089
271
271
  phoenix/server/api/routers/v1/utils.py,sha256=oXIOGPzPTkE0ZWUTRCoRIQQ7wTzoSwtWFaUSjlGBqts,4960
272
272
  phoenix/server/api/types/Annotation.py,sha256=gsl8CwjIbDUbZRj4d9USwZ_w_Tkz4i7zuZh9ftV80jA,1132
273
273
  phoenix/server/api/types/AnnotationConfig.py,sha256=TPukZUgvFC17W93Vnme21EhswasBMR-ZiuSWteiWZOU,3891
@@ -376,6 +376,8 @@ phoenix/server/email/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
376
376
  phoenix/server/email/templates/db_disk_usage_notification.html,sha256=Atv2XnKkC7HasTTCZYAWySr7VbupAGwbIiJ9eQ4r2l0,906
377
377
  phoenix/server/email/templates/password_reset.html,sha256=jv0Pe-06JloPZcubRWxPdAFHYEn9eDj_4SjmuoIwshI,441
378
378
  phoenix/server/email/templates/welcome.html,sha256=AyVsIOtpmyYwWmmkXjuEgXwkbsag4YHHKfkmOTiNo-M,316
379
+ phoenix/server/experiments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
380
+ phoenix/server/experiments/utils.py,sha256=I8OSkjLse7eVJasyiBwql7ihzOrMivsxOQVhD8aWi1E,356
379
381
  phoenix/server/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
380
382
  phoenix/server/middleware/gzip.py,sha256=hoznxxXdJlb6_XSyNXFz0OLd9FhtCHv6Tm0TWh0Zijk,1363
381
383
  phoenix/server/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -389,10 +391,10 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
389
391
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
390
392
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
391
393
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
392
- phoenix/server/static/.vite/manifest.json,sha256=kdJluW5SaIjnZ4iseFuA4LCG0bVknsw1pSXJT2h3XLo,2165
393
- phoenix/server/static/assets/components-CK8hwrPx.js,sha256=g1vGnQRiqgG1yDDRMSbiUES-P4kkwsUbl4BPl9EH3Ek,650224
394
- phoenix/server/static/assets/index-ByWTGseQ.js,sha256=S6_w9RczLWlZc2MxJ5_G_Z7O464nyUIQcns2SXhtAHU,63020
395
- phoenix/server/static/assets/pages-D8Hgmz1V.js,sha256=zVZMTQw6vFjJPf9FjvfXpYwosimlWXCGtMnwxNBbMKs,1208677
394
+ phoenix/server/static/.vite/manifest.json,sha256=48DKS78rqBq8OHQd2eb76lt5r2gQlHve4Ow7LB2xzLM,2165
395
+ phoenix/server/static/assets/components-B7NKnJXz.js,sha256=yk-zZLTZ1djweAS0tVSnq7Ju28H-P8wvh0-HqNS7ny0,650348
396
+ phoenix/server/static/assets/index-9n9lXgT6.js,sha256=GJb5yf2HCr_6u8_0A2xAuMo7ppXVp7MtP_p83GxnBAM,63064
397
+ phoenix/server/static/assets/pages-CvqPVUA3.js,sha256=uhmqphVuAlblKQBOPY5vw4TcfIfj4GH5Ut_1KwNvrf4,1208813
396
398
  phoenix/server/static/assets/vendor-CqDb5u4o.css,sha256=zIyFiNJKxMaQk8AvtLgt1rR01oO10d1MFndSDKH9Clw,5517
397
399
  phoenix/server/static/assets/vendor-_6rG8OMg.js,sha256=stdw5w5Q5kJ0EkGpzu_f_IYaEEwKHkn3eNDZSxBRQUE,2682340
398
400
  phoenix/server/static/assets/vendor-arizeai-BznCmJFh.js,sha256=qFSHnyPDSh4mml_O0oiMaCmZv2e9E3VQXPxzySPABWA,151750
@@ -439,9 +441,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
439
441
  phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
440
442
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
441
443
  phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
442
- arize_phoenix-11.16.0.dist-info/METADATA,sha256=Vhoaw8m0XPZKoPzmUJxOrEfcQ-UZhTdIGXceg5fEiuU,30851
443
- arize_phoenix-11.16.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
444
- arize_phoenix-11.16.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
445
- arize_phoenix-11.16.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
446
- arize_phoenix-11.16.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
447
- arize_phoenix-11.16.0.dist-info/RECORD,,
444
+ arize_phoenix-11.17.0.dist-info/METADATA,sha256=nXdJG0rfxbEw8NVFRaftEkhrc18xcNqpOMRlPgMzd1c,30851
445
+ arize_phoenix-11.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
446
+ arize_phoenix-11.17.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
447
+ arize_phoenix-11.17.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
448
+ arize_phoenix-11.17.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
449
+ arize_phoenix-11.17.0.dist-info/RECORD,,
phoenix/auth.py CHANGED
@@ -47,6 +47,18 @@ def is_valid_password(*, password: Secret, salt: bytes, password_hash: bytes) ->
47
47
  return password_hash == compute_password_hash(password=password, salt=salt)
48
48
 
49
49
 
50
+ def sanitize_email(email: str) -> str:
51
+ """
52
+ Sanitizes an email address by trimming whitespace and converting to lowercase.
53
+
54
+ Args:
55
+ email (str): the email address to sanitize
56
+ Returns:
57
+ str: the sanitized email address
58
+ """
59
+ return email.strip().lower()
60
+
61
+
50
62
  def validate_email_format(email: str) -> None:
51
63
  """
52
64
  Checks that the email has a valid format.
phoenix/config.py CHANGED
@@ -333,6 +333,10 @@ Whether to verify client certificates for mutual TLS (mTLS) authentication.
333
333
  When set to true, clients must provide valid certificates signed by the CA specified in
334
334
  PHOENIX_TLS_CA_FILE.
335
335
  """
336
+ ENV_PHOENIX_DEFAULT_RETENTION_POLICY_DAYS = "PHOENIX_DEFAULT_RETENTION_POLICY_DAYS"
337
+ """
338
+ The default retention policy for traces in days.
339
+ """
336
340
 
337
341
 
338
342
  @dataclass(frozen=True)
@@ -494,6 +498,20 @@ def get_env_tls_verify_client() -> bool:
494
498
  return _bool_val(ENV_PHOENIX_TLS_VERIFY_CLIENT, False)
495
499
 
496
500
 
501
+ def get_env_default_retention_policy_days() -> int:
502
+ """
503
+ Returns the number of days for the default retention policy as set by the
504
+ PHOENIX_DEFAULT_RETENTION_POLICY_DAYS environment variable, defaulting to 0 if not set.
505
+
506
+ Returns:
507
+ int: Number of days for the default retention policy. Defaults to 0 if the environment variable is not set.
508
+ """ # noqa: E501
509
+ days = _int_val(ENV_PHOENIX_DEFAULT_RETENTION_POLICY_DAYS, 0)
510
+ if days < 0:
511
+ raise ValueError("PHOENIX_DEFAULT_RETENTION_POLICY_DAYS must be non-negative")
512
+ return days
513
+
514
+
497
515
  def get_env_tls_config() -> Optional[TLSConfig]:
498
516
  """
499
517
  Retrieves and validates TLS configuration from environment variables.
@@ -865,6 +883,8 @@ def get_env_admins() -> dict[str, str]:
865
883
  """
866
884
  if not (env_value := getenv(ENV_PHOENIX_ADMINS)):
867
885
  return {}
886
+ from phoenix.auth import sanitize_email
887
+
868
888
  usernames = set()
869
889
  emails = set()
870
890
  ans = {}
@@ -881,7 +901,7 @@ def get_env_admins() -> dict[str, str]:
881
901
  f"Expected format: 'username=email'"
882
902
  )
883
903
  username = pair[:last_equals_pos].strip()
884
- email_addr = pair[last_equals_pos + 1 :].strip()
904
+ email_addr = sanitize_email(pair[last_equals_pos + 1 :])
885
905
  try:
886
906
  email_addr = validate_email(email_addr, check_deliverability=False).normalized
887
907
  except EmailNotValidError:
phoenix/db/facilitator.py CHANGED
@@ -28,6 +28,7 @@ from phoenix.auth import (
28
28
  from phoenix.config import (
29
29
  get_env_admins,
30
30
  get_env_default_admin_initial_password,
31
+ get_env_default_retention_policy_days,
31
32
  get_env_disable_basic_auth,
32
33
  )
33
34
  from phoenix.db import models
@@ -334,7 +335,9 @@ async def _ensure_default_project_trace_retention_policy(db: DbSessionFactory) -
334
335
  ):
335
336
  return
336
337
  cron_expression = TraceRetentionCronExpression(root="0 0 * * 0")
337
- rule = TraceRetentionRule(root=MaxDaysRule(max_days=0))
338
+ rule = TraceRetentionRule(
339
+ root=MaxDaysRule(max_days=get_env_default_retention_policy_days())
340
+ )
338
341
  await session.execute(
339
342
  sa.insert(models.ProjectTraceRetentionPolicy),
340
343
  [
@@ -23,6 +23,7 @@ from strawberry.relay import GlobalID
23
23
  from strawberry.types import Info
24
24
  from typing_extensions import assert_never
25
25
 
26
+ from phoenix.config import PLAYGROUND_PROJECT_NAME
26
27
  from phoenix.datetime_utils import local_now, normalize_datetime
27
28
  from phoenix.db import models
28
29
  from phoenix.db.helpers import get_dataset_example_revisions
@@ -64,6 +65,7 @@ from phoenix.server.api.types.DatasetVersion import DatasetVersion
64
65
  from phoenix.server.api.types.node import from_global_id_with_expected_type
65
66
  from phoenix.server.api.types.Span import Span
66
67
  from phoenix.server.dml_event import SpanInsertEvent
68
+ from phoenix.server.experiments.utils import generate_experiment_project_name
67
69
  from phoenix.trace.attributes import unflatten
68
70
  from phoenix.trace.schemas import SpanException
69
71
  from phoenix.utilities.json import jsonify
@@ -163,6 +165,7 @@ class ChatCompletionMutationMixin:
163
165
  if input.dataset_version_id
164
166
  else None
165
167
  )
168
+ project_name = generate_experiment_project_name()
166
169
  async with info.context.db() as session:
167
170
  dataset = await session.scalar(select(models.Dataset).filter_by(id=dataset_id))
168
171
  if dataset is None:
@@ -196,7 +199,7 @@ class ChatCompletionMutationMixin:
196
199
  description=input.experiment_description,
197
200
  repetitions=1,
198
201
  metadata_=input.experiment_metadata or dict(),
199
- project_name=PLAYGROUND_PROJECT_NAME,
202
+ project_name=project_name,
200
203
  )
201
204
  session.add(experiment)
202
205
  await session.flush()
@@ -222,6 +225,7 @@ class ChatCompletionMutationMixin:
222
225
  ),
223
226
  prompt_name=input.prompt_name,
224
227
  ),
228
+ project_name=project_name,
225
229
  )
226
230
  for revision in batch
227
231
  ),
@@ -320,6 +324,8 @@ class ChatCompletionMutationMixin:
320
324
  info: Info[Context, None],
321
325
  llm_client: PlaygroundStreamingClient,
322
326
  input: ChatCompletionInput,
327
+ project_name: str = PLAYGROUND_PROJECT_NAME,
328
+ project_description: str = "Traces from prompt playground",
323
329
  ) -> ChatCompletionMutationPayload:
324
330
  attributes: dict[str, Any] = {}
325
331
  attributes.update(dict(prompt_metadata(input.prompt_name)))
@@ -414,15 +420,15 @@ class ChatCompletionMutationMixin:
414
420
  # Get or create the project ID
415
421
  if (
416
422
  project_id := await session.scalar(
417
- select(models.Project.id).where(models.Project.name == PLAYGROUND_PROJECT_NAME)
423
+ select(models.Project.id).where(models.Project.name == project_name)
418
424
  )
419
425
  ) is None:
420
426
  project_id = await session.scalar(
421
427
  insert(models.Project)
422
428
  .returning(models.Project.id)
423
429
  .values(
424
- name=PLAYGROUND_PROJECT_NAME,
425
- description="Traces from prompt playground",
430
+ name=project_name,
431
+ description=project_description,
426
432
  )
427
433
  )
428
434
  trace = models.Trace(
@@ -622,5 +628,3 @@ TOOL_JSON_SCHEMA = ToolAttributes.TOOL_JSON_SCHEMA
622
628
  PROMPT_TEMPLATE_VARIABLES = SpanAttributes.LLM_PROMPT_TEMPLATE_VARIABLES
623
629
 
624
630
  LLM_PROVIDER = SpanAttributes.LLM_PROVIDER
625
-
626
- PLAYGROUND_PROJECT_NAME = "playground"
@@ -21,6 +21,7 @@ from phoenix.auth import (
21
21
  PASSWORD_REQUIREMENTS,
22
22
  PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
23
23
  PHOENIX_REFRESH_TOKEN_COOKIE_NAME,
24
+ sanitize_email,
24
25
  validate_email_format,
25
26
  validate_password_format,
26
27
  )
@@ -115,20 +116,23 @@ class UserMutationMixin:
115
116
  info: Info[Context, None],
116
117
  input: CreateUserInput,
117
118
  ) -> UserMutationPayload:
119
+ # Sanitize email by trimming and lowercasing
120
+ email = sanitize_email(input.email)
121
+
118
122
  user: models.User
119
123
  if input.auth_method is AuthMethod.OAUTH2:
120
124
  user = models.OAuth2User(
121
- email=input.email,
125
+ email=email,
122
126
  username=input.username,
123
127
  )
124
128
  else:
125
129
  assert input.password
126
- validate_email_format(input.email)
130
+ validate_email_format(email)
127
131
  validate_password_format(input.password)
128
132
  salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
129
133
  password_hash = await info.context.hash_password(Secret(input.password), salt)
130
134
  user = models.LocalUser(
131
- email=input.email,
135
+ email=email,
132
136
  username=input.username,
133
137
  password_hash=password_hash,
134
138
  password_salt=salt,
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from urllib.parse import urlencode, urlparse, urlunparse
7
7
 
8
8
  from fastapi import APIRouter, Depends, HTTPException, Request, Response
9
- from sqlalchemy import select
9
+ from sqlalchemy import func, select
10
10
  from sqlalchemy.orm import joinedload
11
11
  from starlette.status import (
12
12
  HTTP_204_NO_CONTENT,
@@ -29,6 +29,7 @@ from phoenix.auth import (
29
29
  delete_oauth2_state_cookie,
30
30
  delete_refresh_token_cookie,
31
31
  is_valid_password,
32
+ sanitize_email,
32
33
  set_access_token_cookie,
33
34
  set_refresh_token_cookie,
34
35
  validate_password_format,
@@ -87,9 +88,14 @@ async def login(request: Request) -> Response:
87
88
  if not email or not password:
88
89
  raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Email and password required")
89
90
 
91
+ # Sanitize email by trimming and lowercasing
92
+ email = sanitize_email(email)
93
+
90
94
  async with request.app.state.db() as session:
91
95
  user = await session.scalar(
92
- select(models.User).filter_by(email=email).options(joinedload(models.User.role))
96
+ select(models.User)
97
+ .where(func.lower(models.User.email) == email)
98
+ .options(joinedload(models.User.role))
93
99
  )
94
100
  if (
95
101
  user is None
@@ -207,6 +213,10 @@ async def initiate_password_reset(request: Request) -> Response:
207
213
  data = await request.json()
208
214
  if not (email := data.get("email")):
209
215
  raise MISSING_EMAIL
216
+
217
+ # Sanitize email by trimming and lowercasing
218
+ email = sanitize_email(email)
219
+
210
220
  sender: EmailSender = request.app.state.email_sender
211
221
  if sender is None:
212
222
  raise SMTP_UNAVAILABLE
@@ -214,7 +224,7 @@ async def initiate_password_reset(request: Request) -> Response:
214
224
  async with request.app.state.db() as session:
215
225
  user = await session.scalar(
216
226
  select(models.User)
217
- .filter_by(email=email)
227
+ .where(func.lower(models.User.email) == email)
218
228
  .options(
219
229
  joinedload(models.User.password_reset_token).load_only(models.PasswordResetToken.id)
220
230
  )
@@ -27,6 +27,7 @@ from phoenix.auth import (
27
27
  PHOENIX_OAUTH2_STATE_COOKIE_NAME,
28
28
  delete_oauth2_nonce_cookie,
29
29
  delete_oauth2_state_cookie,
30
+ sanitize_email,
30
31
  set_access_token_cookie,
31
32
  set_oauth2_nonce_cookie,
32
33
  set_oauth2_state_cookie,
@@ -217,7 +218,7 @@ class UserInfo:
217
218
  if not (idp_user_id := (self.idp_user_id or "").strip()):
218
219
  raise ValueError("idp_user_id cannot be empty")
219
220
  object.__setattr__(self, "idp_user_id", idp_user_id)
220
- if not (email := (self.email or "").strip()):
221
+ if not (email := sanitize_email(self.email or "")):
221
222
  raise ValueError("email cannot be empty")
222
223
  object.__setattr__(self, "email", email)
223
224
  if username := (self.username or "").strip():
@@ -356,7 +357,7 @@ async def _get_existing_oauth2_user(
356
357
  - User has a password set
357
358
  - User has mismatched OAuth2 credentials
358
359
  """ # noqa: E501
359
- if not (email := (user_info.email or "").strip()):
360
+ if not (email := sanitize_email(user_info.email or "")):
360
361
  raise ValueError("Email is required.")
361
362
  if not (oauth2_user_id := (user_info.idp_user_id or "").strip()):
362
363
  raise ValueError("OAuth2 user ID is required.")
@@ -370,7 +371,7 @@ async def _get_existing_oauth2_user(
370
371
  if email and email != user.email:
371
372
  user.email = email
372
373
  else:
373
- user = await session.scalar(stmt.filter_by(email=email))
374
+ user = await session.scalar(stmt.where(func.lower(models.User.email) == email))
374
375
  if user is None or not isinstance(user, models.OAuth2User):
375
376
  raise SignInNotAllowed("Sign in is not allowed.")
376
377
  # Case 1: Different OAuth2 client - update both client and user IDs
@@ -520,7 +521,7 @@ async def _email_and_username_exist(
520
521
  select(
521
522
  cast(
522
523
  func.coalesce(
523
- func.max(case((models.User.email == email, 1), else_=0)),
524
+ func.max(case((func.lower(models.User.email) == email, 1), else_=0)),
524
525
  0,
525
526
  ),
526
527
  Boolean,
@@ -532,7 +533,7 @@ async def _email_and_username_exist(
532
533
  ),
533
534
  Boolean,
534
535
  ).label("username_exists"),
535
- ).where(or_(models.User.email == email, models.User.username == username))
536
+ ).where(or_(func.lower(models.User.email) == email, models.User.username == username))
536
537
  )
537
538
  ).all()
538
539
  return email_exists, username_exists
@@ -20,6 +20,7 @@ from phoenix.db.insertion.helpers import insert_on_conflict
20
20
  from phoenix.server.api.types.node import from_global_id_with_expected_type
21
21
  from phoenix.server.authorization import is_not_locked
22
22
  from phoenix.server.dml_event import ExperimentInsertEvent
23
+ from phoenix.server.experiments.utils import generate_experiment_project_name
23
24
 
24
25
  from .models import V1RoutesBaseModel
25
26
  from .utils import ResponseBody, add_errors_to_responses, add_text_csv_content_to_responses
@@ -159,7 +160,7 @@ async def create_experiment(
159
160
 
160
161
  # generate a semi-unique name for the experiment
161
162
  experiment_name = request_body.name or _generate_experiment_name(dataset_name)
162
- project_name = f"Experiment-{getrandbits(96).to_bytes(12, 'big').hex()}"
163
+ project_name = generate_experiment_project_name()
163
164
  project_description = (
164
165
  f"dataset_id: {dataset_globalid}\ndataset_version_id: {dataset_version_globalid}"
165
166
  )
@@ -31,6 +31,7 @@ from phoenix.auth import (
31
31
  DEFAULT_SYSTEM_EMAIL,
32
32
  DEFAULT_SYSTEM_USERNAME,
33
33
  compute_password_hash,
34
+ sanitize_email,
34
35
  validate_email_format,
35
36
  validate_password_format,
36
37
  )
@@ -205,6 +206,8 @@ async def create_user(
205
206
  ) -> CreateUserResponseBody:
206
207
  user_data = request_body.user
207
208
  email, username, role = user_data.email, user_data.username, user_data.role
209
+ # Sanitize email by trimming and lowercasing
210
+ email = sanitize_email(email)
208
211
  validate_email_format(email)
209
212
 
210
213
  # Prevent creation of SYSTEM users
@@ -64,6 +64,7 @@ from phoenix.server.api.types.node import from_global_id_with_expected_type
64
64
  from phoenix.server.api.types.Span import Span
65
65
  from phoenix.server.daemons.span_cost_calculator import SpanCostCalculator
66
66
  from phoenix.server.dml_event import SpanInsertEvent
67
+ from phoenix.server.experiments.utils import generate_experiment_project_name
67
68
  from phoenix.server.types import DbSessionFactory
68
69
  from phoenix.utilities.template_formatters import (
69
70
  FStringTemplateFormatter,
@@ -287,16 +288,17 @@ class Subscription:
287
288
  ]
288
289
  ):
289
290
  raise NotFound("No examples found for the given dataset and version")
291
+ project_name = generate_experiment_project_name()
290
292
  if (
291
293
  playground_project_id := await session.scalar(
292
- select(models.Project.id).where(models.Project.name == PLAYGROUND_PROJECT_NAME)
294
+ select(models.Project.id).where(models.Project.name == project_name)
293
295
  )
294
296
  ) is None:
295
297
  playground_project_id = await session.scalar(
296
298
  insert(models.Project)
297
299
  .returning(models.Project.id)
298
300
  .values(
299
- name=PLAYGROUND_PROJECT_NAME,
301
+ name=project_name,
300
302
  description="Traces from prompt playground",
301
303
  )
302
304
  )
@@ -308,7 +310,7 @@ class Subscription:
308
310
  description=input.experiment_description,
309
311
  repetitions=1,
310
312
  metadata_=input.experiment_metadata or dict(),
311
- project_name=PLAYGROUND_PROJECT_NAME,
313
+ project_name=project_name,
312
314
  )
313
315
  session.add(experiment)
314
316
  await session.flush()
File without changes
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from secrets import token_hex
5
+
6
+ EXPERIMENT_PROJECT_NAME_PATTERN = re.compile(r"^Experiment-[0-9a-f]{24}$")
7
+
8
+
9
+ def generate_experiment_project_name() -> str:
10
+ return f"Experiment-{token_hex(12)}"
11
+
12
+
13
+ def is_experiment_project_name(name: str) -> bool:
14
+ return bool(EXPERIMENT_PROJECT_NAME_PATTERN.match(name))
@@ -1,22 +1,22 @@
1
1
  {
2
- "_components-CK8hwrPx.js": {
3
- "file": "assets/components-CK8hwrPx.js",
2
+ "_components-B7NKnJXz.js": {
3
+ "file": "assets/components-B7NKnJXz.js",
4
4
  "name": "components",
5
5
  "imports": [
6
6
  "_vendor-_6rG8OMg.js",
7
- "_pages-D8Hgmz1V.js",
7
+ "_pages-CvqPVUA3.js",
8
8
  "_vendor-arizeai-BznCmJFh.js",
9
9
  "_vendor-codemirror-29fWLPAy.js",
10
10
  "_vendor-three-C5WAXd5r.js"
11
11
  ]
12
12
  },
13
- "_pages-D8Hgmz1V.js": {
14
- "file": "assets/pages-D8Hgmz1V.js",
13
+ "_pages-CvqPVUA3.js": {
14
+ "file": "assets/pages-CvqPVUA3.js",
15
15
  "name": "pages",
16
16
  "imports": [
17
17
  "_vendor-_6rG8OMg.js",
18
18
  "_vendor-arizeai-BznCmJFh.js",
19
- "_components-CK8hwrPx.js",
19
+ "_components-B7NKnJXz.js",
20
20
  "_vendor-codemirror-29fWLPAy.js",
21
21
  "_vendor-recharts-Cu431IpB.js"
22
22
  ]
@@ -69,15 +69,15 @@
69
69
  "name": "vendor-three"
70
70
  },
71
71
  "index.tsx": {
72
- "file": "assets/index-ByWTGseQ.js",
72
+ "file": "assets/index-9n9lXgT6.js",
73
73
  "name": "index",
74
74
  "src": "index.tsx",
75
75
  "isEntry": true,
76
76
  "imports": [
77
77
  "_vendor-_6rG8OMg.js",
78
78
  "_vendor-arizeai-BznCmJFh.js",
79
- "_pages-D8Hgmz1V.js",
80
- "_components-CK8hwrPx.js",
79
+ "_pages-CvqPVUA3.js",
80
+ "_components-B7NKnJXz.js",
81
81
  "_vendor-three-C5WAXd5r.js",
82
82
  "_vendor-codemirror-29fWLPAy.js",
83
83
  "_vendor-shiki-Ce9e01lU.js",