fiddler-client 2.5.0.dev1__py3-none-any.whl → 2.5.0.dev2__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.
Files changed (39) hide show
  1. fiddler/_version.py +1 -1
  2. fiddler/core_objects.py +18 -5
  3. fiddler/schema/alert.py +3 -3
  4. fiddler/schemas/custom_features.py +31 -1
  5. fiddler3/__init__.py +121 -3
  6. fiddler3/configs.py +3 -0
  7. fiddler3/constants/model.py +9 -0
  8. fiddler3/entities/__init__.py +0 -10
  9. fiddler3/entities/alert_rule.py +79 -2
  10. fiddler3/entities/baseline.py +21 -15
  11. fiddler3/entities/dataset.py +1 -1
  12. fiddler3/entities/events.py +68 -52
  13. fiddler3/entities/{files.py → file.py} +7 -15
  14. fiddler3/entities/model.py +81 -13
  15. fiddler3/entities/model_artifact.py +5 -5
  16. fiddler3/entities/model_deployment.py +12 -1
  17. fiddler3/entities/project.py +1 -1
  18. fiddler3/entities/{model_surrogate.py → surrogate.py} +27 -29
  19. fiddler3/entities/xai.py +8 -9
  20. fiddler3/libs/http_client.py +1 -1
  21. fiddler3/schemas/__init__.py +0 -9
  22. fiddler3/schemas/alert_rule.py +7 -0
  23. fiddler3/schemas/custom_features.py +34 -12
  24. fiddler3/schemas/model_deployment.py +5 -4
  25. fiddler3/schemas/webhook.py +0 -3
  26. fiddler3/tests/apis/test_alert_rule.py +71 -1
  27. fiddler3/tests/apis/test_events.py +41 -0
  28. fiddler3/tests/apis/test_files.py +1 -1
  29. fiddler3/tests/apis/test_model.py +5 -1
  30. fiddler3/tests/apis/test_project.py +1 -1
  31. fiddler3/tests/apis/test_webhook.py +0 -23
  32. fiddler3/utils/helpers.py +20 -0
  33. {fiddler_client-2.5.0.dev1.dist-info → fiddler_client-2.5.0.dev2.dist-info}/METADATA +15 -5
  34. {fiddler_client-2.5.0.dev1.dist-info → fiddler_client-2.5.0.dev2.dist-info}/RECORD +38 -39
  35. fiddler3/entities/helpers.py +0 -21
  36. /fiddler3/schemas/{files.py → file.py} +0 -0
  37. {fiddler_client-2.5.0.dev1.dist-info → fiddler_client-2.5.0.dev2.dist-info}/LICENSE.txt +0 -0
  38. {fiddler_client-2.5.0.dev1.dist-info → fiddler_client-2.5.0.dev2.dist-info}/WHEEL +0 -0
  39. {fiddler_client-2.5.0.dev1.dist-info → fiddler_client-2.5.0.dev2.dist-info}/top_level.txt +0 -0
fiddler/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '2.5.0.dev1'
1
+ __version__ = '2.5.0.dev2'
fiddler/core_objects.py CHANGED
@@ -1295,9 +1295,7 @@ class ModelInfo:
1295
1295
  elif isinstance(feature, VectorFeature):
1296
1296
  self.validate_vector_cf(feature, valid_vector_cols)
1297
1297
  elif isinstance(feature, Enrichment):
1298
- self.validate_enrichment_cf(
1299
- feature, available_cols, valid_cf_names
1300
- )
1298
+ self.validate_enrichment_cf(feature, available_cols, valid_cf_names)
1301
1299
 
1302
1300
  def validate_enrichment_cf(
1303
1301
  self,
@@ -1326,6 +1324,17 @@ class ModelInfo:
1326
1324
  valid_vector_cols: Iterable[str],
1327
1325
  valid_enrichment_names: Iterable[str],
1328
1326
  ) -> None:
1327
+ if isinstance(feature, TextEmbedding):
1328
+ if not feature.n_tags:
1329
+ raise ValueError(
1330
+ f'{feature.name} ({feature.__class__.__name__}) must specify a number of tags.'
1331
+ )
1332
+
1333
+ if feature.n_tags < 0:
1334
+ raise ValueError(
1335
+ f'{feature.name} ({feature.__class__.__name__}) must specify a non-negative number of tags.'
1336
+ )
1337
+
1329
1338
  if feature.column not in valid_vector_cols:
1330
1339
  # we require either a column or source_column
1331
1340
  if not feature.source_column:
@@ -1342,13 +1351,17 @@ class ModelInfo:
1342
1351
  f"Column/Enrichment'{feature.column}' defined in {feature.name} ({feature.__class__.__name__}) does not exist or not of type vector."
1343
1352
  )
1344
1353
 
1345
- def validate_vector_cf(self, feature: VectorFeature, valid_vector_cols: Iterable[str]) -> None:
1354
+ def validate_vector_cf(
1355
+ self, feature: VectorFeature, valid_vector_cols: Iterable[str]
1356
+ ) -> None:
1346
1357
  if feature.column not in valid_vector_cols:
1347
1358
  raise ValueError(
1348
1359
  f"Column '{feature.column}' defined in {feature.name} ({feature.__class__.__name__}) does not exist or not of type vector."
1349
1360
  )
1350
1361
 
1351
- def validate_multivariate_cf(self, feature: Multivariate, valid_numeric_names: Iterable[str]) -> None:
1362
+ def validate_multivariate_cf(
1363
+ self, feature: Multivariate, valid_numeric_names: Iterable[str]
1364
+ ) -> None:
1352
1365
  if len(feature.columns or []) < 2:
