arize-phoenix 4.36.0__py3-none-any.whl → 5.1.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 (81) hide show
  1. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.1.0.dist-info}/METADATA +10 -12
  2. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.1.0.dist-info}/RECORD +69 -60
  3. phoenix/__init__.py +86 -0
  4. phoenix/auth.py +275 -14
  5. phoenix/config.py +277 -25
  6. phoenix/db/enums.py +20 -0
  7. phoenix/db/facilitator.py +112 -0
  8. phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
  9. phoenix/db/models.py +145 -60
  10. phoenix/experiments/evaluators/code_evaluators.py +9 -3
  11. phoenix/experiments/functions.py +1 -4
  12. phoenix/server/api/README.md +28 -0
  13. phoenix/server/api/auth.py +32 -0
  14. phoenix/server/api/context.py +50 -2
  15. phoenix/server/api/dataloaders/__init__.py +4 -0
  16. phoenix/server/api/dataloaders/user_roles.py +30 -0
  17. phoenix/server/api/dataloaders/users.py +33 -0
  18. phoenix/server/api/exceptions.py +7 -0
  19. phoenix/server/api/mutations/__init__.py +0 -2
  20. phoenix/server/api/mutations/api_key_mutations.py +104 -86
  21. phoenix/server/api/mutations/dataset_mutations.py +8 -8
  22. phoenix/server/api/mutations/experiment_mutations.py +2 -2
  23. phoenix/server/api/mutations/export_events_mutations.py +3 -3
  24. phoenix/server/api/mutations/project_mutations.py +3 -3
  25. phoenix/server/api/mutations/span_annotations_mutations.py +4 -4
  26. phoenix/server/api/mutations/trace_annotations_mutations.py +4 -4
  27. phoenix/server/api/mutations/user_mutations.py +282 -42
  28. phoenix/server/api/openapi/schema.py +2 -2
  29. phoenix/server/api/queries.py +48 -39
  30. phoenix/server/api/routers/__init__.py +11 -0
  31. phoenix/server/api/routers/auth.py +284 -0
  32. phoenix/server/api/routers/embeddings.py +26 -0
  33. phoenix/server/api/routers/oauth2.py +456 -0
  34. phoenix/server/api/routers/v1/__init__.py +38 -16
  35. phoenix/server/api/types/ApiKey.py +11 -0
  36. phoenix/server/api/types/AuthMethod.py +9 -0
  37. phoenix/server/api/types/User.py +48 -4
  38. phoenix/server/api/types/UserApiKey.py +35 -1
  39. phoenix/server/api/types/UserRole.py +7 -0
  40. phoenix/server/app.py +103 -31
  41. phoenix/server/bearer_auth.py +161 -0
  42. phoenix/server/email/__init__.py +0 -0
  43. phoenix/server/email/sender.py +26 -0
  44. phoenix/server/email/templates/__init__.py +0 -0
  45. phoenix/server/email/templates/password_reset.html +19 -0
  46. phoenix/server/email/types.py +11 -0
  47. phoenix/server/grpc_server.py +6 -0
  48. phoenix/server/jwt_store.py +504 -0
  49. phoenix/server/main.py +40 -9
  50. phoenix/server/oauth2.py +51 -0
  51. phoenix/server/prometheus.py +20 -0
  52. phoenix/server/rate_limiters.py +191 -0
  53. phoenix/server/static/.vite/manifest.json +31 -31
  54. phoenix/server/static/assets/{components-Dte7_KRd.js → components-REunxTt6.js} +348 -286
  55. phoenix/server/static/assets/index-DAPJxlCw.js +101 -0
  56. phoenix/server/static/assets/{pages-CnTvEGEN.js → pages-1VrMk2pW.js} +559 -291
  57. phoenix/server/static/assets/{vendor-BC3OPQuM.js → vendor-B5IC0ivG.js} +5 -5
  58. phoenix/server/static/assets/{vendor-arizeai-NjB3cZzD.js → vendor-arizeai-aFbT4kl1.js} +2 -2
  59. phoenix/server/static/assets/{vendor-codemirror-gE_JCOgX.js → vendor-codemirror-BEGorXSV.js} +1 -1
  60. phoenix/server/static/assets/{vendor-recharts-BXLYwcXF.js → vendor-recharts-6nUU7gU_.js} +1 -1
  61. phoenix/server/templates/index.html +1 -0
  62. phoenix/server/types.py +157 -1
  63. phoenix/session/client.py +7 -2
  64. phoenix/trace/fixtures.py +24 -0
  65. phoenix/utilities/client.py +16 -0
  66. phoenix/version.py +1 -1
  67. phoenix/db/migrations/future_versions/README.md +0 -4
  68. phoenix/db/migrations/future_versions/cd164e83824f_users_and_tokens.py +0 -293
  69. phoenix/db/migrations/versions/.gitignore +0 -1
  70. phoenix/server/api/mutations/auth.py +0 -18
  71. phoenix/server/api/mutations/auth_mutations.py +0 -65
  72. phoenix/server/static/assets/index-fq1-hCK4.js +0 -100
  73. phoenix/trace/langchain/__init__.py +0 -3
  74. phoenix/trace/langchain/instrumentor.py +0 -34
  75. phoenix/trace/llama_index/__init__.py +0 -3
  76. phoenix/trace/llama_index/callback.py +0 -102
  77. phoenix/trace/openai/__init__.py +0 -3
  78. phoenix/trace/openai/instrumentor.py +0 -30
  79. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.1.0.dist-info}/WHEEL +0 -0
  80. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.1.0.dist-info}/licenses/IP_NOTICE +0 -0
  81. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,18 @@
