cornflow 1.2.3a5__py3-none-any.whl → 1.3.0rc1__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 (57) hide show
  1. cornflow/app.py +24 -8
  2. cornflow/cli/service.py +93 -43
  3. cornflow/commands/auxiliar.py +4 -1
  4. cornflow/commands/dag.py +7 -7
  5. cornflow/commands/permissions.py +13 -7
  6. cornflow/commands/views.py +3 -0
  7. cornflow/config.py +26 -6
  8. cornflow/endpoints/__init__.py +27 -0
  9. cornflow/endpoints/case.py +37 -21
  10. cornflow/endpoints/dag.py +5 -5
  11. cornflow/endpoints/data_check.py +8 -7
  12. cornflow/endpoints/example_data.py +4 -2
  13. cornflow/endpoints/execution.py +215 -127
  14. cornflow/endpoints/health.py +30 -11
  15. cornflow/endpoints/instance.py +3 -3
  16. cornflow/endpoints/login.py +9 -2
  17. cornflow/endpoints/permission.py +1 -2
  18. cornflow/endpoints/schemas.py +3 -3
  19. cornflow/endpoints/signup.py +17 -11
  20. cornflow/migrations/versions/999b98e24225.py +34 -0
  21. cornflow/migrations/versions/cef1df240b27_.py +34 -0
  22. cornflow/models/__init__.py +2 -1
  23. cornflow/models/dag.py +8 -9
  24. cornflow/models/dag_permissions.py +3 -3
  25. cornflow/models/execution.py +2 -3
  26. cornflow/models/permissions.py +1 -0
  27. cornflow/models/user.py +1 -1
  28. cornflow/schemas/execution.py +14 -1
  29. cornflow/schemas/health.py +1 -1
  30. cornflow/shared/authentication/auth.py +14 -1
  31. cornflow/shared/authentication/decorators.py +32 -2
  32. cornflow/shared/const.py +58 -1
  33. cornflow/shared/exceptions.py +2 -1
  34. cornflow/tests/base_test_execution.py +798 -0
  35. cornflow/tests/const.py +1 -0
  36. cornflow/tests/integration/test_commands.py +2 -2
  37. cornflow/tests/integration/test_cornflowclient.py +2 -1
  38. cornflow/tests/unit/test_apiview.py +7 -1
  39. cornflow/tests/unit/test_cases.py +1 -1
  40. cornflow/tests/unit/test_cli.py +6 -3
  41. cornflow/tests/unit/test_commands.py +7 -6
  42. cornflow/tests/unit/test_dags.py +3 -3
  43. cornflow/tests/unit/test_example_data.py +1 -1
  44. cornflow/tests/unit/test_executions.py +115 -535
  45. cornflow/tests/unit/test_get_resources.py +103 -0
  46. cornflow/tests/unit/test_health.py +84 -3
  47. cornflow/tests/unit/test_main_alarms.py +1 -1
  48. cornflow/tests/unit/test_roles.py +2 -1
  49. cornflow/tests/unit/test_schema_from_models.py +1 -1
  50. cornflow/tests/unit/test_schemas.py +1 -1
  51. cornflow/tests/unit/test_sign_up.py +181 -6
  52. cornflow/tests/unit/tools.py +93 -10
  53. {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/METADATA +3 -3
  54. {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/RECORD +57 -53
  55. {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/WHEEL +0 -0
  56. {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/entry_points.txt +0 -0
  57. {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/top_level.txt +0 -0
@@ -6,13 +6,16 @@ These endpoints hve different access url, but manage the same data entities
6
6
 
7
7
  # Import from libraries
8
8
  from cornflow_client.airflow.api import Airflow
9
+
10
+ from cornflow_client.databricks.api import Databricks
9
11
  from cornflow_client.constants import INSTANCE_SCHEMA, CONFIG_SCHEMA, SOLUTION_SCHEMA
10
12
  from flask import request, current_app
11
13
  from flask_apispec import marshal_with, use_kwargs, doc
12
14
 
13
15
  # Import from internal modules
14
16
  from cornflow.endpoints.meta_resource import BaseMetaResource
15
- from cornflow.models import InstanceModel, DeployedDAG, ExecutionModel
17
+ from cornflow.models import InstanceModel, DeployedWorkflow, ExecutionModel
18
+ from cornflow.shared.const import config_orchestrator
16
19
  from cornflow.schemas.execution import (
17
20
  ExecutionDetailsEndpointResponse,
18
21
  ExecutionDetailsEndpointWithIndicatorsResponse,
@@ -28,6 +31,10 @@ from cornflow.schemas.execution import (
28
31
  )
29
32
  from cornflow.shared.authentication import Auth, authenticate
30
33
  from cornflow.shared.compress import compressed
34
+ from cornflow.shared.const import (
35
+ AIRFLOW_BACKEND,
36
+ DATABRICKS_BACKEND,
37
+ )
31
38
  from cornflow.shared.const import (
32
39
  AIRFLOW_ERROR_MSG,
33
40
  AIRFLOW_NOT_REACHABLE_MSG,
@@ -39,24 +46,85 @@ from cornflow.shared.const import (
39
46
  EXEC_STATE_UNKNOWN,
40
47
  EXECUTION_STATE_MESSAGE_DICT,
41
48
  AIRFLOW_TO_STATE_MAP,
49
+ DATABRICKS_TO_STATE_MAP,
42
50
  EXEC_STATE_STOPPED,
43
51
  EXEC_STATE_QUEUED,
44
52
  )
45
- from cornflow.shared.exceptions import AirflowError, ObjectDoesNotExist, InvalidData
53
+
54
+ from cornflow.shared.exceptions import (
55
+ AirflowError,
56
+ DatabricksError,
57
+ ObjectDoesNotExist,
58
+ InvalidData,
59
+ EndpointNotImplemented,
60
+ )
46
61
  from cornflow.shared.validators import (
47
62
  json_schema_validate_as_string,
48
63
  json_schema_extend_and_validate_as_string,
49
64
  )
50
65
 
51
66
 
52
- class ExecutionEndpoint(BaseMetaResource):
67
+ class OrchestratorMixin(BaseMetaResource):
68
+ """
69
+ Base class that provides orchestrator-related functionality for execution endpoints.
70
+ This mixin handles the initialization and properties for orchestrator clients (Airflow/Databricks).
71
+ """
72
+
73
+ def __init__(self):
74
+ super().__init__()
75
+ self._orch_type = None
76
+ self._orch_client = None
77
+ self._orch_error = None
78
+ self._orch_to_state_map = None
79
+ self._orch_const = None
80
+
81
+ def _init_orch(self):
82
+ if self._orch_type is None:
83
+ self._orch_type = current_app.config["CORNFLOW_BACKEND"]
84
+ if self._orch_type == AIRFLOW_BACKEND:
85
+ self._orch_client = Airflow.from_config(current_app.config)
86
+ self._orch_error = AirflowError
87
+ self._orch_to_state_map = AIRFLOW_TO_STATE_MAP
88
+ self._orch_const = config_orchestrator["airflow"]
89
+ elif self._orch_type == DATABRICKS_BACKEND:
90
+ self._orch_client = Databricks.from_config(current_app.config)
91
+ self._orch_error = DatabricksError
92
+ self._orch_to_state_map = DATABRICKS_TO_STATE_MAP
93
+ self._orch_const = config_orchestrator["databricks"]
94
+
95
+ @property
96
+ def orch_type(self):
97
+ self._init_orch()
98
+ return self._orch_type
99
+
100
+ @property
101
+ def orch_client(self):
102
+ self._init_orch()
103
+ return self._orch_client
104
+
105
+ @property
106
+ def orch_error(self):
107
+ self._init_orch()
108
+ return self._orch_error
109
+
110
+ @property
111
+ def orch_to_state_map(self):
112
+ self._init_orch()
113
+ return self._orch_to_state_map
114
+
115
+ @property
116
+ def orch_const(self):
117
+ self._init_orch()
118
+ return self._orch_const
119
+
120
+
121
+ class ExecutionEndpoint(OrchestratorMixin):
53
122
  """
54
123
  Endpoint used to create a new execution or get all the executions and their information back
55
124
  """
56
125
 
57
126
  def __init__(self):
58
127
  super().__init__()
59
- self.model = ExecutionModel
60
128
  self.data_model = ExecutionModel
61
129
  self.foreign_data = {"instance_id": InstanceModel}
62
130
 
@@ -91,36 +159,37 @@ class ExecutionEndpoint(BaseMetaResource):
91
159
  ]
92
160
 
93
161
  for execution in running_executions:
94
- dag_run_id = execution.dag_run_id
95
- if not dag_run_id:
96
- # it's safe to say we will never get anything if we did not store the dag_run_id
162
+ run_id = execution.run_id
163
+
164
+ if not run_id:
165
+ # it's safe to say we will never get anything if we did not store the run_id
97
166
  current_app.logger.warning(
98
167
  "Error while the app tried to update the status of all running executions."
99
168
  f"Execution {execution.id} has status {execution.state} but has no dag run associated."
100
169
  )
101
170
  continue
102
171
 
103
- af_client = Airflow.from_config(current_app.config)
104
- if not af_client.is_alive():
172
+ if not self.orch_client.is_alive(config=current_app.config):
105
173
  current_app.logger.warning(
106
174
  f"Error while the app tried to update the status of all running executions."
107
175
  f"{AIRFLOW_NOT_REACHABLE_MSG}"
108
176
  )
109
177
  continue
110
-
111
178
  try:
112
- response = af_client.get_dag_run_status(
113
- dag_name=execution.schema, dag_run_id=dag_run_id
179
+ response = self.orch_client.get_run_status(
180
+ schema=execution.schema, run_id=run_id
114
181
  )
115
- except AirflowError as err:
182
+ except self.orch_error as err:
116
183
  current_app.logger.warning(
117
184
  "Error while the app tried to update the status of all running executions."
118
185
  f"{AIRFLOW_ERROR_MSG} {err}"
119
186
  )
120
187
  continue
121
-
122
- data = response.json()
123
- state = AIRFLOW_TO_STATE_MAP.get(data["state"], EXEC_STATE_UNKNOWN)
188
+ if self.orch_type == DATABRICKS_BACKEND:
189
+ state = self.orch_to_state_map.get(response, EXEC_STATE_UNKNOWN)
190
+ else:
191
+ data = response.json()
192
+ state = self.orch_to_state_map.get(data["state"], EXEC_STATE_UNKNOWN)
124
193
  execution.update_state(state)
125
194
 
126
195
  return executions
@@ -141,12 +210,11 @@ class ExecutionEndpoint(BaseMetaResource):
141
210
  :rtype: Tuple(dict, integer)
142
211
  """
143
212
 
144
- config = current_app.config
145
-
146
213
  if "schema" not in kwargs:
147
- kwargs["schema"] = "solve_model_dag"
148
-
149
- execution, _ = self.post_list(data=kwargs)
214
+ kwargs["schema"] = self.orch_const["def_schema"]
215
+ # region INDEPENDIENTE A AIRFLOW
216
+ config = current_app.config
217
+ execution, status_code = self.post_list(data=kwargs)
150
218
  instance = InstanceModel.get_one_object(
151
219
  user=self.get_user(), idx=execution.instance_id
152
220
  )
@@ -156,7 +224,7 @@ class ExecutionEndpoint(BaseMetaResource):
156
224
  raise InvalidData(error="Instance and execution schema mismatch")
157
225
 
158
226
  current_app.logger.debug(f"The request is: {request.args.get('run')}")
159
- # this allows testing without airflow interaction:
227
+ # this allows testing without orchestrator interaction:
160
228
  if request.args.get("run", "1") == "0":
161
229
  current_app.logger.info(
162
230
  f"User {self.get_user_id()} creates execution {execution.id} but does not run it."
@@ -164,27 +232,30 @@ class ExecutionEndpoint(BaseMetaResource):
164
232
  execution.update_state(EXEC_STATE_NOT_RUN)
165
233
  return execution, 201
166
234
 
167
- # We now try to launch the task in airflow
168
- af_client = Airflow.from_config(config)
169
- if not af_client.is_alive():
235
+ # We now try to launch the task in the orchestrator
236
+ # Note schema is a string with the name of the job/dag
237
+ schema = execution.schema
238
+ # endregion
170
239
 
171
- current_app.logger.error(AIRFLOW_NOT_REACHABLE_MSG)
240
+ # region VALIDACIONES
241
+ # We check if the job/dag exists and orchestrator is alive
242
+ if not self.orch_client.is_alive(config=current_app.config):
243
+ error = f"{self.orch_const['name']} is not accessible"
244
+ current_app.logger.error(error)
172
245
  execution.update_state(EXEC_STATE_ERROR_START)
173
- raise AirflowError(
174
- error=AIRFLOW_NOT_REACHABLE_MSG,
246
+ raise self.orch_error(
247
+ error=error,
175
248
  payload=dict(
176
249
  message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
177
250
  state=EXEC_STATE_ERROR_START,
178
251
  ),
179
- log_txt=f"Error while user {self.get_user()} tries to create an execution "
180
- + AIRFLOW_NOT_REACHABLE_MSG,
252
+ log_txt=f"Error while user {self.get_user()} tries to create an execution. "
253
+ + error,
181
254
  )
182
- # ask airflow if dag_name exists
183
- schema = execution.schema
184
- schema_info = af_client.get_dag_info(schema)
185
255
 
186
- # Validate config before running the dag
187
- config_schema = DeployedDAG.get_one_schema(config, schema, CONFIG_SCHEMA)
256
+ schema_info = self.orch_client.get_workflow_info(workflow_name=schema)
257
+ # Validate config before running the run
258
+ config_schema = DeployedWorkflow.get_one_schema(config, schema, CONFIG_SCHEMA)
188
259
  new_config, config_errors = json_schema_extend_and_validate_as_string(
189
260
  config_schema, kwargs["config"]
190
261
  )
@@ -204,7 +275,9 @@ class ExecutionEndpoint(BaseMetaResource):
204
275
  execution.update_config(new_config)
205
276
 
206
277
  # Validate instance data before running the dag
207
- instance_schema = DeployedDAG.get_one_schema(config, schema, INSTANCE_SCHEMA)
278
+ instance_schema = DeployedWorkflow.get_one_schema(
279
+ config, schema, INSTANCE_SCHEMA
280
+ )
208
281
  instance_errors = json_schema_validate_as_string(instance_schema, instance.data)
209
282
  if instance_errors:
210
283
  execution.update_state(
@@ -220,7 +293,7 @@ class ExecutionEndpoint(BaseMetaResource):
220
293
  )
221
294
  # Validate solution data before running the dag (if it exists)
222
295
  if kwargs.get("data") is not None:
223
- solution_schema = DeployedDAG.get_one_schema(
296
+ solution_schema = DeployedWorkflow.get_one_schema(
224
297
  config, schema, SOLUTION_SCHEMA
225
298
  )
226
299
  solution_errors = json_schema_validate_as_string(
@@ -234,29 +307,30 @@ class ExecutionEndpoint(BaseMetaResource):
234
307
  )
235
308
  execution.update_log_txt(f"{solution_errors}")
236
309
  raise InvalidData(payload=dict(jsonschema_errors=solution_errors))
237
-
238
- info = schema_info.json()
239
- if info["is_paused"]:
240
-
241
- current_app.logger.error(DAG_PAUSED_MSG)
242
- execution.update_state(EXEC_STATE_ERROR_START)
243
- raise AirflowError(
244
- error=DAG_PAUSED_MSG,
245
- payload=dict(
246
- message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
247
- state=EXEC_STATE_ERROR_START,
248
- ),
249
- log_txt=f"Error while user {self.get_user()} tries to create an execution. "
250
- + DAG_PAUSED_MSG,
251
- )
310
+ # endregion
311
+ # TODO: Consider adding similar checks for databricks
312
+ if self.orch_type == AIRFLOW_BACKEND:
313
+ info = schema_info.json()
314
+ if info["is_paused"]:
315
+ current_app.logger.error(DAG_PAUSED_MSG)
316
+ execution.update_state(EXEC_STATE_ERROR_START)
317
+ raise self.orch_error(
318
+ error=DAG_PAUSED_MSG,
319
+ payload=dict(
320
+ message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
321
+ state=EXEC_STATE_ERROR_START,
322
+ ),
323
+ log_txt=f"Error while user {self.get_user()} tries to create an execution. "
324
+ + DAG_PAUSED_MSG,
325
+ )
252
326
 
253
327
  try:
254
- response = af_client.run_dag(execution.id, dag_name=schema)
255
- except AirflowError as err:
256
- error = f"{AIRFLOW_ERROR_MSG} {err}"
328
+ response = self.orch_client.run_workflow(execution.id, workflow_name=schema)
329
+ except self.orch_error as err:
330
+ error = self.orch_const["name"] + " responded with an error: {}".format(err)
257
331
  current_app.logger.error(error)
258
332
  execution.update_state(EXEC_STATE_ERROR)
259
- raise AirflowError(
333
+ raise self.orch_error(
260
334
  error=error,
261
335
  payload=dict(
262
336
  message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR],
@@ -266,9 +340,11 @@ class ExecutionEndpoint(BaseMetaResource):
266
340
  + error,
267
341
  )
268
342
 
269
- # if we succeed, we register the dag_run_id in the execution table:
270
- af_data = response.json()
271
- execution.dag_run_id = af_data["dag_run_id"]
343
+ # if we succeed, we register the run_id in the execution table:
344
+ orch_data = response.json()
345
+ info = "orch data is " + str(orch_data)
346
+ current_app.logger.info(info)
347
+ execution.run_id = orch_data[self.orch_const["run_id"]]
272
348
  execution.update_state(EXEC_STATE_QUEUED)
273
349
  current_app.logger.info(
274
350
  "User {} creates execution {}".format(self.get_user_id(), execution.id)
@@ -276,7 +352,7 @@ class ExecutionEndpoint(BaseMetaResource):
276
352
  return execution, 201
277
353
 
278
354
 
279
- class ExecutionRelaunchEndpoint(BaseMetaResource):
355
+ class ExecutionRelaunchEndpoint(OrchestratorMixin):
280
356
  def __init__(self):
281
357
  super().__init__()
282
358
  self.model = ExecutionModel
@@ -298,9 +374,8 @@ class ExecutionRelaunchEndpoint(BaseMetaResource):
298
374
  :rtype: Tuple(dict, integer)
299
375
  """
300
376
  config = current_app.config
301
-
302
377
  if "schema" not in kwargs:
303
- kwargs["schema"] = "solve_model_dag"
378
+ kwargs["schema"] = self.orch_const["def_schema"]
304
379
 
305
380
  self.put_detail(
306
381
  data=dict(config=kwargs["config"]), user=self.get_user(), idx=idx
@@ -331,7 +406,7 @@ class ExecutionRelaunchEndpoint(BaseMetaResource):
331
406
  }, 201
332
407
 
333
408
  # Validate config before running the dag
334
- config_schema = DeployedDAG.get_one_schema(
409
+ config_schema = DeployedWorkflow.get_one_schema(
335
410
  config, kwargs["schema"], CONFIG_SCHEMA
336
411
  )
337
412
  config_errors = json_schema_validate_as_string(config_schema, kwargs["config"])
@@ -341,48 +416,46 @@ class ExecutionRelaunchEndpoint(BaseMetaResource):
341
416
  log_txt=f"Error while user {self.get_user()} tries to relaunch execution {idx}. "
342
417
  f"Configuration data does not match the jsonschema.",
343
418
  )
344
-
345
- # We now try to launch the task in airflow
346
- af_client = Airflow.from_config(config)
347
- if not af_client.is_alive():
348
-
349
- current_app.logger.error(AIRFLOW_NOT_REACHABLE_MSG)
350
- execution.update_state(EXEC_STATE_ERROR_START)
351
- raise AirflowError(
352
- error=AIRFLOW_NOT_REACHABLE_MSG,
353
- payload=dict(
354
- message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
355
- state=EXEC_STATE_ERROR_START,
356
- ),
357
- log_txt=f"Error while user {self.get_user()} tries to relaunch execution {idx}. "
358
- + AIRFLOW_NOT_REACHABLE_MSG,
359
- )
360
- # ask airflow if dag_name exists
361
419
  schema = execution.schema
362
- schema_info = af_client.get_dag_info(schema)
363
-
364
- info = schema_info.json()
365
- if info["is_paused"]:
366
420
 
367
- current_app.logger.error(DAG_PAUSED_MSG)
421
+ # Check if orchestrator is alive
422
+ if not self.orch_client.is_alive(config=current_app.config):
423
+ error = f"{self.orch_const['name']} is not accessible"
424
+ current_app.logger.error(error)
368
425
  execution.update_state(EXEC_STATE_ERROR_START)
369
- raise AirflowError(
370
- error=DAG_PAUSED_MSG,
426
+ raise self.orch_error(
427
+ error=error,
371
428
  payload=dict(
372
429
  message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
373
430
  state=EXEC_STATE_ERROR_START,
374
431
  ),
375
432
  log_txt=f"Error while user {self.get_user()} tries to relaunch execution {idx}. "
376
- + DAG_PAUSED_MSG,
433
+ + error,
377
434
  )
378
435
 
436
+ schema_info = self.orch_client.get_workflow_info(workflow_name=schema)
437
+ info = schema_info.json()
438
+ if self.orch_type == AIRFLOW_BACKEND:
439
+ if info["is_paused"]:
440
+ current_app.logger.error(AIRFLOW_NOT_REACHABLE_MSG)
441
+ execution.update_state(EXEC_STATE_ERROR_START)
442
+ raise self.orch_error(
443
+ error=AIRFLOW_NOT_REACHABLE_MSG,
444
+ payload=dict(
445
+ message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
446
+ state=EXEC_STATE_ERROR_START,
447
+ ),
448
+ log_txt=f"Error while user {self.get_user()} tries to relaunch execution {idx}. "
449
+ + AIRFLOW_NOT_REACHABLE_MSG,
450
+ )
451
+ # TODO: Consider adding similar checks for databricks
379
452
  try:
380
- response = af_client.run_dag(execution.id, dag_name=schema)
381
- except AirflowError as err:
382
- error = f"{AIRFLOW_ERROR_MSG} {err}"
453
+ response = self.orch_client.run_workflow(execution.id, workflow_name=schema)
454
+ except self.orch_error as err:
455
+ error = self.orch_const["name"] + " responded with an error: {}".format(err)
383
456
  current_app.logger.error(error)
384
457
  execution.update_state(EXEC_STATE_ERROR)
385
- raise AirflowError(
458
+ raise self.orch_error(
386
459
  error=error,
387
460
  payload=dict(
388
461
  message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR],
@@ -392,9 +465,9 @@ class ExecutionRelaunchEndpoint(BaseMetaResource):
392
465
  + error,
393
466
  )
394
467
 
395
- # if we succeed, we register the dag_run_id in the execution table:
396
- af_data = response.json()
397
- execution.dag_run_id = af_data["dag_run_id"]
468
+ # if we succeed, we register the run_id in the execution table:
469
+ orch_data = response.json()
470
+ execution.run_id = orch_data[self.orch_const["run_id"]]
398
471
  execution.update_state(EXEC_STATE_QUEUED)
399
472
  current_app.logger.info(
400
473
  "User {} relaunches execution {}".format(self.get_user_id(), execution.id)
@@ -402,7 +475,7 @@ class ExecutionRelaunchEndpoint(BaseMetaResource):
402
475
  return {"message": "The execution was relaunched correctly"}, 201
403
476
 
404
477
 
405
- class ExecutionDetailsEndpointBase(BaseMetaResource):
478
+ class ExecutionDetailsEndpointBase(OrchestratorMixin):
406
479
  """
407
480
  Endpoint used to get the information of a certain execution. But not the data!
408
481
  """
@@ -451,7 +524,7 @@ class ExecutionDetailsEndpoint(ExecutionDetailsEndpointBase):
451
524
  schema = ExecutionModel.get_one_object(user=self.get_user(), idx=idx).schema
452
525
 
453
526
  if data.get("data") is not None and schema is not None:
454
- data_jsonschema = DeployedDAG.get_one_schema(
527
+ data_jsonschema = DeployedWorkflow.get_one_schema(
455
528
  config, schema, SOLUTION_SCHEMA
456
529
  )
457
530
  validation_errors = json_schema_validate_as_string(
@@ -488,29 +561,33 @@ class ExecutionDetailsEndpoint(ExecutionDetailsEndpointBase):
488
561
  @authenticate(auth_class=Auth())
489
562
  @Auth.dag_permission_required
490
563
  def post(self, idx):
564
+ if self.orch_type != AIRFLOW_BACKEND:
565
+ return {
566
+ "message": f"This feature is not available for {self.orch_const['name']}"
567
+ }, 501
491
568
  execution = ExecutionModel.get_one_object(user=self.get_user(), idx=idx)
492
569
  if execution is None:
493
570
  raise ObjectDoesNotExist(
494
571
  log_txt=f"Error while user {self.get_user()} tries to stop execution {idx}. "
495
572
  f"The execution does not exist."
496
573
  )
497
- af_client = Airflow.from_config(current_app.config)
498
- if not af_client.is_alive():
499
574
 
500
- raise AirflowError(
575
+ if not self.orch_client.is_alive(config=current_app.config):
576
+ raise self.orch_error(
501
577
  error=AIRFLOW_NOT_REACHABLE_MSG,
502
- log_txt=f"Error while user {self.get_user()} tries to stop execution {idx}. "
503
- + AIRFLOW_NOT_REACHABLE_MSG,
578
+ log_txt=f"Error while user {self.get_user()} tries to stop execution {idx}. {AIRFLOW_NOT_REACHABLE_MSG}",
504
579
  )
505
- af_client.set_dag_run_to_fail(
506
- dag_name=execution.schema, dag_run_id=execution.dag_run_id
580
+
581
+ self.orch_client.set_dag_run_to_fail(
582
+ dag_name=execution.schema, run_id=execution.run_id
507
583
  )
584
+ # We should check if the execution has been stopped
508
585
  execution.update_state(EXEC_STATE_STOPPED)
509
586
  current_app.logger.info(f"User {self.get_user()} stopped execution {idx}")
510
587
  return {"message": "The execution has been stopped"}, 200
511
588
 
512
589
 
513
- class ExecutionStatusEndpoint(BaseMetaResource):
590
+ class ExecutionStatusEndpoint(OrchestratorMixin):
514
591
  """
515
592
  Endpoint used to get the status of a certain execution that is running in the airflow webserver
516
593
  """
@@ -544,7 +621,7 @@ class ExecutionStatusEndpoint(BaseMetaResource):
544
621
  EXEC_STATE_QUEUED,
545
622
  EXEC_STATE_UNKNOWN,
546
623
  ]:
547
- # we only care on asking airflow if the status is unknown, queued or running.
624
+ # we only care on asking orchestrator if the status is unknown, queued or running.
548
625
  return execution, 200
549
626
 
550
627
  def _raise_af_error(execution, error, state=EXEC_STATE_UNKNOWN, log_txt=None):
@@ -552,38 +629,35 @@ class ExecutionStatusEndpoint(BaseMetaResource):
552
629
  log_txt = error
553
630
  message = EXECUTION_STATE_MESSAGE_DICT[state]
554
631
  execution.update_state(state)
555
- raise AirflowError(
632
+ raise self.orch_error(
556
633
  error=error, payload=dict(message=message, state=state), log_txt=log_txt
557
634
  )
558
635
 
559
- dag_run_id = execution.dag_run_id
560
- if not dag_run_id:
561
- # it's safe to say we will never get anything if we did not store the dag_run_id
636
+ run_id = execution.run_id
637
+ if not run_id:
638
+ # it's safe to say we will never get anything if we did not store the run_id
562
639
  _raise_af_error(
563
640
  execution,
564
641
  state=EXEC_STATE_ERROR,
565
- error="The execution has no dag_run associated",
642
+ error="The execution has no run_id associated",
566
643
  log_txt=f"Error while user {self.get_user()} tries to get the status of execution {idx}. "
567
- f"The execution has no associated dag run id.",
644
+ f"The execution has no associated run id.",
568
645
  )
569
-
570
- af_client = Airflow.from_config(current_app.config)
571
- if not af_client.is_alive():
572
-
646
+ schema = execution.schema
647
+ # We check if the orchestrator is alive
648
+ if not self.orch_client.is_alive(config=current_app.config):
649
+ error = f"{self.orch_const['name']} is not accessible"
573
650
  _raise_af_error(
574
651
  execution,
575
- AIRFLOW_NOT_REACHABLE_MSG,
652
+ error,
653
+ state=EXEC_STATE_ERROR_START,
576
654
  log_txt=f"Error while user {self.get_user()} tries to get the status of execution {idx}. "
577
- + AIRFLOW_NOT_REACHABLE_MSG,
655
+ + error,
578
656
  )
579
-
580
657
  try:
581
-
582
- response = af_client.get_dag_run_status(
583
- dag_name=execution.schema, dag_run_id=dag_run_id
584
- )
585
- except AirflowError as err:
586
- error = f"{AIRFLOW_ERROR_MSG} {err}"
658
+ state = self.orch_client.get_run_status(schema, run_id)
659
+ except self.orch_error as err:
660
+ error = self.orch_const["name"] + f" responded with an error: {err}"
587
661
  _raise_af_error(
588
662
  execution,
589
663
  error,
@@ -591,8 +665,7 @@ class ExecutionStatusEndpoint(BaseMetaResource):
591
665
  + str(err),
592
666
  )
593
667
 
594
- data = response.json()
595
- state = AIRFLOW_TO_STATE_MAP.get(data["state"], EXEC_STATE_UNKNOWN)
668
+ state = map_run_state(state, self.orch_type)
596
669
  execution.update_state(state)
597
670
  current_app.logger.info(
598
671
  f"User {self.get_user()} gets status of execution {idx}"
@@ -677,3 +750,18 @@ class ExecutionLogEndpoint(ExecutionDetailsEndpointBase):
677
750
  """
678
751
  current_app.logger.info(f"User {self.get_user()} gets log of execution {idx}")
679
752
  return self.get_detail(user=self.get_user(), idx=idx)
753
+
754
+
755
+ # region aux_functions
756
+
757
+
758
+ def map_run_state(state, orch_TYPE):
759
+ """
760
+ Maps the state of the execution in the orchestrator to the state of the execution in cornflow
761
+ """
762
+ if orch_TYPE == AIRFLOW_BACKEND:
763
+ state = state.json()["state"]
764
+ return AIRFLOW_TO_STATE_MAP.get(state, EXEC_STATE_UNKNOWN)
765
+ elif orch_TYPE == DATABRICKS_BACKEND:
766
+ preliminar_state = DATABRICKS_TO_STATE_MAP.get(state, EXEC_STATE_UNKNOWN)
767
+ return preliminar_state
@@ -2,18 +2,28 @@
2
2
  Endpoint to check the health of the services.
3
3
  It performs a health check to airflow and a health check to cornflow database
4
4
  """
5
+
5
6
  import os
6
7
 
7
- # Import from internal modules
8
- from cornflow.endpoints.meta_resource import BaseMetaResource
9
- from cornflow.models import UserModel
10
- from cornflow.schemas.health import HealthResponse
11
- from cornflow.shared.const import STATUS_HEALTHY, STATUS_UNHEALTHY, CORNFLOW_VERSION
12
8
  # Import from libraries
13
9
  from cornflow_client.airflow.api import Airflow
14
10
  from flask import current_app
15
11
  from flask_apispec import marshal_with, doc
16
12
 
13
+ # Import from internal modules
14
+ from cornflow.endpoints.meta_resource import BaseMetaResource
15
+ from cornflow.models import UserModel
16
+ from cornflow.schemas.health import HealthResponse
17
+ from cornflow.shared.const import (
18
+ AIRFLOW_BACKEND,
19
+ DATABRICKS_BACKEND,
20
+ STATUS_HEALTHY,
21
+ STATUS_UNHEALTHY,
22
+ CORNFLOW_VERSION
23
+ )
24
+ from cornflow_client.databricks.api import Databricks
25
+ from cornflow.shared.exceptions import EndpointNotImplemented
26
+
17
27
 
18
28
  class HealthEndpoint(BaseMetaResource):
19
29
  @doc(description="Health check", tags=["Health"])
@@ -26,12 +36,9 @@ class HealthEndpoint(BaseMetaResource):
26
36
  :rtype: dict
27
37
  :doc-author: baobab soluciones
28
38
  """
29
- af_client = Airflow.from_config(current_app.config)
30
- airflow_status = STATUS_UNHEALTHY
39
+ backend_status = self.check_backend_status()
31
40
  cornflow_status = STATUS_UNHEALTHY
32
41
  cornflow_version = CORNFLOW_VERSION
33
- if af_client.is_alive():
34
- airflow_status = STATUS_HEALTHY
35
42
 
36
43
  if (
37
44
  UserModel.get_one_user_by_username(os.getenv("CORNFLOW_SERVICE_USER"))
@@ -40,6 +47,18 @@ class HealthEndpoint(BaseMetaResource):
40
47
  cornflow_status = STATUS_HEALTHY
41
48
 
42
49
  current_app.logger.info(
43
- f"Health check: cornflow {cornflow_status}, airflow {airflow_status}"
50
+ f"Health check: cornflow {cornflow_status}, backend {backend_status}"
44
51
  )
45
- return {"cornflow_status": cornflow_status, "airflow_status": airflow_status, "cornflow_version":cornflow_version}
52
+ return {"cornflow_status": cornflow_status, "backend_status": backend_status,
53
+ "cornflow_version": cornflow_version}
54
+
55
+ def check_backend_status(self):
56
+ if current_app.config["CORNFLOW_BACKEND"] == AIRFLOW_BACKEND:
57
+ client = Airflow.from_config(current_app.config)
58
+ elif current_app.config["CORNFLOW_BACKEND"] == DATABRICKS_BACKEND:
59
+ client = Databricks.from_config(current_app.config)
60
+ else:
61
+ raise EndpointNotImplemented()
62
+ if client.is_alive():
63
+ return STATUS_HEALTHY
64
+ return STATUS_UNHEALTHY