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.
Files changed (61) hide show
  1. fiddler/__init__.py +2 -0
  2. fiddler/_version.py +1 -1
  3. fiddler/api/alert_mixin.py +51 -1
  4. fiddler/api/webhooks_mixin.py +6 -8
  5. fiddler/core_objects.py +76 -26
  6. fiddler/schema/alert.py +9 -2
  7. fiddler/schemas/custom_features.py +44 -1
  8. fiddler3/__init__.py +121 -38
  9. fiddler3/configs.py +3 -0
  10. fiddler3/connection.py +68 -1
  11. fiddler3/constants/alert_rule.py +30 -0
  12. fiddler3/constants/events.py +7 -0
  13. fiddler3/constants/model.py +9 -0
  14. fiddler3/entities/__init__.py +0 -7
  15. fiddler3/entities/alert_record.py +106 -0
  16. fiddler3/entities/alert_rule.py +323 -0
  17. fiddler3/entities/base.py +2 -16
  18. fiddler3/entities/baseline.py +21 -15
  19. fiddler3/entities/dataset.py +1 -1
  20. fiddler3/entities/events.py +136 -0
  21. fiddler3/entities/file.py +162 -0
  22. fiddler3/entities/model.py +141 -24
  23. fiddler3/entities/model_artifact.py +52 -224
  24. fiddler3/entities/model_deployment.py +12 -1
  25. fiddler3/entities/project.py +2 -2
  26. fiddler3/entities/{model_surrogate.py → surrogate.py} +27 -29
  27. fiddler3/entities/webhook.py +128 -0
  28. fiddler3/entities/xai.py +271 -5
  29. fiddler3/libs/http_client.py +1 -1
  30. fiddler3/libs/json_encoder.py +1 -1
  31. fiddler3/schemas/__init__.py +0 -9
  32. fiddler3/schemas/alert_record.py +27 -0
  33. fiddler3/schemas/alert_rule.py +39 -0
  34. fiddler3/schemas/custom_features.py +68 -12
  35. fiddler3/schemas/events.py +15 -0
  36. fiddler3/schemas/file.py +23 -0
  37. fiddler3/schemas/model.py +1 -6
  38. fiddler3/schemas/model_deployment.py +5 -4
  39. fiddler3/schemas/webhook.py +23 -0
  40. fiddler3/tests/apis/test_alert_record.py +99 -0
  41. fiddler3/tests/apis/test_alert_rule.py +393 -0
  42. fiddler3/tests/apis/test_events.py +169 -0
  43. fiddler3/tests/apis/test_files.py +164 -0
  44. fiddler3/tests/apis/test_model.py +60 -9
  45. fiddler3/tests/apis/test_model_artifact.py +21 -0
  46. fiddler3/tests/apis/test_project.py +3 -3
  47. fiddler3/tests/apis/test_webhook.py +257 -0
  48. fiddler3/tests/apis/test_xai.py +233 -1
  49. fiddler3/tests/conftest.py +2 -2
  50. fiddler3/tests/constants.py +5 -0
  51. fiddler3/tests/test_connection.py +24 -1
  52. fiddler3/tests/test_json_encoder.py +2 -2
  53. fiddler3/utils/helpers.py +20 -0
  54. {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/METADATA +15 -5
  55. {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/RECORD +59 -44
  56. tests/fiddler/test_alert.py +95 -5
  57. fiddler3/entities/helpers.py +0 -21
  58. fiddler3/schemas/model_artifact.py +0 -6
  59. {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/LICENSE.txt +0 -0
  60. {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev2.dist-info}/WHEEL +0 -0
  61. {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/{PROJECT_ID}',
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/{PROJECT_ID}',
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()