fiddler-client 3.0.0.dev3__py3-none-any.whl → 3.0.0.dev5__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/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.0.dev3
1
+ 3.0.0.dev5
fiddler/__init__.py CHANGED
@@ -20,6 +20,7 @@ from fiddler.constants.xai import ExplainMethod # noqa
20
20
  from fiddler.entities.alert_record import AlertRecord # noqa
21
21
  from fiddler.entities.alert_rule import AlertRule # noqa
22
22
  from fiddler.entities.baseline import Baseline # noqa
23
+ from fiddler.entities.custom_metric import CustomMetric, Segment # noqa
23
24
  from fiddler.entities.dataset import Dataset # noqa
24
25
  from fiddler.entities.file import File # noqa
25
26
  from fiddler.entities.job import Job # noqa
@@ -103,11 +104,13 @@ __all__ = [
103
104
  'AlertRule',
104
105
  'Baseline',
105
106
  'Dataset',
107
+ 'CustomMetric',
106
108
  'File',
107
109
  'Job',
108
110
  'Model',
109
111
  'ModelDeployment',
110
112
  'Project',
113
+ 'Segment',
111
114
  'Webhook',
112
115
  # Exceptions
113
116
  'NotFound',
@@ -11,5 +11,4 @@ CONTENT_TYPE_OCTET_STREAM = 'application/octet-stream'
11
11
  CONTENT_TYPE_OCTET_STREAM_HEADER = {CONTENT_TYPE_HEADER_KEY: CONTENT_TYPE_OCTET_STREAM}
12
12
 
13
13
  # Multi-part upload
14
- MULTI_PART_UPLOAD_SIZE_THRESHOLD = 5 * 1024 * 1024 # 5MB in bytes
15
14
  MULTI_PART_CHUNK_SIZE = 100 * 1024 * 1024 # 100MB in bytes
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from datetime import datetime
5
+ from typing import Any, Iterator
6
+ from uuid import UUID
7
+
8
+ from fiddler.decorators import handle_api_error
9
+ from fiddler.entities.base import BaseEntity
10
+ from fiddler.entities.model import Model
11
+ from fiddler.schemas.custom_metric import (
12
+ CustomExpressionResp,
13
+ CustomMetricResp,
14
+ SegmentResp,
15
+ )
16
+ from fiddler.schemas.filter_query import OperatorType, QueryCondition, QueryRule
17
+ from fiddler.utils.helpers import raise_not_found
18
+
19
+
20
+ class CustomExpression(BaseEntity):
21
+ def __init__(
22
+ self,
23
+ name: str,
24
+ model_id: UUID,
25
+ definition: str,
26
+ description: str | None = None,
27
+ ) -> None:
28
+ """Construct a custom expression instance."""
29
+ self.name = name
30
+ self.model_id = model_id
31
+ self.definition = definition
32
+ self.description = description
33
+
34
+ self.id: UUID | None = None
35
+ self.created_at: datetime | None = None
36
+
37
+ # Deserialized response object
38
+ self._resp: CustomExpressionResp | None = None
39
+
40
+ @staticmethod
41
+ @abstractmethod
42
+ def _get_url(id_: UUID | str | None = None) -> str:
43
+ """Get custom expression resource/item url."""
44
+
45
+ def _refresh(self, data: dict) -> None:
46
+ """Refresh the fields of this instance from the given response dictionary"""
47
+ # Deserialize the response
48
+ resp_obj = CustomMetricResp(**data)
49
+ assert self.model_id
50
+ fields = [
51
+ 'id',
52
+ 'name',
53
+ 'definition',
54
+ 'description',
55
+ 'created_at',
56
+ ]
57
+ for field in fields:
58
+ setattr(self, field, getattr(resp_obj, field, None))
59
+
60
+ self._resp = resp_obj
61
+
62
+ @classmethod
63
+ def _from_dict(cls, data: dict) -> CustomExpression:
64
+ """Build entity object from the given dictionary."""
65
+
66
+ # Deserialize the response
67
+ resp_obj = CustomMetricResp(**data)
68
+ model = Model.from_name(
69
+ name=resp_obj.model_name, project_name=resp_obj.project_name
70
+ )
71
+ assert model.id
72
+ # Initialize
73
+ instance = cls(
74
+ name=resp_obj.name,
75
+ model_id=model.id,
76
+ definition=resp_obj.definition,
77
+ description=resp_obj.description,
78
+ )
79
+
80
+ # Add remaining fields
81
+ fields = [
82
+ 'id',
83
+ 'created_at',
84
+ ]
85
+ for field in fields:
86
+ setattr(instance, field, getattr(resp_obj, field, None))
87
+
88
+ instance._resp = resp_obj
89
+
90
+ return instance
91
+
92
+ @classmethod
93
+ @handle_api_error
94
+ def get(cls, id_: UUID | str) -> CustomExpression:
95
+ """
96
+ Get the CustomMetric instance using custom metric id.
97
+
98
+ :param id_: unique uuid format identifier for custom metric
99
+ :return: CustomMetric instance
100
+ """
101
+ response = cls._client().get(url=cls._get_url(id_=id_))
102
+ return cls._from_response(response=response)
103
+
104
+ @classmethod
105
+ @handle_api_error
106
+ def from_name(
107
+ cls, name: str, model_name: str, project_name: str
108
+ ) -> CustomExpression:
109
+ """
110
+ Get the custom metric instance using custom metric name, model name and project name.
111
+
112
+ :param name: Custom metric name
113
+ :param model_name: Name of the model associated with the custom metric
114
+ :param project_name: Name of the project associated with the custom metric
115
+
116
+ :return: CustomMetric instance for the provided params
117
+ """
118
+
119
+ _filter = QueryCondition(
120
+ rules=[
121
+ QueryRule(field='name', operator=OperatorType.EQUAL, value=name),
122
+ ]
123
+ )
124
+ params: dict[str, Any] = {
125
+ 'filter': _filter.json(),
126
+ 'organization_name': cls.get_organization_name(),
127
+ 'project_name': project_name,
128
+ 'model_name': model_name,
129
+ }
130
+
131
+ response = cls._client().get(
132
+ url=cls._get_url(),
133
+ params=params,
134
+ )
135
+ if response.json()['data']['total'] == 0:
136
+ raise_not_found('Custom metric not found for the given identifier')
137
+
138
+ return cls._from_dict(data=response.json()['data']['items'][0])
139
+
140
+ @classmethod
141
+ @handle_api_error
142
+ def list(
143
+ cls,
144
+ model_id: UUID,
145
+ ) -> Iterator[CustomExpression]:
146
+ """Get a list of all custom metrics in the organization."""
147
+
148
+ model = Model.get(id_=model_id)
149
+ params: dict[str, Any] = {
150
+ 'organization_name': cls.get_organization_name(),
151
+ 'project_name': model.project.name,
152
+ 'model_name': model.name,
153
+ }
154
+
155
+ for custom_metric in cls._paginate(
156
+ url=cls._get_url(),
157
+ params=params,
158
+ ):
159
+ yield cls._from_dict(data=custom_metric)
160
+
161
+ @handle_api_error
162
+ def create(self) -> CustomExpression:
163
+ """Create a new custom metric."""
164
+ model = Model.get(id_=self.model_id)
165
+ payload = {
166
+ 'name': self.name,
167
+ 'organization_name': self.get_organization_name(),
168
+ 'project_name': model.project.name,
169
+ 'model_name': model.name,
170
+ 'definition': self.definition,
171
+ }
172
+
173
+ if self.description:
174
+ payload['description'] = self.description
175
+
176
+ response = self._client().post(
177
+ url=self._get_url(),
178
+ data=payload,
179
+ )
180
+ self._refresh_from_response(response=response)
181
+ return self
182
+
183
+ @handle_api_error
184
+ def delete(self) -> None:
185
+ """Delete a custom metric."""
186
+ assert self.id is not None
187
+
188
+ self._client().delete(url=self._get_url(id_=self.id))
189
+
190
+
191
+ class CustomMetric(CustomExpression):
192
+ def __init__(
193
+ self, name: str, model_id: UUID, definition: str, description: str | None = None
194
+ ) -> None:
195
+ """Construct a custom metric instance."""
196
+ super().__init__(name, model_id, definition, description)
197
+
198
+ # Deserialized response object
199
+ self._resp: CustomMetricResp | None = None
200
+
201
+ @staticmethod
202
+ def _get_url(id_: UUID | str | None = None) -> str:
203
+ """Get custom metric resource/item url."""
204
+ url = '/v2/custom-metrics'
205
+ return url if not id_ else f'{url}/{id_}'
206
+
207
+
208
+ class Segment(CustomExpression):
209
+ def __init__(
210
+ self, name: str, model_id: UUID, definition: str, description: str | None = None
211
+ ) -> None:
212
+ """Construct a segment instance."""
213
+ super().__init__(name, model_id, definition, description)
214
+
215
+ # Deserialized response object
216
+ self._resp: SegmentResp | None = None
217
+
218
+ @staticmethod
219
+ def _get_url(id_: UUID | str | None = None) -> str:
220
+ """Get segment resource/item url."""
221
+ url = '/v2/segments'
222
+ return url if not id_ else f'{url}/{id_}'
fiddler/entities/file.py CHANGED
@@ -8,7 +8,6 @@ from typing import Any
8
8
  from uuid import UUID
9
9
 
10
10
  from requests import Response
11
- from requests_toolbelt import MultipartEncoder
12
11
 
13
12
  from fiddler.constants.common import MULTI_PART_CHUNK_SIZE
14
13
  from fiddler.decorators import handle_api_error
@@ -74,14 +73,12 @@ class File(BaseEntity, CreatedByMixin, UpdatedByMixin):
74
73
  @handle_api_error
75
74
  def single_upload(self) -> File:
76
75
  """Single part file upload"""
77
- with open(self.path, 'rb') as f:
78
- multipart_data = MultipartEncoder(fields={'file': (self.name, f)})
79
76
 
80
- response = self._client().post(
81
- url='/v3/files/upload',
82
- data=multipart_data,
83
- headers={'Content-Type': multipart_data.content_type},
84
- )
77
+ response = self._client().post(
78
+ url='/v3/files/upload',
79
+ files={'file': (self.name, open(self.path, 'rb'))},
80
+ headers={'Content-Type': None},
81
+ )
85
82
 
86
83
  self._refresh_from_response(response)
87
84
  return self
@@ -66,9 +66,10 @@ class ModelArtifact(ConnectionMixin):
66
66
  )
