cornflow 1.1.2__tar.gz → 1.1.5a1__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 (187) hide show
  1. {cornflow-1.1.2 → cornflow-1.1.5a1}/MANIFEST.in +2 -1
  2. cornflow-1.1.5a1/PKG-INFO +264 -0
  3. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/app.py +8 -0
  4. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/config.py +43 -5
  5. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/login.py +86 -35
  6. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/user.py +18 -2
  7. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/authentication/auth.py +10 -4
  8. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/exceptions.py +9 -8
  9. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/custom_test_case.py +342 -0
  10. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_actions.py +46 -1
  11. cornflow-1.1.5a1/cornflow/tests/unit/test_alarms.py +87 -0
  12. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_apiview.py +45 -1
  13. cornflow-1.1.5a1/cornflow/tests/unit/test_application.py +60 -0
  14. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_cases.py +483 -5
  15. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_cli.py +233 -0
  16. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_commands.py +230 -2
  17. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_dags.py +139 -11
  18. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_data_checks.py +134 -2
  19. cornflow-1.1.5a1/cornflow/tests/unit/test_log_in.py +511 -0
  20. cornflow-1.1.5a1/cornflow.egg-info/PKG-INFO +264 -0
  21. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow.egg-info/SOURCES.txt +2 -0
  22. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow.egg-info/entry_points.txt +0 -1
  23. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow.egg-info/requires.txt +11 -11
  24. cornflow-1.1.5a1/requirements.txt +30 -0
  25. {cornflow-1.1.2 → cornflow-1.1.5a1}/setup.py +2 -2
  26. cornflow-1.1.2/PKG-INFO +0 -228
  27. cornflow-1.1.2/cornflow/tests/unit/test_alarms.py +0 -39
  28. cornflow-1.1.2/cornflow/tests/unit/test_log_in.py +0 -33
  29. cornflow-1.1.2/cornflow.egg-info/PKG-INFO +0 -228
  30. {cornflow-1.1.2 → cornflow-1.1.5a1}/README.rst +0 -0
  31. {cornflow-1.1.2 → cornflow-1.1.5a1}/airflow_config/__init__.py +0 -0
  32. {cornflow-1.1.2 → cornflow-1.1.5a1}/airflow_config/airflow_local_settings.py +0 -0
  33. {cornflow-1.1.2 → cornflow-1.1.5a1}/airflow_config/plugins/XCom/__init__.py +0 -0
  34. {cornflow-1.1.2 → cornflow-1.1.5a1}/airflow_config/plugins/XCom/gce_xcom_backend.py +0 -0
  35. {cornflow-1.1.2 → cornflow-1.1.5a1}/airflow_config/plugins/__init__.py +0 -0
  36. {cornflow-1.1.2 → cornflow-1.1.5a1}/airflow_config/webserver_ldap.py +0 -0
  37. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/__init__.py +0 -0
  38. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/__init__.py +0 -0
  39. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/actions.py +0 -0
  40. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/arguments.py +0 -0
  41. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/config.py +0 -0
  42. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/migrations.py +0 -0
  43. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/permissions.py +0 -0
  44. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/roles.py +0 -0
  45. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/schemas.py +0 -0
  46. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/service.py +0 -0
  47. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/tools/__init__.py +0 -0
  48. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/tools/api_generator.py +0 -0
  49. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/tools/endpoint_tools.py +0 -0
  50. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/tools/models_tools.py +0 -0
  51. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/tools/schema_generator.py +0 -0
  52. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/tools/schemas_tools.py +0 -0
  53. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/tools/tools.py +0 -0
  54. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/users.py +0 -0
  55. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/utils.py +0 -0
  56. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/cli/views.py +0 -0
  57. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/__init__.py +0 -0
  58. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/access.py +0 -0
  59. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/actions.py +0 -0
  60. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/cleanup.py +0 -0
  61. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/dag.py +0 -0
  62. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/permissions.py +0 -0
  63. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/roles.py +0 -0
  64. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/schemas.py +0 -0
  65. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/users.py +0 -0
  66. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/commands/views.py +0 -0
  67. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/__init__.py +0 -0
  68. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/action.py +0 -0
  69. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/alarms.py +0 -0
  70. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/apiview.py +0 -0
  71. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/case.py +0 -0
  72. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/dag.py +0 -0
  73. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/data_check.py +0 -0
  74. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/example_data.py +0 -0
  75. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/execution.py +0 -0
  76. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/health.py +0 -0
  77. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/instance.py +0 -0
  78. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/licenses.py +0 -0
  79. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/main_alarms.py +0 -0
  80. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/meta_resource.py +0 -0
  81. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/permission.py +0 -0
  82. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/roles.py +0 -0
  83. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/schemas.py +0 -0
  84. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/signup.py +0 -0
  85. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/tables.py +0 -0
  86. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/token.py +0 -0
  87. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/user.py +0 -0
  88. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/endpoints/user_role.py +0 -0
  89. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/gunicorn.py +0 -0
  90. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/README +0 -0
  91. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/alembic.ini +0 -0
  92. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/env.py +0 -0
  93. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/script.py.mako +0 -0
  94. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/00757b557b02_.py +0 -0
  95. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/1af47a419bbd_.py +0 -0
  96. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/4aac5e0c6e66_.py +0 -0
  97. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/7c3ea5ab5501_.py +0 -0
  98. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/991b98e24225_.py +0 -0
  99. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/a472b5ad50b7_.py +0 -0
  100. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/c2db9409cb5f_.py +0 -0
  101. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/c8a6c762e818_.py +0 -0
  102. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/ca449af8034c_.py +0 -0
  103. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/d0e0700dcd8e_.py +0 -0
  104. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/d1b5be1f0549_.py +0 -0
  105. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/e1a50dae1ac9_.py +0 -0
  106. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/e937a5234ce4_.py +0 -0
  107. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/ebdd955fcc5e_.py +0 -0
  108. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/migrations/versions/f3bee20314a2_.py +0 -0
  109. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/__init__.py +0 -0
  110. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/action.py +0 -0
  111. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/alarms.py +0 -0
  112. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/base_data_model.py +0 -0
  113. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/case.py +0 -0
  114. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/dag.py +0 -0
  115. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/dag_permissions.py +0 -0
  116. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/execution.py +0 -0
  117. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/instance.py +0 -0
  118. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/main_alarms.py +0 -0
  119. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/meta_models.py +0 -0
  120. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/permissions.py +0 -0
  121. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/role.py +0 -0
  122. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/user.py +0 -0
  123. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/user_role.py +0 -0
  124. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/models/view.py +0 -0
  125. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/__init__.py +0 -0
  126. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/action.py +0 -0
  127. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/alarms.py +0 -0
  128. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/case.py +0 -0
  129. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/common.py +0 -0
  130. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/dag.py +0 -0
  131. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/example_data.py +0 -0
  132. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/execution.py +0 -0
  133. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/health.py +0 -0
  134. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/instance.py +0 -0
  135. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/main_alarms.py +0 -0
  136. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/model_json.py +0 -0
  137. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/patch.py +0 -0
  138. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/permissions.py +0 -0
  139. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/query.py +0 -0
  140. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/role.py +0 -0
  141. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/schemas.py +0 -0
  142. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/solution_log.py +0 -0
  143. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/tables.py +0 -0
  144. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/user_role.py +0 -0
  145. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/schemas/view.py +0 -0
  146. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/__init__.py +0 -0
  147. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/authentication/__init__.py +0 -0
  148. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/authentication/decorators.py +0 -0
  149. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/authentication/ldap.py +0 -0
  150. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/compress.py +0 -0
  151. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/const.py +0 -0
  152. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/email.py +0 -0
  153. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/licenses.py +0 -0
  154. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/log_config.py +0 -0
  155. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/query_tools.py +0 -0
  156. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/utils.py +0 -0
  157. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/utils_tables.py +0 -0
  158. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/shared/validators.py +0 -0
  159. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/__init__.py +0 -0
  160. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/const.py +0 -0
  161. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/custom_liveServer.py +0 -0
  162. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/integration/__init__.py +0 -0
  163. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/integration/test_commands.py +0 -0
  164. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/integration/test_cornflowclient.py +0 -0
  165. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/ldap/__init__.py +0 -0
  166. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/ldap/test_ldap_authentication.py +0 -0
  167. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/__init__.py +0 -0
  168. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_example_data.py +0 -0
  169. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_executions.py +0 -0
  170. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_generate_from_schema.py +0 -0
  171. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_health.py +0 -0
  172. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_instances.py +0 -0
  173. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_instances_file.py +0 -0
  174. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_licenses.py +0 -0
  175. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_main_alarms.py +0 -0
  176. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_permissions.py +0 -0
  177. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_roles.py +0 -0
  178. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_schema_from_models.py +0 -0
  179. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_schemas.py +0 -0
  180. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_sign_up.py +0 -0
  181. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_tables.py +0 -0
  182. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_token.py +0 -0
  183. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/test_users.py +0 -0
  184. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow/tests/unit/tools.py +0 -0
  185. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow.egg-info/dependency_links.txt +0 -0
  186. {cornflow-1.1.2 → cornflow-1.1.5a1}/cornflow.egg-info/top_level.txt +0 -0
  187. {cornflow-1.1.2 → cornflow-1.1.5a1}/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 requirements.txt
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.2
2
+ Name: cornflow
3
+ Version: 1.1.5a1
4
+ Summary: Cornflow is an open source multi-solver optimization server with a REST API built using flask.
5
+ Home-page: https://github.com/baobabsoluciones/cornflow
6
+ Author: baobab soluciones
7
+ Author-email: cornflow@baobabsoluciones.es
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: alembic==1.9.2
14
+ Requires-Dist: apispec<=6.3.0
15
+ Requires-Dist: click<=8.1.7
16
+ Requires-Dist: cornflow-client>=1.1.0
17
+ Requires-Dist: cryptography<=42.0.5
18
+ Requires-Dist: disposable-email-domains>=0.0.86
19
+ Requires-Dist: Flask==2.3.2
20
+ Requires-Dist: flask-apispec<=0.11.4
21
+ Requires-Dist: Flask-Bcrypt<=1.0.1
22
+ Requires-Dist: Flask-Compress<=1.14
23
+ Requires-Dist: flask-cors>=4.0.2
24
+ Requires-Dist: flask-inflate<=0.3
25
+ Requires-Dist: Flask-Migrate<=4.0.5
26
+ Requires-Dist: Flask-RESTful<=0.3.10
27
+ Requires-Dist: Flask-SQLAlchemy==2.5.1
28
+ Requires-Dist: gevent==23.9.1
29
+ Requires-Dist: greenlet<=2.0.2; python_version < "3.11"
30
+ Requires-Dist: greenlet==3.0.3; python_version >= "3.11"
31
+ Requires-Dist: gunicorn<=22.0.0
32
+ Requires-Dist: jsonpatch<=1.33
33
+ Requires-Dist: ldap3<=2.9.1
34
+ Requires-Dist: marshmallow<=3.20.2
35
+ Requires-Dist: PuLP<=2.9.0
36
+ Requires-Dist: psycopg2<=2.9.9
37
+ Requires-Dist: PyJWT<=2.8.0
38
+ Requires-Dist: pytups>=0.86.2
39
+ Requires-Dist: requests<=2.32.3
40
+ Requires-Dist: SQLAlchemy==1.3.21
41
+ Requires-Dist: webargs<=8.3.0
42
+ Requires-Dist: Werkzeug==3.0.6
43
+ Dynamic: author
44
+ Dynamic: author-email
45
+ Dynamic: classifier
46
+ Dynamic: description
47
+ Dynamic: home-page
48
+ Dynamic: requires-dist
49
+ Dynamic: requires-python
50
+ Dynamic: summary
51
+
52
+ Cornflow
53
+ =========
54
+
55
+ .. image:: https://github.com/baobabsoluciones/cornflow/workflows/build/badge.svg?style=svg
56
+ :target: https://github.com/baobabsoluciones/cornflow/actions
57
+
58
+ .. image:: https://github.com/baobabsoluciones/cornflow/workflows/docs/badge.svg?style=svg
59
+ :target: https://github.com/baobabsoluciones/cornflow/actions
60
+
61
+ .. image:: https://github.com/baobabsoluciones/cornflow/workflows/integration/badge.svg?style=svg
62
+ :target: https://github.com/baobabsoluciones/cornflow/actions
63
+
64
+ .. image:: https://img.shields.io/pypi/v/cornflow-client.svg?style=svg
65
+ :target: https://pypi.python.org/pypi/cornflow-client
66
+
67
+ .. image:: https://img.shields.io/pypi/pyversions/cornflow-client.svg?style=svg
68
+ :target: https://pypi.python.org/pypi/cornflow-client
69
+
70
+ .. image:: https://img.shields.io/badge/License-Apache2.0-blue
71
+
72
+ Cornflow is an open source multi-solver optimization server with a REST API built using `flask <https://flask.palletsprojects.com>`_, `airflow <https://airflow.apache.org/>`_ and `pulp <https://coin-or.github.io/pulp/>`_.
73
+
74
+ While most deployment servers are based on the solving technique (MIP, CP, NLP, etc.), Cornflow focuses on the optimization problems themselves. However, it does not impose any constraint on the type of problem and solution method to use.
75
+
76
+ With Cornflow you can deploy a Traveling Salesman Problem solver next to a Knapsack solver or a Nurse Rostering Problem solver. As long as you describe the input and output data, you can upload any solution method for any problem and then use it with any data you want.
77
+
78
+ Cornflow helps you formalize your problem by proposing development guidelines. It also provides a range of functionalities around your deployed solution method, namely:
79
+
80
+ * storage of users, instances, solutions and solution logs.
81
+ * deployment and maintenance of models, solvers and algorithms.
82
+ * scheduling of executions in remote machines.
83
+ * management of said executions: start, monitor, interrupt.
84
+ * centralizing of commercial licenses.
85
+ * scenario storage and comparison.
86
+ * user management, roles and groups.
87
+
88
+
89
+ .. contents:: **Table of Contents**
90
+
91
+ Installation instructions
92
+ -------------------------------
93
+
94
+ Cornflow is tested with Ubuntu 20.04, python >= 3.8 and git.
95
+
96
+ Download the Cornflow project and install requirements::
97
+
98
+ python3 -m venv venv
99
+ venv/bin/pip3 install cornflow
100
+
101
+ initialize the sqlite database::
102
+
103
+ source venv/bin/activate
104
+ export FLASK_APP=cornflow.app
105
+ export DATABASE_URL=sqlite:///cornflow.db
106
+ flask db upgrade
107
+ flask access_init
108
+ flask create_service_user -u airflow -e airflow_test@admin.com -p airflow_test_password
109
+ flask create_admin_user -u cornflow -e cornflow_admin@admin.com -p cornflow_admin_password
110
+
111
+
112
+ activate the virtual environment and run Cornflow::
113
+
114
+ source venv/bin/activate
115
+ export FLASK_APP=cornflow.app
116
+ export SECRET_KEY=THISNEEDSTOBECHANGED
117
+ export DATABASE_URL=sqlite:///cornflow.db
118
+ export AIRFLOW_URL=http://127.0.0.1:8080/
119
+ export AIRFLOW_USER=airflow_user
120
+ export AIRFLOW_PWD=airflow_pwd
121
+ flask run
122
+
123
+ **Cornflow needs a running installation of Airflow to operate and more configuration**. Check `the installation docs <https://baobabsoluciones.github.io/cornflow/main/install.html>`_ for more details on installing airflow, configuring the application and initializing the database.
124
+
125
+ Using cornflow to solve a PuLP model
126
+ ---------------------------------------
127
+
128
+ We're going to test the cornflow server by using the `cornflow-client` and the `pulp` python package::
129
+
130
+ pip install cornflow-client pulp
131
+
132
+ Initialize the api client::
133
+
134
+ from cornflow_client import CornFlow
135
+ email = 'some_email@gmail.com'
136
+ pwd = 'Some_password1'
137
+ username = 'some_name'
138
+ client = CornFlow(url="http://127.0.0.1:5000")
139
+
140
+ Create a user::
141
+
142
+ config = dict(username=username, email=email, pwd=pwd)
143
+ client.sign_up(**config)
144
+
145
+ Log in::
146
+
147
+ client.login(username=username, pwd=pwd)
148
+
149
+ Prepare an instance::
150
+
151
+ import pulp
152
+ prob = pulp.LpProblem("test_export_dict_MIP", pulp.LpMinimize)
153
+ x = pulp.LpVariable("x", 0, 4)
154
+ y = pulp.LpVariable("y", -1, 1)
155
+ z = pulp.LpVariable("z", 0, None, pulp.LpInteger)
156
+ prob += x + 4 * y + 9 * z, "obj"
157
+ prob += x + y <= 5, "c1"
158
+ prob += x + z >= 10, "c2"
159
+ prob += -y + z == 7.5, "c3"
160
+ data = prob.to_dict()
161
+ insName = 'test_export_dict_MIP'
162
+ description = 'very small example'
163
+
164
+ Send instance::
165
+
166
+ instance = client.create_instance(data, name=insName, description=description, schema="solve_model_dag",)
167
+
168
+ Solve an instance::
169
+
170
+ config = dict(
171
+ solver = "PULP_CBC_CMD",
172
+ timeLimit = 10
173
+ )
174
+ execution = client.create_execution(
175
+ instance['id'], config, name='execution1', description='execution of a very small instance',
176
+ schema="solve_model_dag",
177
+ )
178
+
179
+ Check the status of an execution::
180
+
181
+ status = client.get_status(execution["id"])
182
+ print(status['state'])
183
+ # 1 means "finished correctly"
184
+
185
+ Retrieve a solution::
186
+
187
+ results = client.get_solution(execution['id'])
188
+ print(results['data'])
189
+ # returns a json with the solved pulp object
190
+ _vars, prob = pulp.LpProblem.from_dict(results['data'])
191
+
192
+ Retrieve the log of the solver::
193
+
194
+ log = client.get_log(execution['id'])
195
+ print(log['log'])
196
+ # json format of the solver log
197
+
198
+ Using cornflow to deploy a solution method
199
+ ---------------------------------------------
200
+
201
+ To deploy a cornflow solution method, the following tasks need to be accomplished:
202
+
203
+ #. Create an Application for the new problem
204
+ #. Do a PR to a compatible repo linked to a server instance (e.g., like `this one <https://github.com/baobabsoluciones/cornflow>`_).
205
+
206
+ For more details on each part, check the `deployment guide <https://baobabsoluciones.github.io/cornflow/guides/deploy_solver.html>`_.
207
+
208
+ Using cornflow to solve a problem
209
+ -------------------------------------------
210
+
211
+ For this example we only need the cornflow_client package. We will test the graph-coloring demo defined `here <https://github.com/baobabsoluciones/cornflow-dags-public/tree/main/DAG/graph_coloring>`_. We will use the test server to solve it.
212
+
213
+ Initialize the api client::
214
+
215
+ from cornflow_client import CornFlow
216
+ email = 'readme@gmail.com'
217
+ pwd = 'some_password'
218
+ username = 'some_name'
219
+ client = CornFlow(url="https://devsm.cornflow.baobabsoluciones.app/")
220
+ client.login(username=username, pwd=pwd)
221
+
222
+ solve a graph coloring problem and get the solution::
223
+
224
+ data = dict(pairs=[dict(n1=0, n2=1), dict(n1=1, n2=2), dict(n1=1, n2=3)])
225
+ instance = client.create_instance(data, name='gc_4_1', description='very small gc problem', schema="graph_coloring")
226
+ config = dict()
227
+ execution = client.create_execution(
228
+ instance['id'], config, name='gc_4_1_exec', description='execution of very small gc problem',
229
+ schema="graph_coloring",
230
+ )
231
+ status = client.get_status(execution["id"])
232
+ print(status['state'])
233
+ solution = client.get_solution(execution["id"])
234
+ print(solution['data']['assignment'])
235
+
236
+
237
+ Running tests and coverage
238
+ ------------------------------
239
+
240
+ Then you have to run the following commands::
241
+
242
+ export FLASK_ENV=testing
243
+
244
+ Finally you can run all the tests with the following command::
245
+
246
+ python -m unittest discover -s cornflow.tests
247
+
248
+ If you want to only run the unit tests (without a local airflow webserver)::
249
+
250
+ python -m unittest discover -s cornflow.tests.unit
251
+
252
+ If you want to only run the integration test with a local airflow webserver::
253
+
254
+ python -m unittest discover -s cornflow.tests.integration
255
+
256
+ After if you want to check the coverage report you need to run::
257
+
258
+ coverage run --source=./cornflow/ -m unittest discover -s=./cornflow/tests/
259
+ coverage report -m
260
+
261
+ or to get the html reports::
262
+
263
+ coverage html
264
+
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Main file with the creation of the app logic
3
3
  """
4
+
4
5
  # Full imports
5
6
  import os
6
7
  import click
@@ -13,6 +14,8 @@ from flask_cors import CORS
13
14
  from flask_migrate import Migrate
14
15
  from flask_restful import Api
15
16
  from logging.config import dictConfig
17
+ from werkzeug.middleware.dispatcher import DispatcherMiddleware
18
+ from werkzeug.exceptions import NotFound
16
19
 
17
20
  # Module imports
18
21
  from cornflow.commands import (
@@ -112,6 +115,11 @@ def create_app(env_name="development", dataconn=None):
112
115
  app.cli.add_command(register_deployed_dags)
113
116
  app.cli.add_command(register_dag_permissions)
114
117
 
118
+ if app.config["APPLICATION_ROOT"] != "/" and app.config["EXTERNAL_APP"] == 0:
119
+ app.wsgi_app = DispatcherMiddleware(
120
+ NotFound(), {app.config["APPLICATION_ROOT"]: app.wsgi_app}
121
+ )
122
+
115
123
  return app
116
124
 
117
125
 
@@ -5,6 +5,12 @@ from apispec.ext.marshmallow import MarshmallowPlugin
5
5
 
6
6
 
7
7
  class DefaultConfig(object):
8
+ """
9
+ Default configuration class
10
+ """
11
+
12
+ APPLICATION_ROOT = os.getenv("APPLICATION_ROOT", "/")
13
+ EXTERNAL_APP = int(os.getenv("EXTERNAL_APP", 0))
8
14
  SERVICE_NAME = os.getenv("SERVICE_NAME", "Cornflow")
9
15
  SECRET_TOKEN_KEY = os.getenv("SECRET_KEY")
10
16
  SECRET_BI_KEY = os.getenv("SECRET_BI_KEY")
@@ -22,6 +28,11 @@ class DefaultConfig(object):
22
28
  SIGNUP_ACTIVATED = int(os.getenv("SIGNUP_ACTIVATED", 1))
23
29
  CORNFLOW_SERVICE_USER = os.getenv("CORNFLOW_SERVICE_USER", "service_user")
24
30
 
31
+ # If service user is allow to log with username and password
32
+ SERVICE_USER_ALLOW_PASSWORD_LOGIN = int(
33
+ os.getenv("SERVICE_USER_ALLOW_PASSWORD_LOGIN", 1)
34
+ )
35
+
25
36
  # Open deployment (all dags accessible to all users)
26
37
  OPEN_DEPLOYMENT = os.getenv("OPEN_DEPLOYMENT", 1)
27
38
 
@@ -84,14 +95,17 @@ class DefaultConfig(object):
84
95
 
85
96
 
86
97
  class Development(DefaultConfig):
87
-
88
- """ """
98
+ """
99
+ Configuration class for development
100
+ """
89
101
 
90
102
  ENV = "development"
91
103
 
92
104
 
93
105
  class Testing(DefaultConfig):
94
- """ """
106
+ """
107
+ Configuration class for testing
108
+ """
95
109
 
96
110
  ENV = "testing"
97
111
  SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -109,8 +123,26 @@ class Testing(DefaultConfig):
109
123
  LOG_LEVEL = int(os.getenv("LOG_LEVEL", 10))
110
124
 
111
125
 
126
+ class TestingOpenAuth(Testing):
127
+ """
128
+ Configuration class for testing some edge cases with Open Auth login
129
+ """
130
+
131
+ AUTH_TYPE = 0
132
+
133
+
134
+ class TestingApplicationRoot(Testing):
135
+ """
136
+ Configuration class for testing with application root
137
+ """
138
+
139
+ APPLICATION_ROOT = "/test"
140
+
141
+
112
142
  class Production(DefaultConfig):
113
- """ """
143
+ """
144
+ Configuration class for production
145
+ """
114
146
 
115
147
  ENV = "production"
116
148
  SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -121,4 +153,10 @@ class Production(DefaultConfig):
121
153
  PROPAGATE_EXCEPTIONS = True
122
154
 
123
155
 
124
- app_config = {"development": Development, "testing": Testing, "production": Production}
156
+ app_config = {
157
+ "development": Development,
158
+ "testing": Testing,
159
+ "production": Production,
160
+ "testing-oauth": TestingOpenAuth,
161
+ "testing-root": TestingApplicationRoot,
162
+ }
@@ -34,9 +34,11 @@ 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
41
+ self.user_role_association = UserRoleModel
40
42
 
41
43
  def log_in(self, **kwargs):
42
44
  """
@@ -102,7 +104,9 @@ class LoginBaseEndpoint(BaseMetaResource):
102
104
  raise InvalidCredentials()
103
105
  user = self.data_model.get_one_object(username=username)
104
106
  if not user:
105
- current_app.logger.info(f"LDAP user {username} does not exist and is created")
107
+ current_app.logger.info(
108
+ f"LDAP user {username} does not exist and is created"
109
+ )
106
110
  email = ldap_obj.get_user_email(username)
107
111
  if not email:
108
112
  email = ""
@@ -122,68 +126,115 @@ class LoginBaseEndpoint(BaseMetaResource):
122
126
 
123
127
  except IntegrityError as e:
124
128
  db.session.rollback()
125
- current_app.logger.error(f"Integrity error on user role assignment on log in: {e}")
129
+ current_app.logger.error(
130
+ f"Integrity error on user role assignment on log in: {e}"
131
+ )
126
132
  except DBAPIError as e:
127
133
  db.session.rollback()
128
- current_app.logger.error(f"Unknown error on user role assignment on log in: {e}")
134
+ current_app.logger.error(
135
+ f"Unknown error on user role assignment on log in: {e}"
136
+ )
129
137
 
130
138
  return user
131
139
 
132
- def auth_oid_authenticate(self, token):
140
+ def auth_oid_authenticate(
141
+ self, token: str = None, username: str = None, password: str = None
142
+ ):
133
143
  """
134
- Method in charge of performing the log in with the token issued by an Open ID provider
144
+ Method in charge of performing the log in with the token issued by an Open ID provider.
145
+ It has an exception and thus accepts username and password for service users if needed.
135
146
 
136
147
  :param str token: the token that the user has obtained from the Open ID provider
148
+ :param str username: the username of the user to log in
149
+ :param str password: the password of the user to log in
137
150
  :return: the user object or it raises an error if it has not been possible to log in
138
151
  :rtype: :class:`UserModel`
139
152
  """
