arize-phoenix 4.36.0__py3-none-any.whl → 5.0.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 (80) hide show
  1. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/METADATA +10 -12
  2. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/RECORD +68 -59
  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/utilities/client.py +16 -0
  65. phoenix/version.py +1 -1
  66. phoenix/db/migrations/future_versions/README.md +0 -4
  67. phoenix/db/migrations/future_versions/cd164e83824f_users_and_tokens.py +0 -293
  68. phoenix/db/migrations/versions/.gitignore +0 -1
  69. phoenix/server/api/mutations/auth.py +0 -18
  70. phoenix/server/api/mutations/auth_mutations.py +0 -65
  71. phoenix/server/static/assets/index-fq1-hCK4.js +0 -100
  72. phoenix/trace/langchain/__init__.py +0 -3
  73. phoenix/trace/langchain/instrumentor.py +0 -34
  74. phoenix/trace/llama_index/__init__.py +0 -3
  75. phoenix/trace/llama_index/callback.py +0 -102
  76. phoenix/trace/openai/__init__.py +0 -3
  77. phoenix/trace/openai/instrumentor.py +0 -30
  78. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/WHEEL +0 -0
  79. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/licenses/IP_NOTICE +0 -0
  80. {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/licenses/LICENSE +0 -0
phoenix/db/enums.py ADDED
@@ -0,0 +1,20 @@
1
+ from enum import Enum
2
+ from typing import Mapping, Type
3
+
4
+ from sqlalchemy.orm import InstrumentedAttribute
5
+
6
+ from phoenix.db import models
7
+ from phoenix.db.models import AuthMethod
8
+
9
+ __all__ = ["AuthMethod", "UserRole", "COLUMN_ENUMS"]
10
+
11
+
12
+ class UserRole(Enum):
13
+ SYSTEM = "SYSTEM"
14
+ ADMIN = "ADMIN"
15
+ MEMBER = "MEMBER"
16
+
17
+
18
+ COLUMN_ENUMS: Mapping[InstrumentedAttribute[str], Type[Enum]] = {
19
+ models.UserRole.name: UserRole,
20
+ }
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import secrets
5
+ from functools import partial
6
+
7
+ from sqlalchemy import (
8
+ distinct,
9
+ insert,
10
+ select,
11
+ )
12
+ from sqlalchemy.ext.asyncio import AsyncSession
13
+
14
+ from phoenix.auth import (
15
+ DEFAULT_ADMIN_EMAIL,
16
+ DEFAULT_ADMIN_PASSWORD,
17
+ DEFAULT_ADMIN_USERNAME,
18
+ DEFAULT_SECRET_LENGTH,
19
+ DEFAULT_SYSTEM_EMAIL,
20
+ DEFAULT_SYSTEM_USERNAME,
21
+ compute_password_hash,
22
+ )
23
+ from phoenix.db import models
24
+ from phoenix.db.enums import COLUMN_ENUMS, UserRole
25
+ from phoenix.server.types import DbSessionFactory
26
+
27
+
28
+ class Facilitator:
29
+ """
30
+ Facilitates the creation of database records necessary for Phoenix to function. This includes
31
+ ensuring that all enum values are present in their respective tables, ensuring that all user
32
+ roles are present, and ensuring that the admin user has a password hash. These tasks will be
33
+ carried out as callbacks at the very beginning of Starlette's lifespan process.
34
+ """
35
+
36
+ def __init__(self, *, db: DbSessionFactory) -> None:
37
+ self._db = db
38
+
39
+ async def __call__(self) -> None:
40
+ async with self._db() as session:
41
+ for fn in (
42
+ _ensure_enums,
43
+ _ensure_user_roles,
44
+ ):
45
+ async with session.begin_nested():
46
+ await fn(session)
47
+
48
+
49
+ async def _ensure_enums(session: AsyncSession) -> None:
50
+ """
51
+ Ensure that all enum values are present in their respective tables. If any values are missing,
52
+ they will be added. If any values are present in the database but not in the enum, an error will
53
+ be raised. This function is idempotent: it will not add duplicate values to the database.
54
+ """
55
+ for column, enum in COLUMN_ENUMS.items():
56
+ table = column.class_
57
+ existing = set([_ async for _ in await session.stream_scalars(select(distinct(column)))])
58
+ expected = set(e.value for e in enum)
59
+ if unexpected := existing - expected:
60
+ raise ValueError(f"Unexpected values in {table.name}.{column.key}: {unexpected}")
61
+ if not (missing := expected - existing):
62
+ continue
63
+ await session.execute(insert(table), [{column.key: v} for v in missing])
64
+
65
+
66
+ async def _ensure_user_roles(session: AsyncSession) -> None:
67
+ """
68
+ Ensure that the system and admin roles are present in the database. If they are not, they will
69
+ be added. The system user will have the email "system@localhost" and the admin user will have
70
+ the email "admin@localhost".
71
+ """
72
+ role_ids = {
73
+ name: id_
74
+ async for name, id_ in await session.stream(
75
+ select(models.UserRole.name, models.UserRole.id)
76
+ )
77
+ }
78
+ existing_roles = [
79
+ name
80
+ async for name in await session.stream_scalars(
81
+ select(distinct(models.UserRole.name)).join_from(models.User, models.UserRole)
82
+ )
83
+ ]
84
+ if (system_role := UserRole.SYSTEM.value) not in existing_roles and (
85
+ system_role_id := role_ids.get(system_role)
86
+ ) is not None:
87
+ system_user = models.User(
88
+ user_role_id=system_role_id,
89
+ username=DEFAULT_SYSTEM_USERNAME,
90
+ email=DEFAULT_SYSTEM_EMAIL,
91
+ reset_password=False,
92
+ password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
93
+ password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
94
+ )
95
+ session.add(system_user)
96
+ if (admin_role := UserRole.ADMIN.value) not in existing_roles and (
97
+ admin_role_id := role_ids.get(admin_role)
98
+ ) is not None:
99
+ salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
100
+ compute = partial(compute_password_hash, password=DEFAULT_ADMIN_PASSWORD, salt=salt)
101
+ loop = asyncio.get_running_loop()
102
+ hash_ = await loop.run_in_executor(None, compute)
103
+ admin_user = models.User(
104
+ user_role_id=admin_role_id,
105
+ username=DEFAULT_ADMIN_USERNAME,
106
+ email=DEFAULT_ADMIN_EMAIL,
107
+ password_salt=salt,
108
+ password_hash=hash_,
109
+ reset_password=True,
110
+ )
111
+ session.add(admin_user)
112
+ await session.flush()
@@ -0,0 +1,157 @@
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 typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "cd164e83824f"
16
+ down_revision: Union[str, None] = "3be8647b87d8"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ op.create_table(
23
+ "user_roles",
24
+ sa.Column("id", sa.Integer, primary_key=True),
25
+ sa.Column(
26
+ "name",
27
+ sa.String,
28
+ nullable=False,
29
+ unique=True,
30
+ index=True,
31
+ ),
32
+ )
33
+ op.create_table(
34
+ "users",
35
+ sa.Column("id", sa.Integer, primary_key=True),
36
+ sa.Column(
37
+ "user_role_id",
38
+ sa.Integer,
39
+ sa.ForeignKey("user_roles.id", ondelete="CASCADE"),
40
+ nullable=False,
41
+ index=True,
42
+ ),
43
+ sa.Column("username", sa.String, nullable=False, unique=True, index=True),
44
+ sa.Column("email", sa.String, nullable=False, unique=True, index=True),
45
+ sa.Column("profile_picture_url", sa.String, nullable=True),
46
+ sa.Column("password_hash", sa.LargeBinary, nullable=True),
47
+ sa.Column("password_salt", sa.LargeBinary, nullable=True),
48
+ sa.Column("reset_password", sa.Boolean, nullable=False),
49
+ sa.Column("oauth2_client_id", sa.String, nullable=True, index=True),
50
+ sa.Column("oauth2_user_id", sa.String, nullable=True, index=True),
51
+ sa.Column(
52
+ "created_at",
53
+ sa.TIMESTAMP(timezone=True),
54
+ nullable=False,
55
+ server_default=sa.func.now(),
56
+ ),
57
+ sa.Column(
58
+ "updated_at",
59
+ sa.TIMESTAMP(timezone=True),
60
+ nullable=False,
61
+ server_default=sa.func.now(),
62
+ onupdate=sa.func.now(),
63
+ ),
64
+ sa.CheckConstraint(
65
+ "(password_hash IS NULL) = (password_salt IS NULL)",
66
+ name="password_hash_and_salt",
67
+ ),
68
+ sa.CheckConstraint(
69
+ "(oauth2_client_id IS NULL) = (oauth2_user_id IS NULL)",
70
+ name="oauth2_client_id_and_user_id",
71
+ ),
72
+ sa.CheckConstraint(
73
+ "(password_hash IS NULL) != (oauth2_client_id IS NULL)",
74
+ name="exactly_one_auth_method",
75
+ ),
76
+ sa.UniqueConstraint(
77
+ "oauth2_client_id",
78
+ "oauth2_user_id",
79
+ ),
80
+ sqlite_autoincrement=True,
81
+ )
82
+ op.create_table(
83
+ "password_reset_tokens",
84
+ sa.Column("id", sa.Integer, primary_key=True),
85
+ sa.Column(
86
+ "user_id",
87
+ sa.Integer,
88
+ sa.ForeignKey("users.id", ondelete="CASCADE"),
89
+ unique=True,
90
+ index=True,
91
+ ),
92
+ sa.Column(
93
+ "created_at",
94
+ sa.TIMESTAMP(timezone=True),
95
+ nullable=False,
96
+ server_default=sa.func.now(),
97
+ ),
98
+ sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=False, index=True),
99
+ sqlite_autoincrement=True,
100
+ )
101
+ op.create_table(
102
+ "refresh_tokens",
103
+ sa.Column("id", sa.Integer, primary_key=True),
104
+ sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), index=True),
105
+ sa.Column(
106
+ "created_at",
107
+ sa.TIMESTAMP(timezone=True),
108
+ nullable=False,
109
+ server_default=sa.func.now(),
110
+ ),
111
+ sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=False, index=True),
112
+ sqlite_autoincrement=True,
113
+ )
114
+ op.create_table(
115
+ "access_tokens",
116
+ sa.Column("id", sa.Integer, primary_key=True),
117
+ sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), index=True),
118
+ sa.Column(
119
+ "created_at",
120
+ sa.TIMESTAMP(timezone=True),
121
+ nullable=False,
122
+ server_default=sa.func.now(),
123
+ ),
124
+ sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=False, index=True),
125
+ sa.Column(
126
+ "refresh_token_id",
127
+ sa.Integer,
128
+ sa.ForeignKey("refresh_tokens.id", ondelete="CASCADE"),
129
+ index=True,
130
+ unique=True,
131
+ ),
132
+ sqlite_autoincrement=True,
133
+ )
134
+ op.create_table(
135
+ "api_keys",
136
+ sa.Column("id", sa.Integer, primary_key=True),
137
+ sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), index=True),
138
+ sa.Column("name", sa.String, nullable=False),
139
+ sa.Column("description", sa.String, nullable=True),
140
+ sa.Column(
141
+ "created_at",
142
+ sa.TIMESTAMP(timezone=True),
143
+ nullable=False,
144
+ server_default=sa.func.now(),
145
+ ),
146
+ sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=True, index=True),
147
+ sqlite_autoincrement=True,
148
+ )
149
+
150
+
151
+ def downgrade() -> None:
152
+ op.drop_table("api_keys")
153
+ op.drop_table("access_tokens")
154
+ op.drop_table("refresh_tokens")
155
+ op.drop_table("password_reset_tokens")
156
+ op.drop_table("users")
157
+ op.drop_table("user_roles")
phoenix/db/models.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime, timezone
2
+ from enum import Enum
2
3
  from typing import Any, Dict, List, Optional, TypedDict