67
67
 
68
68
  logger.info(
69
- 'Model[%s] - Model artifact tar file created at %s',
69
+ 'Model[%s] - Model artifact tar file created at %s - %d bytes',
70
70
  self.model_id,
71
71
  file_path,
72
+ os.path.getsize(file_path),
72
73
  )
73
74
 
74
75
  # Upload file
@@ -7,7 +7,6 @@ from urllib.parse import urljoin
7
7
  import requests
8
8
  import simplejson
9
9
  from requests.adapters import HTTPAdapter
10
- from requests_toolbelt.multipart.encoder import MultipartEncoder
11
10
 
12
11
  from fiddler.constants.common import JSON_CONTENT_TYPE
13
12
  from fiddler.exceptions import ( # pylint: disable=redefined-builtin
@@ -51,7 +50,6 @@ class RequestClient:
51
50
  headers: dict | None = None,
52
51
  data: dict | bytes | None = None,
53
52
  timeout: int | None = None,
54
- files_encoders: dict[str, MultipartEncoder] | None = None,
55
53
  **kwargs: Any,
56
54
  ) -> requests.Response:
57
55
  """
@@ -63,9 +61,6 @@ class RequestClient:
63
61
  :param headers: Request headers
64
62
  :param data: Dict/binary data
65
63
  :param timeout: Request timeout in seconds
66
- :param files_encoders: Dictionary of 'filename': file-like-objects
67
- for multipart encoding upload. Make sure all MultipartEncoder values have
68
- same content_type.
69
64
  """
