cornflow 1.2.1__py3-none-any.whl → 1.2.2__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.
- cornflow/app.py +3 -1
- cornflow/cli/__init__.py +4 -0
- cornflow/cli/actions.py +4 -0
- cornflow/cli/config.py +4 -0
- cornflow/cli/migrations.py +13 -8
- cornflow/cli/permissions.py +4 -0
- cornflow/cli/roles.py +4 -0
- cornflow/cli/schemas.py +5 -0
- cornflow/cli/service.py +235 -131
- cornflow/cli/tools/api_generator.py +13 -10
- cornflow/cli/tools/endpoint_tools.py +191 -196
- cornflow/cli/tools/models_tools.py +87 -60
- cornflow/cli/tools/schema_generator.py +161 -67
- cornflow/cli/tools/schemas_tools.py +4 -5
- cornflow/cli/users.py +8 -0
- cornflow/cli/views.py +4 -0
- cornflow/commands/dag.py +3 -2
- cornflow/commands/schemas.py +6 -4
- cornflow/commands/users.py +12 -17
- cornflow/endpoints/dag.py +27 -25
- cornflow/endpoints/data_check.py +128 -165
- cornflow/endpoints/example_data.py +9 -3
- cornflow/endpoints/execution.py +40 -34
- cornflow/endpoints/health.py +7 -7
- cornflow/endpoints/instance.py +39 -12
- cornflow/endpoints/meta_resource.py +4 -5
- cornflow/schemas/execution.py +9 -1
- cornflow/schemas/health.py +1 -0
- cornflow/shared/authentication/auth.py +73 -42
- cornflow/shared/const.py +10 -1
- cornflow/shared/exceptions.py +3 -1
- cornflow/shared/utils_tables.py +36 -8
- cornflow/shared/validators.py +1 -1
- cornflow/tests/const.py +1 -0
- cornflow/tests/custom_test_case.py +4 -4
- cornflow/tests/unit/test_alarms.py +1 -2
- cornflow/tests/unit/test_cases.py +4 -7
- cornflow/tests/unit/test_executions.py +22 -1
- cornflow/tests/unit/test_health.py +4 -1
- cornflow/tests/unit/test_log_in.py +46 -9
- cornflow/tests/unit/test_tables.py +3 -3
- {cornflow-1.2.1.dist-info → cornflow-1.2.2.dist-info}/METADATA +2 -2
- {cornflow-1.2.1.dist-info → cornflow-1.2.2.dist-info}/RECORD +46 -46
- {cornflow-1.2.1.dist-info → cornflow-1.2.2.dist-info}/WHEEL +1 -1
- {cornflow-1.2.1.dist-info → cornflow-1.2.2.dist-info}/entry_points.txt +0 -0
- {cornflow-1.2.1.dist-info → cornflow-1.2.2.dist-info}/top_level.txt +0 -0
cornflow/endpoints/dag.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
Internal endpoint for getting and posting execution data
|
3
3
|
These are the endpoints used by airflow in its communication with cornflow
|
4
4
|
"""
|
5
|
+
|
5
6
|
# Import from libraries
|
6
7
|
from cornflow_client.constants import SOLUTION_SCHEMA
|
7
8
|
from flask import current_app
|
@@ -17,7 +18,6 @@ from cornflow.schemas.execution import (
|
|
17
18
|
ExecutionDagPostRequest,
|
18
19
|
ExecutionDagRequest,
|
19
20
|
ExecutionDetailsEndpointResponse,
|
20
|
-
ExecutionSchema,
|
21
21
|
)
|
22
22
|
|
23
23
|
from cornflow.shared.authentication import Auth, authenticate
|
@@ -85,7 +85,7 @@ class DAGDetailEndpoint(BaseMetaResource):
|
|
85
85
|
@doc(description="Edit an execution", tags=["DAGs"])
|
86
86
|
@authenticate(auth_class=Auth())
|
87
87
|
@use_kwargs(ExecutionDagRequest, location="json")
|
88
|
-
def put(self, idx, **
|
88
|
+
def put(self, idx, **kwargs):
|
89
89
|
"""
|
90
90
|
API method to write the results of the execution
|
91
91
|
It requires authentication to be passed in the form of a token that has to be linked to
|
@@ -95,13 +95,20 @@ class DAGDetailEndpoint(BaseMetaResource):
|
|
95
95
|
:return: A dictionary with a message (body) and an integer with the HTTP status code
|
96
96
|
:rtype: Tuple(dict, integer)
|
97
97
|
"""
|
98
|
-
|
98
|
+
execution = ExecutionModel.get_one_object(user=self.get_user(), idx=idx)
|
99
|
+
if execution is None:
|
100
|
+
err = "The execution does not exist."
|
101
|
+
raise ObjectDoesNotExist(
|
102
|
+
error=err,
|
103
|
+
log_txt=f"Error while user {self.get_user()} tries to edit execution {idx}."
|
104
|
+
+ err,
|
105
|
+
)
|
106
|
+
|
107
|
+
solution_schema = execution.schema
|
99
108
|
|
100
|
-
# TODO: the solution_schema maybe we should get it from the created execution_id?
|
101
|
-
# at least, check they have the same schema-name
|
102
109
|
# Check data format
|
103
|
-
data =
|
104
|
-
checks =
|
110
|
+
data = kwargs.get("data")
|
111
|
+
checks = kwargs.get("checks")
|
105
112
|
if data is None:
|
106
113
|
# only check format if executions_results exist
|
107
114
|
solution_schema = None
|
@@ -111,24 +118,19 @@ class DAGDetailEndpoint(BaseMetaResource):
|
|
111
118
|
if solution_schema is not None:
|
112
119
|
config = current_app.config
|
113
120
|
|
114
|
-
solution_schema = DeployedDAG.get_one_schema(
|
121
|
+
solution_schema = DeployedDAG.get_one_schema(
|
122
|
+
config, solution_schema, SOLUTION_SCHEMA
|
123
|
+
)
|
115
124
|
solution_errors = json_schema_validate_as_string(solution_schema, data)
|
116
125
|
|
117
126
|
if solution_errors:
|
118
127
|
raise InvalidData(
|
119
128
|
payload=dict(jsonschema_errors=solution_errors),
|
120
129
|
log_txt=f"Error while user {self.get_user()} tries to edit execution {idx}. "
|
121
|
-
|
130
|
+
f"Solution data do not match the jsonschema.",
|
122
131
|
)
|
123
|
-
|
124
|
-
|
125
|
-
err = "The execution does not exist."
|
126
|
-
raise ObjectDoesNotExist(
|
127
|
-
error=err,
|
128
|
-
log_txt=f"Error while user {self.get_user()} tries to edit execution {idx}."
|
129
|
-
+ err,
|
130
|
-
)
|
131
|
-
state = req_data.get("state", EXEC_STATE_CORRECT)
|
132
|
+
|
133
|
+
state = kwargs.get("state", EXEC_STATE_CORRECT)
|
132
134
|
new_data = dict(
|
133
135
|
state=state,
|
134
136
|
state_message=EXECUTION_STATE_MESSAGE_DICT[state],
|
@@ -141,10 +143,9 @@ class DAGDetailEndpoint(BaseMetaResource):
|
|
141
143
|
new_data["data"] = data
|
142
144
|
if checks is not None:
|
143
145
|
new_data["checks"] = checks
|
144
|
-
|
145
|
-
execution.update(
|
146
|
-
|
147
|
-
execution.save()
|
146
|
+
kwargs.update(new_data)
|
147
|
+
execution.update(kwargs)
|
148
|
+
|
148
149
|
current_app.logger.info(f"User {self.get_user()} edits execution {idx}")
|
149
150
|
return {"message": "results successfully saved"}, 200
|
150
151
|
|
@@ -207,7 +208,6 @@ class DAGEndpointManual(BaseMetaResource):
|
|
207
208
|
|
208
209
|
# Check data format
|
209
210
|
data = kwargs.get("data")
|
210
|
-
# TODO: create a function to validate and replace data/ execution_results
|
211
211
|
if data is None:
|
212
212
|
# only check format if executions_results exist
|
213
213
|
solution_schema = None
|
@@ -215,14 +215,16 @@ class DAGEndpointManual(BaseMetaResource):
|
|
215
215
|
solution_schema = "solve_model_dag"
|
216
216
|
if solution_schema is not None:
|
217
217
|
config = current_app.config
|
218
|
-
solution_schema = DeployedDAG.get_one_schema(
|
218
|
+
solution_schema = DeployedDAG.get_one_schema(
|
219
|
+
config, solution_schema, SOLUTION_SCHEMA
|
220
|
+
)
|
219
221
|
solution_errors = json_schema_validate_as_string(solution_schema, data)
|
220
222
|
|
221
223
|
if solution_errors:
|
222
224
|
raise InvalidData(
|
223
225
|
payload=dict(jsonschema_errors=solution_errors),
|
224
226
|
log_txt=f"Error while user {self.get_user()} tries to manually create an execution. "
|
225
|
-
|
227
|
+
f"Solution data do not match the jsonschema.",
|
226
228
|
)
|
227
229
|
|
228
230
|
kwargs_copy = dict(kwargs)
|
cornflow/endpoints/data_check.py
CHANGED
@@ -14,25 +14,105 @@ from cornflow.models import InstanceModel, ExecutionModel, CaseModel, DeployedDA
|
|
14
14
|
from cornflow.schemas.execution import ExecutionDetailsEndpointResponse
|
15
15
|
from cornflow.shared.authentication import Auth, authenticate
|
16
16
|
from cornflow.shared.const import (
|
17
|
+
AIRFLOW_ERROR_MSG,
|
18
|
+
AIRFLOW_NOT_REACHABLE_MSG,
|
19
|
+
DAG_PAUSED_MSG,
|
17
20
|
EXEC_STATE_QUEUED,
|
18
21
|
EXEC_STATE_ERROR,
|
19
22
|
EXEC_STATE_ERROR_START,
|
20
23
|
EXEC_STATE_NOT_RUN,
|
21
|
-
EXECUTION_STATE_MESSAGE_DICT,
|
24
|
+
EXECUTION_STATE_MESSAGE_DICT,
|
25
|
+
PLANNER_ROLE,
|
26
|
+
ADMIN_ROLE,
|
22
27
|
)
|
23
28
|
from cornflow.shared.exceptions import (
|
24
29
|
AirflowError,
|
25
30
|
ObjectDoesNotExist,
|
26
31
|
InvalidUsage,
|
27
|
-
InvalidData
|
32
|
+
InvalidData,
|
28
33
|
)
|
29
34
|
from cornflow.shared.validators import json_schema_validate_as_string
|
30
35
|
|
31
36
|
|
37
|
+
def _run_airflow_data_check(
|
38
|
+
af_client: Airflow,
|
39
|
+
execution: ExecutionModel,
|
40
|
+
schema: str,
|
41
|
+
user,
|
42
|
+
context_str: str,
|
43
|
+
**run_dag_kwargs,
|
44
|
+
):
|
45
|
+
"""
|
46
|
+
Helper function to check Airflow status and run a data check DAG.
|
47
|
+
|
48
|
+
:param af_client: Initialized Airflow client.
|
49
|
+
:param execution: The ExecutionModel object to update.
|
50
|
+
:param schema: The name of the schema/DAG to run.
|
51
|
+
:param user: The user object performing the action.
|
52
|
+
:param context_str: A string describing the context (e.g., "execution {id}") for logging.
|
53
|
+
:param run_dag_kwargs: Additional keyword arguments for af_client.run_dag.
|
54
|
+
:return: None. Updates execution object in place and raises AirflowError on failure.
|
55
|
+
"""
|
56
|
+
# Check Airflow liveness
|
57
|
+
if not af_client.is_alive():
|
58
|
+
current_app.logger.error(AIRFLOW_NOT_REACHABLE_MSG)
|
59
|
+
execution.update_state(EXEC_STATE_ERROR_START)
|
60
|
+
raise AirflowError(
|
61
|
+
error=AIRFLOW_NOT_REACHABLE_MSG,
|
62
|
+
payload=dict(
|
63
|
+
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
|
64
|
+
state=EXEC_STATE_ERROR_START,
|
65
|
+
),
|
66
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
67
|
+
+ AIRFLOW_NOT_REACHABLE_MSG,
|
68
|
+
)
|
69
|
+
|
70
|
+
# Check if DAG is paused
|
71
|
+
schema_info = af_client.get_dag_info(schema)
|
72
|
+
info = schema_info.json()
|
73
|
+
if info.get("is_paused", False):
|
74
|
+
current_app.logger.error(DAG_PAUSED_MSG)
|
75
|
+
execution.update_state(EXEC_STATE_ERROR_START)
|
76
|
+
raise AirflowError(
|
77
|
+
error=DAG_PAUSED_MSG,
|
78
|
+
payload=dict(
|
79
|
+
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
|
80
|
+
state=EXEC_STATE_ERROR_START,
|
81
|
+
),
|
82
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
83
|
+
+ DAG_PAUSED_MSG,
|
84
|
+
)
|
85
|
+
|
86
|
+
# Run the DAG
|
87
|
+
try:
|
88
|
+
response = af_client.run_dag(
|
89
|
+
execution.id, dag_name=schema, checks_only=True, **run_dag_kwargs
|
90
|
+
)
|
91
|
+
except AirflowError as err:
|
92
|
+
error = f"{AIRFLOW_ERROR_MSG} {err}"
|
93
|
+
current_app.logger.error(error)
|
94
|
+
execution.update_state(EXEC_STATE_ERROR)
|
95
|
+
raise AirflowError(
|
96
|
+
error=error,
|
97
|
+
payload=dict(
|
98
|
+
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR],
|
99
|
+
state=EXEC_STATE_ERROR,
|
100
|
+
),
|
101
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
102
|
+
+ error,
|
103
|
+
)
|
104
|
+
|
105
|
+
# Update execution on success
|
106
|
+
af_data = response.json()
|
107
|
+
execution.dag_run_id = af_data.get("dag_run_id")
|
108
|
+
execution.update_state(EXEC_STATE_QUEUED)
|
109
|
+
|
110
|
+
|
32
111
|
class DataCheckExecutionEndpoint(BaseMetaResource):
|
33
112
|
"""
|
34
113
|
Endpoint used to execute the instance and solution checks on an execution
|
35
114
|
"""
|
115
|
+
|
36
116
|
ROLES_WITH_ACCESS = [PLANNER_ROLE, ADMIN_ROLE]
|
37
117
|
|
38
118
|
def __init__(self):
|
@@ -56,13 +136,16 @@ class DataCheckExecutionEndpoint(BaseMetaResource):
|
|
56
136
|
:rtype: Tuple(dict, integer)
|
57
137
|
"""
|
58
138
|
config = current_app.config
|
139
|
+
user = self.get_user()
|
140
|
+
context_str = f"execution {idx}"
|
59
141
|
|
60
142
|
execution = ExecutionModel.get_one_object(user=self.get_user(), idx=idx)
|
61
143
|
if execution is None:
|
62
144
|
err = "The execution to check does not exist"
|
63
145
|
raise ObjectDoesNotExist(
|
64
146
|
error=err,
|
65
|
-
log_txt=f"Error while user {
|
147
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
148
|
+
+ err,
|
66
149
|
)
|
67
150
|
|
68
151
|
schema = execution.schema
|
@@ -72,7 +155,8 @@ class DataCheckExecutionEndpoint(BaseMetaResource):
|
|
72
155
|
err = "The execution is still running"
|
73
156
|
raise InvalidUsage(
|
74
157
|
error=err,
|
75
|
-
log_txt=f"Error while user {
|
158
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
159
|
+
+ err,
|
76
160
|
)
|
77
161
|
|
78
162
|
# this allows testing without airflow interaction:
|
@@ -80,58 +164,12 @@ class DataCheckExecutionEndpoint(BaseMetaResource):
|
|
80
164
|
execution.update_state(EXEC_STATE_NOT_RUN)
|
81
165
|
return execution, 201
|
82
166
|
|
83
|
-
#
|
167
|
+
# Initialize Airflow client
|
84
168
|
af_client = Airflow.from_config(config)
|
85
|
-
if not af_client.is_alive():
|
86
|
-
err = "Airflow is not accessible"
|
87
|
-
current_app.logger.error(err)
|
88
|
-
execution.update_state(EXEC_STATE_ERROR_START)
|
89
|
-
raise AirflowError(
|
90
|
-
error=err,
|
91
|
-
payload=dict(
|
92
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
|
93
|
-
state=EXEC_STATE_ERROR_START,
|
94
|
-
),
|
95
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on execution {idx}. " + err
|
96
|
-
)
|
97
|
-
# ask airflow if dag_name exists
|
98
|
-
schema_info = af_client.get_dag_info(schema)
|
99
|
-
|
100
|
-
info = schema_info.json()
|
101
|
-
if info["is_paused"]:
|
102
|
-
err = "The dag exists but it is paused in airflow"
|
103
|
-
current_app.logger.error(err)
|
104
|
-
execution.update_state(EXEC_STATE_ERROR_START)
|
105
|
-
raise AirflowError(
|
106
|
-
error=err,
|
107
|
-
payload=dict(
|
108
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
|
109
|
-
state=EXEC_STATE_ERROR_START,
|
110
|
-
),
|
111
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on execution {idx}. " + err
|
112
|
-
)
|
113
169
|
|
114
|
-
|
115
|
-
|
116
|
-
execution.id, dag_name=schema, checks_only=True
|
117
|
-
)
|
118
|
-
except AirflowError as err:
|
119
|
-
error = "Airflow responded with an error: {}".format(err)
|
120
|
-
current_app.logger.error(error)
|
121
|
-
execution.update_state(EXEC_STATE_ERROR)
|
122
|
-
raise AirflowError(
|
123
|
-
error=error,
|
124
|
-
payload=dict(
|
125
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR],
|
126
|
-
state=EXEC_STATE_ERROR,
|
127
|
-
),
|
128
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on execution {idx}. " + error
|
129
|
-
)
|
170
|
+
# Call the shared Airflow execution logic
|
171
|
+
_run_airflow_data_check(af_client, execution, schema, user, context_str)
|
130
172
|
|
131
|
-
# if we succeed, we register the dag_run_id in the execution table:
|
132
|
-
af_data = response.json()
|
133
|
-
execution.dag_run_id = af_data["dag_run_id"]
|
134
|
-
execution.update_state(EXEC_STATE_QUEUED)
|
135
173
|
current_app.logger.info(
|
136
174
|
"User {} launches checks of execution {}".format(
|
137
175
|
self.get_user_id(), execution.id
|
@@ -144,7 +182,9 @@ class DataCheckInstanceEndpoint(BaseMetaResource):
|
|
144
182
|
"""
|
145
183
|
Endpoint used to execute the instance and solution checks on an execution
|
146
184
|
"""
|
185
|
+
|
147
186
|
ROLES_WITH_ACCESS = [PLANNER_ROLE, ADMIN_ROLE]
|
187
|
+
|
148
188
|
def __init__(self):
|
149
189
|
super().__init__()
|
150
190
|
self.model = ExecutionModel
|
@@ -169,13 +209,16 @@ class DataCheckInstanceEndpoint(BaseMetaResource):
|
|
169
209
|
:rtype: Tuple(dict, integer)
|
170
210
|
"""
|
171
211
|
config = current_app.config
|
212
|
+
user = self.get_user()
|
213
|
+
context_str = f"instance {idx}"
|
172
214
|
|
173
215
|
instance = InstanceModel.get_one_object(user=self.get_user(), idx=idx)
|
174
216
|
if instance is None:
|
175
217
|
err = "The instance to check does not exist"
|
176
218
|
raise ObjectDoesNotExist(
|
177
219
|
error=err,
|
178
|
-
log_txt=f"Error while user {
|
220
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
221
|
+
+ err,
|
179
222
|
)
|
180
223
|
payload = dict(
|
181
224
|
config=dict(checks_only=True),
|
@@ -185,67 +228,19 @@ class DataCheckInstanceEndpoint(BaseMetaResource):
|
|
185
228
|
)
|
186
229
|
schema = instance.schema
|
187
230
|
|
188
|
-
execution,
|
231
|
+
execution, _ = self.post_list(data=payload)
|
189
232
|
|
190
233
|
# this allows testing without airflow interaction:
|
191
234
|
if request.args.get("run", "1") == "0":
|
192
235
|
execution.update_state(EXEC_STATE_NOT_RUN)
|
193
236
|
return execution, 201
|
194
237
|
|
195
|
-
#
|
238
|
+
# Initialize Airflow client
|
196
239
|
af_client = Airflow.from_config(config)
|
197
|
-
if not af_client.is_alive():
|
198
|
-
err = "Airflow is not accessible"
|
199
|
-
current_app.logger.error(err)
|
200
|
-
execution.update_state(EXEC_STATE_ERROR_START)
|
201
|
-
raise AirflowError(
|
202
|
-
error=err,
|
203
|
-
payload=dict(
|
204
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
|
205
|
-
state=EXEC_STATE_ERROR_START,
|
206
|
-
),
|
207
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on instance {idx}. " + err
|
208
240
|
|
209
|
-
|
210
|
-
|
211
|
-
schema_info = af_client.get_dag_info(schema)
|
212
|
-
|
213
|
-
info = schema_info.json()
|
214
|
-
if info["is_paused"]:
|
215
|
-
err = "The dag exists but it is paused in airflow"
|
216
|
-
current_app.logger.error(err)
|
217
|
-
execution.update_state(EXEC_STATE_ERROR_START)
|
218
|
-
raise AirflowError(
|
219
|
-
error=err,
|
220
|
-
payload=dict(
|
221
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
|
222
|
-
state=EXEC_STATE_ERROR_START,
|
223
|
-
),
|
224
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on instance {idx}. " + err
|
225
|
-
|
226
|
-
)
|
241
|
+
# Call the shared Airflow execution logic
|
242
|
+
_run_airflow_data_check(af_client, execution, schema, user, context_str)
|
227
243
|
|
228
|
-
try:
|
229
|
-
response = af_client.run_dag(
|
230
|
-
execution.id, dag_name=schema, checks_only=True
|
231
|
-
)
|
232
|
-
except AirflowError as err:
|
233
|
-
error = "Airflow responded with an error: {}".format(err)
|
234
|
-
current_app.logger.error(error)
|
235
|
-
execution.update_state(EXEC_STATE_ERROR)
|
236
|
-
raise AirflowError(
|
237
|
-
error=error,
|
238
|
-
payload=dict(
|
239
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR],
|
240
|
-
state=EXEC_STATE_ERROR,
|
241
|
-
),
|
242
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on instance {idx}. " + error
|
243
|
-
)
|
244
|
-
|
245
|
-
# if we succeed, we register the dag_run_id in the execution table:
|
246
|
-
af_data = response.json()
|
247
|
-
execution.dag_run_id = af_data["dag_run_id"]
|
248
|
-
execution.update_state(EXEC_STATE_QUEUED)
|
249
244
|
current_app.logger.info(
|
250
245
|
"User {} creates instance check execution {}".format(
|
251
246
|
self.get_user_id(), execution.id
|
@@ -258,7 +253,9 @@ class DataCheckCaseEndpoint(BaseMetaResource):
|
|
258
253
|
"""
|
259
254
|
Endpoint used to execute the instance and solution checks on an execution
|
260
255
|
"""
|
256
|
+
|
261
257
|
ROLES_WITH_ACCESS = [PLANNER_ROLE, ADMIN_ROLE]
|
258
|
+
|
262
259
|
def __init__(self):
|
263
260
|
super().__init__()
|
264
261
|
self.model = ExecutionModel
|
@@ -283,13 +280,16 @@ class DataCheckCaseEndpoint(BaseMetaResource):
|
|
283
280
|
:rtype: Tuple(dict, integer)
|
284
281
|
"""
|
285
282
|
config = current_app.config
|
283
|
+
user = self.get_user()
|
284
|
+
context_str = f"case {idx}"
|
286
285
|
|
287
286
|
case = CaseModel.get_one_object(user=self.get_user(), idx=idx)
|
288
287
|
if case is None:
|
289
288
|
err = "The case to check does not exist"
|
290
289
|
raise ObjectDoesNotExist(
|
291
290
|
error=err,
|
292
|
-
log_txt=f"Error while user {
|
291
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
292
|
+
+ err,
|
293
293
|
)
|
294
294
|
|
295
295
|
schema = case.schema or "solve_model_dag"
|
@@ -309,14 +309,18 @@ class DataCheckCaseEndpoint(BaseMetaResource):
|
|
309
309
|
if schema == "pulp":
|
310
310
|
validation_schema = "solve_model_dag"
|
311
311
|
|
312
|
-
data_jsonschema = DeployedDAG.get_one_schema(
|
313
|
-
|
312
|
+
data_jsonschema = DeployedDAG.get_one_schema(
|
313
|
+
config, validation_schema, INSTANCE_SCHEMA
|
314
|
+
)
|
315
|
+
validation_errors = json_schema_validate_as_string(
|
316
|
+
data_jsonschema, instance_payload["data"]
|
317
|
+
)
|
314
318
|
|
315
319
|
if validation_errors:
|
316
320
|
raise InvalidData(
|
317
321
|
payload=dict(jsonschema_errors=validation_errors),
|
318
|
-
log_txt=f"Error while user {
|
319
|
-
|
322
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
323
|
+
f"Instance data does not match the jsonschema.",
|
320
324
|
)
|
321
325
|
|
322
326
|
instance, _ = self.post_list(data=instance_payload)
|
@@ -334,14 +338,18 @@ class DataCheckCaseEndpoint(BaseMetaResource):
|
|
334
338
|
|
335
339
|
payload["data"] = case.solution
|
336
340
|
|
337
|
-
data_jsonschema = DeployedDAG.get_one_schema(
|
338
|
-
|
341
|
+
data_jsonschema = DeployedDAG.get_one_schema(
|
342
|
+
config, validation_schema, SOLUTION_SCHEMA
|
343
|
+
)
|
344
|
+
validation_errors = json_schema_validate_as_string(
|
345
|
+
data_jsonschema, payload["data"]
|
346
|
+
)
|
339
347
|
|
340
348
|
if validation_errors:
|
341
349
|
raise InvalidData(
|
342
350
|
payload=dict(jsonschema_errors=validation_errors),
|
343
|
-
log_txt=f"Error while user {
|
344
|
-
|
351
|
+
log_txt=f"Error while user {user} tries to run data checks on {context_str}. "
|
352
|
+
f"Solution data does not match the jsonschema.",
|
345
353
|
)
|
346
354
|
|
347
355
|
self.data_model = ExecutionModel
|
@@ -354,59 +362,14 @@ class DataCheckCaseEndpoint(BaseMetaResource):
|
|
354
362
|
execution.update_state(EXEC_STATE_NOT_RUN)
|
355
363
|
return execution, 201
|
356
364
|
|
357
|
-
#
|
365
|
+
# Initialize Airflow client
|
358
366
|
af_client = Airflow.from_config(config)
|
359
|
-
if not af_client.is_alive():
|
360
|
-
err = "Airflow is not accessible"
|
361
|
-
current_app.logger.error(err)
|
362
|
-
execution.update_state(EXEC_STATE_ERROR_START)
|
363
|
-
raise AirflowError(
|
364
|
-
error=err,
|
365
|
-
payload=dict(
|
366
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
|
367
|
-
state=EXEC_STATE_ERROR_START,
|
368
|
-
),
|
369
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on case {idx}. " + err
|
370
|
-
)
|
371
|
-
# ask airflow if dag_name exists
|
372
|
-
schema_info = af_client.get_dag_info(schema)
|
373
|
-
|
374
|
-
info = schema_info.json()
|
375
|
-
if info["is_paused"]:
|
376
|
-
err = "The dag exists but it is paused in airflow"
|
377
|
-
current_app.logger.error(err)
|
378
|
-
execution.update_state(EXEC_STATE_ERROR_START)
|
379
|
-
raise AirflowError(
|
380
|
-
error=err,
|
381
|
-
payload=dict(
|
382
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR_START],
|
383
|
-
state=EXEC_STATE_ERROR_START,
|
384
|
-
),
|
385
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on case {idx}. " + err
|
386
|
-
)
|
387
|
-
|
388
|
-
try:
|
389
|
-
response = af_client.run_dag(
|
390
|
-
execution.id, dag_name=schema, checks_only=True, case_id=idx
|
391
|
-
)
|
392
367
|
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
raise AirflowError(
|
398
|
-
error=error,
|
399
|
-
payload=dict(
|
400
|
-
message=EXECUTION_STATE_MESSAGE_DICT[EXEC_STATE_ERROR],
|
401
|
-
state=EXEC_STATE_ERROR,
|
402
|
-
),
|
403
|
-
log_txt=f"Error while user {self.get_user()} tries to run data checks on case {idx}. " + error
|
404
|
-
)
|
368
|
+
# Call the shared Airflow execution logic, passing case_id
|
369
|
+
_run_airflow_data_check(
|
370
|
+
af_client, execution, schema, user, context_str, case_id=idx
|
371
|
+
)
|
405
372
|
|
406
|
-
# if we succeed, we register the dag_run_id in the execution table:
|
407
|
-
af_data = response.json()
|
408
|
-
execution.dag_run_id = af_data["dag_run_id"]
|
409
|
-
execution.update_state(EXEC_STATE_QUEUED)
|
410
373
|
current_app.logger.info(
|
411
374
|
"User {} creates case check execution {}".format(
|
412
375
|
self.get_user_id(), execution.id
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Endpoints to get the example data from a DAG
|
3
3
|
"""
|
4
|
+
|
4
5
|
import json
|
5
6
|
|
6
7
|
from cornflow_client.airflow.api import Airflow
|
@@ -11,7 +12,12 @@ from cornflow.endpoints.meta_resource import BaseMetaResource
|
|
11
12
|
from cornflow.models import PermissionsDAG
|
12
13
|
from cornflow.schemas.example_data import ExampleListData, ExampleDetailData
|
13
14
|
from cornflow.shared.authentication import Auth, authenticate
|
14
|
-
from cornflow.shared.const import
|
15
|
+
from cornflow.shared.const import (
|
16
|
+
AIRFLOW_NOT_REACHABLE_MSG,
|
17
|
+
VIEWER_ROLE,
|
18
|
+
PLANNER_ROLE,
|
19
|
+
ADMIN_ROLE,
|
20
|
+
)
|
15
21
|
from cornflow.shared.exceptions import AirflowError, NoPermission, ObjectDoesNotExist
|
16
22
|
|
17
23
|
|
@@ -44,7 +50,7 @@ class ExampleDataListEndpoint(BaseMetaResource):
|
|
44
50
|
current_app.logger.error(
|
45
51
|
"Airflow not accessible when getting data {}".format(dag_name)
|
46
52
|
)
|
47
|
-
raise AirflowError(error="
|
53
|
+
raise AirflowError(error=f"{AIRFLOW_NOT_REACHABLE_MSG}")
|
48
54
|
|
49
55
|
# try airflow and see if dag_name exists
|
50
56
|
af_client.get_dag_info(dag_name)
|
@@ -87,7 +93,7 @@ class ExampleDataDetailEndpoint(BaseMetaResource):
|
|
87
93
|
current_app.logger.error(
|
88
94
|
"Airflow not accessible when getting data {}".format(dag_name)
|
89
95
|
)
|
90
|
-
raise AirflowError(error="
|
96
|
+
raise AirflowError(error=f"{AIRFLOW_NOT_REACHABLE_MSG}")
|
91
97
|
|
92
98
|
# try airflow and see if dag_name exists
|
93
99
|
af_client.get_dag_info(dag_name)
|