cornflow 1.1.0a2__py3-none-any.whl → 1.1.1a1__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 (35) hide show
  1. cornflow/app.py +4 -0
  2. cornflow/cli/utils.py +1 -1
  3. cornflow/config.py +10 -2
  4. cornflow/endpoints/__init__.py +14 -0
  5. cornflow/endpoints/execution.py +1 -1
  6. cornflow/endpoints/login.py +26 -6
  7. cornflow/endpoints/reports.py +283 -0
  8. cornflow/migrations/versions/83164be03c23_.py +40 -0
  9. cornflow/migrations/versions/96f00d0961d1_reports_table.py +50 -0
  10. cornflow/models/__init__.py +2 -0
  11. cornflow/models/execution.py +8 -0
  12. cornflow/models/meta_models.py +23 -12
  13. cornflow/models/reports.py +119 -0
  14. cornflow/schemas/execution.py +3 -0
  15. cornflow/schemas/reports.py +48 -0
  16. cornflow/shared/const.py +21 -0
  17. cornflow/shared/exceptions.py +20 -9
  18. cornflow/static/v1.json +3854 -0
  19. cornflow/tests/const.py +7 -0
  20. cornflow/tests/{custom_liveServer.py → custom_live_server.py} +3 -1
  21. cornflow/tests/custom_test_case.py +2 -3
  22. cornflow/tests/integration/test_commands.py +1 -1
  23. cornflow/tests/integration/test_cornflowclient.py +116 -28
  24. cornflow/tests/unit/test_alarms.py +22 -9
  25. cornflow/tests/unit/test_cli.py +10 -5
  26. cornflow/tests/unit/test_commands.py +6 -2
  27. cornflow/tests/unit/test_executions.py +5 -0
  28. cornflow/tests/unit/test_main_alarms.py +8 -0
  29. cornflow/tests/unit/test_reports.py +308 -0
  30. cornflow/tests/unit/test_users.py +5 -2
  31. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/METADATA +31 -31
  32. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/RECORD +35 -28
  33. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/WHEEL +1 -1
  34. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/entry_points.txt +0 -0
  35. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/top_level.txt +0 -0
cornflow/tests/const.py CHANGED
@@ -14,6 +14,7 @@ INSTANCE_URL = PREFIX + "/instance/"
14
14
  INSTANCE_MPS = _get_file("./data/test_mps.mps")
15
15
  INSTANCE_GC_20 = _get_file("./data/gc_20_7.json")
16
16
  INSTANCE_FILE_FAIL = _get_file("./unit/test_instances.py")
17
+ INSTANCE_TSP = _get_file("./data/tsp_instance.json")
17
18
 
18
19
  EXECUTION_PATH = _get_file("./data/new_execution.json")
19
20
  BAD_EXECUTION_PATH = _get_file("./data/bad_execution.json")
@@ -34,6 +35,12 @@ CASE_INSTANCE_URL = PREFIX + "/case/instance/"
34
35
  FULL_CASE_PATH = _get_file("./data/full_case_raw.json")
35
36
  FULL_CASE_LIST = [FULL_CASE_PATH, _get_file("./data/full_case_raw_2.json")]
36
37
 
38
+ REPORT_PATH = _get_file("./data/new_report.json")
39
+ REPORT_HTML_FILE_PATH = _get_file("./data/new_report.html")
40
+ REPORT_PDF_FILE_PATH = _get_file("./data/new_report_2.pdf")
41
+ BAD_REPORT_PATH = _get_file("./data/bad_report.json")
42
+ REPORT_URL = PREFIX + "/report/"
43
+
37
44
  JSON_PATCH_GOOD_PATH = _get_file("./data/json_patch_good.json")
38
45
  JSON_PATCH_BAD_PATH = _get_file("./data/json_patch_bad.json")
39
46
  FULL_CASE_JSON_PATCH_1 = _get_file("./data/full_case_patch.json")
@@ -39,7 +39,9 @@ class CustomTestCaseLive(LiveServerTestCase):
39
39
  if create_all:
40
40
  db.create_all()
41
41
  access_init_command(False)
