fiddler-client 2.0.0.dev9__py3-none-any.whl → 2.0.0.dev10__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 CHANGED
@@ -23,7 +23,6 @@ from fiddler.core_objects import (
23
23
  Column,
24
24
  DatasetInfo,
25
25
  DataType,
26
- DeploymentType,
27
26
  ExplanationMethod,
28
27
  FiddlerPublishSchema,
29
28
  FiddlerTimestamp,
@@ -43,9 +42,9 @@ from fiddler.schema.alert import (
43
42
  Metric,
44
43
  Priority,
45
44
  )
46
- from fiddler.schema.model_deployment import DeploymentParams
47
- from fiddler.schemas.custom_features import Multivariate, TextEmbedding, ImageEmbedding
48
- from fiddler.utils import logger, ColorLogger
45
+ from fiddler.schema.model_deployment import DeploymentParams, DeploymentType
46
+ from fiddler.schemas.custom_features import CustomFeature, ImageEmbedding, Multivariate, TextEmbedding, VectorFeature, CustomFeatureType
47
+ from fiddler.utils import ColorLogger, logger
49
48
  from fiddler.utils.validator import (
50
49
  PackageValidator,
51
50
  ValidationChainSettings,
@@ -61,13 +60,17 @@ __all__ = [
61
60
  '__version__',
62
61
  'BatchPublishType',
63
62
  'Column',
63
+ 'CustomFeature',
64
+ 'CustomFeatureType',
64
65
  'Multivariate',
66
+ 'VectorFeature',
65
67
  'TextEmbedding',
66
68
  'ImageEmbedding',
67
69
  'ColorLogger',
68
70
  'DatasetInfo',
69
71
  'DataType',
70
72
  'DeploymentParams',
73
+ 'DeploymentType',
71
74
  'FiddlerClient',
72
75
  'FiddlerApi',
73
76
  'FiddlerTimestamp',
fiddler/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '2.0.0.dev9'
1
+ __version__ = '2.0.0.dev10'
@@ -35,7 +35,7 @@ class AlertMixin:
35
35
  FILTER_ALERT_RULES_API_VERSION = '>=23.1.0'
36
36
  GROUP_OF_COLUMNS_API_VERSION = '>=23.3.0'
37
37
  RULE_V3_API_VERSION = '>=23.4.0'
38
-
38
+
39
39
  client: RequestClient
40
40
  organization_name: str
41
41
 
@@ -54,6 +54,7 @@ class AlertMixin:
54
54
  alert_type: Optional[AlertType] = None,
55
55
  baseline_name: Optional[str] = None,
56
56
  compare_period: Optional[ComparePeriod] = None,
57
+ column: Optional[str] = None,
57
58
  columns: Optional[List[str]] = None,
58
59
  warning_threshold: Optional[float] = None,
59
60
  project_name: str = None,
@@ -120,19 +121,13 @@ class AlertMixin:
120
121
  :param condition: Select from:
121
122
  1) AlertCondition.LESSER
122
123
  2) AlertCondition.GREATER
124
+ :param column: column name on which alert rule is to be created. Supported by server version 23.3.0 or lower.
123
125
  :param columns: List of column names on which alert rule is to be created. It can take ['__ANY__'] to check for all columns
124
126
  :param notifications_config: notifications config object created using helper method build_notifications_config()
125
127
  :param baseline_name: Name of the baselne, whose histogram is compared against the same derived from current data.
126
128
  Used only when alert type is AlertType.DATA_DRIFT.
127
129
  :return: created alert rule object
128
- """
129
- if columns and not match_semver(
130
- self.server_info.server_version, self.GROUP_OF_COLUMNS_API_VERSION
131
- ):
132
- raise ValueError(
133
- f'columns parameter works with server version {self.GROUP_OF_COLUMNS_API_VERSION}'
134
- )
135
-
130
+ """
136
131
  project_name = project_id_compat(
137
132
  project_id=project_id, project_name=project_name
138
133
  )
@@ -154,7 +149,7 @@ class AlertMixin:
154
149
  if compare_period and compare_period not in ComparePeriod.keys():
155
150
  raise ValueError(f'compare_period should be one of{ComparePeriod.keys()}')
156
151
 
157
- request_body = AlertRulePayload(
152
+ alert_rule_payload = AlertRulePayload(
158
153
  organization_name=self.organization_name,
159
154
  project_name=project_name,
160
155
  model_name=model_name,
@@ -166,14 +161,41 @@ class AlertMixin:
166
161
  compare_period=compare_period,
167
162
  priority=priority,
168
163
  baseline_name=baseline_name,
169
- feature_names=columns,
170
164
  warning_threshold=warning_threshold,
171
165
  critical_threshold=critical_threshold,
172
166
  condition=condition,
173
167
  time_bucket=bin_size.value[0],
174
168
  bin_size=bin_size.value[1],
175
169
  notifications=notifications_config,
176
- ).dict()
170
+ )
171
+
172
+ if columns and not match_semver(
173
+ self.server_info.server_version, self.GROUP_OF_COLUMNS_API_VERSION
174
+ ):
175
+ raise ValueError(
176
+ f'columns parameter works with server version {self.GROUP_OF_COLUMNS_API_VERSION}'
177
+ )
178
+
179
+ if match_semver(self.server_info.server_version, self.GROUP_OF_COLUMNS_API_VERSION):
180
+ if columns:
181
+ if match_semver(self.server_info.server_version, self.RULE_V3_API_VERSION):
182
+ alert_rule_payload.feature_names=columns
183
+ else:
184
+ # version 23.3 support columns is comma separated string
185
+ alert_rule_payload.feature_names=",".join(columns)
186
+ elif column:
187
+ if match_semver(self.server_info.server_version, self.RULE_V3_API_VERSION):
188
+ alert_rule_payload.feature_names=[column]
189
+ else:
190
+ alert_rule_payload.feature_names=column
191
+ else:
192
+ # we do not support columns in server version < 23.3
193
+ if columns:
194
+ raise ValueError(f'columns is not supported in the server version {self.server_info.server_version}. Please use column instead.')
195
+ if column:
196
+ alert_rule_payload.feature_name=column
197
+
198
+ request_body = alert_rule_payload.dict()
177
199
  response = self.client.post(
178
200
  url='alert-configs',
179
201
  data=request_body,
fiddler/api/api.py CHANGED
@@ -15,6 +15,7 @@ from fiddler.api.job_mixin import JobMixin
15
15
  from fiddler.api.model_deployment_mixin import ModelDeploymentMixin
16
16
  from fiddler.api.model_mixin import ModelMixin
17
17
  from fiddler.api.project_mixin import ProjectMixin
18
+ from fiddler.api.webhooks_mixin import WebhookMixin
18
19
  from fiddler.constants import (
19
20
  CURRENT_API_VERSION,
20
21
  FIDDLER_CLIENT_VERSION_HEADER,
@@ -41,6 +42,7 @@ class FiddlerClient(
41
42
  ExplainabilityMixin,
42
43
  ModelDeploymentMixin,
43
44
  GenerateSchemaMixin,
45
+ WebhookMixin
44
46
  ):
45
47
  def __init__(
46
48
  self,
@@ -14,11 +14,12 @@ from fiddler.exceptions import HttpException
14
14
  from fiddler.libs.http_client import RequestClient
15
15
  from fiddler.schema.events import EventIngest, EventsIngest
16
16
  from fiddler.utils.compatibility_helpers import (
17
- project_id_compat,
18
17
  model_id_compat,
18
+ project_id_compat,
19
19
  update_event_compat,
20
20
  )
21
21
  from fiddler.utils.decorators import handle_api_error_response
22
+ from fiddler.utils.helpers import match_semver
22
23
  from fiddler.utils.logger import get_logger
23
24
  from fiddler.utils.response_handler import APIResponseHandler
24
25
 
@@ -235,7 +236,7 @@ class EventsMixin:
235
236
  timestamp_field=timestamp_field,
236
237
  timestamp_format=timestamp_format,
237
238
  group_by=group_by,
238
- file_type=FileType.CSV,
239
+ file_type=file_type,
239
240
  wait=wait,
240
241
  )
241
242
  else:
@@ -368,14 +369,23 @@ class EventsMixin:
368
369
  'The batch provided is empty. Please retry with at least one row of data.'
369
370
  )
370
371
 
371
- file_type = FileType.CSV
372
- with tempfile.NamedTemporaryFile(suffix=file_type) as temp:
373
- events_df.to_csv(temp, index=False)
374
- events_path = Path(temp.name)
372
+ if self.server_info and match_semver(
373
+ self.server_info.server_version, '>=23.4.0'
374
+ ):
375
+ file_type = FileType.PARQUET
376
+ else:
377
+ file_type = FileType.CSV
378
+
379
+ with tempfile.NamedTemporaryFile(suffix=file_type) as temp_file:
380
+ if file_type == FileType.PARQUET:
381
+ events_df.to_parquet(temp_file.name, index=False)
382
+ else:
383
+ events_df.to_csv(temp_file.name, index=False)
384
+
375
385
  return self.publish_events_batch(
376
386
  project_name=project_name,
377
387
  model_name=model_name,
378
- events_path=events_path,
388
+ events_path=Path(temp_file.name),
379
389
  batch_id=batch_id,
380
390
  id_field=id_field,
381
391
  is_update=is_update,
@@ -383,6 +393,7 @@ class EventsMixin:
383
393
  timestamp_format=timestamp_format,
384
394
  group_by=group_by,
385
395
  wait=wait,
396
+ file_type=file_type,
386
397
  )
387
398
 
388
399
  @handle_api_error_response
@@ -0,0 +1,121 @@
1
+ from http import HTTPStatus
2
+ from typing import List, Optional
3
+
4
+ from pydantic import parse_obj_as
5
+
6
+ from fiddler.libs.http_client import RequestClient
7
+ from fiddler.schema.webhook import Webhook
8
+ from fiddler.utils.decorators import handle_api_error_response
9
+ from fiddler.utils.logger import get_logger
10
+ from fiddler.utils.response_handler import (
11
+ APIResponseHandler,
12
+ PaginatedResponseHandler,
13
+ )
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class WebhookMixin:
19
+ client: RequestClient
20
+ organization_name: str
21
+
22
+ @handle_api_error_response
23
+ def add_webhook(self, name:str, url:str, provider:str) -> Webhook:
24
+ """
25
+ Add a new `Webhook`.
26
+
27
+ :params name: name of webhook
28
+ :params url: webhook url
29
+ :params provider: Either 'SLACK' or 'OTHER'
30
+
31
+ :returns: Created `Webhook` object.
32
+ """
33
+ request_body = {
34
+ 'name': name,
35
+ 'url': url,
36
+ 'provider': provider,
37
+ }
38
+ response = self.client.post(
39
+ url='webhooks',
40
+ params={'organization_name': self.organization_name},
41
+ data=request_body,
42
+ )
43
+ if response.status_code == HTTPStatus.OK:
44
+ logger.info(f'Webhook created successfully.')
45
+ else:
46
+ logger.info('Webhook creation unsuccessful')
47
+ return Webhook.deserialize(APIResponseHandler(response))
48
+
49
+ @handle_api_error_response
50
+ def delete_webhook(self, uuid: str = None) -> None:
51
+ """
52
+ Delete a webhook
53
+
54
+ :params uuid: uuid of the Webhook to delete
55
+ :returns: None
56
+ """
57
+ response = self.client.delete(
58
+ url=f'webhooks/{uuid}'
59
+ )
60
+ if response.status_code == HTTPStatus.OK:
61
+ logger.info(f'Webhook {uuid} deleted successfully.')
62
+ else:
63
+ logger.info('Webhook deletion unsuccessful')
64
+
65
+ @handle_api_error_response
66
+ def get_webhook(self, uuid:str) -> List[Webhook]:
67
+ """
68
+ Get a webhook
69
+
70
+ :params uuid: UUID belongs to the Webhook
71
+ :returns: `Webhook` object
72
+ """
73
+ response = self.client.get(
74
+ url=f'webhooks/{uuid}',
75
+ )
76
+ return Webhook.deserialize(APIResponseHandler(response))
77
+
78
+ @handle_api_error_response
79
+ def update_webhook(self, uuid: str, name:str, url:str, provider:str) -> Webhook:
80
+ """
81
+ Update a webhook, by changing one or more of name, url or provider.
82
+ :params name: name of webhook
83
+ :params url: webhook url
84
+ :params provider: Either 'SLACK' or 'OTHER'
85
+
86
+ :returns: Updated `Webhook` object.
87
+ """
88
+ request_body = Webhook(
89
+ name=name, url=url, provider=provider
90
+ ).dict()
91
+ response = self.client.patch(
92
+ url=f'webhooks/{uuid}',
93
+ data=request_body,
94
+ )
95
+ if response.status_code == HTTPStatus.OK:
96
+ logger.info(f'Webhook {uuid} updated successfully.')
97
+ else:
98
+ logger.info('Webhook updation unsuccessful')
99
+ return Webhook.deserialize(APIResponseHandler(response))
100
+
101
+ @handle_api_error_response
102
+ def get_webhooks(self, limit: int = 300, offset: int = 0) -> List[Webhook]:
103
+ """
104
+ Get a list of all webhooks in the organization
105
+
106
+ :params limit: Number of webhooks to fetch in a call
107
+ :params offset: Number of rows to skip before any rows are retrived
108
+ :returns: List of `Webhook` object
109
+ """
110
+ response = self.client.get(
111
+ url='webhooks',
112
+ params={
113
+ 'organization_name': self.organization_name,
114
+ 'limit': limit,
115
+ 'offset': offset,
116
+ },
117
+ )
118
+ items = PaginatedResponseHandler(response).get_pagination_items()
119
+ return parse_obj_as(List[Webhook], items)
120
+
121
+
fiddler/core_objects.py CHANGED
@@ -24,7 +24,12 @@ import numpy as np
24
24
  import pandas as pd
25
25
  import pandas.api.types
26
26
 
27
- from fiddler.schemas.custom_features import CustomFeature
27
+ from fiddler.schemas.custom_features import (
28
+ CustomFeature,
29
+ ImageEmbedding,
30
+ TextEmbedding,
31
+ VectorFeature,
32
+ )
28
33
  from fiddler.utils.formatting import prettyprint_number, validate_sanitized_names
29
34
  from fiddler.utils.logger import get_logger
30
35
  from fiddler.utils.pandas_helper import is_datetime, is_list, is_vector_value
@@ -199,6 +204,7 @@ class ModelTask(enum.Enum):
199
204
  MULTICLASS_CLASSIFICATION = 'multiclass_classification'
200
205
  REGRESSION = 'regression'
201
206
  RANKING = 'ranking'
207
+ NOT_SET = 'not_set'
202
208
 
203
209
  def is_classification(self):
204
210
  return self.value in (
@@ -1235,6 +1241,9 @@ class ModelInfo:
1235
1241
  for k, v in numeric_cols.items()
1236
1242
  if v in (DataType.INTEGER.value, DataType.FLOAT.value)
1237
1243
  ]
1244
+ valid_vector_cols = [
1245
+ input.name for input in self.inputs if input.data_type.is_vector()
1246
+ ]
1238
1247
 
1239
1248
  unique_cf_names = []
1240
1249
  for feature in self.custom_features:
@@ -1256,17 +1265,17 @@ class ModelInfo:
1256
1265
  else:
1257
1266
  unique_cf_names.append(feature.name)
1258
1267
 
1259
- if isinstance(feature.columns, list):
1268
+ if isinstance(feature, (TextEmbedding, ImageEmbedding, VectorFeature)):
1269
+ if not feature.column in valid_vector_cols:
1270
+ raise ValueError(
1271
+ f"Custom features '{feature.name}' defined on column '{feature.column}' is not found"
1272
+ )
1273
+ else:
1260
1274
  for col in feature.columns:
1261
1275
  if col not in valid_col_names:
1262
1276
  raise ValueError(
1263
1277
  f"Custom features '{feature.name}' defined on column '{col}' is not found or not of type int/float."
1264
1278
  )
1265
- else:
1266
- if feature.columns not in valid_col_names:
1267
- raise ValueError(
1268
- f"Custom features '{feature.name}' defined on column '{feature.columns}' is not found or not of type int/float."
1269
- )
1270
1279
 
1271
1280
  def warn_deprecated_parameter(self, param_name: str, from_version: str) -> None:
1272
1281
  warnings.warn(
@@ -1814,14 +1823,9 @@ class ModelInfo:
1814
1823
  # todo (pradhuman): replace categorical_target_class_details with target_class_order
1815
1824
 
1816
1825
  if not model_task:
1817
- model_task = cls.infer_model_task(
1818
- dataset_info=dataset_info,
1819
- target=target,
1820
- outputs=outputs,
1821
- )
1822
- LOG.warning(
1823
- f'Fiddler inferred your model as a {model_task.value} model. '
1824
- f'If this is wrong, please specify your model task.'
1826
+ raise ValueError(
1827
+ 'model_task is required to create a model info. Please specify a value '
1828
+ 'using the ModelTask enum.'
1825
1829
  )
1826
1830
 
1827
1831
  (
@@ -2290,42 +2294,6 @@ class ModelInfo:
2290
2294
  )
2291
2295
  return is_binary_ranking_model, top_k, bin_class_threshold
2292
2296
 
2293
- @classmethod
2294
- def infer_model_task(
2295
- cls,
2296
- dataset_info: DatasetInfo,
2297
- target: Optional[str],
2298
- outputs: Optional[List[str]],
2299
- ) -> ModelTask:
2300
- """
2301
- Inference assuming user gave all right parameters.
2302
- After, we run ModelInfoValidator to check all parameters.
2303
- :param dataset_info: dataset info object
2304
- :param target: target column name
2305
- :param outputs: list of output column name(s)
2306
- :return: Inferred Model task when possible
2307
- """
2308
- if not outputs or not target:
2309
- raise ValueError(
2310
- f'Fiddler cannot infer your model task because target and/or outputs '
2311
- f'are not specified. Please specify your model task.'
2312
- )
2313
- if len(outputs) > 1:
2314
- return ModelTask.MULTICLASS_CLASSIFICATION
2315
-
2316
- target_col = dataset_info[target]
2317
- output_col = dataset_info[outputs[0]]
2318
-
2319
- if ModelInfo.check_binary_target(target_column=target_col):
2320
- if output_col.value_range_min >= 0.0 and output_col.value_range_max <= 1.0:
2321
- return ModelTask.BINARY_CLASSIFICATION
2322
- return ModelTask.RANKING
2323
-
2324
- if not target_col.data_type.is_numeric():
2325
- return ModelTask.RANKING
2326
-
2327
- return ModelTask.REGRESSION
2328
-
2329
2297
  @classmethod
2330
2298
  def validate_model_task(
2331
2299
  cls,
@@ -2402,7 +2370,7 @@ class ModelInfo:
2402
2370
  target_column: Column,
2403
2371
  ) -> Optional[List[Union[str, bool, float, int]]]:
2404
2372
 
2405
- if model_task.value == ModelTask.REGRESSION.value:
2373
+ if model_task.value in [ModelTask.REGRESSION.value, ModelTask.NOT_SET.value]:
2406
2374
  return None
2407
2375
 
2408
2376
  if isinstance(target_class_order, (str, int, float, bool)):
fiddler/schema/alert.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import enum
2
2
  from typing import Any, Dict, List, Optional, Union
3
3
 
4
+ from ast import literal_eval
4
5
  from pydantic import Field, root_validator
5
6
 
6
7
  from fiddler.schema.base import BaseDataSchema
@@ -140,6 +141,7 @@ class AlertRulePayload(BaseDataSchema):
140
141
  alert_type: Optional[AlertType]
141
142
  metric: Metric
142
143
  metric_id: Union[Metric, str]
144
+ feature_name: Optional[str]
143
145
  feature_names: Optional[List[str]]
144
146
  priority: Priority
145
147
  compare_to: CompareTo
@@ -161,7 +163,8 @@ class AlertRule(BaseDataSchema):
161
163
  name: Optional[str]
162
164
  alert_type: Optional[AlertType]
163
165
  metric: str
164
- columns: Optional[List[str]] = Field(alias='feature_names')
166
+ column: Optional[str] = Field(alias='feature_name', default=None)
167
+ columns: Optional[List[str]] = Field(alias='feature_names', default=None)
165
168
  baseline_name: Optional[str]
166
169
  priority: Priority
167
170
  compare_to: CompareTo
@@ -172,7 +175,15 @@ class AlertRule(BaseDataSchema):
172
175
  bin_size: Optional[str]
173
176
  time_bucket: Optional[int]
174
177
 
175
-
178
+ @root_validator(pre=True)
179
+ def set_feature_names(cls, values) -> dict:
180
+ if 'feature_names' in values and type(values['feature_names']) == str:
181
+ # in release 23.3 feature_names is stringified list. 23.4 onwards its List[str]
182
+ try:
183
+ values['feature_names'] = literal_eval(values['feature_names'])
184
+ except SyntaxError:
185
+ values['feature_names'] = None
186
+ return values
176
187
 
177
188
  class TriggeredAlerts(BaseDataSchema):
178
189
  id: int
@@ -180,10 +191,12 @@ class TriggeredAlerts(BaseDataSchema):
180
191
  alert_rule_uuid: str = Field(alias='alert_config_uuid')
181
192
  alert_run_start_time: int
182
193
  alert_time_bucket: int
183
- alert_value: Dict[str, float]
194
+ alert_value: Union[float, Dict[str, float]]
184
195
  baseline_time_bucket: Optional[int]
185
196
  baseline_value: Optional[float]
186
197
  is_alert: bool
187
198
  severity: Optional[str]
188
199
  failure_reason: str
189
200
  message: str
201
+ multi_col_values: Optional[Dict[str, float]]
202
+ feature_name: Optional[str]
@@ -0,0 +1,10 @@
1
+ from fiddler.schema.base import BaseDataSchema
2
+
3
+
4
+ class Webhook(BaseDataSchema):
5
+ uuid: str
6
+ name: str
7
+ organization_name: str
8
+ url: str
9
+ # provider is 'SLACK' or 'OTHER' as of Aug 2023.
10
+ provider: str
@@ -1,9 +1,11 @@
1
1
  from enum import Enum
2
- from typing import List, Optional, Type, TypeVar
2
+ from typing import Any, Dict, List, Optional, Type, TypeVar
3
3
 
4
4
  from pydantic import BaseModel
5
5
 
6
- T = TypeVar('T', bound='CustomFeature')
6
+ CustomFeatureTypeVar = TypeVar('CustomFeatureTypeVar', bound='CustomFeature')
7
+ DEFAULT_NUM_CLUSTERS = 5
8
+ DEFAULT_NUM_TAGS = 5
7
9
 
8
10
 
9
11
  class CustomFeatureType(str, Enum):
@@ -16,14 +18,26 @@ class CustomFeatureType(str, Enum):
16
18
  class CustomFeature(BaseModel):
17
19
  name: str
18
20
  type: CustomFeatureType
19
- n_clusters: Optional[int] = 5
21
+ n_clusters: Optional[int] = DEFAULT_NUM_CLUSTERS
20
22
  centroids: Optional[List] = None
21
23
 
22
24
  class Config:
23
25
  allow_mutation = False
24
26
 
25
27
  @classmethod
26
- def from_dict(cls: Type[T], deserialized_json: dict) -> T:
28
+ def from_columns(
29
+ cls, custom_name: str, cols: List[str], n_clusters: int = DEFAULT_NUM_CLUSTERS
30
+ ) -> 'Multivariate':
31
+ return Multivariate(
32
+ name=custom_name,
33
+ columns=cols,
34
+ n_clusters=n_clusters,
35
+ )
36
+
37
+ @classmethod
38
+ def from_dict(
39
+ cls: Type[CustomFeatureTypeVar], deserialized_json: dict
40
+ ) -> CustomFeatureTypeVar:
27
41
  feature_type = CustomFeatureType(deserialized_json['type'])
28
42
  if feature_type == CustomFeatureType.FROM_COLUMNS:
29
43
  return Multivariate.parse_obj(deserialized_json)
@@ -36,20 +50,46 @@ class CustomFeature(BaseModel):
36
50
  else:
37
51
  raise ValueError(f'Unsupported feature type: {feature_type}')
38
52
 
53
+ def to_dict(self) -> Dict[str, Any]:
54
+ return_dict = {
55
+ 'name': self.name,
56
+ 'type': self.type,
57
+ 'n_clusters': self.n_clusters,
58
+ }
59
+ if self.type == CustomFeatureType.FROM_COLUMNS:
60
+ return_dict['columns'] = self.columns
61
+ elif self.type == CustomFeatureType.FROM_VECTOR:
62
+ return_dict['column'] = self.column
63
+ elif self.type in (
64
+ CustomFeatureType.FROM_TEXT_EMBEDDING,
65
+ CustomFeatureType.FROM_IMAGE_EMBEDDING,
66
+ ):
67
+ return_dict['source_column'] = self.source_column
68
+ return_dict['column'] = self.column
69
+
70
+ if self.type == CustomFeatureType.FROM_TEXT_EMBEDDING:
71
+ return_dict['n_tags'] = self.n_tags
72
+ else:
73
+ raise ValueError(f'Unsupported feature type: {self.type}')
74
+
75
+ return return_dict
76
+
39
77
 
40
78
  class Multivariate(CustomFeature):
79
+ type: CustomFeatureType = CustomFeatureType.FROM_COLUMNS
41
80
  columns: List[str]
42
81
  monitor_components: bool = False
43
82
 
44
83
 
45
84
  class VectorFeature(CustomFeature):
46
- type: CustomFeatureType = CustomFeatureType.FROM_VECTOR
85
+ type: CustomFeatureType = CustomFeatureType.FROM_VECTOR
47
86
  source_column: Optional[str] = None
48
87
  column: str
49
88
 
50
89
 
51
90
  class TextEmbedding(VectorFeature):
52
91
  type: CustomFeatureType = CustomFeatureType.FROM_TEXT_EMBEDDING
92
+ n_tags: Optional[int] = DEFAULT_NUM_TAGS
53
93
 
54
94
 
55
95
  class ImageEmbedding(VectorFeature):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fiddler-client
3
- Version: 2.0.0.dev9
3
+ Version: 2.0.0.dev10
4
4
  Summary: Python client for Fiddler Service
5
5
  Home-page: https://fiddler.ai
6
6
  Author: Fiddler Labs
@@ -63,6 +63,15 @@ Version History
63
63
  - Add `monitor_components` as an attribute of `CustomFeature`. Default to `False`
64
64
  - Remove `column` as a parameter in `add_alert_rule` and `get_alert_rules` functions
65
65
 
66
+ ### 1.8.4
67
+ - #### **New Features**
68
+ - New DeploymentType enum for MANUAL deployment
69
+
70
+ ### 1.8.3
71
+ - ### **Modifications**
72
+ - New `columns` parameter in add_alert_rule and get_alert_rules to support multiple columns to be used for server version >= 23.3.0
73
+ - get_triggered_alerts supports `alert_value` as a float as well as a dict
74
+
66
75
  ### 1.8.2
67
76
  - #### **Modifications**
68
77
  - Fixed a bug where `min` and `max` for columns of type `float` in `dataset_info` are cast into `int` after uploaded
@@ -1,15 +1,15 @@
1
- fiddler/__init__.py,sha256=YS0i3OUuVHSgcD7VsdUyWQnGLZxY0JmNHS68ntd2RCc,1826
2
- fiddler/_version.py,sha256=GHa-JrO7-N3Lc0pyyTII9KZ4HKdxBS1XBpBkSAGlTgU,27
1
+ fiddler/__init__.py,sha256=Vv9ypQhEkjFdnhiIuv79m8ZtBVlsegjt2anDH6DVHds,1960
2
+ fiddler/_version.py,sha256=Vkmm08Vum_RV2_Nb1HuOhJ09Soq68UZ8mIOy9lPfsvY,28
3
3
  fiddler/constants.py,sha256=sNPexKXMCVC9iQNIArgIQBITGhgHuBUiNklEXTD_eFI,1822
4
- fiddler/core_objects.py,sha256=LdFJ5XqAGP7oT6yE3GhlnRMVbDXEGZSeFJJjOvHMuDw,112950
4
+ fiddler/core_objects.py,sha256=DZnJRwgfeL7lSVco_veU-Depwe_PJ411uFLvQm8ZHkY,111717
5
5
  fiddler/exceptions.py,sha256=tis5S9jdGsVmXkFjulLNEHeiFQ8lFjJYlYr68H83euU,2490
6
6
  fiddler/api/__init__.py,sha256=9PidyuY4iRgOeEPFyBYo0jcSbTf7KeE3jsDXwFP4ZHs,109
7
- fiddler/api/alert_mixin.py,sha256=GVdlNmuUrDjdfDYyXogCdkzMWYaXLAt9CxR3MOW1wpI,16916
8
- fiddler/api/api.py,sha256=00FZYGOa0TSAn_mLQp_oqdc1LLoPTTMJrHFKCUERr5U,4187
7
+ fiddler/api/alert_mixin.py,sha256=Qgp58GPFY52Dq0JzTmXJ3hjZFn4yieQADWrZKqENrn8,18143
8
+ fiddler/api/api.py,sha256=nNOOJcqUPzYpY1jYL9brzZI5_OxDJYq48K3rQAZJ-1M,4256
9
9
  fiddler/api/baseline_mixin.py,sha256=D-IvvUDHN-J4y_g_irp5_Y5g53cTL3AEYuu121YnHGo,9813
10
10
  fiddler/api/compatibility_mixin.py,sha256=_1vaScKiJeKbPmFjT30IbhCbUoDDQoxHDwQ6o0qSrm0,1313
11
11
  fiddler/api/dataset_mixin.py,sha256=5LuaPU1PbTS3srq3vSUUK-e2-Fc56M7ClNl9oRVX8e0,16963
12
- fiddler/api/events_mixin.py,sha256=EdSAJNRIRXr1nYrbOVJ-mMIrUI7KAww-Muqq7P0piVo,22076
12
+ fiddler/api/events_mixin.py,sha256=zw_wIjrQVIg9LqaAzz8Ye0u4m26LtYMp-dau2v9UVDk,22447
13
13
  fiddler/api/explainability_mixin.py,sha256=WsyDEtGAWfQYN9HdS8b8uUan9vjKWvUSB43F7KkjiCg,15332
14
14
  fiddler/api/generate_schema_mixin.py,sha256=QWFnMRi0k5mf0wPpCxAkrffbQz5JNU2OgERy5KgVP5w,9792
15
15
  fiddler/api/helpers.py,sha256=-ZLyXnZhWJK9IvdKqZVgDo2c3wpYwI5EizFFu4stQjk,4722
@@ -18,6 +18,7 @@ fiddler/api/model_artifact_deploy.py,sha256=t72HkQ2JsQzCB5bUsFuS71KWDJ-KVcKkdtye
18
18
  fiddler/api/model_deployment_mixin.py,sha256=xZ26FwixQ4FvsGSmQvrUB7joveIGqE07Vph7ADoGDRc,4118
19
19
  fiddler/api/model_mixin.py,sha256=7_Y4NSFx8NpxbPQdHpXNVSMN2wBN445YAZOnFPOchuI,21055
20
20
  fiddler/api/project_mixin.py,sha256=_RRuPnBzANP2Kyo0J_CqG6wqFawRz1Y29z88BUx66Ag,3198
21
+ fiddler/api/webhooks_mixin.py,sha256=nh7EU-VAabxZr42f7iUbphBOudyt-DEnjRLfypetBdw,3800
21
22
  fiddler/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
23
  fiddler/assets/pg_reserved_words.py,sha256=Ar8Y4vrw5cJxc6xMmul42cNw0NEu-LjEV5jK6QlKisY,8200
23
24
  fiddler/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -28,7 +29,7 @@ fiddler/packtools/keras_ig_helpers.py,sha256=66KhHhiYYHryieF7DYhmHC-S6D5NUgrm7lM
28
29
  fiddler/packtools/project_attributions_helpers.py,sha256=PzMTDPUX0KqE-kLyrkW46W6F4nDRvRIka0HjzLYxI4k,14619
29
30
  fiddler/packtools/template_model.py,sha256=-I0hMcuZsHc3jw59o4hyTztU-V1onKJ5z0rKkZzSy1A,9500
30
31
  fiddler/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- fiddler/schema/alert.py,sha256=PsKvDzDc0Ysuxb1pGl_HGFPm0NOJjehIySmgXECag2A,4493
32
+ fiddler/schema/alert.py,sha256=8OUJPWZbHHOjdgKS4mvP7rrDoiNYmoCSTrIdUXs8GTo,5199
32
33
  fiddler/schema/base.py,sha256=3P0h6r1flDQeXfLRANSt6Rng2mTy17hKPYtopFxjKF8,343
33
34
  fiddler/schema/baseline.py,sha256=h8SyR32L1AMo2DrcwgEOqWN-U538-XFngjAtQQfMklI,487
34
35
  fiddler/schema/dataset.py,sha256=kFr_BlP9-hFRjYxy_ezr3M7bmedfK_ZznAdLfvBxYlQ,3045
@@ -39,8 +40,9 @@ fiddler/schema/model_deployment.py,sha256=CkYAzgJN4WQFNgElaZrhTvpLnRFMWnZ3F4hYkz
39
40
  fiddler/schema/project.py,sha256=7kKUMIrGmLWjzuvKsIIIEZj9PIoJ8dXsAnyVOSL35pc,121
40
41
  fiddler/schema/server_info.py,sha256=RKpFDUtpzicQ7I6FB9sMNaFEx-esdSjMiNrc0UIUKjg,388
41
42
  fiddler/schema/user.py,sha256=8p4r2ZMqPEggggd10l62w_7A5KsRr6Se5QP-xfrEtks,109
43
+ fiddler/schema/webhook.py,sha256=0OknRLjXWocHNxMFWrxV2VvEfFzGn91vLw74FiGQSFU,218
42
44
  fiddler/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- fiddler/schemas/custom_features.py,sha256=3Bk0o-16Xh3CzugQJBnvGX3R-UECxgzQK3ogZENFNfI,1750
45
+ fiddler/schemas/custom_features.py,sha256=p-kIEd6vx5LH4ZjjqPwAuS33N-4Yp9eu3EMNthKcB7g,3180
44
46
  fiddler/schemas/model_schema.py,sha256=ByUVfnPzImNbl3GsiyqCpf8o0bDxOzwk-sIg0zeHpeQ,1124
45
47
  fiddler/schemas/model_spec.py,sha256=RiZd-efabsJVfHCI5kXRs_6RrWj4xZB6vqajHSDqxgc,642
46
48
  fiddler/schemas/model_task_params.py,sha256=jsBGgHStwBSkW9LZQtddCJJPMY_6EEWdS8lnVolJlbI,679
@@ -67,12 +69,12 @@ tests/fiddler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
69
  tests/fiddler/base.py,sha256=QeDFGtcVP1LscfSvjJeEBPoQN1bp3s726EKaytY1Gis,1418
68
70
  tests/fiddler/helper.py,sha256=lE0blhcQysB9BuVKScK-rpaxau04ddWD7NrjD5AZkb4,1785
69
71
  tests/fiddler/test_add_model.py,sha256=rWbgfMZa3GExL9Y8ldVvw99BqGH-Zvrww09Z8JPhvTY,3787
70
- tests/fiddler/test_alert.py,sha256=E2PH7ZGUPBbZzSgyKJ5lsYmwWDkxXGqbSxBNL0diATk,36786
72
+ tests/fiddler/test_alert.py,sha256=LEgk0iU7DC7QEXHcjpip6FzS-8w3uuCExkKlunJItSU,50919
71
73
  tests/fiddler/test_baseline.py,sha256=JoAX3KkNYC52_uRVOHZzMNBy5dG57pXtwkT-WtmihHk,10277
72
74
  tests/fiddler/test_check_version_decorator.py,sha256=MGcrIX7nscVpIYy1thuLrSvsXBC6KDXfdsHCEST5Xnw,906
73
75
  tests/fiddler/test_custom_features.py,sha256=yjz-5Aq_Ey7FoDJiIzugLPcbh_5QdazE6trwpT38rEw,3028
74
76
  tests/fiddler/test_dataset_api.py,sha256=donx8nw_BQ6s4-61Wq-Ab4CN5oG5UShnAwY9TX-vskk,6307
75
- tests/fiddler/test_events_api.py,sha256=Ecwhrjbq_d-8n-BOiYnBV0tv5sWDGZ-AVBuCEjZKKDM,7718
77
+ tests/fiddler/test_events_api.py,sha256=YG0wWBBe0ygpqFvMs7PWAxu0DkYUH76EDY0u4iXCQY8,7748
76
78
  tests/fiddler/test_exceptions.py,sha256=3dUe85gqf8MVxEM_9D1wrNRU8jvmu1-ja36jaDKgOQk,2801
77
79
  tests/fiddler/test_explainability_apis.py,sha256=B73inIm2tNNRDnTLgiqU0o1Sy3N_QAuzlOms0QvA5oE,15111
78
80
  tests/fiddler/test_file_upload.py,sha256=TBy0gLfu2sfbqcvx_rrgNF0I4keqkBTReG2EkVIei_Y,7611
@@ -84,12 +86,13 @@ tests/fiddler/test_project.py,sha256=sADiYMDeMnS7d90FHsHugUw3Tz1XASmlhjlSOOVSqnk
84
86
  tests/fiddler/test_response_handler.py,sha256=YWhd8QIJrW3nUag-nZ4jutAs1B9eOqQekgUN1vtsbU0,2958
85
87
  tests/fiddler/test_upload_dataset.py,sha256=fQ7fNtB4-kuY7g6XSNftklxgi_-uhkiuXb7CtxmWz9Q,13719
86
88
  tests/fiddler/test_validations.py,sha256=1FUlfXzf1L-DrSth12u4MUBHGGgGAXDCn-x2TS79Y10,581
89
+ tests/fiddler/test_webhook.py,sha256=Js0zNdXYLOyh3k0Y1qRSS94ACITdiId4YXTQPTpeoc0,4395
87
90
  tests/fiddler/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
91
  tests/fiddler/utils/test_general_checks.py,sha256=pwXqEf1KdXnMugej8fpr8zfiRsN1-yDGwQWuVheIl3c,1473
89
92
  tests/fiddler/utils/test_pandas.py,sha256=rsJg_a1Z60FbY1v5WNhhyqXJIjW-qVFZhNjfwptL7is,8084
90
93
  tests/fiddler/utils/tests_utils.py,sha256=kPmMoOlaSIWw6Gk7QnAnn8QJvEyqtd2Rz32kS6RGEc8,1800
91
- fiddler_client-2.0.0.dev9.dist-info/LICENSE.txt,sha256=w8-LUAb_VLBWSsCNmih0pAqLIicJfGu8OJXpDjkIg_o,559
92
- fiddler_client-2.0.0.dev9.dist-info/METADATA,sha256=RXnM-ZQQtrH7wEtsLpdHJCz1rPTzhxvAfY-qMtjyVtI,17588
93
- fiddler_client-2.0.0.dev9.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
94
- fiddler_client-2.0.0.dev9.dist-info/top_level.txt,sha256=GdJimrE4Q9O20-dA6QljDHm5KBrlz25oQTdbMHu41So,14
95
- fiddler_client-2.0.0.dev9.dist-info/RECORD,,
94
+ fiddler_client-2.0.0.dev10.dist-info/LICENSE.txt,sha256=w8-LUAb_VLBWSsCNmih0pAqLIicJfGu8OJXpDjkIg_o,559
95
+ fiddler_client-2.0.0.dev10.dist-info/METADATA,sha256=eSsma_0KiXXx4-YmihMJ4Jqwlpb2JtsaTv8BG7ejUFk,17922
96
+ fiddler_client-2.0.0.dev10.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
97
+ fiddler_client-2.0.0.dev10.dist-info/top_level.txt,sha256=GdJimrE4Q9O20-dA6QljDHm5KBrlz25oQTdbMHu41So,14
98
+ fiddler_client-2.0.0.dev10.dist-info/RECORD,,
@@ -395,6 +395,185 @@ class TestAlert(BaseTestCase):
395
395
  self.assertEqual(response_items[0]['alert_type'], 'drift')
396
396
  self.assertEqual(response_items[0]['name'], 'alert_name_1')
397
397
 
398
+ def test_get_alert_rules_diff_feature_names_format(self):
399
+ response = {
400
+ 'data': {
401
+ 'page_size': 100,
402
+ 'total': 84,
403
+ 'item_count': 84,
404
+ 'page_count': 1,
405
+ 'page_index': 1,
406
+ 'offset': 0,
407
+ 'items': [
408
+ {
409
+ 'critical_threshold': 0.5,
410
+ 'organization_name': self._org,
411
+ 'id': 1,
412
+ 'sub_metric': 'jsd',
413
+ 'metric': 'jsd',
414
+ 'condition': 'greater',
415
+ 'time_bucket': 3600000,
416
+ 'compare_period': 86400000,
417
+ 'details': 'New Alert',
418
+ 'model_name': self._model_name,
419
+ 'name': 'alert_name_1',
420
+ 'feature_names': "['probability_churned']",
421
+ 'baseline_name': 'baseline_name',
422
+ 'created_by': 'admin@fiddler.ai',
423
+ 'alert_type': 'drift',
424
+ 'uuid': '932bb123-3fc6-4ee5-a48c-1f96bb29f7ee',
425
+ 'is_active': True,
426
+ 'created_at': '2022-09-08T18:47:26.889000+00:00',
427
+ 'project_name': self._project_name,
428
+ 'warning_threshold': 0.2,
429
+ 'compare_to': 'time_period',
430
+ 'alert_log_time': '2022-09-08T18:47:28.034165+00:00',
431
+ 'priority': 'LOW',
432
+ },
433
+ {
434
+ 'critical_threshold': 0.7,
435
+ 'organization_name': self._org,
436
+ 'id': 2,
437
+ 'sub_metric': 'jsd',
438
+ 'metric': 'jsd',
439
+ 'condition': 'lesser',
440
+ 'time_bucket': 3600000,
441
+ 'compare_period': 86400000,
442
+ 'details': 'New Alert',
443
+ 'model_name': self._model_name,
444
+ 'name': 'alert_name_2',
445
+ 'feature_names': ['probability_churned'],
446
+ 'created_by': 'admin@fiddler.ai',
447
+ 'alert_type': 'drift',
448
+ 'uuid': 'e7c7f891-53ba-4246-b691-53b6efda332b',
449
+ 'is_active': True,
450
+ 'created_at': '2022-09-08T18:47:27.923000+00:00',
451
+ 'project_name': self._project_name,
452
+ 'warning_threshold': 0.1,
453
+ 'compare_to': 'time_period',
454
+ 'alert_log_time': '2022-09-08T18:47:29.087871+00:00',
455
+ 'priority': 'LOW',
456
+ },
457
+ {
458
+ 'critical_threshold': 0.8,
459
+ 'organization_name': self._org,
460
+ 'id': 3,
461
+ 'sub_metric': 'jsd',
462
+ 'metric': 'jsd',
463
+ 'condition': 'greater',
464
+ 'time_bucket': 3600000,
465
+ 'model_name': self._model_name,
466
+ 'name': 'alert_name_3',
467
+ 'feature_name': 'probability_churned',
468
+ 'created_by': 'admin@fiddler.ai',
469
+ 'alert_type': 'drift',
470
+ 'uuid': '320060fa-abeb-4cae-9718-17d6f917b414',
471
+ 'is_active': True,
472
+ 'created_at': '2022-09-08T18:47:28.975000+00:00',
473
+ 'project_name': self._project_name,
474
+ 'warning_threshold': 0.4,
475
+ 'compare_to': 'raw_value',
476
+ 'alert_log_time': '2022-09-08T18:47:30.100808+00:00',
477
+ 'priority': 'LOW',
478
+ },
479
+ ],
480
+ },
481
+ 'api_version': '2.0',
482
+ 'kind': 'PAGINATED',
483
+ }
484
+
485
+ url = self._get_url()
486
+ query_params = {
487
+ 'organization_name': self._org,
488
+ 'filter': json.dumps(
489
+ {
490
+ 'condition': 'AND',
491
+ 'rules': [
492
+ {
493
+ 'field': 'project_name',
494
+ 'operator': 'equal',
495
+ 'value': 'test_project',
496
+ },
497
+ {
498
+ 'field': 'model_name',
499
+ 'operator': 'equal',
500
+ 'value': 'test_model',
501
+ },
502
+ {'field': 'alert_type', 'operator': 'equal', 'value': 'drift'},
503
+ {'field': 'metric', 'operator': 'equal', 'value': 'jsd'},
504
+ {
505
+ 'field': 'feature_names',
506
+ 'operator': 'contains',
507
+ 'value': ['test_column'],
508
+ },
509
+ {
510
+ 'field': 'baseline_name',
511
+ 'operator': 'equal',
512
+ 'value': 'test_baseline',
513
+ },
514
+ ],
515
+ }
516
+ ),
517
+ 'ordering': 'alert_type,metric',
518
+ 'limit': 20,
519
+ 'offset': 0,
520
+ }
521
+ self.requests_mock.get(
522
+ url, json=response, match=[matchers.query_param_matcher(query_params)]
523
+ )
524
+
525
+ server_info = ServerInfo(**{'feature_flags': {}, 'server_version': '23.3.0'})
526
+ with mock.patch.object(
527
+ self.client, 'server_info', server_info
528
+ ) as mock_server_info:
529
+ alert_rules = self.client.get_alert_rules(
530
+ project_name='test_project',
531
+ model_name='test_model',
532
+ alert_type='drift',
533
+ metrics=['jsd'],
534
+ columns=['test_column'],
535
+ baseline_name='test_baseline',
536
+ ordering=['alert_type', 'metric'],
537
+ )
538
+ for alert_rule in alert_rules:
539
+ self.assertTrue(alert_rule.columns or alert_rule.column)
540
+ if alert_rule.columns:
541
+ self.assertEqual(alert_rule.columns, ['probability_churned'])
542
+ else:
543
+ self.assertEqual(alert_rule.column, 'probability_churned')
544
+
545
+ query_params = {
546
+ 'filter': json.dumps(
547
+ {"condition": "AND", "rules": [{"field": "project_name", "operator": "equal", "value": "test_project"}, {"field": "model_name", "operator": "equal", "value": "test_model"}, {"field": "metric_id", "operator": "contains", "value": ["jsd"]}, {"field": "feature_names", "operator": "contains", "value": ["test_column"]}, {"field": "baseline_name", "operator": "equal", "value": "test_baseline"}]}
548
+ ),
549
+ 'organization_name': 'test_organization',
550
+ 'ordering': 'alert_type,metric',
551
+ 'limit': 20,
552
+ 'offset': 0,
553
+ }
554
+ self.requests_mock.get(
555
+ url, json=response, match=[matchers.query_param_matcher(query_params)]
556
+ )
557
+ server_info = ServerInfo(**{'feature_flags': {}, 'server_version': '23.4.0'})
558
+ with mock.patch.object(
559
+ self.client, 'server_info', server_info
560
+ ) as mock_server_info:
561
+ alert_rules = self.client.get_alert_rules(
562
+ project_name='test_project',
563
+ model_name='test_model',
564
+ alert_type='drift',
565
+ metrics=['jsd'],
566
+ columns=['test_column'],
567
+ baseline_name='test_baseline',
568
+ ordering=['alert_type', 'metric'],
569
+ )
570
+ for alert_rule in alert_rules:
571
+ self.assertTrue(alert_rule.columns or alert_rule.column)
572
+ if alert_rule.columns:
573
+ self.assertEqual(alert_rule.columns, ['probability_churned'])
574
+ else:
575
+ self.assertEqual(alert_rule.column, 'probability_churned')
576
+
398
577
  def test_delete_alert_rule(self):
399
578
  url = self._get_url(alert_rule_uuid=self._alert_rule_uuid)
400
579
  self.requests_mock.delete(url)
@@ -769,8 +948,8 @@ class TestAlert(BaseTestCase):
769
948
  'offset': 0,
770
949
  'items': [
771
950
  {
772
- 'id': 285,
773
- 'uuid': 'ea45ff98-96de-4015-817d-00e571b3fac7',
951
+ 'id': 1,
952
+ 'uuid': 'uuid_1',
774
953
  'alert_config_uuid': self._alert_rule_uuid,
775
954
  'alert_run_start_time': 1658481873833,
776
955
  'alert_time_bucket': 1657710000000,
@@ -783,12 +962,12 @@ class TestAlert(BaseTestCase):
783
962
  'message': 'In project bank_churn and model bank_churn, during the time period of one hour starting |2022-07-13 11:00:00 (UTC time)|, the prediction drift score was 0.789, greater than 0.1.',
784
963
  },
785
964
  {
786
- 'id': 287,
787
- 'uuid': '9cc8df42-73f4-4f5d-aea9-d8a5ab5b195e',
965
+ 'id': 2,
966
+ 'uuid': 'uuid_2',
788
967
  'alert_config_uuid': self._alert_rule_uuid,
789
968
  'alert_run_start_time': 1658481873991,
790
969
  'alert_time_bucket': 1657720800000,
791
- 'alert_value': {'__DEFAULT__': 0.789176795308359},
970
+ 'alert_value': 0.789176795308359,
792
971
  'baseline_time_bucket': None,
793
972
  'baseline_value': None,
794
973
  'is_alert': True,
@@ -797,18 +976,19 @@ class TestAlert(BaseTestCase):
797
976
  'message': 'In project bank_churn and model bank_churn, during the time period of one hour starting |2022-07-13 14:00:00 (UTC time)|, the prediction drift score was 0.789, greater than 0.1.',
798
977
  },
799
978
  {
800
- 'id': 288,
801
- 'uuid': '5d2868cf-0321-46cd-af36-b784710b2295',
979
+ 'id': 3,
980
+ 'uuid': 'uuid_3',
802
981
  'alert_config_uuid': self._alert_rule_uuid,
803
982
  'alert_run_start_time': 1658481874167,
804
983
  'alert_time_bucket': 1657724400000,
805
- 'alert_value': {'__DEFAULT__': 0.789176795308359},
984
+ 'alert_value': 0.789176795308359,
806
985
  'baseline_time_bucket': None,
807
986
  'baseline_value': None,
808
987
  'is_alert': True,
809
988
  'severity': None,
810
989
  'failure_reason': 'NA',
811
990
  'message': 'In project bank_churn and model bank_churn, during the time period of one hour starting |2022-07-13 15:00:00 (UTC time)|, the prediction drift score was 0.789, greater than 0.1.',
991
+ 'feature_name':'feature_name'
812
992
  },
813
993
  ],
814
994
  }
@@ -830,14 +1010,160 @@ class TestAlert(BaseTestCase):
830
1010
  )
831
1011
  self.assertEqual(triggered_alerts[0].failure_reason, 'NA')
832
1012
  self.assertEqual(triggered_alerts[0].is_alert, True)
1013
+ self.assertEqual(triggered_alerts[1].alert_value, 0.789176795308359)
1014
+ self.assertTrue('feature_names' not in triggered_alerts[2].dict())
1015
+ self.assertEqual(triggered_alerts[2].alert_value, 0.789176795308359)
1016
+ self.assertEqual(triggered_alerts[2].feature_name, 'feature_name')
833
1017
 
834
1018
  response_items = self.requests_mock.calls[0].response.json()['data']['items']
835
1019
 
836
1020
  self.assertEqual(response_items[0]['is_alert'], True)
837
- self.assertEqual(response_items[0]['id'], 285)
1021
+ self.assertEqual(response_items[0]['id'], 1)
838
1022
  self.assertEqual(response_items[0]['alert_time_bucket'], 1657710000000)
839
1023
  self.assertEqual(response_items[0]['alert_config_uuid'], self._alert_rule_uuid)
840
1024
 
1025
+ def test_alert_rule_serializer(self):
1026
+ alert_rule_231 = {
1027
+ 'critical_threshold': 0.8,
1028
+ 'organization_name': self._org,
1029
+ 'id': 3,
1030
+ 'sub_metric': 'jsd',
1031
+ 'metric': 'jsd',
1032
+ 'condition': 'greater',
1033
+ 'time_bucket': 3600000,
1034
+ 'model_name': self._model_name,
1035
+ 'name': 'alert_name_3',
1036
+ 'feature_name': 'probability_churned',
1037
+ 'created_by': 'admin@fiddler.ai',
1038
+ 'alert_type': 'drift',
1039
+ 'uuid': '320060fa-abeb-4cae-9718-17d6f917b414',
1040
+ 'is_active': True,
1041
+ 'created_at': '2022-09-08T18:47:28.975000+00:00',
1042
+ 'project_name': self._project_name,
1043
+ 'warning_threshold': 0.4,
1044
+ 'compare_to': 'raw_value',
1045
+ 'alert_log_time': '2022-09-08T18:47:30.100808+00:00',
1046
+ 'priority': 'LOW',
1047
+ }
1048
+ alert_rule_parsed_231:AlertRule = AlertRule.parse_obj(alert_rule_231)
1049
+ self.assertEqual(alert_rule_parsed_231.column, 'probability_churned')
1050
+ self.assertEqual(alert_rule_parsed_231.columns, None)
1051
+
1052
+ alert_rule_232 = {
1053
+ 'critical_threshold': 0.8,
1054
+ 'organization_name': self._org,
1055
+ 'id': 3,
1056
+ 'sub_metric': 'jsd',
1057
+ 'metric': 'jsd',
1058
+ 'condition': 'greater',
1059
+ 'time_bucket': 3600000,
1060
+ 'model_name': self._model_name,
1061
+ 'name': 'alert_name_3',
1062
+ 'feature_names': "['probability_churned']",
1063
+ 'created_by': 'admin@fiddler.ai',
1064
+ 'alert_type': 'drift',
1065
+ 'uuid': '320060fa-abeb-4cae-9718-17d6f917b414',
1066
+ 'is_active': True,
1067
+ 'created_at': '2022-09-08T18:47:28.975000+00:00',
1068
+ 'project_name': self._project_name,
1069
+ 'warning_threshold': 0.4,
1070
+ 'compare_to': 'raw_value',
1071
+ 'alert_log_time': '2022-09-08T18:47:30.100808+00:00',
1072
+ 'priority': 'LOW',
1073
+ }
1074
+ alert_rule_parsed_232:AlertRule = AlertRule.parse_obj(alert_rule_232)
1075
+ self.assertEqual(alert_rule_parsed_232.column, None)
1076
+ self.assertEqual(alert_rule_parsed_232.columns, ['probability_churned'])
1077
+
1078
+ alert_rule_233 = {
1079
+ 'critical_threshold': 0.8,
1080
+ 'organization_name': self._org,
1081
+ 'id': 3,
1082
+ 'sub_metric': 'jsd',
1083
+ 'metric': 'jsd',
1084
+ 'condition': 'greater',
1085
+ 'time_bucket': 3600000,
1086
+ 'model_name': self._model_name,
1087
+ 'name': 'alert_name_3',
1088
+ 'feature_names': ['probability_churned'],
1089
+ 'created_by': 'admin@fiddler.ai',
1090
+ 'alert_type': 'drift',
1091
+ 'uuid': '320060fa-abeb-4cae-9718-17d6f917b414',
1092
+ 'is_active': True,
1093
+ 'created_at': '2022-09-08T18:47:28.975000+00:00',
1094
+ 'project_name': self._project_name,
1095
+ 'warning_threshold': 0.4,
1096
+ 'compare_to': 'raw_value',
1097
+ 'alert_log_time': '2022-09-08T18:47:30.100808+00:00',
1098
+ 'priority': 'LOW',
1099
+ }
1100
+ alert_rule_parsed_233:AlertRule = AlertRule.parse_obj(alert_rule_233)
1101
+ self.assertEqual(alert_rule_parsed_233.column, None)
1102
+ self.assertEqual(alert_rule_parsed_233.columns, ['probability_churned'])
1103
+
1104
+ alert_rule_empty_features = {
1105
+ 'critical_threshold': 0.8,
1106
+ 'organization_name': self._org,
1107
+ 'id': 3,
1108
+ 'sub_metric': 'jsd',
1109
+ 'metric': 'jsd',
1110
+ 'condition': 'greater',
1111
+ 'time_bucket': 3600000,
1112
+ 'model_name': self._model_name,
1113
+ 'name': 'alert_name_3',
1114
+ 'created_by': 'admin@fiddler.ai',
1115
+ 'alert_type': 'drift',
1116
+ 'uuid': '320060fa-abeb-4cae-9718-17d6f917b414',
1117
+ 'is_active': True,
1118
+ 'created_at': '2022-09-08T18:47:28.975000+00:00',
1119
+ 'project_name': self._project_name,
1120
+ 'warning_threshold': 0.4,
1121
+ 'compare_to': 'raw_value',
1122
+ 'alert_log_time': '2022-09-08T18:47:30.100808+00:00',
1123
+ 'priority': 'LOW',
1124
+ }
1125
+ alert_rule_parsed_empty_features:AlertRule = AlertRule.parse_obj(alert_rule_empty_features)
1126
+ self.assertEqual(alert_rule_parsed_empty_features.column, None)
1127
+ self.assertEqual(alert_rule_parsed_empty_features.columns, None)
1128
+
1129
+ def test_triggered_alert_record_serializer(self):
1130
+ tar_with_value_as_float = {
1131
+ 'id': 1,
1132
+ 'uuid': 'uuid',
1133
+ 'alert_config_uuid': 'alert_config_uuid',
1134
+ 'alert_run_start_time': 1,
1135
+ 'alert_time_bucket': 1,
1136
+ 'alert_value': 1.0,
1137
+ 'baseline_time_bucket': 1,
1138
+ 'baseline_value': 0.0,
1139
+ 'is_alert': True,
1140
+ 'severity': 'HIGH',
1141
+ 'failure_reason': 'NA',
1142
+ 'message': 'some message',
1143
+ 'multi_col_values': {'a' : 1.0, 'b': 2.0},
1144
+ 'feature_name': 'age'
1145
+ }
1146
+ tar:TriggeredAlerts = TriggeredAlerts.parse_obj(tar_with_value_as_float)
1147
+ self.assertEqual(tar.alert_value, 1.0)
1148
+ self.assertEqual(tar.feature_name, 'age')
1149
+
1150
+ tar_with_value_as_dict = {
1151
+ 'id': 1,
1152
+ 'uuid': 'uuid',
1153
+ 'alert_config_uuid': 'alert_config_uuid',
1154
+ 'alert_run_start_time': 1,
1155
+ 'alert_time_bucket': 1,
1156
+ 'alert_value': {'a' : 1.0, 'b': 2.0},
1157
+ 'baseline_time_bucket': 1,
1158
+ 'baseline_value': 0.0,
1159
+ 'is_alert': True,
1160
+ 'severity': 'HIGH',
1161
+ 'failure_reason': 'NA',
1162
+ 'message': 'some message',
1163
+ }
1164
+ tar:TriggeredAlerts = TriggeredAlerts.parse_obj(tar_with_value_as_dict)
1165
+ self.assertEqual(tar.alert_value, {'a' : 1.0, 'b': 2.0})
841
1166
 
842
1167
  if __name__ == '__main__':
843
1168
  unittest.main()
1169
+
@@ -145,6 +145,7 @@ class TestEventsAPI(BaseTestCase):
145
145
  timestamp_format=timestamp_format,
146
146
  group_by=group_by,
147
147
  wait=wait,
148
+ file_type='.csv',
148
149
  )
149
150
 
150
151
  _, kwargs = publish_events_patcher.call_args
@@ -0,0 +1,124 @@
1
+ import json
2
+ import unittest
3
+ from http import HTTPStatus
4
+ from typing import Tuple
5
+
6
+ from responses import matchers
7
+
8
+ from fiddler.exceptions import (
9
+ BadRequest,
10
+ Conflict,
11
+ NotFound,
12
+ )
13
+ from fiddler.schema.webhook import Webhook
14
+ from tests.fiddler.base import BaseTestCase
15
+
16
+
17
+ class TestWebhook(BaseTestCase):
18
+ def setUp(self):
19
+ super(TestWebhook, self).setUp()
20
+
21
+ def _get_url(self, uuid: str = None) -> str:
22
+ base_url = f'{self._url}/webhooks'
23
+ if uuid:
24
+ return f'{base_url}/{uuid}',
25
+ return base_url
26
+
27
+ def test_get_webhooks(self):
28
+ response = {
29
+ 'data': {
30
+ 'page_size': 100,
31
+ 'total': 3,
32
+ 'item_count': 3,
33
+ 'page_count': 1,
34
+ 'page_index': 1,
35
+ 'offset': 0,
36
+ 'items': [
37
+ {
38
+ 'id': 0,
39
+ 'uuid': 'uuid_0',
40
+ 'name': 'webhook_0',
41
+ 'url': 'url_0',
42
+ 'provider': 'SLACK',
43
+ 'organization_name': 'test_organization',
44
+ 'created_at': '2022-05-19T18:20:25.991688+00:00',
45
+ 'updated_at': '2022-05-19T18:20:25.991688+00:00',
46
+ 'created_by': 'test_user',
47
+ 'updated_by': 'test_user',
48
+ },
49
+ {
50
+ 'id': 1,
51
+ 'uuid': 'uuid_1',
52
+ 'name': 'webhook_1',
53
+ 'url': 'url_1',
54
+ 'provider': 'SLACK',
55
+ 'organization_name': 'test_organization',
56
+ 'created_at': '2022-05-19T18:20:25.991688+00:00',
57
+ 'updated_at': '2022-05-19T18:20:25.991688+00:00',
58
+ 'created_by': 'test_user',
59
+ 'updated_by': 'test_user',
60
+ },
61
+ {
62
+ 'id': 2,
63
+ 'uuid': 'uuid_2',
64
+ 'name': 'webhook_2',
65
+ 'url': 'url_2',
66
+ 'provider': 'SLACK',
67
+ 'organization_name': 'test_organization',
68
+ 'created_at': '2022-05-19T18:20:25.991688+00:00',
69
+ 'updated_at': '2022-05-19T18:20:25.991688+00:00',
70
+ 'created_by': 'test_user',
71
+ 'updated_by': 'test_user',
72
+ },
73
+ ],
74
+ },
75
+ 'api_version': '2.0',
76
+ 'kind': 'PAGINATED',
77
+ }
78
+ url = self._get_url()
79
+ query_params = {'limit': 300, 'offset': 0, 'organization_name': 'test_organization'}
80
+
81
+ self.requests_mock.get(
82
+ url, json=response, match=[matchers.query_param_matcher(query_params)]
83
+ )
84
+
85
+ webhooks = self.client.get_webhooks()
86
+ self.assertIsInstance(webhooks[0], Webhook)
87
+ self.assertEqual(webhooks[0].name, 'webhook_0')
88
+ self.assertEqual(webhooks[0].organization_name, self._org)
89
+ self.assertEqual(len(webhooks), 3)
90
+
91
+ def test_add_webhook(self):
92
+ response_body = {
93
+ 'data': {
94
+ 'id': 0,
95
+ 'uuid': 'uuid_0',
96
+ 'name': 'webhook_0',
97
+ 'url': 'url_0',
98
+ 'provider': 'SLACK',
99
+ 'organization_name': 'test_organization',
100
+ 'created_at': '2022-05-19T18:20:25.991688+00:00',
101
+ 'updated_at': '2022-05-19T18:20:25.991688+00:00',
102
+ 'created_by': 'test_user',
103
+ 'updated_by': 'test_user',
104
+ },
105
+ 'api_version': '2.0',
106
+ 'kind': 'NORMAL',
107
+ }
108
+
109
+ url = self._get_url()
110
+ query_params = {'organization_name':'test_organization'}
111
+
112
+ self.requests_mock.post(
113
+ url, json=response_body, match=[matchers.query_param_matcher(query_params)]
114
+ )
115
+
116
+ webhook = self.client.add_webhook(
117
+ name='webhook_0', url='url_i', provider='SLACK'
118
+ )
119
+ request_body = json.loads(self.requests_mock.calls[0].request.body)
120
+ self.assertEqual(
121
+ webhook.uuid, 'uuid_0'
122
+ )
123
+ self.assertEqual(len(self.requests_mock.calls), 1)
124
+ self.assertEqual(request_body['name'], 'webhook_0')