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
phoenix/auth.py ADDED
@@ -0,0 +1,309 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timedelta
6
+ from enum import Enum, auto
7
+ from hashlib import pbkdf2_hmac
8
+ from typing import Any, Literal, Optional, Protocol
9
+
10
+ from starlette.responses import Response
11
+ from typing_extensions import TypeVar
12
+
13
+ from phoenix.config import get_env_phoenix_use_secure_cookies
14
+
15
+ ResponseType = TypeVar("ResponseType", bound=Response)
16
+
17
+
18
+ def compute_password_hash(*, password: str, salt: bytes) -> bytes:
19
+ """
20
+ Salts and hashes a password using PBKDF2, HMAC, and SHA256.
21
+
22
+ Args:
23
+ password (str): the password to hash
24
+ salt (bytes): the salt to use, must not be zero-length
25
+ Returns:
26
+ bytes: the hashed password
27
+ """
28
+ assert salt
29
+ password_bytes = password.encode("utf-8")
30
+ return pbkdf2_hmac("sha256", password_bytes, salt, NUM_ITERATIONS)
31
+
32
+
33
+ def is_valid_password(*, password: str, salt: bytes, password_hash: bytes) -> bool:
34
+ """
35
+ Determines whether the password is valid by salting and hashing the password
36
+ and comparing against the existing hash value.
37
+
38
+ Args:
39
+ password (str): the password to validate
40
+ salt (bytes): the salt to use, must not be zero-length
41
+ password_hash (bytes): the hash to compare against
42
+ Returns:
43
+ bool: True if the password is valid, False otherwise
44
+ """
45
+ assert salt
46
+ return password_hash == compute_password_hash(password=password, salt=salt)
47
+
48
+
49
+ def validate_email_format(email: str) -> None:
50
+ """
51
+ Checks that the email has a valid format.
52
+
53
+ Args:
54
+ email (str): the email address to validate
55
+ Returns:
56
+ None
57
+ Raises:
58
+ ValueError: if the email address is invalid
59
+ """
60
+ if EMAIL_PATTERN.match(email) is None:
61
+ raise ValueError("Invalid email address")
62
+
63
+
64
+ def validate_password_format(password: str) -> None:
65
+ """
66
+ Checks that the password has a valid format.
67
+ """
68
+ PASSWORD_REQUIREMENTS.validate(password)
69
+
70
+
71
+ def set_access_token_cookie(
72
+ *, response: ResponseType, access_token: str, max_age: timedelta
73
+ ) -> ResponseType:
74
+ return _set_cookie(
75
+ response=response,
76
+ cookie_name=PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
77
+ cookie_max_age=max_age,
78
+ samesite="strict",
79
+ value=access_token,
80
+ )
81
+
82
+
83
+ def set_refresh_token_cookie(
84
+ *, response: ResponseType, refresh_token: str, max_age: timedelta
85
+ ) -> ResponseType:
86
+ return _set_cookie(
87
+ response=response,
88
+ cookie_name=PHOENIX_REFRESH_TOKEN_COOKIE_NAME,
89
+ cookie_max_age=max_age,
90
+ samesite="strict",
91
+ value=refresh_token,
92
+ )
93
+
94
+
95
+ def set_oauth2_state_cookie(
96
+ *, response: ResponseType, state: str, max_age: timedelta
97
+ ) -> ResponseType:
98
+ return _set_cookie(
99
+ response=response,
100
+ cookie_name=PHOENIX_OAUTH2_STATE_COOKIE_NAME,
101
+ cookie_max_age=max_age,
102
+ samesite="lax",
103
+ value=state,
104
+ )
105
+
106
+
107
+ def set_oauth2_nonce_cookie(
108
+ *, response: ResponseType, nonce: str, max_age: timedelta
109
+ ) -> ResponseType:
110
+ return _set_cookie(
111
+ response=response,
112
+ cookie_name=PHOENIX_OAUTH2_NONCE_COOKIE_NAME,
113
+ cookie_max_age=max_age,
114
+ samesite="lax",
115
+ value=nonce,
116
+ )
117
+
118
+
119
+ def _set_cookie(
120
+ *,
121
+ response: ResponseType,
122
+ cookie_name: str,
123
+ cookie_max_age: timedelta,
124
+ samesite: Literal["strict", "lax"],
125
+ value: str,
126
+ ) -> ResponseType:
127
+ response.set_cookie(
128
+ key=cookie_name,
129
+ value=value,
130
+ secure=get_env_phoenix_use_secure_cookies(),
131
+ httponly=True,
132
+ samesite=samesite,
133
+ max_age=int(cookie_max_age.total_seconds()),
134
+ )
135
+ return response
136
+
137
+
138
+ def delete_access_token_cookie(response: ResponseType) -> ResponseType:
139
+ response.delete_cookie(key=PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
140
+ return response
141
+
142
+
143
+ def delete_refresh_token_cookie(response: ResponseType) -> ResponseType:
144
+ response.delete_cookie(key=PHOENIX_REFRESH_TOKEN_COOKIE_NAME)
145
+ return response
146
+
147
+
148
+ def delete_oauth2_state_cookie(response: ResponseType) -> ResponseType:
149
+ response.delete_cookie(key=PHOENIX_OAUTH2_STATE_COOKIE_NAME)
150
+ return response
151
+
152
+
153
+ def delete_oauth2_nonce_cookie(response: ResponseType) -> ResponseType:
154
+ response.delete_cookie(key=PHOENIX_OAUTH2_NONCE_COOKIE_NAME)
155
+ return response
156
+
157
+
158
+ @dataclass(frozen=True)
159
+ class _PasswordRequirements:
160
+ """
161
+ Password must be at least `length` characters long. Password must not contain whitespace
162
+ characters. Password can contain only ASCII characters. The arguments `special_chars`,
163
+ `digits`, `upper_case`, and `lower_case` control what category of characters will appear
164
+ in the password. If set to True, at least one character from the corresponding category
165
+ is guaranteed to appear. Special characters are characters from `!@#$%^&*()_+`, digits
166
+ are characters from `0123456789`, and uppercase and lowercase characters are characters
167
+ from the ASCII set of letters.
168
+
169
+ Attributes:
170
+ length (int): the minimum length of the password
171
+ digits (bool): whether the password must contain at least one digit
172
+ lower_case (bool): whether the password must contain at least one lowercase letter
173
+ upper_case (bool): whether the password must contain at least one uppercase letter
174
+ special_chars (bool): whether the password must contain at least one special character
175
+ """
176
+
177
+ length: int
178
+ digits: bool = False
179
+ lower_case: bool = False
180
+ upper_case: bool = False
181
+ special_chars: bool = False
182
+
183
+ def validate(
184
+ self,
185
+ string: str,
186
+ /,
187
+ err_msg_subject: Literal["Password", "Phoenix secret"] = "Password",
188
+ ) -> None:
189
+ """
190
+ Validates the password against the requirements.
191
+
192
+ Args:
193
+ string (str): the password to validate
194
+ err_msg_subject (str, optional): the subject of the error message,
195
+ defaults to "Password"
196
+ Returns:
197
+ None
198
+ Raises:
199
+ ValueError: if the password does not meet the requirements
200
+ """
201
+ if not string:
202
+ raise ValueError(f"{err_msg_subject} must be non-empty")
203
+ if any(char.isspace() for char in string):
204
+ raise ValueError(f"{err_msg_subject} must not contain whitespace characters")
205
+ if not string.isascii():
206
+ raise ValueError(f"{err_msg_subject} must contain only ASCII characters")
207
+ err_msg = []
208
+ if len(string) < self.length:
209
+ err_msg.append(f"must be at least {self.length} characters long")
210
+ if self.digits and not any(char.isdigit() for char in string):
211
+ err_msg.append("at least one digit")
212
+ if self.lower_case and not any(char.islower() for char in string):
213
+ err_msg.append("at least one lowercase letter")
214
+ if self.upper_case and not any(char.isupper() for char in string):
215
+ err_msg.append("at least one uppercase letter")
216
+ if self.special_chars and not any(char in "!@#$%^&*()_+" for char in string):
217
+ err_msg.append("at least one special character")
218
+ if not err_msg:
219
+ return
220
+ if len(err_msg) > 1:
221
+ err_text = f"{err_msg_subject} " + ", ".join(err_msg[:-1]) + ", and " + err_msg[-1]
222
+ else:
223
+ err_text = f"{err_msg_subject} {err_msg[0]}"
224
+ raise ValueError(err_text)
225
+
226
+
227
+ DEFAULT_ADMIN_USERNAME = "admin"
228
+ DEFAULT_ADMIN_EMAIL = "admin@localhost"
229
+ DEFAULT_ADMIN_PASSWORD = "admin"
230
+ DEFAULT_SYSTEM_USERNAME = "system"
231
+ DEFAULT_SYSTEM_EMAIL = "system@localhost"
232
+ DEFAULT_SECRET_LENGTH = 32
233
+ DEFAULT_PASSWORD_RESET_TOKEN_EXPIRY_MINUTES = 15
234
+ DEFAULT_ACCESS_TOKEN_EXPIRY_MINUTES = 10
235
+ DEFAULT_REFRESH_TOKEN_EXPIRY_MINUTES = 60 * 24 * 7
236
+ """The default length of a secret key in bytes."""
237
+ EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+[.][^@\s]+\Z")
238
+ """The regular expression pattern for a valid email address."""
239
+ NUM_ITERATIONS = 10_000
240
+ """The number of iterations to use for the PBKDF2 key derivation function."""
241
+ MIN_PASSWORD_LENGTH = 4
242
+ """The minimum length of a password."""
243
+ PASSWORD_REQUIREMENTS = _PasswordRequirements(length=MIN_PASSWORD_LENGTH)
244
+ """The requirements for a valid password."""
245
+ REQUIREMENTS_FOR_PHOENIX_SECRET = _PasswordRequirements(
246
+ length=DEFAULT_SECRET_LENGTH, digits=True, lower_case=True
247
+ )
248
+ """The requirements for the Phoenix secret key."""
249
+ JWT_ALGORITHM = "HS256"
250
+ """The algorithm to use for the JSON Web Token."""
251
+ PHOENIX_ACCESS_TOKEN_COOKIE_NAME = "phoenix-access-token"
252
+ """The name of the cookie that stores the Phoenix access token."""
253
+ PHOENIX_REFRESH_TOKEN_COOKIE_NAME = "phoenix-refresh-token"
254
+ """The name of the cookie that stores the Phoenix refresh token."""
255
+ PHOENIX_OAUTH2_STATE_COOKIE_NAME = "phoenix-oauth2-state"
256
+ """The name of the cookie that stores the state used for the OAuth2 authorization code flow."""
257
+ PHOENIX_OAUTH2_NONCE_COOKIE_NAME = "phoenix-oauth2-nonce"
258
+ """The name of the cookie that stores the nonce used for the OAuth2 authorization code flow."""
259
+ DEFAULT_OAUTH2_LOGIN_EXPIRY_MINUTES = 15
260
+ """
261
+ The default amount of time in minutes that can elapse between the initial
262
+ redirect to the IDP and the invocation of the callback URL during the OAuth2
263
+ authorization code flow.
264
+ """
265
+
266
+
267
+ class Token(str): ...
268
+
269
+
270
+ class ClaimSetStatus(Enum):
271
+ VALID = auto()
272
+ INVALID = auto()
273
+ EXPIRED = auto()
274
+
275
+
276
+ @dataclass(frozen=True)
277
+ class TokenAttributes: ...
278
+
279
+
280
+ @dataclass(frozen=True)
281
+ class ClaimSet:
282
+ issuer: Optional[Any] = None
283
+ "Analog of `iss` claim in JWT RFC7519: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1"
284
+ subject: Optional[Any] = None
285
+ "Analog of `sub` claim in JWT RFC7519: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2"
286
+ audience: Optional[Any] = None
287
+ "Analog of `aud` claim in JWT RFC7519: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3"
288
+ not_before: Optional[datetime] = None
289
+ "Analog of `nbf` claim in JWT RFC7519: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5"
290
+ issued_at: Optional[datetime] = None
291
+ "Analog of `iat` claim in JWT RFC7519: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6"
292
+ expiration_time: Optional[datetime] = None
293
+ "Analog of `exp` claim in JWT RFC7519: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4"
294
+ token_id: Optional[Any] = None
295
+ "Analog of `jti` claim in JWT RFC7519: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7"
296
+ attributes: Optional[TokenAttributes] = None
297
+ "Application/domain-specific claims"
298
+
299
+ @property
300
+ def status(self) -> ClaimSetStatus:
301
+ if self.expiration_time and self.expiration_time.timestamp() < datetime.now().timestamp():
302
+ return ClaimSetStatus.EXPIRED
303
+ if self.token_id is not None and self.subject is not None:
304
+ return ClaimSetStatus.VALID
305
+ return ClaimSetStatus.INVALID
306
+
307
+
308
+ class CanReadToken(Protocol):
309
+ async def read(self, token: Token) -> Optional[ClaimSet]: ...