70
65
  logger.debug('Making %s call to %s', method, url)
71
66
 
@@ -87,31 +82,16 @@ class RequestClient:
87
82
  # So setting that as default here
88
83
  kwargs.setdefault('verify', self.session.verify)
89
84
  try:
90
- if files_encoders:
91
- encoders = list(files_encoders.values())
92
- if encoders:
93
- request_headers['Content-Type'] = encoders[0].content_type
94
- response = self.session.request(
95
- method,
96
- full_url,
97
- params=params,
98
- files=files_encoders,
99
- headers=request_headers,
100
- timeout=timeout,
101
- proxies=self.proxies,
102
- **kwargs,
103
- )
104
- else:
105
- response = self.session.request(
106
- method,
107
- full_url,
108
- params=params,
109
- data=data,
110
- headers=request_headers,
111
- timeout=timeout,
112
- proxies=self.proxies,
113
- **kwargs,
114
- )
85
+ response = self.session.request(
86
+ method,
87
+ full_url,
88
+ params=params,
89
+ data=data,
90
+ headers=request_headers,
91
+ timeout=timeout,
92
+ proxies=self.proxies,
93
+ **kwargs,
94
+ )
115
95
 
116
96
  except requests.Timeout as exc:
117
97
  logger.error('%s call failed with Timeout error for URL %s', method, url)
@@ -173,7 +153,6 @@ class RequestClient:
173
153
  headers: dict | None = None,
174
154
  timeout: int | None = None,
175
155
  data: dict | bytes | None = None,
176
- files_encoders: dict[str, MultipartEncoder] | None = None,
177
156
  **kwargs: dict[str, Any],
178
157
  ) -> requests.Response:
179
158
  """Construct a post request instance."""
@@ -184,7 +163,6 @@ class RequestClient:
184
163
  headers=headers,
185
164
  timeout=timeout,
186
165
  data=data,
187
- files_encoders=files_encoders,
188
166
  **kwargs,
189
167
  )
190
168
 
