cornflow 1.1.0a2__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.
Files changed (188) hide show
  1. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/MANIFEST.in +2 -1
  2. {cornflow-1.1.0a2/cornflow.egg-info → cornflow-1.1.1a1}/PKG-INFO +1 -1
  3. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/app.py +4 -0
  4. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/utils.py +1 -1
  5. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/config.py +10 -2
  6. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/__init__.py +14 -0
  7. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/execution.py +1 -1
  8. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/login.py +26 -6
  9. cornflow-1.1.1a1/cornflow/endpoints/reports.py +283 -0
  10. cornflow-1.1.1a1/cornflow/migrations/versions/83164be03c23_.py +40 -0
  11. cornflow-1.1.1a1/cornflow/migrations/versions/96f00d0961d1_reports_table.py +50 -0
  12. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/__init__.py +2 -0
  13. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/execution.py +8 -0
  14. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/meta_models.py +23 -12
  15. cornflow-1.1.1a1/cornflow/models/reports.py +119 -0
  16. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/execution.py +3 -0
  17. cornflow-1.1.1a1/cornflow/schemas/reports.py +48 -0
  18. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/const.py +21 -0
  19. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/exceptions.py +20 -9
  20. cornflow-1.1.1a1/cornflow/static/v1.json +3854 -0
  21. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/const.py +7 -0
  22. cornflow-1.1.0a2/cornflow/tests/custom_liveServer.py → cornflow-1.1.1a1/cornflow/tests/custom_live_server.py +3 -1
  23. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/custom_test_case.py +2 -3
  24. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/integration/test_commands.py +1 -1
  25. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/integration/test_cornflowclient.py +116 -28
  26. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_alarms.py +22 -9
  27. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_cli.py +10 -5
  28. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_commands.py +6 -2
  29. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_executions.py +5 -0
  30. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_main_alarms.py +8 -0
  31. cornflow-1.1.1a1/cornflow/tests/unit/test_reports.py +308 -0
  32. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_users.py +5 -2
  33. {cornflow-1.1.0a2 → cornflow-1.1.1a1/cornflow.egg-info}/PKG-INFO +1 -1
  34. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow.egg-info/SOURCES.txt +8 -1
  35. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow.egg-info/requires.txt +2 -2
  36. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/setup.py +1 -1
  37. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/README.rst +0 -0
  38. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/airflow_config/__init__.py +0 -0
  39. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/airflow_config/airflow_local_settings.py +0 -0
  40. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/airflow_config/plugins/XCom/__init__.py +0 -0
  41. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/airflow_config/plugins/XCom/gce_xcom_backend.py +0 -0
  42. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/airflow_config/plugins/__init__.py +0 -0
  43. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/airflow_config/webserver_ldap.py +0 -0
  44. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/__init__.py +0 -0
  45. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/__init__.py +0 -0
  46. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/actions.py +0 -0
  47. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/arguments.py +0 -0
  48. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/config.py +0 -0
  49. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/migrations.py +0 -0
  50. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/permissions.py +0 -0
  51. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/roles.py +0 -0
  52. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/schemas.py +0 -0
  53. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/service.py +0 -0
  54. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/tools/__init__.py +0 -0
  55. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/tools/api_generator.py +0 -0
  56. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/tools/endpoint_tools.py +0 -0
  57. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/tools/models_tools.py +0 -0
  58. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/tools/schema_generator.py +0 -0
  59. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/tools/schemas_tools.py +0 -0
  60. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/tools/tools.py +0 -0
  61. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/users.py +0 -0
  62. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/cli/views.py +0 -0
  63. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/__init__.py +0 -0
  64. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/access.py +0 -0
  65. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/actions.py +0 -0
  66. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/cleanup.py +0 -0
  67. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/dag.py +0 -0
  68. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/permissions.py +0 -0
  69. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/roles.py +0 -0
  70. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/schemas.py +0 -0
  71. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/users.py +0 -0
  72. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/commands/views.py +0 -0
  73. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/action.py +0 -0
  74. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/alarms.py +0 -0
  75. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/apiview.py +0 -0
  76. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/case.py +0 -0
  77. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/dag.py +0 -0
  78. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/data_check.py +0 -0
  79. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/example_data.py +0 -0
  80. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/health.py +0 -0
  81. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/instance.py +0 -0
  82. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/licenses.py +0 -0
  83. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/main_alarms.py +0 -0
  84. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/meta_resource.py +0 -0
  85. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/permission.py +0 -0
  86. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/roles.py +0 -0
  87. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/schemas.py +0 -0
  88. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/signup.py +0 -0
  89. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/tables.py +0 -0
  90. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/token.py +0 -0
  91. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/user.py +0 -0
  92. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/endpoints/user_role.py +0 -0
  93. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/gunicorn.py +0 -0
  94. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/README +0 -0
  95. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/alembic.ini +0 -0
  96. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/env.py +0 -0
  97. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/script.py.mako +0 -0
  98. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/00757b557b02_.py +0 -0
  99. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/1af47a419bbd_.py +0 -0
  100. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/4aac5e0c6e66_.py +0 -0
  101. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/7c3ea5ab5501_.py +0 -0
  102. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/991b98e24225_.py +0 -0
  103. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/a472b5ad50b7_.py +0 -0
  104. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/c2db9409cb5f_.py +0 -0
  105. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/c8a6c762e818_.py +0 -0
  106. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/ca449af8034c_.py +0 -0
  107. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/d0e0700dcd8e_.py +0 -0
  108. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/d1b5be1f0549_.py +0 -0
  109. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/e1a50dae1ac9_.py +0 -0
  110. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/e937a5234ce4_.py +0 -0
  111. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/ebdd955fcc5e_.py +0 -0
  112. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/migrations/versions/f3bee20314a2_.py +0 -0
  113. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/action.py +0 -0
  114. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/alarms.py +0 -0
  115. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/base_data_model.py +0 -0
  116. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/case.py +0 -0
  117. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/dag.py +0 -0
  118. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/dag_permissions.py +0 -0
  119. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/instance.py +0 -0
  120. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/main_alarms.py +0 -0
  121. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/permissions.py +0 -0
  122. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/role.py +0 -0
  123. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/user.py +0 -0
  124. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/user_role.py +0 -0
  125. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/models/view.py +0 -0
  126. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/__init__.py +0 -0
  127. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/action.py +0 -0
  128. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/alarms.py +0 -0
  129. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/case.py +0 -0
  130. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/common.py +0 -0
  131. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/dag.py +0 -0
  132. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/example_data.py +0 -0
  133. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/health.py +0 -0
  134. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/instance.py +0 -0
  135. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/main_alarms.py +0 -0
  136. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/model_json.py +0 -0
  137. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/patch.py +0 -0
  138. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/permissions.py +0 -0
  139. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/query.py +0 -0
  140. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/role.py +0 -0
  141. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/schemas.py +0 -0
  142. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/solution_log.py +0 -0
  143. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/tables.py +0 -0
  144. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/user.py +0 -0
  145. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/user_role.py +0 -0
  146. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/schemas/view.py +0 -0
  147. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/__init__.py +0 -0
  148. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/authentication/__init__.py +0 -0
  149. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/authentication/auth.py +0 -0
  150. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/authentication/decorators.py +0 -0
  151. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/authentication/ldap.py +0 -0
  152. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/compress.py +0 -0
  153. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/email.py +0 -0
  154. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/licenses.py +0 -0
  155. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/log_config.py +0 -0
  156. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/query_tools.py +0 -0
  157. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/utils.py +0 -0
  158. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/utils_tables.py +0 -0
  159. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/shared/validators.py +0 -0
  160. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/__init__.py +0 -0
  161. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/integration/__init__.py +0 -0
  162. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/ldap/__init__.py +0 -0
  163. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/ldap/test_ldap_authentication.py +0 -0
  164. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/__init__.py +0 -0
  165. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_actions.py +0 -0
  166. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_apiview.py +0 -0
  167. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_cases.py +0 -0
  168. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_dags.py +0 -0
  169. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_data_checks.py +0 -0
  170. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_example_data.py +0 -0
  171. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_generate_from_schema.py +0 -0
  172. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_health.py +0 -0
  173. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_instances.py +0 -0
  174. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_instances_file.py +0 -0
  175. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_licenses.py +0 -0
  176. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_log_in.py +0 -0
  177. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_permissions.py +0 -0
  178. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_roles.py +0 -0
  179. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_schema_from_models.py +0 -0
  180. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_schemas.py +0 -0
  181. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_sign_up.py +0 -0
  182. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_tables.py +0 -0
  183. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/test_token.py +0 -0
  184. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow/tests/unit/tools.py +0 -0
  185. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow.egg-info/dependency_links.txt +0 -0
  186. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow.egg-info/entry_points.txt +0 -0
  187. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/cornflow.egg-info/top_level.txt +0 -0
  188. {cornflow-1.1.0a2 → cornflow-1.1.1a1}/setup.cfg +0 -0
@@ -3,4 +3,5 @@ include MANIFEST.in
3
3
  include README.rst
4
4
  include setup.py
5
5
  include cornflow/migrations/*
6
- include cornflow/migrations/versions/*
6
+ include cornflow/migrations/versions/*
7
+ include cornflow/static/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.2
2
2
  Name: cornflow
3
- Version: 1.1.0a2
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", "sqlite:///cornflow.db")
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 = False
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}
@@ -37,6 +37,8 @@ from .execution import (
37
37
  ExecutionLogEndpoint,
38
38
  ExecutionRelaunchEndpoint,
39
39
  )
40
+
41
+ from .reports import ReportEndpoint, ReportDetailsEndpoint, ReportDetailsEditEndpoint
40
42
  from .health import HealthEndpoint
41
43
  from .instance import (
42
44
  InstanceEndpoint,
@@ -47,6 +49,7 @@ from .instance import (
47
49
  from .licenses import LicensesEndpoint
48
50
  from .main_alarms import MainAlarmsEndpoint
49
51
  from .permission import PermissionsViewRoleEndpoint, PermissionsViewRoleDetailEndpoint
52
+ from .reports import ReportEndpoint, ReportDetailsEndpoint
50
53
  from .roles import RolesListEndpoint, RoleDetailEndpoint
51
54
  from .schemas import SchemaDetailsEndpoint, SchemaEndpoint
52
55
  from .tables import TablesEndpoint, TablesDetailsEndpoint
@@ -216,6 +219,17 @@ resources = [
216
219
  urls="/table/<string:table_name>/<string:idx>/",
217
220
  endpoint="tables-detail",
218
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"),
219
233
  ]
220
234
 
221
235
 
@@ -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 hve different access url, but manage the same data entities
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(f"LDAP user {username} does not exist and is created")
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(f"Integrity error on user role assignment on log in: {e}")
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(f"Unknown error on user role assignment on log in: {e}")
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(f"OpenID user {username} does not exist and is created")
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 user.pwd_last_change + timedelta(days=int(current_app.config["PWD_ROTATION_TIME"])) < datetime.utcnow():
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
- return self.log_in(**kwargs)
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
+ }
@@ -0,0 +1,40 @@
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 ###
@@ -0,0 +1,50 @@
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,6 +1,7 @@
1
1
  """
2
2
  Initialization file for the models module
3
3
  """
4
+
4
5
  from .action import ActionModel
5
6
  from .alarms import AlarmsModel
6
7
  from .case import CaseModel
@@ -14,3 +15,4 @@ from .role import RoleModel
14
15
  from .user import UserModel
15
16
  from .user_role import UserRoleModel
16
17
  from .view import ViewModel
18
+ from .reports import ReportModel
@@ -64,6 +64,14 @@ class ExecutionModel(BaseDataModel):
64
64
  default=EXECUTION_STATE_MESSAGE_DICT[DEFAULT_EXECUTION_CODE],
65
65
  nullable=True,
66
66
  )
67
+ reports = db.relationship(
68
+ "ReportModel",
69
+ backref="executions",
70
+ lazy=True,
71
+ primaryjoin="and_(ExecutionModel.id==ReportModel.execution_id, "
72
+ "ReportModel.deleted_at==None)",
73
+ cascade="all,delete",
74
+ )
67
75
 
68
76
  def __init__(self, data):
69
77
  super().__init__(data)