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.
Files changed (35) hide show
  1. cornflow/app.py +4 -0
  2. cornflow/cli/utils.py +1 -1
  3. cornflow/config.py +10 -2
  4. cornflow/endpoints/__init__.py +14 -0
  5. cornflow/endpoints/execution.py +1 -1
  6. cornflow/endpoints/login.py +26 -6
  7. cornflow/endpoints/reports.py +283 -0
  8. cornflow/migrations/versions/83164be03c23_.py +40 -0
  9. cornflow/migrations/versions/96f00d0961d1_reports_table.py +50 -0
  10. cornflow/models/__init__.py +2 -0
  11. cornflow/models/execution.py +8 -0
  12. cornflow/models/meta_models.py +23 -12
  13. cornflow/models/reports.py +119 -0
  14. cornflow/schemas/execution.py +3 -0
  15. cornflow/schemas/reports.py +48 -0
  16. cornflow/shared/const.py +21 -0
  17. cornflow/shared/exceptions.py +20 -9
  18. cornflow/static/v1.json +3854 -0
  19. cornflow/tests/const.py +7 -0
  20. cornflow/tests/{custom_liveServer.py → custom_live_server.py} +3 -1
  21. cornflow/tests/custom_test_case.py +2 -3
  22. cornflow/tests/integration/test_commands.py +1 -1
  23. cornflow/tests/integration/test_cornflowclient.py +116 -28
  24. cornflow/tests/unit/test_alarms.py +22 -9
  25. cornflow/tests/unit/test_cli.py +10 -5
  26. cornflow/tests/unit/test_commands.py +6 -2
  27. cornflow/tests/unit/test_executions.py +5 -0
  28. cornflow/tests/unit/test_main_alarms.py +8 -0
  29. cornflow/tests/unit/test_reports.py +308 -0
  30. cornflow/tests/unit/test_users.py +5 -2
  31. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/METADATA +31 -31
  32. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/RECORD +35 -28
  33. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/WHEEL +1 -1
  34. {cornflow-1.1.0a2.dist-info → cornflow-1.1.1a1.dist-info}/entry_points.txt +0 -0
  35. {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__()
@@ -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
  ]
@@ -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
- self.error = error
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 = "The server encountered an internal error and was unable " \
127
- "to complete your request. Either the server is overloaded or " \
128
- "there is an error in the application."
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: