cornflow 1.2.0a1__py3-none-any.whl → 1.2.0a2__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.
- airflow_config/airflow_local_settings.py +1 -1
- cornflow/cli/migrations.py +23 -3
- cornflow/cli/service.py +3 -9
- cornflow/cli/utils.py +16 -1
- cornflow/config.py +1 -1
- cornflow/endpoints/__init__.py +7 -1
- cornflow/endpoints/alarms.py +66 -2
- cornflow/endpoints/login.py +59 -38
- cornflow/endpoints/meta_resource.py +11 -3
- cornflow/models/base_data_model.py +4 -32
- cornflow/models/meta_models.py +28 -22
- cornflow/models/user.py +7 -10
- cornflow/schemas/alarms.py +8 -0
- cornflow/schemas/query.py +2 -1
- cornflow/schemas/user.py +2 -3
- cornflow/shared/authentication/auth.py +19 -39
- cornflow/tests/const.py +1 -0
- cornflow/tests/custom_test_case.py +42 -12
- cornflow/tests/unit/test_alarms.py +55 -1
- cornflow/tests/unit/test_apiview.py +106 -1
- cornflow/tests/unit/test_cli.py +6 -5
- cornflow/tests/unit/test_dags.py +0 -1
- cornflow/tests/unit/test_instances.py +12 -0
- cornflow/tests/unit/test_log_in.py +8 -5
- cornflow/tests/unit/test_roles.py +38 -0
- cornflow/tests/unit/test_token.py +11 -3
- cornflow/tests/unit/test_users.py +22 -6
- {cornflow-1.2.0a1.dist-info → cornflow-1.2.0a2.dist-info}/METADATA +13 -13
- {cornflow-1.2.0a1.dist-info → cornflow-1.2.0a2.dist-info}/RECORD +32 -32
- {cornflow-1.2.0a1.dist-info → cornflow-1.2.0a2.dist-info}/WHEEL +1 -1
- {cornflow-1.2.0a1.dist-info → cornflow-1.2.0a2.dist-info}/entry_points.txt +0 -0
- {cornflow-1.2.0a1.dist-info → cornflow-1.2.0a2.dist-info}/top_level.txt +0 -0
@@ -19,5 +19,5 @@ STATE_COLORS = {
|
|
19
19
|
from airflow.www.utils import UIAlert
|
20
20
|
|
21
21
|
DASHBOARD_UIALERTS = [
|
22
|
-
UIAlert("Welcome! This is the backend of your
|
22
|
+
UIAlert("Welcome! This is the backend of your cornflow environment. Airflow™ is a platform created by the community to programmatically author, schedule and monitor workflows."),
|
23
23
|
]
|
cornflow/cli/migrations.py
CHANGED
@@ -3,7 +3,7 @@ import os.path
|
|
3
3
|
|
4
4
|
import click
|
5
5
|
from cornflow.shared import db
|
6
|
-
from flask_migrate import Migrate, migrate, upgrade, init
|
6
|
+
from flask_migrate import Migrate, migrate, upgrade, downgrade, init
|
7
7
|
|
8
8
|
from .utils import get_app
|
9
9
|
|
@@ -28,7 +28,27 @@ def migrate_migrations():
|
|
28
28
|
|
29
29
|
|
30
30
|
@migrations.command(name="upgrade", help="Apply migrations")
|
31
|
-
|
31
|
+
@click.option(
|
32
|
+
"-r", "--revision", type=str, help="The revision to upgrade to", default="head"
|
33
|
+
)
|
34
|
+
def upgrade_migrations(revision="head"):
|
35
|
+
app = get_app()
|
36
|
+
external = int(os.getenv("EXTERNAL_APP", 0))
|
37
|
+
if external == 0:
|
38
|
+
path = "./cornflow/migrations"
|
39
|
+
else:
|
40
|
+
path = f"./{os.getenv('EXTERNAL_APP_MODULE', 'external_app')}/migrations"
|
41
|
+
|
42
|
+
with app.app_context():
|
43
|
+
migration_client = Migrate(app=app, db=db, directory=path)
|
44
|
+
upgrade(revision=revision)
|
45
|
+
|
46
|
+
|
47
|
+
@migrations.command(name="downgrade", help="Downgrade migrations")
|
48
|
+
@click.option(
|
49
|
+
"-r", "--revision", type=str, help="The revision to downgrade to", default="-1"
|
50
|
+
)
|
51
|
+
def downgrade_migrations(revision="-1"):
|
32
52
|
app = get_app()
|
33
53
|
external = int(os.getenv("EXTERNAL_APP", 0))
|
34
54
|
if external == 0:
|
@@ -38,7 +58,7 @@ def upgrade_migrations():
|
|
38
58
|
|
39
59
|
with app.app_context():
|
40
60
|
migration_client = Migrate(app=app, db=db, directory=path)
|
41
|
-
|
61
|
+
downgrade(revision=revision)
|
42
62
|
|
43
63
|
|
44
64
|
@migrations.command(
|
cornflow/cli/service.py
CHANGED
@@ -6,6 +6,7 @@ from logging import error
|
|
6
6
|
|
7
7
|
|
8
8
|
import click
|
9
|
+
from .utils import get_db_conn
|
9
10
|
import cornflow
|
10
11
|
from cornflow.app import create_app
|
11
12
|
from cornflow.commands import (
|
@@ -56,15 +57,8 @@ def init_cornflow_service():
|
|
56
57
|
os.environ["SECRET_KEY"] = os.getenv("FERNET_KEY", Fernet.generate_key().decode())
|
57
58
|
|
58
59
|
# Cornflow db defaults
|
59
|
-
|
60
|
-
|
61
|
-
cornflow_db_user = os.getenv("CORNFLOW_DB_USER", "cornflow")
|
62
|
-
cornflow_db_password = os.getenv("CORNFLOW_DB_PASSWORD", "cornflow")
|
63
|
-
cornflow_db = os.getenv("CORNFLOW_DB", "cornflow")
|
64
|
-
cornflow_db_conn = os.getenv(
|
65
|
-
"cornflow_db_conn",
|
66
|
-
f"postgresql://{cornflow_db_user}:{cornflow_db_password}@{cornflow_db_host}:{cornflow_db_port}/{cornflow_db}",
|
67
|
-
)
|
60
|
+
os.environ["DEFAULT_POSTGRES"] = "1"
|
61
|
+
cornflow_db_conn = get_db_conn()
|
68
62
|
os.environ["DATABASE_URL"] = cornflow_db_conn
|
69
63
|
|
70
64
|
# Platform auth config and service users
|
cornflow/cli/utils.py
CHANGED
@@ -6,7 +6,7 @@ import warnings
|
|
6
6
|
|
7
7
|
def get_app():
|
8
8
|
env = os.getenv("FLASK_ENV", "development")
|
9
|
-
data_conn =
|
9
|
+
data_conn = get_db_conn()
|
10
10
|
if env == "production":
|
11
11
|
warnings.filterwarnings("ignore")
|
12
12
|
external = int(os.getenv("EXTERNAL_APP", 0))
|
@@ -24,3 +24,18 @@ def get_app():
|
|
24
24
|
app = create_app(env, data_conn)
|
25
25
|
|
26
26
|
return app
|
27
|
+
|
28
|
+
|
29
|
+
def get_db_conn():
|
30
|
+
if int(os.getenv("DEFAULT_POSTGRES", 0)) == 0:
|
31
|
+
return os.getenv("DATABASE_URL", "sqlite:///cornflow.db")
|
32
|
+
else:
|
33
|
+
cornflow_db_host = os.getenv("CORNFLOW_DB_HOST", "cornflow_db")
|
34
|
+
cornflow_db_port = os.getenv("CORNFLOW_DB_PORT", "5432")
|
35
|
+
cornflow_db_user = os.getenv("CORNFLOW_DB_USER", "cornflow")
|
36
|
+
cornflow_db_password = os.getenv("CORNFLOW_DB_PASSWORD", "cornflow")
|
37
|
+
cornflow_db = os.getenv("CORNFLOW_DB", "cornflow")
|
38
|
+
return os.getenv(
|
39
|
+
"cornflow_db_conn",
|
40
|
+
f"postgresql://{cornflow_db_user}:{cornflow_db_password}@{cornflow_db_host}:{cornflow_db_port}/{cornflow_db}",
|
41
|
+
)
|
cornflow/config.py
CHANGED
cornflow/endpoints/__init__.py
CHANGED
@@ -3,8 +3,9 @@ Initialization file for the endpoints module
|
|
3
3
|
All references to endpoints should be imported from here
|
4
4
|
The login resource gets created on app startup as it depends on configuration
|
5
5
|
"""
|
6
|
+
|
6
7
|
from .action import ActionListEndpoint
|
7
|
-
from .alarms import AlarmsEndpoint
|
8
|
+
from .alarms import AlarmsEndpoint, AlarmDetailEndpoint
|
8
9
|
from .apiview import ApiViewListEndpoint
|
9
10
|
from .case import (
|
10
11
|
CaseEndpoint,
|
@@ -225,6 +226,11 @@ alarms_resources = [
|
|
225
226
|
urls="/alarms/",
|
226
227
|
endpoint="alarms",
|
227
228
|
),
|
229
|
+
dict(
|
230
|
+
resource=AlarmDetailEndpoint,
|
231
|
+
urls="/alarms/<int:idx>/",
|
232
|
+
endpoint="alarms-detail",
|
233
|
+
),
|
228
234
|
dict(
|
229
235
|
resource=MainAlarmsEndpoint,
|
230
236
|
urls="/main-alarms/",
|
cornflow/endpoints/alarms.py
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
# Imports from libraries
|
2
|
+
from flask import current_app
|
2
3
|
from flask_apispec import doc, marshal_with, use_kwargs
|
3
4
|
|
4
5
|
# Import from internal modules
|
5
6
|
from cornflow.endpoints.meta_resource import BaseMetaResource
|
6
7
|
from cornflow.models import AlarmsModel
|
7
8
|
from cornflow.schemas.alarms import (
|
9
|
+
AlarmEditRequest,
|
8
10
|
AlarmsResponse,
|
9
11
|
AlarmsPostRequest,
|
10
|
-
QueryFiltersAlarms
|
12
|
+
QueryFiltersAlarms,
|
11
13
|
)
|
12
14
|
from cornflow.shared.authentication import Auth, authenticate
|
15
|
+
from cornflow.shared.exceptions import AirflowError, ObjectDoesNotExist, InvalidData
|
16
|
+
from cornflow.shared.const import SERVICE_ROLE
|
13
17
|
|
14
18
|
|
15
19
|
class AlarmsEndpoint(BaseMetaResource):
|
@@ -56,4 +60,64 @@ class AlarmsEndpoint(BaseMetaResource):
|
|
56
60
|
and an integer with the HTTP status code.
|
57
61
|
:rtype: Tuple(dict, integer)
|
58
62
|
"""
|
59
|
-
return self.post_list(data=kwargs)
|
63
|
+
return self.post_list(data=kwargs)
|
64
|
+
|
65
|
+
|
66
|
+
class AlarmDetailEndpointBase(BaseMetaResource):
|
67
|
+
"""
|
68
|
+
Endpoint used to get the information of a certain alarm. But not the data!
|
69
|
+
"""
|
70
|
+
|
71
|
+
def __init__(self):
|
72
|
+
super().__init__()
|
73
|
+
self.data_model = AlarmsModel
|
74
|
+
self.unique = ["id"]
|
75
|
+
|
76
|
+
|
77
|
+
class AlarmDetailEndpoint(AlarmDetailEndpointBase):
|
78
|
+
@doc(description="Get details of an alarm", tags=["None"], inherit=False)
|
79
|
+
@authenticate(auth_class=Auth())
|
80
|
+
@marshal_with(AlarmsResponse)
|
81
|
+
@BaseMetaResource.get_data_or_404
|
82
|
+
def get(self, idx):
|
83
|
+
"""
|
84
|
+
API method to get an execution created by the user and its related info.
|
85
|
+
It requires authentication to be passed in the form of a token that has to be linked to
|
86
|
+
an existing session (login) made by a user.
|
87
|
+
|
88
|
+
:param str idx: ID of the execution.
|
89
|
+
:return: A dictionary with a message (error if authentication failed, or the execution does not exist or
|
90
|
+
the data of the execution) and an integer with the HTTP status code.
|
91
|
+
:rtype: Tuple(dict, integer)
|
92
|
+
"""
|
93
|
+
current_app.logger.info(
|
94
|
+
f"User {self.get_user()} gets details of execution {idx}"
|
95
|
+
)
|
96
|
+
return self.get_detail(idx=idx)
|
97
|
+
|
98
|
+
@doc(description="Edit an execution", tags=["Executions"], inherit=False)
|
99
|
+
@authenticate(auth_class=Auth())
|
100
|
+
@use_kwargs(AlarmEditRequest, location="json")
|
101
|
+
def put(self, idx, **data):
|
102
|
+
"""
|
103
|
+
Edit an existing alarm
|
104
|
+
|
105
|
+
:param string idx: ID of the alarm.
|
106
|
+
:return: A dictionary with a message (error if authentication failed, or the alarm does not exist or
|
107
|
+
a message) and an integer with the HTTP status code.
|
108
|
+
:rtype: Tuple(dict, integer)
|
109
|
+
"""
|
110
|
+
current_app.logger.info(f"User {self.get_user()} edits alarm {idx}")
|
111
|
+
return self.put_detail(data, track_user=False, idx=idx)
|
112
|
+
|
113
|
+
@doc(description="Disable an alarm", tags=["None"])
|
114
|
+
@authenticate(auth_class=Auth())
|
115
|
+
def delete(self, idx):
|
116
|
+
"""
|
117
|
+
:param int alarm_id: Alarm id.
|
118
|
+
:return:
|
119
|
+
:rtype: Tuple(dict, integer)
|
120
|
+
"""
|
121
|
+
|
122
|
+
current_app.logger.info(f"Alarm {idx} was disabled by user {self.get_user()}")
|
123
|
+
return self.disable_detail(idx=idx)
|
cornflow/endpoints/login.py
CHANGED
@@ -2,15 +2,16 @@
|
|
2
2
|
External endpoint for the user to log in to the cornflow webserver
|
3
3
|
"""
|
4
4
|
|
5
|
+
from datetime import datetime, timezone, timedelta
|
6
|
+
|
5
7
|
# Partial imports
|
6
8
|
from flask import current_app, request
|
7
9
|
from flask_apispec import use_kwargs, doc
|
8
10
|
from sqlalchemy.exc import IntegrityError, DBAPIError
|
9
|
-
from datetime import datetime, timedelta
|
10
11
|
|
11
12
|
# Import from internal modules
|
12
13
|
from cornflow.endpoints.meta_resource import BaseMetaResource
|
13
|
-
from cornflow.models import UserModel, UserRoleModel
|
14
|
+
from cornflow.models import UserModel, UserRoleModel, PermissionsDAG
|
14
15
|
from cornflow.schemas.user import LoginEndpointRequest, LoginOpenAuthRequest
|
15
16
|
from cornflow.shared import db
|
16
17
|
from cornflow.shared.authentication import Auth, LDAPBase
|
@@ -51,22 +52,35 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
51
52
|
if auth_type == AUTH_DB:
|
52
53
|
user = self.auth_db_authenticate(**kwargs)
|
53
54
|
response.update({"change_password": check_last_password_change(user)})
|
54
|
-
current_app.logger.info(
|
55
|
+
current_app.logger.info(
|
56
|
+
f"User {user.id} logged in successfully using database authentication"
|
57
|
+
)
|
55
58
|
elif auth_type == AUTH_LDAP:
|
56
59
|
user = self.auth_ldap_authenticate(**kwargs)
|
57
|
-
current_app.logger.info(
|
60
|
+
current_app.logger.info(
|
61
|
+
f"User {user.id} logged in successfully using LDAP authentication"
|
62
|
+
)
|
58
63
|
elif auth_type == AUTH_OID:
|
59
|
-
if
|
64
|
+
if kwargs.get("username") and kwargs.get("password"):
|
60
65
|
if not current_app.config.get("SERVICE_USER_ALLOW_PASSWORD_LOGIN", 0):
|
61
|
-
raise InvalidUsage(
|
62
|
-
|
63
|
-
|
66
|
+
raise InvalidUsage(
|
67
|
+
"Must provide a token in Authorization header. Cannot log in with username and password",
|
68
|
+
400,
|
69
|
+
)
|
70
|
+
user = self.auth_oid_authenticate(
|
71
|
+
username=kwargs["username"], password=kwargs["password"]
|
72
|
+
)
|
73
|
+
current_app.logger.info(
|
74
|
+
f"Service user {user.id} logged in successfully using password"
|
75
|
+
)
|
64
76
|
token = self.auth_class.generate_token(user.id)
|
65
77
|
else:
|
66
78
|
token = self.auth_class().get_token_from_header(request.headers)
|
67
79
|
user = self.auth_oid_authenticate(token=token)
|
68
|
-
current_app.logger.info(
|
69
|
-
|
80
|
+
current_app.logger.info(
|
81
|
+
f"User {user.id} logged in successfully using OpenID authentication"
|
82
|
+
)
|
83
|
+
|
70
84
|
response.update({"token": token, "id": user.id})
|
71
85
|
return response, 200
|
72
86
|
else:
|
@@ -147,7 +161,9 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
147
161
|
|
148
162
|
return user
|
149
163
|
|
150
|
-
def auth_oid_authenticate(
|
164
|
+
def auth_oid_authenticate(
|
165
|
+
self, token: str = None, username: str = None, password: str = None
|
166
|
+
):
|
151
167
|
"""
|
152
168
|
Method in charge of performing the authentication using OpenID Connect tokens.
|
153
169
|
Supports any OIDC provider configured via provider_url.
|
@@ -158,33 +174,22 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
158
174
|
:return: the user object, or it raises an error if it has not been possible to log in
|
159
175
|
:rtype: :class:`UserModel`
|
160
176
|
"""
|
161
|
-
print("[auth_oid_authenticate] Starting OpenID authentication")
|
162
177
|
if token:
|
163
|
-
|
178
|
+
|
164
179
|
decoded_token = self.auth_class().decode_token(token)
|
165
|
-
print(f"[auth_oid_authenticate] Token decoded successfully: {decoded_token}")
|
166
|
-
|
167
|
-
username = decoded_token.get('username')
|
168
|
-
if not username:
|
169
|
-
print("[auth_oid_authenticate] Missing username in token claims")
|
170
|
-
raise InvalidCredentials(
|
171
|
-
"Invalid token: missing username claim",
|
172
|
-
log_txt="Token validation failed: missing username claim",
|
173
|
-
status_code=400
|
174
|
-
)
|
175
180
|
|
176
|
-
|
181
|
+
username = decoded_token.get("sub")
|
182
|
+
|
177
183
|
user = self.data_model.get_one_object(username=username)
|
178
184
|
|
179
185
|
if not user:
|
180
|
-
print(f"[auth_oid_authenticate] Creating new user: {username}")
|
181
186
|
current_app.logger.info(
|
182
187
|
f"OpenID user {username} does not exist and is created"
|
183
188
|
)
|
184
189
|
|
185
|
-
email = decoded_token.get(
|
186
|
-
first_name = decoded_token.get(
|
187
|
-
last_name = decoded_token.get(
|
190
|
+
email = decoded_token.get("email", f"{username}@cornflow.org")
|
191
|
+
first_name = decoded_token.get("given_name", "")
|
192
|
+
last_name = decoded_token.get("family_name", "")
|
188
193
|
|
189
194
|
data = {
|
190
195
|
"username": username,
|
@@ -203,13 +208,12 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
203
208
|
}
|
204
209
|
)
|
205
210
|
user_role.save()
|
211
|
+
if int(current_app.config["OPEN_DEPLOYMENT"]) == 1:
|
212
|
+
PermissionsDAG.add_all_permissions_to_user(user.id)
|
206
213
|
|
207
214
|
return user
|
208
215
|
|
209
|
-
elif
|
210
|
-
username
|
211
|
-
and password
|
212
|
-
):
|
216
|
+
elif username and password:
|
213
217
|
user = self.auth_db_authenticate(username, password)
|
214
218
|
if user.is_service_user():
|
215
219
|
return user
|
@@ -221,17 +225,34 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
221
225
|
def check_last_password_change(user):
|
222
226
|
"""
|
223
227
|
Check if the user needs to change their password based on the password rotation time.
|
224
|
-
|
228
|
+
|
225
229
|
:param user: The user object to check
|
226
230
|
:return: True if password needs to be changed, False otherwise
|
227
231
|
:rtype: bool
|
228
232
|
"""
|
229
233
|
if user.pwd_last_change:
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
234
|
+
# Handle the case where pwd_last_change is already a datetime object
|
235
|
+
if isinstance(user.pwd_last_change, datetime):
|
236
|
+
# If it's a naive datetime (no timezone info), make it timezone-aware
|
237
|
+
if user.pwd_last_change.tzinfo is None:
|
238
|
+
last_change = user.pwd_last_change.replace(tzinfo=timezone.utc)
|
239
|
+
else:
|
240
|
+
# Already timezone-aware
|
241
|
+
last_change = user.pwd_last_change
|
242
|
+
else:
|
243
|
+
# It's a timestamp (integer), convert to datetime
|
244
|
+
last_change = datetime.fromtimestamp(user.pwd_last_change, timezone.utc)
|
245
|
+
|
246
|
+
# Get current time with UTC timezone for proper comparison
|
247
|
+
current_time = datetime.now(timezone.utc)
|
248
|
+
|
249
|
+
# Calculate the expiration time based on the password rotation setting
|
250
|
+
expiration_time = last_change + timedelta(
|
251
|
+
days=int(current_app.config["PWD_ROTATION_TIME"])
|
252
|
+
)
|
253
|
+
|
254
|
+
# Compare the timezone-aware datetimes
|
255
|
+
if expiration_time < current_time:
|
235
256
|
return True
|
236
257
|
return False
|
237
258
|
|
@@ -13,6 +13,7 @@ from cornflow.shared.const import ALL_DEFAULT_ROLES
|
|
13
13
|
from cornflow.shared.exceptions import InvalidUsage, ObjectDoesNotExist, NoPermission
|
14
14
|
|
15
15
|
|
16
|
+
|
16
17
|
class BaseMetaResource(Resource, MethodResource):
|
17
18
|
"""
|
18
19
|
The base resource from all methods inherit from.
|
@@ -175,11 +176,18 @@ class BaseMetaResource(Resource, MethodResource):
|
|
175
176
|
METHODS USED FOR ACTIVATING / DISABLING RECORDS IN CASE WE DO NOT WANT TO DELETE THEM STRAIGHT AWAY
|
176
177
|
"""
|
177
178
|
|
178
|
-
def disable_detail(self):
|
179
|
+
def disable_detail(self, idx):
|
179
180
|
"""
|
180
|
-
Method
|
181
|
+
Method to DISABLE an object from the database
|
182
|
+
|
183
|
+
:param idx: the idx which identifies the object
|
184
|
+
:return: the object and a status code.
|
181
185
|
"""
|
182
|
-
|
186
|
+
row = self.data_model.query.get(idx)
|
187
|
+
if row is None:
|
188
|
+
raise ObjectDoesNotExist(f"Object with id {idx} not found.")
|
189
|
+
row.disable()
|
190
|
+
return {"message": "Object marked as disabled"}, 200
|
183
191
|
|
184
192
|
def activate_detail(self, **kwargs):
|
185
193
|
"""
|
@@ -1,14 +1,13 @@
|
|
1
1
|
"""
|
2
2
|
|
3
3
|
"""
|
4
|
-
|
4
|
+
|
5
5
|
from flask import current_app
|
6
6
|
from sqlalchemy import desc
|
7
7
|
from sqlalchemy.dialects.postgresql import JSON
|
8
8
|
from sqlalchemy.dialects.postgresql import TEXT
|
9
9
|
from sqlalchemy.ext.declarative import declared_attr
|
10
10
|
|
11
|
-
# Import from internal modules
|
12
11
|
from cornflow.models.meta_models import TraceAttributesModel
|
13
12
|
from cornflow.shared import db
|
14
13
|
from cornflow.shared.utils import hash_json_256
|
@@ -39,7 +38,7 @@ class BaseDataModel(TraceAttributesModel):
|
|
39
38
|
|
40
39
|
def __init__(self, data):
|
41
40
|
self.user_id = data.get("user_id")
|
42
|
-
self.data = data.get("data")
|
41
|
+
self.data = data.get("data")
|
43
42
|
self.data_hash = hash_json_256(self.data)
|
44
43
|
self.name = data.get("name")
|
45
44
|
self.description = data.get("description")
|
@@ -50,23 +49,16 @@ class BaseDataModel(TraceAttributesModel):
|
|
50
49
|
@classmethod
|
51
50
|
def get_all_objects(
|
52
51
|
cls,
|
53
|
-
user,
|
54
52
|
schema=None,
|
55
53
|
creation_date_gte=None,
|
56
54
|
creation_date_lte=None,
|
57
55
|
deletion_date_gte=None,
|
58
56
|
deletion_date_lte=None,
|
59
|
-
id=None,
|
60
57
|
update_date_gte=None,
|
61
58
|
update_date_lte=None,
|
62
|
-
user_name=None,
|
63
|
-
last_name=None,
|
64
|
-
email=None,
|
65
|
-
role_id=None,
|
66
|
-
url_rule=None,
|
67
|
-
description=None,
|
68
59
|
offset=0,
|
69
60
|
limit=10,
|
61
|
+
user=None,
|
70
62
|
):
|
71
63
|
"""
|
72
64
|
Query to get all objects from a user
|
@@ -77,13 +69,6 @@ class BaseDataModel(TraceAttributesModel):
|
|
77
69
|
:param string creation_date_lte: created_at needs to be smaller or equal to this
|
78
70
|
:param string deletion_date_gte: deletion_at needs to be larger or equal to this
|
79
71
|
:param string deletion_date_lte: deletion_at needs to be smaller or equal to this
|
80
|
-
:param string id: user id
|
81
|
-
:param string user_name: user first name
|
82
|
-
:param string last_name: user last name
|
83
|
-
:param string email: user email
|
84
|
-
:param int role_id: user role id
|
85
|
-
:param string url_rule: url_rule
|
86
|
-
:param string description: description
|
87
72
|
:param string update_date_gte: update_at needs to be larger or equal to this
|
88
73
|
:param string update_date_lte: update_at needs to be smaller or equal to this
|
89
74
|
:param int offset: query offset for pagination
|
@@ -114,20 +99,7 @@ class BaseDataModel(TraceAttributesModel):
|
|
114
99
|
if update_date_gte:
|
115
100
|
query = query.filter(cls.update_at >= update_date_gte)
|
116
101
|
if update_date_lte:
|
117
|
-
query = query.filter(cls.
|
118
|
-
if user_name:
|
119
|
-
query = query.filter(cls.user_name == user_name)
|
120
|
-
if last_name:
|
121
|
-
query = query.filter(cls.last_name == last_name)
|
122
|
-
if email:
|
123
|
-
query = query.filter(cls.email == email)
|
124
|
-
if role_id:
|
125
|
-
query = query.filter(cls.role_id == role_id)
|
126
|
-
if url_rule:
|
127
|
-
query = query.filter(cls.url_rule == url_rule)
|
128
|
-
if description:
|
129
|
-
query = query.filter(cls.description == description)
|
130
|
-
# if airflow they also return total_entries = query.count(), for some reason
|
102
|
+
query = query.filter(cls.update_at <= update_date_lte)
|
131
103
|
|
132
104
|
return query.order_by(desc(cls.created_at)).offset(offset).limit(limit).all()
|
133
105
|
|
cornflow/models/meta_models.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
"""
|
2
2
|
This file contains the base abstract models from which the rest of the models inherit
|
3
3
|
"""
|
4
|
-
|
5
|
-
from datetime import datetime
|
4
|
+
|
5
|
+
from datetime import datetime, timezone
|
6
|
+
from typing import Dict, List
|
7
|
+
|
6
8
|
from flask import current_app
|
7
9
|
from sqlalchemy.exc import DBAPIError, IntegrityError
|
8
|
-
from typing import Dict, List
|
9
10
|
|
10
|
-
# Imports from internal modules
|
11
11
|
from cornflow.shared import db
|
12
12
|
from cornflow.shared.exceptions import InvalidData
|
13
13
|
|
@@ -33,7 +33,9 @@ class EmptyBaseModel(db.Model):
|
|
33
33
|
|
34
34
|
try:
|
35
35
|
db.session.commit()
|
36
|
-
current_app.logger.debug(
|
36
|
+
current_app.logger.debug(
|
37
|
+
f"Transaction type: {action}, performed correctly on {self}"
|
38
|
+
)
|
37
39
|
except IntegrityError as err:
|
38
40
|
db.session.rollback()
|
39
41
|
current_app.logger.error(f"Integrity error on {action} data: {err}")
|
@@ -99,7 +101,10 @@ class EmptyBaseModel(db.Model):
|
|
99
101
|
action = "bulk create"
|
100
102
|
try:
|
101
103
|
db.session.commit()
|
102
|
-
current_app.logger.debug(
|
104
|
+
current_app.logger.debug(
|
105
|
+
f"Transaction type: {action}, performed correctly on {cls}"
|
106
|
+
)
|
107
|
+
|
103
108
|
except IntegrityError as err:
|
104
109
|
db.session.rollback()
|
105
110
|
current_app.logger.error(f"Integrity error on {action} data: {err}")
|
@@ -120,7 +125,10 @@ class EmptyBaseModel(db.Model):
|
|
120
125
|
action = "bulk create update"
|
121
126
|
try:
|
122
127
|
db.session.commit()
|
123
|
-
current_app.logger.debug(
|
128
|
+
current_app.logger.debug(
|
129
|
+
f"Transaction type: {action}, performed correctly on {cls}"
|
130
|
+
)
|
131
|
+
|
124
132
|
except IntegrityError as err:
|
125
133
|
db.session.rollback()
|
126
134
|
current_app.logger.error(f"Integrity error on {action} data: {err}")
|
@@ -136,12 +144,7 @@ class EmptyBaseModel(db.Model):
|
|
136
144
|
return instances
|
137
145
|
|
138
146
|
@classmethod
|
139
|
-
def get_all_objects(
|
140
|
-
cls,
|
141
|
-
offset=0,
|
142
|
-
limit=None,
|
143
|
-
**kwargs
|
144
|
-
):
|
147
|
+
def get_all_objects(cls, offset=0, limit=None, **kwargs):
|
145
148
|
"""
|
146
149
|
Method to get all the objects from the database applying the filters passed as keyword arguments
|
147
150
|
|
@@ -154,7 +157,9 @@ class EmptyBaseModel(db.Model):
|
|
154
157
|
"""
|
155
158
|
if "user" in kwargs:
|
156
159
|
kwargs.pop("user")
|
157
|
-
query = cls.query.filter_by(**kwargs)
|
160
|
+
query = cls.query.filter_by(**kwargs)
|
161
|
+
if offset:
|
162
|
+
query = query.offset(offset)
|
158
163
|
if limit:
|
159
164
|
query = query.limit(limit)
|
160
165
|
return query
|
@@ -208,8 +213,8 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
208
213
|
deleted_at = db.Column(db.DateTime, nullable=True)
|
209
214
|
|
210
215
|
def __init__(self):
|
211
|
-
self.created_at = datetime.
|
212
|
-
self.updated_at = datetime.
|
216
|
+
self.created_at = datetime.now(timezone.utc)
|
217
|
+
self.updated_at = datetime.now(timezone.utc)
|
213
218
|
self.deleted_at = None
|
214
219
|
|
215
220
|
def update(self, data):
|
@@ -220,11 +225,11 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
220
225
|
:return: None
|
221
226
|
:rtype: None
|
222
227
|
"""
|
223
|
-
self.updated_at = datetime.
|
228
|
+
self.updated_at = datetime.now(timezone.utc)
|
224
229
|
super().update(data)
|
225
230
|
|
226
231
|
def pre_update(self, data):
|
227
|
-
self.updated_at = datetime.
|
232
|
+
self.updated_at = datetime.now(timezone.utc)
|
228
233
|
super().pre_update(data)
|
229
234
|
|
230
235
|
def disable(self):
|
@@ -234,7 +239,7 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
234
239
|
:return: None
|
235
240
|
:rtype: None
|
236
241
|
"""
|
237
|
-
self.deleted_at = datetime.
|
242
|
+
self.deleted_at = datetime.now(timezone.utc)
|
238
243
|
db.session.add(self)
|
239
244
|
self.commit_changes("disabling")
|
240
245
|
|
@@ -245,7 +250,7 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
245
250
|
:return: None
|
246
251
|
:rtype: None
|
247
252
|
"""
|
248
|
-
self.updated_at = datetime.
|
253
|
+
self.updated_at = datetime.now(timezone.utc)
|
249
254
|
self.deleted_at = None
|
250
255
|
db.session.add(self)
|
251
256
|
self.commit_changes("activating")
|
@@ -261,7 +266,7 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
261
266
|
update_date_lte=None,
|
262
267
|
offset=0,
|
263
268
|
limit=None,
|
264
|
-
**kwargs
|
269
|
+
**kwargs,
|
265
270
|
):
|
266
271
|
"""
|
267
272
|
Method to get all the objects from the database applying the filters passed as keyword arguments
|
@@ -300,7 +305,8 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
300
305
|
if creation_date_lte:
|
301
306
|
query = query.filter(cls.created_at <= creation_date_lte)
|
302
307
|
|
303
|
-
|
308
|
+
if offset:
|
309
|
+
query = query.offset(offset)
|
304
310
|
if limit:
|
305
311
|
query = query.limit(limit)
|
306
312
|
return query
|