arize-phoenix 12.2.0__py3-none-any.whl → 12.4.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 (46) hide show
  1. {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/METADATA +2 -1
  2. {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/RECORD +45 -44
  3. phoenix/auth.py +19 -0
  4. phoenix/config.py +302 -53
  5. phoenix/db/README.md +546 -28
  6. phoenix/server/api/auth_messages.py +46 -0
  7. phoenix/server/api/routers/auth.py +21 -30
  8. phoenix/server/api/routers/oauth2.py +255 -46
  9. phoenix/server/api/routers/v1/__init__.py +2 -3
  10. phoenix/server/api/routers/v1/annotation_configs.py +12 -29
  11. phoenix/server/api/routers/v1/annotations.py +21 -22
  12. phoenix/server/api/routers/v1/datasets.py +38 -56
  13. phoenix/server/api/routers/v1/documents.py +2 -3
  14. phoenix/server/api/routers/v1/evaluations.py +12 -24
  15. phoenix/server/api/routers/v1/experiment_evaluations.py +2 -3
  16. phoenix/server/api/routers/v1/experiment_runs.py +9 -10
  17. phoenix/server/api/routers/v1/experiments.py +16 -17
  18. phoenix/server/api/routers/v1/projects.py +15 -21
  19. phoenix/server/api/routers/v1/prompts.py +30 -31
  20. phoenix/server/api/routers/v1/sessions.py +2 -5
  21. phoenix/server/api/routers/v1/spans.py +35 -26
  22. phoenix/server/api/routers/v1/traces.py +11 -19
  23. phoenix/server/api/routers/v1/users.py +14 -23
  24. phoenix/server/api/routers/v1/utils.py +3 -7
  25. phoenix/server/app.py +6 -2
  26. phoenix/server/authorization.py +2 -3
  27. phoenix/server/bearer_auth.py +4 -5
  28. phoenix/server/cost_tracking/model_cost_manifest.json +54 -54
  29. phoenix/server/oauth2.py +174 -9
  30. phoenix/server/static/.vite/manifest.json +39 -39
  31. phoenix/server/static/assets/{components-BG6v0EM8.js → components-BvsExS75.js} +422 -387
  32. phoenix/server/static/assets/{index-CSVcULw1.js → index-iq8WDxat.js} +12 -12
  33. phoenix/server/static/assets/{pages-DgaM7kpM.js → pages-Ckg4SLQ9.js} +542 -488
  34. phoenix/server/static/assets/vendor-D2eEI-6h.js +914 -0
  35. phoenix/server/static/assets/{vendor-arizeai-DlOj0PQQ.js → vendor-arizeai-kfOei7nf.js} +2 -2
  36. phoenix/server/static/assets/{vendor-codemirror-B2PHH5yZ.js → vendor-codemirror-1bq_t1Ec.js} +3 -3
  37. phoenix/server/static/assets/{vendor-recharts-CKsi4IjN.js → vendor-recharts-DQ4xfrf4.js} +1 -1
  38. phoenix/server/static/assets/{vendor-shiki-DN26BkKE.js → vendor-shiki-GGmcIQxA.js} +1 -1
  39. phoenix/server/templates/index.html +1 -0
  40. phoenix/trace/attributes.py +80 -13
  41. phoenix/version.py +1 -1
  42. phoenix/server/static/assets/vendor-BqTEkGQU.js +0 -903
  43. {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/WHEEL +0 -0
  44. {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/entry_points.txt +0 -0
  45. {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/licenses/IP_NOTICE +0 -0
  46. {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,46 @@
1
+ # ruff: noqa: E501
2
+ """
3
+ Authentication error and success message codes.
4
+
5
+ These codes are used in authentication flows to safely communicate status
6
+ to users via query parameters. Using codes instead of raw messages prevents
7
+ social engineering and phishing attacks.
8
+
9
+ The messages are passed to the frontend via window.Config to ensure a single
10
+ source of truth between backend and frontend.
11
+ """
12
+
13
+ from types import MappingProxyType
14
+ from typing import Literal, Mapping, get_args
15
+
16
+ # Error code type - used for type hints in redirect functions
17
+ AuthErrorCode = Literal[
18
+ "unknown_idp",
19
+ "auth_failed",
20
+ "invalid_state",
21
+ "unsafe_return_url",
22
+ "oauth_error",
23
+ "no_oidc_support",
24
+ "missing_email_scope",
25
+ "email_in_use",
26
+ "sign_in_not_allowed",
27
+ ]
28
+
29
+ # Error messages - passed to frontend via window.Config.authErrorMessages
30
+ # Backend generates these codes when redirecting users after OAuth errors
31
+ AUTH_ERROR_MESSAGES: Mapping[AuthErrorCode, str] = MappingProxyType(
32
+ {
33
+ "unknown_idp": "Unknown identity provider.",
34
+ "auth_failed": "Authentication failed. Please contact your administrator.",
35
+ "invalid_state": "Invalid authentication state. Please try again.",
36
+ "unsafe_return_url": "Invalid return URL. Please try again.",
37
+ "oauth_error": "Authentication failed. Please try again.",
38
+ "no_oidc_support": "Your identity provider does not appear to support OpenID Connect. Please contact your administrator.",
39
+ "missing_email_scope": "Please ensure your identity provider is configured to use the 'email' scope.",
40
+ "email_in_use": "An account with this email already exists.",
41
+ "sign_in_not_allowed": "Sign in is not allowed. Please contact your administrator.",
42
+ }
43
+ )
44
+
45
+ # Runtime assertion to ensure AUTH_ERROR_MESSAGES keys match AuthErrorCode Literal values
46
+ assert set(AUTH_ERROR_MESSAGES.keys()) == set(get_args(AuthErrorCode))
@@ -7,15 +7,6 @@ from urllib.parse import urlencode, urlparse, urlunparse
7
7
  from fastapi import APIRouter, Depends, HTTPException, Request, Response
8
8
  from sqlalchemy import func, select
9
9
  from sqlalchemy.orm import joinedload
10
- from starlette.status import (
11
- HTTP_204_NO_CONTENT,
12
- HTTP_302_FOUND,
13
- HTTP_401_UNAUTHORIZED,
14
- HTTP_403_FORBIDDEN,
15
- HTTP_404_NOT_FOUND,
16
- HTTP_422_UNPROCESSABLE_ENTITY,
17
- HTTP_503_SERVICE_UNAVAILABLE,
18
- )
19
10
 
20
11
  from phoenix.auth import (
21
12
  DEFAULT_SECRET_LENGTH,
@@ -76,7 +67,7 @@ router = APIRouter(prefix="/auth", include_in_schema=False, dependencies=auth_de
76
67
  @router.post("/login")
77
68
  async def login(request: Request) -> Response:
78
69
  if get_env_disable_basic_auth():
79
- raise HTTPException(status_code=HTTP_403_FORBIDDEN)
70
+ raise HTTPException(status_code=403)
80
71
  assert isinstance(access_token_expiry := request.app.state.access_token_expiry, timedelta)
81
72
  assert isinstance(refresh_token_expiry := request.app.state.refresh_token_expiry, timedelta)
82
73
  token_store: TokenStore = request.app.state.get_token_store()
@@ -85,7 +76,7 @@ async def login(request: Request) -> Response:
85
76
  password = data.get("password")
86
77
 
87
78
  if not email or not password:
88
- raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Email and password required")
79
+ raise HTTPException(status_code=401, detail="Email and password required")
89
80
 
90
81
  # Sanitize email by trimming and lowercasing
91
82
  email = sanitize_email(email)
@@ -101,14 +92,14 @@ async def login(request: Request) -> Response:
101
92
  or (password_hash := user.password_hash) is None
102
93
  or (salt := user.password_salt) is None
103
94
  ):
104
- raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=LOGIN_FAILED_MESSAGE)
95
+ raise HTTPException(status_code=401, detail=LOGIN_FAILED_MESSAGE)
105
96
 
106
97
  loop = asyncio.get_running_loop()
107
98
  password_is_valid = partial(
108
99
  is_valid_password, password=password, salt=salt, password_hash=password_hash
109
100
  )
110
101
  if not await loop.run_in_executor(None, password_is_valid):
111
- raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=LOGIN_FAILED_MESSAGE)
102
+ raise HTTPException(status_code=401, detail=LOGIN_FAILED_MESSAGE)
112
103
 
113
104
  access_token, refresh_token = await create_access_and_refresh_tokens(
114
105
  token_store=token_store,
@@ -116,7 +107,7 @@ async def login(request: Request) -> Response:
116
107
  access_token_expiry=access_token_expiry,
117
108
  refresh_token_expiry=refresh_token_expiry,
118
109
  )
119
- response = Response(status_code=HTTP_204_NO_CONTENT)
110
+ response = Response(status_code=204)
120
111
  response = set_access_token_cookie(
121
112
  response=response, access_token=access_token, max_age=access_token_expiry
122
113
  )
@@ -146,7 +137,7 @@ async def logout(
146
137
  await token_store.log_out(user_id)
147
138
  redirect_path = "/logout" if get_env_disable_basic_auth() else "/login"
148
139
  redirect_url = prepend_root_path(request.scope, redirect_path)
149
- response = Response(status_code=HTTP_302_FOUND, headers={"Location": redirect_url})
140
+ response = Response(status_code=302, headers={"Location": redirect_url})
150
141
  response = delete_access_token_cookie(response)
151
142
  response = delete_refresh_token_cookie(response)
152
143
  response = delete_oauth2_state_cookie(response)
@@ -159,7 +150,7 @@ async def refresh_tokens(request: Request) -> Response:
159
150
  assert isinstance(access_token_expiry := request.app.state.access_token_expiry, timedelta)
160
151
  assert isinstance(refresh_token_expiry := request.app.state.refresh_token_expiry, timedelta)
161
152
  if (refresh_token := request.cookies.get(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)) is None:
162
- raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Missing refresh token")
153
+ raise HTTPException(status_code=401, detail="Missing refresh token")
163
154
  token_store: TokenStore = request.app.state.get_token_store()
164
155
  refresh_token_claims = await token_store.read(Token(refresh_token))
165
156
  if (
@@ -169,9 +160,9 @@ async def refresh_tokens(request: Request) -> Response:
169
160
  or (user_id := int(refresh_token_claims.subject)) is None
170
161
  or (expiration_time := refresh_token_claims.expiration_time) is None
171
162
  ):
172
- raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
163
+ raise HTTPException(status_code=401, detail="Invalid refresh token")
173
164
  if expiration_time.timestamp() < datetime.now().timestamp():
174
- raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Expired refresh token")
165
+ raise HTTPException(status_code=401, detail="Expired refresh token")
175
166
  await token_store.revoke(refresh_token_id)
176
167
 
177
168
  if (
@@ -189,14 +180,14 @@ async def refresh_tokens(request: Request) -> Response:
189
180
  select(models.User).filter_by(id=user_id).options(joinedload(models.User.role))
190
181
  )
191
182
  ) is None:
192
- raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="User not found")
183
+ raise HTTPException(status_code=404, detail="User not found")
193
184
  access_token, refresh_token = await create_access_and_refresh_tokens(
194
185
  token_store=token_store,
195
186
  user=user,
196
187
  access_token_expiry=access_token_expiry,
197
188
  refresh_token_expiry=refresh_token_expiry,
198
189
  )
199
- response = Response(status_code=HTTP_204_NO_CONTENT)
190
+ response = Response(status_code=204)
200
191
  response = set_access_token_cookie(
201
192
  response=response, access_token=access_token, max_age=access_token_expiry
202
193
  )
@@ -209,7 +200,7 @@ async def refresh_tokens(request: Request) -> Response:
209
200
  @router.post("/password-reset-email")
210
201
  async def initiate_password_reset(request: Request) -> Response:
211
202
  if get_env_disable_basic_auth():
212
- raise HTTPException(status_code=HTTP_403_FORBIDDEN)
203
+ raise HTTPException(status_code=403)
213
204
  data = await request.json()
214
205
  if not (email := data.get("email")):
215
206
  raise MISSING_EMAIL
@@ -231,7 +222,7 @@ async def initiate_password_reset(request: Request) -> Response:
231
222
  )
232
223
  if user is None or user.auth_method != "LOCAL":
233
224
  # Withold privileged information
234
- return Response(status_code=HTTP_204_NO_CONTENT)
225
+ return Response(status_code=204)
235
226
  token_store: TokenStore = request.app.state.get_token_store()
236
227
  if user.password_reset_token:
237
228
  await token_store.revoke(PasswordResetTokenId(user.password_reset_token.id))
@@ -247,13 +238,13 @@ async def initiate_password_reset(request: Request) -> Response:
247
238
  components = (url.scheme, url.netloc, path, "", query_string, "")
248
239
  reset_url = urlunparse(components)
249
240
  await sender.send_password_reset_email(email, reset_url)
250
- return Response(status_code=HTTP_204_NO_CONTENT)
241
+ return Response(status_code=204)
251
242
 
252
243
 
253
244
  @router.post("/password-reset")
254
245
  async def reset_password(request: Request) -> Response:
255
246
  if get_env_disable_basic_auth():
256
- raise HTTPException(status_code=HTTP_403_FORBIDDEN)
247
+ raise HTTPException(status_code=403)
257
248
  data = await request.json()
258
249
  if not (password := data.get("password")):
259
250
  raise MISSING_PASSWORD
@@ -270,7 +261,7 @@ async def reset_password(request: Request) -> Response:
270
261
  user = await session.scalar(select(models.User).filter_by(id=int(user_id)))
271
262
  if user is None or user.auth_method != "LOCAL":
272
263
  # Withold privileged information
273
- return Response(status_code=HTTP_204_NO_CONTENT)
264
+ return Response(status_code=204)
274
265
  validate_password_format(password)
275
266
  user.password_salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
276
267
  loop = asyncio.get_running_loop()
@@ -281,7 +272,7 @@ async def reset_password(request: Request) -> Response:
281
272
  async with request.app.state.db() as session:
282
273
  session.add(user)
283
274
  await session.flush()
284
- response = Response(status_code=HTTP_204_NO_CONTENT)
275
+ response = Response(status_code=204)
285
276
  assert (token_id := claims.token_id)
286
277
  await token_store.revoke(token_id)
287
278
  await token_store.log_out(UserId(user.id))
@@ -291,18 +282,18 @@ async def reset_password(request: Request) -> Response:
291
282
  LOGIN_FAILED_MESSAGE = "Invalid email and/or password"
292
283
 
293
284
  MISSING_EMAIL = HTTPException(
294
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
285
+ status_code=422,
295
286
  detail="Email required",
296
287
  )
297
288
  MISSING_PASSWORD = HTTPException(
298
- status_code=HTTP_422_UNPROCESSABLE_ENTITY,
289
+ status_code=422,
299
290
  detail="Password required",
300
291
  )
301
292
  SMTP_UNAVAILABLE = HTTPException(
302
- status_code=HTTP_503_SERVICE_UNAVAILABLE,
293
+ status_code=503,
303
294
  detail="SMTP server not configured",
304
295
  )
305
296
  INVALID_TOKEN = HTTPException(
306
- status_code=HTTP_401_UNAUTHORIZED,
297
+ status_code=401,
307
298
  detail="Invalid token",
308
299
  )