fiddler-client 2.3.0.dev2__py3-none-any.whl → 2.4.0.dev1__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.
- fiddler/__init__.py +2 -0
- fiddler/_version.py +1 -1
- fiddler/api/alert_mixin.py +1 -1
- fiddler/api/api.py +2 -0
- fiddler/api/webhooks_mixin.py +9 -7
- fiddler/core_objects.py +98 -47
- fiddler/schemas/custom_features.py +25 -19
- fiddler3/constants/baseline.py +19 -0
- fiddler3/constants/common.py +12 -0
- fiddler3/constants/dataset.py +7 -0
- fiddler3/constants/model_deployment.py +15 -0
- fiddler3/constants/xai.py +11 -0
- fiddler3/entities/__init__.py +5 -1
- fiddler3/entities/base.py +22 -18
- fiddler3/entities/baseline.py +228 -0
- fiddler3/entities/dataset.py +149 -0
- fiddler3/entities/job.py +9 -8
- fiddler3/entities/model.py +160 -29
- fiddler3/entities/model_artifact.py +350 -0
- fiddler3/entities/model_deployment.py +112 -0
- fiddler3/entities/model_surrogate.py +92 -0
- fiddler3/entities/organization.py +38 -0
- fiddler3/entities/project.py +69 -8
- fiddler3/entities/user.py +45 -0
- fiddler3/entities/xai.py +309 -0
- fiddler3/libs/http_client.py +24 -13
- fiddler3/libs/json_encoder.py +12 -0
- fiddler3/schemas/__init__.py +4 -0
- fiddler3/schemas/baseline.py +37 -0
- fiddler3/schemas/custom_features.py +23 -18
- fiddler3/schemas/dataset.py +27 -0
- fiddler3/schemas/deployment_params.py +25 -0
- fiddler3/schemas/filter_query.py +54 -0
- fiddler3/schemas/job.py +2 -2
- fiddler3/schemas/model.py +9 -9
- fiddler3/schemas/model_artifact.py +6 -0
- fiddler3/schemas/model_deployment.py +26 -0
- fiddler3/schemas/organization.py +9 -1
- fiddler3/schemas/project.py +4 -4
- fiddler3/schemas/server_info.py +2 -2
- fiddler3/schemas/user.py +1 -1
- fiddler3/schemas/xai.py +32 -0
- fiddler3/tests/apis/test_baseline.py +292 -0
- fiddler3/tests/apis/test_dataset.py +173 -0
- fiddler3/tests/apis/test_mixin.py +77 -0
- fiddler3/tests/apis/test_model.py +102 -7
- fiddler3/tests/apis/test_model_artifact.py +175 -0
- fiddler3/tests/apis/test_model_deployment.py +98 -0
- fiddler3/tests/apis/test_model_surrogate.py +159 -0
- fiddler3/tests/apis/test_project.py +36 -20
- fiddler3/tests/apis/test_xai.py +690 -0
- fiddler3/tests/constants.py +10 -0
- fiddler3/tests/test_json_encoder.py +16 -0
- fiddler3/tests/test_logger.py +12 -0
- fiddler3/tests/test_utils.py +19 -0
- fiddler3/utils/__init__.py +1 -0
- fiddler3/utils/helpers.py +43 -0
- fiddler3/utils/logger.py +15 -0
- fiddler3/utils/validations.py +11 -0
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/METADATA +7 -5
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/RECORD +65 -33
- tests/fiddler/test_segments.py +227 -0
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/LICENSE.txt +0 -0
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/WHEEL +0 -0
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/top_level.txt +0 -0
fiddler3/entities/xai.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from fiddler3.constants.xai import ExplainMethod
|
|
13
|
+
from fiddler3.decorators import handle_api_error
|
|
14
|
+
from fiddler3.schemas.xai import (
|
|
15
|
+
DatasetDataSource,
|
|
16
|
+
EventIdDataSource,
|
|
17
|
+
RowDataSource,
|
|
18
|
+
SqlSliceQueryDataSource,
|
|
19
|
+
)
|
|
20
|
+
from fiddler3.utils.helpers import try_series_retype
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class XaiMixin:
|
|
24
|
+
id: UUID | None
|
|
25
|
+
_client: Callable
|
|
26
|
+
|
|
27
|
+
@handle_api_error
|
|
28
|
+
def explain( # pylint: disable=too-many-arguments
|
|
29
|
+
self,
|
|
30
|
+
input_data_source: RowDataSource | EventIdDataSource,
|
|
31
|
+
ref_data_source: DatasetDataSource | SqlSliceQueryDataSource | None = None,
|
|
32
|
+
method: ExplainMethod | str = ExplainMethod.FIDDLER_SHAP,
|
|
33
|
+
num_permutations: int | None = None,
|
|
34
|
+
ci_level: float | None = None,
|
|
35
|
+
top_n_class: int | None = None,
|
|
36
|
+
) -> tuple:
|
|
37
|
+
"""
|
|
38
|
+
Get explanation for a single observation.
|
|
39
|
+
|
|
40
|
+
:param input_data_source: DataSource for the input data to compute explanation
|
|
41
|
+
on (RowDataSource, EventIdDataSource)
|
|
42
|
+
:param ref_data_source: DataSource for the reference data to compute explanation
|
|
43
|
+
on (DatasetDataSource, SqlSliceQueryDataSource).
|
|
44
|
+
Only used for non-text models and the following methods:
|
|
45
|
+
'SHAP', 'FIDDLER_SHAP', 'PERMUTE', 'MEAN_RESET'
|
|
46
|
+
:param method: Explanation method name. Could be your custom
|
|
47
|
+
explanation method or one of the following method:
|
|
48
|
+
'SHAP', 'FIDDLER_SHAP', 'IG', 'PERMUTE', 'MEAN_RESET', 'ZERO_RESET'
|
|
49
|
+
:param num_permutations: For Fiddler SHAP, that corresponds to the number of
|
|
50
|
+
coalitions to sample to estimate the Shapley values of each single-reference
|
|
51
|
+
game. For the permutation algorithms, this corresponds to the number
|
|
52
|
+
of permutations from the dataset to use for the computation.
|
|
53
|
+
:param ci_level: The confidence level (between 0 and 1) to use for the
|
|
54
|
+
confidence intervals in Fiddler SHAP. Not used for other methods.
|
|
55
|
+
:param top_n_class: For multiclass classification models only, specifying if
|
|
56
|
+
only the n top classes are computed or all classes (when parameter is None)
|
|
57
|
+
|
|
58
|
+
:return: A named tuple with the explanation results.
|
|
59
|
+
"""
|
|
60
|
+
self._check_id_attributes()
|
|
61
|
+
payload: dict[str, Any] = {
|
|
62
|
+
'model_id': self.id,
|
|
63
|
+
'input_data_source': input_data_source.dict(),
|
|
64
|
+
'explanation_type': method,
|
|
65
|
+
}
|
|
66
|
+
if ref_data_source:
|
|
67
|
+
payload['ref_data_source'] = ref_data_source.dict()
|
|
68
|
+
if num_permutations:
|
|
69
|
+
payload['num_permutations'] = num_permutations
|
|
70
|
+
if ci_level:
|
|
71
|
+
payload['ci_level'] = ci_level
|
|
72
|
+
if top_n_class:
|
|
73
|
+
payload['top_n_class'] = top_n_class
|
|
74
|
+
|
|
75
|
+
response = self._client().post(
|
|
76
|
+
url='v3/explain',
|
|
77
|
+
data=payload,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return namedtuple('Explain', response.json()['data'])(**response.json()['data'])
|
|
81
|
+
|
|
82
|
+
@handle_api_error
|
|
83
|
+
def get_fairness(
|
|
84
|
+
self,
|
|
85
|
+
data_source: DatasetDataSource | SqlSliceQueryDataSource,
|
|
86
|
+
protected_features: list[str],
|
|
87
|
+
positive_outcome: str | int | float | bool,
|
|
88
|
+
score_threshold: float | None = None,
|
|
89
|
+
) -> tuple:
|
|
90
|
+
"""
|
|
91
|
+
Get fairness analysis on a dataset or a slice.
|
|
92
|
+
|
|
93
|
+
:param data_source: DataSource for the input dataset to compute
|
|
94
|
+
fairness on (DatasetDataSource or SqlSliceQueryDataSource)
|
|
95
|
+
:param protected_features: list of protected attribute names to compute
|
|
96
|
+
Fairness analysis on
|
|
97
|
+
:param positive_outcome: name of the positive outcome to compute
|
|
98
|
+
Fairness analysis on
|
|
99
|
+
:param score_threshold: Binary threshold value (between 0 and 1). Default to 0.5
|
|
100
|
+
|
|
101
|
+
:return: A named tuple with the fairness results.
|
|
102
|
+
"""
|
|
103
|
+
self._check_id_attributes()
|
|
104
|
+
|
|
105
|
+
payload: dict[str, Any] = {
|
|
106
|
+
'model_id': self.id,
|
|
107
|
+
'data_source': data_source.dict(),
|
|
108
|
+
'protected_features': protected_features,
|
|
109
|
+
'positive_outcome': positive_outcome,
|
|
110
|
+
}
|
|
111
|
+
if score_threshold:
|
|
112
|
+
payload['score_threshold'] = score_threshold
|
|
113
|
+
|
|
114
|
+
response = self._client().post(url='/v3/analytics/fairness', data=payload)
|
|
115
|
+
|
|
116
|
+
return namedtuple('Fairness', response.json()['data'])(
|
|
117
|
+
**response.json()['data']
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@handle_api_error
|
|
121
|
+
def get_slice(
|
|
122
|
+
self,
|
|
123
|
+
query: str,
|
|
124
|
+
sample: bool = False,
|
|
125
|
+
max_rows: int | None = None,
|
|
126
|
+
columns: list[str] | None = None,
|
|
127
|
+
) -> pd.DataFrame:
|
|
128
|
+
"""
|
|
129
|
+
Fetch data with slice query.
|
|
130
|
+
|
|
131
|
+
:param query: An SQL query that begins with the keyword 'SELECT'
|
|
132
|
+
:param columns: Allows caller to explicitly specify list of
|
|
133
|
+
columns to select overriding columns selected in the query.
|
|
134
|
+
:param max_rows: Number of maximum rows to fetch
|
|
135
|
+
:param sample: Whether rows should be sample or not from the database
|
|
136
|
+
|
|
137
|
+
:return: Dataframe of the query output
|
|
138
|
+
"""
|
|
139
|
+
self._check_id_attributes()
|
|
140
|
+
|
|
141
|
+
payload: dict[str, Any] = {
|
|
142
|
+
'model_id': self.id,
|
|
143
|
+
'query': query,
|
|
144
|
+
'sample': sample,
|
|
145
|
+
}
|
|
146
|
+
if max_rows:
|
|
147
|
+
payload['max_rows'] = max_rows
|
|
148
|
+
if columns:
|
|
149
|
+
payload['columns'] = columns
|
|
150
|
+
|
|
151
|
+
response = self._client().post(
|
|
152
|
+
url='/v3/slice-query/fetch',
|
|
153
|
+
data=payload,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
response_dict = response.json()['data']
|
|
157
|
+
|
|
158
|
+
column_names = response_dict['metadata']['columns']
|
|
159
|
+
dtype_strings = response_dict['metadata']['dtypes']
|
|
160
|
+
df = pd.DataFrame(response_dict['rows'], columns=column_names)
|
|
161
|
+
for column_name, dtype in zip(column_names, dtype_strings):
|
|
162
|
+
df[column_name] = try_series_retype(df[column_name], dtype)
|
|
163
|
+
return df
|
|
164
|
+
|
|
165
|
+
@handle_api_error
|
|
166
|
+
def download_slice( # pylint: disable=too-many-arguments
|
|
167
|
+
self,
|
|
168
|
+
output_dir: Path | str,
|
|
169
|
+
query: str,
|
|
170
|
+
sample: bool = False,
|
|
171
|
+
max_rows: int | None = None,
|
|
172
|
+
columns: list[str] | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Download data with slice query to parquet file.
|
|
176
|
+
|
|
177
|
+
:param output_dir: Path to download the file
|
|
178
|
+
:param query: An SQL query that begins with the keyword 'SELECT'
|
|
179
|
+
:param columns: Allows caller to explicitly specify list of
|
|
180
|
+
columns to select overriding columns selected in the query.
|
|
181
|
+
:param max_rows: Number of maximum rows to fetch
|
|
182
|
+
:param sample: Whether rows should be sample or not from the database
|
|
183
|
+
"""
|
|
184
|
+
self._check_id_attributes()
|
|
185
|
+
output_dir = Path(output_dir)
|
|
186
|
+
if output_dir.exists():
|
|
187
|
+
raise ValueError(f'Output dir already exists {output_dir}')
|
|
188
|
+
payload: dict[str, Any] = {
|
|
189
|
+
'model_id': self.id,
|
|
190
|
+
'query': query,
|
|
191
|
+
'sample': sample,
|
|
192
|
+
}
|
|
193
|
+
if max_rows:
|
|
194
|
+
payload['max_rows'] = max_rows
|
|
195
|
+
if columns:
|
|
196
|
+
payload['columns'] = columns
|
|
197
|
+
|
|
198
|
+
os.makedirs(output_dir)
|
|
199
|
+
file_path = os.path.join(output_dir, 'output.parquet')
|
|
200
|
+
with self._client().post(url='/v3/slice-query/download', data=payload) as resp:
|
|
201
|
+
# Download parquet file
|
|
202
|
+
df = pd.read_parquet(BytesIO(resp.content))
|
|
203
|
+
df.to_parquet(file_path)
|
|
204
|
+
|
|
205
|
+
@handle_api_error
|
|
206
|
+
def get_mutual_info(
|
|
207
|
+
self,
|
|
208
|
+
query: str,
|
|
209
|
+
column_name: str,
|
|
210
|
+
num_samples: int | None = None,
|
|
211
|
+
normalized: bool = False,
|
|
212
|
+
) -> dict:
|
|
213
|
+
"""
|
|
214
|
+
Get mutual information.
|
|
215
|
+
|
|
216
|
+
The Mutual information measures the dependency between
|
|
217
|
+
two random variables. It's a non-negative value. If two random variables are
|
|
218
|
+
independent MI is equal to zero. Higher MI values means higher dependency.
|
|
219
|
+
|
|
220
|
+
:param query: slice query to compute Mutual information on
|
|
221
|
+
:param column_name: column name to compute mutual information with respect to
|
|
222
|
+
all the variables in the dataset.
|
|
223
|
+
:param num_samples: Number of samples to select for computation
|
|
224
|
+
:param normalized: If set to True, it will compute Normalized Mutual Information
|
|
225
|
+
|
|
226
|
+
:return: a dictionary of mutual information w.r.t the given feature
|
|
227
|
+
for each column given
|
|
228
|
+
"""
|
|
229
|
+
self._check_id_attributes()
|
|
230
|
+
|
|
231
|
+
payload: dict[str, Any] = {
|
|
232
|
+
'model_id': self.id,
|
|
233
|
+
'query': query,
|
|
234
|
+
'column_name': column_name,
|
|
235
|
+
'normalized': normalized,
|
|
236
|
+
}
|
|
237
|
+
if num_samples:
|
|
238
|
+
payload['num_samples'] = num_samples
|
|
239
|
+
|
|
240
|
+
response = self._client().post(
|
|
241
|
+
url='/v3/analytics/mutual-info',
|
|
242
|
+
data=payload,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return response.json()['data']
|
|
246
|
+
|
|
247
|
+
@handle_api_error
|
|
248
|
+
def predict(
|
|
249
|
+
self,
|
|
250
|
+
df: pd.DataFrame,
|
|
251
|
+
chunk_size: int | None = None,
|
|
252
|
+
) -> pd.DataFrame:
|
|
253
|
+
"""
|
|
254
|
+
Run model on an input dataframe.
|
|
255
|
+
|
|
256
|
+
:param df: Feature dataframe
|
|
257
|
+
:param chunk_size: Chunk size for fetching predictions
|
|
258
|
+
|
|
259
|
+
:return: Dataframe of the predictions
|
|
260
|
+
"""
|
|
261
|
+
self._check_id_attributes()
|
|
262
|
+
|
|
263
|
+
payload: dict[str, Any] = {
|
|
264
|
+
'model_id': self.id,
|
|
265
|
+
'data': df.to_dict('records'),
|
|
266
|
+
}
|
|
267
|
+
if chunk_size:
|
|
268
|
+
payload['chunk_size'] = chunk_size
|
|
269
|
+
|
|
270
|
+
response = self._client().post(
|
|
271
|
+
url='/v3/predict',
|
|
272
|
+
data=payload,
|
|
273
|
+
)
|
|
274
|
+
return pd.DataFrame(response.json()['data']['predictions'])
|
|
275
|
+
|
|
276
|
+
@handle_api_error
|
|
277
|
+
def get_precomputed_feature_importance(self) -> tuple:
|
|
278
|
+
"""Get precomputed feature importance for a model"""
|
|
279
|
+
|
|
280
|
+
self._check_id_attributes()
|
|
281
|
+
response = self._client().post(
|
|
282
|
+
url='/v3/analytics/feature-importance/precomputed',
|
|
283
|
+
data={'model_id': self.id},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return namedtuple('FeatureImportance', response.json()['data'])(
|
|
287
|
+
**response.json()['data']
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
@handle_api_error
|
|
291
|
+
def get_precomputed_feature_impact(self) -> tuple:
|
|
292
|
+
"""Get precomputed feature impact for a model"""
|
|
293
|
+
|
|
294
|
+
self._check_id_attributes()
|
|
295
|
+
response = self._client().post(
|
|
296
|
+
url='/v3/analytics/feature-impact/precomputed',
|
|
297
|
+
data={'model_id': self.id},
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return namedtuple('FeatureImpact', response.json()['data'])(
|
|
301
|
+
**response.json()['data']
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def _check_id_attributes(self) -> None:
|
|
305
|
+
if not self.id:
|
|
306
|
+
raise AttributeError(
|
|
307
|
+
'This method is available only for model object generated from '
|
|
308
|
+
'API response.'
|
|
309
|
+
)
|
fiddler3/libs/http_client.py
CHANGED
|
@@ -10,7 +10,12 @@ from requests.adapters import HTTPAdapter
|
|
|
10
10
|
from requests_toolbelt.multipart.encoder import MultipartEncoder
|
|
11
11
|
|
|
12
12
|
from fiddler3.constants.common import JSON_CONTENT_TYPE
|
|
13
|
-
from fiddler3.exceptions import
|
|
13
|
+
from fiddler3.exceptions import ( # pylint: disable=redefined-builtin
|
|
14
|
+
ConnectionError,
|
|
15
|
+
ConnectionTimeout,
|
|
16
|
+
HttpError,
|
|
17
|
+
)
|
|
18
|
+
from fiddler3.libs.json_encoder import RequestClientJSONEncoder
|
|
14
19
|
from fiddler3.utils.logger import get_logger
|
|
15
20
|
|
|
16
21
|
logger = get_logger(__name__)
|
|
@@ -24,6 +29,7 @@ class RequestClient:
|
|
|
24
29
|
verify: bool = True,
|
|
25
30
|
proxies: dict | None = None,
|
|
26
31
|
) -> None:
|
|
32
|
+
"""Construct a request instance."""
|
|
27
33
|
self.base_url = base_url
|
|
28
34
|
self.proxies = proxies
|
|
29
35
|
self.headers = headers
|
|
@@ -43,7 +49,7 @@ class RequestClient:
|
|
|
43
49
|
url: str,
|
|
44
50
|
params: dict | None = None,
|
|
45
51
|
headers: dict | None = None,
|
|
46
|
-
data: dict | None = None,
|
|
52
|
+
data: dict | bytes | None = None,
|
|
47
53
|
timeout: int | None = None,
|
|
48
54
|
files_encoders: dict[str, MultipartEncoder] | None = None,
|
|
49
55
|
**kwargs: Any,
|
|
@@ -73,7 +79,7 @@ class RequestClient:
|
|
|
73
79
|
|
|
74
80
|
content_type = request_headers.get('Content-Type')
|
|
75
81
|
if data and content_type == JSON_CONTENT_TYPE:
|
|
76
|
-
data = simplejson.dumps(data, ignore_nan=True) # type: ignore
|
|
82
|
+
data = simplejson.dumps(data, ignore_nan=True, cls=RequestClientJSONEncoder) # type: ignore
|
|
77
83
|
|
|
78
84
|
kwargs.setdefault('allow_redirects', True)
|
|
79
85
|
# requests is not able to pass the value of self.session.verify to the
|
|
@@ -107,16 +113,16 @@ class RequestClient:
|
|
|
107
113
|
**kwargs,
|
|
108
114
|
)
|
|
109
115
|
|
|
110
|
-
except requests.Timeout:
|
|
111
|
-
logger.error(
|
|
112
|
-
raise ConnectionTimeout()
|
|
113
|
-
except requests.ConnectionError:
|
|
114
|
-
logger.error(
|
|
115
|
-
raise ConnectionError()
|
|
116
|
-
except requests.RequestException:
|
|
116
|
+
except requests.Timeout as exc:
|
|
117
|
+
logger.error('%s call failed with Timeout error for URL %s', method, url)
|
|
118
|
+
raise ConnectionTimeout() from exc
|
|
119
|
+
except requests.ConnectionError as exc:
|
|
120
|
+
logger.error('%s call failed with ConnectionError for URL %s', method, url)
|
|
121
|
+
raise ConnectionError() from exc
|
|
122
|
+
except requests.RequestException as exc:
|
|
117
123
|
# catastrophic error.
|
|
118
|
-
logger.error(
|
|
119
|
-
raise HttpError()
|
|
124
|
+
logger.error('%s call failed with RequestException for %s', method, url)
|
|
125
|
+
raise HttpError() from exc
|
|
120
126
|
|
|
121
127
|
response.raise_for_status()
|
|
122
128
|
return response
|
|
@@ -130,6 +136,7 @@ class RequestClient:
|
|
|
130
136
|
timeout: int | None = None,
|
|
131
137
|
**kwargs: dict[str, Any],
|
|
132
138
|
) -> requests.Response:
|
|
139
|
+
"""Construct a get request instance."""
|
|
133
140
|
return self.call(
|
|
134
141
|
method='GET',
|
|
135
142
|
url=url,
|
|
@@ -148,6 +155,7 @@ class RequestClient:
|
|
|
148
155
|
timeout: int | None = None,
|
|
149
156
|
**kwargs: dict[str, Any],
|
|
150
157
|
) -> requests.Response:
|
|
158
|
+
"""Construct a delete request instance."""
|
|
151
159
|
return self.call(
|
|
152
160
|
method='DELETE',
|
|
153
161
|
url=url,
|
|
@@ -164,10 +172,11 @@ class RequestClient:
|
|
|
164
172
|
params: dict | None = None,
|
|
165
173
|
headers: dict | None = None,
|
|
166
174
|
timeout: int | None = None,
|
|
167
|
-
data: dict | None = None,
|
|
175
|
+
data: dict | bytes | None = None,
|
|
168
176
|
files_encoders: dict[str, MultipartEncoder] | None = None,
|
|
169
177
|
**kwargs: dict[str, Any],
|
|
170
178
|
) -> requests.Response:
|
|
179
|
+
"""Construct a post request instance."""
|
|
171
180
|
return self.call(
|
|
172
181
|
method='POST',
|
|
173
182
|
url=url,
|
|
@@ -189,6 +198,7 @@ class RequestClient:
|
|
|
189
198
|
data: dict | None = None,
|
|
190
199
|
**kwargs: dict[str, Any],
|
|
191
200
|
) -> requests.Response:
|
|
201
|
+
"""Construct a put request instance."""
|
|
192
202
|
return self.call(
|
|
193
203
|
method='PUT',
|
|
194
204
|
url=url,
|
|
@@ -209,6 +219,7 @@ class RequestClient:
|
|
|
209
219
|
data: dict | None = None,
|
|
210
220
|
**kwargs: dict[str, Any],
|
|
211
221
|
) -> requests.Response:
|
|
222
|
+
"""Construct a potch request instance."""
|
|
212
223
|
return self.call(
|
|
213
224
|
method='PATCH',
|
|
214
225
|
url=url,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from simplejson import JSONEncoder
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RequestClientJSONEncoder(JSONEncoder):
|
|
8
|
+
def default(self, o: Any) -> Any:
|
|
9
|
+
"""Override JSONEncoder.default to support uuid serialization"""
|
|
10
|
+
if isinstance(o, UUID):
|
|
11
|
+
return o.hex
|
|
12
|
+
return super().default(o)
|
fiddler3/schemas/__init__.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
"""Initialize all schemas."""
|
|
1
2
|
from fiddler3.schemas.custom_features import * # noqa
|
|
3
|
+
from fiddler3.schemas.dataset import EnvType # noqa
|
|
4
|
+
from fiddler3.schemas.deployment_params import DeploymentParams # noqa
|
|
2
5
|
from fiddler3.schemas.model_schema import ModelSchema # noqa
|
|
3
6
|
from fiddler3.schemas.model_spec import ModelSpec # noqa
|
|
4
7
|
from fiddler3.schemas.model_task_params import ModelTaskParams # noqa
|
|
8
|
+
from fiddler3.schemas.xai import * # noqa
|
|
5
9
|
from fiddler3.schemas.xai_params import XaiParams # noqa
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from fiddler3.schemas.base import BaseModel
|
|
8
|
+
from fiddler3.schemas.dataset import DatasetCompactResp
|
|
9
|
+
from fiddler3.schemas.model import ModelCompactResp
|
|
10
|
+
from fiddler3.schemas.organization import OrganizationCompactResp
|
|
11
|
+
from fiddler3.schemas.project import ProjectCompactResp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaselineCompactResp(BaseModel):
|
|
15
|
+
id: UUID
|
|
16
|
+
name: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaselineResp(BaseModel):
|
|
20
|
+
"""Baseline response pydantic model."""
|
|
21
|
+
|
|
22
|
+
id: UUID
|
|
23
|
+
name: str
|
|
24
|
+
type: str
|
|
25
|
+
start_time: Optional[int]
|
|
26
|
+
end_time: Optional[int]
|
|
27
|
+
offset: Optional[int]
|
|
28
|
+
window_size: Optional[int]
|
|
29
|
+
row_count: Optional[int]
|
|
30
|
+
|
|
31
|
+
model: ModelCompactResp
|
|
32
|
+
project: ProjectCompactResp
|
|
33
|
+
organization: OrganizationCompactResp
|
|
34
|
+
dataset: DatasetCompactResp = Field(alias='environment')
|
|
35
|
+
|
|
36
|
+
created_at: datetime
|
|
37
|
+
updated_at: datetime
|
|
@@ -13,6 +13,7 @@ class CustomFeatureType(str, Enum):
|
|
|
13
13
|
FROM_VECTOR = 'FROM_VECTOR'
|
|
14
14
|
FROM_TEXT_EMBEDDING = 'FROM_TEXT_EMBEDDING'
|
|
15
15
|
FROM_IMAGE_EMBEDDING = 'FROM_IMAGE_EMBEDDING'
|
|
16
|
+
ENRICHMENT = 'ENRICHMENT'
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class CustomFeature(BaseModel):
|
|
@@ -20,10 +21,6 @@ class CustomFeature(BaseModel):
|
|
|
20
21
|
type: CustomFeatureType
|
|
21
22
|
n_clusters: Optional[int] = DEFAULT_NUM_CLUSTERS
|
|
22
23
|
centroids: Optional[List] = None
|
|
23
|
-
columns: Optional[List[str]] = None
|
|
24
|
-
column: Optional[str] = None
|
|
25
|
-
source_column: Optional[str] = None
|
|
26
|
-
n_tags: Optional[int] = DEFAULT_NUM_TAGS
|
|
27
24
|
|
|
28
25
|
class Config:
|
|
29
26
|
allow_mutation = False
|
|
@@ -49,30 +46,31 @@ class CustomFeature(BaseModel):
|
|
|
49
46
|
return TextEmbedding.parse_obj(deserialized_json)
|
|
50
47
|
elif feature_type == CustomFeatureType.FROM_IMAGE_EMBEDDING:
|
|
51
48
|
return ImageEmbedding.parse_obj(deserialized_json)
|
|
49
|
+
elif feature_type == CustomFeatureType.ENRICHMENT:
|
|
50
|
+
return Enrichment.parse_obj(deserialized_json)
|
|
52
51
|
else:
|
|
53
52
|
raise ValueError(f'Unsupported feature type: {feature_type}')
|
|
54
53
|
|
|
55
54
|
def to_dict(self) -> Dict[str, Any]:
|
|
56
|
-
return_dict = {
|
|
55
|
+
return_dict: Dict[str, Any] = {
|
|
57
56
|
'name': self.name,
|
|
58
57
|
'type': self.type.value,
|
|
59
58
|
'n_clusters': self.n_clusters,
|
|
60
59
|
}
|
|
61
|
-
if self
|
|
62
|
-
return_dict['columns'] = self.columns
|
|
63
|
-
elif self
|
|
60
|
+
if isinstance(self, Multivariate):
|
|
61
|
+
return_dict['columns'] = self.columns
|
|
62
|
+
elif isinstance(self, VectorFeature):
|
|
64
63
|
return_dict['column'] = self.column
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return_dict['
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return_dict['n_tags'] = self.n_tags
|
|
64
|
+
if isinstance(self, (ImageEmbedding, TextEmbedding)):
|
|
65
|
+
return_dict['source_column'] = self.source_column
|
|
66
|
+
if isinstance(self, TextEmbedding):
|
|
67
|
+
return_dict['n_tags'] = self.n_tags
|
|
68
|
+
elif isinstance(self, Enrichment):
|
|
69
|
+
return_dict['columns'] = self.columns
|
|
70
|
+
return_dict['enrichment'] = self.enrichment
|
|
71
|
+
return_dict['config'] = self.config
|
|
74
72
|
else:
|
|
75
|
-
raise ValueError(f'Unsupported feature type: {self.type}')
|
|
73
|
+
raise ValueError(f'Unsupported feature type: {self.type} {type(self)}')
|
|
76
74
|
|
|
77
75
|
return return_dict
|
|
78
76
|
|
|
@@ -96,3 +94,10 @@ class TextEmbedding(VectorFeature):
|
|
|
96
94
|
|
|
97
95
|
class ImageEmbedding(VectorFeature):
|
|
98
96
|
type: CustomFeatureType = CustomFeatureType.FROM_IMAGE_EMBEDDING
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Enrichment(CustomFeature):
|
|
100
|
+
type: CustomFeatureType = CustomFeatureType.ENRICHMENT
|
|
101
|
+
columns: List[str]
|
|
102
|
+
enrichment: str
|
|
103
|
+
config: Dict[str, Any] = {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from fiddler3.constants.dataset import EnvType
|
|
5
|
+
from fiddler3.schemas.base import BaseModel
|
|
6
|
+
from fiddler3.schemas.model import ModelCompactResp
|
|
7
|
+
from fiddler3.schemas.organization import OrganizationCompactResp
|
|
8
|
+
from fiddler3.schemas.project import ProjectCompactResp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DatasetCompactResp(BaseModel):
|
|
12
|
+
"""Dataset Compact."""
|
|
13
|
+
|
|
14
|
+
id: UUID
|
|
15
|
+
name: str
|
|
16
|
+
type: EnvType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatasetResp(BaseModel):
|
|
20
|
+
"""Dataset response pydantic model."""
|
|
21
|
+
|
|
22
|
+
id: UUID
|
|
23
|
+
name: str
|
|
24
|
+
row_count: Optional[int]
|
|
25
|
+
model: ModelCompactResp
|
|
26
|
+
project: ProjectCompactResp
|
|
27
|
+
organization: OrganizationCompactResp
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from fiddler3.schemas.base import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@enum.unique
|
|
8
|
+
class ArtifactType(str, enum.Enum):
|
|
9
|
+
SURROGATE = 'SURROGATE'
|
|
10
|
+
PYTHON_PACKAGE = 'PYTHON_PACKAGE'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@enum.unique
|
|
14
|
+
class DeploymentType(str, enum.Enum):
|
|
15
|
+
BASE_CONTAINER = 'BASE_CONTAINER'
|
|
16
|
+
MANUAL = 'MANUAL'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DeploymentParams(BaseModel):
|
|
20
|
+
artifact_type: str = ArtifactType.PYTHON_PACKAGE
|
|
21
|
+
deployment_type: DeploymentType
|
|
22
|
+
image_uri: Optional[str]
|
|
23
|
+
replicas: Optional[int]
|
|
24
|
+
cpu: Optional[int]
|
|
25
|
+
memory: Optional[int]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Any, List, Union
|
|
3
|
+
|
|
4
|
+
from fiddler3.schemas.base import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class QueryConditionType(str, enum.Enum):
|
|
8
|
+
AND = 'AND'
|
|
9
|
+
OR = 'OR'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QueryRule(BaseModel):
|
|
13
|
+
field: str
|
|
14
|
+
operator: str
|
|
15
|
+
value: Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QueryCondition(BaseModel):
|
|
19
|
+
condition: QueryConditionType = QueryConditionType.AND
|
|
20
|
+
|
|
21
|
+
rules: List[Union[QueryRule, 'QueryCondition']]
|
|
22
|
+
|
|
23
|
+
def add_rule(self, rule: Union[QueryRule, 'QueryCondition']) -> None:
|
|
24
|
+
"""Add a filter rule"""
|
|
25
|
+
self.rules.append(rule)
|
|
26
|
+
|
|
27
|
+
def add_rules(self, rules: List[Union[QueryRule, 'QueryCondition']]) -> None:
|
|
28
|
+
"""Add multiple filter rules"""
|
|
29
|
+
self.rules.extend(rules)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@enum.unique
|
|
33
|
+
class OperatorType(str, enum.Enum):
|
|
34
|
+
ANY = 'any'
|
|
35
|
+
EQUAL = 'equal'
|
|
36
|
+
NOT_EQUAL = 'not_equal'
|
|
37
|
+
IN = 'in'
|
|
38
|
+
NOT_IN = 'not_in'
|
|
39
|
+
LESS = 'less'
|
|
40
|
+
LESS_OR_EQUAL = 'less_or_equal'
|
|
41
|
+
GREATER = 'greater'
|
|
42
|
+
GREATER_OR_EQUAL = 'greater_or_equal'
|
|
43
|
+
BETWEEN = 'between'
|
|
44
|
+
NOT_BETWEEN = 'not_between'
|
|
45
|
+
BEGINS_WITH = 'begins_with'
|
|
46
|
+
NOT_BEGINS_WITH = 'not_begins_with'
|
|
47
|
+
CONTAINS = 'contains'
|
|
48
|
+
NOT_CONTAINS = 'not_contains'
|
|
49
|
+
ENDS_WITH = 'ends_with'
|
|
50
|
+
NOT_ENDS_WITH = 'not_ends_with'
|
|
51
|
+
IS_EMPTY = 'is_empty'
|
|
52
|
+
IS_NOT_EMPTY = 'is_not_empty'
|
|
53
|
+
IS_NULL = 'is_null'
|
|
54
|
+
IS_NOT_NULL = 'is_not_null'
|
fiddler3/schemas/job.py
CHANGED
|
@@ -4,12 +4,12 @@ from uuid import UUID
|
|
|
4
4
|
from fiddler3.schemas.base import BaseModel
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
class
|
|
7
|
+
class JobCompactResp(BaseModel):
|
|
8
8
|
id: UUID
|
|
9
9
|
name: str
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class
|
|
12
|
+
class JobResp(BaseModel):
|
|
13
13
|
id: UUID
|
|
14
14
|
name: str
|
|
15
15
|
status: str
|