fiddler-client 2.4.0.dev2__py3-none-any.whl → 2.5.0.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fiddler/__init__.py +2 -0
- fiddler/_version.py +1 -1
- fiddler/api/alert_mixin.py +51 -1
- fiddler/api/webhooks_mixin.py +6 -8
- fiddler/core_objects.py +76 -26
- fiddler/schema/alert.py +9 -2
- fiddler/schemas/custom_features.py +44 -1
- fiddler3/__init__.py +121 -38
- fiddler3/configs.py +3 -0
- fiddler3/connection.py +68 -1
- fiddler3/constants/alert_rule.py +30 -0
- fiddler3/constants/events.py +7 -0
- fiddler3/constants/model.py +9 -0
- fiddler3/entities/__init__.py +0 -7
- fiddler3/entities/alert_record.py +106 -0
- fiddler3/entities/alert_rule.py +323 -0
- fiddler3/entities/base.py +2 -16
- fiddler3/entities/baseline.py +21 -15
- fiddler3/entities/dataset.py +1 -1
- fiddler3/entities/events.py +136 -0
- fiddler3/entities/file.py +162 -0
- fiddler3/entities/model.py +141 -24
- fiddler3/entities/model_artifact.py +52 -224
- fiddler3/entities/model_deployment.py +12 -1
- fiddler3/entities/project.py +2 -2
- fiddler3/entities/{model_surrogate.py → surrogate.py} +27 -29
- fiddler3/entities/webhook.py +128 -0
- fiddler3/entities/xai.py +271 -5
- fiddler3/libs/http_client.py +1 -1
- fiddler3/libs/json_encoder.py +1 -1
- fiddler3/schemas/__init__.py +0 -9
- fiddler3/schemas/alert_record.py +27 -0
- fiddler3/schemas/alert_rule.py +39 -0
- fiddler3/schemas/custom_features.py +68 -12
- fiddler3/schemas/events.py +15 -0
- fiddler3/schemas/file.py +23 -0
- fiddler3/schemas/model.py +1 -6
- fiddler3/schemas/model_deployment.py +5 -4
- fiddler3/schemas/webhook.py +23 -0
- fiddler3/tests/apis/test_alert_record.py +99 -0
- fiddler3/tests/apis/test_alert_rule.py +393 -0
- fiddler3/tests/apis/test_events.py +169 -0
- fiddler3/tests/apis/test_files.py +164 -0
- fiddler3/tests/apis/test_model.py +60 -9
- fiddler3/tests/apis/test_model_artifact.py +21 -0
- fiddler3/tests/apis/test_project.py +3 -3
- fiddler3/tests/apis/test_webhook.py +257 -0
- fiddler3/tests/apis/test_xai.py +233 -1
- fiddler3/tests/conftest.py +2 -2
- fiddler3/tests/constants.py +5 -0
- fiddler3/tests/test_connection.py +24 -1
- fiddler3/tests/test_json_encoder.py +2 -2
- fiddler3/utils/helpers.py +20 -0
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/METADATA +15 -5
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/RECORD +59 -44
- tests/fiddler/test_alert.py +95 -5
- fiddler3/entities/helpers.py +0 -21
- fiddler3/schemas/model_artifact.py +0 -6
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/LICENSE.txt +0 -0
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/WHEEL +0 -0
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
import responses
|
|
6
|
+
|
|
7
|
+
from fiddler3.entities.file import File
|
|
8
|
+
from fiddler3.tests.constants import URL, USER_EMAIL, USER_ID, USER_NAME
|
|
9
|
+
|
|
10
|
+
FILE_NAME = 'model.yaml'
|
|
11
|
+
FILE_ID = 'f34ff7db-9653-49b5-b3d8-02c9670513a3'
|
|
12
|
+
|
|
13
|
+
SINGLE_UPLOAD_200_RESPONSE = {
|
|
14
|
+
'api_version': '3.0',
|
|
15
|
+
'data': {
|
|
16
|
+
'created_at': '2023-11-22 16:50:57.705784',
|
|
17
|
+
'updated_at': '2023-11-22 16:50:57.705784',
|
|
18
|
+
'filename': FILE_NAME,
|
|
19
|
+
'id': FILE_ID,
|
|
20
|
+
'status': 'SUCCESSFUL',
|
|
21
|
+
'type': 'CSV',
|
|
22
|
+
'created_by': {
|
|
23
|
+
'id': USER_ID,
|
|
24
|
+
'full_name': USER_NAME,
|
|
25
|
+
'email': USER_EMAIL,
|
|
26
|
+
},
|
|
27
|
+
'updated_by': {
|
|
28
|
+
'id': USER_ID,
|
|
29
|
+
'full_name': USER_NAME,
|
|
30
|
+
'email': USER_EMAIL,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
'kind': 'NORMAL',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
MULTIPART_INIT_200_RESPONSE = {
|
|
37
|
+
'api_version': '3.0',
|
|
38
|
+
'data': {
|
|
39
|
+
'created_at': '2023-11-22 16:50:57.705784',
|
|
40
|
+
'updated_at': '2023-11-22 16:50:57.705784',
|
|
41
|
+
'filename': FILE_NAME,
|
|
42
|
+
'id': FILE_ID,
|
|
43
|
+
'status': 'SUCCESSFUL',
|
|
44
|
+
'type': 'CSV',
|
|
45
|
+
'created_by': {
|
|
46
|
+
'id': USER_ID,
|
|
47
|
+
'full_name': USER_NAME,
|
|
48
|
+
'email': USER_EMAIL,
|
|
49
|
+
},
|
|
50
|
+
'updated_by': {
|
|
51
|
+
'id': USER_ID,
|
|
52
|
+
'full_name': USER_NAME,
|
|
53
|
+
'email': USER_EMAIL,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
'kind': 'NORMAL',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
MULTIPART_UPLOAD_200_RESPONSE = {
|
|
60
|
+
'data': {'part_id': 'e887e82d-1f49-4de4-aca6-5dcdefa5f36c'},
|
|
61
|
+
'api_version': '3.0',
|
|
62
|
+
'kind': 'NORMAL',
|
|
63
|
+
}
|
|
64
|
+
MULTIPART_COMPLETE_200_RESPONSE = {
|
|
65
|
+
'api_version': '3.0',
|
|
66
|
+
'data': {
|
|
67
|
+
'created_at': '2023-11-22 16:50:57.705784',
|
|
68
|
+
'updated_at': '2023-11-22 16:50:57.705784',
|
|
69
|
+
'filename': FILE_NAME,
|
|
70
|
+
'id': FILE_ID,
|
|
71
|
+
'status': 'SUCCESSFUL',
|
|
72
|
+
'type': 'CSV',
|
|
73
|
+
'created_by': {
|
|
74
|
+
'id': USER_ID,
|
|
75
|
+
'full_name': USER_NAME,
|
|
76
|
+
'email': USER_EMAIL,
|
|
77
|
+
},
|
|
78
|
+
'updated_by': {
|
|
79
|
+
'id': USER_ID,
|
|
80
|
+
'full_name': USER_NAME,
|
|
81
|
+
'email': USER_EMAIL,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
'kind': 'NORMAL',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
BASE_TEST_DIR = Path(__file__).resolve().parent.parent
|
|
88
|
+
FILE_PATH = BASE_TEST_DIR / 'artifact_test_dir' / 'model.yaml'
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@responses.activate
|
|
92
|
+
def test_single_upload_success() -> None:
|
|
93
|
+
responses.post(
|
|
94
|
+
url=f'{URL}/v3/files/upload',
|
|
95
|
+
json=SINGLE_UPLOAD_200_RESPONSE,
|
|
96
|
+
)
|
|
97
|
+
file = File(path=FILE_PATH, name=FILE_NAME).single_upload()
|
|
98
|
+
assert isinstance(file, File)
|
|
99
|
+
assert file.id == UUID(FILE_ID)
|
|
100
|
+
|
|
101
|
+
file = File(
|
|
102
|
+
path=str(FILE_PATH),
|
|
103
|
+
).single_upload()
|
|
104
|
+
assert isinstance(file, File)
|
|
105
|
+
assert file.id == UUID(FILE_ID)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@responses.activate
|
|
109
|
+
def test_multipart_upload_success() -> None:
|
|
110
|
+
responses.post(
|
|
111
|
+
url=f'{URL}/v3/files/multipart-init',
|
|
112
|
+
json=MULTIPART_INIT_200_RESPONSE,
|
|
113
|
+
)
|
|
114
|
+
responses.post(
|
|
115
|
+
url=f'{URL}/v3/files/multipart-upload',
|
|
116
|
+
json=MULTIPART_UPLOAD_200_RESPONSE,
|
|
117
|
+
)
|
|
118
|
+
responses.post(
|
|
119
|
+
url=f'{URL}/v3/files/multipart-complete',
|
|
120
|
+
json=MULTIPART_COMPLETE_200_RESPONSE,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
file = File(path=FILE_PATH, name=FILE_NAME).multipart_upload()
|
|
124
|
+
assert isinstance(file, File)
|
|
125
|
+
assert file.id == UUID(FILE_ID)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@responses.activate
|
|
129
|
+
def test_file_upload() -> None:
|
|
130
|
+
multipart_init_resp = responses.post(
|
|
131
|
+
url=f'{URL}/v3/files/multipart-init',
|
|
132
|
+
json=MULTIPART_INIT_200_RESPONSE,
|
|
133
|
+
)
|
|
134
|
+
multipart_upload_resp = responses.post(
|
|
135
|
+
url=f'{URL}/v3/files/multipart-upload',
|
|
136
|
+
json=MULTIPART_UPLOAD_200_RESPONSE,
|
|
137
|
+
)
|
|
138
|
+
multipart_complete_resp = responses.post(
|
|
139
|
+
url=f'{URL}/v3/files/multipart-complete',
|
|
140
|
+
json=MULTIPART_COMPLETE_200_RESPONSE,
|
|
141
|
+
)
|
|
142
|
+
single_upload_resp = responses.post(
|
|
143
|
+
url=f'{URL}/v3/files/upload',
|
|
144
|
+
json=SINGLE_UPLOAD_200_RESPONSE,
|
|
145
|
+
)
|
|
146
|
+
chunk_size = os.path.getsize(FILE_PATH) // 2 + 1
|
|
147
|
+
|
|
148
|
+
file_multi = File(path=FILE_PATH, name=FILE_NAME).upload(
|
|
149
|
+
chunk_size=chunk_size,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
assert isinstance(file_multi, File)
|
|
153
|
+
assert multipart_init_resp.call_count == 1
|
|
154
|
+
assert multipart_upload_resp.call_count == 2
|
|
155
|
+
assert multipart_complete_resp.call_count == 1
|
|
156
|
+
assert single_upload_resp.call_count == 0
|
|
157
|
+
|
|
158
|
+
chunk_size = os.path.getsize(FILE_PATH) + 1
|
|
159
|
+
file_single = File(path=FILE_PATH, name=FILE_NAME).upload(
|
|
160
|
+
chunk_size=chunk_size,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert isinstance(file_single, File)
|
|
164
|
+
assert single_upload_resp.call_count == 1
|
|
@@ -8,12 +8,14 @@ from responses import matchers
|
|
|
8
8
|
|
|
9
9
|
from fiddler3.entities.job import Job
|
|
10
10
|
from fiddler3.entities.model import Model, ModelCompact
|
|
11
|
+
from fiddler3.entities.model_deployment import ModelDeployment
|
|
11
12
|
from fiddler3.exceptions import Conflict, NotFound
|
|
12
13
|
from fiddler3.schemas.model_schema import ModelSchema
|
|
13
14
|
from fiddler3.schemas.model_spec import ModelSpec
|
|
14
15
|
from fiddler3.tests.constants import (
|
|
15
16
|
JOB_ID,
|
|
16
17
|
JOB_NAME,
|
|
18
|
+
MODEL_DEPLOYMENT_ID,
|
|
17
19
|
MODEL_ID,
|
|
18
20
|
MODEL_NAME,
|
|
19
21
|
ORG_ID,
|
|
@@ -193,14 +195,6 @@ API_RESPONSE_200 = {
|
|
|
193
195
|
'full_name': USER_NAME,
|
|
194
196
|
'email': USER_EMAIL,
|
|
195
197
|
},
|
|
196
|
-
'input_cols': [
|
|
197
|
-
MODEL_SCHEMA['columns'][0],
|
|
198
|
-
MODEL_SCHEMA['columns'][1],
|
|
199
|
-
],
|
|
200
|
-
'output_cols': [MODEL_SCHEMA['columns'][2]],
|
|
201
|
-
'target_cols': [MODEL_SCHEMA['columns'][4]],
|
|
202
|
-
'metadata_cols': [],
|
|
203
|
-
'decision_cols': [MODEL_SCHEMA['columns'][3]],
|
|
204
198
|
'is_binary_ranking_model': False,
|
|
205
199
|
},
|
|
206
200
|
}
|
|
@@ -263,6 +257,40 @@ DELETE_202_RESPONSE = {
|
|
|
263
257
|
'kind': 'NORMAL',
|
|
264
258
|
}
|
|
265
259
|
|
|
260
|
+
DEPLOYMENT_RESPONSE_200 = {
|
|
261
|
+
'data': {
|
|
262
|
+
'id': MODEL_DEPLOYMENT_ID,
|
|
263
|
+
'model': {'id': MODEL_ID, 'name': MODEL_NAME},
|
|
264
|
+
'project': {
|
|
265
|
+
'id': PROJECT_ID,
|
|
266
|
+
'name': PROJECT_NAME,
|
|
267
|
+
},
|
|
268
|
+
'organization': {
|
|
269
|
+
'id': ORG_ID,
|
|
270
|
+
'name': ORG_NAME,
|
|
271
|
+
},
|
|
272
|
+
'artifact_type': 'SURROGATE',
|
|
273
|
+
'deployment_type': 'BASE_CONTAINER',
|
|
274
|
+
'active': True,
|
|
275
|
+
'image_uri': '',
|
|
276
|
+
'replicas': 1,
|
|
277
|
+
'cpu': 300,
|
|
278
|
+
'memory': 100,
|
|
279
|
+
'created_at': '2023-11-22 16:50:57.705784',
|
|
280
|
+
'updated_at': '2023-11-22 16:50:57.705784',
|
|
281
|
+
'created_by': {
|
|
282
|
+
'id': USER_ID,
|
|
283
|
+
'full_name': USER_NAME,
|
|
284
|
+
'email': USER_EMAIL,
|
|
285
|
+
},
|
|
286
|
+
'updated_by': {
|
|
287
|
+
'id': USER_ID,
|
|
288
|
+
'full_name': USER_NAME,
|
|
289
|
+
'email': USER_EMAIL,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
266
294
|
|
|
267
295
|
@responses.activate
|
|
268
296
|
def test_add_model_success() -> None:
|
|
@@ -329,6 +357,10 @@ def test_model_from_name_success() -> None:
|
|
|
329
357
|
json=API_RESPONSE_FROM_NAME,
|
|
330
358
|
match=[matchers.query_param_matcher(params)],
|
|
331
359
|
)
|
|
360
|
+
responses.get(
|
|
361
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
362
|
+
json=API_RESPONSE_200,
|
|
363
|
+
)
|
|
332
364
|
model = Model.from_name(name=MODEL_NAME, project_name=PROJECT_NAME)
|
|
333
365
|
assert isinstance(model, Model)
|
|
334
366
|
|
|
@@ -455,7 +487,7 @@ def test_update_model_not_found() -> None:
|
|
|
455
487
|
|
|
456
488
|
@responses.activate
|
|
457
489
|
def test_datasets_property(mocker: MockerFixture) -> None:
|
|
458
|
-
mock_fn = mocker.patch('fiddler3.entities.Dataset.list')
|
|
490
|
+
mock_fn = mocker.patch('fiddler3.entities.dataset.Dataset.list')
|
|
459
491
|
|
|
460
492
|
responses.get(
|
|
461
493
|
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
@@ -466,3 +498,22 @@ def test_datasets_property(mocker: MockerFixture) -> None:
|
|
|
466
498
|
_ = list(model.datasets)
|
|
467
499
|
|
|
468
500
|
mock_fn.assert_called_with(model_id=UUID(MODEL_ID))
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@responses.activate
|
|
504
|
+
def test_model_deployment_property() -> None:
|
|
505
|
+
responses.get(
|
|
506
|
+
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
507
|
+
json=API_RESPONSE_200,
|
|
508
|
+
)
|
|
509
|
+
model = Model.get(id_=MODEL_ID)
|
|
510
|
+
|
|
511
|
+
responses.get(
|
|
512
|
+
url=f'{URL}/v3/models/{MODEL_ID}/deployment',
|
|
513
|
+
json=DEPLOYMENT_RESPONSE_200,
|
|
514
|
+
)
|
|
515
|
+
model_deployment = model.model_deployment
|
|
516
|
+
assert isinstance(model_deployment, ModelDeployment)
|
|
517
|
+
assert model_deployment.id == UUID(MODEL_DEPLOYMENT_ID)
|
|
518
|
+
assert model_deployment.cpu == 300
|
|
519
|
+
assert model_deployment.artifact_type == 'SURROGATE'
|
|
@@ -12,6 +12,7 @@ from fiddler3.entities.job import Job
|
|
|
12
12
|
from fiddler3.entities.model import Model
|
|
13
13
|
from fiddler3.exceptions import NotFound
|
|
14
14
|
from fiddler3.schemas.deployment_params import DeploymentParams
|
|
15
|
+
from fiddler3.tests.apis.test_files import SINGLE_UPLOAD_200_RESPONSE
|
|
15
16
|
from fiddler3.tests.apis.test_model import API_RESPONSE_200, API_RESPONSE_404
|
|
16
17
|
from fiddler3.tests.constants import JOB_ID, MODEL_ID, MODEL_NAME, PROJECT_NAME, URL
|
|
17
18
|
|
|
@@ -54,6 +55,11 @@ MODEL_DIR = os.path.join(BASE_TEST_DIR, 'artifact_test_dir')
|
|
|
54
55
|
|
|
55
56
|
@responses.activate
|
|
56
57
|
def test_add_model_artifact() -> None:
|
|
58
|
+
responses.post(
|
|
59
|
+
url=f'{URL}/v3/files/upload',
|
|
60
|
+
json=SINGLE_UPLOAD_200_RESPONSE,
|
|
61
|
+
)
|
|
62
|
+
|
|
57
63
|
responses.get(
|
|
58
64
|
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
59
65
|
json=API_RESPONSE_200,
|
|
@@ -77,6 +83,11 @@ def test_add_model_artifact() -> None:
|
|
|
77
83
|
|
|
78
84
|
@responses.activate
|
|
79
85
|
def test_add_model_artifact_not_found() -> None:
|
|
86
|
+
responses.post(
|
|
87
|
+
url=f'{URL}/v3/files/upload',
|
|
88
|
+
json=SINGLE_UPLOAD_200_RESPONSE,
|
|
89
|
+
)
|
|
90
|
+
|
|
80
91
|
responses.get(
|
|
81
92
|
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
82
93
|
json=API_RESPONSE_200,
|
|
@@ -96,6 +107,11 @@ def test_add_model_artifact_not_found() -> None:
|
|
|
96
107
|
|
|
97
108
|
@responses.activate
|
|
98
109
|
def test_update_model_artifact() -> None:
|
|
110
|
+
responses.post(
|
|
111
|
+
url=f'{URL}/v3/files/upload',
|
|
112
|
+
json=SINGLE_UPLOAD_200_RESPONSE,
|
|
113
|
+
)
|
|
114
|
+
|
|
99
115
|
responses.get(
|
|
100
116
|
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
101
117
|
json=API_RESPONSE_200,
|
|
@@ -119,6 +135,11 @@ def test_update_model_artifact() -> None:
|
|
|
119
135
|
|
|
120
136
|
@responses.activate
|
|
121
137
|
def test_update_model_artifact_not_found() -> None:
|
|
138
|
+
responses.post(
|
|
139
|
+
url=f'{URL}/v3/files/upload',
|
|
140
|
+
json=SINGLE_UPLOAD_200_RESPONSE,
|
|
141
|
+
)
|
|
142
|
+
|
|
122
143
|
responses.get(
|
|
123
144
|
url=f'{URL}/v3/models/{MODEL_ID}',
|
|
124
145
|
json=API_RESPONSE_200,
|
|
@@ -203,7 +203,7 @@ def test_project_list_empty() -> None:
|
|
|
203
203
|
@responses.activate
|
|
204
204
|
def test_delete_project_success() -> None:
|
|
205
205
|
responses.delete(
|
|
206
|
-
url=f'{URL}/v2/projects/{
|
|
206
|
+
url=f'{URL}/v2/projects/{ORG_NAME}:{PROJECT_NAME}',
|
|
207
207
|
json={},
|
|
208
208
|
)
|
|
209
209
|
project = Project(name=PROJECT_NAME)
|
|
@@ -215,7 +215,7 @@ def test_delete_project_success() -> None:
|
|
|
215
215
|
@responses.activate
|
|
216
216
|
def test_delete_project_not_found() -> None:
|
|
217
217
|
responses.delete(
|
|
218
|
-
url=f'{URL}/v2/projects/{
|
|
218
|
+
url=f'{URL}/v2/projects/{ORG_NAME}:{PROJECT_NAME}',
|
|
219
219
|
json=API_RESPONSE_404,
|
|
220
220
|
status=HTTPStatus.NOT_FOUND,
|
|
221
221
|
)
|
|
@@ -227,7 +227,7 @@ def test_delete_project_not_found() -> None:
|
|
|
227
227
|
|
|
228
228
|
|
|
229
229
|
def test_models_property(mocker: MockerFixture) -> None:
|
|
230
|
-
mock_fn = mocker.patch('fiddler3.entities.Model.list')
|
|
230
|
+
mock_fn = mocker.patch('fiddler3.entities.model.Model.list')
|
|
231
231
|
|
|
232
232
|
project = Project(name=PROJECT_NAME)
|
|
233
233
|
project.id = UUID(PROJECT_ID)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import responses
|
|
8
|
+
|
|
9
|
+
from fiddler3.entities.webhook import Webhook
|
|
10
|
+
from fiddler3.exceptions import Conflict, NotFound
|
|
11
|
+
from fiddler3.tests.constants import (
|
|
12
|
+
ORG_NAME,
|
|
13
|
+
URL,
|
|
14
|
+
WEBHOOK_ID,
|
|
15
|
+
WEBHOOK_NAME,
|
|
16
|
+
WEBHOOK_PROVIDER,
|
|
17
|
+
WEBHOOK_URL,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
API_RESPONSE_200 = {
|
|
21
|
+
'data': {
|
|
22
|
+
'id': 1,
|
|
23
|
+
'uuid': WEBHOOK_ID,
|
|
24
|
+
'name': WEBHOOK_NAME,
|
|
25
|
+
'url': WEBHOOK_URL,
|
|
26
|
+
'provider': WEBHOOK_PROVIDER,
|
|
27
|
+
'created_at': '2023-12-18T10:23:51.232768+00:00',
|
|
28
|
+
'updated_at': '2023-12-18T10:23:51.232768+00:00',
|
|
29
|
+
'organization_name': ORG_NAME,
|
|
30
|
+
},
|
|
31
|
+
'api_version': '2.0',
|
|
32
|
+
'kind': 'NORMAL',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
API_RESPONSE_404 = {
|
|
36
|
+
'error': {
|
|
37
|
+
'code': 404,
|
|
38
|
+
'message': "WebhookConfig({'uuid': '128e4df4-91a8-8d61-d9e2-222eecb829f2'}) not found",
|
|
39
|
+
'errors': [
|
|
40
|
+
{
|
|
41
|
+
'reason': 'ObjectNotFound',
|
|
42
|
+
'message': "WebhookConfig({'uuid': '128e4df4-91a8-8d61-d9e2-222eecb829f2'}) not found",
|
|
43
|
+
'help': '',
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
'api_version': '2.0',
|
|
48
|
+
'kind': 'ERROR',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
LIST_API_RESPONSE = {
|
|
52
|
+
'data': {
|
|
53
|
+
'page_size': 100,
|
|
54
|
+
'total': 2,
|
|
55
|
+
'item_count': 2,
|
|
56
|
+
'page_count': 1,
|
|
57
|
+
'page_index': 1,
|
|
58
|
+
'offset': 0,
|
|
59
|
+
'items': [
|
|
60
|
+
API_RESPONSE_200['data'],
|
|
61
|
+
{
|
|
62
|
+
'id': 2,
|
|
63
|
+
'uuid': '2531bfd9-2ca2-4a7b-bb5a-136c8da09ca1',
|
|
64
|
+
'name': 'test_webhook_config_name2',
|
|
65
|
+
'url': WEBHOOK_URL,
|
|
66
|
+
'provider': WEBHOOK_PROVIDER,
|
|
67
|
+
'created_at': '2023-12-18T10:23:51.232768+00:00',
|
|
68
|
+
'updated_at': '2023-12-18T10:23:51.232768+00:00',
|
|
69
|
+
'organization_name': ORG_NAME,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
API_RESPONSE_EMPTY_LIST = {
|
|
76
|
+
'data': {
|
|
77
|
+
'page_size': 100,
|
|
78
|
+
'total': 0,
|
|
79
|
+
'item_count': 0,
|
|
80
|
+
'page_count': 1,
|
|
81
|
+
'page_index': 1,
|
|
82
|
+
'offset': 0,
|
|
83
|
+
'items': [],
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
API_RESPONSE_409 = {
|
|
88
|
+
'error': {
|
|
89
|
+
'code': 409,
|
|
90
|
+
'message': f'A webhook integration with the provided name ({WEBHOOK_NAME}) \
|
|
91
|
+
already exists. Webhook names must be unique.',
|
|
92
|
+
'errors': [
|
|
93
|
+
{
|
|
94
|
+
'reason': 'Conflict',
|
|
95
|
+
'message': f'A webhook integration with the provided name \
|
|
96
|
+
({WEBHOOK_NAME}) already exists. Webhook names must be unique.',
|
|
97
|
+
'help': '',
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@responses.activate
|
|
105
|
+
def test_get_webhook_success() -> None:
|
|
106
|
+
responses.get(
|
|
107
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
108
|
+
json=API_RESPONSE_200,
|
|
109
|
+
)
|
|
110
|
+
webhook = Webhook.get(id_=WEBHOOK_ID)
|
|
111
|
+
assert isinstance(webhook, Webhook)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@responses.activate
|
|
115
|
+
def test_get_webhook_not_found() -> None:
|
|
116
|
+
webhook_id = '128e4df4-91a8-8d61-d9e2-222eecb829f2'
|
|
117
|
+
responses.get(
|
|
118
|
+
url=f'{URL}/v2/webhooks/{webhook_id}',
|
|
119
|
+
json=API_RESPONSE_404,
|
|
120
|
+
status=HTTPStatus.NOT_FOUND,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
with pytest.raises(NotFound):
|
|
124
|
+
Webhook.get(id_=webhook_id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@responses.activate
|
|
128
|
+
def test_webhook_list_success() -> None:
|
|
129
|
+
responses.get(
|
|
130
|
+
url=f'{URL}/v2/webhooks',
|
|
131
|
+
json=LIST_API_RESPONSE,
|
|
132
|
+
)
|
|
133
|
+
for webhook in Webhook.list():
|
|
134
|
+
assert isinstance(webhook, Webhook)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@responses.activate
|
|
138
|
+
def test_webhook_list_empty() -> None:
|
|
139
|
+
resp = API_RESPONSE_EMPTY_LIST
|
|
140
|
+
|
|
141
|
+
responses.get(
|
|
142
|
+
url=f'{URL}/v2/webhooks',
|
|
143
|
+
json=resp,
|
|
144
|
+
)
|
|
145
|
+
assert len(list(Webhook.list())) == 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@responses.activate
|
|
149
|
+
def test_add_webhook_success() -> None:
|
|
150
|
+
responses.post(
|
|
151
|
+
url=f'{URL}/v2/webhooks',
|
|
152
|
+
json=API_RESPONSE_200,
|
|
153
|
+
)
|
|
154
|
+
webhook = Webhook(
|
|
155
|
+
name=WEBHOOK_NAME, url=WEBHOOK_URL, provider=WEBHOOK_PROVIDER
|
|
156
|
+
).create()
|
|
157
|
+
assert isinstance(webhook, Webhook)
|
|
158
|
+
assert webhook.id == UUID(WEBHOOK_ID)
|
|
159
|
+
assert webhook.name == WEBHOOK_NAME
|
|
160
|
+
assert webhook.created_at == datetime.fromisoformat(
|
|
161
|
+
API_RESPONSE_200['data']['created_at']
|
|
162
|
+
)
|
|
163
|
+
assert webhook.updated_at == datetime.fromisoformat(
|
|
164
|
+
API_RESPONSE_200['data']['updated_at']
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
assert json.loads(responses.calls[0].request.body) == {
|
|
168
|
+
'name': WEBHOOK_NAME,
|
|
169
|
+
'url': WEBHOOK_URL,
|
|
170
|
+
'provider': WEBHOOK_PROVIDER,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@responses.activate
|
|
175
|
+
def test_add_webhook_conflict() -> None:
|
|
176
|
+
responses.post(
|
|
177
|
+
url=f'{URL}/v2/webhooks', json=API_RESPONSE_409, status=HTTPStatus.CONFLICT
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
with pytest.raises(Conflict):
|
|
181
|
+
Webhook(
|
|
182
|
+
name=WEBHOOK_NAME,
|
|
183
|
+
url=WEBHOOK_URL,
|
|
184
|
+
provider=WEBHOOK_PROVIDER,
|
|
185
|
+
).create()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@responses.activate
|
|
189
|
+
def test_update_webhook_success(mocker) -> None:
|
|
190
|
+
responses.get(
|
|
191
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
192
|
+
json=API_RESPONSE_200,
|
|
193
|
+
)
|
|
194
|
+
webhook = Webhook.get(id_=WEBHOOK_ID)
|
|
195
|
+
|
|
196
|
+
mocker.patch.dict(API_RESPONSE_200['data'], {'name': 'test1'})
|
|
197
|
+
responses.patch(
|
|
198
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
199
|
+
json=API_RESPONSE_200,
|
|
200
|
+
)
|
|
201
|
+
webhook.name = 'test1'
|
|
202
|
+
|
|
203
|
+
webhook.update()
|
|
204
|
+
assert isinstance(webhook, Webhook)
|
|
205
|
+
assert webhook.name == 'test1'
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@responses.activate
|
|
209
|
+
def test_update_webhook_not_found() -> None:
|
|
210
|
+
responses.get(
|
|
211
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
212
|
+
json=API_RESPONSE_200,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
responses.patch(
|
|
216
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
217
|
+
json=API_RESPONSE_404,
|
|
218
|
+
status=HTTPStatus.NOT_FOUND,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
with pytest.raises(NotFound):
|
|
222
|
+
webhook = Webhook.get(id_=WEBHOOK_ID)
|
|
223
|
+
webhook.update()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@responses.activate
|
|
227
|
+
def test_delete_webhook() -> None:
|
|
228
|
+
responses.get(
|
|
229
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
230
|
+
json=API_RESPONSE_200,
|
|
231
|
+
)
|
|
232
|
+
webhook = Webhook.get(id_=WEBHOOK_ID)
|
|
233
|
+
|
|
234
|
+
responses.delete(
|
|
235
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
236
|
+
json={},
|
|
237
|
+
)
|
|
238
|
+
response = webhook.delete()
|
|
239
|
+
assert response is None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@responses.activate
|
|
243
|
+
def test_delete_webhook_not_found() -> None:
|
|
244
|
+
responses.get(
|
|
245
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
246
|
+
json=API_RESPONSE_200,
|
|
247
|
+
)
|
|
248
|
+
webhook = Webhook.get(id_=WEBHOOK_ID)
|
|
249
|
+
|
|
250
|
+
responses.delete(
|
|
251
|
+
url=f'{URL}/v2/webhooks/{WEBHOOK_ID}',
|
|
252
|
+
json=API_RESPONSE_404,
|
|
253
|
+
status=HTTPStatus.NOT_FOUND,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
with pytest.raises(NotFound):
|
|
257
|
+
webhook.delete()
|