140
- oid_provider = int(current_app.config["OID_PROVIDER"])
141
153
 
142
- client_id = current_app.config["OID_CLIENT_ID"]
143
- tenant_id = current_app.config["OID_TENANT_ID"]
144
- issuer = current_app.config["OID_ISSUER"]
154
+ if token:
145
155
 
146
- if client_id is None or tenant_id is None or issuer is None:
147
- raise ConfigurationError("The OID provider configuration is not valid")
156
+ oid_provider = int(current_app.config["OID_PROVIDER"])
148
157
 
149
- if oid_provider == OID_AZURE:
150
- decoded_token = self.auth_class().validate_oid_token(
151
- token, client_id, tenant_id, issuer, oid_provider
152
- )
158
+ client_id = current_app.config["OID_CLIENT_ID"]
159
+ tenant_id = current_app.config["OID_TENANT_ID"]
160
+ issuer = current_app.config["OID_ISSUER"]
153
161
 
154
- elif oid_provider == OID_GOOGLE:
155
- raise EndpointNotImplemented("The selected OID provider is not implemented")
156
- elif oid_provider == OID_NONE:
157
- raise EndpointNotImplemented("The OID provider configuration is not valid")
158
- else:
159
- raise EndpointNotImplemented("The OID provider configuration is not valid")
162
+ if client_id is None or tenant_id is None or issuer is None:
163
+ raise ConfigurationError("The OID provider configuration is not valid")
160
164
 
