fiddler-client 2.4.0.dev2__py3-none-any.whl → 2.5.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 +51 -1
- fiddler/api/webhooks_mixin.py +6 -8
- fiddler/core_objects.py +67 -30
- fiddler/schema/alert.py +8 -1
- fiddler/schemas/custom_features.py +13 -0
- fiddler3/__init__.py +1 -36
- fiddler3/connection.py +68 -1
- fiddler3/constants/alert_rule.py +30 -0
- fiddler3/constants/events.py +7 -0
- fiddler3/entities/__init__.py +3 -0
- fiddler3/entities/alert_record.py +106 -0
- fiddler3/entities/alert_rule.py +246 -0
- fiddler3/entities/base.py +2 -16
- fiddler3/entities/events.py +120 -0
- fiddler3/entities/files.py +170 -0
- fiddler3/entities/helpers.py +6 -6
- fiddler3/entities/model.py +67 -18
- fiddler3/entities/model_artifact.py +48 -220
- fiddler3/entities/project.py +1 -1
- fiddler3/entities/webhook.py +128 -0
- fiddler3/entities/xai.py +267 -0
- fiddler3/libs/json_encoder.py +1 -1
- fiddler3/schemas/alert_record.py +27 -0
- fiddler3/schemas/alert_rule.py +32 -0
- fiddler3/schemas/custom_features.py +34 -0
- fiddler3/schemas/events.py +15 -0
- fiddler3/schemas/files.py +23 -0
- fiddler3/schemas/model.py +1 -6
- fiddler3/schemas/webhook.py +26 -0
- fiddler3/tests/apis/test_alert_record.py +99 -0
- fiddler3/tests/apis/test_alert_rule.py +323 -0
- fiddler3/tests/apis/test_events.py +128 -0
- fiddler3/tests/apis/test_files.py +164 -0
- fiddler3/tests/apis/test_model.py +55 -8
- fiddler3/tests/apis/test_model_artifact.py +21 -0
- fiddler3/tests/apis/test_project.py +2 -2
- fiddler3/tests/apis/test_webhook.py +280 -0
- fiddler3/tests/apis/test_xai.py +233 -1
- fiddler3/tests/conftest.py +2 -2
- fiddler3/tests/constants.py +5 -0
- fiddler3/tests/test_connection.py +24 -1
- fiddler3/tests/test_json_encoder.py +2 -2
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/METADATA +1 -1
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/RECORD +50 -34
- tests/fiddler/test_alert.py +95 -5
- fiddler3/schemas/model_artifact.py +0 -6
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/LICENSE.txt +0 -0
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/WHEEL +0 -0
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/top_level.txt +0 -0
fiddler/__init__.py
CHANGED
|
@@ -50,6 +50,7 @@ from fiddler.schemas.custom_features import (
|
|
|
50
50
|
Multivariate,
|
|
51
51
|
TextEmbedding,
|
|
52
52
|
VectorFeature,
|
|
53
|
+
Enrichment,
|
|
53
54
|
)
|
|
54
55
|
from fiddler.utils import ColorLogger
|
|
55
56
|
from fiddler.utils.logger import get_logger
|
|
@@ -71,6 +72,7 @@ __all__ = [
|
|
|
71
72
|
'VectorFeature',
|
|
72
73
|
'TextEmbedding',
|
|
73
74
|
'ImageEmbedding',
|
|
75
|
+
'Enrichment',
|
|
74
76
|
'ColorLogger',
|
|
75
77
|
'DatasetInfo',
|
|
76
78
|
'DataType',
|
fiddler/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '2.
|
|
1
|
+
__version__ = '2.5.0.dev1'
|
fiddler/api/alert_mixin.py
CHANGED
|
@@ -6,6 +6,8 @@ from typing import Any, Dict, List, Optional, Union
|
|
|
6
6
|
|
|
7
7
|
from pydantic import parse_obj_as
|
|
8
8
|
|
|
9
|
+
import fiddler3
|
|
10
|
+
from fiddler3.entities.model import Model
|
|
9
11
|
from fiddler.libs.http_client import RequestClient
|
|
10
12
|
from fiddler.schema.alert import (
|
|
11
13
|
AlertCondition,
|
|
@@ -38,6 +40,8 @@ class AlertMixin:
|
|
|
38
40
|
client: RequestClient
|
|
39
41
|
organization_name: str
|
|
40
42
|
server_info: ServerInfo
|
|
43
|
+
url: str
|
|
44
|
+
auth_token: str
|
|
41
45
|
|
|
42
46
|
@handle_api_error_response
|
|
43
47
|
def add_alert_rule(
|
|
@@ -61,7 +65,7 @@ class AlertMixin:
|
|
|
61
65
|
warning_threshold: Optional[float] = None,
|
|
62
66
|
notifications_config: Optional[Dict[str, Any]] = None,
|
|
63
67
|
) -> AlertRule:
|
|
64
|
-
"""
|
|
68
|
+
f"""
|
|
65
69
|
To add an alert rule
|
|
66
70
|
|
|
67
71
|
:param project_id: Unique project name for which the alert rule is created
|
|
@@ -488,3 +492,49 @@ class AlertMixin:
|
|
|
488
492
|
},
|
|
489
493
|
'webhooks': webhooks_dict,
|
|
490
494
|
}
|
|
495
|
+
|
|
496
|
+
def update_alert_notification_status(
|
|
497
|
+
self,
|
|
498
|
+
notification_status: bool,
|
|
499
|
+
alert_config_ids: Optional[list[str]] = None,
|
|
500
|
+
model_id: Optional[str] = None,
|
|
501
|
+
) -> list[AlertRuleWithNotifications]:
|
|
502
|
+
"""
|
|
503
|
+
To enable/disable the notifications for a list of Alert Ids or for a given Model Id.
|
|
504
|
+
Either the Alert Ids should be given or the Model Id, but not both.
|
|
505
|
+
:param notification_status: The value of notification status for an alert
|
|
506
|
+
:param alert_config_ids: List of Alert Ids, on which we need to update notification
|
|
507
|
+
:param model_id: The Model Id which for whom we want to update all alerts.
|
|
508
|
+
:return: List of updated Alert Rules.
|
|
509
|
+
"""
|
|
510
|
+
if not alert_config_ids and not model_id:
|
|
511
|
+
raise ValueError('Either alert_config_ids or model_id must be specified')
|
|
512
|
+
elif alert_config_ids and model_id:
|
|
513
|
+
raise ValueError(
|
|
514
|
+
'Either alert_config_ids or model_id should be specified, not both.'
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
updated_alert_configs: list[AlertRuleWithNotifications] = []
|
|
518
|
+
alert_ids: list[str] = []
|
|
519
|
+
|
|
520
|
+
if alert_config_ids:
|
|
521
|
+
# verify that all the alert-configs are valid
|
|
522
|
+
for alert_id in alert_config_ids:
|
|
523
|
+
self.client.get(
|
|
524
|
+
url=f'alert-configs/{alert_id}',
|
|
525
|
+
)
|
|
526
|
+
alert_ids = alert_config_ids
|
|
527
|
+
|
|
528
|
+
if model_id:
|
|
529
|
+
fiddler3.init(url=self.url, token=self.auth_token)
|
|
530
|
+
model: Model = Model.get(id_=model_id)
|
|
531
|
+
alerts: list[AlertRule] = self.get_alert_rules(model_id=model.name)
|
|
532
|
+
alert_ids = [alert.alert_rule_uuid for alert in alerts]
|
|
533
|
+
|
|
534
|
+
for alert_id in alert_ids:
|
|
535
|
+
updated_alert_configs.append(
|
|
536
|
+
self.update_alert_rule(
|
|
537
|
+
alert_config_uuid=alert_id, enable_notification=notification_status
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
return updated_alert_configs
|
fiddler/api/webhooks_mixin.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from http import HTTPStatus
|
|
2
|
-
from typing import List
|
|
2
|
+
from typing import Dict, List
|
|
3
3
|
|
|
4
4
|
from pydantic import parse_obj_as
|
|
5
5
|
|
|
@@ -80,13 +80,11 @@ class WebhookMixin:
|
|
|
80
80
|
|
|
81
81
|
:returns: Updated `Webhook` object.
|
|
82
82
|
"""
|
|
83
|
-
request_body =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
provider=provider,
|
|
89
|
-
).dict()
|
|
83
|
+
request_body: Dict[str, str] = {
|
|
84
|
+
'name': name,
|
|
85
|
+
'url': url,
|
|
86
|
+
'provider': provider,
|
|
87
|
+
}
|
|
90
88
|
response = self.client.patch(
|
|
91
89
|
url=f'webhooks/{uuid}',
|
|
92
90
|
data=request_body,
|
fiddler/core_objects.py
CHANGED
|
@@ -28,6 +28,7 @@ from pandas.api.types import CategoricalDtype
|
|
|
28
28
|
|
|
29
29
|
from fiddler.schemas.custom_features import (
|
|
30
30
|
CustomFeature,
|
|
31
|
+
Enrichment,
|
|
31
32
|
ImageEmbedding,
|
|
32
33
|
Multivariate,
|
|
33
34
|
TextEmbedding,
|
|
@@ -1231,10 +1232,10 @@ class ModelInfo:
|
|
|
1231
1232
|
'The custom_features argument only accepts a list of CustomFeature objects.'
|
|
1232
1233
|
)
|
|
1233
1234
|
|
|
1234
|
-
available_cols = (
|
|
1235
|
-
self.get_input_names()
|
|
1236
|
-
|
|
1237
|
-
|
|
1235
|
+
available_cols: Set[str] = set().union(
|
|
1236
|
+
self.get_input_names(),
|
|
1237
|
+
self.get_target_names(),
|
|
1238
|
+
self.get_metadata_names(),
|
|
1238
1239
|
)
|
|
1239
1240
|
|
|
1240
1241
|
numeric_cols = self.get_input_pandas_dtypes()
|
|
@@ -1243,19 +1244,18 @@ class ModelInfo:
|
|
|
1243
1244
|
if self.targets:
|
|
1244
1245
|
numeric_cols.update(self.get_target_pandas_dtypes())
|
|
1245
1246
|
|
|
1246
|
-
valid_numeric_names =
|
|
1247
|
+
valid_numeric_names = set(
|
|
1247
1248
|
k
|
|
1248
1249
|
for k, v in numeric_cols.items()
|
|
1249
1250
|
if v in (DataType.INTEGER.value, DataType.FLOAT.value)
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
valid_vector_cols = [
|
|
1251
|
+
)
|
|
1252
|
+
valid_vector_cols = set(
|
|
1253
1253
|
vec_col.name
|
|
1254
1254
|
for vec_col in (self.inputs or []) + (self.metadata or [])
|
|
1255
1255
|
if vec_col.data_type.is_vector()
|
|
1256
|
-
|
|
1256
|
+
)
|
|
1257
1257
|
|
|
1258
|
-
|
|
1258
|
+
valid_cf_names, valid_enrichment_names = set(), set()
|
|
1259
1259
|
for feature in self.custom_features:
|
|
1260
1260
|
if not isinstance(feature, CustomFeature):
|
|
1261
1261
|
raise ValueError(
|
|
@@ -1267,51 +1267,88 @@ class ModelInfo:
|
|
|
1267
1267
|
f'A column name "{feature.name}" already exists in the dataset. Please use a different name for custom feature {feature}.'
|
|
1268
1268
|
)
|
|
1269
1269
|
|
|
1270
|
-
if feature.name in
|
|
1270
|
+
if feature.name in valid_cf_names:
|
|
1271
1271
|
raise ValueError(
|
|
1272
1272
|
f'Multiple custom features are defined with the same name {feature.name}.'
|
|
1273
1273
|
)
|
|
1274
|
-
|
|
1275
|
-
|
|
1274
|
+
if isinstance(feature, Enrichment):
|
|
1275
|
+
valid_enrichment_names.add(feature.name)
|
|
1276
|
+
valid_cf_names.add(feature.name)
|
|
1276
1277
|
|
|
1278
|
+
for feature in self.custom_features:
|
|
1277
1279
|
if isinstance(feature, Multivariate):
|
|
1278
1280
|
self.validate_multivariate_cf(feature, valid_numeric_names)
|
|
1279
1281
|
elif isinstance(feature, TextEmbedding):
|
|
1280
|
-
self.
|
|
1282
|
+
self.validate_embedding_cf(
|
|
1283
|
+
feature,
|
|
1284
|
+
available_cols,
|
|
1285
|
+
valid_vector_cols,
|
|
1286
|
+
valid_enrichment_names,
|
|
1287
|
+
)
|
|
1288
|
+
elif isinstance(feature, ImageEmbedding):
|
|
1289
|
+
self.validate_embedding_cf(
|
|
1281
1290
|
feature,
|
|
1282
1291
|
available_cols,
|
|
1283
1292
|
valid_vector_cols,
|
|
1293
|
+
[],
|
|
1284
1294
|
)
|
|
1285
1295
|
elif isinstance(feature, VectorFeature):
|
|
1286
1296
|
self.validate_vector_cf(feature, valid_vector_cols)
|
|
1297
|
+
elif isinstance(feature, Enrichment):
|
|
1298
|
+
self.validate_enrichment_cf(
|
|
1299
|
+
feature, available_cols, valid_cf_names
|
|
1300
|
+
)
|
|
1287
1301
|
|
|
1288
|
-
def
|
|
1302
|
+
def validate_enrichment_cf(
|
|
1289
1303
|
self,
|
|
1290
|
-
feature:
|
|
1291
|
-
available_cols:
|
|
1292
|
-
|
|
1304
|
+
feature: Enrichment,
|
|
1305
|
+
available_cols: Iterable[str],
|
|
1306
|
+
valid_cf_names: Iterable[str],
|
|
1293
1307
|
) -> None:
|
|
1294
|
-
if feature.
|
|
1308
|
+
if not feature.columns:
|
|
1295
1309
|
raise ValueError(
|
|
1296
|
-
f
|
|
1297
|
-
)
|
|
1298
|
-
|
|
1299
|
-
if feature.source_column not in available_cols:
|
|
1300
|
-
raise ValueError(
|
|
1301
|
-
f'{feature.name} ({feature.__class__.__name__}) needs to specify source_col.'
|
|
1310
|
+
f'{feature.name} ({feature.__class__.__name__}) must specify at least one enrichment/column/custom feature in columns.'
|
|
1302
1311
|
)
|
|
1312
|
+
for column in feature.columns:
|
|
1313
|
+
if column == feature.name:
|
|
1314
|
+
raise ValueError(
|
|
1315
|
+
f'{feature.name} ({feature.__class__.__name__}) cannot depend on itself.'
|
|
1316
|
+
)
|
|
1317
|
+
if column not in valid_cf_names and column not in available_cols:
|
|
1318
|
+
raise ValueError(
|
|
1319
|
+
f"Column '{column}' defined in {feature.name} ({feature.__class__.__name__}) does not exist in the data, as a custom feature or as an enrichment."
|
|
1320
|
+
)
|
|
1303
1321
|
|
|
1304
|
-
def
|
|
1305
|
-
self,
|
|
1322
|
+
def validate_embedding_cf(
|
|
1323
|
+
self,
|
|
1324
|
+
feature: Union[TextEmbedding, ImageEmbedding],
|
|
1325
|
+
available_cols: Iterable[str],
|
|
1326
|
+
valid_vector_cols: Iterable[str],
|
|
1327
|
+
valid_enrichment_names: Iterable[str],
|
|
1306
1328
|
) -> None:
|
|
1329
|
+
if feature.column not in valid_vector_cols:
|
|
1330
|
+
# we require either a column or source_column
|
|
1331
|
+
if not feature.source_column:
|
|
1332
|
+
raise ValueError(
|
|
1333
|
+
f'{feature.name} ({feature.__class__.__name__}) needs to specify source_column.'
|
|
1334
|
+
)
|
|
1335
|
+
if feature.source_column not in available_cols:
|
|
1336
|
+
raise ValueError(
|
|
1337
|
+
f'source_column {feature.source_column} defined in {feature.name} ({feature.__class__.__name__}) does not exist or cannot be used as a source column.'
|
|
1338
|
+
)
|
|
1339
|
+
# column could be the result of an enrichment
|
|
1340
|
+
if feature.column not in valid_enrichment_names:
|
|
1341
|
+
raise ValueError(
|
|
1342
|
+
f"Column/Enrichment'{feature.column}' defined in {feature.name} ({feature.__class__.__name__}) does not exist or not of type vector."
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
def validate_vector_cf(self, feature: VectorFeature, valid_vector_cols: Iterable[str]) -> None:
|
|
1307
1346
|
if feature.column not in valid_vector_cols:
|
|
1308
1347
|
raise ValueError(
|
|
1309
1348
|
f"Column '{feature.column}' defined in {feature.name} ({feature.__class__.__name__}) does not exist or not of type vector."
|
|
1310
1349
|
)
|
|
1311
1350
|
|
|
1312
|
-
def validate_multivariate_cf(
|
|
1313
|
-
self, feature: Multivariate, valid_numeric_names: List[str]
|
|
1314
|
-
) -> None:
|
|
1351
|
+
def validate_multivariate_cf(self, feature: Multivariate, valid_numeric_names: Iterable[str]) -> None:
|
|
1315
1352
|
if len(feature.columns or []) < 2:
|
|
1316
1353
|
raise ValueError(
|
|
1317
1354
|
f'{feature.name} ({feature.__class__.__name__}) must specify at least two columns.'
|
fiddler/schema/alert.py
CHANGED
|
@@ -34,6 +34,12 @@ class Metric(str, enum.Enum):
|
|
|
34
34
|
MISSING_VALUE = 'null_violation_count'
|
|
35
35
|
RANGE_VIOLATION = 'range_violation_count'
|
|
36
36
|
TYPE_VIOLATION = 'type_violation_count'
|
|
37
|
+
ANY_VIOLATION = 'any_violation_count'
|
|
38
|
+
|
|
39
|
+
MISSING_VALUE_PCT = 'null_violation_percentage'
|
|
40
|
+
RANGE_VIOLATION_PCT = 'range_violation_percentage'
|
|
41
|
+
TYPE_VIOLATION_PCT = 'type_violation_percentage'
|
|
42
|
+
ANY_VIOLATION_PCT = 'any_violation_percentage'
|
|
37
43
|
|
|
38
44
|
## Performance metrics
|
|
39
45
|
# Binary Classification
|
|
@@ -47,7 +53,6 @@ class Metric(str, enum.Enum):
|
|
|
47
53
|
CALIBRATED_THRESHOLD = 'calibrated_threshold'
|
|
48
54
|
GMEAN = 'geometric_mean'
|
|
49
55
|
LOG_LOSS = 'log_loss'
|
|
50
|
-
|
|
51
56
|
|
|
52
57
|
# Regression
|
|
53
58
|
R2 = 'r2'
|
|
@@ -224,4 +229,6 @@ class TriggeredAlerts(BaseDataSchema):
|
|
|
224
229
|
message: str
|
|
225
230
|
multi_col_values: Optional[Dict[str, float]]
|
|
226
231
|
feature_name: Optional[str]
|
|
232
|
+
alert_record_main_version: int
|
|
233
|
+
alert_record_sub_version: int
|
|
227
234
|
segment: Optional[Segment]
|
|
@@ -47,6 +47,8 @@ class CustomFeature(BaseModel):
|
|
|
47
47
|
return TextEmbedding.parse_obj(deserialized_json)
|
|
48
48
|
elif feature_type == CustomFeatureType.FROM_IMAGE_EMBEDDING:
|
|
49
49
|
return ImageEmbedding.parse_obj(deserialized_json)
|
|
50
|
+
elif feature_type == CustomFeatureType.ENRICHMENT:
|
|
51
|
+
return Enrichment.parse_obj(deserialized_json)
|
|
50
52
|
else:
|
|
51
53
|
raise ValueError(f'Unsupported feature type: {feature_type}')
|
|
52
54
|
|
|
@@ -64,6 +66,10 @@ class CustomFeature(BaseModel):
|
|
|
64
66
|
return_dict['source_column'] = self.source_column
|
|
65
67
|
if isinstance(self, TextEmbedding):
|
|
66
68
|
return_dict['n_tags'] = self.n_tags
|
|
69
|
+
elif isinstance(self, Enrichment):
|
|
70
|
+
return_dict['columns'] = self.columns
|
|
71
|
+
return_dict['enrichment'] = self.enrichment
|
|
72
|
+
return_dict['config'] = self.config
|
|
67
73
|
else:
|
|
68
74
|
raise ValueError(f'Unsupported feature type: {self.type} {type(self)}')
|
|
69
75
|
|
|
@@ -89,3 +95,10 @@ class TextEmbedding(VectorFeature):
|
|
|
89
95
|
|
|
90
96
|
class ImageEmbedding(VectorFeature):
|
|
91
97
|
type: CustomFeatureType = CustomFeatureType.FROM_IMAGE_EMBEDDING
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Enrichment(CustomFeature):
|
|
101
|
+
type: CustomFeatureType = CustomFeatureType.ENRICHMENT
|
|
102
|
+
columns: List[str]
|
|
103
|
+
enrichment: str
|
|
104
|
+
config: Dict[str, Any] = {}
|
fiddler3/__init__.py
CHANGED
|
@@ -1,39 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from fiddler3.configs import DEFAULT_CONNECTION_TIMEOUT
|
|
4
|
-
from fiddler3.connection import Connection
|
|
1
|
+
from fiddler3.connection import Connection, ConnectionMixin, init # noqa
|
|
5
2
|
from fiddler3.entities import * # noqa
|
|
6
3
|
from fiddler3.exceptions import * # noqa
|
|
7
4
|
from fiddler3.schemas import * # noqa
|
|
8
|
-
|
|
9
|
-
# Global connection object
|
|
10
|
-
connection: Connection | None = None
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def init(
|
|
14
|
-
url: str,
|
|
15
|
-
token: str,
|
|
16
|
-
proxies: dict | None = None,
|
|
17
|
-
timeout: int = DEFAULT_CONNECTION_TIMEOUT,
|
|
18
|
-
verify: bool = True,
|
|
19
|
-
validate: bool = True,
|
|
20
|
-
) -> None:
|
|
21
|
-
"""
|
|
22
|
-
Initiate Python Client Connection
|
|
23
|
-
:param url: URL of Fiddler Platform
|
|
24
|
-
:param token: Authorization token
|
|
25
|
-
:param proxies: Dictionary mapping protocol to the URL of the proxy
|
|
26
|
-
:param timeout: Seconds to wait for the server to send data before giving up
|
|
27
|
-
:param verify: Controls whether we verify the server’s TLS certificate
|
|
28
|
-
:param validate: Whether to validate the server/client version compatibility.
|
|
29
|
-
Some functionalities might not work if this is turned off.
|
|
30
|
-
"""
|
|
31
|
-
global connection
|
|
32
|
-
connection = Connection(
|
|
33
|
-
url=url,
|
|
34
|
-
token=token,
|
|
35
|
-
proxies=proxies,
|
|
36
|
-
timeout=timeout,
|
|
37
|
-
verify=verify,
|
|
38
|
-
validate=validate,
|
|
39
|
-
)
|
fiddler3/connection.py
CHANGED
|
@@ -15,9 +15,12 @@ from fiddler3.version import __version__
|
|
|
15
15
|
|
|
16
16
|
logger = get_logger(__name__)
|
|
17
17
|
|
|
18
|
+
# Global connection object
|
|
19
|
+
connection: Connection | None = None
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
class Connection:
|
|
20
|
-
def __init__(
|
|
23
|
+
def __init__( # pylint: disable=too-many-arguments
|
|
21
24
|
self,
|
|
22
25
|
url: str,
|
|
23
26
|
token: str,
|
|
@@ -28,6 +31,7 @@ class Connection:
|
|
|
28
31
|
) -> None:
|
|
29
32
|
"""
|
|
30
33
|
Initiate connection to Fiddler Platform
|
|
34
|
+
|
|
31
35
|
:param url: URL of Fiddler Platform
|
|
32
36
|
:param token: Authorization token
|
|
33
37
|
:param proxies: Dictionary mapping protocol to the URL of the proxy
|
|
@@ -115,3 +119,66 @@ class Connection:
|
|
|
115
119
|
return
|
|
116
120
|
|
|
117
121
|
raise IncompatibleClient(server_version=str(self.server_version))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ConnectionMixin:
|
|
125
|
+
@classmethod
|
|
126
|
+
def _connection(cls) -> Connection:
|
|
127
|
+
"""Fiddler connection instance"""
|
|
128
|
+
assert connection is not None
|
|
129
|
+
return connection
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def _client(cls) -> RequestClient:
|
|
133
|
+
"""Request client instance"""
|
|
134
|
+
return cls._connection().client
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def organization_name(self) -> str:
|
|
138
|
+
"""Organization name property"""
|
|
139
|
+
return self._connection().server_info.organization.name
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def organization_id(self) -> UUID:
|
|
143
|
+
"""Organization id property"""
|
|
144
|
+
return self._connection().server_info.organization.id
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def get_organization_name(cls) -> str:
|
|
148
|
+
"""Organization name"""
|
|
149
|
+
return cls._connection().server_info.organization.name
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def get_organization_id(cls) -> UUID:
|
|
153
|
+
"""Organization id"""
|
|
154
|
+
return cls._connection().server_info.organization.id
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def init( # pylint: disable=too-many-arguments
|
|
158
|
+
url: str,
|
|
159
|
+
token: str,
|
|
160
|
+
proxies: dict | None = None,
|
|
161
|
+
timeout: int = DEFAULT_CONNECTION_TIMEOUT,
|
|
162
|
+
verify: bool = True,
|
|
163
|
+
validate: bool = True,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Initiate Python Client Connection
|
|
167
|
+
|
|
168
|
+
:param url: URL of Fiddler Platform
|
|
169
|
+
:param token: Authorization token
|
|
170
|
+
:param proxies: Dictionary mapping protocol to the URL of the proxy
|
|
171
|
+
:param timeout: Seconds to wait for the server to send data before giving up
|
|
172
|
+
:param verify: Controls whether we verify the server’s TLS certificate
|
|
173
|
+
:param validate: Whether to validate the server/client version compatibility.
|
|
174
|
+
Some functionalities might not work if this is turned off.
|
|
175
|
+
"""
|
|
176
|
+
global connection # pylint: disable=global-statement
|
|
177
|
+
connection = Connection(
|
|
178
|
+
url=url,
|
|
179
|
+
token=token,
|
|
180
|
+
proxies=proxies,
|
|
181
|
+
timeout=timeout,
|
|
182
|
+
verify=verify,
|
|
183
|
+
validate=validate,
|
|
184
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@enum.unique
|
|
5
|
+
class BinSize(str, enum.Enum):
|
|
6
|
+
HOUR = 'Hour'
|
|
7
|
+
DAY = 'Day'
|
|
8
|
+
WEEK = 'Week'
|
|
9
|
+
MONTH = 'Month'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@enum.unique
|
|
13
|
+
class CompareTo(str, enum.Enum):
|
|
14
|
+
"""Comparison with Absolute(raw_value) or Relative(time_period)"""
|
|
15
|
+
|
|
16
|
+
TIME_PERIOD = 'time_period'
|
|
17
|
+
RAW_VALUE = 'raw_value'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@enum.unique
|
|
21
|
+
class AlertCondition(str, enum.Enum):
|
|
22
|
+
GREATER = 'greater'
|
|
23
|
+
LESSER = 'lesser'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@enum.unique
|
|
27
|
+
class Priority(str, enum.Enum):
|
|
28
|
+
HIGH = 'HIGH'
|
|
29
|
+
MEDIUM = 'MEDIUM'
|
|
30
|
+
LOW = 'LOW'
|
fiddler3/entities/__init__.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Initialize all new entities."""
|
|
2
|
+
from fiddler3.entities.alert_record import AlertRecord # noqa
|
|
3
|
+
from fiddler3.entities.alert_rule import AlertRule # noqa
|
|
2
4
|
from fiddler3.entities.baseline import Baseline # noqa
|
|
3
5
|
from fiddler3.entities.dataset import Dataset # noqa
|
|
6
|
+
from fiddler3.entities.files import File # noqa
|
|
4
7
|
from fiddler3.entities.job import Job # noqa
|
|
5
8
|
from fiddler3.entities.model import Model # noqa
|
|
6
9
|
from fiddler3.entities.model_deployment import ModelDeployment # noqa
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Any, Iterator
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from fiddler3.decorators import handle_api_error
|
|
8
|
+
from fiddler3.entities.base import BaseEntity
|
|
9
|
+
from fiddler3.schemas.alert_record import AlertRecordResp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AlertRecord(BaseEntity):
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
"""Construct a alert record instance"""
|
|
15
|
+
|
|
16
|
+
self.alert_run_start_time: int | None = None
|
|
17
|
+
self.alert_time_bucket: int | None = None
|
|
18
|
+
self.alert_value: float | None = None
|
|
19
|
+
self.baseline_time_bucket: int | None = None
|
|
20
|
+
self.baseline_value: float | None = None
|
|
21
|
+
self.is_alert: bool | None = None
|
|
22
|
+
self.severity: str | None = None
|
|
23
|
+
self.failure_reason: str | None = None
|
|
24
|
+
self.message: str | None = None
|
|
25
|
+
self.feature_name: str | None = None
|
|
26
|
+
self.alert_record_main_version: int | None = None
|
|
27
|
+
self.alert_record_sub_version: int | None = None
|
|
28
|
+
|
|
29
|
+
self.id: UUID | None = None
|
|
30
|
+
self.alert_rule_id: UUID | None = None
|
|
31
|
+
self.created_at: datetime | None = None
|
|
32
|
+
self.updated_at: datetime | None = None
|
|
33
|
+
|
|
34
|
+
# Deserialized response object
|
|
35
|
+
self._resp: AlertRecordResp | None = None
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _from_dict(cls, data: dict) -> AlertRecord:
|
|
39
|
+
"""Build alert record object from the given dictionary"""
|
|
40
|
+
|
|
41
|
+
# Deserialize the response
|
|
42
|
+
resp_obj = AlertRecordResp(**data)
|
|
43
|
+
|
|
44
|
+
# Initialize
|
|
45
|
+
instance = cls()
|
|
46
|
+
|
|
47
|
+
# Add remaining fields
|
|
48
|
+
fields = [
|
|
49
|
+
'id',
|
|
50
|
+
'alert_rule_id',
|
|
51
|
+
'alert_run_start_time',
|
|
52
|
+
'alert_time_bucket',
|
|
53
|
+
'alert_value',
|
|
54
|
+
'baseline_time_bucket',
|
|
55
|
+
'baseline_value',
|
|
56
|
+
'is_alert',
|
|
57
|
+
'severity',
|
|
58
|
+
'failure_reason',
|
|
59
|
+
'message',
|
|
60
|
+
'feature_name',
|
|
61
|
+
'alert_record_main_version',
|
|
62
|
+
'alert_record_sub_version',
|
|
63
|
+
'created_at',
|
|
64
|
+
'updated_at',
|
|
65
|
+
]
|
|
66
|
+
for field in fields:
|
|
67
|
+
setattr(instance, field, getattr(resp_obj, field, None))
|
|
68
|
+
|
|
69
|
+
instance._resp = resp_obj
|
|
70
|
+
|
|
71
|
+
return instance
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _get_url(alert_rule_id: UUID | str, id_: UUID | str | None = None) -> str:
|
|
75
|
+
"""Get alert record resource/item url"""
|
|
76
|
+
url = f'/v2/alert-configs/{alert_rule_id}/records'
|
|
77
|
+
|
|
78
|
+
return url if not id_ else f'{url}/{id_}'
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
@handle_api_error
|
|
82
|
+
def list(
|
|
83
|
+
cls,
|
|
84
|
+
alert_rule_id: UUID | str,
|
|
85
|
+
start_time: datetime | None = None,
|
|
86
|
+
end_time: datetime | None = None,
|
|
87
|
+
ordering: list[str] | None = None,
|
|
88
|
+
) -> Iterator[AlertRecord]:
|
|
89
|
+
"""
|
|
90
|
+
List alert records triggered for an alert rule
|
|
91
|
+
|
|
92
|
+
:param alert_rule_id: unique identifier for alert rule
|
|
93
|
+
:param start_time: Start time to filter trigger alerts :default: 7 days ago
|
|
94
|
+
:param end_time: End time to filter trigger alerts :default: time now
|
|
95
|
+
|
|
96
|
+
:return: paginated list of alert records for the given alert rule
|
|
97
|
+
"""
|
|
98
|
+
params: dict[str, Any] = {
|
|
99
|
+
'start_time': start_time or datetime.now() - timedelta(days=7),
|
|
100
|
+
'end_time': end_time or datetime.now(),
|
|
101
|
+
}
|
|
102
|
+
if ordering:
|
|
103
|
+
params['ordering'] = ordering
|
|
104
|
+
|
|
105
|
+
for record in cls._paginate(url=cls._get_url(alert_rule_id), params=params):
|
|
106
|
+
yield cls._from_dict(data=record)
|