@@ -0,0 +1,23 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from uuid import UUID
4
+
5
+ from fiddler.schemas.base import BaseModel
6
+
7
+
8
+ class CustomExpressionResp(BaseModel):
9
+ id: UUID
10
+ name: str
11
+ model_name: str
12
+ project_name: str
13
+ definition: str
14
+ description: Optional[str]
15
+ created_at: datetime
16
+
17
+
18
+ class CustomMetricResp(CustomExpressionResp):
19
+ """Custom metric response object"""
20
+
21
+
22
+ class SegmentResp(CustomExpressionResp):
23
+ """Segment response object"""
@@ -0,0 +1,352 @@
1
+ from http import HTTPStatus
2
+ from uuid import UUID
3
+
4
+ import pytest
5
+ import responses
6
+ from responses import matchers
7
+
8
+ from fiddler.entities.custom_metric import CustomMetric
9
+ from fiddler.exceptions import Conflict, NotFound
10
+ from fiddler.tests.apis.test_model import API_RESPONSE_200 as MODEL_API_RESPONSE_200
11
+ from fiddler.tests.apis.test_model import (
12
+ API_RESPONSE_FROM_NAME as MODEL_API_RESPONSE_FROM_NAME,
13
+ )
14
+ from fiddler.tests.constants import MODEL_ID, MODEL_NAME, ORG_NAME, PROJECT_NAME, URL
15
+
16
+ CUSTOM_METRIC_ID = '7057867c-6dd8-4915-89f2-a5f253dd4a3a'
17
+ CUSTOM_METRIC_NAME = 'xyzabc'
18
+
19
+ API_RESPONSE_200 = {
20
+ 'data': {
21
+ 'description': 'meh',
22
+ 'definition': "average(\"Age\")",
23
+ 'created_by': {
24
+ 'id': 2,
25
+ 'full_name': 'Nikhil Singh',
26
+ 'email': 'nikhil@fiddler.ai',
27
+ },
28
+ 'name': CUSTOM_METRIC_NAME,
29
+ 'id': CUSTOM_METRIC_ID,
30
+ 'organization_name': ORG_NAME,
31
+ 'model_name': MODEL_NAME,
32
+ 'project_name': PROJECT_NAME,
33
+ 'created_at': '2024-02-13T07:56:04.275549+00:00',
34
+ },
35
+ 'api_version': '2.0',
36
+ 'kind': 'NORMAL',
37
+ }
38
+
39
+ API_RESPONSE_404 = {
40
+ 'error': {
41
+ 'code': 404,
42
+ 'message': "CustomMetric({'uuid': UUID('7057867c-6dd8-4915-89f2-a5f253dd4a3a')}) not found",
43
+ 'errors': [
44
+ {
45
+ 'reason': 'ObjectNotFound',
46
+ 'message': "CustomMetric({'uuid': UUID('7057867c-6dd8-4915-89f2-a5f253dd4a3a')}) not found",
47
+ 'help': '',
48
+ }
49
+ ],
50
+ },
51
+ 'api_version': '2.0',
52
+ 'kind': 'ERROR',
53
+ }
54
+
55
+ API_RESPONSE_FROM_NAME = {
56
+ 'data': {
57
+ 'page_size': 100,
58
+ 'total': 1,
59
+ 'item_count': 1,
60
+ 'page_count': 1,
61
+ 'page_index': 1,
62
+ 'offset': 0,
63
+ 'items': [API_RESPONSE_200['data']],
64
+ }
65
+ }
66
+
67
+ EMPTY_LIST_API_RESPONSE = {
68
+ 'data': {
69
+ 'page_size': 100,
70
+ 'total': 0,
71
+ 'item_count': 0,
72
+ 'page_count': 1,
73
+ 'page_index': 1,
74
+ 'offset': 0,
75
+ 'items': [],
76
+ }
77
+ }
78
+
79
+ LIST_API_RESPONSE = {
80
+ 'data': {
81
+ 'page_size': 100,
82
+ 'total': 2,
83
+ 'item_count': 2,
84
+ 'page_count': 1,
85
+ 'page_index': 1,
86
+ 'offset': 0,
87
+ 'items': [
88
+ API_RESPONSE_200['data'],
89
+ {
90
+ 'description': 'meh',
91
+ 'definition': "average(\"Age\")",
92
+ 'created_by': {
93
+ 'id': 2,
94
+ 'full_name': 'Nikhil Singh',
95
+ 'email': 'nikhil@fiddler.ai',
96
+ },
97
+ 'name': 'abcxyz',
98
+ 'id': 'a509c450-c00b-4b5c-9a96-89c43e287e5a',
99
+ 'organization_name': ORG_NAME,
100
+ 'model_name': MODEL_NAME,
101
+ 'project_name': PROJECT_NAME,
102
+ 'created_at': '2024-02-12T07:56:04.275549+00:00',
103
+ },
104
+ ],
105
+ }
106
+ }
107
+
108
+ API_RESPONSE_409 = {
109
+ 'error': {
110
+ 'code': 409,
111
+ 'message': 'Custom metric with the same name already exists for this model',
112
+ 'errors': [
113
+ {
114
+ 'reason': 'Conflict',
115
+ 'message': 'Custom metric with the same name already exists for this model',
116
+ 'help': '',
117
+ }
118
+ ],
119
+ },
120
+ 'api_version': '2.0',
121
+ 'kind': 'ERROR',
122
+ }
123
+
124
+
125
+ @responses.activate
126
+ def test_get_custom_metric() -> None:
127
+ responses.get(
128
+ url=f'{URL}/v2/custom-metrics/{CUSTOM_METRIC_ID}',
129
+ json=API_RESPONSE_200,
130
+ )
131
+
132
+ responses.get(
133
+ url=f'{URL}/v3/models',
134
+ json=MODEL_API_RESPONSE_FROM_NAME,
135
+ )
136
+
137
+ responses.get(
138
+ url=f'{URL}/v3/models/{MODEL_ID}',
139
+ json=MODEL_API_RESPONSE_200,
140
+ )
141
+
142
+ cm = CustomMetric.get(id_=CUSTOM_METRIC_ID)
143
+ assert isinstance(cm, CustomMetric)
144
+
145
+
146
+ @responses.activate
147
+ def test_get_cm_not_found() -> None:
148
+ responses.get(
149
+ url=f'{URL}/v2/custom-metrics/{CUSTOM_METRIC_ID}',
150
+ json=API_RESPONSE_404,
151
+ status=HTTPStatus.NOT_FOUND,
152
+ )
153
+
154
+ with pytest.raises(NotFound):
155
+ CustomMetric.get(id_=CUSTOM_METRIC_ID)
156
+
157
+
158
+ @responses.activate
159
+ def test_cm_from_name() -> None:
160
+ params = {
161
+ 'filter': '{"condition": "AND", "rules": [{"field": "name", "operator": "equal", "value": "xyzabc"}]}',
162
+ 'model_name': 'bank_churn',
163
+ 'organization_name': 'fiddler_dev',
164
+ 'project_name': 'bank_churn',
165
+ }
166
+
167
+ responses.get(
168
+ url=f'{URL}/v2/custom-metrics',
169
+ json=API_RESPONSE_FROM_NAME,
170
+ match=[matchers.query_param_matcher(params)],
171
+ )
172
+ responses.get(
173
+ url=f'{URL}/v3/models',
174
+ json=MODEL_API_RESPONSE_FROM_NAME,
175
+ )
176
+
177
+ responses.get(
178
+ url=f'{URL}/v3/models/{MODEL_ID}',
179
+ json=MODEL_API_RESPONSE_200,
180
+ )
181
+ cm = CustomMetric.from_name(
182
+ name=CUSTOM_METRIC_NAME,
183
+ model_name=MODEL_NAME,
184
+ project_name=PROJECT_NAME,
185
+ )
186
+ assert isinstance(cm, CustomMetric)
187
+
188
+
189
+ @responses.activate
190
+ def test_cm_from_name_not_found() -> None:
191
+ params = {
192
+ 'filter': '{"condition": "AND", "rules": [{"field": "name", "operator": "equal", "value": "xyzabc"}]}',
193
+ 'model_name': 'bank_churn',
194
+ 'organization_name': 'fiddler_dev',
195
+ 'project_name': 'bank_churn',
196
+ }
197
+ responses.get(
198
+ url=f'{URL}/v2/custom-metrics',
199
+ json=EMPTY_LIST_API_RESPONSE,
200
+ match=[matchers.query_param_matcher(params)],
201
+ )
202
+ with pytest.raises(NotFound):
203
+ CustomMetric.from_name(
204
+ name=CUSTOM_METRIC_NAME,
205
+ model_name=MODEL_NAME,
206
+ project_name=PROJECT_NAME,
207
+ )
208
+
209
+
210
+ @responses.activate
211
+ def test_cm_list_empty() -> None:
212
+ responses.get(
213
+ url=f'{URL}/v2/custom-metrics',
214
+ json=EMPTY_LIST_API_RESPONSE,
215
+ )
216
+
217
+ responses.get(
218
+ url=f'{URL}/v3/models/{MODEL_ID}',
219
+ json=MODEL_API_RESPONSE_200,
220
+ )
221
+
222
+ assert len(list(CustomMetric.list(model_id=MODEL_ID))) == 0
223
+
224
+
225
+ @responses.activate
226
+ def test_cm_list_success() -> None:
227
+ params = {
228
+ 'organization_name': ORG_NAME,
229
+ 'project_name': PROJECT_NAME,
230
+ 'model_name': MODEL_NAME,
231
+ 'limit': 50,
232
+ 'offset': 0,
233
+ }
234
+
235
+ responses.get(
236
+ url=f'{URL}/v2/custom-metrics',
237
+ json=LIST_API_RESPONSE,
238
+ match=[matchers.query_param_matcher(params)],
239
+ )
240
+
241
+ responses.get(
242
+ url=f'{URL}/v3/models',
243
+ json=MODEL_API_RESPONSE_FROM_NAME,
244
+ )
245
+
246
+ responses.get(
247
+ url=f'{URL}/v3/models/{MODEL_ID}',
248
+ json=MODEL_API_RESPONSE_200,
249
+ )
250
+
251
+ for cm in CustomMetric.list(model_id=MODEL_ID):
252
+ assert isinstance(cm, CustomMetric)
253
+
254
+
255
+ @responses.activate
256
+ def test_cm_create() -> None:
257
+ responses.post(
258
+ url=f'{URL}/v2/custom-metrics',
259
+ json=API_RESPONSE_200,
260
+ )
261
+
262
+ responses.get(
263
+ url=f'{URL}/v3/models/{MODEL_ID}',
264
+ json=MODEL_API_RESPONSE_200,
265
+ )
266
+
267
+ custom_metric = CustomMetric(
268
+ name=CUSTOM_METRIC_NAME,
269
+ model_id=MODEL_ID,
270
+ definition="average(\"Age\")",
271
+ description='meh',
272
+ ).create()
273
+
274
+ assert isinstance(custom_metric, CustomMetric)
275
+ assert custom_metric.id == UUID(CUSTOM_METRIC_ID)
276
+ assert custom_metric.name == CUSTOM_METRIC_NAME
277
+ assert custom_metric.model_id
278
+
279
+
280
+ @responses.activate
281
+ def test_cm_create_conflict() -> None:
282
+ responses.post(
283
+ url=f'{URL}/v2/custom-metrics',
284
+ json=API_RESPONSE_409,
285
+ status=HTTPStatus.CONFLICT,
286
+ )
287
+
288
+ responses.get(
289
+ url=f'{URL}/v3/models/{MODEL_ID}',
290
+ json=MODEL_API_RESPONSE_200,
291
+ )
292
+
293
+ with pytest.raises(Conflict):
294
+ CustomMetric(
295
+ name=CUSTOM_METRIC_NAME,
296
+ model_id=MODEL_ID,
297
+ definition="average(\"Age\")",
298
+ description='meh',
299
+ ).create()
300
+
301
+
302
+ @responses.activate
303
+ def test_delete_cm() -> None:
304
+ responses.get(
305
+ url=f'{URL}/v2/custom-metrics/{CUSTOM_METRIC_ID}',
306
+ json=API_RESPONSE_200,
307
+ )
308
+
309
+ responses.get(
310
+ url=f'{URL}/v3/models',
311
+ json=MODEL_API_RESPONSE_FROM_NAME,
312
+ )
313
+
314
+ responses.get(
315
+ url=f'{URL}/v3/models/{MODEL_ID}',
316
+ json=MODEL_API_RESPONSE_200,
317
+ )
318
+ custom_metric = CustomMetric.get(id_=CUSTOM_METRIC_ID)
319
+
320
+ responses.delete(
321
+ url=f'{URL}/v2/custom-metrics/{CUSTOM_METRIC_ID}',
322
+ )
323
+
324
+ custom_metric.delete()
325
+
326
+
327
+ @responses.activate
328
+ def test_delete_cm_not_found() -> None:
329
+ responses.get(
330
+ url=f'{URL}/v2/custom-metrics/{CUSTOM_METRIC_ID}',
331
+ json=API_RESPONSE_200,
332
+ )
333
+
334
+ responses.get(
335
+ url=f'{URL}/v3/models',
336
+ json=MODEL_API_RESPONSE_FROM_NAME,
337
+ )
338
+
339
+ responses.get(
340
+ url=f'{URL}/v3/models/{MODEL_ID}',
341
+ json=MODEL_API_RESPONSE_200,
342
+ )
343
+ custom_metric = CustomMetric.get(id_=CUSTOM_METRIC_ID)
344
+
345
+ responses.delete(
346
+ url=f'{URL}/v2/custom-metrics/{CUSTOM_METRIC_ID}',
347
+ json=API_RESPONSE_404,
348
+ status=HTTPStatus.NOT_FOUND,
349
+ )
350
+
351
+ with pytest.raises(NotFound):
352
+ custom_metric.delete()
@@ -6,7 +6,7 @@ import pandas as pd
6
6
  import pytest