161
- username = decoded_token["preferred_username"]
165
+ if oid_provider == OID_AZURE:
166
+ decoded_token = self.auth_class().validate_oid_token(
167
+ token, client_id, tenant_id, issuer, oid_provider
168
+ )
162
169
 
163
- user = self.data_model.get_one_object(username=username)
170
+ elif oid_provider == OID_GOOGLE:
171
+ raise EndpointNotImplemented(
172
+ "The selected OID provider is not implemented"
173
+ )
174
+ elif oid_provider == OID_NONE:
175
+ raise EndpointNotImplemented(
176
+ "The OID provider configuration is not valid"
177
+ )
178
+ else:
179
+ raise EndpointNotImplemented(
180
+ "The OID provider configuration is not valid"
181
+ )
164
182
 
165
- if not user:
166
- current_app.logger.info(f"OpenID user {username} does not exist and is created")
183
+ username = decoded_token["preferred_username"]
184
+ email = decoded_token.get("email", f"{username}@test.org")
185
+ first_name = decoded_token.get("given_name", "")
186
+ last_name = decoded_token.get("family_name", "")
167
187
 
168
- data = {"username": username, "email": username}
188
+ user = self.data_model.get_one_object(username=username)
169
189
 
170
- user = self.data_model(data=data)
171
- user.save()
190
+ if not user:
191
+ current_app.logger.info(
192
+ f"OpenID user {username} does not exist and is created"
193
+ )
172
194
 
