cornflow 1.1.1a1__py3-none-any.whl → 1.1.4__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 +0 -4
  2. cornflow/cli/utils.py +1 -1
  3. cornflow/config.py +2 -10
  4. cornflow/endpoints/__init__.py +0 -14
  5. cornflow/endpoints/execution.py +1 -1
  6. cornflow/endpoints/login.py +1 -8
  7. cornflow/models/__init__.py +0 -2
  8. cornflow/models/execution.py +0 -8
  9. cornflow/models/meta_models.py +12 -23
  10. cornflow/schemas/execution.py +0 -3
  11. cornflow/shared/const.py +0 -21
  12. cornflow/shared/exceptions.py +9 -20
  13. cornflow/tests/const.py +0 -7
  14. cornflow/tests/{custom_live_server.py → custom_liveServer.py} +1 -3
  15. cornflow/tests/custom_test_case.py +3 -2
  16. cornflow/tests/integration/test_commands.py +1 -1
  17. cornflow/tests/integration/test_cornflowclient.py +28 -116
  18. cornflow/tests/unit/test_alarms.py +9 -22
  19. cornflow/tests/unit/test_cli.py +5 -10
  20. cornflow/tests/unit/test_commands.py +2 -6
  21. cornflow/tests/unit/test_executions.py +0 -5
  22. cornflow/tests/unit/test_main_alarms.py +0 -8
  23. cornflow/tests/unit/test_users.py +2 -5
  24. {cornflow-1.1.1a1.dist-info → cornflow-1.1.4.dist-info}/METADATA +7 -7
  25. {cornflow-1.1.1a1.dist-info → cornflow-1.1.4.dist-info}/RECORD +28 -35
  26. {cornflow-1.1.1a1.dist-info → cornflow-1.1.4.dist-info}/WHEEL +1 -1
  27. cornflow/endpoints/reports.py +0 -283
  28. cornflow/migrations/versions/83164be03c23_.py +0 -40
  29. cornflow/migrations/versions/96f00d0961d1_reports_table.py +0 -50
  30. cornflow/models/reports.py +0 -119
  31. cornflow/schemas/reports.py +0 -48
  32. cornflow/static/v1.json +0 -3854
  33. cornflow/tests/unit/test_reports.py +0 -308
  34. {cornflow-1.1.1a1.dist-info → cornflow-1.1.4.dist-info}/entry_points.txt +0 -0
  35. {cornflow-1.1.1a1.dist-info → cornflow-1.1.4.dist-info}/top_level.txt +0 -0
