arize-phoenix 4.25.0__py3-none-any.whl → 4.26.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: arize-phoenix
3
- Version: 4.25.0
3
+ Version: 4.26.0
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -90,7 +90,7 @@ Requires-Dist: pandas>=1.0; extra == 'dev'
90
90
  Requires-Dist: portpicker; extra == 'dev'
91
91
  Requires-Dist: pre-commit; extra == 'dev'
92
92
  Requires-Dist: prometheus-client; extra == 'dev'
93
- Requires-Dist: psycopg[binary]; extra == 'dev'
93
+ Requires-Dist: psycopg[binary,pool]; extra == 'dev'
94
94
  Requires-Dist: pytest-asyncio; extra == 'dev'
95
95
  Requires-Dist: pytest-cov; extra == 'dev'
96
96
  Requires-Dist: pytest-postgresql; extra == 'dev'
@@ -111,6 +111,7 @@ Requires-Dist: llama-index-readers-file==0.1.25; extra == 'llama-index'
111
111
  Requires-Dist: llama-index==0.10.51; extra == 'llama-index'
112
112
  Provides-Extra: pg
113
113
  Requires-Dist: asyncpg; extra == 'pg'
114
+ Requires-Dist: psycopg[binary,pool]; extra == 'pg'
114
115
  Provides-Extra: test
115
116
  Description-Content-Type: text/markdown
116
117
 
@@ -1,13 +1,12 @@
1
1
  phoenix/__init__.py,sha256=TGNWqm2UW-l67yIRpOtmqGHVAmdoobSNqUsiTtip7uQ,1542
2
+ phoenix/auth.py,sha256=N8vTFmc5BEsdX4xr6Bmh6OwBrNUQykr74LuCIkC28jA,1455
2
3
  phoenix/config.py,sha256=wYA_8GSSz5rnpfIWDjeBL9ehKuTy9jqXaMZnxUqRYEU,10131
3
4
  phoenix/datetime_utils.py,sha256=yDKjwX2Vtqw9h5F_ProtP-TsXidM43uIvmJ_pOzYc9A,3405
4
5
  phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
5
6
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
7
  phoenix/services.py,sha256=OyML4t2XGnlqF0JXA9_uccL8HslTABxep9Ci7MViKEU,5216
7
8
  phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
8
- phoenix/version.py,sha256=gMIFKEThnzM4tu5IZY3JHWVjHbXMgpnlK4Y7S0AWlGs,23
9
- phoenix/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- phoenix/auth/utils.py,sha256=g-97oiJR219P6JvDUU5likHN4CdWtWo7bDJRmfVb0qk,450
9
+ phoenix/version.py,sha256=4h0uTKk4f4HhC84z3Ggthi61qR_IkvFgq-cnwxm5tCU,23
11
10
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
11
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
13
12
  phoenix/core/model.py,sha256=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
@@ -17,7 +16,7 @@ phoenix/db/README.md,sha256=IvKaZyf9ECbGBYYePaRhBveKZwDbxAc-c7BMxJYZh6Q,595
17
16
  phoenix/db/__init__.py,sha256=pDjEFXukHmJBM-1D8RjmXkvLsz85YWNxMQczt81ec3A,118
18
17
  phoenix/db/alembic.ini,sha256=p8DjVqGUs_tTx8oU56JP7qj-rMUebNFizItUSv_hPhs,3763
19
18
  phoenix/db/bulk_inserter.py,sha256=qgg8pt5k4VnHKOE0-KoReXVAfXRhLt-sMZihI-b4X9I,12761
20
- phoenix/db/engines.py,sha256=QDdRE6AM7JHWotAGCCljbaM_tz2hHB3-TSr_F4KUe-Q,5292
19
+ phoenix/db/engines.py,sha256=l9Zl7mPd1q4RTXpThzYzc4Lo7TuuBwaGrC-zK0SMnn4,5300
21
20
  phoenix/db/helpers.py,sha256=2zSc4n5IJfu-CaOFoBfqTB35M1nTFcAc8tqLsNtF2Jw,3488
22
21
  phoenix/db/migrate.py,sha256=NNcci4LHw0wFR7U6quWrA-sw_A4h2lAA1_LePMLkb4w,2629
23
22
  phoenix/db/models.py,sha256=lYWJYtD2asQwKU1B8JKyteWpHVYjhr1j0tmZdf9CQ5Y,23686