173
- self.user_role_association(user.id)
195
+ data = {
196
+ "username": username,
197
+ "email": email,
198
+ "first_name": first_name,
199
+ "last_name": last_name,
200
+ }
174
201
 
175
- user_role = self.user_role_association(
176
- {"user_id": user.id, "role_id": int(current_app.config["DEFAULT_ROLE"])}
177
- )
202
+ user = self.data_model(data=data)
203
+ user.save()
178
204
 
179
- user_role.save()
205
+ user_role = self.user_role_association(
206
+ {
207
+ "user_id": user.id,
208
+ "role_id": int(current_app.config["DEFAULT_ROLE"]),
209
+ }
210
+ )
180
211
 
181
- return user
212
+ user_role.save()
213
+
214
+ return user
215
+ elif (
216
+ username
217
+ and password
218
+ and current_app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] == 1
219
+ ):
220
+
221
+ user = self.auth_db_authenticate(username, password)
222
+
223
+ if user.is_service_user():
224
+ return user
225
+ else:
226
+ raise InvalidUsage("Invalid request")
227
+ else:
228
+ raise InvalidUsage("Invalid request")
182
229
 
183
230
 
184
231
  def check_last_password_change(user):
185
232
  if user.pwd_last_change:
186
- if user.pwd_last_change + timedelta(days=int(current_app.config["PWD_ROTATION_TIME"])) < datetime.utcnow():
233
+ if (
234
+ user.pwd_last_change
235
+ + timedelta(days=int(current_app.config["PWD_ROTATION_TIME"]))
236
+ < datetime.utcnow()
237
+ ):
187
238
  return True
