cornflow 1.1.0a2__py3-none-any.whl → 1.1.1a1__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 -0
- cornflow/cli/utils.py +1 -1
- cornflow/config.py +10 -2
- cornflow/endpoints/__init__.py +14 -0
- cornflow/endpoints/execution.py +1 -1
- cornflow/endpoints/login.py +26 -6
- cornflow/endpoints/reports.py +283 -0
- cornflow/migrations/versions/83164be03c23_.py +40 -0
- cornflow/migrations/versions/96f00d0961d1_reports_table.py +50 -0
- cornflow/models/__init__.py +2 -0
- cornflow/models/execution.py +8 -0
- cornflow/models/meta_models.py +23 -12
- cornflow/models/reports.py +119 -0
- cornflow/schemas/execution.py +3 -0
- cornflow/schemas/reports.py +48 -0
- cornflow/shared/const.py +21 -0
- cornflow/shared/exceptions.py +20 -9
- cornflow/static/v1.json +3854 -0
- cornflow/tests/const.py +7 -0
- cornflow/tests/{custom_liveServer.py → custom_live_server.py} +3 -1
- cornflow/tests/custom_test_case.py +2 -3
- cornflow/tests/integration/test_commands.py +1 -1
- cornflow/tests/integration/test_cornflowclient.py +116 -28
- cornflow/tests/unit/test_alarms.py +22 -9
- cornflow/tests/unit/test_cli.py +10 -5
- cornflow/tests/unit/test_commands.py +6 -2
- cornflow/tests/unit/test_executions.py +5 -0
- cornflow/tests/unit/test_main_alarms.py +8 -0
- cornflow/tests/unit/test_reports.py +308 -0
- cornflow/tests/unit/test_users.py +5 -2
- {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/METADATA +31 -31
- {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/RECORD +35 -28
- {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/WHEEL +1 -1
- {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/entry_points.txt +0 -0
- {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,119 @@
|
|
1
|
+
"""
|
2
|
+
Model for the reports
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Import from libraries
|
6
|
+
from sqlalchemy.dialects.postgresql import TEXT
|
7
|
+
from sqlalchemy.ext.declarative import declared_attr
|
8
|
+
|
9
|
+
# Imports from internal modules
|
10
|
+
from cornflow.models.base_data_model import TraceAttributesModel
|
11
|
+
from cornflow.shared import db
|
12
|
+
from cornflow.shared.const import REPORT_STATE, REPORT_STATE_MSG
|
13
|
+
|
14
|
+
|
15
|
+
class ReportModel(TraceAttributesModel):
|
16
|
+
"""
|
17
|
+
Model class for the Reports.
|
18
|
+
It inherits from :class:`TraceAttributesModel<cornflow.models.base_data_model.TraceAttributesModel>` to have the trace fields and user field.
|
19
|
+
|
20
|
+
- **id**: int, the report id, primary key for the reports.
|
21
|
+
- **execution_id**: str, the foreign key for the execution (:class:`ExecutionModel`). It links the report to its
|
22
|
+
parent execution.
|
23
|
+
- **file_url**: str, the link with the actual report. It should be a valid url to a cloud storage bucket or a file.
|
24
|
+
- **name**: str, the name of the report given by the user.
|
25
|
+
- **description**: str, the description of the report given by the user. It is optional.
|
26
|
+
- **user_id**: int, the foreign key for the user (:class:`UserModel`). It links the report to its owner.
|
27
|
+
- **created_at**: datetime, the datetime when the report was created (in UTC).
|
28
|
+
This datetime is generated automatically, the user does not need to provide it.
|
29
|
+
- **updated_at**: datetime, the datetime when the report was last updated (in UTC).
|
30
|
+
This datetime is generated automatically, the user does not need to provide it.
|
31
|
+
- **deleted_at**: datetime, the datetime when the report was deleted (in UTC). Even though it is deleted,
|
32
|
+
actually, it is not deleted from the database, in order to have a command that cleans up deleted data
|
33
|
+
after a certain time of its deletion.
|
34
|
+
This datetime is generated automatically, the user does not need to provide it.
|
35
|
+
|
36
|
+
"""
|
37
|
+
|
38
|
+
# Table name in the database
|
39
|
+
__tablename__ = "reports"
|
40
|
+
|
41
|
+
# Model fields
|
42
|
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
43
|
+
execution_id = db.Column(
|
44
|
+
db.String(256), db.ForeignKey("executions.id"), nullable=False
|
45
|
+
)
|
46
|
+
name = db.Column(db.String(256), nullable=False)
|
47
|
+
description = db.Column(TEXT, nullable=True)
|
48
|
+
file_url = db.Column(db.String(256), nullable=True)
|
49
|
+
state = db.Column(db.SmallInteger, default=REPORT_STATE.CORRECT)
|
50
|
+
state_message = db.Column(TEXT, default=REPORT_STATE_MSG[REPORT_STATE.CORRECT])
|
51
|
+
|
52
|
+
@declared_attr
|
53
|
+
def user_id(self):
|
54
|
+
"""
|
55
|
+
The foreign key for the user (:class:`UserModel<cornflow.models.UserModel>`).
|
56
|
+
"""
|
57
|
+
return db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
|
58
|
+
|
59
|
+
@declared_attr
|
60
|
+
def user(self):
|
61
|
+
return db.relationship("UserModel")
|
62
|
+
|
63
|
+
def __init__(self, data: dict):
|
64
|
+
super().__init__()
|
65
|
+
self.user_id = data.get("user_id")
|
66
|
+
self.execution_id = data.get("execution_id")
|
67
|
+
self.name = data.get("name")
|
68
|
+
self.description = data.get("description")
|
69
|
+
self.file_url = data.get("file_url")
|
70
|
+
self.state = data.get("state")
|
71
|
+
if self.state is None:
|
72
|
+
if self.file_url is None:
|
73
|
+
self.state = REPORT_STATE.UNKNOWN
|
74
|
+
else:
|
75
|
+
self.state = REPORT_STATE.CORRECT
|
76
|
+
self.state_message = data.get("state_message")
|
77
|
+
if self.state_message is None:
|
78
|
+
self.state_message = REPORT_STATE_MSG.get(self.state)
|
79
|
+
|
80
|
+
def update(self, data):
|
81
|
+
"""
|
82
|
+
Method used to update a report from the database
|
83
|
+
|
84
|
+
:param dict data: the data of the object
|
85
|
+
:return: None
|
86
|
+
:rtype: None
|
87
|
+
"""
|
88
|
+
# we try to keep the state_message synced, by default
|
89
|
+
if "state" in data and "state_message" not in data:
|
90
|
+
data["state_message"] = REPORT_STATE_MSG[data["state"]]
|
91
|
+
super().update(data)
|
92
|
+
|
93
|
+
def update_link(self, file_url: str):
|
94
|
+
"""
|
95
|
+
Method to update the report link
|
96
|
+
|
97
|
+
:param str file_url: new URL for the report
|
98
|
+
:return: nothing
|
99
|
+
"""
|
100
|
+
self.file_url = file_url
|
101
|
+
super().update({})
|
102
|
+
|
103
|
+
def __repr__(self):
|
104
|
+
"""
|
105
|
+
Method to represent the class :class:`ReportModel`
|
106
|
+
|
107
|
+
:return: The representation of the :class:`ReportModel`
|
108
|
+
:rtype: str
|
109
|
+
"""
|
110
|
+
return f"<Report {self.id}>"
|
111
|
+
|
112
|
+
def __str__(self):
|
113
|
+
"""
|
114
|
+
Method to print a string representation of the :class:`ReportModel`
|
115
|
+
|
116
|
+
:return: The string for the :class:`ReportModel`
|
117
|
+
:rtype: str
|
118
|
+
"""
|
119
|
+
return self.__repr__()
|
cornflow/schemas/execution.py
CHANGED
@@ -5,6 +5,7 @@ from marshmallow import fields, Schema, validate
|
|
5
5
|
from cornflow.shared.const import MIN_EXECUTION_STATUS_CODE, MAX_EXECUTION_STATUS_CODE
|
6
6
|
from .common import QueryFilters, BaseDataEndpointResponse
|
7
7
|
from .solution_log import LogSchema, BasicLogSchema
|
8
|
+
from .reports import ReportSchemaBase
|
8
9
|
|
9
10
|
|
10
11
|
class QueryFiltersExecution(QueryFilters):
|
@@ -30,6 +31,7 @@ class ConfigSchema(Schema):
|
|
30
31
|
threads = fields.Int(required=False)
|
31
32
|
logPath = fields.Str(required=False)
|
32
33
|
MIPGap = fields.Float(required=False)
|
34
|
+
report = fields.Raw(required=False)
|
33
35
|
|
34
36
|
|
35
37
|
class ConfigSchemaResponse(ConfigSchema):
|
@@ -95,6 +97,7 @@ class ExecutionDagPostRequest(ExecutionRequest, ExecutionDagRequest):
|
|
95
97
|
|
96
98
|
|
97
99
|
class ExecutionDetailsEndpointResponse(BaseDataEndpointResponse):
|
100
|
+
reports = fields.Nested(ReportSchemaBase, many=True)
|
98
101
|
config = fields.Nested(ConfigSchemaResponse)
|
99
102
|
instance_id = fields.Str()
|
100
103
|
state = fields.Int()
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Imports from libraries
|
2
|
+
from marshmallow import fields, Schema, INCLUDE
|
3
|
+
|
4
|
+
# Imports from internal modules
|
5
|
+
from .common import BaseQueryFilters
|
6
|
+
|
7
|
+
|
8
|
+
class QueryFiltersReports(BaseQueryFilters):
|
9
|
+
execution_id = fields.Str(required=False)
|
10
|
+
|
11
|
+
|
12
|
+
class ReportSchemaBase(Schema):
|
13
|
+
id = fields.Int(dump_only=True)
|
14
|
+
file_url = fields.Str(required=False)
|
15
|
+
name = fields.Str(required=True)
|
16
|
+
state = fields.Int()
|
17
|
+
|
18
|
+
|
19
|
+
class ReportSchema(ReportSchemaBase):
|
20
|
+
user_id = fields.Int(required=False, load_only=True)
|
21
|
+
execution_id = fields.Str(required=True)
|
22
|
+
description = fields.Str()
|
23
|
+
state_message = fields.Str()
|
24
|
+
created_at = fields.DateTime(dump_only=True)
|
25
|
+
updated_at = fields.DateTime(dump_only=True)
|
26
|
+
deleted_at = fields.DateTime(dump_only=True)
|
27
|
+
|
28
|
+
|
29
|
+
class ReportEditRequest(Schema):
|
30
|
+
class META:
|
31
|
+
unknown = INCLUDE
|
32
|
+
|
33
|
+
name = fields.Str()
|
34
|
+
description = fields.Str()
|
35
|
+
file_url = fields.Str(required=False)
|
36
|
+
state = fields.Int()
|
37
|
+
state_message = fields.Str()
|
38
|
+
|
39
|
+
|
40
|
+
class ReportRequest(Schema):
|
41
|
+
class META:
|
42
|
+
unknown = INCLUDE
|
43
|
+
|
44
|
+
name = fields.Str(required=True)
|
45
|
+
description = fields.Str(required=False)
|
46
|
+
execution_id = fields.Str(required=True)
|
47
|
+
state = fields.Int()
|
48
|
+
state_message = fields.Str()
|
cornflow/shared/const.py
CHANGED
@@ -43,6 +43,25 @@ AIRFLOW_TO_STATE_MAP = dict(
|
|
43
43
|
queued=EXEC_STATE_QUEUED,
|
44
44
|
)
|
45
45
|
|
46
|
+
# Reports codes
|
47
|
+
|
48
|
+
|
49
|
+
class REPORT_STATE:
|
50
|
+
RUNNING = 0
|
51
|
+
CORRECT = 1
|
52
|
+
ERROR = -1
|
53
|
+
UNKNOWN = -5
|
54
|
+
ERROR_NO_QUARTO = -10
|
55
|
+
|
56
|
+
|
57
|
+
REPORT_STATE_MSG = {
|
58
|
+
REPORT_STATE.RUNNING: "The report is currently running.",
|
59
|
+
REPORT_STATE.CORRECT: "The report has been solved correctly.",
|
60
|
+
REPORT_STATE.ERROR: "The report has an error.",
|
61
|
+
REPORT_STATE.UNKNOWN: "The report has an unknown error.",
|
62
|
+
REPORT_STATE.ERROR_NO_QUARTO: "The report failed because Quarto was not found.",
|
63
|
+
}
|
64
|
+
|
46
65
|
# These codes and names are inherited from flask app builder in order to have the same names and values
|
47
66
|
# as this library that is the base of airflow
|
48
67
|
AUTH_DB = 1
|
@@ -122,4 +141,6 @@ BASE_PERMISSION_ASSIGNATION = [
|
|
122
141
|
|
123
142
|
EXTRA_PERMISSION_ASSIGNATION = [
|
124
143
|
(VIEWER_ROLE, PUT_ACTION, "user-detail"),
|
144
|
+
(VIEWER_ROLE, GET_ACTION, "report"),
|
145
|
+
(PLANNER_ROLE, GET_ACTION, "report"),
|
125
146
|
]
|
cornflow/shared/exceptions.py
CHANGED
@@ -21,7 +21,10 @@ class InvalidUsage(Exception):
|
|
21
21
|
def __init__(self, error=None, status_code=None, payload=None, log_txt=None):
|
22
22
|
Exception.__init__(self, error)
|
23
23
|
if error is not None:
|
24
|
-
|
24
|
+
if isinstance(error, Exception):
|
25
|
+
self.error = str(error)
|
26
|
+
else:
|
27
|
+
self.error = error
|
25
28
|
if status_code is not None:
|
26
29
|
self.status_code = status_code
|
27
30
|
self.payload = payload
|
@@ -122,10 +125,21 @@ class ConfigurationError(InvalidUsage):
|
|
122
125
|
error = "No authentication method configured on the server"
|
123
126
|
|
124
127
|
|
128
|
+
class FileError(InvalidUsage):
|
129
|
+
"""
|
130
|
+
Exception used when there is an error regarding the upload of a file to the server
|
131
|
+
"""
|
132
|
+
|
133
|
+
status_code = 400
|
134
|
+
error = "Error uploading the file"
|
135
|
+
|
136
|
+
|
125
137
|
INTERNAL_SERVER_ERROR_MESSAGE = "500 Internal Server Error"
|
126
|
-
INTERNAL_SERVER_ERROR_MESSAGE_DETAIL =
|
127
|
-
|
128
|
-
|
138
|
+
INTERNAL_SERVER_ERROR_MESSAGE_DETAIL = (
|
139
|
+
"The server encountered an internal error and was unable "
|
140
|
+
"to complete your request. Either the server is overloaded or "
|
141
|
+
"there is an error in the application."
|
142
|
+
)
|
129
143
|
|
130
144
|
|
131
145
|
def initialize_errorhandlers(app):
|
@@ -187,10 +201,7 @@ def initialize_errorhandlers(app):
|
|
187
201
|
status_code = error.code or status_code
|
188
202
|
error_msg = f"{status_code} {error.name or INTERNAL_SERVER_ERROR_MESSAGE}"
|
189
203
|
error_str = f"{error_msg}. {str(error.description or '') or INTERNAL_SERVER_ERROR_MESSAGE_DETAIL}"
|
190
|
-
response_dict = {
|
191
|
-
"message": error_msg,
|
192
|
-
"error": error_str
|
193
|
-
}
|
204
|
+
response_dict = {"message": error_msg, "error": error_str}
|
194
205
|
response = jsonify(response_dict)
|
195
206
|
|
196
207
|
elif app.config["ENV"] == "production":
|
@@ -202,7 +213,7 @@ def initialize_errorhandlers(app):
|
|
202
213
|
|
203
214
|
response_dict = {
|
204
215
|
"message": INTERNAL_SERVER_ERROR_MESSAGE,
|
205
|
-
"error": INTERNAL_SERVER_ERROR_MESSAGE_DETAIL
|
216
|
+
"error": INTERNAL_SERVER_ERROR_MESSAGE_DETAIL,
|
206
217
|
}
|
207
218
|
response = jsonify(response_dict)
|
208
219
|
else:
|