fiddler-client 2.3.0.dev2__py3-none-any.whl → 2.4.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fiddler/__init__.py +2 -0
- fiddler/_version.py +1 -1
- fiddler/api/alert_mixin.py +1 -1
- fiddler/api/api.py +2 -0
- fiddler/api/webhooks_mixin.py +9 -7
- fiddler/core_objects.py +98 -47
- fiddler/schemas/custom_features.py +25 -19
- fiddler3/constants/baseline.py +19 -0
- fiddler3/constants/common.py +12 -0
- fiddler3/constants/dataset.py +7 -0
- fiddler3/constants/model_deployment.py +15 -0
- fiddler3/constants/xai.py +11 -0
- fiddler3/entities/__init__.py +5 -1
- fiddler3/entities/base.py +22 -18
- fiddler3/entities/baseline.py +228 -0
- fiddler3/entities/dataset.py +149 -0
- fiddler3/entities/job.py +9 -8
- fiddler3/entities/model.py +160 -29
- fiddler3/entities/model_artifact.py +350 -0
- fiddler3/entities/model_deployment.py +112 -0
- fiddler3/entities/model_surrogate.py +92 -0
- fiddler3/entities/organization.py +38 -0
- fiddler3/entities/project.py +69 -8
- fiddler3/entities/user.py +45 -0
- fiddler3/entities/xai.py +309 -0
- fiddler3/libs/http_client.py +24 -13
- fiddler3/libs/json_encoder.py +12 -0
- fiddler3/schemas/__init__.py +4 -0
- fiddler3/schemas/baseline.py +37 -0
- fiddler3/schemas/custom_features.py +23 -18
- fiddler3/schemas/dataset.py +27 -0
- fiddler3/schemas/deployment_params.py +25 -0
- fiddler3/schemas/filter_query.py +54 -0
- fiddler3/schemas/job.py +2 -2
- fiddler3/schemas/model.py +9 -9
- fiddler3/schemas/model_artifact.py +6 -0
- fiddler3/schemas/model_deployment.py +26 -0
- fiddler3/schemas/organization.py +9 -1
- fiddler3/schemas/project.py +4 -4
- fiddler3/schemas/server_info.py +2 -2
- fiddler3/schemas/user.py +1 -1
- fiddler3/schemas/xai.py +32 -0
- fiddler3/tests/apis/test_baseline.py +292 -0
- fiddler3/tests/apis/test_dataset.py +173 -0
- fiddler3/tests/apis/test_mixin.py +77 -0
- fiddler3/tests/apis/test_model.py +102 -7
- fiddler3/tests/apis/test_model_artifact.py +175 -0
- fiddler3/tests/apis/test_model_deployment.py +98 -0
- fiddler3/tests/apis/test_model_surrogate.py +159 -0
- fiddler3/tests/apis/test_project.py +36 -20
- fiddler3/tests/apis/test_xai.py +690 -0
- fiddler3/tests/constants.py +10 -0
- fiddler3/tests/test_json_encoder.py +16 -0
- fiddler3/tests/test_logger.py +12 -0
- fiddler3/tests/test_utils.py +19 -0
- fiddler3/utils/__init__.py +1 -0
- fiddler3/utils/helpers.py +43 -0
- fiddler3/utils/logger.py +15 -0
- fiddler3/utils/validations.py +11 -0
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/METADATA +7 -5
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/RECORD +65 -33
- tests/fiddler/test_segments.py +227 -0
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/LICENSE.txt +0 -0
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/WHEEL +0 -0
- {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from fiddler3.constants.dataset import EnvType
|
|
6
|
+
from fiddler3.entities.baseline import BaselineCompact, BaselineCompactMixin
|
|
7
|
+
from fiddler3.entities.dataset import DatasetCompact, DatasetCompactMixin
|
|
8
|
+
from fiddler3.entities.model import ModelCompact, ModelCompactMixin
|
|
9
|
+
from fiddler3.entities.project import ProjectCompact, ProjectCompactMixin
|
|
10
|
+
from fiddler3.entities.user import CreatedByMixin, UpdatedByMixin, UserCompact
|
|
11
|
+
from fiddler3.schemas.baseline import BaselineCompactResp
|
|
12
|
+
from fiddler3.schemas.dataset import DatasetCompactResp
|
|
13
|
+
from fiddler3.schemas.model import ModelCompactResp
|
|
14
|
+
from fiddler3.schemas.project import ProjectCompactResp
|
|
15
|
+
from fiddler3.schemas.user import UserCompactResp
|
|
16
|
+
from fiddler3.tests.constants import (
|
|
17
|
+
BASELINE_ID,
|
|
18
|
+
BASELINE_NAME,
|
|
19
|
+
DATASET_ID,
|
|
20
|
+
DATASET_NAME,
|
|
21
|
+
MODEL_ID,
|
|
22
|
+
MODEL_NAME,
|
|
23
|
+
PROJECT_ID,
|
|
24
|
+
PROJECT_NAME,
|
|
25
|
+
USER_EMAIL,
|
|
26
|
+
USER_ID,
|
|
27
|
+
USER_NAME,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DummyEntity(
|
|
32
|
+
ModelCompactMixin,
|
|
33
|
+
ProjectCompactMixin,
|
|
34
|
+
DatasetCompactMixin,
|
|
35
|
+
BaselineCompactMixin,
|
|
36
|
+
CreatedByMixin,
|
|
37
|
+
UpdatedByMixin,
|
|
38
|
+
):
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self._resp = mock.MagicMock()
|
|
41
|
+
self._resp.project = ProjectCompactResp(id=PROJECT_ID, name=PROJECT_NAME)
|
|
42
|
+
self._resp.model = ModelCompactResp(id=MODEL_ID, name=MODEL_NAME)
|
|
43
|
+
self._resp.dataset = DatasetCompactResp(
|
|
44
|
+
id=DATASET_ID, name=DATASET_NAME, type=EnvType.PRE_PRODUCTION
|
|
45
|
+
)
|
|
46
|
+
self._resp.baseline = BaselineCompactResp(id=BASELINE_ID, name=BASELINE_NAME)
|
|
47
|
+
self._resp.created_by = UserCompactResp(
|
|
48
|
+
id=USER_ID, full_name=USER_NAME, email=USER_EMAIL
|
|
49
|
+
)
|
|
50
|
+
self._resp.updated_by = UserCompactResp(
|
|
51
|
+
id=USER_ID, full_name=USER_NAME, email=USER_EMAIL
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture()
|
|
56
|
+
def entity() -> DummyEntity:
|
|
57
|
+
dummy_entity = DummyEntity()
|
|
58
|
+
yield dummy_entity
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize(
|
|
62
|
+
('property_name', 'class_name'),
|
|
63
|
+
[
|
|
64
|
+
('project', ProjectCompact),
|
|
65
|
+
('dataset', DatasetCompact),
|
|
66
|
+
('model', ModelCompact),
|
|
67
|
+
('baseline', BaselineCompact),
|
|
68
|
+
('created_by', UserCompact),
|
|
69
|
+
('updated_by', UserCompact),
|
|
70
|
+
],
|
|
71
|
+
)
|
|
72
|
+
def test_mixins(entity: DummyEntity, property_name: str, class_name: type) -> None:
|
|
73
|
+
assert isinstance(getattr(entity, property_name), class_name)
|
|
74
|
+
|
|
75
|
+
entity._resp = None
|
|
76
|
+
with pytest.raises(AttributeError):
|
|
77
|
+
_ = getattr(entity, property_name)
|
|
@@ -3,10 +3,11 @@ from uuid import UUID
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
import responses
|
|
6
|
+
from pytest_mock import MockerFixture
|
|
6
7
|
from responses import matchers
|
|
7
8
|
|
|
8
9
|
from fiddler3.entities.job import Job
|
|
9
|
-
from fiddler3.entities.model import Model
|
|
10
|
+
from fiddler3.entities.model import Model, ModelCompact
|
|
10
11
|
from fiddler3.exceptions import Conflict, NotFound
|
|
11
12
|
from fiddler3.schemas.model_schema import ModelSchema
|
|
12
13
|
from fiddler3.schemas.model_spec import ModelSpec
|
|
@@ -235,14 +236,27 @@ API_RESPONSE_404 = {
|
|
|
235
236
|
API_RESPONSE_FROM_NAME = {
|
|
236
237
|
'data': {
|
|
237
238
|
'page_size': 100,
|
|
238
|
-
'total':
|
|
239
|
-
'item_count':
|
|
239
|
+
'total': 1,
|
|
240
|
+
'item_count': 1,
|
|
240
241
|
'page_count': 1,
|
|
241
242
|
'page_index': 1,
|
|
242
243
|
'offset': 0,
|
|
243
244
|
'items': [API_RESPONSE_200['data']],
|
|
244
245
|
}
|
|
245
246
|
}
|
|
247
|
+
LIST_API_RESPONSE = {
|
|
248
|
+
'data': {
|
|
249
|
+
'page_size': 100,
|
|
250
|
+
'total': 1,
|
|
251
|
+
'item_count': 1,
|
|
252
|
+
'page_count': 1,
|
|
253
|
+
'page_index': 1,
|
|
254
|
+
'offset': 0,
|
|
255
|
+
'items': [
|
|
256
|
+
API_RESPONSE_200['data'],
|
|
257
|
+
],
|
|
258
|
+
}
|
|
259
|
+
}
|
|
246
260
|
DELETE_202_RESPONSE = {
|
|
247
261
|
'data': {'job': {'id': JOB_ID, 'name': f'Deleting model {MODEL_NAME}'}},
|
|
248
262
|
'api_version': '3.0',
|
|
@@ -307,13 +321,15 @@ def test_get_model_not_found() -> None:
|
|
|
307
321
|
|
|
308
322
|
@responses.activate
|
|
309
323
|
def test_model_from_name_success() -> None:
|
|
310
|
-
params = {
|
|
324
|
+
params = {
|
|
325
|
+
'filter': '{"condition": "AND", "rules": [{"field": "name", "operator": "equal", "value": "bank_churn"}, {"field": "project_name", "operator": "equal", "value": "bank_churn"}]}'
|
|
326
|
+
}
|
|
311
327
|
responses.get(
|
|
312
328
|
url=f'{URL}/v3/models',
|
|
313
329
|
json=API_RESPONSE_FROM_NAME,
|
|
314
330
|
match=[matchers.query_param_matcher(params)],
|
|
315
331
|
)
|
|
316
|
-
model = Model.from_name(
|
|
332
|
+
model = Model.from_name(name=MODEL_NAME, project_name=PROJECT_NAME)
|
|
317
333
|
assert isinstance(model, Model)
|
|
318
334
|
|
|
319
335
|
|
|
@@ -324,7 +340,9 @@ def test_model_from_name_not_found() -> None:
|
|
|
324
340
|
resp['data']['item_count'] = 0
|
|
325
341
|
resp['data']['items'] = []
|
|
326
342
|
|
|
327
|
-
params = {
|
|
343
|
+
params = {
|
|
344
|
+
'filter': '{"condition": "AND", "rules": [{"field": "name", "operator": "equal", "value": "bank_churn"}, {"field": "project_name", "operator": "equal", "value": "bank_churn"}]}'
|
|
345
|
+
}
|
|
328
346
|
responses.get(
|
|
329
347
|
url=f'{URL}/v3/models',
|
|
330
348
|
json=resp,
|
|
@@ -332,7 +350,33 @@ def test_model_from_name_not_found() -> None:
|
|
|
332
350
|
)
|
|
333
351
|
|
|
334
352
|
with pytest.raises(NotFound):
|
|
335
|
-
Model.from_name(
|
|
353
|
+
Model.from_name(name=MODEL_NAME, project_name=PROJECT_NAME)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@responses.activate
|
|
357
|
+
def test_model_list_success() -> None:
|
|
358
|
+
params = {'project_id': str(PROJECT_ID), 'limit': 50, 'offset': 0}
|
|
359
|
+
responses.get(
|
|
360
|
+
url=f'{URL}/v3/models',
|
|
361
|
+
json=LIST_API_RESPONSE,
|
|
362
|
+
match=[matchers.query_param_matcher(params)],
|
|
363
|
+
)
|
|
364
|
+
models = Model.list(project_id=PROJECT_ID)
|
|
365
|
+
for model in models:
|
|
366
|
+
assert isinstance(model, ModelCompact)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@responses.activate
|
|
370
|
+
def test_model_list_success() -> None:
|
|
371
|
+
params = {'project_id': str(PROJECT_ID), 'limit': 50, 'offset': 0}
|
|
372
|
+
responses.get(
|
|
373
|
+
url=f'{URL}/v3/models',
|
|
374
|
+
json=LIST_API_RESPONSE,
|
|
375
|
+
match=[matchers.query_param_matcher(params)],
|
|
376
|
+
)
|
|
377
|
+
models = Model.list(project_id=PROJECT_ID)
|
|
378
|
+
for model in models:
|
|
379
|
+
assert isinstance(model, ModelCompact)
|
|
336
380
|
|
|
337
381
|
|
|
338
382
|
@responses.activate
|
|
@@ -371,3 +415,54 @@ def test_delete_model_not_found() -> None:
|
|
|
371
415
|
|
|
372
416
|
with pytest.raises(NotFound):
|
|
373
417
|
model.delete()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@responses.activate
|
|
421
|
+
def test_update_model_success(mocker) -> None:
|
|
422
|
+
responses.get(
|
|
423
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
424
|
+
json=API_RESPONSE_200,
|
|
425
|
+
)
|
|
426
|
+
model = Model.get(id_=MODEL_ID)
|
|
427
|
+
mocker.patch.dict(API_RESPONSE_200['data'], {'description': 'Test model update'})
|
|
428
|
+
|
|
429
|
+
responses.patch(
|
|
430
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
431
|
+
json=API_RESPONSE_200,
|
|
432
|
+
)
|
|
433
|
+
model.description = 'Test model update'
|
|
434
|
+
|
|
435
|
+
model.update()
|
|
436
|
+
assert model.description == 'Test model update'
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@responses.activate
|
|
440
|
+
def test_update_model_not_found() -> None:
|
|
441
|
+
responses.get(
|
|
442
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
443
|
+
json=API_RESPONSE_200,
|
|
444
|
+
)
|
|
445
|
+
model = Model.get(id_=MODEL_ID)
|
|
446
|
+
|
|
447
|
+
responses.patch(
|
|
448
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
449
|
+
json=API_RESPONSE_404,
|
|
450
|
+
status=HTTPStatus.NOT_FOUND,
|
|
451
|
+
)
|
|
452
|
+
with pytest.raises(NotFound):
|
|
453
|
+
model.update()
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@responses.activate
|
|
457
|
+
def test_datasets_property(mocker: MockerFixture) -> None:
|
|
458
|
+
mock_fn = mocker.patch('fiddler3.entities.Dataset.list')
|
|
459
|
+
|
|
460
|
+
responses.get(
|
|
461
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
462
|
+
json=API_RESPONSE_200,
|
|
463
|
+
)
|
|
464
|
+
model = Model.get(id_=MODEL_ID)
|
|
465
|
+
|
|
466
|
+
_ = list(model.datasets)
|
|
467
|
+
|
|
468
|
+
mock_fn.assert_called_with(model_id=UUID(MODEL_ID))
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tarfile
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import responses
|
|
10
|
+
|
|
11
|
+
from fiddler3.entities.job import Job
|
|
12
|
+
from fiddler3.entities.model import Model
|
|
13
|
+
from fiddler3.exceptions import NotFound
|
|
14
|
+
from fiddler3.schemas.deployment_params import DeploymentParams
|
|
15
|
+
from fiddler3.tests.apis.test_model import API_RESPONSE_200, API_RESPONSE_404
|
|
16
|
+
from fiddler3.tests.constants import JOB_ID, MODEL_ID, MODEL_NAME, PROJECT_NAME, URL
|
|
17
|
+
|
|
18
|
+
ARTIFACT_202_RESPONSE = {
|
|
19
|
+
'data': {'job': {'id': JOB_ID, 'name': 'Deploy artifact model - manual'}},
|
|
20
|
+
'api_version': '3.0',
|
|
21
|
+
'kind': 'NORMAL',
|
|
22
|
+
}
|
|
23
|
+
MODEL_ARTIFACT_JOB_API_RESPONSE_200 = {
|
|
24
|
+
'api_version': '3.0',
|
|
25
|
+
'kind': 'NORMAL',
|
|
26
|
+
'data': {
|
|
27
|
+
'name': 'Deploy artifact model - manual',
|
|
28
|
+
'id': JOB_ID,
|
|
29
|
+
'status': 'SUCCESS',
|
|
30
|
+
'progress': 100.0,
|
|
31
|
+
'error_message': None,
|
|
32
|
+
'error_reason': None,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
DOWNLOAD_RESPONSE_404 = {
|
|
37
|
+
'error': {
|
|
38
|
+
'code': 404,
|
|
39
|
+
'message': f'Model artifact not found for {PROJECT_NAME}/{MODEL_NAME}',
|
|
40
|
+
'errors': [
|
|
41
|
+
{
|
|
42
|
+
'reason': 'ObjectNotFound',
|
|
43
|
+
'message': 'Model artifact not found for {PROJECT_NAME}/{MODEL_NAME}',
|
|
44
|
+
'help': '',
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
DEPLOYMENT_PARAMS = {'deployment_type': 'MANUAL', 'cpu': 1000}
|
|
50
|
+
|
|
51
|
+
BASE_TEST_DIR = f'{Path(__file__).resolve().parent.parent}'
|
|
52
|
+
MODEL_DIR = os.path.join(BASE_TEST_DIR, 'artifact_test_dir')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@responses.activate
|
|
56
|
+
def test_add_model_artifact() -> None:
|
|
57
|
+
responses.get(
|
|
58
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
59
|
+
json=API_RESPONSE_200,
|
|
60
|
+
)
|
|
61
|
+
model = Model.get(id_=MODEL_ID)
|
|
62
|
+
|
|
63
|
+
responses.post(
|
|
64
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deploy-artifact',
|
|
65
|
+
json=ARTIFACT_202_RESPONSE,
|
|
66
|
+
)
|
|
67
|
+
responses.get(
|
|
68
|
+
url=f'{URL}/v3/jobs/{JOB_ID}',
|
|
69
|
+
json=MODEL_ARTIFACT_JOB_API_RESPONSE_200,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
job_obj = model.add_artifact(
|
|
73
|
+
model_dir=MODEL_DIR, deployment_params=DeploymentParams(**DEPLOYMENT_PARAMS)
|
|
74
|
+
)
|
|
75
|
+
assert isinstance(job_obj, Job)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@responses.activate
|
|
79
|
+
def test_add_model_artifact_not_found() -> None:
|
|
80
|
+
responses.get(
|
|
81
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
82
|
+
json=API_RESPONSE_200,
|
|
83
|
+
)
|
|
84
|
+
model = Model.get(id_=MODEL_ID)
|
|
85
|
+
|
|
86
|
+
responses.post(
|
|
87
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deploy-artifact',
|
|
88
|
+
json=API_RESPONSE_404,
|
|
89
|
+
status=HTTPStatus.NOT_FOUND,
|
|
90
|
+
)
|
|
91
|
+
with pytest.raises(NotFound):
|
|
92
|
+
model.add_artifact(
|
|
93
|
+
model_dir=MODEL_DIR, deployment_params=DeploymentParams(**DEPLOYMENT_PARAMS)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@responses.activate
|
|
98
|
+
def test_update_model_artifact() -> None:
|
|
99
|
+
responses.get(
|
|
100
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
101
|
+
json=API_RESPONSE_200,
|
|
102
|
+
)
|
|
103
|
+
model = Model.get(id_=MODEL_ID)
|
|
104
|
+
|
|
105
|
+
responses.put(
|
|
106
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deploy-artifact',
|
|
107
|
+
json=ARTIFACT_202_RESPONSE,
|
|
108
|
+
)
|
|
109
|
+
responses.get(
|
|
110
|
+
url=f'{URL}/v3/jobs/{JOB_ID}',
|
|
111
|
+
json=MODEL_ARTIFACT_JOB_API_RESPONSE_200,
|
|
112
|
+
)
|
|
113
|
+
job_obj = model.update_artifact(
|
|
114
|
+
model_dir=MODEL_DIR, deployment_params=DeploymentParams(**DEPLOYMENT_PARAMS)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
assert isinstance(job_obj, Job)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@responses.activate
|
|
121
|
+
def test_update_model_artifact_not_found() -> None:
|
|
122
|
+
responses.get(
|
|
123
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
124
|
+
json=API_RESPONSE_200,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
model = Model.get(id_=MODEL_ID)
|
|
128
|
+
responses.put(
|
|
129
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deploy-artifact',
|
|
130
|
+
json=API_RESPONSE_404,
|
|
131
|
+
status=HTTPStatus.NOT_FOUND,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
with pytest.raises(NotFound):
|
|
135
|
+
model.update_artifact(
|
|
136
|
+
model_dir=MODEL_DIR, deployment_params=DeploymentParams(**DEPLOYMENT_PARAMS)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@responses.activate
|
|
141
|
+
def test_download_model_artifact() -> None:
|
|
142
|
+
responses.get(
|
|
143
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
144
|
+
json=API_RESPONSE_200,
|
|
145
|
+
)
|
|
146
|
+
model = Model.get(id_=MODEL_ID)
|
|
147
|
+
|
|
148
|
+
output_dir = os.path.join(BASE_TEST_DIR, 'test_download')
|
|
149
|
+
tar_file_path = os.path.join(BASE_TEST_DIR, 'artifact.tar')
|
|
150
|
+
with tarfile.open(tar_file_path, 'w:gz') as tar:
|
|
151
|
+
tar.add(MODEL_DIR, arcname=os.path.basename(MODEL_DIR))
|
|
152
|
+
with open(tar_file_path, 'rb') as tar_file:
|
|
153
|
+
data = io.BufferedReader(tar_file)
|
|
154
|
+
responses.get(url=f'{URL}/v3/models/{MODEL_ID}/download-artifact', body=data)
|
|
155
|
+
model.download_artifact(output_dir=output_dir)
|
|
156
|
+
shutil.rmtree(str(output_dir))
|
|
157
|
+
os.remove(tar_file_path)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@responses.activate
|
|
161
|
+
def test_download_model_artifact_not_found() -> None:
|
|
162
|
+
responses.get(
|
|
163
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
164
|
+
json=API_RESPONSE_200,
|
|
165
|
+
)
|
|
166
|
+
model = Model.get(id_=MODEL_ID)
|
|
167
|
+
output_dir = os.path.join(BASE_TEST_DIR, 'test_download')
|
|
168
|
+
responses.get(
|
|
169
|
+
url=f'{URL}/v3/models/{MODEL_ID}/download-artifact',
|
|
170
|
+
json=DOWNLOAD_RESPONSE_404,
|
|
171
|
+
status=HTTPStatus.NOT_FOUND,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
with pytest.raises(NotFound):
|
|
175
|
+
model.download_artifact(output_dir=output_dir)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import responses
|
|
5
|
+
|
|
6
|
+
from fiddler3.entities.model_deployment import ModelDeployment
|
|
7
|
+
from fiddler3.exceptions import NotFound
|
|
8
|
+
from fiddler3.tests.constants import (
|
|
9
|
+
MODEL_DEPLOYMENT_ID,
|
|
10
|
+
MODEL_ID,
|
|
11
|
+
MODEL_NAME,
|
|
12
|
+
ORG_ID,
|
|
13
|
+
ORG_NAME,
|
|
14
|
+
PROJECT_ID,
|
|
15
|
+
PROJECT_NAME,
|
|
16
|
+
URL,
|
|
17
|
+
USER_EMAIL,
|
|
18
|
+
USER_ID,
|
|
19
|
+
USER_NAME,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
API_RESPONSE_200 = {
|
|
23
|
+
'data': {
|
|
24
|
+
'id': MODEL_DEPLOYMENT_ID,
|
|
25
|
+
'model': {'id': MODEL_ID, 'name': MODEL_NAME},
|
|
26
|
+
'project': {
|
|
27
|
+
'id': PROJECT_ID,
|
|
28
|
+
'name': PROJECT_NAME,
|
|
29
|
+
},
|
|
30
|
+
'organization': {
|
|
31
|
+
'id': ORG_ID,
|
|
32
|
+
'name': ORG_NAME,
|
|
33
|
+
},
|
|
34
|
+
'artifact_type': 'SURROGATE',
|
|
35
|
+
'deployment_type': 'BASE_CONTAINER',
|
|
36
|
+
'active': True,
|
|
37
|
+
'image_uri': '',
|
|
38
|
+
'replicas': 1,
|
|
39
|
+
'cpu': 300,
|
|
40
|
+
'memory': 100,
|
|
41
|
+
'created_at': '2023-11-22 16:50:57.705784',
|
|
42
|
+
'updated_at': '2023-11-22 16:50:57.705784',
|
|
43
|
+
'created_by': {
|
|
44
|
+
'id': USER_ID,
|
|
45
|
+
'full_name': USER_NAME,
|
|
46
|
+
'email': USER_EMAIL,
|
|
47
|
+
},
|
|
48
|
+
'updated_by': {
|
|
49
|
+
'id': USER_ID,
|
|
50
|
+
'full_name': USER_NAME,
|
|
51
|
+
'email': USER_EMAIL,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
API_RESPONSE_404 = {
|
|
57
|
+
'error': {
|
|
58
|
+
'code': 404,
|
|
59
|
+
'message': 'Model deployment not found for the given identifier',
|
|
60
|
+
'errors': [
|
|
61
|
+
{
|
|
62
|
+
'reason': 'ObjectNotFound',
|
|
63
|
+
'message': 'Model deployment not found for the given identifier',
|
|
64
|
+
'help': '',
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@responses.activate
|
|
72
|
+
def test_update_model_deployment_success() -> None:
|
|
73
|
+
model_deployment = ModelDeployment(model_id=MODEL_ID)
|
|
74
|
+
|
|
75
|
+
responses.patch(
|
|
76
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deployment',
|
|
77
|
+
json=API_RESPONSE_200,
|
|
78
|
+
)
|
|
79
|
+
model_deployment.cpu = 300
|
|
80
|
+
model_deployment.active = True
|
|
81
|
+
|
|
82
|
+
model_deployment.update()
|
|
83
|
+
assert isinstance(model_deployment, ModelDeployment)
|
|
84
|
+
assert model_deployment.cpu == 300
|
|
85
|
+
assert model_deployment.active
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@responses.activate
|
|
89
|
+
def test_update_model_deployment_not_found() -> None:
|
|
90
|
+
responses.patch(
|
|
91
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deployment',
|
|
92
|
+
json=API_RESPONSE_404,
|
|
93
|
+
status=HTTPStatus.NOT_FOUND,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
with pytest.raises(NotFound):
|
|
97
|
+
model_deployment = ModelDeployment(model_id=MODEL_ID)
|
|
98
|
+
model_deployment.update()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import responses
|
|
5
|
+
|
|
6
|
+
from fiddler3.entities.job import Job
|
|
7
|
+
from fiddler3.entities.model import Model
|
|
8
|
+
from fiddler3.exceptions import NotFound
|
|
9
|
+
from fiddler3.schemas.deployment_params import DeploymentParams
|
|
10
|
+
from fiddler3.tests.apis.test_model import API_RESPONSE_200
|
|
11
|
+
from fiddler3.tests.constants import JOB_ID, MODEL_ID, MODEL_NAME, URL
|
|
12
|
+
|
|
13
|
+
SURROGATE_202_RESPONSE = {
|
|
14
|
+
'data': {
|
|
15
|
+
'job': {
|
|
16
|
+
'id': JOB_ID,
|
|
17
|
+
'name': 'Add model surrogate - manual',
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
'api_version': '3.0',
|
|
21
|
+
'kind': 'NORMAL',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
SURROGATE_JOB_API_RESPONSE_200 = {
|
|
25
|
+
'api_version': '3.0',
|
|
26
|
+
'kind': 'NORMAL',
|
|
27
|
+
'data': {
|
|
28
|
+
'name': 'Add model surrogate - manual',
|
|
29
|
+
'id': JOB_ID,
|
|
30
|
+
'status': 'SUCCESS',
|
|
31
|
+
'progress': 100.0,
|
|
32
|
+
'error_message': None,
|
|
33
|
+
'error_reason': None,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
SURROGATE_JOB_API_RESPONSE_400 = (
|
|
38
|
+
{
|
|
39
|
+
'error': {
|
|
40
|
+
'code': 400,
|
|
41
|
+
'message': f'Model {MODEL_NAME}/'
|
|
42
|
+
f'{MODEL_NAME} already has artifact. Please '
|
|
43
|
+
f'use update_model_artifact or update_model_surrogate '
|
|
44
|
+
f'instead',
|
|
45
|
+
'errors': [
|
|
46
|
+
{
|
|
47
|
+
'reason': 'BadRequest',
|
|
48
|
+
'message': f'Model {MODEL_NAME}/'
|
|
49
|
+
f'{MODEL_NAME} already has artifact. '
|
|
50
|
+
f'Please use update_model_artifact or '
|
|
51
|
+
f'update_model_surrogate instead',
|
|
52
|
+
'help': '',
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
'api_version': '3.0',
|
|
57
|
+
'kind': 'ERROR',
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
API_RESPONSE_404 = {
|
|
62
|
+
'error': {
|
|
63
|
+
'code': 404,
|
|
64
|
+
'message': 'Model not found for the given identifier',
|
|
65
|
+
'errors': [
|
|
66
|
+
{
|
|
67
|
+
'reason': 'ObjectNotFound',
|
|
68
|
+
'message': 'Model not found for the given identifier',
|
|
69
|
+
'help': '',
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
DEPLOYMENT_PARAMS = {'deployment_type': 'MANUAL', 'cpu': 1000}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@responses.activate
|
|
79
|
+
def test_add_model_surrogate() -> None:
|
|
80
|
+
responses.get(
|
|
81
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
82
|
+
json=API_RESPONSE_200,
|
|
83
|
+
)
|
|
84
|
+
model = Model.get(id_=MODEL_ID)
|
|
85
|
+
|
|
86
|
+
responses.post(
|
|
87
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deploy-surrogate',
|
|
88
|
+
json=SURROGATE_202_RESPONSE,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
responses.get(
|
|
92
|
+
url=f'{URL}/v3/jobs/{JOB_ID}',
|
|
93
|
+
json=SURROGATE_JOB_API_RESPONSE_200,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
job_obj = model.add_surrogate(
|
|
97
|
+
deployment_params=DeploymentParams(**DEPLOYMENT_PARAMS)
|
|
98
|
+
)
|
|
99
|
+
assert isinstance(job_obj, Job)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@responses.activate
|
|
103
|
+
def test_add_model_surrogate_no_model() -> None:
|
|
104
|
+
responses.get(
|
|
105
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
106
|
+
json=API_RESPONSE_200,
|
|
107
|
+
)
|
|
108
|
+
model = Model.get(id_=MODEL_ID)
|
|
109
|
+
|
|
110
|
+
responses.post(
|
|
111
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deploy-surrogate',
|
|
112
|
+
json=API_RESPONSE_404,
|
|
113
|
+
status=HTTPStatus.NOT_FOUND,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
with pytest.raises(NotFound):
|
|
117
|
+
model.add_surrogate(deployment_params=DeploymentParams(**DEPLOYMENT_PARAMS))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@responses.activate
|
|
121
|
+
def test_update_model_surrogate() -> None:
|
|
122
|
+
responses.get(
|
|
123
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
124
|
+
json=API_RESPONSE_200,
|
|
125
|
+
)
|
|
126
|
+
model = Model.get(id_=MODEL_ID)
|
|
127
|
+
|
|
128
|
+
responses.put(
|
|
129
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deploy-surrogate',
|
|
130
|
+
json=SURROGATE_202_RESPONSE,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
responses.get(
|
|
134
|
+
url=f'{URL}/v3/jobs/{JOB_ID}',
|
|
135
|
+
json=SURROGATE_JOB_API_RESPONSE_200,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
job_obj = model.update_surrogate(
|
|
139
|
+
deployment_params=DeploymentParams(**DEPLOYMENT_PARAMS)
|
|
140
|
+
)
|
|
141
|
+
assert isinstance(job_obj, Job)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@responses.activate
|
|
145
|
+
def test_update_model_surrogate_no_model() -> None:
|
|
146
|
+
responses.get(
|
|
147
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
148
|
+
json=API_RESPONSE_200,
|
|
149
|
+
)
|
|
150
|
+
model = Model.get(id_=MODEL_ID)
|
|
151
|
+
|
|
152
|
+
responses.put(
|
|
153
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deploy-surrogate',
|
|
154
|
+
json=API_RESPONSE_404,
|
|
155
|
+
status=HTTPStatus.NOT_FOUND,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
with pytest.raises(NotFound):
|
|
159
|
+
model.update_surrogate(deployment_params=DeploymentParams(**DEPLOYMENT_PARAMS))
|