gsctl 0.29.0a20250114__py2.py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. graphscope/flex/rest/__init__.py +106 -0
  2. graphscope/flex/rest/api/__init__.py +12 -0
  3. graphscope/flex/rest/api/alert_api.py +2790 -0
  4. graphscope/flex/rest/api/data_source_api.py +1177 -0
  5. graphscope/flex/rest/api/deployment_api.py +1323 -0
  6. graphscope/flex/rest/api/graph_api.py +2813 -0
  7. graphscope/flex/rest/api/job_api.py +1408 -0
  8. graphscope/flex/rest/api/service_api.py +1316 -0
  9. graphscope/flex/rest/api/stored_procedure_api.py +1454 -0
  10. graphscope/flex/rest/api/utils_api.py +310 -0
  11. graphscope/flex/rest/api_client.py +789 -0
  12. graphscope/flex/rest/api_response.py +21 -0
  13. graphscope/flex/rest/configuration.py +451 -0
  14. graphscope/flex/rest/exceptions.py +200 -0
  15. graphscope/flex/rest/models/__init__.py +82 -0
  16. graphscope/flex/rest/models/base_edge_type.py +102 -0
  17. graphscope/flex/rest/models/base_edge_type_vertex_type_pair_relations_inner.py +108 -0
  18. graphscope/flex/rest/models/base_edge_type_vertex_type_pair_relations_inner_x_csr_params.py +98 -0
  19. graphscope/flex/rest/models/base_property_meta.py +105 -0
  20. graphscope/flex/rest/models/base_vertex_type.py +96 -0
  21. graphscope/flex/rest/models/base_vertex_type_x_csr_params.py +88 -0
  22. graphscope/flex/rest/models/column_mapping.py +94 -0
  23. graphscope/flex/rest/models/column_mapping_column.py +90 -0
  24. graphscope/flex/rest/models/create_alert_receiver_request.py +103 -0
  25. graphscope/flex/rest/models/create_alert_rule_request.py +112 -0
  26. graphscope/flex/rest/models/create_dataloading_job_response.py +88 -0
  27. graphscope/flex/rest/models/create_edge_type.py +114 -0
  28. graphscope/flex/rest/models/create_graph_request.py +106 -0
  29. graphscope/flex/rest/models/create_graph_response.py +88 -0
  30. graphscope/flex/rest/models/create_graph_schema_request.py +106 -0
  31. graphscope/flex/rest/models/create_property_meta.py +105 -0
  32. graphscope/flex/rest/models/create_stored_proc_request.py +101 -0
  33. graphscope/flex/rest/models/create_stored_proc_response.py +88 -0
  34. graphscope/flex/rest/models/create_vertex_type.py +108 -0
  35. graphscope/flex/rest/models/dataloading_job_config.py +136 -0
  36. graphscope/flex/rest/models/dataloading_job_config_edges_inner.py +92 -0
  37. graphscope/flex/rest/models/dataloading_job_config_loading_config.py +104 -0
  38. graphscope/flex/rest/models/dataloading_job_config_loading_config_format.py +90 -0
  39. graphscope/flex/rest/models/dataloading_job_config_vertices_inner.py +88 -0
  40. graphscope/flex/rest/models/dataloading_mr_job_config.py +88 -0
  41. graphscope/flex/rest/models/date_type.py +88 -0
  42. graphscope/flex/rest/models/edge_mapping.py +122 -0
  43. graphscope/flex/rest/models/edge_mapping_type_triplet.py +92 -0
  44. graphscope/flex/rest/models/error.py +90 -0
  45. graphscope/flex/rest/models/get_alert_message_response.py +123 -0
  46. graphscope/flex/rest/models/get_alert_receiver_response.py +107 -0
  47. graphscope/flex/rest/models/get_alert_rule_response.py +114 -0
  48. graphscope/flex/rest/models/get_edge_type.py +116 -0
  49. graphscope/flex/rest/models/get_graph_response.py +139 -0
  50. graphscope/flex/rest/models/get_graph_schema_response.py +106 -0
  51. graphscope/flex/rest/models/get_pod_log_response.py +88 -0
  52. graphscope/flex/rest/models/get_property_meta.py +107 -0
  53. graphscope/flex/rest/models/get_resource_usage_response.py +105 -0
  54. graphscope/flex/rest/models/get_storage_usage_response.py +88 -0
  55. graphscope/flex/rest/models/get_stored_proc_response.py +130 -0
  56. graphscope/flex/rest/models/get_vertex_type.py +110 -0
  57. graphscope/flex/rest/models/gs_data_type.py +152 -0
  58. graphscope/flex/rest/models/job_status.py +107 -0
  59. graphscope/flex/rest/models/long_text.py +93 -0
  60. graphscope/flex/rest/models/node_status.py +94 -0
  61. graphscope/flex/rest/models/parameter.py +96 -0
  62. graphscope/flex/rest/models/pod_status.py +108 -0
  63. graphscope/flex/rest/models/primitive_type.py +95 -0
  64. graphscope/flex/rest/models/resource_usage.py +92 -0
  65. graphscope/flex/rest/models/running_deployment_info.py +128 -0
  66. graphscope/flex/rest/models/running_deployment_status.py +124 -0
  67. graphscope/flex/rest/models/schema_mapping.py +106 -0
  68. graphscope/flex/rest/models/service_status.py +112 -0
  69. graphscope/flex/rest/models/service_status_sdk_endpoints.py +94 -0
  70. graphscope/flex/rest/models/start_service_request.py +88 -0
  71. graphscope/flex/rest/models/stored_procedure_meta.py +126 -0
  72. graphscope/flex/rest/models/string_type.py +92 -0
  73. graphscope/flex/rest/models/string_type_string.py +124 -0
  74. graphscope/flex/rest/models/temporal_type.py +92 -0
  75. graphscope/flex/rest/models/temporal_type_temporal.py +138 -0
  76. graphscope/flex/rest/models/time_stamp_type.py +88 -0
  77. graphscope/flex/rest/models/update_alert_message_status_request.py +97 -0
  78. graphscope/flex/rest/models/update_stored_proc_request.py +88 -0
  79. graphscope/flex/rest/models/upload_file_response.py +90 -0
  80. graphscope/flex/rest/models/vertex_mapping.py +100 -0
  81. graphscope/flex/rest/py.typed +0 -0
  82. graphscope/flex/rest/rest.py +258 -0
  83. graphscope/gsctl/V6D_VERSION +1 -0
  84. graphscope/gsctl/VERSION +1 -0
  85. graphscope/gsctl/__init__.py +22 -0
  86. graphscope/gsctl/commands/__init__.py +148 -0
  87. graphscope/gsctl/commands/common.py +200 -0
  88. graphscope/gsctl/commands/dev.py +448 -0
  89. graphscope/gsctl/commands/insight/__init__.py +17 -0
  90. graphscope/gsctl/commands/insight/glob.py +234 -0
  91. graphscope/gsctl/commands/insight/graph.py +205 -0
  92. graphscope/gsctl/commands/interactive/__init__.py +17 -0
  93. graphscope/gsctl/commands/interactive/glob.py +280 -0
  94. graphscope/gsctl/commands/interactive/graph.py +259 -0
  95. graphscope/gsctl/config.py +221 -0
  96. graphscope/gsctl/gsctl.py +51 -0
  97. graphscope/gsctl/impl/__init__.py +64 -0
  98. graphscope/gsctl/impl/alert.py +135 -0
  99. graphscope/gsctl/impl/common.py +53 -0
  100. graphscope/gsctl/impl/datasource.py +80 -0
  101. graphscope/gsctl/impl/deployment.py +62 -0
  102. graphscope/gsctl/impl/graph.py +150 -0
  103. graphscope/gsctl/impl/job.py +63 -0
  104. graphscope/gsctl/impl/service.py +62 -0
  105. graphscope/gsctl/impl/stored_procedure.py +92 -0
  106. graphscope/gsctl/impl/utils.py +38 -0
  107. graphscope/gsctl/scripts/install_deps.sh +969 -0
  108. graphscope/gsctl/tests/__init__.py +17 -0
  109. graphscope/gsctl/tests/test_graphscope_insight.py +401 -0
  110. graphscope/gsctl/tests/test_interactive.py +516 -0
  111. graphscope/gsctl/utils.py +337 -0
  112. graphscope/gsctl/version.py +31 -0
  113. gsctl-0.29.0a20250114.dist-info/METADATA +20 -0
  114. gsctl-0.29.0a20250114.dist-info/RECORD +117 -0
  115. gsctl-0.29.0a20250114.dist-info/WHEEL +6 -0
  116. gsctl-0.29.0a20250114.dist-info/entry_points.txt +3 -0
  117. gsctl-0.29.0a20250114.dist-info/top_level.txt +1 -0
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright 2024 Alibaba Group Holding Limited. All Rights Reserved.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright 2024 Alibaba Group Holding Limited. All Rights Reserved.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ import warnings
20
+
21
+ # Disable warnings
22
+ warnings.filterwarnings("ignore", category=Warning)
23
+
24
+ import os
25
+ import time
26
+
27
+ import pytest
28
+
29
+ from graphscope.gsctl.impl import bind_datasource_in_batch
30
+ from graphscope.gsctl.impl import connect_coordinator
31
+ from graphscope.gsctl.impl import create_alert_receiver
32
+ from graphscope.gsctl.impl import create_edge_type
33
+ from graphscope.gsctl.impl import create_vertex_type
34
+ from graphscope.gsctl.impl import delete_alert_receiver_by_id
35
+ from graphscope.gsctl.impl import delete_alert_rule_by_id
36
+ from graphscope.gsctl.impl import delete_edge_type_by_name
37
+ from graphscope.gsctl.impl import delete_vertex_type_by_name
38
+ from graphscope.gsctl.impl import get_datasource_by_id
39
+ from graphscope.gsctl.impl import get_deployment_pod_log
40
+ from graphscope.gsctl.impl import get_deployment_resource_usage
41
+ from graphscope.gsctl.impl import get_deployment_status
42
+ from graphscope.gsctl.impl import get_graph_by_id
43
+ from graphscope.gsctl.impl import get_storage_usage
44
+ from graphscope.gsctl.impl import import_schema
45
+ from graphscope.gsctl.impl import list_alert_messages
46
+ from graphscope.gsctl.impl import list_alert_receivers
47
+ from graphscope.gsctl.impl import list_alert_rules
48
+ from graphscope.gsctl.impl import list_graphs
49
+ from graphscope.gsctl.impl import list_jobs
50
+ from graphscope.gsctl.impl import submit_dataloading_job
51
+ from graphscope.gsctl.impl import update_alert_receiver_by_id
52
+ from graphscope.gsctl.impl import update_alert_rule_by_id
53
+ from graphscope.gsctl.impl import upload_file
54
+
55
+
56
+ def get_coordinator_endpoint():
57
+ return os.environ.get("COORDINATOR_SERVICE_ENDPOINT", "http://127.0.0.1:8080")
58
+
59
+
60
+ COORDINATOR_ENDPOINT = get_coordinator_endpoint()
61
+
62
+
63
+ modern_graph_person_type = {
64
+ "type_name": "person2",
65
+ "primary_keys": ["id"],
66
+ "properties": [
67
+ {
68
+ "property_name": "id",
69
+ "property_type": {"primitive_type": "DT_SIGNED_INT64"},
70
+ "description": "",
71
+ "property_id": 19,
72
+ },
73
+ {
74
+ "property_name": "name",
75
+ "property_type": {"string": {"long_text": ""}},
76
+ "description": "",
77
+ "property_id": 20,
78
+ },
79
+ {
80
+ "property_name": "age",
81
+ "property_type": {"primitive_type": "DT_SIGNED_INT32"},
82
+ "description": "",
83
+ "property_id": 21,
84
+ },
85
+ ],
86
+ "description": "",
87
+ }
88
+
89
+
90
+ modern_graph_knows_type = {
91
+ "type_name": "knows2",
92
+ "vertex_type_pair_relations": [
93
+ {"source_vertex": "person2", "destination_vertex": "person2"}
94
+ ],
95
+ "primary_keys": [],
96
+ "properties": [
97
+ {
98
+ "property_name": "weight",
99
+ "property_type": {"primitive_type": "DT_DOUBLE"},
100
+ "description": "",
101
+ "property_id": 22,
102
+ }
103
+ ],
104
+ "description": "",
105
+ }
106
+
107
+
108
+ modern_graph_schema = {
109
+ "vertex_types": [
110
+ {
111
+ "type_name": "person2",
112
+ "primary_keys": ["id"],
113
+ "properties": [
114
+ {
115
+ "property_name": "id",
116
+ "property_type": {"primitive_type": "DT_SIGNED_INT64"},
117
+ "description": "",
118
+ "property_id": 19,
119
+ },
120
+ {
121
+ "property_name": "name",
122
+ "property_type": {"string": {"long_text": ""}},
123
+ "description": "",
124
+ "property_id": 20,
125
+ },
126
+ {
127
+ "property_name": "age",
128
+ "property_type": {"primitive_type": "DT_SIGNED_INT32"},
129
+ "description": "",
130
+ "property_id": 21,
131
+ },
132
+ ],
133
+ "description": "",
134
+ }
135
+ ],
136
+ "edge_types": [
137
+ {
138
+ "type_name": "knows2",
139
+ "vertex_type_pair_relations": [
140
+ {"source_vertex": "person2", "destination_vertex": "person2"}
141
+ ],
142
+ "primary_keys": [],
143
+ "properties": [
144
+ {
145
+ "property_name": "weight",
146
+ "property_type": {"primitive_type": "DT_DOUBLE"},
147
+ "description": "",
148
+ "property_id": 22,
149
+ }
150
+ ],
151
+ "description": "",
152
+ }
153
+ ],
154
+ }
155
+
156
+
157
+ class TestE2E(object):
158
+ def setup_class(self):
159
+ self.deployment_info = connect_coordinator(COORDINATOR_ENDPOINT)
160
+ # graph id
161
+ graphs = list_graphs()
162
+ assert len(graphs) == 1
163
+ self._graph_id = graphs[0].id
164
+ assert self._graph_id is not None
165
+
166
+ @pytest.mark.skipif(
167
+ os.environ.get("RUN_ON_MINIKUBE", None) == "ON",
168
+ reason="Minikube does not support metric API",
169
+ )
170
+ def test_deployment(self):
171
+ assert self.deployment_info.instance_name is not None
172
+ assert self.deployment_info.cluster_type == "KUBERNETES"
173
+ assert self.deployment_info.engine == "Gaia"
174
+ assert self.deployment_info.storage == "MutablePersistent"
175
+ assert self.deployment_info.frontend == "Cypher/Gremlin"
176
+ assert self.deployment_info.version is not None
177
+ assert self.deployment_info.creation_time is not None
178
+ # status
179
+ status = get_deployment_status()
180
+ assert status is not None
181
+ # resource usage
182
+ rlt = get_deployment_resource_usage()
183
+ assert len(rlt.cpu_usage) > 0
184
+ assert len(rlt.memory_usage) > 0
185
+ # storage usage
186
+ rlt = get_storage_usage()
187
+ assert rlt.storage_usage is not None
188
+ # frontend pod log
189
+ frontend_pod = status.pods["frontend"][0]
190
+ # fetch log from k8s
191
+ pod_log = get_deployment_pod_log(
192
+ frontend_pod.name, frontend_pod.component_belong_to, False
193
+ ).to_dict()
194
+ for container, container_log in pod_log["log"].items():
195
+ assert container_log != ""
196
+
197
+ def test_schema(self):
198
+ # create vertex type
199
+ create_vertex_type(self._graph_id, modern_graph_person_type)
200
+ graph = get_graph_by_id(self._graph_id).to_dict()
201
+ vertex_type_exists = False
202
+ for vertex in graph["schema"]["vertex_types"]:
203
+ if vertex["type_name"] == modern_graph_person_type["type_name"]:
204
+ vertex_type_exists = True
205
+ assert vertex_type_exists
206
+ # create type edge
207
+ create_edge_type(self._graph_id, modern_graph_knows_type)
208
+ graph = get_graph_by_id(self._graph_id).to_dict()
209
+ edge_type_exists = False
210
+ for edge in graph["schema"]["edge_types"]:
211
+ if edge["type_name"] == modern_graph_knows_type["type_name"]:
212
+ edge_type_exists = True
213
+ assert edge_type_exists
214
+ # delete edge type
215
+ for relation in modern_graph_knows_type["vertex_type_pair_relations"]:
216
+ delete_edge_type_by_name(
217
+ self._graph_id,
218
+ modern_graph_knows_type["type_name"],
219
+ relation["source_vertex"],
220
+ relation["destination_vertex"],
221
+ )
222
+ # delete vertex type
223
+ delete_vertex_type_by_name(
224
+ self._graph_id, modern_graph_person_type["type_name"]
225
+ )
226
+
227
+ @pytest.mark.skip(reason="")
228
+ def test_import_schema_in_batch(self):
229
+ import_schema(self._graph_id, modern_graph_schema)
230
+ # check
231
+ graph = get_graph_by_id(self._graph_id).to_dict()
232
+ assert len(graph["schema"]["vertex_types"]) == len(
233
+ modern_graph_schema["vertex_types"]
234
+ )
235
+ assert len(graph["schema"]["edge_types"]) == len(
236
+ modern_graph_schema["edge_types"]
237
+ )
238
+ # delete
239
+ for edge in modern_graph_schema["edge_types"]:
240
+ for relation in edge["vertex_type_pair_relations"]:
241
+ delete_edge_type_by_name(
242
+ self._graph_id,
243
+ edge["type_name"],
244
+ relation["source_vertex"],
245
+ relation["destination_vertex"],
246
+ )
247
+ for vertex in modern_graph_schema["vertex_types"]:
248
+ delete_vertex_type_by_name(self._graph_id, vertex["type_name"])
249
+
250
+ def test_dataloading(self, tmpdir):
251
+ # person
252
+ person = tmpdir.join("person.csv")
253
+ person.write("id|name|age\n1|marko|29\n2|vadas|27\n4|josh|32\n6|peter|35")
254
+ # person -> knows -> person
255
+ person_knows_person = tmpdir.join("person_knows_person.csv")
256
+ person_knows_person.write("person.id|person.id|weight\n1|2|0.5\n1|4|1.0")
257
+ # data source mapping
258
+ datasource = {
259
+ "vertex_mappings": [
260
+ {
261
+ "type_name": "person2",
262
+ "inputs": [upload_file(str(person))],
263
+ "column_mappings": [
264
+ {"column": {"index": 0}, "property": "id"},
265
+ {"column": {"index": 1}, "property": "name"},
266
+ {"column": {"index": 2}, "property": "age"},
267
+ ],
268
+ }
269
+ ],
270
+ "edge_mappings": [
271
+ {
272
+ "type_triplet": {
273
+ "edge": "knows2",
274
+ "source_vertex": "person2",
275
+ "destination_vertex": "person2",
276
+ },
277
+ "inputs": [upload_file(str(person_knows_person))],
278
+ "source_vertex_mappings": [
279
+ {"column": {"index": 0}, "property": "id"}
280
+ ],
281
+ "destination_vertex_mappings": [
282
+ {"column": {"index": 1}, "property": "id"}
283
+ ],
284
+ "column_mappings": [{"column": {"index": 2}, "property": "weight"}],
285
+ }
286
+ ],
287
+ }
288
+ # test bind data source
289
+ create_vertex_type(self._graph_id, modern_graph_person_type)
290
+ create_edge_type(self._graph_id, modern_graph_knows_type)
291
+ bind_datasource_in_batch(self._graph_id, datasource)
292
+ ds = get_datasource_by_id(self._graph_id).to_dict()
293
+ for vertex_mapping in datasource["vertex_mappings"]:
294
+ for vertex_mapping2 in ds["vertex_mappings"]:
295
+ if vertex_mapping["type_name"] == vertex_mapping2["type_name"]:
296
+ assert vertex_mapping == vertex_mapping2
297
+ for edge_mapping in datasource["edge_mappings"]:
298
+ for edge_mapping2 in ds["edge_mappings"]:
299
+ if edge_mapping["type_triplet"] == edge_mapping2["type_triplet"]:
300
+ assert edge_mapping == edge_mapping2
301
+ # test data loading
302
+ job_config = {
303
+ "loading_config": {
304
+ "import_option": "overwrite",
305
+ },
306
+ "vertices": [
307
+ {"type_name": "person2"},
308
+ ],
309
+ "edges": [
310
+ {
311
+ "type_name": "knows2",
312
+ "source_vertex": "person2",
313
+ "destination_vertex": "person2",
314
+ },
315
+ ],
316
+ }
317
+ scheduler_id = submit_dataloading_job(self._graph_id, job_config)
318
+ start_time = time.time()
319
+ # waiting for 30s
320
+ flag = False
321
+ while True:
322
+ time.sleep(1)
323
+ jobs = list_jobs()
324
+ for status in jobs:
325
+ if status.detail["scheduler_id"] == scheduler_id:
326
+ if status.status == "SUCCESS":
327
+ flag = True
328
+ break
329
+ if flag:
330
+ break
331
+ if time.time() - start_time > 30:
332
+ raise TimeoutError(f"Waiting timeout for loading job {scheduler_id}")
333
+ # delete edge type
334
+ for relation in modern_graph_knows_type["vertex_type_pair_relations"]:
335
+ delete_edge_type_by_name(
336
+ self._graph_id,
337
+ modern_graph_knows_type["type_name"],
338
+ relation["source_vertex"],
339
+ relation["destination_vertex"],
340
+ )
341
+ # delete vertex type
342
+ delete_vertex_type_by_name(
343
+ self._graph_id, modern_graph_person_type["type_name"]
344
+ )
345
+
346
+ def test_alert(self):
347
+ # list alert rules
348
+ alert_rules = list_alert_rules()
349
+ high_disk_utilication_alert = None
350
+ for rule in alert_rules:
351
+ if rule.name == "HighDiskUtilization":
352
+ high_disk_utilication_alert = rule.to_dict()
353
+ assert high_disk_utilication_alert["enable"] is True
354
+ # update alert rule
355
+ high_disk_utilication_alert["enable"] = False
356
+ update_alert_rule_by_id(
357
+ high_disk_utilication_alert["id"], high_disk_utilication_alert
358
+ )
359
+ alert_rules = list_alert_rules()
360
+ for rule in alert_rules:
361
+ if rule.name == "HighDiskUtilization":
362
+ assert rule.enable is False
363
+ # delete alert rule
364
+ delete_alert_rule_by_id(high_disk_utilication_alert["id"])
365
+ alert_rules = list_alert_rules()
366
+ for rule in alert_rules:
367
+ if rule.name == "HighDiskUtilization":
368
+ assert False
369
+ # list alert messages
370
+ alert_mesages = list_alert_messages()
371
+ assert alert_mesages is not None
372
+ # create alert receiver
373
+ receiver = {
374
+ "type": "webhook",
375
+ "webhook_url": "https://www.abc.com/robot",
376
+ "is_at_all": False,
377
+ "at_user_ids": ["111111"],
378
+ "enable": False,
379
+ }
380
+ create_alert_receiver(receiver)
381
+ # list alert receivers
382
+ alert_receivers = list_alert_receivers()
383
+ assert len(alert_receivers) > 0
384
+ # update alert receiver
385
+ receiver = alert_receivers[0].to_dict()
386
+ receiver["enable"] = True
387
+ receiver["at_user_ids"] = []
388
+ update_alert_receiver_by_id(receiver["id"], receiver)
389
+ alert_receivers = list_alert_receivers()
390
+ for r in alert_receivers:
391
+ if r.id == receiver["id"]:
392
+ assert r.to_dict() == receiver
393
+ # delete alert receiver
394
+ delete_alert_receiver_by_id(receiver["id"])
395
+ alert_receivers = list_alert_receivers()
396
+ for r in alert_receivers:
397
+ if r.id == receiver["id"]:
398
+ assert False
399
+
400
+ def teardown_class(self):
401
+ pass