3
4
 
4
5
  from sqlalchemy import (
@@ -18,6 +19,7 @@ from sqlalchemy import (
18
19
  case,
19
20
  func,
20
21
  insert,
22
+ not_,
21
23
  select,
22
24
  text,
23
25
  )
@@ -34,10 +36,15 @@ from sqlalchemy.orm import (
34
36
  )
35
37
  from sqlalchemy.sql import expression
36
38
 
37
- from phoenix.config import ENABLE_AUTH, get_env_database_schema
39
+ from phoenix.config import get_env_database_schema
38
40
  from phoenix.datetime_utils import normalize_datetime
39
41
 
40
42
 
43
+ class AuthMethod(Enum):
44
+ LOCAL = "LOCAL"
45
+ OAUTH2 = "OAUTH2"
46
+
47
+
41
48
  class JSONB(JSON):
42
49
  # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
43
50
  __visit_name__ = "JSONB"
@@ -620,65 +627,143 @@ class ExperimentRunAnnotation(Base):
620
627
  )
621
628
 
622
629
 
623
- # todo: unnest the following models when auth is released (https://github.com/Arize-ai/phoenix/issues/4183)
624
- if ENABLE_AUTH:
630
+ class UserRole(Base):
631
+ __tablename__ = "user_roles"
632
+ id: Mapped[int] = mapped_column(primary_key=True)
633
+ name: Mapped[str] = mapped_column(unique=True, index=True)
634
+ users: Mapped[List["User"]] = relationship("User", back_populates="role")
625
635
 
626
- class UserRole(Base):
627
- __tablename__ = "user_roles"
628
- id: Mapped[int] = mapped_column(primary_key=True)
629
- name: Mapped[str] = mapped_column(unique=True)
630
- users: Mapped[List["User"]] = relationship("User", back_populates="role")
631
636
 
632
- class User(Base):
633
- __tablename__ = "users"
634
- id: Mapped[int] = mapped_column(primary_key=True)
635
- user_role_id: Mapped[int] = mapped_column(
636
- ForeignKey("user_roles.id"),
637
- index=True,
638
- )
639
- role: Mapped["UserRole"] = relationship("UserRole", back_populates="users")
640
- username: Mapped[Optional[str]] = mapped_column(nullable=True, unique=True, index=True)
641
- email: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
642
- auth_method: Mapped[str] = mapped_column(
643
- CheckConstraint("auth_method IN ('LOCAL')", name="valid_auth_method")
644
- )
645
- password_hash: Mapped[Optional[str]]
646
- reset_password: Mapped[bool]
647
- created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
648
- updated_at: Mapped[datetime] = mapped_column(
649
- UtcTimeStamp, server_default=func.now(), onupdate=func.now()
650
- )
651
- deleted_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
652
- api_keys: Mapped[List["APIKey"]] = relationship("APIKey", back_populates="user")
653
-
654
- class APIKey(Base):
655
- __tablename__ = "api_keys"
656
- id: Mapped[int] = mapped_column(primary_key=True)
657
- user_id: Mapped[int] = mapped_column(
658
- ForeignKey("users.id"),
659
- index=True,
660
- )
661
- user: Mapped["User"] = relationship("User", back_populates="api_keys")
662
- name: Mapped[str]
663
- description: Mapped[Optional[str]]
664
- created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
665
- expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
666
-
667
- # todo: standardize audit table format (https://github.com/Arize-ai/phoenix/issues/4185)
668
- class AuditAPIKey(Base):
669
- __tablename__ = "audit_api_keys"
670
- id: Mapped[int] = mapped_column(primary_key=True)
671
- api_key_id: Mapped[int] = mapped_column(
672
- ForeignKey("api_keys.id"),
673
- nullable=False,
674
- index=True,
675
- )
676
- user_id: Mapped[int] = mapped_column(
677
- ForeignKey("users.id"),
678
- nullable=False,
679
- index=True,
680
- )
681
- action: Mapped[str] = mapped_column(
682
- CheckConstraint("action IN ('CREATE', 'DELETE')", name="valid_action")
637
+ class User(Base):
638
+ __tablename__ = "users"
639
+ id: Mapped[int] = mapped_column(primary_key=True)
640
+ user_role_id: Mapped[int] = mapped_column(
641
+ ForeignKey("user_roles.id", ondelete="CASCADE"),
642
+ index=True,
643
+ )
644
+ role: Mapped["UserRole"] = relationship("UserRole", back_populates="users")
645
+ username: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
646
+ email: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
647
+ profile_picture_url: Mapped[Optional[str]]
648
+ password_hash: Mapped[Optional[bytes]]
649
+ password_salt: Mapped[Optional[bytes]]
650
+ reset_password: Mapped[bool]
651
+ oauth2_client_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
652
+ oauth2_user_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
653
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
654
+ updated_at: Mapped[datetime] = mapped_column(
655
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
656
+ )
657
+ password_reset_token: Mapped[Optional["PasswordResetToken"]] = relationship(
658
+ "PasswordResetToken",
659
+ back_populates="user",
660
+ uselist=False,
661
+ )
662
+ access_tokens: Mapped[List["AccessToken"]] = relationship("AccessToken", back_populates="user")
663
+ refresh_tokens: Mapped[List["RefreshToken"]] = relationship(
664
+ "RefreshToken", back_populates="user"
665
+ )
666
+ api_keys: Mapped[List["ApiKey"]] = relationship("ApiKey", back_populates="user")
667
+
668
+ @hybrid_property
669
+ def auth_method(self) -> Optional[str]:
670
+ if self.password_hash is not None:
671
+ return AuthMethod.LOCAL.value
672
+ elif self.oauth2_client_id is not None:
673
+ return AuthMethod.OAUTH2.value
674
+ return None
675
+
676
+ @auth_method.inplace.expression
677
+ @classmethod
678
+ def _auth_method_expression(cls) -> ColumnElement[Optional[str]]:
679
+ return case(
680
+ (
681
+ not_(cls.password_hash.is_(None)),
682
+ AuthMethod.LOCAL.value,
683
+ ),
684
+ (
685
+ not_(cls.oauth2_client_id.is_(None)),
686
+ AuthMethod.OAUTH2.value,
687
+ ),
688
+ else_=None,
683
689
  )
684
- created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
690
+
691
+ __table_args__ = (
692
+ CheckConstraint(
693
+ "(password_hash IS NULL) = (password_salt IS NULL)",
694
+ name="password_hash_and_salt",
695
+ ),
696
+ CheckConstraint(
697
+ "(oauth2_client_id IS NULL) = (oauth2_user_id IS NULL)",
698
+ name="oauth2_client_id_and_user_id",
699
+ ),
700
+ CheckConstraint(
701
+ "(password_hash IS NULL) != (oauth2_client_id IS NULL)",
702
+ name="exactly_one_auth_method",
703
+ ),
704
+ UniqueConstraint(
705
+ "oauth2_client_id",
706
+ "oauth2_user_id",
707
+ ),
708
+ dict(sqlite_autoincrement=True),
709
+ )
710
+
711
+
712
+ class PasswordResetToken(Base):
713
+ __tablename__ = "password_reset_tokens"
714
+ id: Mapped[int] = mapped_column(primary_key=True)
715
+ user_id: Mapped[int] = mapped_column(
716
+ ForeignKey("users.id", ondelete="CASCADE"),
717
+ unique=True,
718
+ index=True,
719
+ )
720
+ user: Mapped["User"] = relationship("User", back_populates="password_reset_token")
721
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
722
+ expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
723
+ __table_args__ = (dict(sqlite_autoincrement=True),)
724
+
725
+
726
+ class RefreshToken(Base):
727
+ __tablename__ = "refresh_tokens"
728
+ id: Mapped[int] = mapped_column(primary_key=True)
729
+ user_id: Mapped[int] = mapped_column(
730
+ ForeignKey("users.id", ondelete="CASCADE"),
731
+ index=True,
732
+ )
733
+ user: Mapped["User"] = relationship("User", back_populates="refresh_tokens")
734
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
735
+ expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
736
+ __table_args__ = (dict(sqlite_autoincrement=True),)
737
+
738
+
739
+ class AccessToken(Base):
740
+ __tablename__ = "access_tokens"
741
+ id: Mapped[int] = mapped_column(primary_key=True)
742
+ user_id: Mapped[int] = mapped_column(
743
+ ForeignKey("users.id", ondelete="CASCADE"),
744
+ index=True,
745
+ )
746
+ user: Mapped["User"] = relationship("User", back_populates="access_tokens")
747
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
748
+ expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
749
+ refresh_token_id: Mapped[int] = mapped_column(
750
+ ForeignKey("refresh_tokens.id", ondelete="CASCADE"),
751
+ index=True,
752
+ unique=True,
753
+ )
754
+ __table_args__ = (dict(sqlite_autoincrement=True),)
755
+
756
+
757
+ class ApiKey(Base):
758
+ __tablename__ = "api_keys"
759
+ id: Mapped[int] = mapped_column(primary_key=True)
760
+ user_id: Mapped[int] = mapped_column(
761
+ ForeignKey("users.id", ondelete="CASCADE"),
762
+ index=True,
763
+ )
764
+ user: Mapped["User"] = relationship("User", back_populates="api_keys")
765
+ name: Mapped[str]
766
+ description: Mapped[Optional[str]]
767
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
768
+ expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=True, index=True)
769
+ __table_args__ = (dict(sqlite_autoincrement=True),)
@@ -2,7 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import re
5
- from typing import Any, List, Optional, Union
5
+ from typing import (
6
+ Any,
7
+ List,
8
+ Optional,
9
+ Pattern, # import from re module when we drop support for 3.8
10
+ Union,
11
+ )
6
12
 