7
7
 
8
8
  from fiddler.schemas.server_info import Version
9
- from fiddler.utils.helpers import group_by
9
+ from fiddler.utils.helpers import group_by, try_series_retype
10
10
  from fiddler.utils.validations import validate_artifact_dir
11
11
  from fiddler.utils.version import match_semver
12
12
 
@@ -66,3 +66,34 @@ def test_group_by_helper() -> None:
66
66
  ]
67
67
  )
68
68
  )
69
+
70
+
71
+ def test_try_series_retype() -> None:
72
+ series = pd.Series([1, 2, 3], dtype='float')
73
+ series = try_series_retype(series, 'int')
74
+ assert series.dtype == 'int'
75
+
76
+ series = pd.Series([1, 2, None])
77
+ series = try_series_retype(series, 'int')
78
+ assert series.dtype == 'float'
79
+
80
+
81
+ def test_try_series_retype_str_or_unkown() -> None:
82
+ series = pd.Series(['HIGH', 'MEDIUM', '', None], dtype='str')
83
+ series = try_series_retype(series, 'str')
84
+ assert series.dtype == 'object'
85
+
86
+
87
+ def test_try_series_retype_timestamp() -> None:
88
+ series = pd.Series(
89
+ ['2023-11-12 09:15:32.23', '2023-12-11 09:15:32.45'], dtype='str'
90
+ )
91
+ series = try_series_retype(series, 'timestamp')
92
+ assert series.dtype == 'datetime64[ns]'
93
+
94
+
95
+ def test_try_series_retype_timestamp_error() -> None:
96
+ series = pd.Series(['2023-11-12 09:15:32.23', None], dtype='str')
97
+
98
+ with pytest.raises(TypeError):
99
+ try_series_retype(series, 'timestamp')
fiddler/utils/helpers.py CHANGED
@@ -17,7 +17,7 @@ logger = get_logger(__name__)
17
17
 