@@ -70,7 +69,7 @@ phoenix/pointcloud/pointcloud.py,sha256=4zAIkKs2xOUbchpj4XDAV-iPMXrfAJ15TG6rlIYG
70
69
  phoenix/pointcloud/projectors.py,sha256=zO_RrtDYSv2rqVOfIP2_9Cv11Dc8EmcZR94xhFcBYPU,1057
71
70
  phoenix/pointcloud/umap_parameters.py,sha256=3UQSjrysVOvq2V4KNpTMqNqNiK0BsTZnPBHWZ4fyJtQ,1708
72
71
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
- phoenix/server/app.py,sha256=Jy_QX0YpX6wSHe9oz45gU3VCaQ18dyyWsOxM-xcOul4,21149
72
+ phoenix/server/app.py,sha256=KcxSXDR_awZlIcJg4YkUTZIXlje85_dpdMB2OM-M7SQ,22059
74
73
  phoenix/server/dml_event.py,sha256=MpjCFqljxvgb9OB5Cez9vJesb3oHb3XxXictynBfcis,2851
75
74
  phoenix/server/dml_event_handler.py,sha256=6p-PucctivelVHfO-_9zNxWZYPr_eGjDF3bKjLtc5co,8251
76
75
  phoenix/server/grpc_server.py,sha256=jllxDNkpLQxDkvej4RhTokobowbvydF-SU8gSw1MTCc,3378
@@ -138,8 +137,9 @@ phoenix/server/api/input_types/SpanAnnotationSort.py,sha256=T5pAGzmh4MiJp9JMAzND
138
137
  phoenix/server/api/input_types/SpanSort.py,sha256=Dhvl8BIoV52yHoqntfOax_gUc15uH8ITI_00Ha7PvYc,5959
139
138
  phoenix/server/api/input_types/TimeRange.py,sha256=yzx-gxj8mDeGLft1FzU_x1MVEgIG5Pt6-f8PUVDgipQ,522
140
139
  phoenix/server/api/input_types/TraceAnnotationSort.py,sha256=BzwiUnMh2VsgQYnhDlbJ6ljHugqIS4YDUlYzvq_tl3o,365
140
+ phoenix/server/api/input_types/UserRoleInput.py,sha256=xxhFe0ITZOgRVEJbVem_W6F1Ip_H6xDENdQqMMx-kKE,129
141
141
  phoenix/server/api/input_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
- phoenix/server/api/mutations/__init__.py,sha256=XFlHUZKD6oJWfnXVXOoP1HYeC7Jdw1TntmAUnOQWn8M,880
142
+ phoenix/server/api/mutations/__init__.py,sha256=Cu4lPgUFRAGzKO528jKepwKtfre9lkLTN059S2Shmnw,977
143
143
  phoenix/server/api/mutations/api_key_mutations.py,sha256=cv6AT6UAL55lTC_UqMdcN-1TjWAgqqZi__S9Tw12t6I,3688
144
144
  phoenix/server/api/mutations/auth.py,sha256=8o4tTfGCPkpUauuB9ijPH84Od77UX_UrQWfmUsnujI4,524
145
145
  phoenix/server/api/mutations/dataset_mutations.py,sha256=0feBUW_07FEIx6uzepjxfRVhk5lAck0AkrqS1GVdoF4,27041
@@ -148,10 +148,12 @@ phoenix/server/api/mutations/export_events_mutations.py,sha256=t_wYBxaqvBJYRoHsl
148
148
  phoenix/server/api/mutations/project_mutations.py,sha256=MLm7I97lJ85hTuc1tq8sdYA8Ps5WKMV-bGqeeN-Ey90,2279
149
149
  phoenix/server/api/mutations/span_annotations_mutations.py,sha256=DM9gzxrMSAcxwXQ6jNaNGDVgl8oP50LZsBWRYQwLaSo,5955
150
150
  phoenix/server/api/mutations/trace_annotations_mutations.py,sha256=VDiNzX63Agci7WeMbiK-C770JedlC5R7TZVe1UaRhDE,5930
151
+ phoenix/server/api/mutations/user_mutations.py,sha256=uUZ9LEPQAWRxGA4CVHFClHSGpyMlFHwgi6blu3pkuVA,2998
151
152
  phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
152
153
  phoenix/server/api/openapi/main.py,sha256=KNutA_7AvV_WlGX8cOkvvDujcJKQ7AD1HT6rTpCpR8A,616
153
154
  phoenix/server/api/openapi/schema.py,sha256=oVZoflWMfzOrLKMIrjr3iLnJ13rmN-t_DOe9g6KoN5s,471
154
155
  phoenix/server/api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