1353
1366
  raise ValueError(
1354
1367
  f'{feature.name} ({feature.__class__.__name__}) must specify at least two columns.'
fiddler/schema/alert.py CHANGED
@@ -197,7 +197,7 @@ class AlertRule(BaseDataSchema):
197
197
  time_bucket: Optional[int]
198
198
  alert_type_display_name: str
199
199
  metric_display_name: str
200
- enable_notification: bool
200
+ enable_notification: Optional[bool]
201
201
 
202
202
  @root_validator(pre=True)
203
203
  def set_feature_names(cls, values: dict) -> dict:
@@ -229,6 +229,6 @@ class TriggeredAlerts(BaseDataSchema):
229
229
  message: str
230
230
  multi_col_values: Optional[Dict[str, float]]
231
231
  feature_name: Optional[str]
232
- alert_record_main_version: int
233
- alert_record_sub_version: int
232
+ alert_record_main_version: Optional[int]
233
+ alert_record_sub_version: Optional[int]
234
234
  segment: Optional[Segment]
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
  from typing import Any, Dict, List, Optional, Type, TypeVar
3
3
 
4
- from pydantic import BaseModel, Extra
4
+ from pydantic import BaseModel, Extra, validator
5
5
 
6
6
  CustomFeatureTypeVar = TypeVar('CustomFeatureTypeVar', bound='CustomFeature')
7
7
  DEFAULT_NUM_CLUSTERS = 5
@@ -22,6 +22,12 @@ class CustomFeature(BaseModel):
22
22
  n_clusters: Optional[int] = DEFAULT_NUM_CLUSTERS
23
23
  centroids: Optional[List] = None
24
24
 
25
+ @validator('n_clusters')
26
+ def validate_n_clusters(cls, value: int) -> int: # noqa: N805
27
+ if value < 0:
28
+ raise ValueError('n_clusters must be greater than 0')
29
+ return value
30
+
25
31
  class Config:
26
32
  allow_mutation = False
27
33
  extra = Extra.forbid
@@ -81,6 +87,12 @@ class Multivariate(CustomFeature):
81
87
  columns: List[str]
82
88
  monitor_components: bool = False
83
89
 
90
+ @validator('columns')
91
+ def validate_columns(cls, value: List[str]) -> List[str]: # noqa: N805
92
+ if len(value) < 2:
93
+ raise ValueError('Multivariate features must have at least 2 columns')
94
+ return value
95
+
84
96
 
85
97
  class VectorFeature(CustomFeature):
86
98
  type: CustomFeatureType = CustomFeatureType.FROM_VECTOR
@@ -92,10 +104,28 @@ class TextEmbedding(VectorFeature):
92
104
  type: CustomFeatureType = CustomFeatureType.FROM_TEXT_EMBEDDING
93
105
  n_tags: Optional[int] = DEFAULT_NUM_TAGS
94
106
 
107
+ @validator('source_column')
108
+ def validate_source_column(cls, value: str) -> str: # noqa: N805
109
+ if value is None:
110
+ raise ValueError('source_column must be specified')
111
+ return value
112
+
113
+ @validator('n_tags')
114
+ def validate_n_tags(cls, value: int) -> int: # noqa: N805
115
+ if value < 1:
116
+ raise ValueError('n_tags must be greater than 0')
117
+ return value
118
+
95
119
 
96
120
  class ImageEmbedding(VectorFeature):
97
121
  type: CustomFeatureType = CustomFeatureType.FROM_IMAGE_EMBEDDING
98
122
 
123
+ @validator('source_column')
124
+ def validate_source_column(cls, value: str) -> str: # noqa: N805
125
+ if value is None:
126
+ raise ValueError('source_column must be specified')
127
+ return value
128
+
99
129
 
100
130
  class Enrichment(CustomFeature):
101
131
  type: CustomFeatureType = CustomFeatureType.ENRICHMENT
fiddler3/__init__.py CHANGED
@@ -1,4 +1,122 @@
1
1
  from fiddler3.connection import Connection, ConnectionMixin, init # noqa
2
- from fiddler3.entities import * # noqa
3
- from fiddler3.exceptions import * # noqa
4
- from fiddler3.schemas import * # noqa
2
+ from fiddler3.constants.alert_rule import ( # noqa
3
+ AlertCondition,
4
+ BinSize,
5
+ CompareTo,
6
+ Priority,
7
+ )
8
+ from fiddler3.constants.baseline import BaselineType, WindowSize # noqa
9
+ from fiddler3.constants.dataset import EnvType # noqa
10
+ from fiddler3.constants.job import JobStatus # noqa
11
+ from fiddler3.constants.model import ( # noqa
12
+ ArtifactStatus,
13
+ CustomFeatureType,
14
+ DataType,
15
+ ModelInputType,
16
+ ModelTask,
17
+ )
18
+ from fiddler3.constants.model_deployment import ArtifactType, DeploymentType # noqa
19
+ from fiddler3.constants.xai import ExplainMethod # noqa
20
+ from fiddler3.entities.alert_record import AlertRecord # noqa
21
+ from fiddler3.entities.alert_rule import AlertRule # noqa
22
+ from fiddler3.entities.baseline import Baseline # noqa
23
+ from fiddler3.entities.dataset import Dataset # noqa
24
+ from fiddler3.entities.file import File # noqa
25
+ from fiddler3.entities.job import Job # noqa
26
+ from fiddler3.entities.model import Model # noqa
27
+ from fiddler3.entities.model_deployment import ModelDeployment # noqa
28
+ from fiddler3.entities.project import Project # noqa
29
+ from fiddler3.entities.webhook import Webhook # noqa
30
+ from fiddler3.exceptions import ( # noqa
31
+ ApiError,
32
+ AsyncJobFailed,
33
+ Conflict,
34
+ ConnectionError,
35
+ ConnectionTimeout,
36
+ HttpError,
37
+ IncompatibleClient,
38
+ NotFound,
39
+ Unsupported,
40
+ )
41
+ from fiddler3.schemas.custom_features import ( # noqa
42
+ CustomFeature,
43
+ Enrichment,
44
+ ImageEmbedding,
45
+ Multivariate,
46
+ TextEmbedding,
47
+ VectorFeature,
48
+ )
49
+ from fiddler3.schemas.dataset import EnvType # noqa
50
+ from fiddler3.schemas.deployment_params import DeploymentParams # noqa
51
+ from fiddler3.schemas.model_schema import ModelSchema # noqa
52
+ from fiddler3.schemas.model_spec import ModelSpec # noqa
53
+ from fiddler3.schemas.model_task_params import ModelTaskParams # noqa
54
+ from fiddler3.schemas.xai import ( # noqa
55
+ DatasetDataSource,
56
+ EventIdDataSource,
57
+ RowDataSource,
58
+ SqlSliceQueryDataSource,
59
+ )
60
+ from fiddler3.schemas.xai_params import XaiParams # noqa
61
+ from fiddler3.utils.logger import set_logging # noqa
62
+
63
+ __all__ = [
64
+ 'Connection',
65
+ 'ConnectionMixin',
66
+ 'init',
67
+ # Constants
68
+ 'AlertCondition',
69
+ 'ArtifactStatus',
70
+ 'ArtifactType',
71
+ 'BaselineType',
72
+ 'BinSize',
73
+ 'CompareTo',
74
+ 'DataType',
75
+ 'DeploymentType',
76
+ 'EnvType',
77
+ 'ExplainMethod',
78
+ 'JobStatus',
79
+ 'ModelInputType',
80
+ 'ModelTask',
81
+ 'Priority',
82
+ 'WindowSize',
83
+ # Schemas
84
+ 'CustomFeature',
85
+ 'DatasetDataSource',
86
+ 'DeploymentParams',
87
+ 'Enrichment',
88
+ 'EventIdDataSource',
89
+ 'ImageEmbedding',
90
+ 'ModelSchema',
91
+ 'ModelSpec',
92
+ 'ModelTaskParams',
93
+ 'Multivariate',
94
+ 'RowDataSource',
95
+ 'SqlSliceQueryDataSource',
96
+ 'TextEmbedding',
97
+ 'VectorFeature',
98
+ 'XaiParams',
99
+ # Entities
100
+ 'AlertRecord',
101
+ 'AlertRule',
102
+ 'Baseline',
103
+ 'Dataset',
104
+ 'File',
105
+ 'Job',
106
+ 'Model',
107
+ 'ModelDeployment',
108
+ 'Project',
109
+ 'Webhook',
110
+ # Exceptions
111
+ 'NotFound',
112
+ 'Conflict',
113
+ 'IncompatibleClient',
114
+ 'AsyncJobFailed',
115
+ 'Unsupported',
116
+ 'HttpError',
117
+ 'ConnectionTimeout',
118
+ 'ConnectionError',
119
+ 'ApiError',
120
+ # Utilities
121
+ 'set_logging',
122
+ ]
fiddler3/configs.py CHANGED
@@ -3,3 +3,6 @@ MIN_SERVER_VERSION = '23.7.0' # @TODO: Change this before 3.0 release
3
3
  DEFAULT_PAGE_SIZE = 50
4
4
  JOB_POLL_INTERVAL = 3
5
5
  JOB_WAIT_TIMEOUT = 1800
6
+ STREAM_EVENT_LIMIT = 1000
7
+ DEFAULT_NUM_CLUSTERS = 5
8
+ DEFAULT_NUM_TAGS = 5
@@ -62,3 +62,12 @@ class ArtifactStatus(str, enum.Enum):
62
62
  NO_MODEL = 'no_model'
63
63
  SURROGATE = 'surrogate'
64
64
  USER_UPLOADED = 'user_uploaded'
65
+
66
+
67
+ @enum.unique
68
+ class CustomFeatureType(str, enum.Enum):
69
+ FROM_COLUMNS = 'FROM_COLUMNS'
70
+ FROM_VECTOR = 'FROM_VECTOR'
71
+ FROM_TEXT_EMBEDDING = 'FROM_TEXT_EMBEDDING'
72
+ FROM_IMAGE_EMBEDDING = 'FROM_IMAGE_EMBEDDING'
73
+ ENRICHMENT = 'ENRICHMENT'
@@ -1,10 +0,0 @@
1
- """Initialize all new entities."""
2
- from fiddler3.entities.alert_record import AlertRecord # noqa
3
- from fiddler3.entities.alert_rule import AlertRule # noqa
4
- from fiddler3.entities.baseline import Baseline # noqa
5
- from fiddler3.entities.dataset import Dataset # noqa
6
- from fiddler3.entities.files import File # noqa
7
- from fiddler3.entities.job import Job # noqa
8
- from fiddler3.entities.model import Model # noqa
9
- from fiddler3.entities.model_deployment import ModelDeployment # noqa
10
- from fiddler3.entities.project import Project # noqa
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import builtins
3
4
  from datetime import datetime
4
5
  from typing import Any, Iterator
5
6
  from uuid import UUID
@@ -10,7 +11,7 @@ from fiddler3.entities.base import BaseEntity
10
11
  from fiddler3.entities.baseline import Baseline, BaselineCompactMixin
11
12
  from fiddler3.entities.model import Model, ModelCompactMixin
12
13
  from fiddler3.entities.project import ProjectCompactMixin
13
- from fiddler3.schemas.alert_rule import AlertRuleResp
14
+ from fiddler3.schemas.alert_rule import AlertRuleResp, NotificationConfig
14
15
  from fiddler3.schemas.filter_query import OperatorType, QueryCondition, QueryRule
15
16
  from fiddler3.utils.logger import get_logger
16
17
 
@@ -237,10 +238,86 @@ class AlertRule(
237
238
 
238
239
  @handle_api_error
239
240
  def disable_notifications(self) -> None:
240
- """Diable notifications for an alert rule"""
241
+ """Disable notifications for an alert rule"""
241
242
  self._client().patch(
242
243
  url=self._get_url(id_=self.id), data={'enable_notification': False}
243
244
  )
244
245
  logger.info(
245
246
  'Notifications have been disabled for alert rule with id: %s', self.id
246
247
  )
248
+
249
+ @handle_api_error
250
+ def set_notification_config(
251
+ self,
252
+ emails: builtins.list[str] | None = None,
253
+ pagerduty_services: builtins.list[str] | None = None,
254
+ pagerduty_severity: str | None = None,
255
+ webhooks: builtins.list[UUID] | None = None,
256
+ ) -> NotificationConfig:
257
+ """
258
+ Set notifications for an alert rule
259
+
260
+ :param emails: list of emails
261
+ :param pagerduty_services: list of pagerduty services
262
+ :param pagerduty_severity: severity of pagerduty
263
+ :param webhooks: list of webhooks UUIDs
264
+
265
+ :return: AlertNotifications object
266
+ """
267
+ notifications = {
268
+ 'emails': {
269
+ 'email': ','.join(emails) if emails else '',
270
+ },
271
+ 'pagerduty': {
272
+ 'service': ','.join(pagerduty_services) if pagerduty_services else '',
273
+ 'severity': pagerduty_severity,
274
+ },
275
+ 'webhooks': [{'uuid': webhook_uuid} for webhook_uuid in webhooks]
276
+ if webhooks
277
+ else None,
278
+ }
279
+ response = self._client().patch(
280
+ url=self._get_url(id_=self.id),
281
+ data={'notifications': notifications},
282
+ )
283
+
284
+ return self._get_notifications_from_dict(
285
+ response.json()['data']['notifications']
286
+ )
287
+
288
+ @handle_api_error
289
+ def get_notification_config(self) -> NotificationConfig:
290
+ """
291
+ Get notifications setting for an alert rule
292
+
293
+ :return: AlertNotifications object
294
+ """
295
+
296
+ response = self._client().get(url=self._get_url(id_=self.id))
297
+
298
+ return self._get_notifications_from_dict(
299
+ response.json()['data']['notifications']
300
+ )
301
+
302
+ @staticmethod
303
+ def _get_notifications_from_dict(notif_dict: dict) -> NotificationConfig:
304
+ emails = notif_dict.get('emails', {}).get('email')
305
+ notifications: dict = {}
306
+ if emails:
307
+ notifications['emails'] = [email.strip() for email in emails.split(',')]
308
+
309
+ pd_services = notif_dict.get('pagerduty', {}).get('service')
310
+ if pd_services:
311
+ notifications['pagerduty_services'] = [
312
+ pd_service.strip() for pd_service in pd_services.split(',')
313
+ ]
314
+ notifications['pagerduty_severity'] = notif_dict.get('pagerduty', {}).get(
315
+ 'severity'
316
+ )
317
+
318
+ if notif_dict.get('webhooks'):
319
+ notifications['webhooks'] = [
320
+ webhook['uuid'] for webhook in notif_dict.get('webhooks', [])
321
+ ]
322
+
323
+ return NotificationConfig(**notifications)
@@ -9,11 +9,11 @@ from fiddler3.constants.dataset import EnvType
9
9
  from fiddler3.decorators import handle_api_error
10
10
  from fiddler3.entities.base import BaseEntity
11
11
  from fiddler3.entities.dataset import DatasetCompactMixin
12
- from fiddler3.entities.helpers import raise_not_found
13
12
  from fiddler3.entities.model import ModelCompactMixin
14
13
  from fiddler3.entities.project import ProjectCompactMixin
15
14
  from fiddler3.schemas.baseline import BaselineResp
16
15
  from fiddler3.schemas.filter_query import OperatorType, QueryCondition, QueryRule
16
+ from fiddler3.utils.helpers import raise_not_found
17
17
 
18
18
 
19
19
  class Baseline(
@@ -138,9 +138,9 @@ class Baseline(
138
138
  """
139
139
  Get the baseline instance using baseline name, model name and project name.
140
140
 
141
- :param name: baseline name
142
- :param model_name: name of the model associated with the baseline
143
- :param project_name: name of the project associated with the baseline
141
+ :param name: Baseline name
142
+ :param model_name: Name of the model associated with the baseline
143
+ :param project_name: Name of the project associated with the baseline
144
144
  :return: Baseline instance for the provided params
145
145
  """
146
146
 
@@ -179,19 +179,25 @@ class Baseline(
179
179
  @handle_api_error
180
180
  def create(self) -> Baseline:
181
181
  """Create a new baseline."""
182
+ payload = {
183
+ 'name': self.name,
184
+ 'model_id': self.model_id,
185
+ 'type': self.type,
186
+ 'env_type': self.environment,
187
+ 'env_id': self.dataset_id,
188
+ }
189
+ if self.start_time:
190
+ payload['start_time'] = self.start_time
191
+ if self.end_time:
192
+ payload['end_time'] = self.end_time
193
+ if self.offset:
194
+ payload['offset'] = self.offset
195
+ if self.window_size:
196
+ payload['window_size'] = self.window_size
197
+
182
198
  response = self._client().post(
183
199
  url=self._get_url(),
184
- data={
185
- 'name': self.name,
186
- 'model_id': self.model_id,
187
- 'type': self.type,
188
- 'env_type': self.environment,
189
- 'start_time': self.start_time,
190
- 'end_time': self.end_time,
191
- 'offset': self.offset,
192
- 'window_size': self.window_size,
193
- 'env_id': self.dataset_id,
194
- },
200
+ data=payload,
195
201
  )
196
202
  self._refresh_from_response(response=response)
197
203
  return self
@@ -7,10 +7,10 @@ from uuid import UUID
7
7
  from fiddler3.constants.dataset import EnvType
8
8
  from fiddler3.decorators import handle_api_error
9
9
  from fiddler3.entities.base import BaseEntity
10
- from fiddler3.entities.helpers import raise_not_found
11
10
  from fiddler3.entities.project import ProjectCompactMixin
12
11
  from fiddler3.schemas.dataset import DatasetResp
13
12
  from fiddler3.schemas.filter_query import OperatorType, QueryCondition, QueryRule
13
+ from fiddler3.utils.helpers import raise_not_found
14
14
 
15
15
 
16
16
  class Dataset(BaseEntity, ProjectCompactMixin):
@@ -1,70 +1,94 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any, Callable
4
+ from typing import Any, Iterator
5
5
  from uuid import UUID
6
6
 
7
7
  import pandas as pd
8
8
  from requests import Response
9
9
 
10
+ from fiddler3.configs import STREAM_EVENT_LIMIT
11
+ from fiddler3.connection import ConnectionMixin
10
12
  from fiddler3.decorators import handle_api_error
11
- from fiddler3.entities.files import File
13
+ from fiddler3.entities.file import File
12
14
  from fiddler3.entities.job import Job
13
15
  from fiddler3.schemas.dataset import EnvType
14
16
  from fiddler3.schemas.events import EventsSource, FileSource
15
17
  from fiddler3.schemas.job import JobCompactResp
16
18
 
17
- STREAM_EVENTS_MAX = 1000
18
19
 
20
+ class EventPublisher(ConnectionMixin):
21
+ STREAM_LIMIT = STREAM_EVENT_LIMIT
19
22
 
20
- class EventMixin:
21
- id: UUID | None
22
- _client: Callable
23
+ def __init__(self, model_id: UUID) -> None:
24
+ """
25
+ Event publishing methods
26
+
27
+ :param model_id: Model identifier
28
+ """
29
+ self.model_id = model_id
30
+
31
+ def _get_stream_chunks(
32
+ self, source: pd.DataFrame | list[dict[str, Any]]
33
+ ) -> Iterator[list[dict[str, Any]]]:
34
+ """Chunk the source based on stream limit"""
35
+
36
+ for i in range(0, len(source), self.STREAM_LIMIT):
37
+ chunk = source[i : i + self.STREAM_LIMIT]
38
+
39
+ if isinstance(chunk, pd.DataFrame):
40
+ events = chunk.to_dict('records')
41
+ else:
42
+ events = chunk
43
+
44
+ yield events
23
45
 
24
46
  @handle_api_error
25
47
  def publish(
26
48
  self,
27
- source: list[dict[Any, Any]] | str | Path | pd.DataFrame,
28
- environment: EnvType,
49
+ source: list[dict[str, Any]] | str | Path | pd.DataFrame,
50
+ environment: EnvType = EnvType.PRODUCTION,
29
51
  dataset_name: str | None = None,
52
+ update: bool = False,
30
53
  ) -> list[UUID] | Job:
31
54
  """
32
55
  Publish Pre-production or Production data
33
56
 
34
- :param source: source can be:
57
+ :param source: one of:
35
58
  Path or str path: path for data file.
36
- list[dict]: list of event dicts. max_len=1000. EnvType.PRE_PRODUCTION not supported
59
+ list[dict]: list of event dicts (max 1000) EnvType.PRE_PRODUCTION is not supported
37
60
  dataframe: events dataframe. EnvType.PRE_PRODUCTION not supported.
38
- :param environment: either EnvType.PRE_PRODUCTION or EnvType.PRODUCTION
39
- :param dataset_name: name of the dataset. Not supported for EnvType.PRODUCTION
61
+ :param environment: Either EnvType.PRE_PRODUCTION or EnvType.PRODUCTION
62
+ :param dataset_name: Name of the dataset. Not supported for EnvType.PRODUCTION
63
+ :param update: flag indicating if the events are updates to previously published rows
40
64
 
41
65
  :return: list[UUID] for list of dicts or dataframe source and Job object for file path source.
42
66
  """
43
- _sources = self._get_sources(source)
44
- if isinstance(_sources, FileSource):
67
+ if isinstance(source, (str, Path)):
45
68
  return self._publish_file(
46
- source=_sources,
69
+ source=source,
47
70
  environment=environment,
48
71
  dataset_name=dataset_name,
72
+ update=update,
49
73
  )
50
- return self._publish_stream(
51
- sources=_sources,
52
- environment=environment,
53
- dataset_name=dataset_name,
54
- )
74
+
75
+ if isinstance(source, (list, pd.DataFrame)):
76
+ return self._publish_stream(source=source, update=update)
77
+
78
+ raise ValueError(f'Unsupported source - {type(source)}')
55
79
 
56
80
  def _publish_stream(
57
81
  self,
58
- sources: list[EventsSource],
59
- environment: EnvType,
60
- dataset_name: str | None = None,
82
+ source: list[dict[str, Any]] | pd.DataFrame,
83
+ update: bool = False,
61
84
  ) -> list[UUID]:
62
85
  fiddler_ids = []
63
- for _source in sources:
86
+ for events in self._get_stream_chunks(source=source):
64
87
  response = self._publish_call(
65
- source=_source,
66
- environment=environment,
67
- dataset_name=dataset_name,
88
+ source=EventsSource(events=events),
89
+ environment=EnvType.PRODUCTION,
90
+ dataset_name=None,
91
+ update=update,
68
92
  )
69
93
  fiddler_ids.extend(response.json()['data']['fiddler_ids'])
70
94
 
@@ -72,48 +96,40 @@ class EventMixin:
72
96
 
73
97
  def _publish_file(
74
98
  self,
75
- source: FileSource,
99
+ source: Path | str,
76
100
  environment: EnvType,
77
101
  dataset_name: str | None = None,
102
+ update: bool = False,
78
103
  ) -> Job:
104
+ file = File(path=Path(source)).upload()
105
+
106
+ assert file.id is not None
107
+
79
108
  response = self._publish_call(
80
- source=source, environment=environment, dataset_name=dataset_name
109
+ source=FileSource(file_id=file.id),
110
+ environment=environment,
111
+ dataset_name=dataset_name,
112
+ update=update,
81
113
  )
82
114
  job_compact = JobCompactResp(**response.json()['data']['job'])
83
115
  return Job.get(id_=job_compact.id)
84
116
 
85
- def _get_sources(
86
- self, source: list[dict[str, Any]] | str | Path | pd.DataFrame
87
- ) -> list[EventsSource] | FileSource:
88
- _source = []
89
- if isinstance(source, (str, Path)):
90
- file = File(path=Path(source)).upload()
91
- assert file.id is not None
92
- return FileSource(file_id=file.id)
93
-
94
- if isinstance(source, list):
95
- _source.append(EventsSource(events=source))
96
-
97
- elif isinstance(source, pd.DataFrame):
98
- for event_dict in [
99
- source[i : i + STREAM_EVENTS_MAX]
100
- for i in range(0, source.shape[0], STREAM_EVENTS_MAX)
101
- ]:
102
- _source.append(EventsSource(events=event_dict.to_dict('records')))
103
-
104
- return _source
105
-
106
117
  def _publish_call(
107
118
  self,
108
119
  source: FileSource | EventsSource,
109
120
  environment: EnvType,
110
121
  dataset_name: str | None = None,
122
+ update: bool = False,
111
123
  ) -> Response:
112
- return self._client().post(
124
+ if update:
125
+ method = self._client().patch
126
+ else:
127
+ method = self._client().post
128
+ return method(
113
129
  url='/v3/events',
114
130
  data={
115
131
  'source': source.dict(),
116
- 'model_id': self.id,
132
+ 'model_id': self.model_id,
117
133
  'env_type': environment,
118
134
  'env_name': dataset_name,
119
135
  },