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,88 +1,99 @@
1
- import sys
2
- from importlib import import_module
1
+ from flask import current_app
2
+ from sqlalchemy.exc import DBAPIError, IntegrityError
3
3
 
4
- from cornflow.shared.const import (
5
- BASE_PERMISSION_ASSIGNATION,
6
- EXTRA_PERMISSION_ASSIGNATION,
4
+ from cornflow.commands.auxiliar import (
5
+ get_all_external,
6
+ get_all_resources,
7
7
  )
8
8
  from cornflow.models import ViewModel, PermissionViewRoleModel
9
9
  from cornflow.shared import db
10
- from flask import current_app
11
- from sqlalchemy.exc import DBAPIError, IntegrityError
10
+ from cornflow.shared.const import ALL_DEFAULT_ROLES, GET_ACTION
11
+ from cornflow.shared.const import (
12
+ BASE_PERMISSION_ASSIGNATION,
13
+ )
12
14
 
13
15
 
14
16
  def register_base_permissions_command(external_app: str = None, verbose: bool = False):
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
-
17
+ """
18
+ Register base permissions for the application.
19
+ external_app: If provided, it will register the permissions for the external app.
20
+ verbose: If True, it will print the permissions that are being registered.
21
+ """
22
+ # Get all resources, extra permissions, and custom roles actions
23
+ resources_to_register, extra_permissions, custom_roles_actions = get_all_external(
24
+ external_app
25
+ )
26
+
27
+ # Get all views in the database
28
28
  views_in_db = {view.name: view.id for view in ViewModel.get_all_objects()}
29
- permissions_in_db = [perm for perm in PermissionViewRoleModel.get_all_objects()]
30
- permissions_in_db_keys = [
31
- (perm.role_id, perm.action_id, perm.api_view_id) for perm in permissions_in_db
32
- ]
33
- resources_names = [resource["endpoint"] for resource in resources_to_register]
29
+ permissions_in_db, permissions_in_db_keys = get_db_permissions()
30
+
31
+ # Get all resources and roles with access
32
+ resources_roles_with_access = get_all_resources(resources_to_register)
33
+
34
+ # Get the new roles and base permissions assignation
35
+ base_permissions_assignation = get_base_permissions(
36
+ resources_roles_with_access, custom_roles_actions
37
+ )
38
+ # Get the permissions to register and delete
39
+ permissions_tuples = get_permissions_in_code_as_tuples(
40
+ resources_to_register,
41
+ views_in_db,
42
+ base_permissions_assignation,
43
+ extra_permissions,
44
+ )
45
+ permissions_to_register = get_permissions_to_register(
46
+ permissions_tuples, permissions_in_db_keys
47
+ )
48
+ permissions_to_delete = get_permissions_to_delete(
49
+ permissions_tuples, resources_roles_with_access.keys(), permissions_in_db
50
+ )
51
+
52
+ # Save the new permissions in the data
53
+ save_and_delete_permissions(permissions_to_register, permissions_to_delete)
34
54
 
35
- # Create base permissions
36
- permissions_in_app = [
37
- PermissionViewRoleModel(
38
- {
39
- "role_id": role,
40
- "action_id": action,
41
- "api_view_id": views_in_db[view["endpoint"]],
42
- }
43
- )
44
- for role, action in BASE_PERMISSION_ASSIGNATION
45
- for view in resources_to_register
46
- if role in view["resource"].ROLES_WITH_ACCESS
47
- ] + [
48
- PermissionViewRoleModel(
49
- {
50
- "role_id": role,
51
- "action_id": action,
52
- "api_view_id": views_in_db[endpoint],
53
- }
54
- )
55
- for role, action, endpoint in EXTRA_PERMISSION_ASSIGNATION
56
- ]
55
+ if len(permissions_to_register) > 0:
56
+ current_app.logger.info(f"Permissions registered: {permissions_to_register}")
57
+ else:
58
+ current_app.logger.info("No new permissions to register")
57
59
 
58
- permissions_in_app_keys = [
59
- (perm.role_id, perm.action_id, perm.api_view_id) for perm in permissions_in_app
60
- ]
60
+ if len(permissions_to_delete) > 0:
61
+ current_app.logger.info(f"Permissions deleted: {permissions_to_delete}")
62
+ else:
63
+ current_app.logger.info("No permissions to delete")
61
64
 
62
- permissions_to_register = [
63
- permission
64
- for permission in permissions_in_app
65
- if (permission.role_id, permission.action_id, permission.api_view_id)
66
- not in permissions_in_db_keys
67
- ]
68
65
 
69
- permissions_to_delete = [
70
- permission
71
- for permission in permissions_in_db
72
- if (permission.role_id, permission.action_id, permission.api_view_id)
73
- not in permissions_in_app_keys
74
- and permission.api_view.name in resources_names
75
- ]
66
+ def save_new_roles(new_roles_to_add):
67
+ """
68
+ Save the new roles in the database.
69
+ new_roles_to_add: List of new roles to add.
70
+ """
71
+ if len(new_roles_to_add) > 0:
72
+ db.session.bulk_save_objects(new_roles_to_add)
73
+ try:
74
+ db.session.commit()
75
+ except IntegrityError as e:
76
+ db.session.rollback()
77
+ current_app.logger.error(
78
+ f"Integrity error on base permissions register: {e}"
79
+ )
80
+ except DBAPIError as e:
81
+ db.session.rollback()
82
+ current_app.logger.error(f"Unknown error on base permissions register: {e}")
83
+
76
84
 
85
+ def save_and_delete_permissions(permissions_to_register, permissions_to_delete):
86
+ """
87
+ Save and delete permissions in the database.
88
+ permissions_to_register: List of permissions to register.
89
+ permissions_to_delete: List of permissions to delete.
90
+ """
77
91
  if len(permissions_to_register) > 0:
78
92
  db.session.bulk_save_objects(permissions_to_register)
79
93
 
80
- # TODO: for now the permission are not going to get deleted just in case.
81
- # We are just going to register new permissions
82
- # if len(permissions_to_delete) > 0:
83
- # for permission in permissions_to_delete:
84
- # db.session.delete(permission)
85
-
94
+ if len(permissions_to_delete) > 0:
95
+ for permission in permissions_to_delete:
96
+ db.session.delete(permission)
86
97
  try:
87
98
  db.session.commit()
88
99
  except IntegrityError as e:
@@ -105,25 +116,119 @@ def register_base_permissions_command(external_app: str = None, verbose: bool =
105
116
  f"Unknown error on base permissions sequence updating: {e}"
106
117
  )
107
118
 
108
- if verbose:
109
- if len(permissions_to_register) > 0:
110
- current_app.logger.info(
111
- f"Permissions registered: {permissions_to_register}"
112
- )
113
- else:
114
- current_app.logger.info("No new permissions to register")
115
119
 
116
- if len(permissions_to_delete) > 0:
117
- current_app.logger.info(f"Permissions deleted: {permissions_to_delete}")
118
- else:
119
- current_app.logger.info("No permissions to delete")
120
+ def get_permissions_to_delete(permissions_tuples, resources_names, permissions_in_db):
121
+ """
122
+ Get the permissions to delete.
123
+ """
124
+ permissions_to_delete = [
125
+ permission
126
+ for permission in permissions_in_db
127
+ if (permission.role_id, permission.action_id, permission.api_view_id)
128
+ not in permissions_tuples
129
+ ]
130
+
131
+ return permissions_to_delete
132
+
133
+
134
+ def get_permissions_to_register(permissions_tuples, permissions_in_db_keys):
135
+ """
136
+ Get the permissions to register.
137
+ """
138
+ # Convert set of tuples to list of PermissionViewRoleModel objects
139
+ return [
140
+ PermissionViewRoleModel(
141
+ {
142
+ "role_id": role_id,
143
+ "action_id": action_id,
144
+ "api_view_id": api_view_id,
145
+ }
146
+ )
147
+ for role_id, action_id, api_view_id in permissions_tuples
148
+ if (role_id, action_id, api_view_id) not in permissions_in_db_keys
149
+ ]
150
+
151
+
152
+ def get_permissions_in_code_as_tuples(
153
+ resources_to_register, views_in_db, base_permissions_assignation, extra_permissions
154
+ ):
155
+ """
156
+ Get the permissions in code as tuples.
157
+ """
158
+ # Create base permissions using a set to avoid duplicates
159
+ permissions_tuples = set()
160
+
161
+ # Add permissions from ROLES_WITH_ACCESS
162
+ for role, action in base_permissions_assignation:
163
+ for view in resources_to_register:
164
+ if role in view["resource"].ROLES_WITH_ACCESS:
165
+ permissions_tuples.add((role, action, views_in_db[view["endpoint"]]))
166
+
167
+ # Add permissions from extra_permissions
168
+ for role, action, endpoint in extra_permissions:
169
+ if endpoint in views_in_db:
170
+ permissions_tuples.add((role, action, views_in_db[endpoint]))
171
+
172
+ return permissions_tuples
173
+
174
+
175
+ def get_base_permissions(resources_roles_with_access, custom_roles_actions):
176
+ """
177
+ Get the new roles and base permissions assignation.
178
+ resources_roles_with_access: Dictionary of resources and roles with access.
179
+ custom_roles_actions: Dictionary mapping custom roles to their allowed actions.
180
+ """
181
+ # Get all custom roles (both new and existing) that appear in ROLES_WITH_ACCESS
182
+ all_custom_roles_in_access = set(
183
+ [
184
+ role
185
+ for roles in resources_roles_with_access.values()
186
+ for role in roles
187
+ if role not in ALL_DEFAULT_ROLES
188
+ ]
189
+ )
190
+
191
+ # Validate that all custom roles are defined in custom_roles_actions
192
+ undefined_roles = all_custom_roles_in_access - set(custom_roles_actions.keys())
193
+ if undefined_roles:
194
+ raise ValueError(
195
+ f"The following custom roles are used in code but not defined in CUSTOM_ROLES_ACTIONS: {undefined_roles}. "
196
+ f"Please define their allowed actions in the CUSTOM_ROLES_ACTIONS dictionary in shared/const.py."
197
+ )
198
+
199
+ # Create extended permission assignation including all custom roles
200
+ # For custom roles, use the actions defined in custom_roles_actions
201
+ custom_permissions = [
202
+ (custom_role, action)
203
+ for custom_role in all_custom_roles_in_access
204
+ for action in custom_roles_actions[custom_role]
205
+ ]
206
+
207
+ base_permissions_assignation = BASE_PERMISSION_ASSIGNATION + custom_permissions
208
+
209
+ return base_permissions_assignation
210
+
211
+
212
+ def get_db_permissions():
213
+ """
214
+ Get all permissions in the database.
215
+ """
216
+ permissions_in_db = [perm for perm in PermissionViewRoleModel.get_all_objects()]
217
+ permissions_in_db_keys = [
218
+ (perm.role_id, perm.action_id, perm.api_view_id) for perm in permissions_in_db
219
+ ]
120
220
 
121
- return True
221
+ return permissions_in_db, permissions_in_db_keys
122
222
 
123
223
 
124
224
  def register_dag_permissions_command(
125
225
  open_deployment: int = None, verbose: bool = False
126
226
  ):
227
+ """
228
+ Register DAG permissions.
229
+ open_deployment: If 1, it will register the permissions for the open deployment.
230
+ verbose: If True, it will print the permissions that are being registered.
231
+ """
127
232
 
128
233
  from flask import current_app
129
234
  from sqlalchemy.exc import DBAPIError, IntegrityError
@@ -1,22 +1,23 @@
1
- def register_roles_command(verbose: bool = True):
1
+ def register_roles_command(external_app: str = None, verbose: bool = True):
2
2
 
3
3
  from sqlalchemy.exc import DBAPIError, IntegrityError
4
4
  from flask import current_app
5
5
 
6
- from cornflow.models import RoleModel
7
- from cornflow.shared.const import ROLES_MAP
8
6
  from cornflow.shared import db
7
+ from cornflow.commands.auxiliar import (
8
+ get_all_external,
9
+ get_all_resources,
10
+ get_new_roles_to_add,
11
+ )
9
12
 
10
- roles_registered = [role.name for role in RoleModel.get_all_objects()]
13
+ resources_to_register, extra_permissions, _ = get_all_external(external_app)
14
+ resources_roles_with_access = get_all_resources(resources_to_register)
15
+ new_roles_to_add = get_new_roles_to_add(
16
+ extra_permissions, resources_roles_with_access
17
+ )
11
18
 
12
- roles_to_register = [
13
- RoleModel({"id": key, "name": value})
14
- for key, value in ROLES_MAP.items()
15
- if value not in roles_registered
16
- ]
17
-
18
- if len(roles_to_register) > 0:
19
- db.session.bulk_save_objects(roles_to_register)
19
+ if len(new_roles_to_add) > 0:
20
+ db.session.bulk_save_objects(new_roles_to_add)
20
21
 
21
22
  try:
22
23
  db.session.commit()
@@ -38,8 +39,8 @@ def register_roles_command(verbose: bool = True):
38
39
  current_app.logger.error(f"Unknown error on roles sequence updating: {e}")
39
40
 
40
41
  if verbose:
41
- if len(roles_to_register) > 0:
42
- current_app.logger.info(f"Roles registered: {roles_to_register}")
42
+ if len(new_roles_to_add) > 0:
43
+ current_app.logger.info(f"Roles registered: {new_roles_to_add}")
43
44
  else:
44
45
  current_app.logger.info("No new roles to be registered")
45
46
 
@@ -3,6 +3,7 @@ def update_schemas_command(url, user, pwd, verbose: bool = False):
3
3
  from flask import current_app
4
4
 
5
5
  from cornflow_client.airflow.api import Airflow
6
+ from cornflow.shared.const import AIRFLOW_NOT_REACHABLE_MSG
6
7
 
7
8
  af_client = Airflow(url, user, pwd)
8
9
  max_attempts = 20
@@ -10,12 +11,12 @@ def update_schemas_command(url, user, pwd, verbose: bool = False):
10
11
  while not af_client.is_alive() and attempts < max_attempts:
11
12
  attempts += 1
12
13
  if verbose == 1:
13
- current_app.logger.info(f"Airflow is not reachable (attempt {attempts})")
14
+ current_app.logger.info(f"{AIRFLOW_NOT_REACHABLE_MSG} (attempt {attempts})")
14
15
  time.sleep(15)
15
16
 
16
17
  if not af_client.is_alive():
17
18
  if verbose == 1:
18
- current_app.logger.info("Airflow is not reachable")
19
+ current_app.logger.info(f"{AIRFLOW_NOT_REACHABLE_MSG}")
19
20
  return False
20
21
 
21
22
  response = af_client.update_schemas()
@@ -34,6 +35,7 @@ def update_dag_registry_command(url, user, pwd, verbose: bool = False):
34
35
  from flask import current_app
35
36
 
36
37
  from cornflow_client.airflow.api import Airflow
38
+ from cornflow.shared.const import AIRFLOW_NOT_REACHABLE_MSG
37
39
 
38
40
  af_client = Airflow(url, user, pwd)
39
41
  max_attempts = 20
@@ -41,12 +43,12 @@ def update_dag_registry_command(url, user, pwd, verbose: bool = False):
41
43
  while not af_client.is_alive() and attempts < max_attempts:
42
44
  attempts += 1
43
45
  if verbose == 1:
44
- current_app.logger.info(f"Airflow is not reachable (attempt {attempts})")
46
+ current_app.logger.info(f"{AIRFLOW_NOT_REACHABLE_MSG} (attempt {attempts})")
45
47
  time.sleep(15)
46
48
 
47
49
  if not af_client.is_alive():
48
50
  if verbose == 1:
49
- current_app.logger.info("Airflow is not reachable")
51
+ current_app.logger.info(f"{AIRFLOW_NOT_REACHABLE_MSG}")
50
52
  return False
51
53
 
52
54
  response = af_client.update_dag_registry()
@@ -16,19 +16,16 @@ def create_user_with_role(
16
16
  current_app.logger.info(
17
17
  f"User {username} is created and assigned {role_name} role"
18
18
  )
19
- return True
19
+ return
20
20
 
21
21
  user_roles = UserRoleModel.get_all_objects(user_id=user.id)
22
22
  user_actual_roles = [ur.role for ur in user_roles]
23
- if (
24
- user_roles is not None
25
- and RoleModel.get_one_object(role) in user_actual_roles
26
- ):
23
+ if user_roles is not None and RoleModel.get_one_object(role) in user_actual_roles:
27
24
  if verbose:
28
25
  current_app.logger.info(
29
26
  f"User {username} exists and already has {role_name} role assigned"
30
27
  )
31
- return True
28
+ return
32
29
 
33
30
  user_role = UserRoleModel({"user_id": user.id, "role_id": role})
34
31
  user_role.save()
@@ -36,7 +33,6 @@ def create_user_with_role(
36
33
  current_app.logger.info(
37
34
  f"User {username} already exists and is assigned a {role_name} role"
38
35
  )
39
- return True
40
36
 
41
37
 
42
38
  def create_service_user_command(username, email, password, verbose: bool = True):
@@ -45,8 +41,9 @@ def create_service_user_command(username, email, password, verbose: bool = True)
45
41
 
46
42
  if username is None or email is None or password is None:
47
43
  current_app.logger.info("Missing required arguments")
48
- return False
49
- return create_user_with_role(
44
+ return
45
+
46
+ create_user_with_role(
50
47
  username, email, password, "serviceuser", SERVICE_ROLE, verbose
51
48
  )
52
49
 
@@ -57,10 +54,9 @@ def create_admin_user_command(username, email, password, verbose: bool = True):
57
54
 
58
55
  if username is None or email is None or password is None:
59
56
  current_app.logger.info("Missing required arguments")
60
- return False
61
- return create_user_with_role(
62
- username, email, password, "admin", ADMIN_ROLE, verbose
63
- )
57
+ return
58
+
59
+ create_user_with_role(username, email, password, "admin", ADMIN_ROLE, verbose)
64
60
 
65
61
 
66
62
  def create_planner_user_command(username, email, password, verbose: bool = True):
@@ -69,7 +65,6 @@ def create_planner_user_command(username, email, password, verbose: bool = True)
69
65
 
70
66
  if username is None or email is None or password is None:
71
67
  current_app.logger.info("Missing required arguments")
72
- return False
73
- return create_user_with_role(
74
- username, email, password, "planner", PLANNER_ROLE, verbose
75
- )
68
+ return
69
+
70
+ create_user_with_role(username, email, password, "planner", PLANNER_ROLE, verbose)