156
+ phoenix/server/api/routers/auth.py,sha256=dGug0NjOjW1mmIghmheAgHutG7_0-RjL-FcEReWzTHc,1806
155
157
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
156
158
  phoenix/server/api/routers/v1/__init__.py,sha256=nb49zcOdAi3DSGuC9gUubN9Yri-o7-WFdlGak4jGuFw,1462
157
159
  phoenix/server/api/routers/v1/datasets.py,sha256=l3Hlc9AVyvX5GdT9iOXBsV-i4c_vtnCaXeSAWdNzcw8,37090
@@ -289,8 +291,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
289
291
  phoenix/utilities/project.py,sha256=8IJuMM4yUMoooPi37sictGj8Etu9rGmq6RFtc9848cQ,436
290
292
  phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
291
293
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
292
- arize_phoenix-4.25.0.dist-info/METADATA,sha256=zbUCVE7dfjz9AjNV6ZOauoujdQmaAZwXsn02rUz4UhM,11967
293
- arize_phoenix-4.25.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
294
- arize_phoenix-4.25.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
295
- arize_phoenix-4.25.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
296
- arize_phoenix-4.25.0.dist-info/RECORD,,
294
+ arize_phoenix-4.26.0.dist-info/METADATA,sha256=tf_mydvCrJ0GBxa58iEnat5FdCDItFKPN88hnZj7Dqs,12023
295
+ arize_phoenix-4.26.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
296
+ arize_phoenix-4.26.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
297
+ arize_phoenix-4.26.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
298
+ arize_phoenix-4.26.0.dist-info/RECORD,,
phoenix/auth.py ADDED
@@ -0,0 +1,45 @@
1
+ import re
2
+ from hashlib import pbkdf2_hmac
3
+
4
+
5
+ def compute_password_hash(*, password: str, salt: str) -> str:
6
+ """
7
+ Salts and hashes a password using PBKDF2, HMAC, and SHA256.
8
+ """
9
+ password_bytes = password.encode("utf-8")
10
+ salt_bytes = salt.encode("utf-8")
11
+ password_hash_bytes = pbkdf2_hmac("sha256", password_bytes, salt_bytes, NUM_ITERATIONS)
12
+ password_hash = password_hash_bytes.hex()
13
+ return password_hash
14
+
15
+
16
+ def is_valid_password(*, password: str, salt: str, password_hash: str) -> bool:
17
+ """
18
+ Determines whether the password is valid by salting and hashing the password
19
+ and comparing against the existing hash value.
20
+ """
21
+ return password_hash == compute_password_hash(password=password, salt=salt)
22
+
23
+
24
+ def validate_email_format(email: str) -> None:
25
+ """
26
+ Checks that the email has a valid format.
27
+ """
28
+ if EMAIL_PATTERN.match(email) is None:
29
+ raise ValueError("Invalid email address")
30
+
31
+
32
+ def validate_password_format(password: str) -> None:
33
+ """
34
+ Checks that the password has a valid format.
35
+ """
36
+ if not password:
37
+ raise ValueError("Password must be non-empty")
38
+ if any(char.isspace() for char in password):
39
+ raise ValueError("Password cannot contain whitespace characters")
40
+ if not password.isascii():
41
+ raise ValueError("Password can contain only ASCII characters")
42
+
43
+
44
+ EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+[.][^@\s]+\Z")
45
+ NUM_ITERATIONS = 10_000
phoenix/db/engines.py CHANGED
@@ -139,7 +139,7 @@ def aio_postgresql_engine(
139
139
  if not migrate:
140
140
  return engine
141
141
  sync_engine = sqlalchemy.create_engine(
142
- url=url.set(drivername="postgresql"),
142
+ url=url.set(drivername="postgresql+psycopg"),
143
143
  echo=Settings.log_migrations,
144
144
  json_serializer=_dumps,
145
145
  )
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+ import strawberry
4
+
5
+
6
+ @strawberry.enum
7
+ class UserRoleInput(Enum):
8
+ ADMIN = "ADMIN"
9
+ MEMBER = "MEMBER"
@@ -7,6 +7,7 @@ from phoenix.server.api.mutations.export_events_mutations import ExportEventsMut
7
7
  from phoenix.server.api.mutations.project_mutations import ProjectMutationMixin
8
8
  from phoenix.server.api.mutations.span_annotations_mutations import SpanAnnotationMutationMixin
9
9
  from phoenix.server.api.mutations.trace_annotations_mutations import TraceAnnotationMutationMixin
10
+ from phoenix.server.api.mutations.user_mutations import UserMutationMixin
10
11
 
11
12
 
12
13
  @strawberry.type
@@ -18,5 +19,6 @@ class Mutation(
18
19
  SpanAnnotationMutationMixin,
19
20
  TraceAnnotationMutationMixin,
20
21
  ApiKeyMutationMixin,
22
+ UserMutationMixin,
21
23
  ):
22
24
  pass
@@ -0,0 +1,89 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+ import strawberry
5
+ from sqlalchemy import insert, select
6
+ from sqlean.dbapi2 import IntegrityError # type: ignore[import-untyped]
7
+ from strawberry.types import Info
8
+
9
+ from phoenix.auth import compute_password_hash, validate_email_format, validate_password_format
10
+ from phoenix.db import models
11
+ from phoenix.server.api.context import Context
12
+ from phoenix.server.api.input_types.UserRoleInput import UserRoleInput
13
+ from phoenix.server.api.types.User import User
14
+ from phoenix.server.api.types.UserRole import UserRole
15
+
16
+
17
+ @strawberry.input
18
+ class CreateUserInput:
19
+ email: str
20
+ username: Optional[str]
21
+ password: str
22
+ role: UserRoleInput
23
+
24
+
25
+ @strawberry.type
26
+ class UserMutationPayload:
27
+ user: User
28
+
29
+
30
+ @strawberry.type
31
+ class UserMutationMixin:
32
+ @strawberry.mutation
33
+ async def create_user(
34
+ self,
35
+ info: Info[Context, None],
36
+ input: CreateUserInput,
37
+ ) -> UserMutationPayload:
38
+ validate_email_format(email := input.email)
39
+ validate_password_format(password := input.password)
40
+ role_name = input.role.value
41
+ user_role_id = (
42
+ select(models.UserRole.id).where(models.UserRole.name == role_name).scalar_subquery()
43
+ )
44
+ secret = info.context.get_secret()
45
+ loop = asyncio.get_running_loop()
46
+ password_hash = await loop.run_in_executor(
47
+ executor=None,
48
+ func=lambda: compute_password_hash(password=password, salt=secret),
49
+ )
50
+ try:
51
+ async with info.context.db() as session:
52
+ user = await session.scalar(
53
+ insert(models.User)
54
+ .values(
55
+ user_role_id=user_role_id,
56
+ username=input.username,
57
+ email=email,
58
+ auth_method="LOCAL",
59
+ password_hash=password_hash,
60
+ reset_password=True,
61
+ )
62
+ .returning(models.User)
63
+ )
64
+ assert user is not None
65
+ except IntegrityError as error:
66
+ raise ValueError(_get_user_create_error_message(error))
67
+ return UserMutationPayload(
68
+ user=User(
69
+ id_attr=user.id,
70
+ email=user.email,
71
+ username=user.username,
72
+ created_at=user.created_at,
73
+ role=UserRole(id_attr=user.user_role_id, name=role_name),
74
+ )
75
+ )
76
+
77
+
78
+ def _get_user_create_error_message(error: IntegrityError) -> str:
79
+ """
80
+ Gets a user-facing error message to explain why user creation failed.
81
+ """
82
+ original_error_message = str(error)
83
+ username_already_exists = "users.username" in original_error_message
84
+ email_already_exists = "users.email" in original_error_message
85
+ if username_already_exists:
86
+ return "Username already exists"
87
+ elif email_already_exists:
88
+ return "Email already exists"
89
+ return "Failed to create user"
@@ -0,0 +1,52 @@
1
+ import asyncio
2
+ from datetime import timedelta
3
+
4
+ from fastapi import APIRouter, Form, Request, Response
5
+ from sqlalchemy import select
6
+ from starlette.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED
7
+ from typing_extensions import Annotated
8
+
9
+ from phoenix.auth import is_valid_password
10
+ from phoenix.db import models
11
+
12
+ router = APIRouter(include_in_schema=False)
13
+
14
+ PHOENIX_ACCESS_TOKEN_COOKIE_NAME = "phoenix-access-token"
15
+ PHOENIX_ACCESS_TOKEN_COOKIE_MAX_AGE_IN_SECONDS = int(timedelta(days=31).total_seconds())
16
+
17
+
18
+ @router.post("/login")
19
+ async def login(
20
+ request: Request,
21
+ email: Annotated[str, Form()],
22
+ password: Annotated[str, Form()],
23
+ ) -> Response:
24
+ async with request.app.state.db() as session:
25
+ if (
26
+ user := await session.scalar(select(models.User).where(models.User.email == email))
27
+ ) is None or (password_hash := user.password_hash) is None:
28
+ return Response(status_code=HTTP_401_UNAUTHORIZED)
29
+ secret = request.app.state.get_secret()
30
+ loop = asyncio.get_running_loop()
31
+ if not await loop.run_in_executor(
32
+ executor=None,
33
+ func=lambda: is_valid_password(password=password, salt=secret, password_hash=password_hash),
34
+ ):
35
+ return Response(status_code=HTTP_401_UNAUTHORIZED)
36
+ response = Response(status_code=HTTP_204_NO_CONTENT)
37
+ response.set_cookie(
38
+ key=PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
39
+ value="token", # todo: compute access token
40
+ secure=True,
41
+ httponly=True,
42
+ samesite="strict",
43
+ max_age=PHOENIX_ACCESS_TOKEN_COOKIE_MAX_AGE_IN_SECONDS,
44
+ )
45
+ return response
46
+
47
+
48
+ @router.post("/logout")
49
+ async def logout() -> Response:
50
+ response = Response(status_code=HTTP_204_NO_CONTENT)
51
+ response.delete_cookie(key=PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
52
+ return response
phoenix/server/app.py CHANGED
@@ -4,6 +4,7 @@ import json
4
4
  import logging
5
5
  from functools import cached_property
6
6
  from pathlib import Path
7
+ from types import MethodType
7
8
  from typing import (
8
9
  TYPE_CHECKING,
9
10
  Any,
@@ -30,6 +31,7 @@ from sqlalchemy.ext.asyncio import (
30
31
  AsyncSession,
31
32
  async_sessionmaker,
32
33
  )
34
+ from starlette.datastructures import State as StarletteState
33
35
  from starlette.exceptions import HTTPException
34
36
  from starlette.middleware import Middleware
35
37
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
@@ -80,6 +82,7 @@ from phoenix.server.api.dataloaders import (
80
82
  TokenCountDataLoader,
81
83
  TraceRowIdsDataLoader,
82
84
  )
85
+ from phoenix.server.api.routers.auth import router as auth_router
83
86
  from phoenix.server.api.routers.v1 import REST_API_VERSION
84
87
  from phoenix.server.api.routers.v1 import router as v1_router
85
88
  from phoenix.server.api.schema import schema
@@ -536,6 +539,8 @@ def create_app(
536
539
  app.include_router(router)
537
540
  app.include_router(graphql_router)
538
541
  app.add_middleware(GZipMiddleware)
542
+ if authentication_enabled:
543
+ app.include_router(auth_router)
539
544
  if serve_ui:
540
545
  app.mount(
541
546
  "/",
@@ -554,8 +559,7 @@ def create_app(
554
559
  ),
555
560
  name="static",
556
561
  )
557
-
558
- app.state.db = db
562
+ app = _update_app_state(app, db=db, secret=secret)
559
563
  if tracer_provider:
560
564
  from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
561
565
 
@@ -563,3 +567,22 @@ def create_app(
563
567
  FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
564
568
  shutdown_callbacks_list.append(FastAPIInstrumentor().uninstrument)
565
569
  return app
570
+
571
+
572
+ def _update_app_state(app: FastAPI, /, *, db: DbSessionFactory, secret: Optional[str]) -> FastAPI:
573
+ """
574
+ Dynamically updates the app's `state` to include useful fields and methods
575
+ (at the time of this writing, FastAPI does not support setting this state
576
+ during the creation of the app).
577
+ """
578
+ app.state.db = db
579
+ app.state._secret = secret
580
+
581
+ def get_secret(self: StarletteState) -> str:
582
+ if (secret := self._secret) is None:
583
+ raise ValueError("app secret is not set")
584
+ assert isinstance(secret, str)
585
+ return secret
586
+
587
+ app.state.get_secret = MethodType(get_secret, app.state)
588
+ return app
phoenix/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.25.0"
1
+ __version__ = "4.26.0"
phoenix/auth/__init__.py DELETED
File without changes
phoenix/auth/utils.py DELETED
@@ -1,15 +0,0 @@
1
- from hashlib import pbkdf2_hmac
2
-
3
-
4
- def compute_password_hash(password: str, salt: str) -> str:
5
- """
6
- Salts and hashes a password using PBKDF2, HMAC, and SHA256.
7
- """
8
- password_bytes = password.encode("utf-8")
9
- salt_bytes = salt.encode("utf-8")
10
- password_hash_bytes = pbkdf2_hmac("sha256", password_bytes, salt_bytes, NUM_ITERATIONS)
11
- password_hash = password_hash_bytes.hex()
12
- return password_hash
13
-
14
-
15
- NUM_ITERATIONS = 1_000_000