arize-phoenix 3.16.1__py3-none-any.whl → 7.7.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 (338) hide show
  1. arize_phoenix-7.7.0.dist-info/METADATA +261 -0
  2. arize_phoenix-7.7.0.dist-info/RECORD +345 -0
  3. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/WHEEL +1 -1
  4. arize_phoenix-7.7.0.dist-info/entry_points.txt +3 -0
  5. phoenix/__init__.py +86 -14
  6. phoenix/auth.py +309 -0
  7. phoenix/config.py +675 -45
  8. phoenix/core/model.py +32 -30
  9. phoenix/core/model_schema.py +102 -109
  10. phoenix/core/model_schema_adapter.py +48 -45
  11. phoenix/datetime_utils.py +24 -3
  12. phoenix/db/README.md +54 -0
  13. phoenix/db/__init__.py +4 -0
  14. phoenix/db/alembic.ini +85 -0
  15. phoenix/db/bulk_inserter.py +294 -0
  16. phoenix/db/engines.py +208 -0
  17. phoenix/db/enums.py +20 -0
  18. phoenix/db/facilitator.py +113 -0
  19. phoenix/db/helpers.py +159 -0
  20. phoenix/db/insertion/constants.py +2 -0
  21. phoenix/db/insertion/dataset.py +227 -0
  22. phoenix/db/insertion/document_annotation.py +171 -0
  23. phoenix/db/insertion/evaluation.py +191 -0
  24. phoenix/db/insertion/helpers.py +98 -0
  25. phoenix/db/insertion/span.py +193 -0
  26. phoenix/db/insertion/span_annotation.py +158 -0
  27. phoenix/db/insertion/trace_annotation.py +158 -0
  28. phoenix/db/insertion/types.py +256 -0
  29. phoenix/db/migrate.py +86 -0
  30. phoenix/db/migrations/data_migration_scripts/populate_project_sessions.py +199 -0
  31. phoenix/db/migrations/env.py +114 -0
  32. phoenix/db/migrations/script.py.mako +26 -0
  33. phoenix/db/migrations/versions/10460e46d750_datasets.py +317 -0
  34. phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py +126 -0
  35. phoenix/db/migrations/versions/4ded9e43755f_create_project_sessions_table.py +66 -0
  36. phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
  37. phoenix/db/migrations/versions/cf03bd6bae1d_init.py +280 -0
  38. phoenix/db/models.py +807 -0
  39. phoenix/exceptions.py +5 -1
  40. phoenix/experiments/__init__.py +6 -0
  41. phoenix/experiments/evaluators/__init__.py +29 -0
  42. phoenix/experiments/evaluators/base.py +158 -0
  43. phoenix/experiments/evaluators/code_evaluators.py +184 -0
  44. phoenix/experiments/evaluators/llm_evaluators.py +473 -0
  45. phoenix/experiments/evaluators/utils.py +236 -0
  46. phoenix/experiments/functions.py +772 -0
  47. phoenix/experiments/tracing.py +86 -0
  48. phoenix/experiments/types.py +726 -0
  49. phoenix/experiments/utils.py +25 -0
  50. phoenix/inferences/__init__.py +0 -0
  51. phoenix/{datasets → inferences}/errors.py +6 -5
  52. phoenix/{datasets → inferences}/fixtures.py +49 -42
  53. phoenix/{datasets/dataset.py → inferences/inferences.py} +121 -105
  54. phoenix/{datasets → inferences}/schema.py +11 -11
  55. phoenix/{datasets → inferences}/validation.py +13 -14
  56. phoenix/logging/__init__.py +3 -0
  57. phoenix/logging/_config.py +90 -0
  58. phoenix/logging/_filter.py +6 -0
  59. phoenix/logging/_formatter.py +69 -0
  60. phoenix/metrics/__init__.py +5 -4
  61. phoenix/metrics/binning.py +4 -3
  62. phoenix/metrics/metrics.py +2 -1
  63. phoenix/metrics/mixins.py +7 -6
  64. phoenix/metrics/retrieval_metrics.py +2 -1
  65. phoenix/metrics/timeseries.py +5 -4
  66. phoenix/metrics/wrappers.py +9 -3
  67. phoenix/pointcloud/clustering.py +5 -5
  68. phoenix/pointcloud/pointcloud.py +7 -5
  69. phoenix/pointcloud/projectors.py +5 -6
  70. phoenix/pointcloud/umap_parameters.py +53 -52
  71. phoenix/server/api/README.md +28 -0
  72. phoenix/server/api/auth.py +44 -0
  73. phoenix/server/api/context.py +152 -9
  74. phoenix/server/api/dataloaders/__init__.py +91 -0
  75. phoenix/server/api/dataloaders/annotation_summaries.py +139 -0
  76. phoenix/server/api/dataloaders/average_experiment_run_latency.py +54 -0
  77. phoenix/server/api/dataloaders/cache/__init__.py +3 -0
  78. phoenix/server/api/dataloaders/cache/two_tier_cache.py +68 -0
  79. phoenix/server/api/dataloaders/dataset_example_revisions.py +131 -0
  80. phoenix/server/api/dataloaders/dataset_example_spans.py +38 -0
  81. phoenix/server/api/dataloaders/document_evaluation_summaries.py +144 -0
  82. phoenix/server/api/dataloaders/document_evaluations.py +31 -0
  83. phoenix/server/api/dataloaders/document_retrieval_metrics.py +89 -0
  84. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +79 -0
  85. phoenix/server/api/dataloaders/experiment_error_rates.py +58 -0
  86. phoenix/server/api/dataloaders/experiment_run_annotations.py +36 -0
  87. phoenix/server/api/dataloaders/experiment_run_counts.py +49 -0
  88. phoenix/server/api/dataloaders/experiment_sequence_number.py +44 -0
  89. phoenix/server/api/dataloaders/latency_ms_quantile.py +188 -0
  90. phoenix/server/api/dataloaders/min_start_or_max_end_times.py +85 -0
  91. phoenix/server/api/dataloaders/project_by_name.py +31 -0
  92. phoenix/server/api/dataloaders/record_counts.py +116 -0
  93. phoenix/server/api/dataloaders/session_io.py +79 -0
  94. phoenix/server/api/dataloaders/session_num_traces.py +30 -0
  95. phoenix/server/api/dataloaders/session_num_traces_with_error.py +32 -0
  96. phoenix/server/api/dataloaders/session_token_usages.py +41 -0
  97. phoenix/server/api/dataloaders/session_trace_latency_ms_quantile.py +55 -0
  98. phoenix/server/api/dataloaders/span_annotations.py +26 -0
  99. phoenix/server/api/dataloaders/span_dataset_examples.py +31 -0
  100. phoenix/server/api/dataloaders/span_descendants.py +57 -0
  101. phoenix/server/api/dataloaders/span_projects.py +33 -0
  102. phoenix/server/api/dataloaders/token_counts.py +124 -0
  103. phoenix/server/api/dataloaders/trace_by_trace_ids.py +25 -0
  104. phoenix/server/api/dataloaders/trace_root_spans.py +32 -0
  105. phoenix/server/api/dataloaders/user_roles.py +30 -0
  106. phoenix/server/api/dataloaders/users.py +33 -0
  107. phoenix/server/api/exceptions.py +48 -0
  108. phoenix/server/api/helpers/__init__.py +12 -0
  109. phoenix/server/api/helpers/dataset_helpers.py +217 -0
  110. phoenix/server/api/helpers/experiment_run_filters.py +763 -0
  111. phoenix/server/api/helpers/playground_clients.py +948 -0
  112. phoenix/server/api/helpers/playground_registry.py +70 -0
  113. phoenix/server/api/helpers/playground_spans.py +455 -0
  114. phoenix/server/api/input_types/AddExamplesToDatasetInput.py +16 -0
  115. phoenix/server/api/input_types/AddSpansToDatasetInput.py +14 -0
  116. phoenix/server/api/input_types/ChatCompletionInput.py +38 -0
  117. phoenix/server/api/input_types/ChatCompletionMessageInput.py +24 -0
  118. phoenix/server/api/input_types/ClearProjectInput.py +15 -0
  119. phoenix/server/api/input_types/ClusterInput.py +2 -2
  120. phoenix/server/api/input_types/CreateDatasetInput.py +12 -0
  121. phoenix/server/api/input_types/CreateSpanAnnotationInput.py +18 -0
  122. phoenix/server/api/input_types/CreateTraceAnnotationInput.py +18 -0
  123. phoenix/server/api/input_types/DataQualityMetricInput.py +5 -2
  124. phoenix/server/api/input_types/DatasetExampleInput.py +14 -0
  125. phoenix/server/api/input_types/DatasetSort.py +17 -0
  126. phoenix/server/api/input_types/DatasetVersionSort.py +16 -0
  127. phoenix/server/api/input_types/DeleteAnnotationsInput.py +7 -0
  128. phoenix/server/api/input_types/DeleteDatasetExamplesInput.py +13 -0
  129. phoenix/server/api/input_types/DeleteDatasetInput.py +7 -0
  130. phoenix/server/api/input_types/DeleteExperimentsInput.py +7 -0
  131. phoenix/server/api/input_types/DimensionFilter.py +4 -4
  132. phoenix/server/api/input_types/GenerativeModelInput.py +17 -0
  133. phoenix/server/api/input_types/Granularity.py +1 -1
  134. phoenix/server/api/input_types/InvocationParameters.py +162 -0
  135. phoenix/server/api/input_types/PatchAnnotationInput.py +19 -0
  136. phoenix/server/api/input_types/PatchDatasetExamplesInput.py +35 -0
  137. phoenix/server/api/input_types/PatchDatasetInput.py +14 -0
  138. phoenix/server/api/input_types/PerformanceMetricInput.py +5 -2
  139. phoenix/server/api/input_types/ProjectSessionSort.py +29 -0
  140. phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
  141. phoenix/server/api/input_types/SpanSort.py +134 -69
  142. phoenix/server/api/input_types/TemplateOptions.py +10 -0
  143. phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
  144. phoenix/server/api/input_types/UserRoleInput.py +9 -0
  145. phoenix/server/api/mutations/__init__.py +28 -0
  146. phoenix/server/api/mutations/api_key_mutations.py +167 -0
  147. phoenix/server/api/mutations/chat_mutations.py +593 -0
  148. phoenix/server/api/mutations/dataset_mutations.py +591 -0
  149. phoenix/server/api/mutations/experiment_mutations.py +75 -0
  150. phoenix/server/api/{types/ExportEventsMutation.py → mutations/export_events_mutations.py} +21 -18
  151. phoenix/server/api/mutations/project_mutations.py +57 -0
  152. phoenix/server/api/mutations/span_annotations_mutations.py +128 -0
  153. phoenix/server/api/mutations/trace_annotations_mutations.py +127 -0
  154. phoenix/server/api/mutations/user_mutations.py +329 -0
  155. phoenix/server/api/openapi/__init__.py +0 -0
  156. phoenix/server/api/openapi/main.py +17 -0
  157. phoenix/server/api/openapi/schema.py +16 -0
  158. phoenix/server/api/queries.py +738 -0
  159. phoenix/server/api/routers/__init__.py +11 -0
  160. phoenix/server/api/routers/auth.py +284 -0
  161. phoenix/server/api/routers/embeddings.py +26 -0
  162. phoenix/server/api/routers/oauth2.py +488 -0
  163. phoenix/server/api/routers/v1/__init__.py +64 -0
  164. phoenix/server/api/routers/v1/datasets.py +1017 -0
  165. phoenix/server/api/routers/v1/evaluations.py +362 -0
  166. phoenix/server/api/routers/v1/experiment_evaluations.py +115 -0
  167. phoenix/server/api/routers/v1/experiment_runs.py +167 -0
  168. phoenix/server/api/routers/v1/experiments.py +308 -0
  169. phoenix/server/api/routers/v1/pydantic_compat.py +78 -0
  170. phoenix/server/api/routers/v1/spans.py +267 -0
  171. phoenix/server/api/routers/v1/traces.py +208 -0
  172. phoenix/server/api/routers/v1/utils.py +95 -0
  173. phoenix/server/api/schema.py +44 -241
  174. phoenix/server/api/subscriptions.py +597 -0
  175. phoenix/server/api/types/Annotation.py +21 -0
  176. phoenix/server/api/types/AnnotationSummary.py +55 -0
  177. phoenix/server/api/types/AnnotatorKind.py +16 -0
  178. phoenix/server/api/types/ApiKey.py +27 -0
  179. phoenix/server/api/types/AuthMethod.py +9 -0
  180. phoenix/server/api/types/ChatCompletionMessageRole.py +11 -0
  181. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +46 -0
  182. phoenix/server/api/types/Cluster.py +25 -24
  183. phoenix/server/api/types/CreateDatasetPayload.py +8 -0
  184. phoenix/server/api/types/DataQualityMetric.py +31 -13
  185. phoenix/server/api/types/Dataset.py +288 -63
  186. phoenix/server/api/types/DatasetExample.py +85 -0
  187. phoenix/server/api/types/DatasetExampleRevision.py +34 -0
  188. phoenix/server/api/types/DatasetVersion.py +14 -0
  189. phoenix/server/api/types/Dimension.py +32 -31
  190. phoenix/server/api/types/DocumentEvaluationSummary.py +9 -8
  191. phoenix/server/api/types/EmbeddingDimension.py +56 -49
  192. phoenix/server/api/types/Evaluation.py +25 -31
  193. phoenix/server/api/types/EvaluationSummary.py +30 -50
  194. phoenix/server/api/types/Event.py +20 -20
  195. phoenix/server/api/types/ExampleRevisionInterface.py +14 -0
  196. phoenix/server/api/types/Experiment.py +152 -0
  197. phoenix/server/api/types/ExperimentAnnotationSummary.py +13 -0
  198. phoenix/server/api/types/ExperimentComparison.py +17 -0
  199. phoenix/server/api/types/ExperimentRun.py +119 -0
  200. phoenix/server/api/types/ExperimentRunAnnotation.py +56 -0
  201. phoenix/server/api/types/GenerativeModel.py +9 -0
  202. phoenix/server/api/types/GenerativeProvider.py +85 -0
  203. phoenix/server/api/types/Inferences.py +80 -0
  204. phoenix/server/api/types/InferencesRole.py +23 -0
  205. phoenix/server/api/types/LabelFraction.py +7 -0
  206. phoenix/server/api/types/MimeType.py +2 -2
  207. phoenix/server/api/types/Model.py +54 -54
  208. phoenix/server/api/types/PerformanceMetric.py +8 -5
  209. phoenix/server/api/types/Project.py +407 -142
  210. phoenix/server/api/types/ProjectSession.py +139 -0
  211. phoenix/server/api/types/Segments.py +4 -4
  212. phoenix/server/api/types/Span.py +221 -176
  213. phoenix/server/api/types/SpanAnnotation.py +43 -0
  214. phoenix/server/api/types/SpanIOValue.py +15 -0
  215. phoenix/server/api/types/SystemApiKey.py +9 -0
  216. phoenix/server/api/types/TemplateLanguage.py +10 -0
  217. phoenix/server/api/types/TimeSeries.py +19 -15
  218. phoenix/server/api/types/TokenUsage.py +11 -0
  219. phoenix/server/api/types/Trace.py +154 -0
  220. phoenix/server/api/types/TraceAnnotation.py +45 -0
  221. phoenix/server/api/types/UMAPPoints.py +7 -7
  222. phoenix/server/api/types/User.py +60 -0
  223. phoenix/server/api/types/UserApiKey.py +45 -0
  224. phoenix/server/api/types/UserRole.py +15 -0
  225. phoenix/server/api/types/node.py +4 -112
  226. phoenix/server/api/types/pagination.py +156 -57
  227. phoenix/server/api/utils.py +34 -0
  228. phoenix/server/app.py +864 -115
  229. phoenix/server/bearer_auth.py +163 -0
  230. phoenix/server/dml_event.py +136 -0
  231. phoenix/server/dml_event_handler.py +256 -0
  232. phoenix/server/email/__init__.py +0 -0
  233. phoenix/server/email/sender.py +97 -0
  234. phoenix/server/email/templates/__init__.py +0 -0
  235. phoenix/server/email/templates/password_reset.html +19 -0
  236. phoenix/server/email/types.py +11 -0
  237. phoenix/server/grpc_server.py +102 -0
  238. phoenix/server/jwt_store.py +505 -0
  239. phoenix/server/main.py +305 -116
  240. phoenix/server/oauth2.py +52 -0
  241. phoenix/server/openapi/__init__.py +0 -0
  242. phoenix/server/prometheus.py +111 -0
  243. phoenix/server/rate_limiters.py +188 -0
  244. phoenix/server/static/.vite/manifest.json +87 -0
  245. phoenix/server/static/assets/components-Cy9nwIvF.js +2125 -0
  246. phoenix/server/static/assets/index-BKvHIxkk.js +113 -0
  247. phoenix/server/static/assets/pages-CUi2xCVQ.js +4449 -0
  248. phoenix/server/static/assets/vendor-DvC8cT4X.js +894 -0
  249. phoenix/server/static/assets/vendor-DxkFTwjz.css +1 -0
  250. phoenix/server/static/assets/vendor-arizeai-Do1793cv.js +662 -0
  251. phoenix/server/static/assets/vendor-codemirror-BzwZPyJM.js +24 -0
  252. phoenix/server/static/assets/vendor-recharts-_Jb7JjhG.js +59 -0
  253. phoenix/server/static/assets/vendor-shiki-Cl9QBraO.js +5 -0
  254. phoenix/server/static/assets/vendor-three-DwGkEfCM.js +2998 -0
  255. phoenix/server/telemetry.py +68 -0
  256. phoenix/server/templates/index.html +82 -23
  257. phoenix/server/thread_server.py +3 -3
  258. phoenix/server/types.py +275 -0
  259. phoenix/services.py +27 -18
  260. phoenix/session/client.py +743 -68
  261. phoenix/session/data_extractor.py +31 -7
  262. phoenix/session/evaluation.py +3 -9
  263. phoenix/session/session.py +263 -219
  264. phoenix/settings.py +22 -0
  265. phoenix/trace/__init__.py +2 -22
  266. phoenix/trace/attributes.py +338 -0
  267. phoenix/trace/dsl/README.md +116 -0
  268. phoenix/trace/dsl/filter.py +663 -213
  269. phoenix/trace/dsl/helpers.py +73 -21
  270. phoenix/trace/dsl/query.py +574 -201
  271. phoenix/trace/exporter.py +24 -19
  272. phoenix/trace/fixtures.py +368 -32
  273. phoenix/trace/otel.py +71 -219
  274. phoenix/trace/projects.py +3 -2
  275. phoenix/trace/schemas.py +33 -11
  276. phoenix/trace/span_evaluations.py +21 -16
  277. phoenix/trace/span_json_decoder.py +6 -4
  278. phoenix/trace/span_json_encoder.py +2 -2
  279. phoenix/trace/trace_dataset.py +47 -32
  280. phoenix/trace/utils.py +21 -4
  281. phoenix/utilities/__init__.py +0 -26
  282. phoenix/utilities/client.py +132 -0
  283. phoenix/utilities/deprecation.py +31 -0
  284. phoenix/utilities/error_handling.py +3 -2
  285. phoenix/utilities/json.py +109 -0
  286. phoenix/utilities/logging.py +8 -0
  287. phoenix/utilities/project.py +2 -2
  288. phoenix/utilities/re.py +49 -0
  289. phoenix/utilities/span_store.py +0 -23
  290. phoenix/utilities/template_formatters.py +99 -0
  291. phoenix/version.py +1 -1
  292. arize_phoenix-3.16.1.dist-info/METADATA +0 -495
  293. arize_phoenix-3.16.1.dist-info/RECORD +0 -178
  294. phoenix/core/project.py +0 -619
  295. phoenix/core/traces.py +0 -96
  296. phoenix/experimental/evals/__init__.py +0 -73
  297. phoenix/experimental/evals/evaluators.py +0 -413
  298. phoenix/experimental/evals/functions/__init__.py +0 -4
  299. phoenix/experimental/evals/functions/classify.py +0 -453
  300. phoenix/experimental/evals/functions/executor.py +0 -353
  301. phoenix/experimental/evals/functions/generate.py +0 -138
  302. phoenix/experimental/evals/functions/processing.py +0 -76
  303. phoenix/experimental/evals/models/__init__.py +0 -14
  304. phoenix/experimental/evals/models/anthropic.py +0 -175
  305. phoenix/experimental/evals/models/base.py +0 -170
  306. phoenix/experimental/evals/models/bedrock.py +0 -221
  307. phoenix/experimental/evals/models/litellm.py +0 -134
  308. phoenix/experimental/evals/models/openai.py +0 -448
  309. phoenix/experimental/evals/models/rate_limiters.py +0 -246
  310. phoenix/experimental/evals/models/vertex.py +0 -173
  311. phoenix/experimental/evals/models/vertexai.py +0 -186
  312. phoenix/experimental/evals/retrievals.py +0 -96
  313. phoenix/experimental/evals/templates/__init__.py +0 -50
  314. phoenix/experimental/evals/templates/default_templates.py +0 -472
  315. phoenix/experimental/evals/templates/template.py +0 -195
  316. phoenix/experimental/evals/utils/__init__.py +0 -172
  317. phoenix/experimental/evals/utils/threads.py +0 -27
  318. phoenix/server/api/helpers.py +0 -11
  319. phoenix/server/api/routers/evaluation_handler.py +0 -109
  320. phoenix/server/api/routers/span_handler.py +0 -70
  321. phoenix/server/api/routers/trace_handler.py +0 -60
  322. phoenix/server/api/types/DatasetRole.py +0 -23
  323. phoenix/server/static/index.css +0 -6
  324. phoenix/server/static/index.js +0 -7447
  325. phoenix/storage/span_store/__init__.py +0 -23
  326. phoenix/storage/span_store/text_file.py +0 -85
  327. phoenix/trace/dsl/missing.py +0 -60
  328. phoenix/trace/langchain/__init__.py +0 -3
  329. phoenix/trace/langchain/instrumentor.py +0 -35
  330. phoenix/trace/llama_index/__init__.py +0 -3
  331. phoenix/trace/llama_index/callback.py +0 -102
  332. phoenix/trace/openai/__init__.py +0 -3
  333. phoenix/trace/openai/instrumentor.py +0 -30
  334. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/IP_NOTICE +0 -0
  335. {arize_phoenix-3.16.1.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/LICENSE +0 -0
  336. /phoenix/{datasets → db/insertion}/__init__.py +0 -0
  337. /phoenix/{experimental → db/migrations}/__init__.py +0 -0
  338. /phoenix/{storage → db/migrations/data_migration_scripts}/__init__.py +0 -0
@@ -0,0 +1,329 @@
1
+ import secrets
2
+ from contextlib import AsyncExitStack
3
+ from datetime import datetime, timezone
4
+ from typing import Literal, Optional
5
+
6
+ import strawberry
7
+ from sqlalchemy import Boolean, Select, and_, case, cast, delete, distinct, func, select
8
+ from sqlalchemy.orm import joinedload
9
+ from sqlean.dbapi2 import IntegrityError # type: ignore[import-untyped]
10
+ from strawberry import UNSET
11
+ from strawberry.relay import GlobalID
12
+ from strawberry.types import Info
13
+
14
+ from phoenix.auth import (
15
+ DEFAULT_ADMIN_EMAIL,
16
+ DEFAULT_ADMIN_USERNAME,
17
+ DEFAULT_SECRET_LENGTH,
18
+ PASSWORD_REQUIREMENTS,
19
+ PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
20
+ PHOENIX_REFRESH_TOKEN_COOKIE_NAME,
21
+ validate_email_format,
22
+ validate_password_format,
23
+ )
24
+ from phoenix.db import enums, models
25
+ from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly
26
+ from phoenix.server.api.context import Context
27
+ from phoenix.server.api.exceptions import Conflict, NotFound, Unauthorized
28
+ from phoenix.server.api.input_types.UserRoleInput import UserRoleInput
29
+ from phoenix.server.api.types.node import from_global_id_with_expected_type
30
+ from phoenix.server.api.types.User import User, to_gql_user
31
+ from phoenix.server.bearer_auth import PhoenixUser
32
+ from phoenix.server.types import AccessTokenId, ApiKeyId, PasswordResetTokenId, RefreshTokenId
33
+
34
+
35
+ @strawberry.input
36
+ class CreateUserInput:
37
+ email: str
38
+ username: str
39
+ password: str
40
+ role: UserRoleInput
41
+
42
+
43
+ @strawberry.input
44
+ class PatchViewerInput:
45
+ new_username: Optional[str] = UNSET
46
+ new_password: Optional[str] = UNSET
47
+ current_password: Optional[str] = UNSET
48
+
49
+ def __post_init__(self) -> None:
50
+ if not self.new_username and not self.new_password:
51
+ raise ValueError("At least one field must be set")
52
+ if self.new_password and not self.current_password:
53
+ raise ValueError("current_password is required when modifying password")
54
+ if self.new_password:
55
+ PASSWORD_REQUIREMENTS.validate(self.new_password)
56
+
57
+
58
+ @strawberry.input
59
+ class PatchUserInput:
60
+ user_id: GlobalID
61
+ new_role: Optional[UserRoleInput] = UNSET
62
+ new_username: Optional[str] = UNSET
63
+ new_password: Optional[str] = UNSET
64
+
65
+ def __post_init__(self) -> None:
66
+ if not self.new_role and not self.new_username and not self.new_password:
67
+ raise ValueError("At least one field must be set")
68
+ if self.new_password:
69
+ PASSWORD_REQUIREMENTS.validate(self.new_password)
70
+
71
+
72
+ @strawberry.input
73
+ class DeleteUsersInput:
74
+ user_ids: list[GlobalID]
75
+
76
+
77
+ @strawberry.type
78
+ class UserMutationPayload:
79
+ user: User
80
+
81
+
82
+ @strawberry.type
83
+ class UserMutationMixin:
84
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
85
+ async def create_user(
86
+ self,
87
+ info: Info[Context, None],
88
+ input: CreateUserInput,
89
+ ) -> UserMutationPayload:
90
+ validate_email_format(email := input.email)
91
+ validate_password_format(password := input.password)
92
+ salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
93
+ password_hash = await info.context.hash_password(password, salt)
94
+ user = models.User(
95
+ reset_password=True,
96
+ username=input.username,
97
+ email=email,
98
+ password_hash=password_hash,
99
+ password_salt=salt,
100
+ )
101
+ async with AsyncExitStack() as stack:
102
+ session = await stack.enter_async_context(info.context.db())
103
+ user_role_id = await session.scalar(_select_role_id_by_name(input.role.value))
104
+ if user_role_id is None:
105
+ raise NotFound(f"Role {input.role.value} not found")
106
+ stack.enter_context(session.no_autoflush)
107
+ user.user_role_id = user_role_id
108
+ session.add(user)
109
+ try:
110
+ await session.flush()
111
+ except IntegrityError as error:
112
+ raise Conflict(_user_operation_error_message(error))
113
+ return UserMutationPayload(user=to_gql_user(user))
114
+
115
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
116
+ async def patch_user(
117
+ self,
118
+ info: Info[Context, None],
119
+ input: PatchUserInput,
120
+ ) -> UserMutationPayload:
121
+ assert (request := info.context.request)
122
+ assert isinstance(request.user, PhoenixUser)
123
+ assert (requester_id := int(request.user.identity))
124
+ user_id = from_global_id_with_expected_type(input.user_id, expected_type_name=User.__name__)
125
+ async with AsyncExitStack() as stack:
126
+ session = await stack.enter_async_context(info.context.db())
127
+ requester = await session.scalar(_select_user_by_id(requester_id))
128
+ assert requester
129
+ if not (user := await session.scalar(_select_user_by_id(user_id))):
130
+ raise NotFound("User not found")
131
+ stack.enter_context(session.no_autoflush)
132
+ if input.new_role:
133
+ if user.email == DEFAULT_ADMIN_EMAIL:
134
+ raise Unauthorized("Cannot modify role for the default admin user")
135
+ user_role_id = await session.scalar(_select_role_id_by_name(input.new_role.value))
136
+ if user_role_id is None:
137
+ raise NotFound(f"Role {input.new_role.value} not found")
138
+ user.user_role_id = user_role_id
139
+ if password := input.new_password:
140
+ if user.auth_method != enums.AuthMethod.LOCAL.value:
141
+ raise Conflict("Cannot modify password for non-local user")
142
+ validate_password_format(password)
143
+ user.password_salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
144
+ user.password_hash = await info.context.hash_password(password, user.password_salt)
145
+ user.reset_password = True
146
+ if username := input.new_username:
147
+ user.username = username
148
+ assert user in session.dirty
149
+ try:
150
+ await session.flush()
151
+ except IntegrityError as error:
152
+ raise Conflict(_user_operation_error_message(error, "modify"))
153
+ assert user
154
+ if input.new_password:
155
+ await info.context.log_out(user.id)
156
+ return UserMutationPayload(user=to_gql_user(user))
157
+
158
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
159
+ async def patch_viewer(
160
+ self,
161
+ info: Info[Context, None],
162
+ input: PatchViewerInput,
163
+ ) -> UserMutationPayload:
164
+ assert (request := info.context.request)
165
+ assert isinstance(user := request.user, PhoenixUser)
166
+ user_id = int(user.identity)
167
+ async with AsyncExitStack() as stack:
168
+ session = await stack.enter_async_context(info.context.db())
169
+ if not (user := await session.scalar(_select_user_by_id(user_id))):
170
+ raise NotFound("User not found")
171
+ stack.enter_context(session.no_autoflush)
172
+ if password := input.new_password:
173
+ if user.auth_method != enums.AuthMethod.LOCAL.value:
174
+ raise Conflict("Cannot modify password for non-local user")
175
+ if not (
176
+ current_password := input.current_password
177
+ ) or not await info.context.is_valid_password(current_password, user):
178
+ raise Conflict("Valid current password is required to modify password")
179
+ validate_password_format(password)
180
+ user.password_salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
181
+ user.password_hash = await info.context.hash_password(password, user.password_salt)
182
+ user.reset_password = False
183
+ if username := input.new_username:
184
+ user.username = username
185
+ assert user in session.dirty
186
+ user.updated_at = datetime.now(timezone.utc)
187
+ try:
188
+ await session.flush()
189
+ except IntegrityError as error:
190
+ raise Conflict(_user_operation_error_message(error, "modify"))
191
+ assert user
192
+ if input.new_password:
193
+ await info.context.log_out(user.id)
194
+ response = info.context.get_response()
195
+ response.delete_cookie(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)
196
+ response.delete_cookie(PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
197
+ return UserMutationPayload(user=to_gql_user(user))
198
+
199
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
200
+ async def delete_users(
201
+ self,
202
+ info: Info[Context, None],
203
+ input: DeleteUsersInput,
204
+ ) -> None:
205
+ assert (token_store := info.context.token_store) is not None
206
+ if not input.user_ids:
207
+ return
208
+ user_ids = tuple(
209
+ map(
210
+ lambda gid: from_global_id_with_expected_type(gid, User.__name__),
211
+ set(input.user_ids),
212
+ )
213
+ )
214
+ system_user_role_id = (
215
+ select(models.UserRole.id)
216
+ .where(models.UserRole.name == enums.UserRole.SYSTEM.value)
217
+ .scalar_subquery()
218
+ )
219
+ admin_user_role_id = (
220
+ select(models.UserRole.id)
221
+ .where(models.UserRole.name == enums.UserRole.ADMIN.value)
222
+ .scalar_subquery()
223
+ )
224
+ default_admin_user_id = (
225
+ select(models.User.id)
226
+ .where(
227
+ (
228
+ and_(
229
+ models.User.user_role_id == admin_user_role_id,
230
+ models.User.username == DEFAULT_ADMIN_USERNAME,
231
+ models.User.email == DEFAULT_ADMIN_EMAIL,
232
+ )
233
+ )
234
+ )
235
+ .scalar_subquery()
236
+ )
237
+ async with info.context.db() as session:
238
+ [
239
+ (
240
+ deletes_default_admin,
241
+ num_resolved_user_ids,
242
+ )
243
+ ] = (
244
+ await session.execute(
245
+ select(
246
+ cast(
247
+ func.coalesce(
248
+ func.max(
249
+ case((models.User.id == default_admin_user_id, 1), else_=0)
250
+ ),
251
+ 0,
252
+ ),
253
+ Boolean,
254
+ ).label("deletes_default_admin"),
255
+ func.count(distinct(models.User.id)).label("num_resolved_user_ids"),
256
+ )
257
+ .select_from(models.User)
258
+ .where(
259
+ and_(
260
+ models.User.id.in_(user_ids),
261
+ models.User.user_role_id != system_user_role_id,
262
+ )
263
+ )
264
+ )
265
+ ).all()
266
+ if deletes_default_admin:
267
+ raise Conflict("Cannot delete the default admin user")
268
+ if num_resolved_user_ids < len(user_ids):
269
+ raise NotFound("Some user IDs could not be found")
270
+ password_reset_token_ids = [
271
+ PasswordResetTokenId(id_)
272
+ async for id_ in await session.stream_scalars(
273
+ select(models.PasswordResetToken.id).where(
274
+ models.PasswordResetToken.user_id.in_(user_ids)
275
+ )
276
+ )
277
+ ]
278
+ access_token_ids = [
279
+ AccessTokenId(id_)
280
+ async for id_ in await session.stream_scalars(
281
+ select(models.AccessToken.id).where(models.AccessToken.user_id.in_(user_ids))
282
+ )
283
+ ]
284
+ refresh_token_ids = [
285
+ RefreshTokenId(id_)
286
+ async for id_ in await session.stream_scalars(
287
+ select(models.RefreshToken.id).where(models.RefreshToken.user_id.in_(user_ids))
288
+ )
289
+ ]
290
+ api_key_ids = [
291
+ ApiKeyId(id_)
292
+ async for id_ in await session.stream_scalars(
293
+ select(models.ApiKey.id).where(models.ApiKey.user_id.in_(user_ids))
294
+ )
295
+ ]
296
+ await session.execute(delete(models.User).where(models.User.id.in_(user_ids)))
297
+ await token_store.revoke(
298
+ *password_reset_token_ids,
299
+ *access_token_ids,
300
+ *refresh_token_ids,
301
+ *api_key_ids,
302
+ )
303
+
304
+
305
+ def _select_role_id_by_name(role_name: str) -> Select[tuple[int]]:
306
+ return select(models.UserRole.id).where(models.UserRole.name == role_name)
307
+
308
+
309
+ def _select_user_by_id(user_id: int) -> Select[tuple[models.User]]:
310
+ return (
311
+ select(models.User).where(models.User.id == user_id).options(joinedload(models.User.role))
312
+ )
313
+
314
+
315
+ def _user_operation_error_message(
316
+ error: IntegrityError,
317
+ operation: Literal["create", "modify"] = "create",
318
+ ) -> str:
319
+ """
320
+ User-facing error message to explain why user creation/modification failed.
321
+ """
322
+ original_error_message = str(error)
323
+ username_already_exists = "users.username" in original_error_message
324
+ email_already_exists = "users.email" in original_error_message
325
+ if username_already_exists:
326
+ return "Username already exists"
327
+ elif email_already_exists:
328
+ return "Email already exists"
329
+ return f"Failed to {operation} user"
File without changes
@@ -0,0 +1,17 @@
1
+ import json
2
+ from argparse import ArgumentParser
3
+
4
+ from phoenix.server.api.openapi.schema import get_openapi_schema
5
+
6
+ if __name__ == "__main__":
7
+ parser = ArgumentParser()
8
+ parser.add_argument(
9
+ "-o",
10
+ "--output",
11
+ type=str,
12
+ required=True,
13
+ help="Path to the output file (e.g., openapi.json)",
14
+ )
15
+ args = parser.parse_args()
16
+ with open(args.output, "w") as f:
17
+ json.dump(get_openapi_schema(), f, indent=2)
@@ -0,0 +1,16 @@
1
+ from typing import Any
2
+
3
+ from fastapi.openapi.utils import get_openapi
4
+
5
+ from phoenix.server.api.routers.v1 import REST_API_VERSION, create_v1_router
6
+
7
+
8
+ def get_openapi_schema() -> dict[str, Any]:
9
+ v1_router = create_v1_router(authentication_enabled=False)
10
+ return get_openapi(
11
+ title="Arize-Phoenix REST API",
12
+ version=REST_API_VERSION,
13
+ openapi_version="3.1.0",
14
+ description="Schema for Arize-Phoenix REST API",
15
+ routes=v1_router.routes,
16
+ )