fractal-server 2.7.0a5__tar.gz → 2.7.0a6__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 (230) hide show
  1. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/PKG-INFO +1 -1
  2. fractal_server-2.7.0a6/fractal_server/__init__.py +1 -0
  3. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/__init__.py +4 -0
  4. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/_aux_functions_tasks.py +72 -11
  5. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/workflow.py +14 -82
  6. fractal_server-2.7.0a6/fractal_server/app/routes/api/v2/workflow_import.py +355 -0
  7. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/group.py +27 -0
  8. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/__init__.py +13 -7
  9. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/__init__.py +1 -0
  10. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/manifest.py +13 -0
  11. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/task.py +20 -5
  12. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/workflowtask.py +3 -1
  13. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/data_migrations/2_7_0.py +62 -3
  14. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/pyproject.toml +2 -2
  15. fractal_server-2.7.0a5/fractal_server/__init__.py +0 -1
  16. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/LICENSE +0 -0
  17. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/README.md +0 -0
  18. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/__main__.py +0 -0
  19. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/alembic.ini +0 -0
  20. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/__init__.py +0 -0
  21. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/db/__init__.py +0 -0
  22. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/__init__.py +0 -0
  23. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/linkusergroup.py +0 -0
  24. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/linkuserproject.py +0 -0
  25. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/security.py +0 -0
  26. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/user_settings.py +0 -0
  27. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v1/__init__.py +0 -0
  28. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v1/dataset.py +0 -0
  29. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v1/job.py +0 -0
  30. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v1/project.py +0 -0
  31. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v1/state.py +0 -0
  32. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v1/task.py +0 -0
  33. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v1/workflow.py +0 -0
  34. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v2/__init__.py +0 -0
  35. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v2/collection_state.py +0 -0
  36. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v2/dataset.py +0 -0
  37. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v2/job.py +0 -0
  38. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v2/project.py +0 -0
  39. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v2/task.py +0 -0
  40. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v2/workflow.py +0 -0
  41. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/models/v2/workflowtask.py +0 -0
  42. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/__init__.py +0 -0
  43. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/admin/__init__.py +0 -0
  44. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/admin/v1.py +0 -0
  45. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/admin/v2/__init__.py +0 -0
  46. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/admin/v2/job.py +0 -0
  47. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/admin/v2/project.py +0 -0
  48. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/admin/v2/task.py +0 -0
  49. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/admin/v2/task_group.py +0 -0
  50. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/__init__.py +0 -0
  51. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/__init__.py +0 -0
  52. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/_aux_functions.py +0 -0
  53. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/dataset.py +0 -0
  54. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/job.py +0 -0
  55. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/project.py +0 -0
  56. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/task.py +0 -0
  57. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/task_collection.py +0 -0
  58. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/workflow.py +0 -0
  59. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v1/workflowtask.py +0 -0
  60. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/_aux_functions.py +0 -0
  61. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/dataset.py +0 -0
  62. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/images.py +0 -0
  63. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/job.py +0 -0
  64. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/project.py +0 -0
  65. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/status.py +0 -0
  66. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/submit.py +0 -0
  67. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/task.py +0 -0
  68. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/task_collection.py +0 -0
  69. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/task_collection_custom.py +0 -0
  70. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/task_group.py +0 -0
  71. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/api/v2/workflowtask.py +0 -0
  72. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/__init__.py +0 -0
  73. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/_aux_auth.py +0 -0
  74. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/current_user.py +0 -0
  75. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/login.py +0 -0
  76. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/oauth.py +0 -0
  77. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/register.py +0 -0
  78. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/router.py +0 -0
  79. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/auth/users.py +0 -0
  80. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/aux/__init__.py +0 -0
  81. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/aux/_job.py +0 -0
  82. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/aux/_runner.py +0 -0
  83. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/routes/aux/validate_user_settings.py +0 -0
  84. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/.gitignore +0 -0
  85. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/__init__.py +0 -0
  86. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/async_wrap.py +0 -0
  87. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/components.py +0 -0
  88. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/compress_folder.py +0 -0
  89. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/exceptions.py +0 -0
  90. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/__init__.py +0 -0
  91. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/__init__.py +0 -0
  92. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/_batching.py +0 -0
  93. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/_slurm_config.py +0 -0
  94. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/remote.py +0 -0
  95. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/ssh/__init__.py +0 -0
  96. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +0 -0
  97. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +0 -0
  98. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/ssh/executor.py +0 -0
  99. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/sudo/__init__.py +0 -0
  100. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +0 -0
  101. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +0 -0
  102. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/sudo/_subprocess_run_as_user.py +0 -0
  103. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/executors/slurm/sudo/executor.py +0 -0
  104. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/extract_archive.py +0 -0
  105. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/filenames.py +0 -0
  106. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/run_subprocess.py +0 -0
  107. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/set_start_and_last_task_index.py +0 -0
  108. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/shutdown.py +0 -0
  109. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/task_files.py +0 -0
  110. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/__init__.py +0 -0
  111. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/_common.py +0 -0
  112. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/_local/__init__.py +0 -0
  113. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/_local/_local_config.py +0 -0
  114. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/_local/_submit_setup.py +0 -0
  115. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/_local/executor.py +0 -0
  116. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/_slurm/__init__.py +0 -0
  117. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/_slurm/_submit_setup.py +0 -0
  118. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/_slurm/get_slurm_config.py +0 -0
  119. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/common.py +0 -0
  120. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v1/handle_failed_job.py +0 -0
  121. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_local/__init__.py +0 -0
  122. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_local/_local_config.py +0 -0
  123. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_local/_submit_setup.py +0 -0
  124. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_local/executor.py +0 -0
  125. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_local_experimental/__init__.py +0 -0
  126. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_local_experimental/_local_config.py +0 -0
  127. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_local_experimental/_submit_setup.py +0 -0
  128. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_local_experimental/executor.py +0 -0
  129. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_slurm_common/__init__.py +0 -0
  130. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_slurm_common/get_slurm_config.py +0 -0
  131. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_slurm_ssh/__init__.py +0 -0
  132. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_slurm_ssh/_submit_setup.py +0 -0
  133. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_slurm_sudo/__init__.py +0 -0
  134. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/_slurm_sudo/_submit_setup.py +0 -0
  135. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/deduplicate_list.py +0 -0
  136. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/handle_failed_job.py +0 -0
  137. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/merge_outputs.py +0 -0
  138. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/runner.py +0 -0
  139. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/runner_functions.py +0 -0
  140. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/runner_functions_low_level.py +0 -0
  141. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/v2/task_interface.py +0 -0
  142. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/runner/versions.py +0 -0
  143. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/__init__.py +0 -0
  144. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/_validators.py +0 -0
  145. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/user.py +0 -0
  146. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/user_group.py +0 -0
  147. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/user_settings.py +0 -0
  148. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/__init__.py +0 -0
  149. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/applyworkflow.py +0 -0
  150. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/dataset.py +0 -0
  151. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/dumps.py +0 -0
  152. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/manifest.py +0 -0
  153. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/project.py +0 -0
  154. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/state.py +0 -0
  155. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/task.py +0 -0
  156. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/task_collection.py +0 -0
  157. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v1/workflow.py +0 -0
  158. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/dataset.py +0 -0
  159. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/dumps.py +0 -0
  160. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/job.py +0 -0
  161. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/project.py +0 -0
  162. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/status.py +0 -0
  163. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/task_collection.py +0 -0
  164. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/task_group.py +0 -0
  165. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/schemas/v2/workflow.py +0 -0
  166. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/security/__init__.py +0 -0
  167. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/app/user_settings.py +0 -0
  168. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/config.py +0 -0
  169. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/data_migrations/README.md +0 -0
  170. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/data_migrations/tools.py +0 -0
  171. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/gunicorn_fractal.py +0 -0
  172. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/images/__init__.py +0 -0
  173. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/images/models.py +0 -0
  174. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/images/tools.py +0 -0
  175. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/logger.py +0 -0
  176. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/main.py +0 -0
  177. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/README +0 -0
  178. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/env.py +0 -0
  179. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/naming_convention.py +0 -0
  180. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/script.py.mako +0 -0
  181. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/034a469ec2eb_task_groups.py +0 -0
  182. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/091b01f51f88_add_usergroup_and_linkusergroup_table.py +0 -0
  183. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/4c308bcaea2b_add_task_args_schema_and_task_args_.py +0 -0
  184. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/4cedeb448a53_workflowtask_foreign_keys_not_nullables.py +0 -0
  185. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/501961cfcd85_remove_link_between_v1_and_v2_tasks_.py +0 -0
  186. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/50a13d6138fd_initial_schema.py +0 -0
  187. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/5bf02391cfef_v2.py +0 -0
  188. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/70e77f1c38b0_add_applyworkflow_first_task_index_and_.py +0 -0
  189. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/71eefd1dd202_add_slurm_accounts.py +0 -0
  190. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/84bf0fffde30_add_dumps_to_applyworkflow.py +0 -0
  191. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/8f79bd162e35_add_docs_info_and_docs_link_to_task_.py +0 -0
  192. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/94a47ea2d3ff_remove_cache_dir_slurm_user_and_slurm_.py +0 -0
  193. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/97f444d47249_add_applyworkflow_project_dump.py +0 -0
  194. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/99ea79d9e5d2_add_dataset_history.py +0 -0
  195. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/9c5ae74c9b98_add_user_settings_table.py +0 -0
  196. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/9fd26a2b0de4_add_workflow_timestamp_created.py +0 -0
  197. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/a7f4d6137b53_add_workflow_dump_to_applyworkflow.py +0 -0
  198. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/d4fe3708d309_make_applyworkflow_workflow_dump_non_.py +0 -0
  199. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/da2cb2ac4255_user_group_viewer_paths.py +0 -0
  200. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/e75cac726012_make_applyworkflow_start_timestamp_not_.py +0 -0
  201. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/efa89c30e0a4_add_project_timestamp_created.py +0 -0
  202. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/migrations/versions/f384e1c0cf5d_drop_task_default_args_columns.py +0 -0
  203. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/py.typed +0 -0
  204. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/ssh/__init__.py +0 -0
  205. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/ssh/_fabric.py +0 -0
  206. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/string_tools.py +0 -0
  207. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/syringe.py +0 -0
  208. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/__init__.py +0 -0
  209. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/utils.py +0 -0
  210. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v1/_TaskCollectPip.py +0 -0
  211. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v1/__init__.py +0 -0
  212. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v1/background_operations.py +0 -0
  213. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v1/endpoint_operations.py +0 -0
  214. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v1/get_collection_data.py +0 -0
  215. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v1/utils.py +0 -0
  216. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/__init__.py +0 -0
  217. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/_venv_pip.py +0 -0
  218. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/background_operations.py +0 -0
  219. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/background_operations_ssh.py +0 -0
  220. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/database_operations.py +0 -0
  221. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/endpoint_operations.py +0 -0
  222. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -0
  223. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/templates/_2_upgrade_pip.sh +0 -0
  224. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/templates/_3_pip_install.sh +0 -0
  225. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/templates/_4_pip_freeze.sh +0 -0
  226. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/templates/_5_pip_show.sh +0 -0
  227. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/tasks/v2/utils.py +0 -0
  228. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/urls.py +0 -0
  229. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/utils.py +0 -0
  230. {fractal_server-2.7.0a5 → fractal_server-2.7.0a6}/fractal_server/zip_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fractal-server
3
- Version: 2.7.0a5
3
+ Version: 2.7.0a6
4
4
  Summary: Server component of the Fractal analytics platform
5
5
  Home-page: https://github.com/fractal-analytics-platform/fractal-server
6
6
  License: BSD-3-Clause
@@ -0,0 +1 @@
1
+ __VERSION__ = "2.7.0a6"
@@ -14,6 +14,7 @@ from .task_collection import router as task_collection_router_v2
14
14
  from .task_collection_custom import router as task_collection_router_v2_custom
15
15
  from .task_group import router as task_group_router_v2
16
16
  from .workflow import router as workflow_router_v2
17
+ from .workflow_import import router as workflow_import_router_v2
17
18
  from .workflowtask import router as workflowtask_router_v2
18
19
  from fractal_server.config import get_settings
19
20
  from fractal_server.syringe import Inject
@@ -42,5 +43,8 @@ router_api_v2.include_router(
42
43
  task_group_router_v2, prefix="/task-group", tags=["V2 TaskGroup"]
43
44
  )
44
45
  router_api_v2.include_router(workflow_router_v2, tags=["V2 Workflow"])
46
+ router_api_v2.include_router(
47
+ workflow_import_router_v2, tags=["V2 Workflow Import"]
48
+ )
45
49
  router_api_v2.include_router(workflowtask_router_v2, tags=["V2 WorkflowTask"])
