cornflow 1.1.0a1__tar.gz → 1.1.1a1__tar.gz
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-1.1.0a1 → cornflow-1.1.1a1}/MANIFEST.in +2 -1
- {cornflow-1.1.0a1/cornflow.egg-info → cornflow-1.1.1a1}/PKG-INFO +1 -1
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/app.py +4 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/utils.py +1 -1
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/config.py +10 -2
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/__init__.py +28 -19
- cornflow-1.1.1a1/cornflow/endpoints/example_data.py +119 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/execution.py +1 -1
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/login.py +26 -6
- cornflow-1.1.1a1/cornflow/endpoints/reports.py +283 -0
- cornflow-1.1.1a1/cornflow/migrations/versions/83164be03c23_.py +40 -0
- cornflow-1.1.1a1/cornflow/migrations/versions/96f00d0961d1_reports_table.py +50 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/__init__.py +2 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/execution.py +8 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/meta_models.py +23 -12
- cornflow-1.1.1a1/cornflow/models/reports.py +119 -0
- cornflow-1.1.1a1/cornflow/schemas/example_data.py +11 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/execution.py +3 -0
- cornflow-1.1.1a1/cornflow/schemas/reports.py +48 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/const.py +21 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/exceptions.py +20 -9
- cornflow-1.1.1a1/cornflow/static/v1.json +3854 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/const.py +7 -0
- cornflow-1.1.0a1/cornflow/tests/custom_liveServer.py → cornflow-1.1.1a1/cornflow/tests/custom_live_server.py +3 -1
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/custom_test_case.py +2 -3
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/integration/test_commands.py +1 -1
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/integration/test_cornflowclient.py +116 -28
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_alarms.py +22 -9
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_cli.py +10 -5
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_commands.py +6 -2
- cornflow-1.1.1a1/cornflow/tests/unit/test_example_data.py +124 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_executions.py +5 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_main_alarms.py +8 -0
- cornflow-1.1.1a1/cornflow/tests/unit/test_reports.py +308 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_users.py +5 -2
- {cornflow-1.1.0a1 → cornflow-1.1.1a1/cornflow.egg-info}/PKG-INFO +1 -1
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow.egg-info/SOURCES.txt +8 -1
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow.egg-info/requires.txt +2 -2
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/setup.py +1 -1
- cornflow-1.1.0a1/cornflow/endpoints/example_data.py +0 -68
- cornflow-1.1.0a1/cornflow/schemas/example_data.py +0 -6
- cornflow-1.1.0a1/cornflow/tests/unit/test_example_data.py +0 -54
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/README.rst +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/airflow_config/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/airflow_config/airflow_local_settings.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/airflow_config/plugins/XCom/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/airflow_config/plugins/XCom/gce_xcom_backend.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/airflow_config/plugins/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/airflow_config/webserver_ldap.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/actions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/arguments.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/config.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/migrations.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/permissions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/roles.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/schemas.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/service.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/tools/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/tools/api_generator.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/tools/endpoint_tools.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/tools/models_tools.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/tools/schema_generator.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/tools/schemas_tools.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/tools/tools.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/users.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/cli/views.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/access.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/actions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/cleanup.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/dag.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/permissions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/roles.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/schemas.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/users.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/commands/views.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/action.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/alarms.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/apiview.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/case.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/dag.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/data_check.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/health.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/instance.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/licenses.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/main_alarms.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/meta_resource.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/permission.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/roles.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/schemas.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/signup.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/tables.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/token.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/user.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/endpoints/user_role.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/gunicorn.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/README +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/alembic.ini +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/env.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/script.py.mako +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/00757b557b02_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/1af47a419bbd_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/4aac5e0c6e66_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/7c3ea5ab5501_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/991b98e24225_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/a472b5ad50b7_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/c2db9409cb5f_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/c8a6c762e818_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/ca449af8034c_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/d0e0700dcd8e_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/d1b5be1f0549_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/e1a50dae1ac9_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/e937a5234ce4_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/ebdd955fcc5e_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/migrations/versions/f3bee20314a2_.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/action.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/alarms.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/base_data_model.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/case.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/dag.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/dag_permissions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/instance.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/main_alarms.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/permissions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/role.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/user.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/user_role.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/models/view.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/action.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/alarms.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/case.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/common.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/dag.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/health.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/instance.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/main_alarms.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/model_json.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/patch.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/permissions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/query.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/role.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/schemas.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/solution_log.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/tables.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/user.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/user_role.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/schemas/view.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/authentication/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/authentication/auth.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/authentication/decorators.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/authentication/ldap.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/compress.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/email.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/licenses.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/log_config.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/query_tools.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/utils.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/utils_tables.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/shared/validators.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/integration/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/ldap/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/ldap/test_ldap_authentication.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/__init__.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_actions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_apiview.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_cases.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_dags.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_data_checks.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_generate_from_schema.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_health.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_instances.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_instances_file.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_licenses.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_log_in.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_permissions.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_roles.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_schema_from_models.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_schemas.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_sign_up.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_tables.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/test_token.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow/tests/unit/tools.py +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow.egg-info/dependency_links.txt +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow.egg-info/entry_points.txt +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/cornflow.egg-info/top_level.txt +0 -0
- {cornflow-1.1.0a1 → cornflow-1.1.1a1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 1.2
|
2
2
|
Name: cornflow
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.1a1
|
4
4
|
Summary: Cornflow is an open source multi-solver optimization server with a REST API built using flask.
|
5
5
|
Home-page: https://github.com/baobabsoluciones/cornflow
|
6
6
|
Author: baobab soluciones
|
@@ -46,6 +46,9 @@ def create_app(env_name="development", dataconn=None):
|
|
46
46
|
:return: the application that is going to be running :class:`Flask`
|
47
47
|
:rtype: :class:`Flask`
|
48
48
|
"""
|
49
|
+
if os.getenv("FLASK_ENV", None) is not None:
|
50
|
+
env_name = os.getenv("FLASK_ENV")
|
51
|
+
|
49
52
|
dictConfig(log_config(app_config[env_name].LOG_LEVEL))
|
50
53
|
|
51
54
|
app = Flask(__name__)
|
@@ -74,6 +77,7 @@ def create_app(env_name="development", dataconn=None):
|
|
74
77
|
api = Api(app)
|
75
78
|
for res in resources:
|
76
79
|
api.add_resource(res["resource"], res["urls"], endpoint=res["endpoint"])
|
80
|
+
|
77
81
|
if app.config["ALARMS_ENDPOINTS"]:
|
78
82
|
for res in alarms_resources:
|
79
83
|
api.add_resource(res["resource"], res["urls"], endpoint=res["endpoint"])
|
@@ -6,7 +6,7 @@ import warnings
|
|
6
6
|
|
7
7
|
def get_app():
|
8
8
|
env = os.getenv("FLASK_ENV", "development")
|
9
|
-
data_conn = os.getenv("DATABASE_URL"
|
9
|
+
data_conn = os.getenv("DATABASE_URL")
|
10
10
|
if env == "production":
|
11
11
|
warnings.filterwarnings("ignore")
|
12
12
|
external = int(os.getenv("EXTERNAL_APP", 0))
|
@@ -22,6 +22,14 @@ class DefaultConfig(object):
|
|
22
22
|
SIGNUP_ACTIVATED = int(os.getenv("SIGNUP_ACTIVATED", 1))
|
23
23
|
CORNFLOW_SERVICE_USER = os.getenv("CORNFLOW_SERVICE_USER", "service_user")
|
24
24
|
|
25
|
+
# file support for reports
|
26
|
+
FILE_BACKEND = os.getenv("FILE_BACKEND", "local")
|
27
|
+
UPLOAD_FOLDER = os.getenv(
|
28
|
+
"UPLOAD_FOLDER",
|
29
|
+
os.path.abspath(os.path.join(os.path.dirname(__file__), "./static")),
|
30
|
+
)
|
31
|
+
ALLOWED_EXTENSIONS = os.getenv("ALLOWED_EXTENSIONS", ["pdf", "html"])
|
32
|
+
|
25
33
|
# Open deployment (all dags accessible to all users)
|
26
34
|
OPEN_DEPLOYMENT = os.getenv("OPEN_DEPLOYMENT", 1)
|
27
35
|
|
@@ -84,7 +92,6 @@ class DefaultConfig(object):
|
|
84
92
|
|
85
93
|
|
86
94
|
class Development(DefaultConfig):
|
87
|
-
|
88
95
|
""" """
|
89
96
|
|
90
97
|
ENV = "development"
|
@@ -95,7 +102,7 @@ class Testing(DefaultConfig):
|
|
95
102
|
|
96
103
|
ENV = "testing"
|
97
104
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
98
|
-
DEBUG =
|
105
|
+
DEBUG = True
|
99
106
|
TESTING = True
|
100
107
|
PROPAGATE_EXCEPTIONS = True
|
101
108
|
SECRET_TOKEN_KEY = "TESTINGSECRETKEY"
|
@@ -119,6 +126,7 @@ class Production(DefaultConfig):
|
|
119
126
|
# needs to be on to avoid getting only 500 codes:
|
120
127
|
# and https://medium.com/@johanesriandy/flask-error-handler-not-working-on-production-mode-3adca4c7385c
|
121
128
|
PROPAGATE_EXCEPTIONS = True
|
129
|
+
UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER", "/usr/src/app/static")
|
122
130
|
|
123
131
|
|
124
132
|
app_config = {"development": Development, "testing": Testing, "production": Production}
|
@@ -4,8 +4,8 @@ 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
|
from .action import ActionListEndpoint
|
7
|
+
from .alarms import AlarmsEndpoint
|
7
8
|
from .apiview import ApiViewListEndpoint
|
8
|
-
|
9
9
|
from .case import (
|
10
10
|
CaseEndpoint,
|
11
11
|
CaseFromInstanceExecutionEndpoint,
|
@@ -15,7 +15,6 @@ from .case import (
|
|
15
15
|
CaseToInstance,
|
16
16
|
CaseCompare,
|
17
17
|
)
|
18
|
-
|
19
18
|
from .dag import (
|
20
19
|
DAGDetailEndpoint,
|
21
20
|
DAGEndpointManual,
|
@@ -24,7 +23,12 @@ from .dag import (
|
|
24
23
|
DeployedDAGEndpoint,
|
25
24
|
DeployedDagDetailEndpoint,
|
26
25
|
)
|
27
|
-
|
26
|
+
from .data_check import (
|
27
|
+
DataCheckExecutionEndpoint,
|
28
|
+
DataCheckInstanceEndpoint,
|
29
|
+
DataCheckCaseEndpoint,
|
30
|
+
)
|
31
|
+
from .example_data import ExampleDataListEndpoint, ExampleDataDetailEndpoint
|
28
32
|
from .execution import (
|
29
33
|
ExecutionEndpoint,
|
30
34
|
ExecutionDetailsEndpoint,
|
@@ -34,35 +38,24 @@ from .execution import (
|
|
34
38
|
ExecutionRelaunchEndpoint,
|
35
39
|
)
|
36
40
|
|
41
|
+
from .reports import ReportEndpoint, ReportDetailsEndpoint, ReportDetailsEditEndpoint
|
37
42
|
from .health import HealthEndpoint
|
38
|
-
|
39
43
|
from .instance import (
|
40
44
|
InstanceEndpoint,
|
41
45
|
InstanceDetailsEndpoint,
|
42
46
|
InstanceFileEndpoint,
|
43
47
|
InstanceDataEndpoint,
|
44
48
|
)
|
45
|
-
|
46
|
-
from .data_check import (
|
47
|
-
DataCheckExecutionEndpoint,
|
48
|
-
DataCheckInstanceEndpoint,
|
49
|
-
DataCheckCaseEndpoint,
|
50
|
-
)
|
51
49
|
from .licenses import LicensesEndpoint
|
50
|
+
from .main_alarms import MainAlarmsEndpoint
|
52
51
|
from .permission import PermissionsViewRoleEndpoint, PermissionsViewRoleDetailEndpoint
|
53
|
-
|
52
|
+
from .reports import ReportEndpoint, ReportDetailsEndpoint
|
54
53
|
from .roles import RolesListEndpoint, RoleDetailEndpoint
|
55
|
-
|
56
54
|
from .schemas import SchemaDetailsEndpoint, SchemaEndpoint
|
55
|
+
from .tables import TablesEndpoint, TablesDetailsEndpoint
|
57
56
|
from .token import TokenEndpoint
|
58
|
-
from .example_data import ExampleDataDetailsEndpoint
|
59
57
|
from .user import UserEndpoint, UserDetailsEndpoint, ToggleUserAdmin, RecoverPassword
|
60
58
|
from .user_role import UserRoleListEndpoint, UserRoleDetailEndpoint
|
61
|
-
from .alarms import AlarmsEndpoint
|
62
|
-
from .main_alarms import MainAlarmsEndpoint
|
63
|
-
|
64
|
-
from .tables import TablesEndpoint, TablesDetailsEndpoint
|
65
|
-
|
66
59
|
|
67
60
|
resources = [
|
68
61
|
dict(resource=InstanceEndpoint, urls="/instance/", endpoint="instance"),
|
@@ -157,10 +150,15 @@ resources = [
|
|
157
150
|
endpoint="schema-details",
|
158
151
|
),
|
159
152
|
dict(
|
160
|
-
resource=
|
153
|
+
resource=ExampleDataListEndpoint,
|
161
154
|
urls="/example/<string:dag_name>/",
|
162
155
|
endpoint="example-data",
|
163
156
|
),
|
157
|
+
dict(
|
158
|
+
resource=ExampleDataDetailEndpoint,
|
159
|
+
urls="/example/<string:dag_name>/<string:example_name>/",
|
160
|
+
endpoint="example-data-detail",
|
161
|
+
),
|
164
162
|
dict(resource=HealthEndpoint, urls="/health/", endpoint="health"),
|
165
163
|
dict(
|
166
164
|
resource=CaseFromInstanceExecutionEndpoint,
|
@@ -221,6 +219,17 @@ resources = [
|
|
221
219
|
urls="/table/<string:table_name>/<string:idx>/",
|
222
220
|
endpoint="tables-detail",
|
223
221
|
),
|
222
|
+
dict(
|
223
|
+
resource=ReportDetailsEndpoint,
|
224
|
+
urls="/report/<int:idx>/",
|
225
|
+
endpoint="report-detail",
|
226
|
+
),
|
227
|
+
dict(
|
228
|
+
resource=ReportDetailsEditEndpoint,
|
229
|
+
urls="/report/<int:idx>/edit/",
|
230
|
+
endpoint="report-detail-edit",
|
231
|
+
),
|
232
|
+
dict(resource=ReportEndpoint, urls="/report/", endpoint="report"),
|
224
233
|
]
|
225
234
|
|
226
235
|
|
@@ -0,0 +1,119 @@
|
|
1
|
+
"""
|
2
|
+
Endpoints to get the example data from a DAG
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
|
6
|
+
from cornflow_client.airflow.api import Airflow
|
7
|
+
from flask import current_app, request
|
8
|
+
from flask_apispec import marshal_with, doc
|
9
|
+
|
10
|
+
from cornflow.endpoints.meta_resource import BaseMetaResource
|
11
|
+
from cornflow.models import PermissionsDAG
|
12
|
+
from cornflow.schemas.example_data import ExampleListData, ExampleDetailData
|
13
|
+
from cornflow.shared.authentication import Auth, authenticate
|
14
|
+
from cornflow.shared.const import VIEWER_ROLE, PLANNER_ROLE, ADMIN_ROLE
|
15
|
+
from cornflow.shared.exceptions import AirflowError, NoPermission, ObjectDoesNotExist
|
16
|
+
|
17
|
+
|
18
|
+
class ExampleDataListEndpoint(BaseMetaResource):
|
19
|
+
"""
|
20
|
+
Endpoint used to obtain schemas for one app
|
21
|
+
"""
|
22
|
+
|
23
|
+
ROLES_WITH_ACCESS = [VIEWER_ROLE, PLANNER_ROLE, ADMIN_ROLE]
|
24
|
+
|
25
|
+
@doc(description="Get lsit of example data from DAG", tags=["DAG"])
|
26
|
+
@authenticate(auth_class=Auth())
|
27
|
+
@marshal_with(ExampleListData(many=True))
|
28
|
+
def get(self, dag_name):
|
29
|
+
"""
|
30
|
+
API method to get example data for a given dag
|
31
|
+
|
32
|
+
:return: A dictionary with the names and descriptions of available data examples
|
33
|
+
and an integer with the HTTP status code
|
34
|
+
:rtype: Tuple(dict, integer)
|
35
|
+
"""
|
36
|
+
user = Auth().get_user_from_header(request.headers)
|
37
|
+
permission = PermissionsDAG.check_if_has_permissions(
|
38
|
+
user_id=user.id, dag_id=dag_name
|
39
|
+
)
|
40
|
+
|
41
|
+
if permission:
|
42
|
+
af_client = Airflow.from_config(current_app.config)
|
43
|
+
if not af_client.is_alive():
|
44
|
+
current_app.logger.error(
|
45
|
+
"Airflow not accessible when getting data {}".format(dag_name)
|
46
|
+
)
|
47
|
+
raise AirflowError(error="Airflow is not accessible")
|
48
|
+
|
49
|
+
# try airflow and see if dag_name exists
|
50
|
+
af_client.get_dag_info(dag_name)
|
51
|
+
|
52
|
+
current_app.logger.info("User gets example data from {}".format(dag_name))
|
53
|
+
|
54
|
+
variable_name = f"z_{dag_name}_examples"
|
55
|
+
response = af_client.get_one_variable(variable_name)
|
56
|
+
|
57
|
+
return json.loads(response["value"])
|
58
|
+
else:
|
59
|
+
err = "User does not have permission to access this dag."
|
60
|
+
raise NoPermission(
|
61
|
+
error=err,
|
62
|
+
status_code=403,
|
63
|
+
log_txt=f"Error while user {user} tries to get example data for dag {dag_name}. "
|
64
|
+
+ err,
|
65
|
+
)
|
66
|
+
|
67
|
+
|
68
|
+
class ExampleDataDetailEndpoint(BaseMetaResource):
|
69
|
+
@doc(description="Get example data from DAG", tags=["DAG"])
|
70
|
+
@authenticate(auth_class=Auth())
|
71
|
+
@marshal_with(ExampleDetailData)
|
72
|
+
def get(self, dag_name, example_name):
|
73
|
+
"""
|
74
|
+
API method to get one example data for a given dag
|
75
|
+
|
76
|
+
:return: A dictionary with a message and a integer with the HTTP status code
|
77
|
+
:rtype: Tuple(dict, integer)
|
78
|
+
"""
|
79
|
+
user = Auth().get_user_from_header(request.headers)
|
80
|
+
permission = PermissionsDAG.check_if_has_permissions(
|
81
|
+
user_id=user.id, dag_id=dag_name
|
82
|
+
)
|
83
|
+
|
84
|
+
if permission:
|
85
|
+
af_client = Airflow.from_config(current_app.config)
|
86
|
+
if not af_client.is_alive():
|
87
|
+
current_app.logger.error(
|
88
|
+
"Airflow not accessible when getting data {}".format(dag_name)
|
89
|
+
)
|
90
|
+
raise AirflowError(error="Airflow is not accessible")
|
91
|
+
|
92
|
+
# try airflow and see if dag_name exists
|
93
|
+
af_client.get_dag_info(dag_name)
|
94
|
+
|
95
|
+
current_app.logger.info("User gets example data from {}".format(dag_name))
|
96
|
+
|
97
|
+
variable_name = f"z_{dag_name}_examples"
|
98
|
+
response = af_client.get_one_variable(variable_name)
|
99
|
+
|
100
|
+
example = None
|
101
|
+
for item in json.loads(response["value"]):
|
102
|
+
if item["name"] == example_name:
|
103
|
+
example = item
|
104
|
+
break
|
105
|
+
|
106
|
+
if example is None:
|
107
|
+
raise ObjectDoesNotExist(
|
108
|
+
error="The example does not exist", status_code=404
|
109
|
+
)
|
110
|
+
|
111
|
+
return example
|
112
|
+
else:
|
113
|
+
err = "User does not have permission to access this dag."
|
114
|
+
raise NoPermission(
|
115
|
+
error=err,
|
116
|
+
status_code=403,
|
117
|
+
log_txt=f"Error while user {user} tries to get example data for dag {dag_name}. "
|
118
|
+
+ err,
|
119
|
+
)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
External endpoints to manage the executions: create new ones, list all of them, get one in particular
|
3
3
|
or check the status of an ongoing one
|
4
|
-
These endpoints
|
4
|
+
These endpoints have different access url, but manage the same data entities
|
5
5
|
"""
|
6
6
|
|
7
7
|
# Import from libraries
|
@@ -34,6 +34,7 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
34
34
|
"""
|
35
35
|
Base endpoint to perform a login action from a user
|
36
36
|
"""
|
37
|
+
|
37
38
|
def __init__(self):
|
38
39
|
super().__init__()
|
39
40
|
self.ldap_class = LDAPBase
|
@@ -63,6 +64,7 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
63
64
|
try:
|
64
65
|
token = self.auth_class.generate_token(user.id)
|
65
66
|
except Exception as e:
|
67
|
+
current_app.logger.error(f"Error in generating user token: {str(e)}")
|
66
68
|
raise InvalidUsage(f"Error in generating user token: {str(e)}", 400)
|
67
69
|
|
68
70
|
response.update({"token": token, "id": user.id})
|
@@ -81,9 +83,11 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
81
83
|
user = self.data_model.get_one_object(username=username)
|
82
84
|
|
83
85
|
if not user:
|
86
|
+
current_app.logger.error(f"Error on login user does not exist")
|
84
87
|
raise InvalidCredentials()
|
85
88
|
|
86
89
|
if not user.check_hash(password):
|
90
|
+
current_app.logger.error(f"Error on login invalid credentials")
|
87
91
|
raise InvalidCredentials()
|
88
92
|
|
89
93
|
return user
|
@@ -102,7 +106,9 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
102
106
|
raise InvalidCredentials()
|
103
107
|
user = self.data_model.get_one_object(username=username)
|
104
108
|
if not user:
|
105
|
-
current_app.logger.info(
|
109
|
+
current_app.logger.info(
|
110
|
+
f"LDAP user {username} does not exist and is created"
|
111
|
+
)
|
106
112
|
email = ldap_obj.get_user_email(username)
|
107
113
|
if not email:
|
108
114
|
email = ""
|
@@ -122,10 +128,14 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
122
128
|
|
123
129
|
except IntegrityError as e:
|
124
130
|
db.session.rollback()
|
125
|
-
current_app.logger.error(
|
131
|
+
current_app.logger.error(
|
132
|
+
f"Integrity error on user role assignment on log in: {e}"
|
133
|
+
)
|
126
134
|
except DBAPIError as e:
|
127
135
|
db.session.rollback()
|
128
|
-
current_app.logger.error(
|
136
|
+
current_app.logger.error(
|
137
|
+
f"Unknown error on user role assignment on log in: {e}"
|
138
|
+
)
|
129
139
|
|
130
140
|
return user
|
131
141
|
|
@@ -163,7 +173,9 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
163
173
|
user = self.data_model.get_one_object(username=username)
|
164
174
|
|
165
175
|
if not user:
|
166
|
-
current_app.logger.info(
|
176
|
+
current_app.logger.info(
|
177
|
+
f"OpenID user {username} does not exist and is created"
|
178
|
+
)
|
167
179
|
|
168
180
|
data = {"username": username, "email": username}
|
169
181
|
|
@@ -183,7 +195,11 @@ class LoginBaseEndpoint(BaseMetaResource):
|
|
183
195
|
|
184
196
|
def check_last_password_change(user):
|
185
197
|
if user.pwd_last_change:
|
186
|
-
if
|
198
|
+
if (
|
199
|
+
user.pwd_last_change
|
200
|
+
+ timedelta(days=int(current_app.config["PWD_ROTATION_TIME"]))
|
201
|
+
< datetime.utcnow()
|
202
|
+
):
|
187
203
|
return True
|
188
204
|
return False
|
189
205
|
|
@@ -210,7 +226,11 @@ class LoginEndpoint(LoginBaseEndpoint):
|
|
210
226
|
:rtype: Tuple(dict, integer)
|
211
227
|
"""
|
212
228
|
|
213
|
-
|
229
|
+
try:
|
230
|
+
return self.log_in(**kwargs)
|
231
|
+
except Exception as e:
|
232
|
+
current_app.logger.error(f"Final exception: {str(e)}")
|
233
|
+
raise e
|
214
234
|
|
215
235
|
|
216
236
|
class LoginOpenAuthEndpoint(LoginBaseEndpoint):
|
@@ -0,0 +1,283 @@
|
|
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
|
+
}
|