cornflow 1.2.1__py3-none-any.whl → 1.2.3__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 +4 -2
- 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 +5 -1
- cornflow/cli/schemas.py +5 -0
- cornflow/cli/service.py +263 -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/access.py +14 -3
- cornflow/commands/auxiliar.py +106 -0
- cornflow/commands/dag.py +3 -2
- cornflow/commands/permissions.py +186 -81
- cornflow/commands/roles.py +15 -14
- cornflow/commands/schemas.py +6 -4
- cornflow/commands/users.py +12 -17
- cornflow/commands/views.py +171 -41
- 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 +76 -45
- 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_external_role_creation.py +785 -0
- 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.3.dist-info}/METADATA +2 -2
- {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/RECORD +52 -50
- {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/WHEEL +1 -1
- {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/entry_points.txt +0 -0
- {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/top_level.txt +0 -0
cornflow/commands/views.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
|
2
1
|
# Imports from external libraries
|
3
|
-
|
2
|
+
import sys
|
4
3
|
from importlib import import_module
|
4
|
+
|
5
|
+
from flask import current_app
|
5
6
|
from sqlalchemy.exc import DBAPIError, IntegrityError
|
6
|
-
|
7
|
+
|
8
|
+
from cornflow.endpoints import resources, alarms_resources
|
7
9
|
|
8
10
|
# Imports from internal libraries
|
9
11
|
from cornflow.models import ViewModel
|
@@ -11,45 +13,21 @@ from cornflow.shared import db
|
|
11
13
|
|
12
14
|
|
13
15
|
def register_views_command(external_app: str = None, verbose: bool = False):
|
16
|
+
"""
|
17
|
+
Register views for the application.
|
18
|
+
external_app: If provided, it will register the views for the external app.
|
19
|
+
verbose: If True, it will print the views that are being registered.
|
20
|
+
"""
|
21
|
+
resources_to_register = get_resources_to_register(external_app)
|
22
|
+
views_registered_urls_all_attributes = get_database_view()
|
23
|
+
views_to_register, views_registered_urls_all_attributes = get_views_to_register(
|
24
|
+
resources_to_register, views_registered_urls_all_attributes
|
25
|
+
)
|
26
|
+
views_to_delete, views_to_update = get_views_to_update_and_delete(
|
27
|
+
resources_to_register, views_registered_urls_all_attributes
|
28
|
+
)
|
14
29
|
|
15
|
-
|
16
|
-
from cornflow.endpoints import resources, alarms_resources
|
17
|
-
resources_to_register = resources
|
18
|
-
if current_app.config["ALARMS_ENDPOINTS"]:
|
19
|
-
resources_to_register = resources + alarms_resources
|
20
|
-
elif external_app is not None:
|
21
|
-
sys.path.append("./")
|
22
|
-
external_module = import_module(external_app)
|
23
|
-
resources_to_register = external_module.endpoints.resources
|
24
|
-
else:
|
25
|
-
resources_to_register = []
|
26
|
-
exit()
|
27
|
-
|
28
|
-
views_registered = [view.name for view in ViewModel.get_all_objects()]
|
29
|
-
|
30
|
-
views_to_register = [
|
31
|
-
ViewModel(
|
32
|
-
{
|
33
|
-
"name": view["endpoint"],
|
34
|
-
"url_rule": view["urls"],
|
35
|
-
"description": view["resource"].DESCRIPTION,
|
36
|
-
}
|
37
|
-
)
|
38
|
-
for view in resources_to_register
|
39
|
-
if view["endpoint"] not in views_registered
|
40
|
-
]
|
41
|
-
|
42
|
-
if len(views_to_register) > 0:
|
43
|
-
db.session.bulk_save_objects(views_to_register)
|
44
|
-
|
45
|
-
try:
|
46
|
-
db.session.commit()
|
47
|
-
except IntegrityError as e:
|
48
|
-
db.session.rollback()
|
49
|
-
current_app.logger.error(f"Integrity error on views register: {e}")
|
50
|
-
except DBAPIError as e:
|
51
|
-
db.session.rollback()
|
52
|
-
current_app.logger.error(f"Unknow error on views register: {e}")
|
30
|
+
load_changes_to_db(views_to_delete, views_to_register, views_to_update)
|
53
31
|
|
54
32
|
if "postgres" in str(db.session.get_bind()):
|
55
33
|
db.engine.execute(
|
@@ -68,3 +46,155 @@ def register_views_command(external_app: str = None, verbose: bool = False):
|
|
68
46
|
current_app.logger.info("No new endpoints to be registered")
|
69
47
|
|
70
48
|
return True
|
49
|
+
|
50
|
+
|
51
|
+
def load_changes_to_db(views_to_delete, views_to_register, views_to_update):
|
52
|
+
"""
|
53
|
+
Load changes to the database.
|
54
|
+
views_to_delete: List of views to delete.
|
55
|
+
views_to_register: List of views to register.
|
56
|
+
views_to_update: List of views to update.
|
57
|
+
"""
|
58
|
+
if len(views_to_register) > 0:
|
59
|
+
db.session.bulk_save_objects(views_to_register)
|
60
|
+
if len(views_to_update) > 0:
|
61
|
+
db.session.bulk_update_mappings(ViewModel, views_to_update)
|
62
|
+
# If the list views_to_delete is not empty, we will iterate over it and delete the views
|
63
|
+
# If it is empty, we will not delete any view since we are iterating over an empty list
|
64
|
+
for view_id in views_to_delete:
|
65
|
+
view_to_delete = ViewModel.get_one_object(idx=view_id)
|
66
|
+
if view_to_delete:
|
67
|
+
view_to_delete.delete()
|
68
|
+
try:
|
69
|
+
db.session.commit()
|
70
|
+
except IntegrityError as e:
|
71
|
+
db.session.rollback()
|
72
|
+
current_app.logger.error(f"Integrity error on views register: {e}")
|
73
|
+
except DBAPIError as e:
|
74
|
+
db.session.rollback()
|
75
|
+
current_app.logger.error(f"Unknow error on views register: {e}")
|
76
|
+
|
77
|
+
|
78
|
+
def get_views_to_delete(
|
79
|
+
all_resources_to_register_views_endpoints, views_registered_urls_all_attributes
|
80
|
+
):
|
81
|
+
"""
|
82
|
+
Get the views to delete.
|
83
|
+
Views to delete: exist in registered views but not in resources to register.
|
84
|
+
"""
|
85
|
+
return [
|
86
|
+
view_attrs["id"]
|
87
|
+
for view_name, view_attrs in views_registered_urls_all_attributes.items()
|
88
|
+
if view_name not in all_resources_to_register_views_endpoints
|
89
|
+
]
|
90
|
+
|
91
|
+
|
92
|
+
def get_views_to_update(
|
93
|
+
all_resources_to_register_views_endpoints, views_registered_urls_all_attributes
|
94
|
+
):
|
95
|
+
"""
|
96
|
+
Get the views to update.
|
97
|
+
Views to update: exist in both but with different url_rule or description.
|
98
|
+
"""
|
99
|
+
return [
|
100
|
+
{
|
101
|
+
"id": view_attrs["id"],
|
102
|
+
"name": view_name,
|
103
|
+
"url_rule": all_resources_to_register_views_endpoints[view_name][
|
104
|
+
"url_rule"
|
105
|
+
],
|
106
|
+
"description": all_resources_to_register_views_endpoints[view_name][
|
107
|
+
"description"
|
108
|
+
],
|
109
|
+
}
|
110
|
+
for view_name, view_attrs in views_registered_urls_all_attributes.items()
|
111
|
+
if view_name in all_resources_to_register_views_endpoints
|
112
|
+
and (
|
113
|
+
view_attrs["url_rule"]
|
114
|
+
!= all_resources_to_register_views_endpoints[view_name]["url_rule"]
|
115
|
+
or view_attrs["description"]
|
116
|
+
!= all_resources_to_register_views_endpoints[view_name]["description"]
|
117
|
+
)
|
118
|
+
]
|
119
|
+
|
120
|
+
|
121
|
+
def get_views_to_update_and_delete(
|
122
|
+
resources_to_register, views_registered_urls_all_attributes
|
123
|
+
):
|
124
|
+
"""
|
125
|
+
Get the views to update and delete.
|
126
|
+
all_resources_to_register_views_endpoints: Dictionary of all resources to register views endpoints.
|
127
|
+
views_registered_urls_all_attributes: Dictionary of views registered urls all attributes.
|
128
|
+
"""
|
129
|
+
all_resources_to_register_views_endpoints = {
|
130
|
+
view["endpoint"]: {
|
131
|
+
"url_rule": view["urls"],
|
132
|
+
"description": view["resource"].DESCRIPTION,
|
133
|
+
}
|
134
|
+
for view in resources_to_register
|
135
|
+
}
|
136
|
+
|
137
|
+
views_to_delete = get_views_to_delete(
|
138
|
+
all_resources_to_register_views_endpoints, views_registered_urls_all_attributes
|
139
|
+
)
|
140
|
+
|
141
|
+
views_to_update = get_views_to_update(
|
142
|
+
all_resources_to_register_views_endpoints, views_registered_urls_all_attributes
|
143
|
+
)
|
144
|
+
|
145
|
+
return views_to_delete, views_to_update
|
146
|
+
|
147
|
+
|
148
|
+
def get_views_to_register(resources_to_register, views_registered_urls_all_attributes):
|
149
|
+
"""
|
150
|
+
Get the views to register.
|
151
|
+
resources_to_register: List of resources to register.
|
152
|
+
"""
|
153
|
+
|
154
|
+
views_to_register = [
|
155
|
+
ViewModel(
|
156
|
+
{
|
157
|
+
"name": view["endpoint"],
|
158
|
+
"url_rule": view["urls"],
|
159
|
+
"description": view["resource"].DESCRIPTION,
|
160
|
+
}
|
161
|
+
)
|
162
|
+
for view in resources_to_register
|
163
|
+
if view["endpoint"] not in views_registered_urls_all_attributes.keys()
|
164
|
+
]
|
165
|
+
|
166
|
+
return views_to_register, views_registered_urls_all_attributes
|
167
|
+
|
168
|
+
|
169
|
+
def get_database_view():
|
170
|
+
"""
|
171
|
+
Get the database views.
|
172
|
+
"""
|
173
|
+
views_registered_urls_all_attributes = {
|
174
|
+
view.name: {
|
175
|
+
"url_rule": view.url_rule,
|
176
|
+
"description": view.description,
|
177
|
+
"id": view.id,
|
178
|
+
}
|
179
|
+
for view in ViewModel.get_all_objects()
|
180
|
+
}
|
181
|
+
return views_registered_urls_all_attributes
|
182
|
+
|
183
|
+
|
184
|
+
def get_resources_to_register(external_app):
|
185
|
+
if external_app is None:
|
186
|
+
resources_to_register = resources
|
187
|
+
if current_app.config["ALARMS_ENDPOINTS"]:
|
188
|
+
resources_to_register = resources + alarms_resources
|
189
|
+
current_app.logger.info(" ALARMS ENDPOINTS ENABLED ")
|
190
|
+
else:
|
191
|
+
current_app.logger.info(f" USING EXTERNAL APP: {external_app} ")
|
192
|
+
sys.path.append("./")
|
193
|
+
external_module = import_module(external_app)
|
194
|
+
if current_app.config["ALARMS_ENDPOINTS"]:
|
195
|
+
resources_to_register = (
|
196
|
+
external_module.endpoints.resources + resources + alarms_resources
|
197
|
+
)
|
198
|
+
else:
|
199
|
+
resources_to_register = external_module.endpoints.resources + resources
|
200
|
+
return resources_to_register
|
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)
|