arize-phoenix 4.20.2__py3-none-any.whl → 4.22.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.20.2
3
+ Version: 4.22.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
@@ -1,11 +1,11 @@
1
1
  phoenix/__init__.py,sha256=TGNWqm2UW-l67yIRpOtmqGHVAmdoobSNqUsiTtip7uQ,1542
2
- phoenix/config.py,sha256=eXciIho_PDh4ZSmq4Gtuo7Qz__yTluDP3_WUwig5OiU,8141
2
+ phoenix/config.py,sha256=wYA_8GSSz5rnpfIWDjeBL9ehKuTy9jqXaMZnxUqRYEU,10131
3
3
  phoenix/datetime_utils.py,sha256=yDKjwX2Vtqw9h5F_ProtP-TsXidM43uIvmJ_pOzYc9A,3405
4
4
  phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
5
5
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
6
  phoenix/services.py,sha256=aTxhcOA1pZHB6U-B3TEcp6fqDF5oT0xCUvEUNMZVTUQ,5175
7
7
  phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
8
- phoenix/version.py,sha256=hh55rhGJPmETx024BaT0pa35ouf8-J2B1_CJUXY4XFg,23
8
+ phoenix/version.py,sha256=QzEW4rw3knzx0cHKAbjoAdUuGoTl82VIDrnWP2UhHw0,23
9
9
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
11
11
  phoenix/core/model.py,sha256=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
@@ -18,7 +18,7 @@ phoenix/db/bulk_inserter.py,sha256=qgg8pt5k4VnHKOE0-KoReXVAfXRhLt-sMZihI-b4X9I,1
18
18
  phoenix/db/engines.py,sha256=R3btYTSOSd6BwRA59EmhhojL0HCQ7NnzFIXQrPYS0iU,4812
19
19
  phoenix/db/helpers.py,sha256=2zSc4n5IJfu-CaOFoBfqTB35M1nTFcAc8tqLsNtF2Jw,3488
20
20
  phoenix/db/migrate.py,sha256=MuhtNWnR24riROvarvKfbRb4_D5xuQi6P760vBUKl1E,2270
21
- phoenix/db/models.py,sha256=1fSwI9NjXcVv7PT40VEeKGGI13TWbqzrYfIemXV-DYY,20837
21
+ phoenix/db/models.py,sha256=C_s1TAgMRi2eExc48NwInErsfSzVhNZtj4ow23jUKSc,23352
22
22
  phoenix/db/insertion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  phoenix/db/insertion/constants.py,sha256=8wifm7X-1XvroZ__R2Gc96NsgLhTDn0zXl4lehlXtcA,70
24
24
  phoenix/db/insertion/dataset.py,sha256=_vxy5e6W5jEuvO2fMKbbNCn9JvHkwI4LRKk_10eKFVg,7171
@@ -32,6 +32,9 @@ phoenix/db/insertion/types.py,sha256=nQYYnpzcPxj2kdUoXfKE8ilOKlx1zpKLPc40OGuBlfk
32
32
  phoenix/db/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  phoenix/db/migrations/env.py,sha256=QbzB5zrRs6XQQmrYeUpuzeilcMlM-MsbaAgHHYcIHTI,3626
34
34
  phoenix/db/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
35
+ phoenix/db/migrations/future_versions/README.md,sha256=3QtDx40SAD-IITjbdlKR2N_CBxT5y37C1OQs05EDt7o,184
36
+ phoenix/db/migrations/future_versions/cd164e83824f_users_and_tokens.py,sha256=L3UlrrT9_mgIakkjRl4MfSHd5dNmZi0D0jizi5V4dTI,8795
37
+ phoenix/db/migrations/versions/.gitignore,sha256=chLdMrfkICZvLY7lCEcuqF32sVp61Jml4PodFryEU94,33
35
38
  phoenix/db/migrations/versions/10460e46d750_datasets.py,sha256=eZAyz720DmpOd7RnuxDN2dVNXVuMrdlCA7eAOxyMtfs,8695
36
39
  phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py,sha256=x6oKFwn7Zmite4G0trDQPpMCn0I7jejuBcN3-ivEuDg,3938
