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

Files changed (32) hide show
  1. {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/METADATA +14 -2
  2. {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/RECORD +32 -31
  3. phoenix/config.py +33 -0
  4. phoenix/datetime_utils.py +112 -1
  5. phoenix/db/helpers.py +156 -1
  6. phoenix/server/api/auth.py +28 -6
  7. phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +6 -7
  8. phoenix/server/api/exceptions.py +6 -0
  9. phoenix/server/api/input_types/TimeBinConfig.py +23 -0
  10. phoenix/server/api/routers/oauth2.py +19 -2
  11. phoenix/server/api/types/CostBreakdown.py +4 -7
  12. phoenix/server/api/types/Project.py +341 -73
  13. phoenix/server/app.py +7 -3
  14. phoenix/server/authorization.py +27 -2
  15. phoenix/server/cost_tracking/cost_details_calculator.py +22 -16
  16. phoenix/server/daemons/span_cost_calculator.py +2 -8
  17. phoenix/server/email/sender.py +2 -1
  18. phoenix/server/email/templates/db_disk_usage_notification.html +3 -0
  19. phoenix/server/static/.vite/manifest.json +36 -36
  20. phoenix/server/static/assets/{components-J3qjrjBf.js → components-5M9nebi4.js} +344 -263
  21. phoenix/server/static/assets/{index-CEObsQf_.js → index-OU2WTnGN.js} +11 -11
  22. phoenix/server/static/assets/{pages-CW1UdBht.js → pages-DF8rqxJ4.js} +451 -444
  23. phoenix/server/static/assets/{vendor-BnPh9i9e.js → vendor-Bl7CyFDw.js} +147 -147
  24. phoenix/server/static/assets/{vendor-arizeai-Cr9o_Iu_.js → vendor-arizeai-B_viEUUA.js} +18 -480
  25. phoenix/server/static/assets/{vendor-codemirror-k3zCIjlN.js → vendor-codemirror-vlcH1_iR.js} +1 -1
  26. phoenix/server/static/assets/{vendor-recharts-BdblEuGB.js → vendor-recharts-C9cQu72o.js} +25 -25
  27. phoenix/server/static/assets/{vendor-shiki-DPtuv2M4.js → vendor-shiki-BsknB7bv.js} +1 -1
  28. phoenix/version.py +1 -1
  29. {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/WHEEL +0 -0
  30. {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/entry_points.txt +0 -0
  31. {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/licenses/IP_NOTICE +0 -0
  32. {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arize-phoenix
3
- Version: 11.7.0
3
+ Version: 11.8.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
@@ -205,6 +205,7 @@ The `arize-phoenix` package includes the entire Phoenix platfom. However if you
205
205
  | [arize-phoenix-client](https://github.com/Arize-ai/phoenix/tree/main/packages/phoenix-client) | Python [![PyPI Version](https://img.shields.io/pypi/v/arize-phoenix-client)](https://pypi.org/project/arize-phoenix-client/) | Lightweight client for interacting with the Phoenix server via its OpenAPI REST interface |
206
206
  | [arize-phoenix-evals](https://github.com/Arize-ai/phoenix/tree/main/packages/phoenix-evals) | Python [![PyPI Version](https://img.shields.io/pypi/v/arize-phoenix-evals)](https://pypi.org/project/arize-phoenix-evals/) | Tooling to evaluate LLM applications including RAG relevance, answer relevance, and more |
207
207
  | [@arizeai/phoenix-client](https://github.com/Arize-ai/phoenix/tree/main/js/packages/phoenix-client) | JavaScript [![NPM Version](https://img.shields.io/npm/v/%40arizeai%2Fphoenix-client)](https://www.npmjs.com/package/@arizeai/phoenix-client) | Client for the Arize Phoenix API |
208
+ | [@arizeai/phoenix-evals](https://github.com/Arize-ai/phoenix/tree/main/js/packages/phoenix-evals) | TypeScript [![NPM Version](https://img.shields.io/npm/v/%40arizeai%2Fphoenix-evals)](https://www.npmjs.com/package/@arizeai/phoenix-evals) | TypeScript evaluation library for LLM applications (alpha release) |
208
209
  | [@arizeai/phoenix-mcp](https://github.com/Arize-ai/phoenix/tree/main/js/packages/phoenix-mcp) | JavaScript [![NPM Version](https://img.shields.io/npm/v/%40arizeai%2Fphoenix-mcp)](https://www.npmjs.com/package/@arizeai/phoenix-mcp) | MCP server implementation for Arize Phoenix providing unified interface to Phoenix's capabilities |
209
210
 
210
211
  ## Tracing Integrations
@@ -248,9 +249,20 @@ Phoenix is built on top of OpenTelemetry and is vendor, language, and framework
248
249
  | [BeeAI](https://arize.com/docs/phoenix/tracing/integrations-tracing/beeai) | `@arizeai/openinference-instrumentation-beeai` | [![NPM Version](https://img.shields.io/npm/v/@arizeai/openinference-vercel)](https://www.npmjs.com/package/@arizeai/openinference-instrumentation-beeai) |
249
250
  | [Mastra](https://arize.com/docs/phoenix/integrations/mastra) | `@arizeai/openinference-mastra` | [![NPM Version](https://img.shields.io/npm/v/@arizeai/openinference-mastra.svg)](https://www.npmjs.com/package/@arizeai/openinference-mastra) |
250
251
 
252
+ ### Java Integrations
253
+
254
+ | Integration | Package | Version Badge |
255
+ | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
256
+ | [LangChain4j](https://github.com/Arize-ai/openinference/tree/main/java/instrumentation/openinference-instrumentation-langchain4j) | `openinference-instrumentation-langchain4j` | [![Maven Central](https://img.shields.io/maven-central/v/com.arize/openinference-instrumentation-langchain4j.svg)](https://central.sonatype.com/artifact/com.arize/openinference-instrumentation-langchain4j) |
257
+
251
258
  ### Platforms
252
259
 
253
- Phoenix has native integrations with [LangFlow](https://arize.com/docs/phoenix/tracing/integrations-tracing/langflow), LiteLLM Proxy, and [BeeAI](https://docs.beeai.dev/observability/agents-traceability).
260
+ | Platform | Description | Docs |
261
+ | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
262
+ | [Dify](https://docs.dify.ai/en/guides/monitoring/integrate-external-ops-tools/integrate-phoenix) | Open-source LLM app development platform | [Integration Guide](https://docs.dify.ai/en/guides/monitoring/integrate-external-ops-tools/integrate-phoenix) |
263
+ | [LangFlow](https://arize.com/docs/phoenix/tracing/integrations-tracing/langflow) | Visual framework for building multi-agent and RAG applications | [Integration Guide](https://arize.com/docs/phoenix/tracing/integrations-tracing/langflow) |
264
+ | [BeeAI](https://docs.beeai.dev/observability/agents-traceability) | AI agent framework with built-in observability | [Integration Guide](https://docs.beeai.dev/observability/agents-traceability) |
265
+ | [LiteLLM Proxy](https://docs.litellm.ai/docs/observability/phoenix_integration#using-with-litellm-proxy) | Proxy server for LLMs | [Integration Guide](https://docs.litellm.ai/docs/observability/phoenix_integration#using-with-litellm-proxy) |
254
266
 
255
267
  ## Community
256
268
 
@@ -1,12 +1,12 @@
1
1
  phoenix/__init__.py,sha256=xkpXH76HFbEDCq8IhiFp-2GnEHx39xPMdOpV5Skew1w,5481
2
2
  phoenix/auth.py,sha256=yW78f1xWNjTE30ACGUM14nOd5BzkukhlzA9B45kSUkM,11053
3
- phoenix/config.py,sha256=Xir2ke9AZluThsYeaiq57a8GyQOuumjIqqqeCh8PKw8,60350
4
- phoenix/datetime_utils.py,sha256=fpISOdaeDBdfIa-psFp6FJyAUPWEsiT5s3-_aLV12bg,3576
3
+ phoenix/config.py,sha256=NqSYbx1-Fdr0SSOt5RSQ5jmTz3r3xvGElVXvd9kK1gc,61495
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=rSjRBQs9AcakQYnfj7YZFHQKbSsnq8BzM_MJeZ3UZ1M,23
9
+ phoenix/version.py,sha256=i-ilTWt69mdcN4GqSDKAmTIOlguLxG2mm9Xhzz2U_aw,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
@@ -20,7 +20,7 @@ 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
22
  phoenix/db/facilitator.py,sha256=aRbkIJkIDP2zMsLKbO7Y8jJq4U2HbV7Lf6GYVWXVImU,20151
23
- phoenix/db/helpers.py,sha256=rbbHcl-STzcEpcXCYx6jbKzko7r3ggrWHHsXjZ48HsM,5352
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=0avhbUmDEBZ1_NgBSr9Ck9BYxXtJTraPfQqDGZFE2-Y,60388
26
26
  phoenix/db/pg_config.py,sha256=h6mB7qF7t4Zk6VGvAiyefHGVu74o-yJynaWzeE39k9Y,6001
@@ -91,8 +91,8 @@ phoenix/pointcloud/pointcloud.py,sha256=SN_1wXZcwKrtSnHGZLDZGx71orqE1WyVF7E-D58d
91
91
  phoenix/pointcloud/projectors.py,sha256=TQgwc9cJDjJkin1WZyZzgl3HsYrLLiyWD7Czy4jNW3U,1088
92
92
  phoenix/pointcloud/umap_parameters.py,sha256=db_WEPoamuWtopZx7tQfAXPnoE0MS8FkAV0_ThjEx_Q,1735
93
93
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
- phoenix/server/app.py,sha256=GwItmVRwSM5JspXIHwEO1hXjRAqQRQ4djZfbzJjnh3Q,46694
95
- phoenix/server/authorization.py,sha256=AGpQT6HpPn2ROOxxVBvjPV1393KF4v-K5IHJ4JaTOHM,2175
94
+ phoenix/server/app.py,sha256=tB00x9r0PcCLYkvOtTQ7LqdSyw7jqy8OY5e7UVYl5UE,46951
95
+ phoenix/server/authorization.py,sha256=OxROn7ibpKtCTrcgDkzWuNxVaQcSQ8MAx7zbjZiliK0,3201
96
96
  phoenix/server/bearer_auth.py,sha256=f4v4W94KyTdGGCPsK1tXOe0vouPuvanAEa03XSdCvPE,6650
97
97
  phoenix/server/dml_event.py,sha256=MjJmVEKytq75chBOSyvYDusUnEbg1pHpIjR3pZkUaJA,2838
98
98
  phoenix/server/dml_event_handler.py,sha256=gkDIONyTz9sLbSA6qOZCigiO5val-fvVcLzDrWNcVcg,8306
@@ -108,9 +108,9 @@ phoenix/server/thread_server.py,sha256=Ea2AWreN1lwJsT2wYvGaRaiXrzBqH4kgkZpx0FO5O
108
108
  phoenix/server/types.py,sha256=j8erl9iRNaR8t0DCQG84-uDVbHy9Qnm7T2EuTtDNNsU,8013
109
109
  phoenix/server/api/README.md,sha256=Pyq1PLPgTzXAswrfIhGXrjI3Skq8it2jTVnanT6Ba4Q,1162
110
110
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
- phoenix/server/api/auth.py,sha256=6GgM0l_h7mbsP7WadE3PmPmqfeLw5TUnXDEfY3m8rkk,1600
111
+ phoenix/server/api/auth.py,sha256=AyYhnZIbY9ALVjg2K6aC2UXSa3Pva5GVDBXyaZ3nD3o,2749
112
112
  phoenix/server/api/context.py,sha256=mqsq_8Ru50e-PxKWNTzh9zptb1PFjYFUf58uW59UYL0,8996
113
- phoenix/server/api/exceptions.py,sha256=TA0JuY2YRnj35qGuMSQ8d0ToHum9gWm9W--3fSKHrX0,1171
113
+ phoenix/server/api/exceptions.py,sha256=E2W0x63CBzc0CoQPptrLr9nZxPF9zIP8MCJ3RuJMddw,1322
114
114
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
115
115
  phoenix/server/api/queries.py,sha256=fvbdyhJ57I6DPrNEBkWwhw8BRF7DyIZ3LtYwXwI7yVw,45651
116
116
  phoenix/server/api/schema.py,sha256=fcs36xQwFF_Qe41_5cWR8wYpDvOrnbcyTeo5WNMbDsA,1702
@@ -152,7 +152,7 @@ phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_project_sessi
152
152
  phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_span.py,sha256=6I0Fdu2RYRm2gN64PYO3-bCY_e8vQX-BdOgoFFDjLCE,1723
153
153
  phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_trace.py,sha256=pxgMRFGLQHqf9swuJf7c-n5ONaE7k8NCJaqROa-QK3o,2067
154
154
  phoenix/server/api/dataloaders/span_cost_details_by_span_cost.py,sha256=f8ztQACGfKuf-nigyXiBUnGxXVzhNG9tOsPujxvOoX4,974
155
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py,sha256=O3KVRgoAfuP32w2wmQdnXAa8GWnxm9wFJPqng9Ct2LQ,2577
155
+ phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py,sha256=EvivIoLsg1KxlRm4MM-LiInxFsRzqZpLarsvjyQk8e8,2453
156
156
  phoenix/server/api/dataloaders/span_cost_summary_by_experiment_run.py,sha256=IJFirEPeR1UGKTg6Gz3MJQGDigoPy6SaFnUdBibyVac,2539
157
157
  phoenix/server/api/dataloaders/span_cost_summary_by_generative_model.py,sha256=Fhp0NPsoSbTBgnE38hregVVeVHEhPw_QANiOfNyaTf8,2295
158
158
  phoenix/server/api/dataloaders/span_cost_summary_by_project.py,sha256=UjLkGvsVAY5-vltd08iR_NyWZ-_dWLc70yglMKSPPzY,5203
@@ -222,6 +222,7 @@ phoenix/server/api/input_types/PromptVersionInput.py,sha256=n6zBeSkK8ZFRHTjtVx4B
222
222
  phoenix/server/api/input_types/SpanAnnotationFilter.py,sha256=-djfIXYCxV6sV3GPOZQUV0SPfiWDhRlTORfeQ7tCBgQ,2671
223
223
  phoenix/server/api/input_types/SpanAnnotationSort.py,sha256=T5pAGzmh4MiJp9JMAzNDByFVTczfw02FH4WFWwFezyI,361
224
224
  phoenix/server/api/input_types/SpanSort.py,sha256=GReQx9yOo0Kehi2y4AtY69aZhRtcqvcg-9bSIFru69U,7540
225
+ phoenix/server/api/input_types/TimeBinConfig.py,sha256=s4p0SNTGRY6rbHfZhEMwmon5zX85j-DbN06GkapF6jg,516
225
226
  phoenix/server/api/input_types/TimeRange.py,sha256=pwhC2jx6dFIgT0qFfnO68qiJp9-m7Je5QkNscNsuVxA,700
226
227
  phoenix/server/api/input_types/TraceAnnotationSort.py,sha256=BzwiUnMh2VsgQYnhDlbJ6ljHugqIS4YDUlYzvq_tl3o,365
227
228
  phoenix/server/api/input_types/UserRoleInput.py,sha256=xxhFe0ITZOgRVEJbVem_W6F1Ip_H6xDENdQqMMx-kKE,129
@@ -249,7 +250,7 @@ phoenix/server/api/openapi/schema.py,sha256=WGmHWSIyJhtc5EIh_M3vlXU-EgHkFuTlyVof
249
250
  phoenix/server/api/routers/__init__.py,sha256=YIzHsIFOOXuCRbDkMUHx-McrANFJK5UfUn6a4BNIzmo,277
250
251
  phoenix/server/api/routers/auth.py,sha256=nwoum3HKlrSkNF0MWN91rrsTcWMVKefGI9xPz9s0DGw,11604
251
252
  phoenix/server/api/routers/embeddings.py,sha256=BpZGJee0pdL0W5Rp1L0b30dEtZTgJeVqXky8LgZ0ZXw,898
252
- phoenix/server/api/routers/oauth2.py,sha256=EUoBeh4Ix-Uwt_q_RD75xbMcdVdd0xJLDYjELdVHBTw,24051
253
+ phoenix/server/api/routers/oauth2.py,sha256=rwIqeJe4T5jK5NeuPAmrfNsbBqUZhMSFRNJdOJYyFPc,24445
253
254
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
254
255
  phoenix/server/api/routers/v1/__init__.py,sha256=ngLMPjC7lgZxgKy_Is33KxTRnMzSqy25qTTChCVx_Mo,2696
255
256
  phoenix/server/api/routers/v1/annotation_configs.py,sha256=xp5lJmKYlRsINCUrRD9-lTAElw2v4hdFndS5BWrxICA,16048
@@ -276,7 +277,7 @@ phoenix/server/api/types/AuthMethod.py,sha256=M25usT6FCNKi-QFMdtMqRjJVGfq1ACnVoS
276
277
  phoenix/server/api/types/ChatCompletionMessageRole.py,sha256=kmQOilOlVntlmqbaHkqXZuimfljfYaHc3kbo53uK8_0,227
277
278
  phoenix/server/api/types/ChatCompletionSubscriptionPayload.py,sha256=G0YsirJzDMFDqEK7I_FCkPmIugAof-8D9fNfFlHdhjY,1031
278
279
  phoenix/server/api/types/Cluster.py,sha256=duX0pSC7P3YT1IztduuJtPsRj1x6oOOLfmWf2Y-FdEk,5699
279
- phoenix/server/api/types/CostBreakdown.py,sha256=YeLfvHSK4Ik-ApEmBqtLR1ZjGGZcazFXdjXVudpP45A,328
280
+ phoenix/server/api/types/CostBreakdown.py,sha256=yw9dlb0blGIB_dWNP8yEvDHJztHjpiV6_CN7waC-ILY,292
280
281
  phoenix/server/api/types/CreateDatasetPayload.py,sha256=R-6zCmuD0f76RU9Giu78xwTHlASQs6Aq8yzvX1Kxc3g,140
281
282
  phoenix/server/api/types/CronExpression.py,sha256=R7oxuSSX_eTUHQWaoaSueQqWDmkkHr5dBKRN6q-6ROk,331
282
283
  phoenix/server/api/types/DataQualityMetric.py,sha256=Aieg3bHeBFaAf4mqeRcH1zT04sXAtQD8ATSHJt7FaBQ,1538
@@ -318,7 +319,7 @@ phoenix/server/api/types/ModelInterface.py,sha256=Qe7H23wDb_Q2-HmeY2t0R5Jsn4aAfY
318
319
  phoenix/server/api/types/NumericRange.py,sha256=afEjgF97Go_OvmjMggbPBt-zGM8IONewAyEiKEHRds0,192
319
320
  phoenix/server/api/types/PerformanceMetric.py,sha256=KFkmJDqP43eDUtARQOUqR7NYcxvL6Vh2uisHWU6H3ko,387
320
321
  phoenix/server/api/types/PlaygroundModel.py,sha256=IqJFxsAAJMRyaFI9ryI3GQrpFOJ5Llf6kIutEO-tFvM,321
321
- phoenix/server/api/types/Project.py,sha256=-Be5ZL0mBfCV2pTi6oKYt8napwQWiCnUjSGdoSfnSJU,31154
322
+ phoenix/server/api/types/Project.py,sha256=byVCH6gG1o0G5OZwa4M_MA76HmfpPZQAugFcoTr8SfQ,41592
322
323
  phoenix/server/api/types/ProjectSession.py,sha256=uwqTsDTfSGz13AvP-cwS_mJR5JZ1lHqu10ungbl7g5s,6245
323
324
  phoenix/server/api/types/ProjectTraceRetentionPolicy.py,sha256=tYy2kgalPDyuaYZr0VUHjH0YpXaiF_QOzg5yfaV_c7c,3782
324
325
  phoenix/server/api/types/Prompt.py,sha256=ccP4eq1e38xbF0afclGWLOuDpBVpNbJ3AOSRClF8yFQ,4955
@@ -355,7 +356,7 @@ phoenix/server/api/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
355
356
  phoenix/server/api/types/node.py,sha256=BLl_IOFr0zrqUxaAtGLGui5aeM5VNVXFTzGeAKrztr0,822
356
357
  phoenix/server/api/types/pagination.py,sha256=BXm46gXZfrBS4hpiLvVSEdsbb29ctUMVJYjKXlOLxUA,9064
357
358
  phoenix/server/cost_tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
358
- phoenix/server/cost_tracking/cost_details_calculator.py,sha256=ywWQO6foRZC0CmbNPGpa3DaPoWzuIp3ke9s4NqHLuUw,8736
359
+ phoenix/server/cost_tracking/cost_details_calculator.py,sha256=Tt0YcuLhgPuXKWJemWVmYQfG0xQUvH4VziIj6KcDnoA,8945
359
360
  phoenix/server/cost_tracking/cost_model_lookup.py,sha256=jhtVdnQBzrTUHeOGPWgOebk-Io5hpJ1vAgWOu8ojeJ4,6801
360
361
  phoenix/server/cost_tracking/helpers.py,sha256=Pk6ECjnYreTxrldtRwxnwFcxIPVsvDq_yAwDA_spkOc,2122
361
362
  phoenix/server/cost_tracking/model_cost_manifest.json,sha256=sMXOjssVwcJkmdMapA5-eYjXIboUu8FDosuXGL3q1Cw,53862
@@ -364,12 +365,12 @@ phoenix/server/cost_tracking/token_cost_calculator.py,sha256=2JEZnvusx2-xbhp8krp
364
365
  phoenix/server/daemons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
365
366
  phoenix/server/daemons/db_disk_usage_monitor.py,sha256=_MckKf9GbQ3Is4WZ3RmwJmvVElkF9Ipss3yNHjF8R3c,8016
366
367
  phoenix/server/daemons/generative_model_store.py,sha256=CkMG0jFWtxcUNP_7iFTgzHPyl5IgnXUwwJb7584pmkI,1566
367
- phoenix/server/daemons/span_cost_calculator.py,sha256=0BXMe9sHN8k5RjIQjCBzZdwvFuBHu8zDqAJc8rXbQA0,3199
368
+ phoenix/server/daemons/span_cost_calculator.py,sha256=M7bG8fas99xz37bZUkBXnG37U2otUcu1-FI5RmrtPgU,3052
368
369
  phoenix/server/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
369
- phoenix/server/email/sender.py,sha256=mTZUdf_M9GRiA2_MXmWO6hXvQHXbuRUyea-_YoTC5Mo,4974
370
+ phoenix/server/email/sender.py,sha256=qUurj76b9IKFEbYsKO7lB7Lxie3GOFrnhZ5ahBzFaCs,5048
370
371
  phoenix/server/email/types.py,sha256=f1XXk4OQJlLSCHweujrGlkZic7CtbUSnfdPXzqP9WRI,772
371
372
  phoenix/server/email/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
372
- phoenix/server/email/templates/db_disk_usage_notification.html,sha256=VsGW_yPXPgspwp_xhFYANoqDENaO3En36qSw26l4fh4,733
373
+ phoenix/server/email/templates/db_disk_usage_notification.html,sha256=Atv2XnKkC7HasTTCZYAWySr7VbupAGwbIiJ9eQ4r2l0,906
373
374
  phoenix/server/email/templates/password_reset.html,sha256=jv0Pe-06JloPZcubRWxPdAFHYEn9eDj_4SjmuoIwshI,441
374
375
  phoenix/server/email/templates/welcome.html,sha256=AyVsIOtpmyYwWmmkXjuEgXwkbsag4YHHKfkmOTiNo-M,316
375
376
  phoenix/server/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -385,16 +386,16 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
385
386
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
386
387
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
387
388
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
388
- phoenix/server/static/.vite/manifest.json,sha256=ds-n_Eo2eRbzplNS-h2Dzaj-_lbPCBYySCioOAnl4uA,2165
389
- phoenix/server/static/assets/components-J3qjrjBf.js,sha256=Za9a7UixoZeWyxhjTMt9iJ4nPnHB7qoQp1ADPYy_gXg,613734
390
- phoenix/server/static/assets/index-CEObsQf_.js,sha256=N77fIXIIiRijOKu6RNWX3dozZj0ewUASn1BC639Nyi0,61724
391
- phoenix/server/static/assets/pages-CW1UdBht.js,sha256=Y9ZOHX-y5Hpdwmy_EKZULiebIsK_KQAjVwmToCvHSW4,1148455
392
- phoenix/server/static/assets/vendor-BnPh9i9e.js,sha256=K_Ewiog0baSwYbnjeDO5efkeYEi7FRYTiRLmD4_oLaw,2738792
389
+ phoenix/server/static/.vite/manifest.json,sha256=TyIBjaXaDHUOqmU0l4oXKq2CPJP8mvLfjzljq2lvLSI,2165
390
+ phoenix/server/static/assets/components-5M9nebi4.js,sha256=q_bd0ZLoJNT4GRB6t3HlPFge6BE7_Zm0Ba1pPMszXho,617280
391
+ phoenix/server/static/assets/index-OU2WTnGN.js,sha256=Ffe2oGJzh4KIQBhqrGMSJmLB0IDtzQZqTzEHCfJ1n00,61725
392
+ phoenix/server/static/assets/pages-DF8rqxJ4.js,sha256=2SsqPLQAtFywkipQhDy-Vvp6Xyud-DDhHGb-85JYzSQ,1150693
393
+ phoenix/server/static/assets/vendor-Bl7CyFDw.js,sha256=RX9lDZywB-FfD-MNfY5KYxVVq5laI2AcmH9rhVuCsnE,2739824
393
394
  phoenix/server/static/assets/vendor-WIZid84E.css,sha256=spZD2r7XL5GfLO13ln-IuXfnjAref8l6g_n_AvxxOlI,5517
394
- phoenix/server/static/assets/vendor-arizeai-Cr9o_Iu_.js,sha256=kybhi48Pux2UUA1gfmDGLDkwRbWyLwwAIS_F7pUMOXE,176579
395
- phoenix/server/static/assets/vendor-codemirror-k3zCIjlN.js,sha256=zuUSept08wa_k0xCJ-XtMhLVyerrbRZKXkn5Qxtd8GQ,781264
396
- phoenix/server/static/assets/vendor-recharts-BdblEuGB.js,sha256=JjY_9IvRxXNz-fXWV-ddK4BzNOejSLq0ql_lOacO8Ec,282150
397
- phoenix/server/static/assets/vendor-shiki-DPtuv2M4.js,sha256=8JbN-Qx5EiaQw_FgHLDqVhDsJ7gDgCFJDQXrZ8imczY,8980312
395
+ phoenix/server/static/assets/vendor-arizeai-B_viEUUA.js,sha256=ir0WQ2dSxIEhK6wDi7A1FKPaG7Bmgze_V5S7RfBxFBM,167428
396
+ phoenix/server/static/assets/vendor-codemirror-vlcH1_iR.js,sha256=BFxUiyr9u03kekfUjR4JIVR1D8-VnZjUo8bjmAigUtw,781264
397
+ phoenix/server/static/assets/vendor-recharts-C9cQu72o.js,sha256=fm3quX1HWJ9sbIZhDrHsnZZpSg0LhsQtMV21NB5MPTM,282150
398
+ phoenix/server/static/assets/vendor-shiki-BsknB7bv.js,sha256=5X-FxGYbuoFMIfOtX9LD9Qju-2oT1smMbgYyoHprI7k,8980312
398
399
  phoenix/server/static/assets/vendor-three-C5WAXd5r.js,sha256=ELkg06u70N7h8oFmvqdoHyPuUf9VgGEWeT4LKFx4VWo,620975
399
400
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
400
401
  phoenix/server/templates/index.html,sha256=rTdJZOlbZmm1dMCrHWikAwcecZDxduPU5zCFd2h2cbs,6819
@@ -435,9 +436,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
435
436
  phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
436
437
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
437
438
  phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
438
- arize_phoenix-11.7.0.dist-info/METADATA,sha256=zbc4IVpy2eyDH5X8918cyrIj3FgCFtlXtyE_4nP_QIU,27804
439
- arize_phoenix-11.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
440
- arize_phoenix-11.7.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
441
- arize_phoenix-11.7.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
442
- arize_phoenix-11.7.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
443
- arize_phoenix-11.7.0.dist-info/RECORD,,
439
+ arize_phoenix-11.8.0.dist-info/METADATA,sha256=QEqV4jJ6yjmlUIiyR2119BCyHw-Hi-pH6wm6rUc5c8k,30850
440
+ arize_phoenix-11.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
441
+ arize_phoenix-11.8.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
442
+ arize_phoenix-11.8.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
443
+ arize_phoenix-11.8.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
444
+ arize_phoenix-11.8.0.dist-info/RECORD,,
phoenix/config.py CHANGED
@@ -246,7 +246,14 @@ The URL to use for redirecting to a management interface that may be hosting Pho
246
246
  the current user is within PHOENIX_ADMINS, a link will be added to the navigation menu to return to
247
247
  this URL.
248
248
  """
249
+ ENV_PHOENIX_SUPPORT_EMAIL = "PHOENIX_SUPPORT_EMAIL"
250
+ """
251
+ The support email address to display in error messages and notifications.
249
252
 
253
+ When set, this email will be included in error messages for insufficient storage
254
+ conditions and database usage notification emails, providing users with a direct
255
+ contact for assistance. If not set, error messages will not include contact information.
256
+ """
250
257
 
251
258
  # SMTP settings
252
259
  ENV_PHOENIX_SMTP_HOSTNAME = "PHOENIX_SMTP_HOSTNAME"
@@ -1599,6 +1606,31 @@ def get_env_management_url() -> Optional[str]:
1599
1606
  return getenv(ENV_PHOENIX_MANAGEMENT_URL)
1600
1607
 
1601
1608
 
1609
+ def get_env_support_email() -> Optional[str]:
1610
+ """
1611
+ Get the support email address from the PHOENIX_SUPPORT_EMAIL environment variable.
1612
+
1613
+ Returns:
1614
+ The support email address if set, None otherwise.
1615
+ """
1616
+ return getenv(ENV_PHOENIX_SUPPORT_EMAIL)
1617
+
1618
+
1619
+ def validate_env_support_email() -> None:
1620
+ """
1621
+ Validate the support email address configured in PHOENIX_SUPPORT_EMAIL.
1622
+
1623
+ Raises:
1624
+ ValueError: If the email address is invalid.
1625
+ """
1626
+ if not (email := get_env_support_email()):
1627
+ return
1628
+ try:
1629
+ validate_email(email, check_deliverability=False)
1630
+ except EmailNotValidError as e:
1631
+ raise ValueError(f"Invalid email in {ENV_PHOENIX_SUPPORT_EMAIL}: '{email}'") from e
1632
+
1633
+
1602
1634
  def verify_server_environment_variables() -> None:
1603
1635
  """Verify that the environment variables are set correctly. Raises an error otherwise."""
1604
1636
  get_env_root_url()
@@ -1607,6 +1639,7 @@ def verify_server_environment_variables() -> None:
1607
1639
  get_env_database_allocated_storage_capacity_gibibytes()
1608
1640
  get_env_database_usage_email_warning_threshold_percentage()
1609
1641
  get_env_database_usage_insertion_blocking_threshold_percentage()
1642
+ validate_env_support_email()
1610
1643
 
1611
1644
  # Notify users about deprecated environment variables if they are being used.
1612
1645
  if os.getenv("PHOENIX_ENABLE_WEBSOCKETS") is not None:
phoenix/datetime_utils.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from datetime import datetime, timedelta, timezone, tzinfo
2
- from typing import Any, Optional, cast
2
+ from typing import Any, Iterator, Literal, Optional, cast
3
3
 
4
4
  import pandas as pd
5
5
  import pytz
@@ -10,6 +10,7 @@ from pandas.core.dtypes.common import (
10
10
  is_numeric_dtype,
11
11
  is_object_dtype,
12
12
  )
13
+ from typing_extensions import assert_never
13
14
 
14
15
  _LOCAL_TIMEZONE = datetime.now(timezone.utc).astimezone().tzinfo
15
16
 
@@ -113,3 +114,113 @@ def is_timezone_aware(dt: datetime) -> bool:
113
114
  Returns True if the datetime is timezone-aware, False otherwise.
114
115
  """
115
116
  return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
117
+
118
+
119
+ def get_timestamp_range(
120
+ start_time: datetime,
121
+ end_time: datetime,
122
+ stride: Literal["minute", "hour", "day", "week", "month", "year"] = "minute",
123
+ utc_offset_minutes: int = 0,
124
+ ) -> Iterator[datetime]:
125
+ """
126
+ Generate a sequence of datetime objects at regular intervals between start and end times.
127
+
128
+ This function creates time intervals by rounding down the start time to the nearest
129
+ stride boundary in the specified timezone, then yielding timestamps at regular
130
+ intervals until reaching the end time. All returned timestamps are in UTC.
131
+
132
+ Args:
133
+ start_time: The starting datetime (inclusive after rounding down to stride boundary).
134
+ Must be timezone-aware.
135
+ end_time: The ending datetime (exclusive). Must be timezone-aware.
136
+ stride: The interval between generated timestamps. Options:
137
+ - "minute": Generate timestamps every minute
138
+ - "hour": Generate timestamps every hour
139
+ - "day": Generate timestamps every day at midnight
140
+ - "week": Generate timestamps every week at Monday midnight
141
+ - "month": Generate timestamps on the 1st of each month at midnight
142
+ - "year": Generate timestamps on January 1st of each year at midnight
143
+ utc_offset_minutes: Timezone offset in minutes from UTC. Used to determine
144
+ the correct stride boundaries in local time. Positive values
145
+ are east of UTC, negative values are west of UTC.
146
+
147
+ Returns:
148
+ Iterator of datetime objects in UTC timezone, spaced at the specified stride
149
+ interval. The first timestamp is rounded down to the nearest stride boundary
150
+ in the local timezone (considering utc_offset_minutes).
151
+
152
+ Examples:
153
+ >>> from datetime import datetime, timezone
154
+ >>> start = datetime(2024, 1, 1, 12, 30, 45, tzinfo=timezone.utc)
155
+ >>> end = datetime(2024, 1, 1, 12, 33, 0, tzinfo=timezone.utc)
156
+ >>> list(get_timestamp_range(start, end, "minute"))
157
+ [datetime(2024, 1, 1, 12, 30, tzinfo=timezone.utc),
158
+ datetime(2024, 1, 1, 12, 31, tzinfo=timezone.utc),
159
+ datetime(2024, 1, 1, 12, 32, tzinfo=timezone.utc)]
160
+
161
+ >>> # Week stride rounds down to Monday
162
+ >>> start = datetime(2024, 1, 10, 12, 0, tzinfo=timezone.utc) # Wednesday
163
+ >>> end = datetime(2024, 1, 22, 0, 0, tzinfo=timezone.utc)
164
+ >>> list(get_timestamp_range(start, end, "week"))
165
+ [datetime(2024, 1, 8, 0, 0, tzinfo=timezone.utc), # Monday
166
+ datetime(2024, 1, 15, 0, 0, tzinfo=timezone.utc)] # Next Monday
167
+
168
+ Note:
169
+ - If end_time <= start_time (after rounding), returns an empty iterator
170
+ - Week intervals always start on Monday (weekday 0)
171
+ - Month intervals handle variable month lengths correctly including leap years
172
+ - The function works in local timezone for stride calculations but returns UTC
173
+ """
174
+ if not is_timezone_aware(start_time) or not is_timezone_aware(end_time):
175
+ raise ValueError("start_time and end_time must be timezone-aware")
176
+
177
+ # Apply UTC offset to work in local timezone
178
+ offset_delta = timedelta(minutes=utc_offset_minutes)
179
+ local_start_time = start_time + offset_delta
180
+
181
+ # round down start_time to the nearest stride in local timezone
182
+ if stride == "minute":
183
+ t = local_start_time.replace(second=0, microsecond=0)
184
+ elif stride == "hour":
185
+ t = local_start_time.replace(minute=0, second=0, microsecond=0)
186
+ elif stride == "day":
187
+ t = local_start_time.replace(hour=0, minute=0, second=0, microsecond=0)
188
+ elif stride == "week":
189
+ # Round down to the beginning of the week (Monday)
190
+ days_since_monday = local_start_time.weekday()
191
+ t = local_start_time.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(
192
+ days=days_since_monday
193
+ )
194
+ elif stride == "month":
195
+ t = local_start_time.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
196
+ elif stride == "year":
197
+ t = local_start_time.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
198
+ else:
199
+ assert_never(stride)
200
+
201
+ # Convert back to UTC for comparisons and yielding
202
+ local_end_time = end_time + offset_delta
203
+
204
+ while t < local_end_time:
205
+ # Yield timestamp converted back to UTC
206
+ yield t - offset_delta
207
+ if stride == "minute":
208
+ t += timedelta(minutes=1)
209
+ elif stride == "hour":
210
+ t += timedelta(hours=1)
211
+ elif stride == "day":
212
+ t += timedelta(days=1)
213
+ elif stride == "week":
214
+ t += timedelta(weeks=1)
215
+ elif stride == "month":
216
+ next_month = t.month % 12 + 1
217
+ next_year = t.year + (t.month // 12)
218
+ t = t.replace(
219
+ year=next_year, month=next_month, day=1, hour=0, minute=0, second=0, microsecond=0
220
+ )
221
+ elif stride == "year":
222
+ t = t.replace(
223
+ year=t.year + 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
224
+ )
225
+ else:
226
+ assert_never(stride)
phoenix/db/helpers.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from collections.abc import Callable, Hashable, Iterable
2
+ from datetime import datetime
2
3
  from enum import Enum
3
- from typing import Any, Optional, TypeVar
4
+ from typing import Any, Literal, Optional, TypeVar, Union
4
5
 
6
+ import sqlalchemy as sa
5
7
  from openinference.semconv.trace import (
6
8
  OpenInferenceSpanKindValues,
7
9
  RerankerAttributes,
@@ -17,6 +19,7 @@ from sqlalchemy import (
17
19
  func,
18
20
  select,
19
21
  )
22
+ from sqlalchemy.orm import QueryableAttribute
20
23
  from typing_extensions import assert_never
21
24
 
22
25
  from phoenix.config import PLAYGROUND_PROJECT_NAME
@@ -173,3 +176,155 @@ def exclude_experiment_projects(
173
176
  models.Experiment.project_name != PLAYGROUND_PROJECT_NAME,
174
177
  ),
175
178
  ).where(models.Experiment.project_name.is_(None))
179
+
180
+
181
+ def date_trunc(
182
+ dialect: SupportedSQLDialect,
183
+ field: Literal["minute", "hour", "day", "week", "month", "year"],
184
+ source: Union[QueryableAttribute[datetime], sa.TextClause],
185
+ utc_offset_minutes: int = 0,
186
+ ) -> SQLColumnExpression[datetime]:
187
+ """
188
+ Truncate a datetime to the specified field with optional UTC offset adjustment.
189
+
190
+ This function provides a cross-dialect way to truncate datetime values to a specific
191
+ time unit (minute, hour, day, week, month, or year). It handles UTC offset conversion
192
+ by applying the offset before truncation and then converting back to UTC.
193
+
194
+ Args:
195
+ dialect: The SQL dialect to use (PostgreSQL or SQLite).
196
+ field: The time unit to truncate to. Valid values are:
197
+ - "minute": Truncate to the start of the minute (seconds set to 0)
198
+ - "hour": Truncate to the start of the hour (minutes and seconds set to 0)
199
+ - "day": Truncate to the start of the day (time set to 00:00:00)
200
+ - "week": Truncate to the start of the week (Monday at 00:00:00)
201
+ - "month": Truncate to the first day of the month (day set to 1, time to 00:00:00)
202
+ - "year": Truncate to the first day of the year (date set to Jan 1, time to 00:00:00)
203
+ source: The datetime column or expression to truncate.
204
+ utc_offset_minutes: UTC offset in minutes to apply before truncation.
205
+ Positive values represent time zones ahead of UTC (e.g., +60 for UTC+1).
206
+ Negative values represent time zones behind UTC (e.g., -300 for UTC-5).
207
+ Defaults to 0 (no offset).
208
+
209
+ Returns:
210
+ A SQL column expression representing the truncated datetime in UTC.
211
+
212
+ Note:
213
+ - For PostgreSQL, uses the native `date_trunc` function with timezone support.
214
+ - For SQLite, implements custom truncation logic using datetime functions.
215
+ - Week truncation starts on Monday (ISO 8601 standard).
216
+ - The result is always returned in UTC, regardless of the input offset.
217
+
218
+ Examples:
219
+ >>> # Truncate to hour with no offset
220
+ >>> date_trunc(SupportedSQLDialect.POSTGRESQL, "hour", Span.start_time)
221
+
222
+ >>> # Truncate to day with UTC-5 offset (Eastern Time)
223
+ >>> date_trunc(SupportedSQLDialect.SQLITE, "day", Span.start_time, -300)
224
+ """
225
+ if dialect is SupportedSQLDialect.POSTGRESQL:
226
+ # Note: the usage of the timezone parameter in the form of e.g. "+05:00"
227
+ # appears to be an undocumented feature of PostgreSQL's date_trunc function.
228
+ # Below is an example query and its output executed on PostgreSQL v12 and v17.
229
+ # SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 15:38:40-05'),
230
+ # date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00', '+05:00'),
231
+ # date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00', '-05:00');
232
+ # ┌────────────────────────┬────────────────────────┬────────────────────────┐
233
+ # │ date_trunc │ date_trunc │ date_trunc │
234
+ # ├────────────────────────┼────────────────────────┼────────────────────────┤
235
+ # │ 2001-02-16 00:00:00+00 │ 2001-02-16 05:00:00+00 │ 2001-02-16 19:00:00+00 │
236
+ # └────────────────────────┴────────────────────────┴────────────────────────┘
237
+ # (1 row)
238
+ sign = "-" if utc_offset_minutes >= 0 else "+"
239
+ timezone = f"{sign}{abs(utc_offset_minutes) // 60}:{abs(utc_offset_minutes) % 60:02d}"
240
+ return sa.func.date_trunc(field, source, timezone)
241
+ elif dialect is SupportedSQLDialect.SQLITE:
242
+ return _date_trunc_for_sqlite(field, source, utc_offset_minutes)
243
+ else:
244
+ assert_never(dialect)
245
+
246
+
247
+ def _date_trunc_for_sqlite(
248
+ field: Literal["minute", "hour", "day", "week", "month", "year"],
249
+ source: Union[QueryableAttribute[datetime], sa.TextClause],
250
+ utc_offset_minutes: int = 0,
251
+ ) -> SQLColumnExpression[datetime]:
252
+ """
253
+ SQLite-specific implementation of datetime truncation with UTC offset handling.
254
+
255
+ This private helper function implements date truncation for SQLite databases, which
256
+ lack a native date_trunc function. It uses SQLite's datetime and strftime functions
257
+ to achieve the same result as PostgreSQL's date_trunc function.
258
+
259
+ Args:
260
+ field: The time unit to truncate to. Valid values are:
261
+ - "minute": Truncate to the start of the minute (seconds set to 0)
262
+ - "hour": Truncate to the start of the hour (minutes and seconds set to 0)
263
+ - "day": Truncate to the start of the day (time set to 00:00:00)
264
+ - "week": Truncate to the start of the week (Monday at 00:00:00)
265
+ - "month": Truncate to the first day of the month (day set to 1, time to 00:00:00)
266
+ - "year": Truncate to the first day of the year (date set to Jan 1, time to 00:00:00)
267
+ source: The datetime column or expression to truncate.
268
+ utc_offset_minutes: UTC offset in minutes to apply before truncation.
269
+ Positive values represent time zones ahead of UTC (e.g., +60 for UTC+1).
270
+ Negative values represent time zones behind UTC (e.g., -300 for UTC-5).
271
+
272
+ Returns:
273
+ A SQL column expression representing the truncated datetime in UTC.
274
+
275
+ Implementation Details:
276
+ - Uses SQLite's strftime() function to format and extract date components
277
+ - Applies UTC offset before truncation using datetime(source, "N minutes")
278
+ - Converts result back to UTC by subtracting the offset
279
+ - Week truncation uses day-of-week calculations where:
280
+ * strftime('%w') returns 0=Sunday, 1=Monday, ..., 6=Saturday
281
+ * Truncates to Monday (start of week) using case-based day adjustments
282
+ - Month/year truncation reconstructs dates using extracted components
283
+
284
+ Raises:
285
+ ValueError: If the field parameter is not one of the supported values.
286
+
287
+ Note:
288
+ This is a private helper function intended only for use by the date_trunc function
289
+ when the dialect is SupportedSQLDialect.SQLITE.
290
+ """
291
+ # SQLite does not have a built-in date truncation function, so we use datetime functions
292
+ # First apply UTC offset, then truncate
293
+ offset_source = func.datetime(source, f"{utc_offset_minutes} minutes")
294
+
295
+ if field == "minute":
296
+ t = func.datetime(func.strftime("%Y-%m-%d %H:%M:00", offset_source))
297
+ elif field == "hour":
298
+ t = func.datetime(func.strftime("%Y-%m-%d %H:00:00", offset_source))
299
+ elif field == "day":
300
+ t = func.datetime(func.strftime("%Y-%m-%d 00:00:00", offset_source))
301
+ elif field == "week":
302
+ # Truncate to Monday (start of week)
303
+ # SQLite strftime('%w') returns: 0=Sunday, 1=Monday, ..., 6=Saturday
304
+ dow = func.strftime("%w", offset_source)
305
+ t = func.datetime(
306
+ case(
307
+ (dow == "0", func.date(offset_source, "-6 days")), # Sunday -> go back 6 days
308
+ (dow == "1", func.date(offset_source, "+0 days")), # Monday -> stay
309
+ (dow == "2", func.date(offset_source, "-1 days")), # Tuesday -> go back 1 day
310
+ (dow == "3", func.date(offset_source, "-2 days")), # Wednesday -> go back 2 days
311
+ (dow == "4", func.date(offset_source, "-3 days")), # Thursday -> go back 3 days
312
+ (dow == "5", func.date(offset_source, "-4 days")), # Friday -> go back 4 days
313
+ (dow == "6", func.date(offset_source, "-5 days")), # Saturday -> go back 5 days
314
+ ),
315
+ "00:00:00",
316
+ )
317
+ elif field == "month":
318
+ # Extract year and month, then construct first day of month
319
+ year = func.strftime("%Y", offset_source)
320
+ month = func.strftime("%m", offset_source)
321
+ t = func.datetime(year + "-" + month + "-01 00:00:00")
322
+ elif field == "year":
323
+ # Extract year, then construct first day of year
324
+ year = func.strftime("%Y", offset_source)
325
+ t = func.datetime(year + "-01-01 00:00:00")
326
+ else:
327
+ raise ValueError(f"Unsupported field for date truncation: {field}")
328
+
329
+ # Convert back to UTC by subtracting the offset
330
+ return func.datetime(t, f"{-utc_offset_minutes} minutes")
@@ -3,8 +3,10 @@ from typing import Any
3
3
 
4
4
  from strawberry import Info
5
5
  from strawberry.permission import BasePermission
6
+ from typing_extensions import override
6
7
 
7
- from phoenix.server.api.exceptions import Unauthorized
8
+ from phoenix.config import get_env_support_email
9
+ from phoenix.server.api.exceptions import InsufficientStorage, Unauthorized
8
10
  from phoenix.server.bearer_auth import PhoenixUser
9
11
 
10
12
 
@@ -20,15 +22,35 @@ class IsNotReadOnly(Authorization):
20
22
  return not info.context.read_only
21
23
 
22
24
 
23
- class IsLocked(Authorization):
24
- """
25
- Disables mutations and subscriptions that create or update data but allows
26
- queries and delete mutations.
25
+ class IsLocked(BasePermission):
27
26
  """
27
+ Permission class that restricts data-modifying operations when insufficient storage.
28
+
29
+ When database storage capacity is exceeded, this permission blocks mutations and
30
+ subscriptions that create or update data, while allowing queries and delete mutations
31
+ to continue. This prevents database overflow while maintaining read access and the
32
+ ability to free up space through deletions.
28
33
 
29
- message = "Operations that write or modify data are locked"
34
+ Raises:
35
+ InsufficientStorage: When storage capacity is exceeded and data operations
36
+ are temporarily disabled. The error includes guidance for resolution
37
+ and support contact information if configured.
38
+ """
30
39
 
40
+ @override
41
+ def on_unauthorized(self) -> None:
42
+ """Create user-friendly error message when storage operations are blocked."""
43
+ message = (
44
+ "Database operations are disabled due to insufficient storage. "
45
+ "Please delete old data or increase storage."
46
+ )
47
+ if support_email := get_env_support_email():
48
+ message += f" Need help? Contact us at {support_email}"
49
+ raise InsufficientStorage(message)
50
+
51
+ @override
31
52
  def has_permission(self, source: Any, info: Info, **kwargs: Any) -> bool:
53
+ """Check if database operations are allowed based on storage capacity and lock status."""
32
54
  return not (info.context.db.should_not_insert_or_update or info.context.locked)
33
55
 
34
56