46
50
  router_api_v2.include_router(status_router_v2, tags=["V2 Status"])
@@ -9,13 +9,21 @@ from fastapi import HTTPException
9
9
  from fastapi import status
10
10
  from sqlmodel import select
11
11
 
12
- from ....db import AsyncSession
13
- from ....models import LinkUserGroup
14
- from ....models.v2 import TaskGroupV2
15
- from ....models.v2 import TaskV2
16
- from ....models.v2 import WorkflowTaskV2
17
- from ...auth._aux_auth import _get_default_usergroup_id
18
- from ...auth._aux_auth import _verify_user_belongs_to_group
12
+ from fractal_server.app.db import AsyncSession
13
+ from fractal_server.app.models import LinkUserGroup
14
+ from fractal_server.app.models import UserGroup
15
+ from fractal_server.app.models import UserOAuth
16
+ from fractal_server.app.models.v2 import CollectionStateV2
17
+ from fractal_server.app.models.v2 import TaskGroupV2
18
+ from fractal_server.app.models.v2 import TaskV2
19
+ from fractal_server.app.models.v2 import WorkflowTaskV2
20
+ from fractal_server.app.routes.auth._aux_auth import _get_default_usergroup_id
21
+ from fractal_server.app.routes.auth._aux_auth import (
22
+ _verify_user_belongs_to_group,
23
+ )
24
+ from fractal_server.logger import set_logger
25
+
26
+ logger = set_logger(__name__)
19
27
 