37
40
  phoenix/db/migrations/versions/cf03bd6bae1d_init.py,sha256=09cpofqje8zi4eQFfUn-i21x7VcsUYOfLKKUlrtKrGc,8662
@@ -227,13 +230,13 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
227
230
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
228
231
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
229
232
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
230
- phoenix/server/static/.vite/manifest.json,sha256=f_O0wfSXGRjeXvLiAE1oNla0ADk3X6PTSnCeHtd4xB8,1929
231
- phoenix/server/static/assets/components-BSw2e1Zr.js,sha256=nvfZM0lYZkeOklzR8VLMikEgIOTpb2jNpF8b9IMMcps,185927
232
- phoenix/server/static/assets/index-BYUFcdtx.js,sha256=bYcfXq3huGgFkCR0SX2680A1MbACjpdxd5MIbMcOIyY,7362
233
- phoenix/server/static/assets/pages-p_fuED5k.js,sha256=MA3xlc64hC6bRMJRcXZ1v1sWZdsngaU8bcejbho402A,449363
233
+ phoenix/server/static/.vite/manifest.json,sha256=HOOexYONSka_hn_J9xhFRtKsvGXqttAcXqmC1C9uSLI,1929
234
+ phoenix/server/static/assets/components-Bhx3QVW0.js,sha256=5UWD5GSontt9-H7IMs7lRbds6I4RyCbeRwFJRM52DGw,187118
235
+ phoenix/server/static/assets/index-CZg-95kd.js,sha256=NOPlnhvT31fNg9srw7-kvKfoBLFqVbwlTvUrYKYIYeQ,7362
236
+ phoenix/server/static/assets/pages-DG-5zgoV.js,sha256=m2FGLIvrM7I3uweAC_5YcXZfmdTI3mVeVCgLCcB23B0,452737
234
237
  phoenix/server/static/assets/vendor-BMWfu6zp.js,sha256=AAVTM5SjGUI_CmAWFUFmhpp5VDhvCD-MrEoh-pXXADY,1355423
235
238
  phoenix/server/static/assets/vendor-DxkFTwjz.css,sha256=nZrkr0u6NNElFGvpWHk9GTHeGoibCXCli1bE7mXZGZg,1816
236
- phoenix/server/static/assets/vendor-arizeai-CIETbKDq.js,sha256=JYd0o3cIY0av7lzU5LT5GDMMhmMPE3FbbGjtlhKBhLo,304008
239
+ phoenix/server/static/assets/vendor-arizeai-Sj74jm5V.js,sha256=9lD4YeMt5WtyfrqIApcH9WFQxyJJUtth0syWabkzX-I,304008
237
240
  phoenix/server/static/assets/vendor-codemirror-DO3VqEcD.js,sha256=M7t6xd6WpgKes25OOeGyxT1MU1dDrEKdmUBHgy5zslw,503031
238
241
  phoenix/server/static/assets/vendor-recharts-BGN0SxgJ.js,sha256=L9LAYSjuf0GHh1_PQh9bF4l9euWCDVQcnQN1RgMDMBw,282859
239
242
  phoenix/server/static/assets/vendor-three-DwGkEfCM.js,sha256=0D12ZgKzfKCTSdSTKJBFR2RZO_xxeMXrqDp0AszZqHY,620972
@@ -281,8 +284,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
281
284
  phoenix/utilities/project.py,sha256=8IJuMM4yUMoooPi37sictGj8Etu9rGmq6RFtc9848cQ,436
282
285
  phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
283
286
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
284
- arize_phoenix-4.20.2.dist-info/METADATA,sha256=DAffUocQNJm0BONMNSzCucpDNIILl9e2JIdARb3As3c,11902
285
- arize_phoenix-4.20.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
286
- arize_phoenix-4.20.2.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
287
- arize_phoenix-4.20.2.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
288
- arize_phoenix-4.20.2.dist-info/RECORD,,
287
+ arize_phoenix-4.22.0.dist-info/METADATA,sha256=knv747RowfHFOmv5WqkavBVG6KBadXQ8kLol_iYJK-Q,11902
288
+ arize_phoenix-4.22.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
289
+ arize_phoenix-4.22.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
290
+ arize_phoenix-4.22.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
291
+ arize_phoenix-4.22.0.dist-info/RECORD,,
phoenix/config.py CHANGED
@@ -2,7 +2,7 @@ import os
2
2
  import tempfile
