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.
Files changed (52) hide show
  1. cornflow/app.py +4 -2
  2. cornflow/cli/__init__.py +4 -0
  3. cornflow/cli/actions.py +4 -0
  4. cornflow/cli/config.py +4 -0
  5. cornflow/cli/migrations.py +13 -8
  6. cornflow/cli/permissions.py +4 -0
  7. cornflow/cli/roles.py +5 -1
  8. cornflow/cli/schemas.py +5 -0
  9. cornflow/cli/service.py +263 -131
  10. cornflow/cli/tools/api_generator.py +13 -10
  11. cornflow/cli/tools/endpoint_tools.py +191 -196
  12. cornflow/cli/tools/models_tools.py +87 -60
  13. cornflow/cli/tools/schema_generator.py +161 -67
  14. cornflow/cli/tools/schemas_tools.py +4 -5
  15. cornflow/cli/users.py +8 -0
  16. cornflow/cli/views.py +4 -0
  17. cornflow/commands/access.py +14 -3
  18. cornflow/commands/auxiliar.py +106 -0
  19. cornflow/commands/dag.py +3 -2
  20. cornflow/commands/permissions.py +186 -81
  21. cornflow/commands/roles.py +15 -14
  22. cornflow/commands/schemas.py +6 -4
  23. cornflow/commands/users.py +12 -17
  24. cornflow/commands/views.py +171 -41
  25. cornflow/endpoints/dag.py +27 -25
  26. cornflow/endpoints/data_check.py +128 -165
  27. cornflow/endpoints/example_data.py +9 -3
  28. cornflow/endpoints/execution.py +40 -34
  29. cornflow/endpoints/health.py +7 -7
  30. cornflow/endpoints/instance.py +39 -12
  31. cornflow/endpoints/meta_resource.py +4 -5
  32. cornflow/schemas/execution.py +9 -1
  33. cornflow/schemas/health.py +1 -0
  34. cornflow/shared/authentication/auth.py +76 -45
  35. cornflow/shared/const.py +10 -1
  36. cornflow/shared/exceptions.py +3 -1
  37. cornflow/shared/utils_tables.py +36 -8
  38. cornflow/shared/validators.py +1 -1
  39. cornflow/tests/const.py +1 -0
  40. cornflow/tests/custom_test_case.py +4 -4
  41. cornflow/tests/unit/test_alarms.py +1 -2
  42. cornflow/tests/unit/test_cases.py +4 -7
  43. cornflow/tests/unit/test_executions.py +22 -1
  44. cornflow/tests/unit/test_external_role_creation.py +785 -0
  45. cornflow/tests/unit/test_health.py +4 -1
  46. cornflow/tests/unit/test_log_in.py +46 -9
  47. cornflow/tests/unit/test_tables.py +3 -3
  48. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/METADATA +2 -2
  49. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/RECORD +52 -50
  50. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/WHEEL +1 -1
  51. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/entry_points.txt +0 -0
  52. {cornflow-1.2.1.dist-info → cornflow-1.2.3.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,11 @@
1
-
2
1
  # Imports from external libraries
3
- from flask import current_app
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
- import sys
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
- if external_app is None:
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, **req_data):
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
- solution_schema = req_data.pop("solution_schema", "pulp")
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 = req_data.get("data")
104
- checks = req_data.get("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(config, solution_schema, SOLUTION_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
- f"Solution data do not match the jsonschema.",
130
+ f"Solution data do not match the jsonschema.",
122
131
  )
123
- execution = ExecutionModel.get_one_object(user=self.get_user(), idx=idx)
124
- if execution is None:
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
- req_data.update(new_data)
145
- execution.update(req_data)
146
- # TODO: is this save necessary?
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(config, solution_schema, SOLUTION_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
- f"Solution data do not match the jsonschema.",
227
+ f"Solution data do not match the jsonschema.",
226
228
  )
227
229
 
228
230
  kwargs_copy = dict(kwargs)