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.
Files changed (65) hide show
  1. fiddler/__init__.py +2 -0
  2. fiddler/_version.py +1 -1
  3. fiddler/api/alert_mixin.py +1 -1
  4. fiddler/api/api.py +2 -0
  5. fiddler/api/webhooks_mixin.py +9 -7
  6. fiddler/core_objects.py +98 -47
  7. fiddler/schemas/custom_features.py +25 -19
  8. fiddler3/constants/baseline.py +19 -0
  9. fiddler3/constants/common.py +12 -0
  10. fiddler3/constants/dataset.py +7 -0
  11. fiddler3/constants/model_deployment.py +15 -0
  12. fiddler3/constants/xai.py +11 -0
  13. fiddler3/entities/__init__.py +5 -1
  14. fiddler3/entities/base.py +22 -18
  15. fiddler3/entities/baseline.py +228 -0
  16. fiddler3/entities/dataset.py +149 -0
  17. fiddler3/entities/job.py +9 -8
  18. fiddler3/entities/model.py +160 -29
  19. fiddler3/entities/model_artifact.py +350 -0
  20. fiddler3/entities/model_deployment.py +112 -0
  21. fiddler3/entities/model_surrogate.py +92 -0
  22. fiddler3/entities/organization.py +38 -0
  23. fiddler3/entities/project.py +69 -8
  24. fiddler3/entities/user.py +45 -0
  25. fiddler3/entities/xai.py +309 -0
  26. fiddler3/libs/http_client.py +24 -13
  27. fiddler3/libs/json_encoder.py +12 -0
  28. fiddler3/schemas/__init__.py +4 -0
  29. fiddler3/schemas/baseline.py +37 -0
  30. fiddler3/schemas/custom_features.py +23 -18
  31. fiddler3/schemas/dataset.py +27 -0
  32. fiddler3/schemas/deployment_params.py +25 -0
  33. fiddler3/schemas/filter_query.py +54 -0
  34. fiddler3/schemas/job.py +2 -2
  35. fiddler3/schemas/model.py +9 -9
  36. fiddler3/schemas/model_artifact.py +6 -0
  37. fiddler3/schemas/model_deployment.py +26 -0
  38. fiddler3/schemas/organization.py +9 -1
  39. fiddler3/schemas/project.py +4 -4
  40. fiddler3/schemas/server_info.py +2 -2
  41. fiddler3/schemas/user.py +1 -1
  42. fiddler3/schemas/xai.py +32 -0
  43. fiddler3/tests/apis/test_baseline.py +292 -0
  44. fiddler3/tests/apis/test_dataset.py +173 -0
  45. fiddler3/tests/apis/test_mixin.py +77 -0
  46. fiddler3/tests/apis/test_model.py +102 -7
  47. fiddler3/tests/apis/test_model_artifact.py +175 -0
  48. fiddler3/tests/apis/test_model_deployment.py +98 -0
  49. fiddler3/tests/apis/test_model_surrogate.py +159 -0
  50. fiddler3/tests/apis/test_project.py +36 -20
  51. fiddler3/tests/apis/test_xai.py +690 -0
  52. fiddler3/tests/constants.py +10 -0
  53. fiddler3/tests/test_json_encoder.py +16 -0
  54. fiddler3/tests/test_logger.py +12 -0
  55. fiddler3/tests/test_utils.py +19 -0
  56. fiddler3/utils/__init__.py +1 -0
  57. fiddler3/utils/helpers.py +43 -0
  58. fiddler3/utils/logger.py +15 -0
  59. fiddler3/utils/validations.py +11 -0
  60. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/METADATA +7 -5
  61. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/RECORD +65 -33
  62. tests/fiddler/test_segments.py +227 -0
  63. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/LICENSE.txt +0 -0
  64. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/WHEEL +0 -0
  65. {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': 2,
239
- 'item_count': 2,
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 = {'name': PROJECT_NAME, 'project_name': PROJECT_NAME}
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(model_name=MODEL_NAME, project_name=PROJECT_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 = {'name': PROJECT_NAME, 'project_name': PROJECT_NAME}
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(model_name=MODEL_NAME, project_name=PROJECT_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))