3
3
  from logging import getLogger
4
4
  from pathlib import Path
5
- from typing import Dict, List, Optional
5
+ from typing import Dict, List, Optional, Tuple
6
6
 
7
7
  from .utilities.re import parse_env_headers
8
8
 
@@ -60,6 +60,11 @@ ENV_PHOENIX_SERVER_INSTRUMENTATION_OTLP_TRACE_COLLECTOR_GRPC_ENDPOINT = (
60
60
  "PHOENIX_SERVER_INSTRUMENTATION_OTLP_TRACE_COLLECTOR_GRPC_ENDPOINT"
61
61
  )
62
62
 
63
+ # Auth is under active development. Phoenix users are strongly advised not to
64
+ # set these environment variables until the feature is officially released.
65
+ ENV_DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH = "DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH"
66
+ ENV_DANGEROUSLY_SET_PHOENIX_SECRET = "DANGEROUSLY_SET_PHOENIX_SECRET"
67
+
63
68
 
64
69
  def server_instrumentation_is_enabled() -> bool:
65
70
  return bool(
@@ -100,6 +105,57 @@ def get_working_dir() -> Path:
100
105
  return Path.home().resolve() / ".phoenix"
101
106
 
102
107
 
108
+ def get_boolean_env_var(env_var: str) -> Optional[bool]:
109
+ """
110
+ Parses a boolean environment variable, returning None if the variable is not set.
111
+ """
112
+ if (value := os.environ.get(env_var)) is None:
113
+ return None
114
+ assert (lower := value.lower()) in (
115
+ "true",
116
+ "false",
117
+ ), f"{env_var} must be set to TRUE or FALSE (case-insensitive)"
118
+ return lower == "true"
119
+
120
+
121
+ def get_env_enable_auth() -> bool:
122
+ """
123
+ Gets the value of the DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH environment variable.
124
+ """
125
+ return get_boolean_env_var(ENV_DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH) is True
126
+
127
+
128
+ def get_env_phoenix_secret() -> Optional[str]:
129
+ """
130
+ Gets the value of the DANGEROUSLY_SET_PHOENIX_SECRET environment variable
131
+ and performs validation.
132
+ """
133
+ phoenix_secret = os.environ.get(ENV_DANGEROUSLY_SET_PHOENIX_SECRET)
134
+ if phoenix_secret is None:
135
+ return None
136
+ # todo: add validation for the phoenix secret
137
+ return phoenix_secret
138
+
139
+
140
+ def get_auth_settings() -> Tuple[bool, Optional[str]]:
141
+ """
142
+ Gets auth settings and performs validation.
143
+ """
144
+ enable_auth = get_env_enable_auth()
145
+ phoenix_secret = get_env_phoenix_secret()
146
+ if enable_auth:
147
+ assert phoenix_secret, (
148
+ "DANGEROUSLY_SET_PHOENIX_SECRET must be set "
149
+ "when auth is enabled with DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH"
150
+ )
151
+ else:
152
+ assert not phoenix_secret, (
153
+ "DANGEROUSLY_SET_PHOENIX_SECRET cannot be set "
154
+ "unless auth is enabled with DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH"
155
+ )
156
+ return enable_auth, phoenix_secret
157
+
158
+
103
159
  PHOENIX_DIR = Path(__file__).resolve().parent
104
160
  # Server config
105
161
  SERVER_DIR = PHOENIX_DIR / "server"
@@ -123,6 +179,8 @@ EXPORT_DIR = ROOT_DIR / "exports"
123
179
  INFERENCES_DIR = ROOT_DIR / "inferences"
124
180
  TRACE_DATASETS_DIR = ROOT_DIR / "trace_datasets"
125
181
 
182
+ ENABLE_AUTH, PHOENIX_SECRET = get_auth_settings()
183
+
126
184
 
127
185
  def ensure_working_dir() -> None:
128
186
  """
@@ -0,0 +1,4 @@
1
+ # Future Migrations
2
+
3
+ This folder contains future migrations for unreleased features that are under active development. Do not run these migrations unless you are a Phoenix developer.
4
+
@@ -0,0 +1,292 @@
1
+ """users and tokens
2
+
3
+ Revision ID: cd164e83824f
4
+ Revises: 10460e46d750
5
+ Create Date: 2024-08-01 18:36:52.157604
6
+
7
+ """
8
+
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Dict, List, Optional, Sequence, TypedDict, Union
11
+
12
+ import sqlalchemy as sa
13
+ from alembic import op
14
+ from phoenix.datetime_utils import normalize_datetime
15
+ from sqlalchemy import (
16
+ JSON,
17
+ TIMESTAMP,
18
+ CheckConstraint,
19
+ Dialect,
20
+ ForeignKey,
21
+ MetaData,
22
+ TypeDecorator,
23
+ func,
24
+ insert,
25
+ )
26
+ from sqlalchemy.dialects import postgresql
27
+ from sqlalchemy.ext.asyncio.engine import AsyncConnection
28
+ from sqlalchemy.ext.compiler import compiles
29
+ from sqlalchemy.orm import (
30
+ DeclarativeBase,
31
+ Mapped,
32
+ mapped_column,
33
+ )
34
+
35
+
36
+ class JSONB(JSON):
37
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
38
+ __visit_name__ = "JSONB"
39
+
40
+
41
+ @compiles(JSONB, "sqlite") # type: ignore
42
+ def _(*args: Any, **kwargs: Any) -> str:
43
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
44
+ return "JSONB"
45
+
46
+
47
+ JSON_ = (
48
+ JSON()
49
+ .with_variant(
50
+ postgresql.JSONB(), # type: ignore
51
+ "postgresql",
52
+ )
53
+ .with_variant(
54
+ JSONB(),
55
+ "sqlite",
56
+ )
57
+ )
58
+
59
+
60
+ class JsonDict(TypeDecorator[Dict[str, Any]]):
61
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
62
+ cache_ok = True
63
+ impl = JSON_
64
+
65
+ def process_bind_param(self, value: Optional[Dict[str, Any]], _: Dialect) -> Dict[str, Any]:
66
+ return value if isinstance(value, dict) else {}
67
+
68
+
69
+ class JsonList(TypeDecorator[List[Any]]):
70
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
71
+ cache_ok = True
72
+ impl = JSON_
73
+
74
+ def process_bind_param(self, value: Optional[List[Any]], _: Dialect) -> List[Any]:
75
+ return value if isinstance(value, list) else []
76
+
77
+
78
+ class UtcTimeStamp(TypeDecorator[datetime]):
79
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
80
+ cache_ok = True
81
+ impl = TIMESTAMP(timezone=True)
82
+
83
+ def process_bind_param(self, value: Optional[datetime], _: Dialect) -> Optional[datetime]:
84
+ return normalize_datetime(value)
85
+
86
+ def process_result_value(self, value: Optional[Any], _: Dialect) -> Optional[datetime]:
87
+ return normalize_datetime(value, timezone.utc)
88
+
89
+
90
+ class ExperimentRunOutput(TypedDict, total=False):
91
+ task_output: Any
92
+
93
+
94
+ class Base(DeclarativeBase):
95
+ # Enforce best practices for naming constraints
96
+ # https://alembic.sqlalchemy.org/en/latest/naming.html#integration-of-naming-conventions-into-operations-autogenerate
97
+ metadata = MetaData(
98
+ naming_convention={
99
+ "ix": "ix_%(table_name)s_%(column_0_N_name)s",
100
+ "uq": "uq_%(table_name)s_%(column_0_N_name)s",
101
+ "ck": "ck_%(table_name)s_`%(constraint_name)s`",
102
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
103
+ "pk": "pk_%(table_name)s",
104
+ }
105
+ )
106
+ type_annotation_map = {
107
+ Dict[str, Any]: JsonDict,
108
+ List[Dict[str, Any]]: JsonList,
109
+ ExperimentRunOutput: JsonDict,
110
+ }
111
+
112
+
113
+ class UserRole(Base):
114
+ __tablename__ = "user_roles"
115
+ id: Mapped[int] = mapped_column(primary_key=True)
116
+ role: Mapped[str] = mapped_column(unique=True)
117
+
118
+
119
+ class User(Base):
120
+ __tablename__ = "users"
121
+ id: Mapped[int] = mapped_column(primary_key=True)
122
+ user_role_id: Mapped[int] = mapped_column(
123
+ ForeignKey("user_roles.id"),
124
+ index=True,
125
+ )
126
+ username: Mapped[Optional[str]] = mapped_column(nullable=True, unique=True, index=True)
127
+ email: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
128
+ auth_method: Mapped[str] = mapped_column(
129
+ CheckConstraint("auth_method IN ('LOCAL')", name="valid_auth_method")
130
+ )
131
+ password_hash: Mapped[Optional[str]]
132
+ reset_password: Mapped[bool]
133
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
134
+ updated_at: Mapped[datetime] = mapped_column(
135
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
136
+ )
137
+ deleted_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
138
+
139
+
140
+ # revision identifiers, used by Alembic.
141
+ revision: str = "cd164e83824f"
142
+ down_revision: Union[str, None] = "3be8647b87d8"
143
+ branch_labels: Union[str, Sequence[str], None] = None
144
+ depends_on: Union[str, Sequence[str], None] = None
145
+
146
+
147
+ async def insert_roles_and_users(connection: AsyncConnection) -> None:
148
+ """
149
+ Populates the `user_roles` table and adds a system user and initial admin
150
+ user to the `users` table.
151
+ """
152
+ await connection.execute(
153
+ insert(UserRole).values([{"role": "SYSTEM"}, {"role": "ADMIN"}, {"role": "GENERAL"}])
154
+ )
155
+ system_user_role_id = sa.select(UserRole.id).where(UserRole.role == "SYSTEM").scalar_subquery()
156
+ admin_user_role_id = sa.select(UserRole.id).where(UserRole.role == "ADMIN").scalar_subquery()
157
+ await connection.execute(
158
+ insert(User).values(
159
+ [
160
+ {
161
+ "user_role_id": system_user_role_id,
162
+ "username": None,
163
+ "email": "system@localhost",
164
+ "auth_method": "LOCAL",
165
+ "password_hash": None,
166
+ "reset_password": False,
167
+ },
168
+ {
169
+ "user_role_id": admin_user_role_id,
170
+ "username": "admin",
171
+ "email": "admin@localhost",
172
+ "auth_method": "LOCAL",
173
+ "password_hash": None, # todo: replace this with the hashed PHOENIX_SECRET
174
+ "reset_password": True,
175
+ },
176
+ ]
177
+ )
178
+ )
179
+
180
+
181
+ def upgrade() -> None:
182
+ op.create_table(
183
+ "user_roles",
184
+ sa.Column("id", sa.Integer, primary_key=True),
185
+ sa.Column(
186
+ "role",
187
+ sa.String,
188
+ nullable=False,
189
+ unique=True,
190
+ ),
191
+ )
192
+ op.create_table(
193
+ "users",
194
+ sa.Column("id", sa.Integer, primary_key=True),
195
+ sa.Column(
196
+ "user_role_id",
197
+ sa.Integer,
198
+ sa.ForeignKey("user_roles.id"),
199
+ nullable=False,
200
+ index=True,
201
+ ),
202
+ sa.Column("username", sa.String, nullable=True, unique=True, index=True),
203
+ sa.Column("email", sa.String, nullable=False, unique=True, index=True),
204
+ sa.Column(
205
+ "auth_method",
206
+ sa.String,
207
+ sa.CheckConstraint("auth_method IN ('LOCAL')", "valid_auth_method"),
208
+ nullable=False,
209
+ ),
210
+ sa.Column("password_hash", sa.String, nullable=True),
211
+ sa.Column("reset_password", sa.Boolean, nullable=False),
212
+ sa.Column(
213
+ "created_at",
214
+ sa.TIMESTAMP(timezone=True),
215
+ nullable=False,
216
+ server_default=sa.func.now(),
217
+ ),
218
+ sa.Column(
219
+ "updated_at",
220
+ sa.TIMESTAMP(timezone=True),
221
+ nullable=False,
222
+ server_default=sa.func.now(),
223
+ onupdate=sa.func.now(),
224
+ ),
225
+ sa.Column(
226
+ "deleted_at",
227
+ sa.TIMESTAMP(timezone=True),
228
+ nullable=True,
229
+ ),
230
+ )
231
+ op.create_table(
232
+ "api_keys",
233
+ sa.Column("id", sa.Integer, primary_key=True),
234
+ sa.Column(
235
+ "user_id",
236
+ sa.Integer,
237
+ sa.ForeignKey("users.id"),
238
+ nullable=False,
239
+ index=True,
240
+ ),
241
+ sa.Column("name", sa.String, nullable=False),
242
+ sa.Column("description", sa.String, nullable=True),
243
+ sa.Column(
244
+ "created_at",
245
+ sa.TIMESTAMP(timezone=True),
246
+ nullable=False,
247
+ server_default=sa.func.now(),
248
+ ),
249
+ sa.Column(
250
+ "expires_at",
251
+ sa.TIMESTAMP(timezone=True),
252
+ nullable=True,
253
+ ),
254
+ )
255
+ op.create_table(
256
+ "audit_api_keys",
257
+ sa.Column("id", sa.Integer, primary_key=True),
258
+ sa.Column(
259
+ "api_key_id",
260
+ sa.Integer,
261
+ sa.ForeignKey("api_keys.id"),
262
+ nullable=False,
263
+ index=True,
264
+ ),
265
+ sa.Column(
266
+ "user_id",
267
+ sa.Integer,
268
+ sa.ForeignKey("users.id"),
269
+ nullable=False,
270
+ index=True,
271
+ ),
272
+ sa.Column(
273
+ "action",
274
+ sa.String,
275
+ sa.CheckConstraint("action IN ('CREATE', 'DELETE')", "valid_action"),
276
+ nullable=False,
277
+ ),
278
+ sa.Column(
279
+ "created_at",
280
+ sa.TIMESTAMP(timezone=True),
281
+ nullable=False,
282
+ server_default=sa.func.now(),
283
+ ),
284
+ )
285
+ op.run_async(insert_roles_and_users)
286
+
287
+
288
+ def downgrade() -> None:
289
+ op.drop_table("audit_api_keys")
290
+ op.drop_table("api_keys")
291
+ op.drop_table("users")
292
+ op.drop_table("user_roles")
@@ -0,0 +1 @@
1
+ cd164e83824f_users_and_tokens.py
phoenix/db/models.py CHANGED
@@ -34,6 +34,7 @@ from sqlalchemy.orm import (
34
34
  )
35
35
  from sqlalchemy.sql import expression
36
36
 
37
+ from phoenix.config import ENABLE_AUTH
37
38
  from phoenix.datetime_utils import normalize_datetime
38
39
 
39
40
 
@@ -616,3 +617,63 @@ class ExperimentRunAnnotation(Base):
616
617
  "name",
617
618
  ),
618
619
  )
620
+
621
+
622
+ # todo: unnest the following models when auth is released (https://github.com/Arize-ai/phoenix/issues/4183)
623
+ if ENABLE_AUTH:
624
+
625
+ class UserRole(Base):
626
+ __tablename__ = "user_roles"
627
+ id: Mapped[int] = mapped_column(primary_key=True)
628
+ role: Mapped[str] = mapped_column(unique=True)
629
+
630
+ class User(Base):
631
+ __tablename__ = "users"
632
+ id: Mapped[int] = mapped_column(primary_key=True)
633
+ user_role_id: Mapped[int] = mapped_column(
634
+ ForeignKey("user_roles.id"),
635
+ index=True,
636
+ )
637
+ username: Mapped[Optional[str]] = mapped_column(nullable=True, unique=True, index=True)
638
+ email: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
639
+ auth_method: Mapped[str] = mapped_column(
640
+ CheckConstraint("auth_method IN ('LOCAL')", name="valid_auth_method")
641
+ )
642
+ password_hash: Mapped[Optional[str]]
643
+ reset_password: Mapped[bool]
644
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
645
+ updated_at: Mapped[datetime] = mapped_column(
646
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
647
+ )
648
+ deleted_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
649
+
650
+ class APIKey(Base):
651
+ __tablename__ = "api_keys"
652
+ id: Mapped[int] = mapped_column(primary_key=True)
653
+ user_id: Mapped[int] = mapped_column(
654
+ ForeignKey("users.id"),
655
+ index=True,
656
+ )
657
+ name: Mapped[str]
658
+ description: Mapped[Optional[str]]
659
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
660
+ expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
661
+
662
+ # todo: standardize audit table format (https://github.com/Arize-ai/phoenix/issues/4185)
663
+ class AuditAPIKey(Base):
664
+ __tablename__ = "audit_api_keys"
665
+ id: Mapped[int] = mapped_column(primary_key=True)
666
+ api_key_id: Mapped[int] = mapped_column(
667
+ ForeignKey("api_keys.id"),
668
+ nullable=False,
669
+ index=True,
670
+ )
671
+ user_id: Mapped[int] = mapped_column(
672
+ ForeignKey("users.id"),
673
+ nullable=False,
674
+ index=True,
675
+ )
676
+ action: Mapped[str] = mapped_column(
677
+ CheckConstraint("action IN ('CREATE', 'DELETE')", name="valid_action")
678
+ )
679
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
@@ -1,22 +1,22 @@
1
1
  {
2
- "_components-BSw2e1Zr.js": {
3
- "file": "assets/components-BSw2e1Zr.js",
2
+ "_components-Bhx3QVW0.js": {
3
+ "file": "assets/components-Bhx3QVW0.js",
4
4
  "name": "components",
5
5
  "imports": [
6
6
  "_vendor-BMWfu6zp.js",
7
- "_vendor-arizeai-CIETbKDq.js",
8
- "_pages-p_fuED5k.js",
7
+ "_vendor-arizeai-Sj74jm5V.js",
8
+ "_pages-DG-5zgoV.js",
9
9
  "_vendor-three-DwGkEfCM.js",
10
10
  "_vendor-codemirror-DO3VqEcD.js"
11
11
  ]
12
12
  },
13
- "_pages-p_fuED5k.js": {
14
- "file": "assets/pages-p_fuED5k.js",
13
+ "_pages-DG-5zgoV.js": {
14
+ "file": "assets/pages-DG-5zgoV.js",
15
15
  "name": "pages",
16
16
  "imports": [
17
17
  "_vendor-BMWfu6zp.js",
18
- "_components-BSw2e1Zr.js",
19
- "_vendor-arizeai-CIETbKDq.js",
18
+ "_components-Bhx3QVW0.js",
19
+ "_vendor-arizeai-Sj74jm5V.js",
20
20
  "_vendor-recharts-BGN0SxgJ.js",
21
21
  "_vendor-codemirror-DO3VqEcD.js"
22
22
  ]
@@ -35,8 +35,8 @@
35
35
  "assets/vendor-DxkFTwjz.css"
36
36
  ]
37
37
  },
38
- "_vendor-arizeai-CIETbKDq.js": {
39
- "file": "assets/vendor-arizeai-CIETbKDq.js",
38
+ "_vendor-arizeai-Sj74jm5V.js": {
39
+ "file": "assets/vendor-arizeai-Sj74jm5V.js",
40
40
  "name": "vendor-arizeai",
41
41
  "imports": [
42
42
  "_vendor-BMWfu6zp.js"
@@ -61,15 +61,15 @@
61
61
  "name": "vendor-three"
62
62
  },
63
63
  "index.tsx": {
64
- "file": "assets/index-BYUFcdtx.js",
64
+ "file": "assets/index-CZg-95kd.js",
65
65
  "name": "index",
66
66
  "src": "index.tsx",
67
67
  "isEntry": true,
68
68
  "imports": [
69
69
  "_vendor-BMWfu6zp.js",
70
- "_vendor-arizeai-CIETbKDq.js",
71
- "_components-BSw2e1Zr.js",
72
- "_pages-p_fuED5k.js",
70
+ "_vendor-arizeai-Sj74jm5V.js",
71
+ "_components-Bhx3QVW0.js",
72
+ "_pages-DG-5zgoV.js",
73
73
  "_vendor-three-DwGkEfCM.js",
74
74
  "_vendor-codemirror-DO3VqEcD.js",
75
75
  "_vendor-recharts-BGN0SxgJ.js"