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.
- {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/METADATA +14 -2
- {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/RECORD +32 -31
- phoenix/config.py +33 -0
- phoenix/datetime_utils.py +112 -1
- phoenix/db/helpers.py +156 -1
- phoenix/server/api/auth.py +28 -6
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +6 -7
- phoenix/server/api/exceptions.py +6 -0
- phoenix/server/api/input_types/TimeBinConfig.py +23 -0
- phoenix/server/api/routers/oauth2.py +19 -2
- phoenix/server/api/types/CostBreakdown.py +4 -7
- phoenix/server/api/types/Project.py +341 -73
- phoenix/server/app.py +7 -3
- phoenix/server/authorization.py +27 -2
- phoenix/server/cost_tracking/cost_details_calculator.py +22 -16
- phoenix/server/daemons/span_cost_calculator.py +2 -8
- phoenix/server/email/sender.py +2 -1
- phoenix/server/email/templates/db_disk_usage_notification.html +3 -0
- phoenix/server/static/.vite/manifest.json +36 -36
- phoenix/server/static/assets/{components-J3qjrjBf.js → components-5M9nebi4.js} +344 -263
- phoenix/server/static/assets/{index-CEObsQf_.js → index-OU2WTnGN.js} +11 -11
- phoenix/server/static/assets/{pages-CW1UdBht.js → pages-DF8rqxJ4.js} +451 -444
- phoenix/server/static/assets/{vendor-BnPh9i9e.js → vendor-Bl7CyFDw.js} +147 -147
- phoenix/server/static/assets/{vendor-arizeai-Cr9o_Iu_.js → vendor-arizeai-B_viEUUA.js} +18 -480
- phoenix/server/static/assets/{vendor-codemirror-k3zCIjlN.js → vendor-codemirror-vlcH1_iR.js} +1 -1
- phoenix/server/static/assets/{vendor-recharts-BdblEuGB.js → vendor-recharts-C9cQu72o.js} +25 -25
- phoenix/server/static/assets/{vendor-shiki-DPtuv2M4.js → vendor-shiki-BsknB7bv.js} +1 -1
- phoenix/version.py +1 -1
- {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-11.7.0.dist-info → arize_phoenix-11.8.0.dist-info}/licenses/IP_NOTICE +0 -0
- {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.
|
|
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 [](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 [](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 [](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 [](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 [](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` | [](https://www.npmjs.com/package/@arizeai/openinference-instrumentation-beeai) |
|
|
249
250
|
| [Mastra](https://arize.com/docs/phoenix/integrations/mastra) | `@arizeai/openinference-mastra` | [](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` | [](https://central.sonatype.com/artifact/com.arize/openinference-instrumentation-langchain4j) |
|
|
257
|
+
|
|
251
258
|
### Platforms
|
|
252
259
|
|
|
253
|
-
|
|
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=
|
|
4
|
-
phoenix/datetime_utils.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
95
|
-
phoenix/server/authorization.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
389
|
-
phoenix/server/static/assets/components-
|
|
390
|
-
phoenix/server/static/assets/index-
|
|
391
|
-
phoenix/server/static/assets/pages-
|
|
392
|
-
phoenix/server/static/assets/vendor-
|
|
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-
|
|
395
|
-
phoenix/server/static/assets/vendor-codemirror-
|
|
396
|
-
phoenix/server/static/assets/vendor-recharts-
|
|
397
|
-
phoenix/server/static/assets/vendor-shiki-
|
|
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.
|
|
439
|
-
arize_phoenix-11.
|
|
440
|
-
arize_phoenix-11.
|
|
441
|
-
arize_phoenix-11.
|
|
442
|
-
arize_phoenix-11.
|
|
443
|
-
arize_phoenix-11.
|
|
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")
|
phoenix/server/api/auth.py
CHANGED
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
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
|
|