42
- register_deployed_dags_command_test(verbose=False)
42
+ register_deployed_dags_command_test(
43
+ verbose=False, dags=["solve_model_dag", "gc", "timer", "tsp"]
44
+ )
43
45
  user_data = dict(
44
46
  username="testname",
45
47
  email="test@test.com",
@@ -90,8 +90,8 @@ class CustomTestCase(TestCase):
90
90
  self.roles_with_access = []
91
91
 
92
92
  @staticmethod
93
- def get_header_with_auth(token):
94
- return {"Content-Type": "application/json", "Authorization": "Bearer " + token}
93
+ def get_header_with_auth(token, content_type="application/json"):
94
+ return {"Content-Type": content_type, "Authorization": "Bearer " + token}
95
95
 
96
96
  def create_user(self, data):
97
97
  return self.client.post(
@@ -169,7 +169,6 @@ class CustomTestCase(TestCase):
169
169
  self.assertEqual(row.id, response.json["id"])
170
170
 
171
171
  for key in self.get_keys_to_check(payload):
172
- getattr(row, key)
173
172
  if key in payload:
174
173
  self.assertEqual(getattr(row, key), payload[key])
175
174
  return row.id
@@ -3,7 +3,7 @@ from flask import current_app
3
3
  from cornflow.commands.dag import register_deployed_dags_command
4
4
  from cornflow.models import DeployedDAG
5
5
  from cornflow.tests.const import PUBLIC_DAGS
6
- from cornflow.tests.custom_liveServer import CustomTestCaseLive
6
+ from cornflow.tests.custom_live_server import CustomTestCaseLive
7
7
 
8
8
 
9
9
  class TestCornflowCommands(CustomTestCaseLive):
@@ -1,17 +1,17 @@
1
1
  """
2
-
2
+ Main script to run the integration tests of cornflow-server
3
3
  """
4
- # Full imports
4
+
5
5
  import json
6
- import pulp
7
6
  import logging as log
7
+ import os
8
8
  import time
9
+ from typing import Callable, Any
9
10
 
10
- # Imports from environment
11
+ import pulp
11
12
  from cornflow_client import CornFlowApiError
12
13
  from cornflow_client.constants import INSTANCE_SCHEMA, SOLUTION_SCHEMA
13
14
 
14
- # Import internal modules
15
15
  from cornflow.app import create_app
16
16
  from cornflow.shared.const import (
17
17
  EXEC_STATE_CORRECT,
@@ -19,12 +19,13 @@ from cornflow.shared.const import (
19
19
  EXEC_STATE_RUNNING,
20
20
  EXEC_STATE_QUEUED,
21
21
  STATUS_HEALTHY,
22
+ REPORT_STATE,
22
23
  )
23
- from cornflow.tests.const import INSTANCE_PATH, CASE_PATH
24
- from cornflow.tests.custom_liveServer import CustomTestCaseLive
24
+ from cornflow.tests.const import INSTANCE_PATH, CASE_PATH, INSTANCE_MPS, INSTANCE_TSP
25
+ from cornflow.tests.custom_live_server import CustomTestCaseLive
25
26
 
26
27
 
27
- def load_file(_file):
28
+ def load_file(_file: str):
28
29
  with open(_file) as f:
29
30
  temp = json.load(f)
30
31
  return temp
@@ -34,6 +35,7 @@ class TestCornflowClientBasic(CustomTestCaseLive):
34
35
  def setUp(self, create_all=False):
35
36
  super().setUp()
36
37
  self.items_to_check = ["name", "description"]
38
+ log.info(f"Start test case name: {self.id()}")
37
39
 
38
40
  def check_status_evolution(self, execution, end_state=EXEC_STATE_CORRECT):
39
41
  statuses = [execution["state"]]
@@ -127,7 +129,7 @@ class TestCornflowClientBasic(CustomTestCaseLive):
127
129
  return execution
128
130
 
129
131
  def create_instance_and_execution(self):
130
- one_instance = self.create_new_instance("./cornflow/tests/data/test_mps.mps")
132
+ one_instance = self.create_new_instance(INSTANCE_MPS)
131
133
  name = "test_execution_name_123"
132
134
  description = "test_execution_description_123"
133
135
  schema = "solve_model_dag"
@@ -140,6 +142,31 @@ class TestCornflowClientBasic(CustomTestCaseLive):
140
142
  )
141
143
  return self.create_new_execution(payload)
142
144
 
145
+ def create_instance_and_execution_report(
146
+ self,
147
+ schema="tsp",
148
+ solver="cpsat",
149
+ data=None,
150
+ timeLimit=10,
151
+ report_name="report",
152
+ ):
153
+ name = "test_instance_1"
154
+ description = "description123"
155
+ if data is None:
156
+ data = load_file(INSTANCE_TSP)
157
+ payload = dict(data=data, name=name, description=description, schema=schema)
158
+ one_instance = self.create_new_instance_payload(payload)
159
+ payload = dict(
160
+ instance_id=one_instance["id"],
161
+ config=dict(
162
+ solver=solver, timeLimit=timeLimit, report=dict(name=report_name)
163
+ ),
164
+ description="test_execution_description_123",
165
+ name="test_execution_123",
166
+ schema=schema,
167
+ )
168
+ return self.create_new_execution(payload)
169
+
143
170
  def create_timer_instance_and_execution(self, seconds=5):
144
171
  payload = dict(
145
172
  data=dict(seconds=seconds),
@@ -157,16 +184,47 @@ class TestCornflowClientBasic(CustomTestCaseLive):
157
184
  )
158
185
  return self.create_new_execution(payload)
159
186
 
187
+ @staticmethod
188
+ def try_until_condition(
189
+ func: Callable,
190
+ condition: Callable[[Any], bool],
191
+ number_of_times: int = 10,
192
+ sleep_time: float = 10,
193
+ ):
194
+ for i in range(number_of_times):
195
+ time.sleep(sleep_time)
196
+ result = func()
197
+ if condition(result):
198
+ return result
199
+ raise TimeoutError(
200
+ "Timed out after {} seconds".format(number_of_times * sleep_time)
201
+ )
202
+
203
+ @staticmethod
204
+ def wait_until_report_finishes(
205
+ client, execution_id: str, report_status=REPORT_STATE.CORRECT
206
+ ):
207
+ def func():
208
+ my_reports = client.raw.get_results(execution_id).json()["reports"]
209
+ if len(my_reports) == 0:
210
+ return None
211
+ first = my_reports[0]
212
+ if first["state"] != report_status:
213
+ return None
214
+ return first
215
+
216
+ return func
217
+
160
218
 
161
219
  class TestCornflowClientOpen(TestCornflowClientBasic):
162
220
  # TODO: user management
163
221
  # TODO: infeasible execution
164
222
 
165
223
  def test_new_instance_file(self):
166
- self.create_new_instance_file("./cornflow/tests/data/test_mps.mps")
224
+ self.create_new_instance_file(INSTANCE_MPS)
167
225
 
168
226
  def test_new_instance(self):
169
- return self.create_new_instance("./cornflow/tests/data/test_mps.mps")
227
+ return self.create_new_instance(INSTANCE_MPS)
170
228
 
171
229
  # TODO: reactivate test with new version of cornflow client which allows to pass
172
230
  # optional arguments for the headers of the request
@@ -189,6 +247,49 @@ class TestCornflowClientOpen(TestCornflowClientBasic):
189
247
  def test_new_execution(self):
190
248
  return self.create_instance_and_execution()
191
249
 
250
+ def test_new_execution_with_tsp_report(self):
251
+ return self.create_instance_and_execution_report()
252
+
253
+ #
254
+ def test_new_execution_with_tsp_report_wait(self):
255
+ execution = self.create_instance_and_execution_report()
256
+ func = self.wait_until_report_finishes(self.client, execution["id"])
257
+ reports_info = self.try_until_condition(func, lambda v: v is not None, 20, 5)
258
+ id_report = reports_info["id"]
259
+ my_name = "./my_report.html"
260
+ try:
261
+ os.remove(my_name)
262
+ except:
263
+ pass
264
+ self.client.raw.get_one_report(id_report, "./", my_name)
265
+ self.assertTrue(os.path.exists(my_name))
266
+ try:
267
+ os.remove(my_name)
268
+ except OSError:
269
+ pass
270
+
271
+ # read header of file? we can parse it with beatifulsoup
272
+
273
+ def test_new_execution_with_timer_report_wait(self):
274
+ payload = dict(
275
+ solver="default", schema="timer", data={"seconds": 1}, timeLimit=1
276
+ )
277
+ execution = self.create_instance_and_execution_report(**payload)
278
+ func = self.wait_until_report_finishes(self.client, execution["id"])
279
+ reports_info = self.try_until_condition(func, lambda v: v is not None, 20, 5)
280
+ id_report = reports_info["id"]
281
+ my_name = "./my_report.html"
282
+ try:
283
+ os.remove(my_name)
284
+ except:
285
+ pass
286
+ self.client.raw.get_one_report(id_report, "./", my_name)
287
+ self.assertTrue(os.path.exists(my_name))
288
+ try:
289
+ os.remove(my_name)
290
+ except OSError:
291
+ pass
292
+
192
293
  def test_delete_execution(self):
193
294
  execution = self.test_new_execution()
194
295
  response = self.client.raw.get_api_for_id("execution/", execution["id"])
@@ -213,7 +314,7 @@ class TestCornflowClientOpen(TestCornflowClientBasic):
213
314
  self.assertTrue("error" in response.json())
214
315
 
215
316
  def test_new_execution_bad_dag_name(self):
216
- one_instance = self.create_new_instance("./cornflow/tests/data/test_mps.mps")
317
+ one_instance = self.create_new_instance(INSTANCE_MPS)
217
318
  name = "test_execution_name_123"
218
319
  description = "test_execution_description_123"
219
320
  payload = dict(
@@ -227,7 +328,7 @@ class TestCornflowClientOpen(TestCornflowClientBasic):
227
328
  self.assertRaises(CornFlowApiError, _bad_func)
228
329
 
229
330
  def test_new_execution_with_schema(self):
230
- one_instance = self.create_new_instance("./cornflow/tests/data/test_mps.mps")
331
+ one_instance = self.create_new_instance(INSTANCE_MPS)
231
332
  name = "test_execution_name_123"
232
333
  description = "test_execution_description_123"
233
334
  payload = dict(
@@ -309,19 +410,6 @@ class TestCornflowClientAdmin(TestCornflowClientBasic):
309
410
  def setUp(self, create_all=False):
310
411
  super().setUp()
311
412
 
312
- # we create a service user:
313
- self.create_service_user(
314
- dict(username="airflow", pwd="Airflow_test_password1", email="af@cf.com")
315
- )
316
-
317
- self.create_service_user(
318
- dict(
319
- username="service_user@cornflow.com",
320
- pwd="Serviceuser_1234",
321
- email="service_user@cornflow.com",
322
- )
323
- )
324
-
325
413
  # we create an admin user
326
414
  # we guarantee that the admin is there for airflow
327
415
  self.client.token = self.create_admin(
@@ -404,7 +492,7 @@ class TestCornflowClientAdmin(TestCornflowClientBasic):
404
492
  self.assertIsNone(execution_data["data"])
405
493
 
406
494
  def test_edit_one_execution(self):
407
- one_instance = self.create_new_instance("./cornflow/tests/data/test_mps.mps")
495
+ one_instance = self.create_new_instance(INSTANCE_MPS)
408
496
  payload = dict(
409
497
  name="bla",
410
498
  config=dict(solver="CBC"),
@@ -455,7 +543,7 @@ class TestCornflowClientAdmin(TestCornflowClientBasic):
455
543
  self.assertRaises(CornFlowApiError, _launch_too_soon_func)
456
544
 
457
545
  def test_check_instance(self):
458
- instance = self.create_new_instance("./cornflow/tests/data/test_mps.mps")
546
+ instance = self.create_new_instance(INSTANCE_MPS)
459
547
  data_check_execution = self.client.create_instance_data_check(instance["id"])
460
548
  self.assertEqual(data_check_execution["instance_id"], instance["id"])
461
549
  status = self.client.get_status(data_check_execution["id"])
@@ -1,6 +1,10 @@
1
1
  """
2
2
 
3
3
  """
4
+
5
+ import unittest
6
+ import os
7
+
4
8
  # Imports from internal modules
5
9
  from cornflow.models import AlarmsModel
6
10
  from cornflow.tests.const import ALARMS_URL
@@ -15,25 +19,34 @@ class TestAlarmsEndpoint(CustomTestCase):
15
19
  self.response_items = {"id", "name", "description", "criticality", "schema"}
16
20
  self.items_to_check = ["name", "description", "schema", "criticality"]
17
21
 
22
+ @unittest.skipUnless(
23
+ int(os.getenv("CF_ALARMS_ENDPOINT")) == 1, "No alarms implemented"
24
+ )
18
25
  def test_post_alarm(self):
19
- payload = {"name": "Alarm 1", "description": "Description Alarm 1", "criticality": 1}
26
+ payload = {
27
+ "name": "Alarm 1",
28
+ "description": "Description Alarm 1",
29
+ "criticality": 1,
30
+ }
20
31
  self.create_new_row(self.url, self.model, payload)
21
32
 
33
+ @unittest.skipUnless(
34
+ int(os.getenv("CF_ALARMS_ENDPOINT")) == 1, "No alarms implemented"
35
+ )
22
36
  def test_get_alarms(self):
23
37
  data = [
24
38
  {"name": "Alarm 1", "description": "Description Alarm 1", "criticality": 1},
25
- {"name": "Alarm 2", "description": "Description Alarm 2", "criticality": 2, "schema": "solve_model_dag"},
39
+ {
40
+ "name": "Alarm 2",
41
+ "description": "Description Alarm 2",
42
+ "criticality": 2,
43
+ "schema": "solve_model_dag",
44
+ },
26
45
  ]
27
- rows = self.get_rows(
28
- self.url,
29
- data,
30
- check_data=False
31
- )
46
+ rows = self.get_rows(self.url, data, check_data=False)
32
47
  rows_data = list(rows.json)
33
48
  for i in range(len(data)):
34
49
  for key in self.get_keys_to_check(data[i]):
35
50
  self.assertIn(key, rows_data[i])
36
51
  if key in data[i]:
37
52
  self.assertEqual(rows_data[i][key], data[i][key])
38
-
39
-
@@ -20,6 +20,11 @@ from cornflow.shared.exceptions import NoPermission, ObjectDoesNotExist
20
20
  class CLITests(TestCase):
21
21
  def setUp(self):
22
22
  db.create_all()
23
+ self.number_of_views = 52
24
+ self.number_of_permissions = 574
25
+ if int(os.getenv("CF_ALARMS_ENDPOINT", 0)) != 1:
26
+ self.number_of_views = 50
27
+ self.number_of_permissions = 519
23
28
 
24
29
  def tearDown(self):
25
30
  db.session.remove()
@@ -131,7 +136,7 @@ class CLITests(TestCase):
131
136
  result = runner.invoke(cli, ["views", "init", "-v"])
132
137
  self.assertEqual(result.exit_code, 0)
133
138
  views = ViewModel.get_all_objects().all()
134
- self.assertEqual(len(views), 49)
139
+ self.assertEqual(len(views), self.number_of_views)
135
140
 
136
141
  def test_permissions_entrypoint(self):
137
142
  runner = CliRunner()
@@ -155,8 +160,8 @@ class CLITests(TestCase):
155
160
  permissions = PermissionViewRoleModel.get_all_objects().all()
156
161
  self.assertEqual(len(actions), 5)
157
162
  self.assertEqual(len(roles), 4)
158
- self.assertEqual(len(views), 49)
159
- self.assertEqual(len(permissions), 546)
163
+ self.assertEqual(len(views), self.number_of_views)
164
+ self.assertEqual(len(permissions), self.number_of_permissions)
160
165
 
161
166
  def test_permissions_base_command(self):
162
167
  runner = CliRunner()
@@ -171,8 +176,8 @@ class CLITests(TestCase):
171
176
  permissions = PermissionViewRoleModel.get_all_objects().all()
172
177
  self.assertEqual(len(actions), 5)
173
178
  self.assertEqual(len(roles), 4)
174
- self.assertEqual(len(views), 49)
175
- self.assertEqual(len(permissions), 546)
179
+ self.assertEqual(len(views), self.number_of_views)
180
+ self.assertEqual(len(permissions), self.number_of_permissions)
176
181
 
177
182
  def test_service_entrypoint(self):
178
183
  runner = CliRunner()
@@ -1,5 +1,5 @@
1
1
  import json
2
-
2
+ import os
3
3
  from flask_testing import TestCase
4
4
 
5
5
  from cornflow.app import (
@@ -48,7 +48,11 @@ class TestCommands(TestCase):
48
48
  "email": "testemail@test.org",
49
49
  "password": "Testpassword1!",
50
50
  }
51
- self.resources = resources + alarms_resources
51
+
52
+ if int(os.getenv("CF_ALARMS_ENDPOINT")) == 1:
53
+ self.resources = resources + alarms_resources
54
+ else:
55
+ self.resources = resources
52
56
  self.runner = self.create_app().test_cli_runner()
53
57
  self.runner.invoke(register_roles, ["-v"])
54
58
 
@@ -57,6 +57,7 @@ class TestExecutionsListEndpoint(BaseTestCases.ListFilters):
57
57
  "instance_id",
58
58
  "name",
59
59
  "indicators",
60
+ "reports",
60
61
  ]
61
62
 
62
63
  def test_new_execution(self):
@@ -260,6 +261,7 @@ class TestExecutionsDetailEndpointMock(CustomTestCase):
260
261
  "schema",
261
262
  "user_id",
262
263
  "indicators",
264
+ "reports",
263
265
  }
264
266
  # we only check the following because this endpoint does not return data
265
267
  self.items_to_check = ["name", "description"]
@@ -303,6 +305,7 @@ class TestExecutionsDetailEndpoint(
303
305
  "name",
304
306
  "created_at",
305
307
  "state",
308
+ "reports",
306
309
  ]
307
310
  execution = self.get_one_row(
308
311
  self.url + idx,
@@ -408,6 +411,7 @@ class TestExecutionsDataEndpoint(TestExecutionsDetailEndpointMock):
408
411
  "state",
409
412
  "name",
410
413
  "id",
414
+ "reports",
411
415
  ]
412
416
 
413
417
  def test_get_one_execution(self):
@@ -450,6 +454,7 @@ class TestExecutionsLogEndpoint(TestExecutionsDetailEndpointMock):
450
454
  "user_id",
451
455
  "config",
452
456
  "indicators",
457
+ "reports",
453
458
  ]
454
459
 
455
460
  def test_get_one_execution(self):
@@ -2,6 +2,8 @@
2
2
 
3
3
  """
4
4
 
5
+ import unittest
6
+ import os
5
7
  import json
6
8
 
7
9
  # Imports from internal modules
@@ -29,6 +31,9 @@ class TestMainAlarmsEndpoint(CustomTestCase):
29
31
  headers=self.get_header_with_auth(self.token),
30
32
  ).json["id"]
31
33
 
34
+ @unittest.skipUnless(
35
+ int(os.getenv("CF_ALARMS_ENDPOINT")) == 1, "No alarms implemented"
36
+ )
32
37
  def test_post_main_alarm(self):
33
38
  payload = {
34
39
  "message": "Message Main Alarm 1",
@@ -37,6 +42,9 @@ class TestMainAlarmsEndpoint(CustomTestCase):
37
42
  }
38
43
  self.create_new_row(self.url, self.model, payload)
39
44
 
45
+ @unittest.skipUnless(
46
+ int(os.getenv("CF_ALARMS_ENDPOINT")) == 1, "No alarms implemented"
47
+ )
40
48
  def test_get_main_alarms(self):
41
49
  data = [
42
50
  {