18
18
  def try_series_retype(series: pd.Series, new_type: str) -> pd.DataFrame | pd.Series:
19
19
  """Retype series."""
20
- if new_type in ['unknown', 'str']:
20
+ if new_type in ['unknown', 'str', 'vector']:
21
21
  # Do not retype data
22
22
  return series
23
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fiddler-client
3
- Version: 3.0.0.dev3
3
+ Version: 3.0.0.dev5
4
4
  Summary: Python client for Fiddler Platform
5
5
  Home-page: https://fiddler.ai
6
6
  Author: Fiddler Labs
@@ -49,6 +49,10 @@ Version History
49
49
  - Add percentage violation metrics for alert
50
50
  - Support for alert revisions
51
51
 
52
+ ### 2.4.1
53
+ - ### **Modifications**
54
+ - Fix slice query with `vector` type columns
55
+
52
56
  ### 2.4.0
53
57
  - ### **New Features**
54
58
  - Add support for segments
@@ -1,6 +1,5 @@
1
- fiddler/VERSION,sha256=yO0l7dPNd5kEmvr-FF91Tc-71LgfA35oMSIFv--D6Vg,11
2
- fiddler/__init__.py,sha256=ld-wK07BBilK902CLMDq1t0UA4kjrCYy2FYOtY44-s4,3270
3
- fiddler/_version.py,sha256=yJge9cTI7RlcMxUwRv_9Rr7NuUL4zJTXfcamq-ObEkU,27
1
+ fiddler/VERSION,sha256=UQ3IJpH6gZGP-vWjehD7g68w-idDM4WCOAjN3EGl-xk,11
2
+ fiddler/__init__.py,sha256=VCfPx51-NUioKq0OZSKmQTq0Qd20G088BcZt4CIzmG0,3378
4
3
  fiddler/configs.py,sha256=s8nKw_2gCEKYk8dnmRxd92J8rCl08V4vPcH0xATT4Lw,258
5
4
  fiddler/connection.py,sha256=pYaOyyJytsljofaMyRPV-Vs_i4wHKO48BxIJMqEBxHg,5760
6
5
  fiddler/decorators.py,sha256=tWZliQQeUGwY0rU52FO9JXuOBueQXVEJywEbrgUEw1U,1820
@@ -9,7 +8,7 @@ fiddler/version.py,sha256=8fyg3UhFpqghMpxbtYIwNcB1lhCG6yTkPBbDS7IrwxY,140
9
8
  fiddler/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
9
  fiddler/constants/alert_rule.py,sha256=cmFq-VKAlQbRKEkTtMmVNdD5R52uMI2zJai6TESFwfs,512
11
10
  fiddler/constants/baseline.py,sha256=NhmiGf73gy2IHKKpbm64HCE4eJfbxkAPy3uk_Z-6qSo,321
12
- fiddler/constants/common.py,sha256=4C14p_Kdng3canYWDyT244ikhFH6jo8oAfoYkZaq5vw,610
11
+ fiddler/constants/common.py,sha256=pfUe_aA3GCDJcnXxOQXyv8TEBoHePbh8nJz0GTaSDCs,543
13
12
  fiddler/constants/dataset.py,sha256=ZxPmzycSlrKpUYShpfTDqfmz8LAebzyBtIV_dBy-pRI,126
14
13
  fiddler/constants/events.py,sha256=3SgjNo4IS-_Nod8wbj98di4B4GctF9R414MZxfQ4MkU,114
15
14
  fiddler/constants/job.py,sha256=tMT0vr3x3IiCTQcifP2s1-VWC5gskflKexfU_VfDIzA,233
@@ -21,12 +20,13 @@ fiddler/entities/alert_record.py,sha256=RcDACWDNpntkiId-JpkLQvVeIbuaZAzn6AWl2zYI
21
20
  fiddler/entities/alert_rule.py,sha256=hdIJ6n2lTCPRRTdyf-x9yVDjCaYlgYzu0jxa7iBbQkc,11908
22
21
  fiddler/entities/base.py,sha256=YCTLt1ta8UbD7u-5BW7v_ocA4S7LevEncI5XPS_SB60,2106
23
22
  fiddler/entities/baseline.py,sha256=1y2bFIapzlDSBd4EUfNy6e-Q8fm3t0AGAZTdiz97hXQ,8157
23
+ fiddler/entities/custom_metric.py,sha256=Oxcnx-o1D4WowDIa5nclGDbw5eYwpemlIyhDhjyq0Ow,6802
24
24
  fiddler/entities/dataset.py,sha256=dmOWae2IxwoIbIRX62fCtjr6uNrht3mSuFw_QTnvZwI,4712
25
25
  fiddler/entities/events.py,sha256=usFxs8Cq0ds1MIrOjZT5jVt3XDXMdGYakOoNFnTFQ8Y,4370
26
- fiddler/entities/file.py,sha256=14DzU3Cs86IWEgXsfGggPlmsGBpiCsO8qareimc6p34,4846
26
+ fiddler/entities/file.py,sha256=U8IlMYl2iOdGwRWSCYmVbvFlx3rVJM-Xxr0soqLux0U,4667
27
27
  fiddler/entities/job.py,sha256=Feiqo6ns4bRcp2uaV-kuGYAKs90-aYXvzqqi1yLivYE,5220
28
28
  fiddler/entities/model.py,sha256=TiI-n6TfFP8gdML_kbrowBotFIsRqb2UmiClkyzXn3c,16499
29
- fiddler/entities/model_artifact.py,sha256=6sDTUwKwKXdnBml8xj-2SErxO_IP3RRPO-4NSz2VViU,5459
29
+ fiddler/entities/model_artifact.py,sha256=ktyFkg-nUPtdpSUGNXHfLOwSMiBaaRkdU0s9DQ67lLs,5514
30
30
  fiddler/entities/model_deployment.py,sha256=JefFd7uUgPaxWhtVTJZqTMCLWz_18DM8o2zZmY_Hn6c,3757
31
31
  fiddler/entities/organization.py,sha256=XvUAdBUOYXzdmblxIs4hi0mAg45NtAwy5DBaMiqi47o,1074
32
32
  fiddler/entities/project.py,sha256=_ONa4srk6zrDl6o6-BznmjiINn7TDabYSsWxve7BZxw,4595
@@ -35,7 +35,7 @@ fiddler/entities/user.py,sha256=ev_Xx9QHAdEHYzNDafzQihXOcD8ME5XkpKgc8MiqCnw,1243
35
35
  fiddler/entities/webhook.py,sha256=I-mcl92Tg2-oUUqNMOeDPBe0XawVYhNcTGXHBway7_A,3798
36
36
  fiddler/entities/xai.py,sha256=t5LOOWRtr0KnNJHOPfdhrH1WdNT6MBpAZoQ8Teiu9a8,20232
37
37
  fiddler/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- fiddler/libs/http_client.py,sha256=NvENXPm5w713OH9l6yq_KcJmXm4N1AsetYRIHY4oNBQ,7072
38
+ fiddler/libs/http_client.py,sha256=bl2V-q66NoPHm950CiDqOoF-kCPztYWUYNy9hVUsbqg,6027
39
39
  fiddler/libs/json_encoder.py,sha256=brsrWySvtZdaBArIkqKM5WwJUbvZf6ZbKozn1CewjOc,512
40
40
  fiddler/libs/semver.py,sha256=3HbXG1S-ABIxL3E0FHSm5OR_YrhYtCB2Q3XJ8-tywts,15995
41
41
  fiddler/packtools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -49,6 +49,7 @@ fiddler/schemas/alert_rule.py,sha256=-V0AibEImrfcEZfJ4J_Ed5SuZISOGsk-nIoVlX5mOOg
49
49
  fiddler/schemas/base.py,sha256=upzAmneEz169h_w-UDH_tY0Abo7DeK68YOZ5qVwIMa8,189
50
50
  fiddler/schemas/baseline.py,sha256=0z7ohzDmUKENmlVZDFx4vB-0SRVTOKaeM1N-pDSCIDk,923
51
51
  fiddler/schemas/custom_features.py,sha256=G0UqikCyvhnLg6oUAutvq4mEsGgVcLkPlJaMCbRZj68,5382
52
+ fiddler/schemas/custom_metric.py,sha256=SEadAmmBxHHQV9z2gCvbY19zJfLel_tpEiy1jJTjt7s,475
52
53
  fiddler/schemas/dataset.py,sha256=rGmpMrMfPLSs9d3_sxDTjrKqWp-o-bCe9pf_nlYyHfw,657
53
54
  fiddler/schemas/events.py,sha256=dH6SGPBO_rgGZetIhSo4Fq0L1pTU7TEKR7zW7gg3fnI,409
54
55
  fiddler/schemas/file.py,sha256=z_7bOxKbwQsgI8F7MMQvC6RE5dgdzYCMyD5XcBirjCc,462
@@ -73,12 +74,13 @@ fiddler/tests/constants.py,sha256=-_uoidMv6eQ-ldHCN5u_uBXuX3aS4YuylaUSbBfGHYY,10
73
74
  fiddler/tests/test_connection.py,sha256=d8WoB0QRZEObWfeRAZRXRm1kw9wfEysmeo4Bld7x8js,1865