20
28
 
21
29
  async def _get_task_group_or_404(
@@ -211,6 +219,33 @@ async def _get_valid_user_group_id(
211
219
  return user_group_id
212
220
 
213
221
 
222
+ async def _get_collection_status_message(
223
+ task_group: TaskGroupV2, db: AsyncSession
224
+ ) -> str:
225
+ res = await db.execute(
226
+ select(CollectionStateV2).where(
227
+ CollectionStateV2.taskgroupv2_id == task_group.id
228
+ )
229
+ )
230
+ states = res.scalars().all()
231
+ if len(states) > 1:
232
+ msg = (
233
+ "Expected one CollectionStateV2 associated to TaskGroup "
234
+ f"{task_group.id}, found {len(states)} "
235
+ f"(IDs: {[state.id for state in states]}).\n"
236
+ "Warning: this should have not happened, please contact an admin."
237
+ )
238
+ elif len(states) == 1:
239
+ msg = (
240
+ f"\nThere exists a task collection state (ID={states[0].id}) for "
241
+ f"this task group (ID={task_group.id}), with status "
242
+ f"{states[0].data.get('status')}."
243
+ )
244
+ else:
245
+ msg = ""
246
+ return msg
247
+
248
+
214
249
  async def _verify_non_duplication_user_constraint(
215
250
  db: AsyncSession,
216
251
  user_id: int,
@@ -226,11 +261,24 @@ async def _verify_non_duplication_user_constraint(
226
261
  res = await db.execute(stm)
227
262
  duplicate = res.scalars().all()
228
263
  if duplicate:
264
+ user = await db.get(UserOAuth, user_id)
265
+ if len(duplicate) > 1:
266
+ raise HTTPException(
267
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
268
+ detail=(
269
+ "Invalid state:\n"
270
+ f"User '{user.email}' already owns {len(duplicate)} task "
271
+ f"groups with name='{pkg_name}' and {version=} "
272
+ f"(IDs: {[group.id for group in duplicate]}).\n"
273
+ "This should have not happened: please contact an admin."
274
+ ),
275
+ )
276
+ state_msg = await _get_collection_status_message(duplicate[0], db)
229
277
  raise HTTPException(
230
278
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
231
279
  detail=(
232
- "There is already a TaskGroupV2 with "
233
- f"({pkg_name=}, {version=}, {user_id=})."
280
+ f"User '{user.email}' already owns a task group "
281
+ f"with name='{pkg_name}' and {version=}.{state_msg}"
234
282
  ),
235
283
  )
236
284
 
@@ -253,11 +301,24 @@ async def _verify_non_duplication_group_constraint(
253
301
  res = await db.execute(stm)
254
302
  duplicate = res.scalars().all()
255
303
  if duplicate:
304
+ user_group = await db.get(UserGroup, user_group_id)
305
+ if len(duplicate) > 1:
306
+ raise HTTPException(
307
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
308
+ detail=(
309
+ "Invalid state:\n"
310
+ f"UserGroup '{user_group.name}' already owns "
311
+ f"{len(duplicate)} task groups with name='{pkg_name}' and "
312
+ f"{version=} (IDs: {[group.id for group in duplicate]}).\n"
313
+ "This should have not happened: please contact an admin."
314
+ ),
315
+ )
316
+ state_msg = await _get_collection_status_message(duplicate[0], db)
256
317
  raise HTTPException(
257
318
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
258
319
  detail=(
259
- "There is already a TaskGroupV2 with "
260
- f"({pkg_name=}, {version=}, {user_group_id=})."
320
+ f"UserGroup {user_group.name} already owns a task group "
321
+ f"with {pkg_name=} and {version=}.{state_msg}"
261
322
  ),
262
323
  )
263
324
 
@@ -11,25 +11,21 @@ from ....db import AsyncSession
11
11
  from ....db import get_async_db
12
12
  from ....models.v2 import JobV2
13
13
  from ....models.v2 import ProjectV2
14
- from ....models.v2 import TaskV2
15
14
  from ....models.v2 import WorkflowV2
16
15
  from ....schemas.v2 import WorkflowCreateV2
17
16
  from ....schemas.v2 import WorkflowExportV2
18
- from ....schemas.v2 import WorkflowImportV2
19
17
  from ....schemas.v2 import WorkflowReadV2
20
18
  from ....schemas.v2 import WorkflowReadV2WithWarnings
21
- from ....schemas.v2 import WorkflowTaskCreateV2
22
19
  from ....schemas.v2 import WorkflowUpdateV2
23
20
  from ._aux_functions import _check_workflow_exists
24
21
  from ._aux_functions import _get_project_check_owner
25
22
  from ._aux_functions import _get_submitted_jobs_statement
26
23
  from ._aux_functions import _get_workflow_check_owner
27
- from ._aux_functions import _workflow_insert_task
28
24
  from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
29
25
  from fractal_server.app.models import UserOAuth
26
+ from fractal_server.app.models.v2.task import TaskGroupV2
30
27
  from fractal_server.app.routes.auth import current_active_user
31
28
 
32
-
33
29
  router = APIRouter()
34
30
 
35
31
 
@@ -256,85 +252,21 @@ async def export_worfklow(
256
252
  user_id=user.id,
257
253
  db=db,
258
254
  )
259
- return workflow
260
-
261
-
262
- @router.post(
263
- "/project/{project_id}/workflow/import/",
264
- response_model=WorkflowReadV2,
265
- status_code=status.HTTP_201_CREATED,
266
- )
267
- async def import_workflow(
268
- project_id: int,
269
- workflow: WorkflowImportV2,
270
- user: UserOAuth = Depends(current_active_user),
271
- db: AsyncSession = Depends(get_async_db),
272
- ) -> Optional[WorkflowReadV2]:
273
- """
274
- Import an existing workflow into a project
275
-
276
- Also create all required objects (i.e. Workflow and WorkflowTask's) along
277
- the way.
278
- """
279
-
280
- # Preliminary checks
281
- await _get_project_check_owner(
282
- project_id=project_id,
283
- user_id=user.id,
284
- db=db,
285
- )
286
-
287
- await _check_workflow_exists(
288
- name=workflow.name, project_id=project_id, db=db
289
- )
290
-
291
- # Check that all required tasks are available
292
- source_to_id = {}
293
-
294
- for wf_task in workflow.task_list:
295
-
296
- source = wf_task.task.source
297
- if source not in source_to_id.keys():
298
- stm = select(TaskV2).where(TaskV2.source == source)
299
- tasks_by_source = (await db.execute(stm)).scalars().all()
300
- if len(tasks_by_source) != 1:
301
- raise HTTPException(
302
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
303
- detail=(
304
- f"Found {len(tasks_by_source)} tasks "
305
- f"with {source=}."
306
- ),
307
- )
308
- source_to_id[source] = tasks_by_source[0].id
309
-
310
- # Create new Workflow (with empty task_list)
311
- db_workflow = WorkflowV2(
312
- project_id=project_id,
313
- **workflow.dict(exclude_none=True, exclude={"task_list"}),
314
- )
315
- db.add(db_workflow)
316
- await db.commit()
317
- await db.refresh(db_workflow)
318
-
319
- # Insert tasks
320
-
321
- for wf_task in workflow.task_list:
322
- source = wf_task.task.source
323
- task_id = source_to_id[source]
324
-
325
- new_wf_task = WorkflowTaskCreateV2(
326
- **wf_task.dict(exclude_none=True, exclude={"task"})
327
- )
328
- # Insert task
329
- await _workflow_insert_task(
330
- **new_wf_task.dict(),
331
- workflow_id=db_workflow.id,
332
- task_id=task_id,
333
- db=db,
255
+ wf_task_list = []
256
+ for wftask in workflow.task_list:
257
+ task_group = await db.get(TaskGroupV2, wftask.task.taskgroupv2_id)
258
+ wf_task_list.append(wftask.dict())
259
+ wf_task_list[-1]["task"] = dict(
260
+ pkg_name=task_group.pkg_name,
261
+ version=task_group.version,
262
+ name=wftask.task.name,
334
263
  )
335
264
 
336
- await db.close()
337
- return db_workflow
265
+ wf = WorkflowExportV2(
266
+ **workflow.model_dump(),
267
+ task_list=wf_task_list,
268
+ )
269
+ return wf
338
270
 
339
271
 
340
272
  @router.get("/workflow/", response_model=list[WorkflowReadV2])
@@ -0,0 +1,355 @@
1
+ from typing import Optional
2
+
3
+ from fastapi import APIRouter
4
+ from fastapi import Depends
5
+ from fastapi import HTTPException
6
+ from fastapi import status
7
+ from sqlmodel import or_
8
+ from sqlmodel import select
9
+
10
+ from ....db import AsyncSession
11
+ from ....db import get_async_db
12
+ from ....models.v2 import TaskV2
13
+ from ....models.v2 import WorkflowV2
14
+ from ....schemas.v2 import TaskImportV2Legacy
15
+ from ....schemas.v2 import WorkflowImportV2
16
+ from ....schemas.v2 import WorkflowReadV2WithWarnings
17
+ from ....schemas.v2 import WorkflowTaskCreateV2
18
+ from ._aux_functions import _check_workflow_exists
19
+ from ._aux_functions import _get_project_check_owner
20
+ from ._aux_functions import _workflow_insert_task
21
+ from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
22
+ from fractal_server.app.models import LinkUserGroup
23
+ from fractal_server.app.models import UserOAuth
24
+ from fractal_server.app.models.v2.task import TaskGroupV2
25
+ from fractal_server.app.routes.auth import current_active_user
26
+ from fractal_server.app.routes.auth._aux_auth import _get_default_usergroup_id
27
+ from fractal_server.app.schemas.v2.task import TaskImportV2
28
+ from fractal_server.logger import set_logger
29
+
30
+ router = APIRouter()
31
+
32
+
33
+ logger = set_logger(__name__)
34
+
35
+
36
+ async def _get_user_accessible_taskgroups(
37
+ *,
38
+ user_id: int,
39
+ db: AsyncSession,
40
+ ) -> list[TaskGroupV2]:
41
+ """
42
+ Retrieve list of task groups that the user has access to.
43
+ """
44
+ stm = select(TaskGroupV2).where(
45
+ or_(
46
+ TaskGroupV2.user_id == user_id,
47
+ TaskGroupV2.user_group_id.in_(
48
+ select(LinkUserGroup.group_id).where(
49
+ LinkUserGroup.user_id == user_id
50
+ )
51
+ ),
52
+ )
53
+ )
54
+ res = await db.execute(stm)
55
+ accessible_task_groups = res.scalars().all()
56
+ logger.info(
57
+ f"Found {len(accessible_task_groups)} accessible "
58
+ f"task groups for {user_id=}."
59
+ )
60
+ return accessible_task_groups
61
+
62
+
63
+ async def _get_task_by_source(
64
+ source: str,
65
+ task_groups_list: list[TaskGroupV2],
66
+ ) -> Optional[int]:
67
+ """
68
+ Find task with a given source.
69
+
70
+ Args:
71
+ task_import: Info on task to be imported.
72
+ user_id: ID of current user.
73
+ default_group_id: ID of default user group.
74
+ task_group_list: Current list of valid task groups.
75
+ db: Asynchronous db session
76
+
77
+ Return:
78
+ `id` of the matching task, or `None`.
79
+ """
80
+ task_id = next(
81
+ iter(
82
+ task.id
83
+ for task_group in task_groups_list
84
+ for task in task_group.task_list
85
+ if task.source == source
86
+ ),
87
+ None,
88
+ )
89
+ return task_id
90
+
91
+
92
+ async def _disambiguate_task_groups(
93
+ *,
94
+ matching_task_groups: list[TaskGroupV2],
95
+ user_id: int,
96
+ db: AsyncSession,
97
+ default_group_id: int,
98
+ ) -> Optional[TaskV2]:
99
+ """
100
+ Disambiguate task groups based on ownership information.
101
+ """
102
+ # Highest priority: task groups created by user
103
+ for task_group in matching_task_groups:
104
+ if task_group.user_id == user_id:
105
+ logger.info(
106
+ "[_disambiguate_task_groups] "
107
+ f"Found task group {task_group.id} with {user_id=}, return."
108
+ )
109
+ return task_group
110
+ logger.info(
111
+ "[_disambiguate_task_groups] "
112
+ f"No task group found with {user_id=}, continue."
113
+ )
114
+
115
+ # Medium priority: task groups owned by default user group
116
+ for task_group in matching_task_groups:
117
+ if task_group.user_group_id == default_group_id:
118
+ logger.info(
119
+ "[_disambiguate_task_groups] "
120
+ f"Found task group {task_group.id} with user_group_id="
121
+ f"{default_group_id}, return."
122
+ )
123
+ return task_group
124
+ logger.info(
125
+ "[_disambiguate_task_groups] "
126
+ "No task group found with user_group_id="
127
+ f"{default_group_id}, continue."
128
+ )
129
+
130
+ # Lowest priority: task groups owned by other groups, sorted
131
+ # according to age of the user/usergroup link
132
+ logger.info(
133
+ "[_disambiguate_task_groups] "
134
+ "Now sorting remaining task groups by oldest-user-link."
135
+ )
136
+ user_group_ids = [
137
+ task_group.user_group_id for task_group in matching_task_groups
138
+ ]
139
+ stm = (
140
+ select(LinkUserGroup.group_id)
141
+ .where(LinkUserGroup.user_id == user_id)
142
+ .where(LinkUserGroup.group_id.in_(user_group_ids))
143
+ .order_by(LinkUserGroup.timestamp_created.asc())
144
+ )
145
+ res = await db.execute(stm)
146
+ oldest_user_group_id = res.scalars().first()
147
+ logger.info(
148
+ "[_disambiguate_task_groups] "
149
+ f"Result of sorting: {oldest_user_group_id=}."
150
+ )
151
+ task_group = next(
152
+ iter(
153
+ task_group
154
+ for task_group in matching_task_groups
155
+ if task_group.user_group_id == oldest_user_group_id
156
+ ),
157
+ None,
158
+ )
159
+ return task_group
160
+
161
+
162
+ async def _get_task_by_taskimport(
163
+ *,
164
+ task_import: TaskImportV2,
165
+ task_groups_list: list[TaskGroupV2],
166
+ user_id: int,
167
+ default_group_id: int,
168
+ db: AsyncSession,
169
+ ) -> Optional[int]:
170
+ """
171
+ Find a task based on `task_import`.
172
+
173
+ Args:
174
+ task_import: Info on task to be imported.
175
+ user_id: ID of current user.
176
+ default_group_id: ID of default user group.
177
+ task_group_list: Current list of valid task groups.
178
+ db: Asynchronous db session
179
+
180
+ Return:
181
+ `id` of the matching task, or `None`.
182
+ """
183
+
184
+ logger.info(f"[_get_task_by_taskimport] START, {task_import=}")
185
+
186
+ # Filter by `pkg_name` and by presence of a task with given `name`.
187
+ matching_task_groups = [
188
+ task_group
189
+ for task_group in task_groups_list
190
+ if (
191
+ task_group.pkg_name == task_import.pkg_name
192
+ and task_import.name
193
+ in [task.name for task in task_group.task_list]
194
+ )
195
+ ]
196
+ if len(matching_task_groups) < 1:
197
+ logger.info(
198
+ "[_get_task_by_taskimport] "
199
+ f"No task group with {task_import.pkg_name=} "
200
+ f"and a task with {task_import.name=}."
201
+ )
202
+ return None
203
+
204
+ # Determine target `version`
205
+ # Note that task_import.version cannot be "", due to a validator
206
+ if task_import.version is None:
207
+ logger.info(
208
+ "[_get_task_by_taskimport] "
209
+ "No version requested, looking for latest."
210
+ )
211
+ latest_task = max(
212
+ matching_task_groups, key=lambda tg: tg.version or ""
213
+ )
214
+ version = latest_task.version
215
+ logger.info(
216
+ f"[_get_task_by_taskimport] Latest version set to {version}."
217
+ )
218
+ else:
219
+ version = task_import.version
220
+
221
+ # Filter task groups by version
222
+ final_matching_task_groups = list(
223
+ filter(lambda tg: tg.version == version, task_groups_list)
224
+ )
225
+
226
+ if len(final_matching_task_groups) < 1:
227
+ logger.info(
228
+ "[_get_task_by_taskimport] "
229
+ "No task group left after filtering by version."
230
+ )
231
+ return None
232
+ elif len(final_matching_task_groups) == 1:
233
+ final_task_group = final_matching_task_groups[0]
234
+ logger.info(
235
+ "[_get_task_by_taskimport] "
236
+ "Found a single task group, after filtering by version."
237
+ )
238
+ else:
239
+ logger.info(
240
+ "[_get_task_by_taskimport] "
241
+ "Found many task groups, after filtering by version."
242
+ )
243
+ final_task_group = await _disambiguate_task_groups(
244
+ matching_task_groups, user_id, db, default_group_id
245
+ )
246
+ if final_task_group is None:
247
+ logger.info(
248
+ "[_get_task_by_taskimport] Disambiguation returned None."
249
+ )
250
+ return None
251
+
252
+ # Find task with given name
253
+ task_id = next(
254
+ iter(
255
+ task.id
256
+ for task in final_task_group.task_list
257
+ if task.name == task_import.name
258
+ ),
259
+ None,
260
+ )
261
+
262
+ logger.info(f"[_get_task_by_taskimport] END, {task_import=}, {task_id=}.")
263
+
264
+ return task_id
265
+
266
+
267
+ @router.post(
268
+ "/project/{project_id}/workflow/import/",
269
+ response_model=WorkflowReadV2WithWarnings,
270
+ status_code=status.HTTP_201_CREATED,
271
+ )
272
+ async def import_workflow(
273
+ project_id: int,
274
+ workflow_import: WorkflowImportV2,
275
+ user: UserOAuth = Depends(current_active_user),
276
+ db: AsyncSession = Depends(get_async_db),
277
+ ) -> WorkflowReadV2WithWarnings:
278
+ """
279
+ Import an existing workflow into a project and create required objects.
280
+ """
281
+
282
+ # Preliminary checks
283
+ await _get_project_check_owner(
284
+ project_id=project_id,
285
+ user_id=user.id,
286
+ db=db,
287
+ )
288
+ await _check_workflow_exists(
289
+ name=workflow_import.name,
290
+ project_id=project_id,
291
+ db=db,
292
+ )
293
+
294
+ task_group_list = await _get_user_accessible_taskgroups(
295
+ user_id=user.id,
296
+ db=db,
297
+ )
298
+ default_group_id = await _get_default_usergroup_id(db)
299
+
300
+ list_wf_tasks = []
301
+ for wf_task in workflow_import.task_list:
302
+ task_import = wf_task.task
303
+ if isinstance(task_import, TaskImportV2Legacy):
304
+ task_id = await _get_task_by_source(
305
+ source=task_import.source,
306
+ task_groups_list=task_group_list,
307
+ )
308
+ else:
309
+ task_id = await _get_task_by_taskimport(
310
+ task_import=task_import,
311
+ user_id=user.id,
312
+ default_group_id=default_group_id,
313
+ task_groups_list=task_group_list,
314
+ db=db,
315
+ )
316
+ if task_id is None:
317
+ raise HTTPException(
318
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
319
+ detail=f"Could not find a task matching with {wf_task.task}.",
320
+ )
321
+ new_wf_task = WorkflowTaskCreateV2(
322
+ **wf_task.dict(exclude_none=True, exclude={"task"})
323
+ )
324
+ list_wf_tasks.append(new_wf_task)
325
+
326
+ # Create new Workflow
327
+ db_workflow = WorkflowV2(
328
+ project_id=project_id,
329
+ **workflow_import.dict(exclude_none=True, exclude={"task_list"}),
330
+ )
331
+ db.add(db_workflow)
332
+ await db.commit()
333
+ await db.refresh(db_workflow)
334
+
335
+ # Insert task into the workflow
336
+ for new_wf_task in list_wf_tasks:
337
+ await _workflow_insert_task(
338
+ **new_wf_task.dict(),
339
+ workflow_id=db_workflow.id,
340
+ task_id=task_id,
341
+ db=db,
342
+ )
343
+
344
+ # Add warnings for non-active tasks (or non-accessible tasks,
345
+ # although that should never happen)
346
+ wftask_list_with_warnings = await _add_warnings_to_workflow_tasks(
347
+ wftask_list=db_workflow.task_list, user_id=user.id, db=db
348
+ )
349
+ workflow_data = dict(
350
+ **db_workflow.model_dump(),
351
+ project=db_workflow.project,
352
+ task_list=wftask_list_with_warnings,
353
+ )
354
+
355
+ return workflow_data
@@ -19,10 +19,12 @@ from fractal_server.app.db import get_async_db
19
19
  from fractal_server.app.models import LinkUserGroup
20
20
  from fractal_server.app.models import UserGroup
21
21
  from fractal_server.app.models import UserOAuth
22
+ from fractal_server.app.models import UserSettings
22
23
  from fractal_server.app.models.v2 import TaskGroupV2
23
24
  from fractal_server.app.schemas.user_group import UserGroupCreate
24
25
  from fractal_server.app.schemas.user_group import UserGroupRead
25
26
  from fractal_server.app.schemas.user_group import UserGroupUpdate
27
+ from fractal_server.app.schemas.user_settings import UserSettingsUpdate
26
28
  from fractal_server.app.security import FRACTAL_DEFAULT_GROUP_NAME
27
29
  from fractal_server.logger import set_logger
28
30
 
@@ -212,3 +214,28 @@ async def delete_single_group(
212
214
  await db.commit()
213
215
 
214
216
  return Response(status_code=status.HTTP_204_NO_CONTENT)
217
+
218
+
219
+ @router_group.patch("/group/{group_id}/user-settings/", status_code=200)
220
+ async def patch_user_settings_bulk(
221
+ group_id: int,
222
+ settings_update: UserSettingsUpdate,
223
+ superuser: UserOAuth = Depends(current_active_superuser),
224
+ db: AsyncSession = Depends(get_async_db),
225
+ ):
226
+ await _usergroup_or_404(group_id, db)
227
+ res = await db.execute(
228
+ select(UserSettings)
229
+ .join(UserOAuth)
230
+ .where(LinkUserGroup.user_id == UserOAuth.id)
231
+ .where(LinkUserGroup.group_id == group_id)
232
+ )
233
+ settings_list = res.scalars().all()
234
+ update = settings_update.dict(exclude_unset=True)
235
+ for settings in settings_list:
236
+ for k, v in update.items():
237
+ setattr(settings, k, v)
238
+ db.add(settings)
239
+ await db.commit()
240
+
241
+ return Response(status_code=status.HTTP_200_OK)