fiddler-client 3.8.2__tar.gz → 3.9.0__tar.gz
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-client-3.8.2/fiddler_client.egg-info → fiddler-client-3.9.0}/PKG-INFO +1 -1
- fiddler-client-3.9.0/fiddler/VERSION +1 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/webhook.py +2 -2
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/webhook.py +4 -2
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_webhook.py +19 -15
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/constants.py +2 -1
- fiddler-client-3.9.0/fiddler/tests/test_utils.py +431 -0
- fiddler-client-3.9.0/fiddler/utils/column_generator.py +68 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/utils/model_generator.py +1 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0/fiddler_client.egg-info}/PKG-INFO +1 -1
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler_client.egg-info/SOURCES.txt +1 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler_client.egg-info/requires.txt +1 -1
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/setup.py +1 -1
- fiddler-client-3.8.2/fiddler/VERSION +0 -1
- fiddler-client-3.8.2/fiddler/tests/test_utils.py +0 -99
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/LICENSE.txt +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/MANIFEST.in +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/PUBLIC.md +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/README.md +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/configs.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/connection.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/alert_rule.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/baseline.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/common.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/dataset.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/events.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/job.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/model.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/model_deployment.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/constants/xai.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/decorators.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/alert_record.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/alert_rule.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/base.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/baseline.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/custom_expression.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/dataset.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/events.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/file.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/job.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/model.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/model_artifact.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/model_deployment.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/organization.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/project.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/surrogate.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/user.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/entities/xai.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/exceptions.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/libs/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/libs/aws.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/libs/http_client.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/libs/json_encoder.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/libs/semver.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/packtools/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/packtools/gem.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/packtools/keras_ig_helpers.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/packtools/project_attributions_helpers.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/packtools/template_model.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/alert_record.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/alert_rule.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/base.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/baseline.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/custom_expression.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/custom_features.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/dataset.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/events.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/file.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/filter_query.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/job.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/model.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/model_deployment.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/model_schema.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/model_spec.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/model_task_params.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/organization.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/project.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/response.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/server_info.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/user.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/xai.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/schemas/xai_params.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_alert_record.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_alert_rule.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_baseline.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_custom_metric.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_dataset.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_events.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_files.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_generate_model.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_job.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_mixin.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_model.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_model_artifact.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_model_deployment.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_model_surrogate.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_project.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_segment.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/apis/test_xai.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/conftest.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/test_connection.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/test_json_encoder.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/test_logger.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/test_sagemaker_auth.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/tests/utils.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/utils/__init__.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/utils/decorators.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/utils/helpers.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/utils/logger.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/utils/validations.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/utils/version.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/version.py +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler_client.egg-info/dependency_links.txt +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler_client.egg-info/top_level.txt +0 -0
- {fiddler-client-3.8.2 → fiddler-client-3.9.0}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.9.0
|
|
@@ -20,7 +20,7 @@ class Webhook(BaseEntity):
|
|
|
20
20
|
"""
|
|
21
21
|
self.name = name
|
|
22
22
|
self.url = url
|
|
23
|
-
# provider is 'SLACK'
|
|
23
|
+
# provider is 'SLACK', 'MS_TEAMS' as of May 2025.
|
|
24
24
|
self.provider = provider
|
|
25
25
|
|
|
26
26
|
self.id: UUID | None = None
|
|
@@ -113,7 +113,7 @@ class Webhook(BaseEntity):
|
|
|
113
113
|
|
|
114
114
|
:params name: name of webhook
|
|
115
115
|
:params url: webhook url
|
|
116
|
-
:params provider: Either 'SLACK' or '
|
|
116
|
+
:params provider: Either 'SLACK' or 'MS_TEAMS'
|
|
117
117
|
|
|
118
118
|
:returns: Created `Webhook` object.
|
|
119
119
|
"""
|
|
@@ -2,15 +2,17 @@ import enum
|
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from uuid import UUID
|
|
4
4
|
|
|
5
|
-
from pydantic.
|
|
5
|
+
from pydantic.v1 import Field
|
|
6
6
|
|
|
7
7
|
from fiddler.schemas.base import BaseModel
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@enum.unique
|
|
11
11
|
class WebhookProvider(str, enum.Enum):
|
|
12
|
-
# provider is 'SLACK' or '
|
|
12
|
+
# provider is 'SLACK' or 'MS_TEAMS' as of May 2025.
|
|
13
|
+
# keeping OTHER in enum for existing webhooks for backwards compatibility
|
|
13
14
|
SLACK = 'SLACK'
|
|
15
|
+
MS_TEAMS = 'MS_TEAMS'
|
|
14
16
|
OTHER = 'OTHER'
|
|
15
17
|
|
|
16
18
|
|
|
@@ -13,6 +13,7 @@ from fiddler.tests.constants import (
|
|
|
13
13
|
ORG_NAME,
|
|
14
14
|
URL,
|
|
15
15
|
WEBHOOK_ID,
|
|
16
|
+
WEBHOOK_UUID,
|
|
16
17
|
WEBHOOK_NAME,
|
|
17
18
|
WEBHOOK_PROVIDER,
|
|
18
19
|
WEBHOOK_URL,
|
|
@@ -21,6 +22,7 @@ from fiddler.tests.constants import (
|
|
|
21
22
|
API_RESPONSE_200 = {
|
|
22
23
|
'data': {
|
|
23
24
|
'id': WEBHOOK_ID,
|
|
25
|
+
'uuid': WEBHOOK_UUID,
|
|
24
26
|
'name': WEBHOOK_NAME,
|
|
25
27
|
'url': WEBHOOK_URL,
|
|
26
28
|
'provider': WEBHOOK_PROVIDER,
|
|
@@ -62,6 +64,7 @@ API_RESPONSE_FROM_NAME = {
|
|
|
62
64
|
# and `id` is of type UUID. Should we allow for additional
|
|
63
65
|
# properties?
|
|
64
66
|
'id': WEBHOOK_ID,
|
|
67
|
+
'uuid': WEBHOOK_UUID,
|
|
65
68
|
'name': WEBHOOK_NAME,
|
|
66
69
|
'url': WEBHOOK_URL,
|
|
67
70
|
'provider': WEBHOOK_PROVIDER,
|
|
@@ -85,6 +88,7 @@ LIST_API_RESPONSE = {
|
|
|
85
88
|
API_RESPONSE_200['data'],
|
|
86
89
|
{
|
|
87
90
|
'id': WEBHOOK_ID,
|
|
91
|
+
'uuid': WEBHOOK_UUID,
|
|
88
92
|
'name': 'test_webhook_config_name2',
|
|
89
93
|
'url': WEBHOOK_URL,
|
|
90
94
|
'provider': WEBHOOK_PROVIDER,
|
|
@@ -128,10 +132,10 @@ API_RESPONSE_409 = {
|
|
|
128
132
|
@responses.activate
|
|
129
133
|
def test_get_webhook_success() -> None:
|
|
130
134
|
responses.get(
|
|
131
|
-
url=f'{URL}/v2/webhooks/{
|
|
135
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
132
136
|
json=API_RESPONSE_200,
|
|
133
137
|
)
|
|
134
|
-
webhook = Webhook.get(id_=
|
|
138
|
+
webhook = Webhook.get(id_=WEBHOOK_UUID)
|
|
135
139
|
assert isinstance(webhook, Webhook)
|
|
136
140
|
|
|
137
141
|
|
|
@@ -213,7 +217,7 @@ def test_add_webhook_success() -> None:
|
|
|
213
217
|
name=WEBHOOK_NAME, url=WEBHOOK_URL, provider=WEBHOOK_PROVIDER
|
|
214
218
|
).create()
|
|
215
219
|
assert isinstance(webhook, Webhook)
|
|
216
|
-
assert webhook.id == UUID(
|
|
220
|
+
assert webhook.id == UUID(WEBHOOK_UUID)
|
|
217
221
|
assert webhook.name == WEBHOOK_NAME
|
|
218
222
|
assert webhook.created_at == datetime.fromisoformat(
|
|
219
223
|
API_RESPONSE_200['data']['created_at']
|
|
@@ -246,14 +250,14 @@ def test_add_webhook_conflict() -> None:
|
|
|
246
250
|
@responses.activate
|
|
247
251
|
def test_update_webhook_success(mocker) -> None:
|
|
248
252
|
responses.get(
|
|
249
|
-
url=f'{URL}/v2/webhooks/{
|
|
253
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
250
254
|
json=API_RESPONSE_200,
|
|
251
255
|
)
|
|
252
|
-
webhook = Webhook.get(id_=
|
|
256
|
+
webhook = Webhook.get(id_=WEBHOOK_UUID)
|
|
253
257
|
|
|
254
258
|
mocker.patch.dict(API_RESPONSE_200['data'], {'name': 'test1'})
|
|
255
259
|
responses.patch(
|
|
256
|
-
url=f'{URL}/v2/webhooks/{
|
|
260
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
257
261
|
json=API_RESPONSE_200,
|
|
258
262
|
)
|
|
259
263
|
webhook.name = 'test1'
|
|
@@ -266,31 +270,31 @@ def test_update_webhook_success(mocker) -> None:
|
|
|
266
270
|
@responses.activate
|
|
267
271
|
def test_update_webhook_not_found() -> None:
|
|
268
272
|
responses.get(
|
|
269
|
-
url=f'{URL}/v2/webhooks/{
|
|
273
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
270
274
|
json=API_RESPONSE_200,
|
|
271
275
|
)
|
|
272
276
|
|
|
273
277
|
responses.patch(
|
|
274
|
-
url=f'{URL}/v2/webhooks/{
|
|
278
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
275
279
|
json=API_RESPONSE_404,
|
|
276
280
|
status=HTTPStatus.NOT_FOUND,
|
|
277
281
|
)
|
|
278
282
|
|
|
279
283
|
with pytest.raises(NotFound):
|
|
280
|
-
webhook = Webhook.get(id_=
|
|
284
|
+
webhook = Webhook.get(id_=WEBHOOK_UUID)
|
|
281
285
|
webhook.update()
|
|
282
286
|
|
|
283
287
|
|
|
284
288
|
@responses.activate
|
|
285
289
|
def test_delete_webhook() -> None:
|
|
286
290
|
responses.get(
|
|
287
|
-
url=f'{URL}/v2/webhooks/{
|
|
291
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
288
292
|
json=API_RESPONSE_200,
|
|
289
293
|
)
|
|
290
|
-
webhook = Webhook.get(id_=
|
|
294
|
+
webhook = Webhook.get(id_=WEBHOOK_UUID)
|
|
291
295
|
|
|
292
296
|
responses.delete(
|
|
293
|
-
url=f'{URL}/v2/webhooks/{
|
|
297
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
294
298
|
json={},
|
|
295
299
|
)
|
|
296
300
|
response = webhook.delete()
|
|
@@ -300,13 +304,13 @@ def test_delete_webhook() -> None:
|
|
|
300
304
|
@responses.activate
|
|
301
305
|
def test_delete_webhook_not_found() -> None:
|
|
302
306
|
responses.get(
|
|
303
|
-
url=f'{URL}/v2/webhooks/{
|
|
307
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
304
308
|
json=API_RESPONSE_200,
|
|
305
309
|
)
|
|
306
|
-
webhook = Webhook.get(id_=
|
|
310
|
+
webhook = Webhook.get(id_=WEBHOOK_UUID)
|
|
307
311
|
|
|
308
312
|
responses.delete(
|
|
309
|
-
url=f'{URL}/v2/webhooks/{
|
|
313
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_UUID}',
|
|
310
314
|
json=API_RESPONSE_404,
|
|
311
315
|
status=HTTPStatus.NOT_FOUND,
|
|
312
316
|
)
|
|
@@ -30,7 +30,8 @@ TEST_WEBHOOKS = [
|
|
|
30
30
|
]
|
|
31
31
|
BASELINE_NAME = 'test_baseline'
|
|
32
32
|
MODEL_DEPLOYMENT_ID = 'fc07562a-1a27-4f66-9154-732d3b14b98a'
|
|
33
|
-
|
|
33
|
+
WEBHOOK_UUID: str = 'a5b654eb-15c8-43c8-9d50-9ba6eea9a0ff'
|
|
34
|
+
WEBHOOK_ID: int = 1
|
|
34
35
|
WEBHOOK_NAME = 'test_webhook_config_name'
|
|
35
36
|
WEBHOOK_URL = 'https://www.slack.com'
|
|
36
37
|
WEBHOOK_PROVIDER = 'SLACK'
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from fiddler.constants.model import DataType
|
|
9
|
+
from fiddler.schemas.server_info import Version
|
|
10
|
+
from fiddler.utils.helpers import group_by, try_series_retype
|
|
11
|
+
from fiddler.utils.column_generator import create_columns_from_df
|
|
12
|
+
from fiddler.utils.validations import validate_artifact_dir
|
|
13
|
+
from fiddler.utils.version import match_semver
|
|
14
|
+
|
|
15
|
+
MODEL_DATA = [{'Unnamed: 0': 43660,
|
|
16
|
+
'CreditScore': 681,
|
|
17
|
+
'Geography': 'France',
|
|
18
|
+
'Gender': 'Female',
|
|
19
|
+
'Age': 45,
|
|
20
|
+
'Tenure': 4,
|
|
21
|
+
'Balance': 150461.78248572032,
|
|
22
|
+
'NumOfProducts': 1,
|
|
23
|
+
'HasCrCard': True,
|
|
24
|
+
'IsActiveMember': True,
|
|
25
|
+
'EstimatedSalary': 121476.50483532489,
|
|
26
|
+
'Churned': 'Not Churned',
|
|
27
|
+
'probability_churned': 0.2070909278091097},
|
|
28
|
+
{'Unnamed: 0': 87278,
|
|
29
|
+
'CreditScore': 604,
|
|
30
|
+
'Geography': 'Spain',
|
|
31
|
+
'Gender': 'Male',
|
|
32
|
+
'Age': 25,
|
|
33
|
+
'Tenure': 8,
|
|
34
|
+
'Balance': 870.563948633173,
|
|
35
|
+
'NumOfProducts': 2,
|
|
36
|
+
'HasCrCard': False,
|
|
37
|
+
'IsActiveMember': True,
|
|
38
|
+
'EstimatedSalary': 39030.86584360056,
|
|
39
|
+
'Churned': 'Not Churned',
|
|
40
|
+
'probability_churned': 0.0619905773329336},
|
|
41
|
+
{'Unnamed: 0': 14317,
|
|
42
|
+
'CreditScore': 648,
|
|
43
|
+
'Geography': 'France',
|
|
44
|
+
'Gender': 'Female',
|
|
45
|
+
'Age': 50,
|
|
46
|
+
'Tenure': 6,
|
|
47
|
+
'Balance': 127661.4013780268,
|
|
48
|
+
'NumOfProducts': 1,
|
|
49
|
+
'HasCrCard': True,
|
|
50
|
+
'IsActiveMember': True,
|
|
51
|
+
'EstimatedSalary': -8216.031548157727,
|
|
52
|
+
'Churned': 'Churned',
|
|
53
|
+
'probability_churned': 0.1026814066436183},
|
|
54
|
+
{'Unnamed: 0': 81932,
|
|
55
|
+
'CreditScore': 670,
|
|
56
|
+
'Geography': 'France',
|
|
57
|
+
'Gender': 'Male',
|
|
58
|
+
'Age': 49,
|
|
59
|
+
'Tenure': 7,
|
|
60
|
+
'Balance': 95199.48061342211,
|
|
61
|
+
'NumOfProducts': 3,
|
|
62
|
+
'HasCrCard': False,
|
|
63
|
+
'IsActiveMember': True,
|
|
64
|
+
'EstimatedSalary': 47114.97249302002,
|
|
65
|
+
'Churned': 'Not Churned',
|
|
66
|
+
'probability_churned': 0.2134035806234498},
|
|
67
|
+
{'Unnamed: 0': 95321,
|
|
68
|
+
'CreditScore': 616,
|
|
69
|
+
'Geography': 'Germany',
|
|
70
|
+
'Gender': 'Female',
|
|
71
|
+
'Age': 35,
|
|
72
|
+
'Tenure': 1,
|
|
73
|
+
'Balance': 88151.28675933508,
|
|
74
|
+
'NumOfProducts': 1,
|
|
75
|
+
'HasCrCard': True,
|
|
76
|
+
'IsActiveMember': False,
|
|
77
|
+
'EstimatedSalary': 107882.06957422369,
|
|
78
|
+
'Churned': 'Churned',
|
|
79
|
+
'probability_churned': 0.8824657828379177},
|
|
80
|
+
{'Unnamed: 0': 5405,
|
|
81
|
+
'CreditScore': 536,
|
|
82
|
+
'Geography': 'France',
|
|
83
|
+
'Gender': 'Female',
|
|
84
|
+
'Age': 41,
|
|
85
|
+
'Tenure': 4,
|
|
86
|
+
'Balance': 167680.40434914205,
|
|
87
|
+
'NumOfProducts': 1,
|
|
88
|
+
'HasCrCard': True,
|
|
89
|
+
'IsActiveMember': True,
|
|
90
|
+
'EstimatedSalary': 133588.33926550962,
|
|
91
|
+
'Churned': 'Not Churned',
|
|
92
|
+
'probability_churned': 0.1189636863349831},
|
|
93
|
+
{'Unnamed: 0': 33188,
|
|
94
|
+
'CreditScore': 666,
|
|
95
|
+
'Geography': 'Spain',
|
|
96
|
+
'Gender': 'Male',
|
|
97
|
+
'Age': 34,
|
|
98
|
+
'Tenure': 2,
|
|
99
|
+
'Balance': 120370.38776657668,
|
|
100
|
+
'NumOfProducts': 2,
|
|
101
|
+
'HasCrCard': False,
|
|
102
|
+
'IsActiveMember': True,
|
|
103
|
+
'EstimatedSalary': 60070.356958972116,
|
|
104
|
+
'Churned': 'Not Churned',
|
|
105
|
+
'probability_churned': 0.1244325159224678},
|
|
106
|
+
{'Unnamed: 0': 63421,
|
|
107
|
+
'CreditScore': 613,
|
|
108
|
+
'Geography': 'France',
|
|
109
|
+
'Gender': 'Male',
|
|
110
|
+
'Age': 35,
|
|
111
|
+
'Tenure': 2,
|
|
112
|
+
'Balance': 1632.9882268964022,
|
|
113
|
+
'NumOfProducts': 2,
|
|
114
|
+
'HasCrCard': True,
|
|
115
|
+
'IsActiveMember': True,
|
|
116
|
+
'EstimatedSalary': 77206.59807746019,
|
|
117
|
+
'Churned': 'Not Churned',
|
|
118
|
+
'probability_churned': 0.1037151191218035},
|
|
119
|
+
{'Unnamed: 0': 72897,
|
|
120
|
+
'CreditScore': 591,
|
|
121
|
+
'Geography': 'Germany',
|
|
122
|
+
'Gender': 'Male',
|
|
123
|
+
'Age': 85,
|
|
124
|
+
'Tenure': 4,
|
|
125
|
+
'Balance': 115817.11190439922,
|
|
126
|
+
'NumOfProducts': 1,
|
|
127
|
+
'HasCrCard': False,
|
|
128
|
+
'IsActiveMember': False,
|
|
129
|
+
'EstimatedSalary': 23310.677701467852,
|
|
130
|
+
'Churned': 'Not Churned',
|
|
131
|
+
'probability_churned': 0.2658220429852892},
|
|
132
|
+
{'Unnamed: 0': 9507,
|
|
133
|
+
'CreditScore': 488,
|
|
134
|
+
'Geography': 'Spain',
|
|
135
|
+
'Gender': 'Female',
|
|
136
|
+
'Age': 40,
|
|
137
|
+
'Tenure': 8,
|
|
138
|
+
'Balance': 101678.5269411524,
|
|
139
|
+
'NumOfProducts': 1,
|
|
140
|
+
'HasCrCard': True,
|
|
141
|
+
'IsActiveMember': False,
|
|
142
|
+
'EstimatedSalary': 9013.186551877934,
|
|
143
|
+
'Churned': 'Not Churned',
|
|
144
|
+
'probability_churned': 0.1167422545893403}
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
def test_match_semvar_version() -> None:
|
|
148
|
+
assert match_semver(None, '>=22.9.0') is False
|
|
149
|
+
assert match_semver(Version.parse('22.9.0'), '>=22.10.0') is False
|
|
150
|
+
assert match_semver(Version.parse('22.10.0'), '>=22.10.0') is True
|
|
151
|
+
assert match_semver(Version.parse('22.10.0'), '>22.10.0') is False
|
|
152
|
+
assert match_semver(Version.parse('22.11.0'), '>=22.10.0') is True
|
|
153
|
+
assert match_semver(Version.parse('22.11.0'), '>22.10.0') is True
|
|
154
|
+
assert match_semver(Version.parse('22.10.0'), '<=22.10.0') is True
|
|
155
|
+
assert match_semver(Version.parse('22.10.0'), '<22.10.0') is False
|
|
156
|
+
assert match_semver(Version.parse('22.9.0'), '<22.10.0') is True
|
|
157
|
+
assert match_semver(Version.parse('22.11.0-RC1'), '>=22.11.0') is True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_validate_artifact_dir(tmp_path) -> None:
|
|
161
|
+
artifact_dir = os.path.join(Path(__file__).resolve().parent, 'artifact_test_dir')
|
|
162
|
+
assert validate_artifact_dir(Path(artifact_dir)) is None
|
|
163
|
+
# Test for artifact_dir not valid directory
|
|
164
|
+
with pytest.raises(ValueError):
|
|
165
|
+
validate_artifact_dir(Path('test'))
|
|
166
|
+
# Test for package.py file not found
|
|
167
|
+
mock_dir = tmp_path / 'test'
|
|
168
|
+
mock_dir.mkdir()
|
|
169
|
+
with pytest.raises(ValueError):
|
|
170
|
+
validate_artifact_dir(Path(mock_dir))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_group_by_helper() -> None:
|
|
174
|
+
df = pd.DataFrame(
|
|
175
|
+
[
|
|
176
|
+
{'col1': 1, 'col2': 'foo'},
|
|
177
|
+
{'col1': 2, 'col2': 'bar'},
|
|
178
|
+
{'col1': 3, 'col2': 'baz'},
|
|
179
|
+
{'col1': 3, 'col2': 'foo'},
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# with output_path
|
|
184
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
185
|
+
file_path = Path(temp_dir) / 'data.csv'
|
|
186
|
+
|
|
187
|
+
assert file_path.exists() is False
|
|
188
|
+
group_by(df=df, group_by_col='col2', output_path=file_path)
|
|
189
|
+
assert file_path.exists() is True
|
|
190
|
+
|
|
191
|
+
# without output path
|
|
192
|
+
grouped_df = group_by(df=df, group_by_col='col2')
|
|
193
|
+
assert grouped_df.equals(
|
|
194
|
+
pd.DataFrame(
|
|
195
|
+
[
|
|
196
|
+
{'col2': 'foo', 'col1': [1, 3]},
|
|
197
|
+
{'col2': 'bar', 'col1': [2]},
|
|
198
|
+
{'col2': 'baz', 'col1': [3]},
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_try_series_retype() -> None:
|
|
205
|
+
series = pd.Series([1, 2, 3], dtype='float')
|
|
206
|
+
series = try_series_retype(series, 'int')
|
|
207
|
+
assert series.dtype == 'int'
|
|
208
|
+
|
|
209
|
+
series = pd.Series([1, 2, None])
|
|
210
|
+
series = try_series_retype(series, 'int')
|
|
211
|
+
assert series.dtype == 'float'
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_try_series_retype_str_or_unkown() -> None:
|
|
215
|
+
series = pd.Series(['HIGH', 'MEDIUM', '', None], dtype='str')
|
|
216
|
+
series = try_series_retype(series, 'str')
|
|
217
|
+
assert series.dtype == 'object'
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_try_series_retype_timestamp() -> None:
|
|
221
|
+
series = pd.Series(
|
|
222
|
+
['2023-11-12 09:15:32.23', '2023-12-11 09:15:32.45'], dtype='str'
|
|
223
|
+
)
|
|
224
|
+
series = try_series_retype(series, 'timestamp')
|
|
225
|
+
assert series.dtype == 'datetime64[ns]'
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_try_series_retype_timestamp_error() -> None:
|
|
229
|
+
series = pd.Series(['2023-11-12 09:15:32.23', None], dtype='str')
|
|
230
|
+
|
|
231
|
+
with pytest.raises(TypeError):
|
|
232
|
+
try_series_retype(series, 'timestamp')
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_create_columns_from_df() -> None:
|
|
236
|
+
# test with an empty dataframe
|
|
237
|
+
df = pd.DataFrame()
|
|
238
|
+
with pytest.raises(ValueError):
|
|
239
|
+
create_columns_from_df(df)
|
|
240
|
+
|
|
241
|
+
# test with a dataframe from dictionary
|
|
242
|
+
df = pd.DataFrame(MODEL_DATA)
|
|
243
|
+
|
|
244
|
+
df['Gender'] = df['Gender'].astype('category')
|
|
245
|
+
df['Churned'] = df['Churned'].astype('category')
|
|
246
|
+
|
|
247
|
+
model_schema = create_columns_from_df(df)
|
|
248
|
+
columns = model_schema.columns
|
|
249
|
+
assert len(columns) == 13
|
|
250
|
+
assert columns[0].name == 'Unnamed: 0'
|
|
251
|
+
assert columns[0].data_type == DataType.INTEGER
|
|
252
|
+
|
|
253
|
+
assert columns[1].name == 'CreditScore'
|
|
254
|
+
assert columns[1].data_type == DataType.INTEGER
|
|
255
|
+
|
|
256
|
+
assert columns[2].name == 'Geography'
|
|
257
|
+
assert columns[2].data_type == DataType.STRING
|
|
258
|
+
|
|
259
|
+
assert columns[3].name == 'Gender'
|
|
260
|
+
assert columns[3].data_type == DataType.CATEGORY
|
|
261
|
+
|
|
262
|
+
assert columns[4].name == 'Age'
|
|
263
|
+
assert columns[4].data_type == DataType.INTEGER
|
|
264
|
+
|
|
265
|
+
assert columns[5].name == 'Tenure'
|
|
266
|
+
assert columns[5].data_type == DataType.INTEGER
|
|
267
|
+
|
|
268
|
+
assert columns[6].name == 'Balance'
|
|
269
|
+
assert columns[6].data_type == DataType.FLOAT
|
|
270
|
+
|
|
271
|
+
assert columns[7].name == 'NumOfProducts'
|
|
272
|
+
assert columns[7].data_type == DataType.INTEGER
|
|
273
|
+
|
|
274
|
+
assert columns[8].name == 'HasCrCard'
|
|
275
|
+
assert columns[8].data_type == DataType.BOOLEAN
|
|
276
|
+
|
|
277
|
+
assert columns[9].name == 'IsActiveMember'
|
|
278
|
+
assert columns[9].data_type == DataType.BOOLEAN
|
|
279
|
+
|
|
280
|
+
assert columns[10].name == 'EstimatedSalary'
|
|
281
|
+
assert columns[10].data_type == DataType.FLOAT
|
|
282
|
+
|
|
283
|
+
assert columns[11].name == 'Churned'
|
|
284
|
+
assert columns[11].data_type == DataType.CATEGORY
|
|
285
|
+
|
|
286
|
+
assert columns[12].name == 'probability_churned'
|
|
287
|
+
assert columns[12].data_type == DataType.FLOAT
|
|
288
|
+
|
|
289
|
+
def test_numeric_type_casting():
|
|
290
|
+
# Create a DataFrame with various integer types
|
|
291
|
+
df = pd.DataFrame({
|
|
292
|
+
'int8_col': pd.Series([1, 2, 3], dtype='int8'),
|
|
293
|
+
'int16_col': pd.Series([1000, 2000, 3000], dtype='int16'),
|
|
294
|
+
'int32_col': pd.Series([100000, 200000, 300000], dtype='int32'),
|
|
295
|
+
'int64_col': pd.Series([10000000000, 20000000000, 30000000000], dtype='int64'),
|
|
296
|
+
'uint8_col': pd.Series([1, 2, 3], dtype='uint8'),
|
|
297
|
+
'uint16_col': pd.Series([1000, 2000, 3000], dtype='uint16'),
|
|
298
|
+
'uint32_col': pd.Series([100000, 200000, 300000], dtype='uint32'),
|
|
299
|
+
'uint64_col': pd.Series([10000000000, 20000000000, 30000000000], dtype='uint64'),
|
|
300
|
+
|
|
301
|
+
# Float types
|
|
302
|
+
'float16_col': pd.Series([1.0, 2.0, 3.0], dtype='float16'),
|
|
303
|
+
'float32_col': pd.Series([1.234, 2.345, 3.456], dtype='float32'),
|
|
304
|
+
'float64_col': pd.Series([1.23456789, 2.34567890, 3.45678901], dtype='float64'),
|
|
305
|
+
|
|
306
|
+
# Control columns
|
|
307
|
+
'string_col': pd.Series(['1', '2', '3'], dtype='string'),
|
|
308
|
+
'bool_col': pd.Series([True, False, True], dtype='bool')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
# Generate columns from DataFrame
|
|
312
|
+
columns = create_columns_from_df(df).columns
|
|
313
|
+
|
|
314
|
+
# Create a mapping of column names to their expected data types
|
|
315
|
+
expected_types = {
|
|
316
|
+
'int8_col': DataType.INTEGER,
|
|
317
|
+
'int16_col': DataType.INTEGER,
|
|
318
|
+
'int32_col': DataType.INTEGER,
|
|
319
|
+
'int64_col': DataType.INTEGER,
|
|
320
|
+
'uint8_col': DataType.INTEGER,
|
|
321
|
+
'uint16_col': DataType.INTEGER,
|
|
322
|
+
'uint32_col': DataType.INTEGER,
|
|
323
|
+
'uint64_col': DataType.INTEGER,
|
|
324
|
+
|
|
325
|
+
# Float columns
|
|
326
|
+
'float16_col': DataType.FLOAT,
|
|
327
|
+
'float32_col': DataType.FLOAT,
|
|
328
|
+
'float64_col': DataType.FLOAT,
|
|
329
|
+
|
|
330
|
+
# control columns
|
|
331
|
+
'string_col': DataType.STRING,
|
|
332
|
+
'bool_col': DataType.BOOLEAN
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Verify each column's data type
|
|
336
|
+
for column in columns:
|
|
337
|
+
assert column.data_type == expected_types[column.name], \
|
|
338
|
+
f"Column {column.name} should be {expected_types[column.name]}, but got {column.data_type}"
|
|
339
|
+
|
|
340
|
+
# Verify min and max values for numeric columns
|
|
341
|
+
for col_name in df.select_dtypes(include=['int', 'float']).columns:
|
|
342
|
+
col = next(c for c in columns if c.name == col_name)
|
|
343
|
+
assert col.min == df[col_name].min(), \
|
|
344
|
+
f"Min value mismatch for {col_name}: expected {df[col_name].min()}, got {col.min}"
|
|
345
|
+
assert col.max == df[col_name].max(), \
|
|
346
|
+
f"Max value mismatch for {col_name}: expected {df[col_name].max()}, got {col.max}"
|
|
347
|
+
|
|
348
|
+
def test_integer_edge_cases():
|
|
349
|
+
# Test with empty DataFrame
|
|
350
|
+
empty_df = pd.DataFrame({
|
|
351
|
+
'int_col': pd.Series([], dtype='int64')
|
|
352
|
+
})
|
|
353
|
+
columns = create_columns_from_df(empty_df).columns
|
|
354
|
+
assert len(columns) == 1
|
|
355
|
+
assert columns[0].data_type == DataType.INTEGER
|
|
356
|
+
|
|
357
|
+
# Test with NaN values
|
|
358
|
+
df_with_nan = pd.DataFrame({
|
|
359
|
+
'int_col': pd.Series([1, 2, np.nan], dtype='float64') # NaN forces float dtype
|
|
360
|
+
})
|
|
361
|
+
columns = create_columns_from_df(df_with_nan).columns
|
|
362
|
+
assert len(columns) == 1
|
|
363
|
+
assert columns[0].data_type == DataType.FLOAT # Should be float due to NaN
|
|
364
|
+
|
|
365
|
+
# Test with mixed types
|
|
366
|
+
df_mixed = pd.DataFrame({
|
|
367
|
+
'mixed_col': pd.Series([1, '2', 3], dtype='object')
|
|
368
|
+
})
|
|
369
|
+
columns = create_columns_from_df(df_mixed).columns
|
|
370
|
+
assert len(columns) == 1
|
|
371
|
+
assert columns[0].data_type == DataType.STRING # Should be string due to mixed types
|
|
372
|
+
|
|
373
|
+
def test_float_edge_cases():
|
|
374
|
+
# Test with NaN values
|
|
375
|
+
df_nan = pd.DataFrame({
|
|
376
|
+
'float_nan': pd.Series([1.0, np.nan, 3.0], dtype='float64')
|
|
377
|
+
})
|
|
378
|
+
columns = create_columns_from_df(df_nan).columns
|
|
379
|
+
assert len(columns) == 1
|
|
380
|
+
assert columns[0].data_type == DataType.FLOAT
|
|
381
|
+
assert columns[0].min == 1.0
|
|
382
|
+
assert columns[0].max == 3.0
|
|
383
|
+
|
|
384
|
+
# Test with infinity
|
|
385
|
+
df_inf = pd.DataFrame({
|
|
386
|
+
'float_inf': pd.Series([1.0, np.inf, -np.inf], dtype='float64')
|
|
387
|
+
})
|
|
388
|
+
columns = create_columns_from_df(df_inf).columns
|
|
389
|
+
assert len(columns) == 1
|
|
390
|
+
assert columns[0].data_type == DataType.FLOAT
|
|
391
|
+
assert columns[0].min == -np.inf
|
|
392
|
+
assert columns[0].max == np.inf
|
|
393
|
+
|
|
394
|
+
# Test with very small numbers
|
|
395
|
+
df_small = pd.DataFrame({
|
|
396
|
+
'float_small': pd.Series([1e-10, 1e-20, 1e-30], dtype='float64')
|
|
397
|
+
})
|
|
398
|
+
columns = create_columns_from_df(df_small).columns
|
|
399
|
+
assert len(columns) == 1
|
|
400
|
+
assert columns[0].data_type == DataType.FLOAT
|
|
401
|
+
assert columns[0].min == 1e-30
|
|
402
|
+
assert columns[0].max == 1e-10
|
|
403
|
+
|
|
404
|
+
# Test with very large numbers
|
|
405
|
+
df_large = pd.DataFrame({
|
|
406
|
+
'float_large': pd.Series([1e10, 1e20, 1e30], dtype='float64')
|
|
407
|
+
})
|
|
408
|
+
columns = create_columns_from_df(df_large).columns
|
|
409
|
+
assert len(columns) == 1
|
|
410
|
+
assert columns[0].data_type == DataType.FLOAT
|
|
411
|
+
assert columns[0].min == 1e10
|
|
412
|
+
assert columns[0].max == 1e30
|
|
413
|
+
|
|
414
|
+
def test_mixed_numeric_types():
|
|
415
|
+
# Test with mixed integer and float values
|
|
416
|
+
df_mixed = pd.DataFrame({
|
|
417
|
+
'mixed_numeric': pd.Series([1, 2.0, 3], dtype='float64')
|
|
418
|
+
})
|
|
419
|
+
columns = create_columns_from_df(df_mixed).columns
|
|
420
|
+
assert len(columns) == 1
|
|
421
|
+
assert columns[0].data_type == DataType.FLOAT
|
|
422
|
+
assert columns[0].min == 1.0
|
|
423
|
+
assert columns[0].max == 3.0
|
|
424
|
+
|
|
425
|
+
# Test with mixed numeric and string values
|
|
426
|
+
df_mixed_str = pd.DataFrame({
|
|
427
|
+
'mixed_str': pd.Series([1, '2.0', 3], dtype='object')
|
|
428
|
+
})
|
|
429
|
+
columns = create_columns_from_df(df_mixed_str).columns
|
|
430
|
+
assert len(columns) == 1
|
|
431
|
+
assert columns[0].data_type == DataType.STRING # Should be string due to mixed types
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from fiddler.schemas.model_schema import Column, ModelSchema
|
|
4
|
+
from fiddler.constants.model import DataType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_columns_from_df(df: pd.DataFrame) -> ModelSchema:
|
|
8
|
+
"""
|
|
9
|
+
Helper function to create Columns from a pandas DataFrame column dtypes.
|
|
10
|
+
timedelta, period, interval & object dtypes are converted to string.
|
|
11
|
+
Sparse dtypes are not handled.
|
|
12
|
+
Args:
|
|
13
|
+
df: Input pandas DataFrame
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
ModelSchema
|
|
17
|
+
"""
|
|
18
|
+
columns: List[Column] = []
|
|
19
|
+
|
|
20
|
+
# Check if the DataFrame is empty - a dataframe with columns but no rows is valid
|
|
21
|
+
if len(df.columns) == 0:
|
|
22
|
+
raise ValueError("Cannot create columns from an empty DataFrame")
|
|
23
|
+
|
|
24
|
+
for col_name, dtype in df.dtypes.items():
|
|
25
|
+
# Map pandas dtypes to Fiddler DataTypes
|
|
26
|
+
if pd.api.types.is_float_dtype(dtype):
|
|
27
|
+
data_type = DataType.FLOAT
|
|
28
|
+
col_min, col_max = float(df[col_name].min()), float(df[col_name].max())
|
|
29
|
+
col = Column(
|
|
30
|
+
name=col_name,
|
|
31
|
+
data_type=data_type,
|
|
32
|
+
min=col_min,
|
|
33
|
+
max=col_max
|
|
34
|
+
)
|
|
35
|
+
elif pd.api.types.is_integer_dtype(dtype):
|
|
36
|
+
data_type = DataType.INTEGER
|
|
37
|
+
col = Column(
|
|
38
|
+
name=col_name,
|
|
39
|
+
data_type=data_type,
|
|
40
|
+
min=df[col_name].min(),
|
|
41
|
+
max=df[col_name].max()
|
|
42
|
+
)
|
|
43
|
+
elif pd.api.types.is_bool_dtype(dtype):
|
|
44
|
+
col = Column(
|
|
45
|
+
name=col_name,
|
|
46
|
+
data_type=DataType.BOOLEAN
|
|
47
|
+
)
|
|
48
|
+
elif pd.api.types.is_datetime64_any_dtype(dtype):
|
|
49
|
+
col = Column(
|
|
50
|
+
name=col_name,
|
|
51
|
+
data_type=DataType.TIMESTAMP
|
|
52
|
+
)
|
|
53
|
+
elif pd.api.types.is_categorical_dtype(dtype):
|
|
54
|
+
col = Column(
|
|
55
|
+
name=col_name,
|
|
56
|
+
data_type=DataType.CATEGORY,
|
|
57
|
+
categories=df[col_name].cat.categories.tolist()
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
# Default to string for other types
|
|
61
|
+
col = Column(
|
|
62
|
+
name=col_name,
|
|
63
|
+
data_type=DataType.STRING
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
columns.append(col)
|
|
67
|
+
|
|
68
|
+
return ModelSchema(columns=columns)
|
|
@@ -103,6 +103,7 @@ fiddler/tests/apis/test_segment.py
|
|
|
103
103
|
fiddler/tests/apis/test_webhook.py
|
|
104
104
|
fiddler/tests/apis/test_xai.py
|
|
105
105
|
fiddler/utils/__init__.py
|
|
106
|
+
fiddler/utils/column_generator.py
|
|
106
107
|
fiddler/utils/decorators.py
|
|
107
108
|
fiddler/utils/helpers.py
|
|
108
109
|
fiddler/utils/logger.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.8.2
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import tempfile
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import pandas as pd
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from fiddler.schemas.server_info import Version
|
|
9
|
-
from fiddler.utils.helpers import group_by, try_series_retype
|
|
10
|
-
from fiddler.utils.validations import validate_artifact_dir
|
|
11
|
-
from fiddler.utils.version import match_semver
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_match_semvar_version() -> None:
|
|
15
|
-
assert match_semver(None, '>=22.9.0') is False
|
|
16
|
-
assert match_semver(Version.parse('22.9.0'), '>=22.10.0') is False
|
|
17
|
-
assert match_semver(Version.parse('22.10.0'), '>=22.10.0') is True
|
|
18
|
-
assert match_semver(Version.parse('22.10.0'), '>22.10.0') is False
|
|
19
|
-
assert match_semver(Version.parse('22.11.0'), '>=22.10.0') is True
|
|
20
|
-
assert match_semver(Version.parse('22.11.0'), '>22.10.0') is True
|
|
21
|
-
assert match_semver(Version.parse('22.10.0'), '<=22.10.0') is True
|
|
22
|
-
assert match_semver(Version.parse('22.10.0'), '<22.10.0') is False
|
|
23
|
-
assert match_semver(Version.parse('22.9.0'), '<22.10.0') is True
|
|
24
|
-
assert match_semver(Version.parse('22.11.0-RC1'), '>=22.11.0') is True
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_validate_artifact_dir(tmp_path) -> None:
|
|
28
|
-
artifact_dir = os.path.join(Path(__file__).resolve().parent, 'artifact_test_dir')
|
|
29
|
-
assert validate_artifact_dir(Path(artifact_dir)) is None
|
|
30
|
-
# Test for artifact_dir not valid directory
|
|
31
|
-
with pytest.raises(ValueError):
|
|
32
|
-
validate_artifact_dir(Path('test'))
|
|
33
|
-
# Test for package.py file not found
|
|
34
|
-
mock_dir = tmp_path / 'test'
|
|
35
|
-
mock_dir.mkdir()
|
|
36
|
-
with pytest.raises(ValueError):
|
|
37
|
-
validate_artifact_dir(Path(mock_dir))
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_group_by_helper() -> None:
|
|
41
|
-
df = pd.DataFrame(
|
|
42
|
-
[
|
|
43
|
-
{'col1': 1, 'col2': 'foo'},
|
|
44
|
-
{'col1': 2, 'col2': 'bar'},
|
|
45
|
-
{'col1': 3, 'col2': 'baz'},
|
|
46
|
-
{'col1': 3, 'col2': 'foo'},
|
|
47
|
-
]
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# with output_path
|
|
51
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
52
|
-
file_path = Path(temp_dir) / 'data.csv'
|
|
53
|
-
|
|
54
|
-
assert file_path.exists() is False
|
|
55
|
-
group_by(df=df, group_by_col='col2', output_path=file_path)
|
|
56
|
-
assert file_path.exists() is True
|
|
57
|
-
|
|
58
|
-
# without output path
|
|
59
|
-
grouped_df = group_by(df=df, group_by_col='col2')
|
|
60
|
-
assert grouped_df.equals(
|
|
61
|
-
pd.DataFrame(
|
|
62
|
-
[
|
|
63
|
-
{'col2': 'foo', 'col1': [1, 3]},
|
|
64
|
-
{'col2': 'bar', 'col1': [2]},
|
|
65
|
-
{'col2': 'baz', 'col1': [3]},
|
|
66
|
-
]
|
|
67
|
-
)
|
|
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')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fiddler-client-3.8.2 → fiddler-client-3.9.0}/fiddler/packtools/project_attributions_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|