74
75
  fiddler/tests/test_json_encoder.py,sha256=GJeFT5tTA4RTfZn1q2xm1V5SEj5DNsW3LeHPVfFxcFw,783
75
76
  fiddler/tests/test_logger.py,sha256=StDWgtVoBOplRsvcH44Su-ikOWcafT29Kof-K-jiTxc,312
76
- fiddler/tests/test_utils.py,sha256=Rr50DIxghloxtCNiri1ToD2EKp9Xf9r95lPX-Fg9naM,2366
77
+ fiddler/tests/test_utils.py,sha256=AF6y7UfboEkCa-nNw0aF30SdHC7yW2Hh78J4H_4s78k,3333
77
78
  fiddler/tests/utils.py,sha256=FEDbASW4pYZktdEK2FLnIDyZCwBzpb60m6EzkvOkwSc,335
78
79
  fiddler/tests/apis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
80
  fiddler/tests/apis/test_alert_record.py,sha256=_0EBRDn87TqTiO1KYEHKQQVtVZjj4lYjbt8Ix-RRTSg,3118
80
81
  fiddler/tests/apis/test_alert_rule.py,sha256=wBZVU4pwPLS3QJMbO06k9DsyJJb5hI4SxPqU0RjOIWM,11197
81
82
  fiddler/tests/apis/test_baseline.py,sha256=6KRXol1AxVwyfGH9epTGd_U4_5b-IVU4ArTWRbuJ0nc,7580
83
+ fiddler/tests/apis/test_custom_metric.py,sha256=I2-CqEScMOaGJsQ-jZmZlzfh35OtilJqzk6RXH3UR6A,8942
82
84
  fiddler/tests/apis/test_dataset.py,sha256=k-H2o1txgxN0kmsHNAC5ZYqB3atPM2Vlx3TwqcTZrcw,4765
83
85
  fiddler/tests/apis/test_events.py,sha256=e5sd3dIfMPyHoX-jX1QjCP90D-2ijJq1NERGbbfmSls,4510
84
86
  fiddler/tests/apis/test_files.py,sha256=-BisPck7bHMSW3N40ffdQJe027Qe3079tjTlU2OI9DU,4501
@@ -93,13 +95,13 @@ fiddler/tests/apis/test_project.py,sha256=IEYT3imD4dZJg-OT888G4jQJ_AxxHN5_VkCHcm
93
95
  fiddler/tests/apis/test_webhook.py,sha256=DTaEI9r2lossAaXb3b1CX4C8L3bYAzZmLIWH8VeauUs,6440
94
96
  fiddler/tests/apis/test_xai.py,sha256=5TzRv3jSTn2xi3qpwPyKyybEVjHcSyszSHTLzuXU30s,28537
95
97
  fiddler/utils/__init__.py,sha256=ooAJR6_tVaF6UKZriz5ynZPj8IeCzaDIPKV23F-e4Fk,53
96
- fiddler/utils/helpers.py,sha256=9cfAdpkIg16VPVv7jZzcHpJ5H5yi4bBPHKppAAZV734,2898
98
+ fiddler/utils/helpers.py,sha256=Xl_T_se9SOeMTmVJAOVzzmkekUsmpFMlAb2KvPCWfTw,2908
97
99
  fiddler/utils/logger.py,sha256=FdJ3LkS9dbRjWsw5oJmNNsd7q0XRVEIvx5-TWys7L0k,669
98
100
  fiddler/utils/model_generator.py,sha256=TwQMQDpy-0gcq6HyL68s37N-sk_nGPq33mmppK6i7hI,2203
99
101
  fiddler/utils/validations.py,sha256=i8NtgrpCsitq6BPa5lygpKDh07oaOXu7PAlCIcMvqzY,408
100
102
  fiddler/utils/version.py,sha256=iC8Ry7UFl9EJW_xU1WbzO_l4yK6V7eQ6U5exB3xT864,531
101
- fiddler_client-3.0.0.dev3.dist-info/LICENSE.txt,sha256=w8-LUAb_VLBWSsCNmih0pAqLIicJfGu8OJXpDjkIg_o,559
102
- fiddler_client-3.0.0.dev3.dist-info/METADATA,sha256=Mg2x1U2ToKTJjZKEiK4D2aVsqVyNAlRK2ff0NZ2oUNg,20593
103
- fiddler_client-3.0.0.dev3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
104
- fiddler_client-3.0.0.dev3.dist-info/top_level.txt,sha256=ndC6LqvKmrTTs8czRlBOYdYgSqZbHuMf0zHt_HWpzzk,8
105
- fiddler_client-3.0.0.dev3.dist-info/RECORD,,
103
+ fiddler_client-3.0.0.dev5.dist-info/LICENSE.txt,sha256=w8-LUAb_VLBWSsCNmih0pAqLIicJfGu8OJXpDjkIg_o,559
104
+ fiddler_client-3.0.0.dev5.dist-info/METADATA,sha256=iyTcPoK3ebTms6F_Tmm3csGfUcmxgy-Ced_ASIAg4s4,20679
105
+ fiddler_client-3.0.0.dev5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
106
+ fiddler_client-3.0.0.dev5.dist-info/top_level.txt,sha256=ndC6LqvKmrTTs8czRlBOYdYgSqZbHuMf0zHt_HWpzzk,8
107
+ fiddler_client-3.0.0.dev5.dist-info/RECORD,,
fiddler/_version.py DELETED
@@ -1 +0,0 @@
1
- __version__ = '3.0.0.dev3'