1
+ from asyncio import get_running_loop
1
2
  from dataclasses import dataclass
3
+ from functools import cached_property, partial
2
4
  from pathlib import Path
3
- from typing import Any, Optional
5
+ from typing import Any, Optional, cast
4
6
 
7
+ from starlette.requests import Request as StarletteRequest
5
8
  from starlette.responses import Response as StarletteResponse
6
9
  from strawberry.fastapi import BaseContext
7
10
 
11
+ from phoenix.auth import (
12
+ compute_password_hash,
13
+ )
8
14
  from phoenix.core.model_schema import Model
15
+ from phoenix.db import models
9
16
  from phoenix.server.api.dataloaders import (
10
17
  AnnotationSummaryDataLoader,
11
18
  AverageExperimentRunLatencyDataLoader,
@@ -30,9 +37,18 @@ from phoenix.server.api.dataloaders import (
30
37
  SpanProjectsDataLoader,
31
38
  TokenCountDataLoader,
32
39
  TraceRowIdsDataLoader,
40
+ UserRolesDataLoader,
41
+ UsersDataLoader,
33
42
  )
43
+ from phoenix.server.bearer_auth import PhoenixUser
34
44
  from phoenix.server.dml_event import DmlEvent
35
- from phoenix.server.types import CanGetLastUpdatedAt, CanPutItem, DbSessionFactory
45
+ from phoenix.server.types import (
46
+ CanGetLastUpdatedAt,
47
+ CanPutItem,
48
+ DbSessionFactory,
49
+ TokenStore,
50
+ UserId,
51
+ )
36
52
 
37
53
 
38
54
  @dataclass
@@ -59,6 +75,8 @@ class DataLoaders:
59
75
  token_counts: TokenCountDataLoader
60
76
  trace_row_ids: TraceRowIdsDataLoader
61
77
  project_by_name: ProjectByNameDataLoader
78
+ users: UsersDataLoader
79
+ user_roles: UserRolesDataLoader
62
80
 
63
81
 
64
82
  class _NoOp:
@@ -77,7 +95,9 @@ class Context(BaseContext):
77
95
  event_queue: CanPutItem[DmlEvent] = _NoOp()
78
96
  corpus: Optional[Model] = None
79
97
  read_only: bool = False
98
+ auth_enabled: bool = False
80
99
  secret: Optional[str] = None
100
+ token_store: Optional[TokenStore] = None
81
101
 
82
102
  def get_secret(self) -> str:
83
103
  """A type-safe way to get the application secret. Throws an error if the secret is not set.
@@ -92,6 +112,14 @@ class Context(BaseContext):
92
112
  )
93
113
  return self.secret
94
114
 
115
+ def get_request(self) -> StarletteRequest:
116
+ """
117
+ A type-safe way to get the request object. Throws an error if the request is not set.
118
+ """
119
+ if not isinstance(request := self.request, StarletteRequest):
120
+ raise ValueError("no request is set")
121
+ return request
122
+
95
123
  def get_response(self) -> StarletteResponse:
96
124
  """
97
125
  A type-safe way to get the response object. Throws an error if the response is not set.
@@ -99,3 +127,23 @@ class Context(BaseContext):
99
127
  if (response := self.response) is None:
100
128
  raise ValueError("no response is set")
101
129
  return response
130
+
131
+ async def is_valid_password(self, password: str, user: models.User) -> bool:
132
+ return (
133
+ (hash_ := user.password_hash) is not None
134
+ and (salt := user.password_salt) is not None
135
+ and hash_ == await self.hash_password(password, salt)
136
+ )
137
+
138
+ @staticmethod
139
+ async def hash_password(password: str, salt: bytes) -> bytes:
140
+ compute = partial(compute_password_hash, password=password, salt=salt)
141
+ return await get_running_loop().run_in_executor(None, compute)
142
+
143
+ async def log_out(self, user_id: int) -> None:
144
+ assert self.token_store is not None
145
+ await self.token_store.log_out(UserId(user_id))
146
+
147
+ @cached_property
148
+ def user(self) -> PhoenixUser:
149
+ return cast(PhoenixUser, self.get_request().user)
@@ -25,6 +25,8 @@ from .span_descendants import SpanDescendantsDataLoader
25
25
  from .span_projects import SpanProjectsDataLoader
26
26
  from .token_counts import TokenCountCache, TokenCountDataLoader
27
27
  from .trace_row_ids import TraceRowIdsDataLoader
28
+ from .user_roles import UserRolesDataLoader
29
+ from .users import UsersDataLoader
28
30
 
29
31
  __all__ = [
30
32
  "CacheForDataLoaders",
@@ -50,6 +52,8 @@ __all__ = [
50
52
  "TraceRowIdsDataLoader",
51
53
  "ProjectByNameDataLoader",
52
54
  "SpanAnnotationsDataLoader",
55
+ "UsersDataLoader",
56
+ "UserRolesDataLoader",
53
57
  ]
54
58
 
55
59
 
@@ -0,0 +1,30 @@
1
+ from collections import defaultdict
2
+ from typing import DefaultDict, List, Optional
3
+
4
+ from sqlalchemy import select
5
+ from strawberry.dataloader import DataLoader
6
+ from typing_extensions import TypeAlias
7
+
8
+ from phoenix.db import models
9
+ from phoenix.server.types import DbSessionFactory
10
+
11
+ UserRoleId: TypeAlias = int
12
+ Key: TypeAlias = UserRoleId
13
+ Result: TypeAlias = Optional[models.UserRole]
14
+
15
+
16
+ class UserRolesDataLoader(DataLoader[Key, Result]):
17
+ """DataLoader that batches together user roles by their ids."""
18
+
19
+ def __init__(self, db: DbSessionFactory) -> None:
20
+ super().__init__(load_fn=self._load_fn)
21
+ self._db = db
22
+
23
+ async def _load_fn(self, keys: List[Key]) -> List[Result]:
24
+ user_roles_by_id: DefaultDict[Key, Result] = defaultdict(None)
25
+ async with self._db() as session:
26
+ data = await session.stream_scalars(select(models.UserRole))
27
+ async for user_role in data:
28
+ user_roles_by_id[user_role.id] = user_role
29
+
30
+ return [user_roles_by_id.get(role_id) for role_id in keys]
@@ -0,0 +1,33 @@
1
+ from collections import defaultdict
2
+ from typing import DefaultDict, List, Optional
3
+
4
+ from sqlalchemy import select
5
+ from strawberry.dataloader import DataLoader
6
+ from typing_extensions import TypeAlias
7
+
8
+ from phoenix.db import models
9
+ from phoenix.server.types import DbSessionFactory
10
+
11
+ UserId: TypeAlias = int
12
+ Key: TypeAlias = UserId
13
+ Result: TypeAlias = Optional[models.User]
14
+
15
+
16
+ class UsersDataLoader(DataLoader[Key, Result]):
17
+ """DataLoader that batches together users by their ids."""
18
+
19
+ def __init__(self, db: DbSessionFactory) -> None:
20
+ super().__init__(load_fn=self._load_fn)
21
+ self._db = db
22
+
23
+ async def _load_fn(self, keys: List[Key]) -> List[Result]:
24
+ user_ids = list(set(keys))
25
+ users_by_id: DefaultDict[Key, Result] = defaultdict(None)
26
+ async with self._db() as session:
27
+ data = await session.stream_scalars(
28
+ select(models.User).where(models.User.id.in_(user_ids))
29
+ )
30
+ async for user in data:
31
+ users_by_id[user.id] = user
32
+
33
+ return [users_by_id.get(user_id) for user_id in keys]
@@ -27,6 +27,13 @@ class Unauthorized(CustomGraphQLError):
27
27
  """
28
28
 
29
29
 
30
+ class Conflict(CustomGraphQLError):
31
+ """
32
+ An error raised when a mutation cannot be completed due to a conflict with
33
+ the current state of one or more resources.
34
+ """
35
+
36
+
30
37
  def get_mask_errors_extension() -> MaskErrors:
31
38
  return MaskErrors(
32
39
  should_mask_error=_should_mask_error,
@@ -1,7 +1,6 @@
1
1
  import strawberry
2
2
 
3
3
  from phoenix.server.api.mutations.api_key_mutations import ApiKeyMutationMixin
4
- from phoenix.server.api.mutations.auth_mutations import AuthMutationMixin
5
4
  from phoenix.server.api.mutations.dataset_mutations import DatasetMutationMixin
6
5
  from phoenix.server.api.mutations.experiment_mutations import ExperimentMutationMixin
7
6
  from phoenix.server.api.mutations.export_events_mutations import ExportEventsMutationMixin
@@ -14,7 +13,6 @@ from phoenix.server.api.mutations.user_mutations import UserMutationMixin
14
13
  @strawberry.type
15
14
  class Mutation(
16
15
  ApiKeyMutationMixin,
17
- AuthMutationMixin,
18
16
  DatasetMutationMixin,
19
17
  ExperimentMutationMixin,
20
18
  ExportEventsMutationMixin,
@@ -1,20 +1,22 @@
1
- from datetime import datetime
2
- from typing import Any, Dict, Optional
1
+ from datetime import datetime, timezone
2
+ from typing import Optional
3
3
 
4
- import jwt
5
4
  import strawberry
6
- from sqlalchemy import insert, select
5
+ from sqlalchemy import select
7
6
  from strawberry import UNSET
8
7
  from strawberry.relay import GlobalID
9
8
  from strawberry.types import Info
10
9
 
11
- from phoenix.db import models
10
+ from phoenix.db import enums, models
11
+ from phoenix.server.api.auth import IsAdmin, IsNotReadOnly
12
12
  from phoenix.server.api.context import Context
13
- from phoenix.server.api.exceptions import NotFound
14
- from phoenix.server.api.mutations.auth import HasSecret, IsAuthenticated
13
+ from phoenix.server.api.exceptions import Unauthorized
15
14
  from phoenix.server.api.queries import Query
16
15
  from phoenix.server.api.types.node import from_global_id_with_expected_type
17
16
  from phoenix.server.api.types.SystemApiKey import SystemApiKey
17
+ from phoenix.server.api.types.UserApiKey import UserApiKey
18
+ from phoenix.server.bearer_auth import PhoenixUser
19
+ from phoenix.server.types import ApiKeyAttributes, ApiKeyClaims, ApiKeyId, UserId
18
20
 
19
21
 
20
22
  @strawberry.type
@@ -31,119 +33,135 @@ class CreateApiKeyInput:
31
33
  expires_at: Optional[datetime] = UNSET
32
34
 
33
35
 
36
+ @strawberry.type
37
+ class CreateUserApiKeyMutationPayload:
38
+ jwt: str
39
+ api_key: UserApiKey
40
+ query: Query
41
+
42
+
43
+ @strawberry.input
44
+ class CreateUserApiKeyInput:
45
+ name: str
46
+ description: Optional[str] = UNSET
47
+ expires_at: Optional[datetime] = UNSET
48
+
49
+
34
50
  @strawberry.input
35
51
  class DeleteApiKeyInput:
36
52
  id: GlobalID
37
53
 
38
54
 
39
55
  @strawberry.type
40
- class DeleteSystemApiKeyMutationPayload:
41
- id: GlobalID
56
+ class DeleteApiKeyMutationPayload:
57
+ apiKeyId: GlobalID
42
58
  query: Query
43
59
 
44
60
 
45
61
  @strawberry.type
46
62
  class ApiKeyMutationMixin:
47
- @strawberry.mutation(permission_classes=[HasSecret, IsAuthenticated]) # type: ignore
63
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin]) # type: ignore
48
64
  async def create_system_api_key(
49
65
  self, info: Info[Context, None], input: CreateApiKeyInput
50
66
  ) -> CreateSystemApiKeyMutationPayload:
51
- # TODO(auth): safe guard against auth being disabled and secret not being set
67
+ assert (token_store := info.context.token_store) is not None
68
+ user_role = enums.UserRole.SYSTEM
52
69
  async with info.context.db() as session:
53
70
  # Get the system user - note this could be pushed into a dataloader
54
71
  system_user = await session.scalar(
55
72
  select(models.User)
56
73
  .join(models.UserRole) # Join User with UserRole
57
- .where(models.UserRole.name == "SYSTEM") # Filter where role is SYSTEM
74
+ .where(models.UserRole.name == user_role.value) # Filter where role is SYSTEM
75
+ .order_by(models.User.id)
58
76
  .limit(1)
59
77
  )
60
78
  if system_user is None:
61
79
  raise ValueError("System user not found")
62
-
63
- insert_stmt = (
64
- insert(models.APIKey)
65
- .values(
66
- user_id=system_user.id,
67
- name=input.name,
68
- description=input.description or None,
69
- expires_at=input.expires_at or None,
70
- )
71
- .returning(models.APIKey)
72
- )
73
- api_key = await session.scalar(insert_stmt)
74
- assert api_key is not None
75
-
76
- encoded_jwt = create_jwt(
77
- secret=info.context.get_secret(),
78
- name=api_key.name,
79
- id=api_key.id,
80
- description=api_key.description,
81
- iat=api_key.created_at,
82
- exp=api_key.expires_at,
80
+ issued_at = datetime.now(timezone.utc)
81
+ claims = ApiKeyClaims(
82
+ subject=UserId(system_user.id),
83
+ issued_at=issued_at,
84
+ expiration_time=input.expires_at or None,
85
+ attributes=ApiKeyAttributes(
86
+ user_role=user_role,
87
+ name=input.name,
88
+ description=input.description,
89
+ ),
83
90
  )
91
+ token, token_id = await token_store.create_api_key(claims)
84
92
  return CreateSystemApiKeyMutationPayload(
85
- jwt=encoded_jwt,
93
+ jwt=token,
86
94
  api_key=SystemApiKey(
87
- id_attr=api_key.id,
88
- name=api_key.name,
89
- description=api_key.description,
90
- created_at=api_key.created_at,
91
- expires_at=api_key.expires_at,
95
+ id_attr=int(token_id),
96
+ name=input.name,
97
+ description=input.description or None,
98
+ created_at=issued_at,
99
+ expires_at=input.expires_at or None,
92
100
  ),
93
101
  query=Query(),
94
102
  )
95
103
 
96
- @strawberry.mutation(permission_classes=[HasSecret, IsAuthenticated]) # type: ignore
104
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
105
+ async def create_user_api_key(
106
+ self, info: Info[Context, None], input: CreateUserApiKeyInput
107
+ ) -> CreateUserApiKeyMutationPayload:
108
+ assert (token_store := info.context.token_store) is not None
109
+ try:
110
+ user = info.context.request.user # type: ignore
111
+ assert isinstance(user, PhoenixUser)
112
+ except AttributeError:
113
+ raise ValueError("User not found")
114
+ issued_at = datetime.now(timezone.utc)
115
+ claims = ApiKeyClaims(
116
+ subject=user.identity,
117
+ issued_at=issued_at,
118
+ expiration_time=input.expires_at or None,
119
+ attributes=ApiKeyAttributes(
120
+ user_role=enums.UserRole.MEMBER,
121
+ name=input.name,
122
+ description=input.description,
123
+ ),
124
+ )
125
+ token, token_id = await token_store.create_api_key(claims)
126
+ return CreateUserApiKeyMutationPayload(
127
+ jwt=token,
128
+ api_key=UserApiKey(
129
+ id_attr=int(token_id),
130
+ name=input.name,
131
+ description=input.description or None,
132
+ created_at=issued_at,
133
+ expires_at=input.expires_at or None,
134
+ user_id=int(user.identity),
135
+ ),
136
+ query=Query(),
137
+ )
138
+
139
+ @strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin]) # type: ignore
97
140
  async def delete_system_api_key(
98
141
  self, info: Info[Context, None], input: DeleteApiKeyInput
99
- ) -> DeleteSystemApiKeyMutationPayload:
142
+ ) -> DeleteApiKeyMutationPayload:
143
+ assert (token_store := info.context.token_store) is not None
100
144
  api_key_id = from_global_id_with_expected_type(
101
145
  input.id, expected_type_name=SystemApiKey.__name__
102
146
  )
147
+ await token_store.revoke(ApiKeyId(api_key_id))
148
+ return DeleteApiKeyMutationPayload(apiKeyId=input.id, query=Query())
149
+
150
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
151
+ async def delete_user_api_key(
152
+ self, info: Info[Context, None], input: DeleteApiKeyInput
153
+ ) -> DeleteApiKeyMutationPayload:
154
+ assert (token_store := info.context.token_store) is not None
155
+ api_key_id = from_global_id_with_expected_type(
156
+ input.id, expected_type_name=UserApiKey.__name__
157
+ )
103
158
  async with info.context.db() as session:
104
- api_key = await session.get(models.APIKey, api_key_id)
159
+ api_key = await session.scalar(
160
+ select(models.ApiKey).where(models.ApiKey.id == api_key_id)
161
+ )
105
162
  if api_key is None:
106
- raise NotFound(f"Unknown System API Key: {input.id}")
107
-
108
- await session.delete(api_key)
109
-
110
- return DeleteSystemApiKeyMutationPayload(id=input.id, query=Query())
111
-
112
-
113
- def create_jwt(
114
- *,
115
- secret: str,
116
- algorithm: str = "HS256",
117
- name: str,
118
- description: Optional[str],
119
- iat: datetime,
120
- exp: Optional[datetime],
121
- id: int,
122
- ) -> str:
123
- """Create a signed JSON Web Token for authentication
124
-
125
- Args:
126
- secret (str): the secret to sign with
127
- name (str): name of the key / token
128
- description (Optional[str]): description of the token
129
- iat (datetime): the issued at time
130
- exp (Optional[datetime]): the expiry, if set
131
- id (int): the id of the key
132
- algorithm (str, optional): the algorithm to use. Defaults to "HS256".
133
-
134
- Returns:
135
- str: The encoded JWT
136
- """
137
- payload: Dict[str, Any] = {
138
- "name": name,
139
- "description": description,
140
- "iat": iat.utcnow(),
141
- "id": id,
142
- }
143
- if exp is not None:
144
- payload["exp"] = exp.utcnow()
145
-
146
- # Encode the payload to create the JWT
147
- token = jwt.encode(payload, secret, algorithm=algorithm)
148
-
149
- return token
163
+ raise ValueError(f"API key with id {input.id} not found")
164
+ if int((user := info.context.user).identity) != api_key.user_id and not user.is_admin:
165
+ raise Unauthorized("User not authorized to delete")
166
+ await token_store.revoke(ApiKeyId(api_key_id))
167
+ return DeleteApiKeyMutationPayload(apiKeyId=input.id, query=Query())
@@ -12,6 +12,7 @@ from strawberry.types import Info
12
12
 
13
13
  from phoenix.db import models
14
14
  from phoenix.db.helpers import get_eval_trace_ids_for_datasets, get_project_names_for_datasets
15
+ from phoenix.server.api.auth import IsNotReadOnly
15
16
  from phoenix.server.api.context import Context
16
17
  from phoenix.server.api.exceptions import BadRequest, NotFound
17
18
  from phoenix.server.api.helpers.dataset_helpers import (
@@ -28,7 +29,6 @@ from phoenix.server.api.input_types.PatchDatasetExamplesInput import (
28
29
  PatchDatasetExamplesInput,
29
30
  )
30
31
  from phoenix.server.api.input_types.PatchDatasetInput import PatchDatasetInput
31
- from phoenix.server.api.mutations.auth import IsAuthenticated
32
32
  from phoenix.server.api.types.Dataset import Dataset, to_gql_dataset
33
33
  from phoenix.server.api.types.DatasetExample import DatasetExample
34
34
  from phoenix.server.api.types.node import from_global_id_with_expected_type
@@ -44,7 +44,7 @@ class DatasetMutationPayload:
44
44
 
45
45
  @strawberry.type
46
46
  class DatasetMutationMixin:
47
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
47
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
48
48
  async def create_dataset(
49
49
  self,
50
50
  info: Info[Context, None],
@@ -67,7 +67,7 @@ class DatasetMutationMixin:
67
67
  info.context.event_queue.put(DatasetInsertEvent((dataset.id,)))
68
68
  return DatasetMutationPayload(dataset=to_gql_dataset(dataset))
69
69
 
70
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
70
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
71
71
  async def patch_dataset(
72
72
  self,
73
73
  info: Info[Context, None],
@@ -96,7 +96,7 @@ class DatasetMutationMixin:
96
96
  info.context.event_queue.put(DatasetInsertEvent((dataset.id,)))
97
97
  return DatasetMutationPayload(dataset=to_gql_dataset(dataset))
98
98
 
99
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
99
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
100
100
  async def add_spans_to_dataset(
101
101
  self,
102
102
  info: Info[Context, None],
@@ -225,7 +225,7 @@ class DatasetMutationMixin:
225
225
  info.context.event_queue.put(DatasetInsertEvent((dataset.id,)))
226
226
  return DatasetMutationPayload(dataset=to_gql_dataset(dataset))
227
227
 
228
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
228
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
229
229
  async def add_examples_to_dataset(
230
230
  self, info: Info[Context, None], input: AddExamplesToDatasetInput
231
231
  ) -> DatasetMutationPayload:
@@ -351,7 +351,7 @@ class DatasetMutationMixin:
351
351
  info.context.event_queue.put(DatasetInsertEvent((dataset.id,)))
352
352
  return DatasetMutationPayload(dataset=to_gql_dataset(dataset))
353
353
 
354
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
354
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
355
355
  async def delete_dataset(
356
356
  self,
357
357
  info: Info[Context, None],
@@ -382,7 +382,7 @@ class DatasetMutationMixin:
382
382
  info.context.event_queue.put(DatasetDeleteEvent((dataset.id,)))
383
383
  return DatasetMutationPayload(dataset=to_gql_dataset(dataset))
384
384
 
385
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
385
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
386
386
  async def patch_dataset_examples(
387
387
  self,
388
388
  info: Info[Context, None],
@@ -474,7 +474,7 @@ class DatasetMutationMixin:
474
474
  info.context.event_queue.put(DatasetInsertEvent((dataset.id,)))
475
475
  return DatasetMutationPayload(dataset=to_gql_dataset(dataset))
476
476
 
477
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
477
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
478
478
  async def delete_dataset_examples(
479
479
  self, info: Info[Context, None], input: DeleteDatasetExamplesInput
480
480
  ) -> DatasetMutationPayload:
@@ -8,10 +8,10 @@ from strawberry.types import Info
8
8
 
9
9
  from phoenix.db import models
10
10
  from phoenix.db.helpers import get_eval_trace_ids_for_experiments, get_project_names_for_experiments
11
+ from phoenix.server.api.auth import IsNotReadOnly
11
12
  from phoenix.server.api.context import Context
12
13
  from phoenix.server.api.exceptions import CustomGraphQLError
13
14
  from phoenix.server.api.input_types.DeleteExperimentsInput import DeleteExperimentsInput
14
- from phoenix.server.api.mutations.auth import IsAuthenticated
15
15
  from phoenix.server.api.types.Experiment import Experiment, to_gql_experiment
16
16
  from phoenix.server.api.types.node import from_global_id_with_expected_type
17
17
  from phoenix.server.api.utils import delete_projects, delete_traces
@@ -25,7 +25,7 @@ class ExperimentMutationPayload:
25
25
 
26
26
  @strawberry.type
27
27
  class ExperimentMutationMixin:
28
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
28
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
29
29
  async def delete_experiments(
30
30
  self,
31
31
  info: Info[Context, None],
@@ -8,9 +8,9 @@ from strawberry import ID, UNSET
8
8
  from strawberry.types import Info
9
9
 
10
10
  import phoenix.core.model_schema as ms
11
+ from phoenix.server.api.auth import IsNotReadOnly
11
12
  from phoenix.server.api.context import Context
12
13
  from phoenix.server.api.input_types.ClusterInput import ClusterInput
13
- from phoenix.server.api.mutations.auth import IsAuthenticated
14
14
  from phoenix.server.api.types.Event import parse_event_ids_by_inferences_role, unpack_event_id
15
15
  from phoenix.server.api.types.ExportedFile import ExportedFile
16
16
  from phoenix.server.api.types.InferencesRole import AncillaryInferencesRole, InferencesRole
@@ -19,7 +19,7 @@ from phoenix.server.api.types.InferencesRole import AncillaryInferencesRole, Inf
19
19
  @strawberry.type
20
20
  class ExportEventsMutationMixin:
21
21
  @strawberry.mutation(
22
- permission_classes=[IsAuthenticated],
22
+ permission_classes=[IsNotReadOnly],
23
23
  description=(
24
24
  "Given a list of event ids, export the corresponding data subset in Parquet format."
25
25
  " File name is optional, but if specified, should be without file extension. By default"
@@ -51,7 +51,7 @@ class ExportEventsMutationMixin:
51
51
  return ExportedFile(file_name=file_name)
52
52
 
53
53
  @strawberry.mutation(
54
- permission_classes=[IsAuthenticated],
54
+ permission_classes=[IsNotReadOnly],
55
55
  description=(
56
56
  "Given a list of clusters, export the corresponding data subset in Parquet format."
57
57
  " File name is optional, but if specified, should be without file extension. By default"
@@ -6,9 +6,9 @@ from strawberry.types import Info
6
6
 
7
7
  from phoenix.config import DEFAULT_PROJECT_NAME
8
8
  from phoenix.db import models
9
+ from phoenix.server.api.auth import IsNotReadOnly
9
10
  from phoenix.server.api.context import Context
10
11
  from phoenix.server.api.input_types.ClearProjectInput import ClearProjectInput
11
- from phoenix.server.api.mutations.auth import IsAuthenticated
12
12
  from phoenix.server.api.queries import Query
13
13
  from phoenix.server.api.types.node import from_global_id_with_expected_type
14
14
  from phoenix.server.dml_event import ProjectDeleteEvent, SpanDeleteEvent
@@ -16,7 +16,7 @@ from phoenix.server.dml_event import ProjectDeleteEvent, SpanDeleteEvent
16
16
 
17
17
  @strawberry.type
18
18
  class ProjectMutationMixin:
19
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
19
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
20
20
  async def delete_project(self, info: Info[Context, None], id: GlobalID) -> Query:
21
21
  project_id = from_global_id_with_expected_type(global_id=id, expected_type_name="Project")
22
22
  async with info.context.db() as session:
@@ -33,7 +33,7 @@ class ProjectMutationMixin:
33
33
  info.context.event_queue.put(ProjectDeleteEvent((project_id,)))
34
34
  return Query()
35
35
 
36
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
36
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
37
37
  async def clear_project(self, info: Info[Context, None], input: ClearProjectInput) -> Query:
38
38
  project_id = from_global_id_with_expected_type(
39
39
  global_id=input.id, expected_type_name="Project"
@@ -6,11 +6,11 @@ from strawberry import UNSET
6
6
  from strawberry.types import Info
7
7
 
8
8
  from phoenix.db import models
9
+ from phoenix.server.api.auth import IsNotReadOnly
9
10
  from phoenix.server.api.context import Context
10
11
  from phoenix.server.api.input_types.CreateSpanAnnotationInput import CreateSpanAnnotationInput
11
12
  from phoenix.server.api.input_types.DeleteAnnotationsInput import DeleteAnnotationsInput
12
13
  from phoenix.server.api.input_types.PatchAnnotationInput import PatchAnnotationInput
13
- from phoenix.server.api.mutations.auth import IsAuthenticated
14
14
  from phoenix.server.api.queries import Query
15
15
  from phoenix.server.api.types.node import from_global_id_with_expected_type
16
16
  from phoenix.server.api.types.SpanAnnotation import SpanAnnotation, to_gql_span_annotation
@@ -25,7 +25,7 @@ class SpanAnnotationMutationPayload:
25
25
 
26
26
  @strawberry.type
27
27
  class SpanAnnotationMutationMixin:
28
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
28
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
29
29
  async def create_span_annotations(
30
30
  self, info: Info[Context, None], input: List[CreateSpanAnnotationInput]
31
31
  ) -> SpanAnnotationMutationPayload:
@@ -59,7 +59,7 @@ class SpanAnnotationMutationMixin:
59
59
  query=Query(),
60
60
  )
61
61
 
62
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
62
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
63
63
  async def patch_span_annotations(
64
64
  self, info: Info[Context, None], input: List[PatchAnnotationInput]
65
65
  ) -> SpanAnnotationMutationPayload:
@@ -99,7 +99,7 @@ class SpanAnnotationMutationMixin:
99
99
  info.context.event_queue.put(SpanAnnotationInsertEvent((span_annotation.id,)))
100
100
  return SpanAnnotationMutationPayload(span_annotations=patched_annotations, query=Query())
101
101
 
102
- @strawberry.mutation(permission_classes=[IsAuthenticated]) # type: ignore
102
+ @strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
103
103
  async def delete_span_annotations(
104
104
  self, info: Info[Context, None], input: DeleteAnnotationsInput
105
105
  ) -> SpanAnnotationMutationPayload: