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.
- {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/METADATA +10 -12
- {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/RECORD +68 -59
- phoenix/__init__.py +86 -0
- phoenix/auth.py +275 -14
- phoenix/config.py +277 -25
- phoenix/db/enums.py +20 -0
- phoenix/db/facilitator.py +112 -0
- phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
- phoenix/db/models.py +145 -60
- phoenix/experiments/evaluators/code_evaluators.py +9 -3
- phoenix/experiments/functions.py +1 -4
- phoenix/server/api/README.md +28 -0
- phoenix/server/api/auth.py +32 -0
- phoenix/server/api/context.py +50 -2
- phoenix/server/api/dataloaders/__init__.py +4 -0
- phoenix/server/api/dataloaders/user_roles.py +30 -0
- phoenix/server/api/dataloaders/users.py +33 -0
- phoenix/server/api/exceptions.py +7 -0
- phoenix/server/api/mutations/__init__.py +0 -2
- phoenix/server/api/mutations/api_key_mutations.py +104 -86
- phoenix/server/api/mutations/dataset_mutations.py +8 -8
- phoenix/server/api/mutations/experiment_mutations.py +2 -2
- phoenix/server/api/mutations/export_events_mutations.py +3 -3
- phoenix/server/api/mutations/project_mutations.py +3 -3
- phoenix/server/api/mutations/span_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/trace_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/user_mutations.py +282 -42
- phoenix/server/api/openapi/schema.py +2 -2
- phoenix/server/api/queries.py +48 -39
- phoenix/server/api/routers/__init__.py +11 -0
- phoenix/server/api/routers/auth.py +284 -0
- phoenix/server/api/routers/embeddings.py +26 -0
- phoenix/server/api/routers/oauth2.py +456 -0
- phoenix/server/api/routers/v1/__init__.py +38 -16
- phoenix/server/api/types/ApiKey.py +11 -0
- phoenix/server/api/types/AuthMethod.py +9 -0
- phoenix/server/api/types/User.py +48 -4
- phoenix/server/api/types/UserApiKey.py +35 -1
- phoenix/server/api/types/UserRole.py +7 -0
- phoenix/server/app.py +103 -31
- phoenix/server/bearer_auth.py +161 -0
- phoenix/server/email/__init__.py +0 -0
- phoenix/server/email/sender.py +26 -0
- phoenix/server/email/templates/__init__.py +0 -0
- phoenix/server/email/templates/password_reset.html +19 -0
- phoenix/server/email/types.py +11 -0
- phoenix/server/grpc_server.py +6 -0
- phoenix/server/jwt_store.py +504 -0
- phoenix/server/main.py +40 -9
- phoenix/server/oauth2.py +51 -0
- phoenix/server/prometheus.py +20 -0
- phoenix/server/rate_limiters.py +191 -0
- phoenix/server/static/.vite/manifest.json +31 -31
- phoenix/server/static/assets/{components-Dte7_KRd.js → components-REunxTt6.js} +348 -286
- phoenix/server/static/assets/index-DAPJxlCw.js +101 -0
- phoenix/server/static/assets/{pages-CnTvEGEN.js → pages-1VrMk2pW.js} +559 -291
- phoenix/server/static/assets/{vendor-BC3OPQuM.js → vendor-B5IC0ivG.js} +5 -5
- phoenix/server/static/assets/{vendor-arizeai-NjB3cZzD.js → vendor-arizeai-aFbT4kl1.js} +2 -2
- phoenix/server/static/assets/{vendor-codemirror-gE_JCOgX.js → vendor-codemirror-BEGorXSV.js} +1 -1
- phoenix/server/static/assets/{vendor-recharts-BXLYwcXF.js → vendor-recharts-6nUU7gU_.js} +1 -1
- phoenix/server/templates/index.html +1 -0
- phoenix/server/types.py +157 -1
- phoenix/session/client.py +7 -2
- phoenix/utilities/client.py +16 -0
- phoenix/version.py +1 -1
- phoenix/db/migrations/future_versions/README.md +0 -4
- phoenix/db/migrations/future_versions/cd164e83824f_users_and_tokens.py +0 -293
- phoenix/db/migrations/versions/.gitignore +0 -1
- phoenix/server/api/mutations/auth.py +0 -18
- phoenix/server/api/mutations/auth_mutations.py +0 -65
- phoenix/server/static/assets/index-fq1-hCK4.js +0 -100
- phoenix/trace/langchain/__init__.py +0 -3
- phoenix/trace/langchain/instrumentor.py +0 -34
- phoenix/trace/llama_index/__init__.py +0 -3
- phoenix/trace/llama_index/callback.py +0 -102
- phoenix/trace/openai/__init__.py +0 -3
- phoenix/trace/openai/instrumentor.py +0 -30
- {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-4.36.0.dist-info → arize_phoenix-5.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import time
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Callable,
|
|
8
|
+
Coroutine,
|
|
9
|
+
DefaultDict,
|
|
10
|
+
List,
|
|
11
|
+
Optional,
|
|
12
|
+
Pattern, # import from re module when we drop support for 3.8
|
|
13
|
+
Union,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from fastapi import HTTPException, Request
|
|
17
|
+
|
|
18
|
+
from phoenix.config import get_env_enable_prometheus
|
|
19
|
+
from phoenix.exceptions import PhoenixException
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnavailableTokensError(PhoenixException):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TokenBucket:
|
|
27
|
+
"""
|
|
28
|
+
An implementation of the token-bucket algorithm for use as a rate limiter.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
per_second_request_rate (float): The allowed request rate.
|
|
32
|
+
enforcement_window_minutes (float): The time window over which the rate limit is enforced.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
per_second_request_rate: float,
|
|
38
|
+
enforcement_window_seconds: float = 1,
|
|
39
|
+
):
|
|
40
|
+
self.enforcement_window = enforcement_window_seconds
|
|
41
|
+
self.rate = per_second_request_rate
|
|
42
|
+
|
|
43
|
+
now = time.time()
|
|
44
|
+
self.last_checked = now
|
|
45
|
+
self.tokens = self.max_tokens()
|
|
46
|
+
|
|
47
|
+
def max_tokens(self) -> float:
|
|
48
|
+
return self.rate * self.enforcement_window
|
|
49
|
+
|
|
50
|
+
def available_tokens(self) -> float:
|
|
51
|
+
now = time.time()
|
|
52
|
+
time_since_last_checked = now - self.last_checked
|
|
53
|
+
self.tokens = min(self.max_tokens(), self.rate * time_since_last_checked + self.tokens)
|
|
54
|
+
self.last_checked = now
|
|
55
|
+
return self.tokens
|
|
56
|
+
|
|
57
|
+
def make_request_if_ready(self) -> None:
|
|
58
|
+
if self.available_tokens() < 1:
|
|
59
|
+
if get_env_enable_prometheus():
|
|
60
|
+
from phoenix.server.prometheus import RATE_LIMITER_THROTTLES
|
|
61
|
+
|
|
62
|
+
RATE_LIMITER_THROTTLES.inc()
|
|
63
|
+
raise UnavailableTokensError
|
|
64
|
+
self.tokens -= 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ServerRateLimiter:
|
|
68
|
+
"""
|
|
69
|
+
This rate limiter holds a cache of token buckets that enforce rate limits.
|
|
70
|
+
|
|
71
|
+
The cache is kept in partitions that rotate every `partition_seconds`. Each user's rate limiter
|
|
72
|
+
can be accessed from all active partitions, the number of active partitions is set with
|
|
73
|
+
`active_partitions`. This guarantees that a user's rate limiter will sit in the cache for at
|
|
74
|
+
least:
|
|
75
|
+
|
|
76
|
+
minimum_cache_lifetime = (active_partitions - 1) * partition_seconds
|
|
77
|
+
|
|
78
|
+
Every time the cache is accessed, inactive partitions are purged. If enough time has passed,
|
|
79
|
+
the entire cache is purged.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
per_second_rate_limit: float = 0.5,
|
|
85
|
+
enforcement_window_seconds: float = 5,
|
|
86
|
+
partition_seconds: float = 60,
|
|
87
|
+
active_partitions: int = 2,
|
|
88
|
+
):
|
|
89
|
+
self.bucket_factory = partial(
|
|
90
|
+
TokenBucket,
|
|
91
|
+
per_second_request_rate=per_second_rate_limit,
|
|
92
|
+
enforcement_window_seconds=enforcement_window_seconds,
|
|
93
|
+
)
|
|
94
|
+
self.partition_seconds = partition_seconds
|
|
95
|
+
self.active_partitions = active_partitions
|
|
96
|
+
self.num_partitions = active_partitions + 2 # two overflow partitions to avoid edge cases
|
|
97
|
+
self._reset_rate_limiters()
|
|
98
|
+
self._last_cleanup_time = time.time()
|
|
99
|
+
|
|
100
|
+
def _reset_rate_limiters(self) -> None:
|
|
101
|
+
self.cache_partitions: List[DefaultDict[Any, TokenBucket]] = [
|
|
102
|
+
defaultdict(self.bucket_factory) for _ in range(self.num_partitions)
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def _current_partition_index(self, timestamp: float) -> int:
|
|
106
|
+
return (
|
|
107
|
+
int(timestamp // self.partition_seconds) % self.num_partitions
|
|
108
|
+
) # a cyclic bucket index
|
|
109
|
+
|
|
110
|
+
def _active_partition_indices(self, current_index: int) -> List[int]:
|
|
111
|
+
return [(current_index - ii) % self.num_partitions for ii in range(self.active_partitions)]
|
|
112
|
+
|
|
113
|
+
def _inactive_partition_indices(self, current_index: int) -> List[int]:
|
|
114
|
+
active_indices = set(self._active_partition_indices(current_index))
|
|
115
|
+
all_indices = set(range(self.num_partitions))
|
|
116
|
+
return list(all_indices - active_indices)
|
|
117
|
+
|
|
118
|
+
def _cleanup_expired_limiters(self, request_time: float) -> None:
|
|
119
|
+
time_since_last_cleanup = request_time - self._last_cleanup_time
|
|
120
|
+
if time_since_last_cleanup >= ((self.num_partitions - 1) * self.partition_seconds):
|
|
121
|
+
# Reset the cache to avoid "looping" back to the same partitions
|
|
122
|
+
self._reset_rate_limiters()
|
|
123
|
+
self._last_cleanup_time = request_time
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
current_partition_index = self._current_partition_index(request_time)
|
|
127
|
+
inactive_indices = self._inactive_partition_indices(current_partition_index)
|
|
128
|
+
for ii in inactive_indices:
|
|
129
|
+
self.cache_partitions[ii] = defaultdict(self.bucket_factory)
|
|
130
|
+
self._last_cleanup_time = request_time
|
|
131
|
+
|
|
132
|
+
def _fetch_token_bucket(self, key: str, request_time: float) -> TokenBucket:
|
|
133
|
+
current_partition_index = self._current_partition_index(request_time)
|
|
134
|
+
active_indices = self._active_partition_indices(current_partition_index)
|
|
135
|
+
bucket: Optional[TokenBucket] = None
|
|
136
|
+
for ii in active_indices:
|
|
137
|
+
partition = self.cache_partitions[ii]
|
|
138
|
+
if key in partition:
|
|
139
|
+
bucket = partition.pop(key)
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
current_partition = self.cache_partitions[current_partition_index]
|
|
143
|
+
if key not in current_partition and bucket is not None:
|
|
144
|
+
current_partition[key] = bucket
|
|
145
|
+
return current_partition[key]
|
|
146
|
+
|
|
147
|
+
def make_request(self, key: str) -> None:
|
|
148
|
+
request_time = time.time()
|
|
149
|
+
self._cleanup_expired_limiters(request_time)
|
|
150
|
+
rate_limiter = self._fetch_token_bucket(key, request_time)
|
|
151
|
+
rate_limiter.make_request_if_ready()
|
|
152
|
+
if get_env_enable_prometheus():
|
|
153
|
+
from phoenix.server.prometheus import RATE_LIMITER_CACHE_SIZE
|
|
154
|
+
|
|
155
|
+
RATE_LIMITER_CACHE_SIZE.set(sum(len(partition) for partition in self.cache_partitions))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def fastapi_ip_rate_limiter(
|
|
159
|
+
rate_limiter: ServerRateLimiter, paths: Optional[List[Union[str, Pattern[str]]]] = None
|
|
160
|
+
) -> Callable[[Request], Coroutine[Any, Any, Request]]:
|
|
161
|
+
async def dependency(request: Request) -> Request:
|
|
162
|
+
if paths is None or any(path_match(request.url.path, path) for path in paths):
|
|
163
|
+
client = request.client
|
|
164
|
+
if client: # bypasses rate limiter if no client
|
|
165
|
+
client_ip = client.host
|
|
166
|
+
try:
|
|
167
|
+
rate_limiter.make_request(client_ip)
|
|
168
|
+
except UnavailableTokensError:
|
|
169
|
+
raise HTTPException(status_code=429, detail="Too Many Requests")
|
|
170
|
+
return request
|
|
171
|
+
|
|
172
|
+
return dependency
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def fastapi_route_rate_limiter(
|
|
176
|
+
rate_limiter: ServerRateLimiter,
|
|
177
|
+
) -> Callable[[Request], Coroutine[Any, Any, Request]]:
|
|
178
|
+
async def dependency(request: Request) -> Request:
|
|
179
|
+
try:
|
|
180
|
+
rate_limiter.make_request(request.url.path)
|
|
181
|
+
except UnavailableTokensError:
|
|
182
|
+
raise HTTPException(status_code=429, detail="Too Many Requests")
|
|
183
|
+
return request
|
|
184
|
+
|
|
185
|
+
return dependency
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def path_match(path: str, match_pattern: Union[str, Pattern[str]]) -> bool:
|
|
189
|
+
if isinstance(match_pattern, re.Pattern):
|
|
190
|
+
return bool(match_pattern.match(path))
|
|
191
|
+
return path == match_pattern
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_components-
|
|
3
|
-
"file": "assets/components-
|
|
2
|
+
"_components-REunxTt6.js": {
|
|
3
|
+
"file": "assets/components-REunxTt6.js",
|
|
4
4
|
"name": "components",
|
|
5
5
|
"imports": [
|
|
6
|
-
"_vendor-
|
|
7
|
-
"_vendor-arizeai-
|
|
8
|
-
"_pages-
|
|
6
|
+
"_vendor-B5IC0ivG.js",
|
|
7
|
+
"_vendor-arizeai-aFbT4kl1.js",
|
|
8
|
+
"_pages-1VrMk2pW.js",
|
|
9
9
|
"_vendor-three-DwGkEfCM.js",
|
|
10
|
-
"_vendor-codemirror-
|
|
10
|
+
"_vendor-codemirror-BEGorXSV.js"
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
|
-
"_pages-
|
|
14
|
-
"file": "assets/pages-
|
|
13
|
+
"_pages-1VrMk2pW.js": {
|
|
14
|
+
"file": "assets/pages-1VrMk2pW.js",
|
|
15
15
|
"name": "pages",
|
|
16
16
|
"imports": [
|
|
17
|
-
"_vendor-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"_vendor-recharts-
|
|
21
|
-
"_vendor-codemirror-
|
|
17
|
+
"_vendor-B5IC0ivG.js",
|
|
18
|
+
"_vendor-arizeai-aFbT4kl1.js",
|
|
19
|
+
"_components-REunxTt6.js",
|
|
20
|
+
"_vendor-recharts-6nUU7gU_.js",
|
|
21
|
+
"_vendor-codemirror-BEGorXSV.js"
|
|
22
22
|
]
|
|
23
23
|
},
|
|
24
24
|
"_vendor-!~{003}~.js": {
|
|
25
25
|
"file": "assets/vendor-DxkFTwjz.css",
|
|
26
26
|
"src": "_vendor-!~{003}~.js"
|
|
27
27
|
},
|
|
28
|
-
"_vendor-
|
|
29
|
-
"file": "assets/vendor-
|
|
28
|
+
"_vendor-B5IC0ivG.js": {
|
|
29
|
+
"file": "assets/vendor-B5IC0ivG.js",
|
|
30
30
|
"name": "vendor",
|
|
31
31
|
"imports": [
|
|
32
32
|
"_vendor-three-DwGkEfCM.js"
|
|
@@ -35,25 +35,25 @@
|
|
|
35
35
|
"assets/vendor-DxkFTwjz.css"
|
|
36
36
|
]
|
|
37
37
|
},
|
|
38
|
-
"_vendor-arizeai-
|
|
39
|
-
"file": "assets/vendor-arizeai-
|
|
38
|
+
"_vendor-arizeai-aFbT4kl1.js": {
|
|
39
|
+
"file": "assets/vendor-arizeai-aFbT4kl1.js",
|
|
40
40
|
"name": "vendor-arizeai",
|
|
41
41
|
"imports": [
|
|
42
|
-
"_vendor-
|
|
42
|
+
"_vendor-B5IC0ivG.js"
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
|
-
"_vendor-codemirror-
|
|
46
|
-
"file": "assets/vendor-codemirror-
|
|
45
|
+
"_vendor-codemirror-BEGorXSV.js": {
|
|
46
|
+
"file": "assets/vendor-codemirror-BEGorXSV.js",
|
|
47
47
|
"name": "vendor-codemirror",
|
|
48
48
|
"imports": [
|
|
49
|
-
"_vendor-
|
|
49
|
+
"_vendor-B5IC0ivG.js"
|
|
50
50
|
]
|
|
51
51
|
},
|
|
52
|
-
"_vendor-recharts-
|
|
53
|
-
"file": "assets/vendor-recharts-
|
|
52
|
+
"_vendor-recharts-6nUU7gU_.js": {
|
|
53
|
+
"file": "assets/vendor-recharts-6nUU7gU_.js",
|
|
54
54
|
"name": "vendor-recharts",
|
|
55
55
|
"imports": [
|
|
56
|
-
"_vendor-
|
|
56
|
+
"_vendor-B5IC0ivG.js"
|
|
57
57
|
]
|
|
58
58
|
},
|
|
59
59
|
"_vendor-three-DwGkEfCM.js": {
|
|
@@ -61,18 +61,18 @@
|
|
|
61
61
|
"name": "vendor-three"
|
|
62
62
|
},
|
|
63
63
|
"index.tsx": {
|
|
64
|
-
"file": "assets/index-
|
|
64
|
+
"file": "assets/index-DAPJxlCw.js",
|
|
65
65
|
"name": "index",
|
|
66
66
|
"src": "index.tsx",
|
|
67
67
|
"isEntry": true,
|
|
68
68
|
"imports": [
|
|
69
|
-
"_vendor-
|
|
70
|
-
"_vendor-arizeai-
|
|
71
|
-
"_pages-
|
|
72
|
-
"_components-
|
|
69
|
+
"_vendor-B5IC0ivG.js",
|
|
70
|
+
"_vendor-arizeai-aFbT4kl1.js",
|
|
71
|
+
"_pages-1VrMk2pW.js",
|
|
72
|
+
"_components-REunxTt6.js",
|
|
73
73
|
"_vendor-three-DwGkEfCM.js",
|
|
74
|
-
"_vendor-recharts-
|
|
75
|
-
"_vendor-codemirror-
|
|
74
|
+
"_vendor-recharts-6nUU7gU_.js",
|
|
75
|
+
"_vendor-codemirror-BEGorXSV.js"
|
|
76
76
|
]
|
|
77
77
|
}
|
|
78
78
|
}
|