7
13
  from phoenix.experiments.evaluators.base import CodeEvaluator
8
14
  from phoenix.experiments.types import EvaluationResult, TaskOutput
@@ -144,7 +150,7 @@ class MatchesRegex(CodeEvaluator):
144
150
  An experiment evaluator that checks if the output of an experiment run matches a regex pattern.
145
151
 
146
152
  Args:
147
- pattern (Union[str, re.Pattern[str]]): The regex pattern to match the output against.
153
+ pattern (Union[str, Pattern[str]]): The regex pattern to match the output against.
148
154
  name (str, optional): An optional name for the evaluator. Defaults to "matches_({pattern})".
149
155
 
150
156
  Example:
@@ -157,7 +163,7 @@ class MatchesRegex(CodeEvaluator):
157
163
  run_experiment(dataset, task, evaluators=[phone_number_evaluator])
158
164
  """
159
165
 
160
- def __init__(self, pattern: Union[str, re.Pattern[str]], name: Optional[str] = None) -> None:
166
+ def __init__(self, pattern: Union[str, Pattern[str]], name: Optional[str] = None) -> None:
161
167
  if isinstance(pattern, str):
162
168
  pattern = re.compile(pattern)
163
169
  self.pattern = pattern
@@ -41,7 +41,7 @@ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
41
41
  from opentelemetry.trace import Status, StatusCode, Tracer
42
42
  from typing_extensions import TypeAlias
43
43
 
44
- from phoenix.config import get_base_url, get_env_client_headers
44
+ from phoenix.config import get_base_url
45
45
  from phoenix.evals.executors import get_executor_on_sync_context
46
46
  from phoenix.evals.models.rate_limiters import RateLimiter
47
47
  from phoenix.evals.utils import get_tqdm_progress_bar_formatter
@@ -77,13 +77,10 @@ from phoenix.utilities.json import jsonify
77
77
 
78
78
 
79
79
  def _phoenix_clients() -> Tuple[httpx.Client, httpx.AsyncClient]:
80
- headers = get_env_client_headers()
81
80
  return VersionedClient(
82
81
  base_url=get_base_url(),
83
- headers=headers,
84
82
  ), VersionedAsyncClient(
85
83
  base_url=get_base_url(),
86
- headers=headers,
87
84
  )
88
85
 
89
86
 
@@ -0,0 +1,28 @@
1
+ # Permission Matrix for GraphQL API
2
+
3
+ ## Mutations
4
+
5
+ | Action | Admin | Member |
6
+ |:-----------------------------|:-----:|:------:|
7
+ | Create User | Yes | No |
8
+ | Delete User | Yes | No |
9
+ | Change Own Password | Yes | Yes |
10
+ | Change Other's Password | Yes | No |
11
+ | Change Own Username | Yes | Yes |
12
+ | Change Other's Username | Yes | No |
13
+ | Change Own Email | No | No |
14
+ | Change Other's Email | No | No |
15
+ | Create System API Keys | Yes | No |
16
+ | Delete System API Keys | Yes | No |
17
+ | Create Own User API Keys | Yes | Yes |
18
+ | Delete Own User API Keys | Yes | Yes |
19
+ | Delete Other's User API Keys | Yes | No |
20
+
21
+ ## Queries
22
+
23
+ | Action | Admin | Member |
24
+ |:-------------------------------------|:-----:|:------:|
25
+ | List All System API Keys | Yes | No |
26
+ | List All User API Keys | Yes | No |
27
+ | List All Users | Yes | No |
28
+ | Fetch Other User's Info, e.g. emails | Yes | No |
@@ -0,0 +1,32 @@
1
+ from abc import ABC
2
+ from typing import Any
3
+
4
+ from strawberry import Info
5
+ from strawberry.permission import BasePermission
6
+
7
+ from phoenix.server.api.exceptions import Unauthorized
8
+ from phoenix.server.bearer_auth import PhoenixUser
9
+
10
+
11
+ class Authorization(BasePermission, ABC):
12
+ def on_unauthorized(self) -> None:
13
+ raise Unauthorized(self.message)
14
+
15
+
16
+ class IsNotReadOnly(Authorization):
17
+ message = "Application is read-only"
18
+
19
+ def has_permission(self, source: Any, info: Info, **kwargs: Any) -> bool:
20
+ return not info.context.read_only
21
+
22
+
23
+ MSG_ADMIN_ONLY = "Only admin can perform this action"
24
+
25
+
26
+ class IsAdmin(Authorization):
27
+ message = MSG_ADMIN_ONLY
28
+
29
+ def has_permission(self, source: Any, info: Info, **kwargs: Any) -> bool:
30
+ if not info.context.auth_enabled:
31
+ return False
32
+ return isinstance((user := info.context.user), PhoenixUser) and user.is_admin