@@ -1,283 +0,0 @@
1
- """
2
- External endpoints to manage the reports: create new ones, list all of them, get one in particular
3
- These endpoints have different access url, but manage the same data entities
4
- """
5
- import os
6
-
7
- from flask import current_app, request, send_from_directory
8
- from flask_apispec import marshal_with, use_kwargs, doc
9
- from werkzeug.utils import secure_filename
10
- import uuid
11
-
12
- from cornflow.endpoints.meta_resource import BaseMetaResource
13
- from cornflow.models import ExecutionModel, ReportModel
14
- from cornflow.schemas.reports import (
15
- ReportSchema,
16
- ReportEditRequest,
17
- QueryFiltersReports,
18
- ReportRequest,
19
- )
20
- from cornflow.shared.authentication import Auth, authenticate
21
- from cornflow.shared.const import SERVICE_ROLE
22
- from cornflow.shared.exceptions import (
23
- FileError,
24
- ObjectDoesNotExist,
25
- NoPermission,
26
- )
27
-
28
-
29
- class ReportEndpoint(BaseMetaResource):
30
- """
31
- Endpoint used to create a new report or get all the reports and their information back
32
- """
33
-
34
- ROLES_WITH_ACCESS = [SERVICE_ROLE]
35
-
36
- def __init__(self):
37
- super().__init__()
38
- self.model = ReportModel
39
- self.data_model = ReportModel
40
- self.foreign_data = {"execution_id": ExecutionModel}
41
-
42
- @doc(description="Get all reports", tags=["Reports"])
43
- @authenticate(auth_class=Auth())
44
- @marshal_with(ReportSchema(many=True))
45
- @use_kwargs(QueryFiltersReports, location="query")
46
- def get(self, **kwargs):
47
- """
48
- API method to get all the reports created by the user and its related info
49
- It requires authentication to be passed in the form of a token that has to be linked to
50
- an existing session (login) made by a user
51
-
52
- :return: A dictionary with a message (error if authentication failed or a list with all the reports
53
- created by the authenticated user) and a integer with the HTTP status code
54
- :rtype: Tuple(dict, integer)
55
- """
56
- reports = self.get_list(user=self.get_user(), **kwargs)
57
- current_app.logger.info(f"User {self.get_user()} gets list of reports")
58
- return reports
59
-
60
- @doc(description="Create a report", tags=["Reports"])
61
- @authenticate(auth_class=Auth())
62
- @use_kwargs(ReportRequest, location="form")
63
- @marshal_with(ReportSchema)
64
- def post(self, **kwargs):
65
- """
66
- API method to create a new report linked to an existing execution
67
- It requires authentication to be passed in the form of a token that has to be linked to
68
- an existing session (login) made by a user
69
-
70
- :return: A dictionary with a message (error if authentication failed, error if data is not validated or
71
- the reference_id for the newly created report if successful) and a integer with the HTTP status code
72
- :rtype: Tuple(dict, integer)
73
- """
74
-
75
- execution = ExecutionModel.get_one_object(idx=kwargs["execution_id"])
76
-
77
- if execution is None:
78
- raise ObjectDoesNotExist("The execution does not exist")
79
- if "file" not in request.files:
80
- # we're creating an empty report.
81
- # which is possible
82
- report = ReportModel(get_report_info(kwargs, execution, None))
83
-
84
- report.save()
85
- return report, 201
86
-
87
- file = request.files["file"]
88
- report_name = new_file_name(file)
89
-
90
- report = ReportModel(get_report_info(kwargs, execution, report_name))
91
-
92
- report.save()
93
-
94
- # We try to save the file, if an error is raised then we delete the record on the database
95
- try:
96
- write_file(file, execution.id, report_name)
97
- return report, 201
98
-
99
- except Exception as error:
100
- report.delete()
101
- current_app.logger.error(error)
102
- raise FileError(error=str(error))
103
-
104
-
105
- class ReportDetailsEndpointBase(BaseMetaResource):
106
- """
107
- Endpoint used to get the information of a certain report. But not the data!
108
- """
109
-
110
- def __init__(self):
111
- super().__init__()
112
- self.data_model = ReportModel
113
- self.foreign_data = {"execution_id": ExecutionModel}
114
-
115
-
116
- class ReportDetailsEditEndpoint(ReportDetailsEndpointBase):
117
-
118
- ROLES_WITH_ACCESS = [SERVICE_ROLE]
119
-
120
- @doc(description="Edit a report", tags=["Reports"], inherit=False)
121
- @authenticate(auth_class=Auth())
122
- @use_kwargs(ReportEditRequest, location="form")
123
- def put(self, idx, **data):
124
- """
125
- Edit an existing report
126
-
127
- :param string idx: ID of the report.
128
- :return: A dictionary with a message (error if authentication failed, or the report does not exist or
129
- a message) and an integer with the HTTP status code.
130
- :rtype: Tuple(dict, integer)
131
- """
132
- # TODO: forbid non-service users from running put
133
- current_app.logger.info(f"User {self.get_user()} edits report {idx}")
134
-
135
- report = self.get_detail(idx=idx)
136
-
137
- if "file" not in request.files:
138
- # we're creating an empty report.
139
- # which is possible
140
- report.update(data)
141
- report.save()
142
- return {"message": "Updated correctly"}, 200
143
-
144
- # there's two cases,
145
- # (1) the report already has a file
146
- # (2) the report doesn't yet have a file
147
- file = request.files["file"]
148
- report_name = new_file_name(file)
149
- old_name = report.file_url
150
- # we update the report with the new content, including the new name
151
- report.update(dict(**data, file_url=report_name))
152
-
153
- # We try to save the file, if an error is raised then we delete the record on the database
154
- try:
155
- write_file(file, report.execution_id, report_name)
156
- report.save()
157
-
158
- except Exception as error:
159
- # we do not save the report
160
- current_app.logger.error(error)
161
- raise FileError(error=str(error))
162
-
163
- # if it saves correctly, we delete the old file, if exists
164
- # if unsuccessful, we still return 201 but log the error
165
- if old_name is not None:
166
- try:
167
- os.remove(get_report_path(report))
168
- except OSError as error:
169
- current_app.logger.error(error)
170
- return {"message": "Updated correctly"}, 200
171
-
172
-
173
- class ReportDetailsEndpoint(ReportDetailsEndpointBase):
174
- @doc(description="Get details of a report", tags=["Reports"], inherit=False)
175
- @authenticate(auth_class=Auth())
176
- @marshal_with(ReportSchema)
177
- @BaseMetaResource.get_data_or_404
178
- def get(self, idx):
179
- """
180
- API method to get a report created by the user and its related info.
181
- It requires authentication to be passed in the form of a token that has to be linked to
182
- an existing session (login) made by a user.
183
-
184
- :param str idx: ID of the report.
185
- :return: A dictionary with a message (error if authentication failed, or the report does not exist or
186
- the data of the report) and an integer with the HTTP status code.
187
- :rtype: Tuple(dict, integer)
188
- """
189
- # TODO: are we able to download the name in the database and not as part of the file?
190
- current_app.logger.info(f"User {self.get_user()} gets details of report {idx}")
191
- report = self.get_detail(user=self.get_user(), idx=idx)
192
-
193
- if report is None:
194
- raise ObjectDoesNotExist
195
-
196
- # if there's no file, we do not return it:
197
- if report.file_url is None:
198
- return report, 200
199
-
200
- my_dir = get_report_dir(report.execution_id)
201
- response = send_from_directory(my_dir, report.file_url)
202
- response.headers["File-Description"] = report.description
203
- response.headers["File-Name"] = report.file_url
204
- return response
205
-
206
- @doc(description="Delete a report", tags=["Reports"], inherit=False)
207
- @authenticate(auth_class=Auth())
208
- def delete(self, idx):
209
- """
210
- API method to delete a report created by the user and its related info.
211
- It requires authentication to be passed in the form of a token that has to be linked to
212
- an existing session (login) made by a user.
213
-
214
- :param string idx: ID of the report.
215
- :return: A dictionary with a message (error if authentication failed, or the report does not exist or
216
- a message) and an integer with the HTTP status code.
217
- :rtype: Tuple(dict, integer)
218
- """
219
-
220
- # get report objet
221
- report = self.get_detail(user_id=self.get_user_id(), idx=idx)
222
-
223
- if report is None:
224
- raise ObjectDoesNotExist
225
-
226
- # delete file
227
- os.remove(get_report_path(report))
228
-
229
- return self.delete_detail(user_id=self.get_user_id(), idx=idx)
230
-
231
-
232
- def get_report_dir(execution_id):
233
- return f"{current_app.config['UPLOAD_FOLDER']}/{execution_id}"
234
-
235
-
236
- def get_report_path(report):
237
- try:
238
- return f"{get_report_dir(report['execution_id'])}/{report['file_url']}"
239
- except:
240
- return f"{get_report_dir(report.execution_id)}/{report.file_url}"
241
-
242
-
243
- def new_file_name(file):
244
-
245
- filename = secure_filename(file.filename)
246
- filename_extension = filename.split(".")[-1]
247
-
248
- if filename_extension not in current_app.config["ALLOWED_EXTENSIONS"]:
249
- return {
250
- "message": f"Invalid file extension. "
251
- f"Valid extensions are: {current_app.config['ALLOWED_EXTENSIONS']}"
252
- }, 400
253
-
254
- report_name = f"{uuid.uuid4().hex}.{filename_extension}"
255
-
256
- return report_name
257
-
258
-
259
- def write_file(file, execution_id, file_name):
260
- my_directory = get_report_dir(execution_id)
261
-
262
- # we create a directory for the execution
263
- if not os.path.exists(my_directory):
264
- current_app.logger.info(f"Creating directory {my_directory}")
265
- os.mkdir(my_directory)
266
-
267
- save_path = os.path.normpath(os.path.join(my_directory, file_name))
268
-
269
- if "static" not in save_path or ".." in save_path:
270
- raise NoPermission("Invalid file name")
271
- file.save(save_path)
272
-
273
-
274
- def get_report_info(data, execution, file_url=None):
275
- return {
276
- "name": data["name"],
277
- "file_url": file_url,
278
- "execution_id": execution.id,
279
- "user_id": execution.user_id,
280
- "description": data.get("description", ""),
281
- "state": data.get("state"),
282
- "state_message": data.get("state_message"),
283
- }
@@ -1,40 +0,0 @@
1
- """empty message
2
-
3
- Revision ID: 83164be03c23
4
- Revises: 96f00d0961d1
5
- Create Date: 2024-07-23 13:18:47.748324
6
-
7
- """
8
- from alembic import op
9
- import sqlalchemy as sa
10
-
11
-
12
- # revision identifiers, used by Alembic.
13
- revision = '83164be03c23'
14
- down_revision = '96f00d0961d1'
15
- branch_labels = None
16
- depends_on = None
17
-
18
-
19
- def upgrade():
20
- # ### commands auto generated by Alembic - please adjust! ###
21
- with op.batch_alter_table('reports', schema=None) as batch_op:
22
- batch_op.add_column(sa.Column('state', sa.SmallInteger(), nullable=True))
23
- batch_op.add_column(sa.Column('state_message', sa.TEXT(), nullable=True))
24
- batch_op.alter_column('file_url',
25
- existing_type=sa.VARCHAR(length=256),
26
- nullable=True)
27
-
28
- # ### end Alembic commands ###
29
-
30
-
31
- def downgrade():
32
- # ### commands auto generated by Alembic - please adjust! ###
33
- with op.batch_alter_table('reports', schema=None) as batch_op:
34
- batch_op.alter_column('file_url',
35
- existing_type=sa.VARCHAR(length=256),
36
- nullable=False)
37
- batch_op.drop_column('state_message')
38
- batch_op.drop_column('state')
39
-
40
- # ### end Alembic commands ###
@@ -1,50 +0,0 @@
1
- """
2
- Adds reports table to database
3
-
4
- Revision ID: 96f00d0961d1
5
- Revises: 991b98e24225
6
- Create Date: 2024-06-12 18:47:06.366487
7
-
8
- """
9
-
10
- from alembic import op
11
- import sqlalchemy as sa
12
-
13
-
14
- # revision identifiers, used by Alembic.
15
- revision = "96f00d0961d1"
16
- down_revision = "991b98e24225"
17
- branch_labels = None
18
- depends_on = None
19
-
20
-
21
- def upgrade():
22
- # ### commands auto generated by Alembic - please adjust! ###
23
- op.create_table(
24
- "reports",
25
- sa.Column("created_at", sa.DateTime(), nullable=False),
26
- sa.Column("updated_at", sa.DateTime(), nullable=False),
27
- sa.Column("deleted_at", sa.DateTime(), nullable=True),
28
- sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
29
- sa.Column("execution_id", sa.String(length=256), nullable=False),
30
- sa.Column("name", sa.String(length=256), nullable=False),
31
- sa.Column("description", sa.TEXT(), nullable=True),
32
- sa.Column("file_url", sa.String(length=256), nullable=False),
33
- sa.Column("user_id", sa.Integer(), nullable=False),
34
- sa.ForeignKeyConstraint(
35
- ["execution_id"],
36
- ["executions.id"],
37
- ),
38
- sa.ForeignKeyConstraint(
39
- ["user_id"],
40
- ["users.id"],
41
- ),
42
- sa.PrimaryKeyConstraint("id"),
43
- )
44
- # ### end Alembic commands ###
45
-
46
-
47
- def downgrade():
48
- # ### commands auto generated by Alembic - please adjust! ###
49
- op.drop_table("reports")
50
- # ### end Alembic commands ###
@@ -1,119 +0,0 @@
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__()
@@ -1,48 +0,0 @@
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()