188
239
  return False
189
240
 
@@ -1,12 +1,14 @@
1
1
  """
2
2
  This file contains the schemas used for the users defined in the application
3
3
  """
4
- from marshmallow import fields, Schema
4
+
5
+ from marshmallow import fields, Schema, validates_schema, ValidationError
5
6
  from .instance import InstanceSchema
6
7
 
7
8
 
8
9
  class UserSchema(Schema):
9
10
  """ """
11
+
10
12
  id = fields.Int(dump_only=True)
11
13
  first_name = fields.Str()
12
14
  last_name = fields.Str()
@@ -66,9 +68,23 @@ class LoginEndpointRequest(Schema):
66
68
  class LoginOpenAuthRequest(Schema):
67
69
  """
68
70
  This is the schema used by the login endpoint with Open ID protocol
71
+ Validates that either a token is provided, or both username and password are present
69
72
  """
70
73
 
71
- token = fields.Str(required=True)
74
+ token = fields.Str(required=False)
75
+ username = fields.Str(required=False)
76
+ password = fields.Str(required=False)
77
+
78
+ @validates_schema
79
+ def validate_fields(self, data, **kwargs):
80
+ if data.get("token") is None:
81
+ if not data.get("username") or not data.get("password"):
82
+ raise ValidationError(
83
+ "A token needs to be provided when using Open ID authentication"
84
+ )
85
+ else:
86
+ if data.get("username") or data.get("password"):
87
+ raise ValidationError("The login needs to be done with a token only")
72
88
 
73
89
 
74
90
  class SignupRequest(Schema):
@@ -14,6 +14,8 @@ from datetime import datetime, timedelta
14
14
  from flask import request, g, current_app, Request
15
15
  from functools import wraps
16
16
  from typing import Union, Tuple
17
+
18
+ from jwt import DecodeError
17
19
  from werkzeug.datastructures import Headers
18
20
 
19
21
  # Imports from internal modules
@@ -103,7 +105,8 @@ class Auth:
103
105
  )
104
106
 
105
107
  payload = {
106
- "exp": datetime.utcnow() + timedelta(hours=float(current_app.config["TOKEN_DURATION"])),
108
+ "exp": datetime.utcnow()
109
+ + timedelta(hours=float(current_app.config["TOKEN_DURATION"])),
107
110
  "iat": datetime.utcnow(),
108
111
  "sub": user_id,
109
112
  }
@@ -314,7 +317,10 @@ class Auth:
314
317
  :return: the key identifier
315
318
  :rtype: str
316
319
  """
317
- headers = jwt.get_unverified_header(token)
320
+ try:
321
+ headers = jwt.get_unverified_header(token)
322
+ except DecodeError as err:
323
+ raise InvalidCredentials("Token is not valid")
318
324
  if not headers:
319
325
  raise InvalidCredentials("Token is missing the headers")
320
326
  try:
@@ -346,9 +352,9 @@ class Auth:
346
352
  try:
347
353
  response = requests.get(discovery_url)
348
354
  response.raise_for_status()
349
- except requests.exceptions.HTTPError as error:
355
+ except requests.exceptions.HTTPError:
350
356
  raise CommunicationError(
351
- f"Error getting issuer discovery meta from {discovery_url}", error
357
+ f"Error getting issuer discovery meta from {discovery_url}"
352
358
  )
353
359
  return response.json()
354
360