fractal-server 2.17.1a1__py3-none-any.whl → 2.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +21 -19
  3. fractal_server/app/db/__init__.py +3 -3
  4. fractal_server/app/models/__init__.py +1 -0
  5. fractal_server/app/models/linkuserproject.py +43 -1
  6. fractal_server/app/models/security.py +28 -8
  7. fractal_server/app/models/v2/__init__.py +3 -1
  8. fractal_server/app/models/v2/accounting.py +9 -1
  9. fractal_server/app/models/v2/dataset.py +5 -1
  10. fractal_server/app/models/v2/history.py +15 -1
  11. fractal_server/app/models/v2/job.py +17 -2
  12. fractal_server/app/models/v2/profile.py +29 -0
  13. fractal_server/app/models/v2/project.py +4 -10
  14. fractal_server/app/models/v2/resource.py +17 -0
  15. fractal_server/app/models/v2/task_group.py +4 -3
  16. fractal_server/app/models/v2/workflow.py +2 -1
  17. fractal_server/app/routes/admin/v2/__init__.py +12 -13
  18. fractal_server/app/routes/admin/v2/accounting.py +3 -3
  19. fractal_server/app/routes/admin/v2/job.py +35 -24
  20. fractal_server/app/routes/admin/v2/profile.py +3 -2
  21. fractal_server/app/routes/admin/v2/resource.py +5 -5
  22. fractal_server/app/routes/admin/v2/sharing.py +103 -0
  23. fractal_server/app/routes/admin/v2/task.py +37 -26
  24. fractal_server/app/routes/admin/v2/task_group.py +94 -17
  25. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +21 -22
  26. fractal_server/app/routes/api/__init__.py +1 -9
  27. fractal_server/app/routes/api/v2/__init__.py +49 -50
  28. fractal_server/app/routes/api/v2/_aux_functions.py +132 -124
  29. fractal_server/app/routes/api/v2/_aux_functions_history.py +51 -23
  30. fractal_server/app/routes/api/v2/_aux_functions_sharing.py +97 -0
  31. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +6 -8
  32. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +7 -9
  33. fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +1 -2
  34. fractal_server/app/routes/api/v2/dataset.py +95 -102
  35. fractal_server/app/routes/api/v2/history.py +59 -33
  36. fractal_server/app/routes/api/v2/images.py +24 -9
  37. fractal_server/app/routes/api/v2/job.py +52 -33
  38. fractal_server/app/routes/api/v2/pre_submission_checks.py +16 -8
  39. fractal_server/app/routes/api/v2/project.py +65 -37
  40. fractal_server/app/routes/api/v2/sharing.py +311 -0
  41. fractal_server/app/routes/api/v2/status_legacy.py +31 -41
  42. fractal_server/app/routes/api/v2/submit.py +82 -78
  43. fractal_server/app/routes/api/v2/task.py +19 -20
  44. fractal_server/app/routes/api/v2/task_collection.py +41 -43
  45. fractal_server/app/routes/api/v2/task_collection_custom.py +19 -20
  46. fractal_server/app/routes/api/v2/task_collection_pixi.py +10 -11
  47. fractal_server/app/routes/api/v2/task_group.py +25 -24
  48. fractal_server/app/routes/api/v2/task_group_lifecycle.py +32 -32
  49. fractal_server/app/routes/api/v2/task_version_update.py +23 -19
  50. fractal_server/app/routes/api/v2/workflow.py +50 -55
  51. fractal_server/app/routes/api/v2/workflow_import.py +37 -37
  52. fractal_server/app/routes/api/v2/workflowtask.py +32 -26
  53. fractal_server/app/routes/auth/__init__.py +1 -3
  54. fractal_server/app/routes/auth/_aux_auth.py +101 -2
  55. fractal_server/app/routes/auth/current_user.py +2 -66
  56. fractal_server/app/routes/auth/group.py +8 -35
  57. fractal_server/app/routes/auth/login.py +1 -0
  58. fractal_server/app/routes/auth/oauth.py +4 -3
  59. fractal_server/app/routes/auth/register.py +4 -2
  60. fractal_server/app/routes/auth/router.py +2 -0
  61. fractal_server/app/routes/auth/users.py +19 -10
  62. fractal_server/app/routes/auth/viewer_paths.py +43 -0
  63. fractal_server/app/routes/aux/_job.py +1 -1
  64. fractal_server/app/routes/aux/_runner.py +2 -2
  65. fractal_server/app/routes/pagination.py +1 -1
  66. fractal_server/app/schemas/user.py +29 -12
  67. fractal_server/app/schemas/user_group.py +0 -15
  68. fractal_server/app/schemas/v2/__init__.py +55 -48
  69. fractal_server/app/schemas/v2/accounting.py +11 -0
  70. fractal_server/app/schemas/v2/dataset.py +57 -11
  71. fractal_server/app/schemas/v2/dumps.py +10 -9
  72. fractal_server/app/schemas/v2/job.py +11 -11
  73. fractal_server/app/schemas/v2/manifest.py +4 -3
  74. fractal_server/app/schemas/v2/profile.py +53 -2
  75. fractal_server/app/schemas/v2/project.py +3 -3
  76. fractal_server/app/schemas/v2/resource.py +121 -16
  77. fractal_server/app/schemas/v2/sharing.py +99 -0
  78. fractal_server/app/schemas/v2/status_legacy.py +3 -3
  79. fractal_server/app/schemas/v2/task.py +6 -7
  80. fractal_server/app/schemas/v2/task_collection.py +5 -5
  81. fractal_server/app/schemas/v2/task_group.py +16 -16
  82. fractal_server/app/schemas/v2/workflow.py +16 -16
  83. fractal_server/app/schemas/v2/workflowtask.py +16 -15
  84. fractal_server/app/security/__init__.py +5 -8
  85. fractal_server/app/security/signup_email.py +4 -5
  86. fractal_server/app/shutdown.py +6 -6
  87. fractal_server/config/__init__.py +0 -6
  88. fractal_server/config/_data.py +0 -68
  89. fractal_server/config/_database.py +19 -20
  90. fractal_server/config/_email.py +30 -38
  91. fractal_server/config/_main.py +38 -52
  92. fractal_server/config/_oauth.py +17 -21
  93. fractal_server/data_migrations/2_18_0.py +30 -0
  94. fractal_server/exceptions.py +4 -0
  95. fractal_server/images/models.py +4 -5
  96. fractal_server/images/status_tools.py +4 -2
  97. fractal_server/logger.py +1 -1
  98. fractal_server/main.py +75 -13
  99. fractal_server/migrations/versions/034a469ec2eb_task_groups.py +4 -8
  100. fractal_server/migrations/versions/091b01f51f88_add_usergroup_and_linkusergroup_table.py +1 -1
  101. fractal_server/migrations/versions/0f5f85bb2ae7_add_pre_pinned_packages.py +1 -0
  102. fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py +1 -1
  103. fractal_server/migrations/versions/1a83a5260664_rename.py +1 -1
  104. fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py +1 -0
  105. fractal_server/migrations/versions/316140ff7ee1_remove_usersettings_cache_dir.py +1 -1
  106. fractal_server/migrations/versions/40d6d6511b20_add_index_to_history_models.py +47 -0
  107. fractal_server/migrations/versions/45fbb391d7af_make_resource_id_fk_non_nullable.py +1 -1
  108. fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +1 -0
  109. fractal_server/migrations/versions/49d0856e9569_drop_table.py +2 -3
  110. fractal_server/migrations/versions/4c308bcaea2b_add_task_args_schema_and_task_args_.py +1 -1
  111. fractal_server/migrations/versions/4cedeb448a53_workflowtask_foreign_keys_not_nullables.py +1 -1
  112. fractal_server/migrations/versions/501961cfcd85_remove_link_between_v1_and_v2_tasks_.py +2 -1
  113. fractal_server/migrations/versions/50a13d6138fd_initial_schema.py +7 -19
  114. fractal_server/migrations/versions/5bf02391cfef_v2.py +4 -10
  115. fractal_server/migrations/versions/70e77f1c38b0_add_applyworkflow_first_task_index_and_.py +1 -0
  116. fractal_server/migrations/versions/71eefd1dd202_add_slurm_accounts.py +1 -1
  117. fractal_server/migrations/versions/7673fe18c05d_remove_project_dir_server_default.py +1 -1
  118. fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
  119. fractal_server/migrations/versions/791ce783d3d8_add_indices.py +1 -1
  120. fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +1 -0
  121. fractal_server/migrations/versions/84bf0fffde30_add_dumps_to_applyworkflow.py +1 -0
  122. fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
  123. fractal_server/migrations/versions/8e8f227a3e36_update_taskv2_post_2_7_0.py +2 -4
  124. fractal_server/migrations/versions/8f79bd162e35_add_docs_info_and_docs_link_to_task_.py +1 -1
  125. fractal_server/migrations/versions/94a47ea2d3ff_remove_cache_dir_slurm_user_and_slurm_.py +1 -0
  126. fractal_server/migrations/versions/969d84257cac_add_historyrun_task_id.py +1 -1
  127. fractal_server/migrations/versions/97f444d47249_add_applyworkflow_project_dump.py +1 -1
  128. fractal_server/migrations/versions/981d588fe248_add_executor_error_log.py +1 -1
  129. fractal_server/migrations/versions/99ea79d9e5d2_add_dataset_history.py +2 -4
  130. fractal_server/migrations/versions/9c5ae74c9b98_add_user_settings_table.py +1 -1
  131. fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py +1 -1
  132. fractal_server/migrations/versions/9fd26a2b0de4_add_workflow_timestamp_created.py +1 -1
  133. fractal_server/migrations/versions/a7f4d6137b53_add_workflow_dump_to_applyworkflow.py +1 -1
  134. fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +1 -0
  135. fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py +1 -0
  136. fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +1 -1
  137. fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py +1 -0
  138. fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
  139. fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py +1 -1
  140. fractal_server/migrations/versions/caba9fb1ea5e_drop_useroauth_user_settings_id.py +1 -1
  141. fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +4 -9
  142. fractal_server/migrations/versions/d4fe3708d309_make_applyworkflow_workflow_dump_non_.py +1 -0
  143. fractal_server/migrations/versions/da2cb2ac4255_user_group_viewer_paths.py +1 -1
  144. fractal_server/migrations/versions/db09233ad13a_split_filters_and_keep_old_columns.py +1 -0
  145. fractal_server/migrations/versions/e0e717ae2f26_delete_linkuserproject_ondelete_project.py +50 -0
  146. fractal_server/migrations/versions/e75cac726012_make_applyworkflow_start_timestamp_not_.py +1 -0
  147. fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +1 -1
  148. fractal_server/migrations/versions/efa89c30e0a4_add_project_timestamp_created.py +1 -0
  149. fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
  150. fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py +1 -1
  151. fractal_server/migrations/versions/f384e1c0cf5d_drop_task_default_args_columns.py +1 -0
  152. fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +4 -9
  153. fractal_server/runner/config/_local.py +8 -5
  154. fractal_server/runner/config/_slurm.py +39 -33
  155. fractal_server/runner/config/slurm_mem_to_MB.py +0 -1
  156. fractal_server/runner/executors/base_runner.py +29 -4
  157. fractal_server/runner/executors/local/get_local_config.py +1 -0
  158. fractal_server/runner/executors/local/runner.py +14 -13
  159. fractal_server/runner/executors/slurm_common/_batching.py +9 -20
  160. fractal_server/runner/executors/slurm_common/base_slurm_runner.py +53 -27
  161. fractal_server/runner/executors/slurm_common/get_slurm_config.py +14 -7
  162. fractal_server/runner/executors/slurm_common/remote.py +3 -1
  163. fractal_server/runner/executors/slurm_common/slurm_config.py +2 -0
  164. fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -3
  165. fractal_server/runner/executors/slurm_ssh/runner.py +16 -11
  166. fractal_server/runner/executors/slurm_ssh/tar_commands.py +1 -0
  167. fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +1 -0
  168. fractal_server/runner/executors/slurm_sudo/runner.py +16 -11
  169. fractal_server/runner/task_files.py +9 -3
  170. fractal_server/runner/v2/_local.py +12 -6
  171. fractal_server/runner/v2/_slurm_ssh.py +14 -7
  172. fractal_server/runner/v2/_slurm_sudo.py +14 -7
  173. fractal_server/runner/v2/db_tools.py +0 -1
  174. fractal_server/runner/v2/deduplicate_list.py +2 -1
  175. fractal_server/runner/v2/runner.py +44 -28
  176. fractal_server/runner/v2/runner_functions.py +22 -28
  177. fractal_server/runner/v2/submit_workflow.py +29 -15
  178. fractal_server/ssh/_fabric.py +6 -13
  179. fractal_server/string_tools.py +0 -1
  180. fractal_server/syringe.py +1 -1
  181. fractal_server/tasks/config/_pixi.py +1 -1
  182. fractal_server/tasks/config/_python.py +16 -9
  183. fractal_server/tasks/utils.py +0 -1
  184. fractal_server/tasks/v2/local/_utils.py +3 -3
  185. fractal_server/tasks/v2/local/collect.py +15 -18
  186. fractal_server/tasks/v2/local/collect_pixi.py +14 -16
  187. fractal_server/tasks/v2/local/deactivate.py +14 -15
  188. fractal_server/tasks/v2/local/deactivate_pixi.py +7 -7
  189. fractal_server/tasks/v2/local/delete.py +6 -8
  190. fractal_server/tasks/v2/local/reactivate.py +12 -12
  191. fractal_server/tasks/v2/local/reactivate_pixi.py +12 -12
  192. fractal_server/tasks/v2/ssh/_utils.py +3 -3
  193. fractal_server/tasks/v2/ssh/collect.py +19 -24
  194. fractal_server/tasks/v2/ssh/collect_pixi.py +22 -24
  195. fractal_server/tasks/v2/ssh/deactivate.py +17 -15
  196. fractal_server/tasks/v2/ssh/deactivate_pixi.py +8 -7
  197. fractal_server/tasks/v2/ssh/delete.py +12 -10
  198. fractal_server/tasks/v2/ssh/reactivate.py +16 -16
  199. fractal_server/tasks/v2/ssh/reactivate_pixi.py +13 -14
  200. fractal_server/tasks/v2/templates/1_create_venv.sh +2 -0
  201. fractal_server/tasks/v2/templates/2_pip_install.sh +2 -0
  202. fractal_server/tasks/v2/templates/3_pip_freeze.sh +2 -0
  203. fractal_server/tasks/v2/templates/4_pip_show.sh +2 -0
  204. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +3 -1
  205. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +2 -0
  206. fractal_server/tasks/v2/templates/pixi_1_extract.sh +2 -0
  207. fractal_server/tasks/v2/templates/pixi_2_install.sh +2 -0
  208. fractal_server/tasks/v2/templates/pixi_3_post_install.sh +2 -0
  209. fractal_server/tasks/v2/utils_background.py +10 -10
  210. fractal_server/tasks/v2/utils_database.py +5 -5
  211. fractal_server/tasks/v2/utils_package_names.py +1 -2
  212. fractal_server/tasks/v2/utils_pixi.py +1 -3
  213. fractal_server/types/__init__.py +98 -1
  214. fractal_server/types/validators/__init__.py +3 -0
  215. fractal_server/types/validators/_common_validators.py +33 -3
  216. fractal_server/types/validators/_workflow_task_arguments_validators.py +1 -2
  217. fractal_server/utils.py +1 -0
  218. fractal_server/zip_tools.py +34 -0
  219. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/METADATA +3 -2
  220. fractal_server-2.18.0.dist-info/RECORD +275 -0
  221. fractal_server/app/routes/admin/v2/project.py +0 -41
  222. fractal_server-2.17.1a1.dist-info/RECORD +0 -264
  223. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/WHEEL +0 -0
  224. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/entry_points.txt +0 -0
  225. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,11 +5,10 @@ from fractal_server.types import DictStrAny
5
5
  from fractal_server.types import ImageAttributes
6
6
  from fractal_server.types import ImageAttributesWithNone
7
7
  from fractal_server.types import ImageTypes
8
- from fractal_server.types import ZarrDirStr
9
8
  from fractal_server.types import ZarrUrlStr
10
9
 
11
10
 
12
- class _SingleImageBase(BaseModel):
11
+ class SingleImageBase(BaseModel):
13
12
  """
14
13
  Base for SingleImage and SingleImageTaskOutput.
15
14
 
@@ -21,13 +20,13 @@ class _SingleImageBase(BaseModel):
21
20
  """
22
21
 
23
22
  zarr_url: ZarrUrlStr
24
- origin: ZarrDirStr | None = None
23
+ origin: ZarrUrlStr | None = None
25
24
 
26
25
  attributes: DictStrAny = Field(default_factory=dict)
27
26
  types: ImageTypes = Field(default_factory=dict)
28
27
 
29
28
 
30
- class SingleImageTaskOutput(_SingleImageBase):
29
+ class SingleImageTaskOutput(SingleImageBase):
31
30
  """
32
31
  `SingleImageBase`, with scalar `attributes` values (`None` included).
33
32
  """
@@ -35,7 +34,7 @@ class SingleImageTaskOutput(_SingleImageBase):
35
34
  attributes: ImageAttributesWithNone = Field(default_factory=dict)
36
35
 
37
36
 
38
- class SingleImage(_SingleImageBase):
37
+ class SingleImage(SingleImageBase):
39
38
  """
40
39
  `SingleImageBase`, with scalar `attributes` values (`None` excluded).
41
40
  """
@@ -37,10 +37,12 @@ def _prepare_query(
37
37
  """
38
38
  stm = (
39
39
  select(HistoryImageCache.zarr_url, HistoryUnit.status)
40
- .join(HistoryUnit)
40
+ .join(
41
+ HistoryUnit,
42
+ HistoryImageCache.latest_history_unit_id == HistoryUnit.id,
43
+ )
41
44
  .where(HistoryImageCache.dataset_id == dataset_id)
42
45
  .where(HistoryImageCache.workflowtask_id == workflowtask_id)
43
- .where(HistoryImageCache.latest_history_unit_id == HistoryUnit.id)
44
46
  )
45
47
  return stm
46
48
 
fractal_server/logger.py CHANGED
@@ -12,13 +12,13 @@
12
12
  """
13
13
  This module provides logging utilities
14
14
  """
15
+
15
16
  import logging
16
17
  from pathlib import Path
17
18
 
18
19
  from .config import get_settings
19
20
  from .syringe import Inject
20
21
 
21
-
22
22
  LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
23
23
  LOG_FORMATTER = logging.Formatter(LOG_FORMAT)
24
24
 
fractal_server/main.py CHANGED
@@ -1,12 +1,20 @@
1
1
  import os
2
+ import time
2
3
  from contextlib import asynccontextmanager
4
+ from datetime import datetime
3
5
  from itertools import chain
4
6
 
5
7
  from fastapi import FastAPI
8
+ from starlette.types import Message
9
+ from starlette.types import Receive
10
+ from starlette.types import Scope
11
+ from starlette.types import Send
12
+
13
+ from fractal_server import __VERSION__
14
+ from fractal_server.app.schemas.v2 import ResourceType
6
15
 
7
16
  from .app.routes.aux._runner import _backend_supports_shutdown
8
17
  from .app.shutdown import cleanup_after_shutdown
9
- from .config import get_data_settings
10
18
  from .config import get_db_settings
11
19
  from .config import get_email_settings
12
20
  from .config import get_settings
@@ -15,8 +23,6 @@ from .logger import get_logger
15
23
  from .logger import reset_logger_handlers
16
24
  from .logger import set_logger
17
25
  from .syringe import Inject
18
- from fractal_server import __VERSION__
19
- from fractal_server.app.schemas.v2 import ResourceType
20
26
 
21
27
 
22
28
  def collect_routers(app: FastAPI) -> None:
@@ -27,16 +33,14 @@ def collect_routers(app: FastAPI) -> None:
27
33
  app:
28
34
  The application to register the routers to.
29
35
  """
36
+ from .app.routes.admin.v2 import router_admin
30
37
  from .app.routes.api import router_api
31
- from .app.routes.api.v2 import router_api_v2
32
- from .app.routes.admin.v2 import router_admin_v2
38
+ from .app.routes.api.v2 import router_api as router_api_v2
33
39
  from .app.routes.auth.router import router_auth
34
40
 
35
41
  app.include_router(router_api, prefix="/api")
36
42
  app.include_router(router_api_v2, prefix="/api/v2")
37
- app.include_router(
38
- router_admin_v2, prefix="/admin/v2", tags=["V2 Admin area"]
39
- )
43
+ app.include_router(router_admin, prefix="/admin/v2", tags=["Admin area"])
40
44
  app.include_router(router_auth, prefix="/auth", tags=["Authentication"])
41
45
 
42
46
 
@@ -53,14 +57,12 @@ def check_settings() -> None:
53
57
  settings = Inject(get_settings)
54
58
  db_settings = Inject(get_db_settings)
55
59
  email_settings = Inject(get_email_settings)
56
- data_settings = Inject(get_data_settings)
57
60
  logger = set_logger("fractal_server_settings")
58
61
  logger.debug("Fractal Settings:")
59
62
  for key, value in chain(
60
63
  db_settings.model_dump().items(),
61
64
  settings.model_dump().items(),
62
65
  email_settings.model_dump().items(),
63
- data_settings.model_dump().items(),
64
66
  ):
65
67
  if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
66
68
  value = "*****"
@@ -70,7 +72,7 @@ def check_settings() -> None:
70
72
 
71
73
  @asynccontextmanager
72
74
  async def lifespan(app: FastAPI):
73
- app.state.jobsV2 = []
75
+ app.state.jobs = []
74
76
  logger = set_logger("fractal_server.lifespan")
75
77
  logger.info(f"[startup] START (fractal-server {__VERSION__})")
76
78
  check_settings()
@@ -107,12 +109,12 @@ async def lifespan(app: FastAPI):
107
109
 
108
110
  logger.info(
109
111
  f"[teardown] Current worker with pid {os.getpid()} is shutting down. "
110
- f"Current jobs: {app.state.jobsV2=}"
112
+ f"Current jobs: {app.state.jobs=}"
111
113
  )
112
114
  if _backend_supports_shutdown(settings.FRACTAL_RUNNER_BACKEND):
113
115
  try:
114
116
  await cleanup_after_shutdown(
115
- jobsV2=app.state.jobsV2,
117
+ jobs=app.state.jobs,
116
118
  logger_name="fractal_server.lifespan",
117
119
  )
118
120
  except Exception as e:
@@ -130,6 +132,59 @@ async def lifespan(app: FastAPI):
130
132
  reset_logger_handlers(logger)
131
133
 
132
134
 
135
+ slow_response_logger = set_logger("slow-response")
136
+
137
+
138
+ def _endpoint_has_background_task(method: str, path: str) -> bool:
139
+ has_background_task = (method == "POST") and (
140
+ "/job/submit/" in path
141
+ or "/task/collect/pi" in path # "/pip" and "/pixi"
142
+ or "/task-group/" in path
143
+ )
144
+ return has_background_task
145
+
146
+
147
+ class SlowResponseMiddleware:
148
+ def __init__(self, app: FastAPI, time_threshold: float):
149
+ self.app = app
150
+ self.time_threshold = time_threshold
151
+
152
+ async def __call__(self, scope: Scope, receive: Receive, send: Send):
153
+ if (
154
+ scope["type"] != "http" # e.g. `scope["type"] == "lifespan"`
155
+ or _endpoint_has_background_task(scope["method"], scope["path"])
156
+ ):
157
+ await self.app(scope, receive, send)
158
+ return
159
+
160
+ # Mutable variable which can be updated from within `send_wrapper`
161
+ context = {"status_code": None}
162
+
163
+ async def send_wrapper(message: Message):
164
+ if message["type"] == "http.response.start":
165
+ context["status_code"] = message["status"]
166
+ await send(message)
167
+
168
+ # Measure request time
169
+ start_timestamp = datetime.now()
170
+ start_time = time.perf_counter()
171
+ await self.app(scope, receive, send_wrapper)
172
+ stop_time = time.perf_counter()
173
+ request_time = stop_time - start_time
174
+
175
+ # Log if process time is too high
176
+ if request_time > self.time_threshold:
177
+ end_timestamp = datetime.now()
178
+ slow_response_logger.warning(
179
+ f"{scope['method']} {scope['route'].path}"
180
+ f"?{scope['query_string'].decode('utf-8')}, "
181
+ f"{context['status_code']}, "
182
+ f"{request_time:.2f}, "
183
+ f"{start_timestamp.isoformat(timespec='milliseconds')}, "
184
+ f"{end_timestamp.isoformat(timespec='milliseconds')}"
185
+ )
186
+
187
+
133
188
  def start_application() -> FastAPI:
134
189
  """
135
190
  Create the application, initialise it and collect all available routers.
@@ -139,6 +194,13 @@ def start_application() -> FastAPI:
139
194
  The fully initialised application.
140
195
  """
141
196
  app = FastAPI(lifespan=lifespan)
197
+
198
+ settings = Inject(get_settings)
199
+ app.add_middleware(
200
+ SlowResponseMiddleware,
201
+ time_threshold=settings.FRACTAL_LONG_REQUEST_TIME,
202
+ )
203
+
142
204
  collect_routers(app)
143
205
  return app
144
206
 
@@ -5,6 +5,7 @@ Revises: da2cb2ac4255
5
5
  Create Date: 2024-10-10 16:14:13.976231
6
6
 
7
7
  """
8
+
8
9
  from datetime import datetime
9
10
  from datetime import timezone
10
11
 
@@ -12,7 +13,6 @@ import sqlalchemy as sa
12
13
  import sqlmodel
13
14
  from alembic import op
14
15
 
15
-
16
16
  # revision identifiers, used by Alembic.
17
17
  revision = "034a469ec2eb"
18
18
  down_revision = "da2cb2ac4255"
@@ -26,15 +26,11 @@ def upgrade() -> None:
26
26
  sa.Column("id", sa.Integer(), nullable=False),
27
27
  sa.Column("user_id", sa.Integer(), nullable=False),
28
28
  sa.Column("user_group_id", sa.Integer(), nullable=True),
29
- sa.Column(
30
- "origin", sqlmodel.sql.sqltypes.AutoString(), nullable=False
31
- ),
29
+ sa.Column("origin", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
32
30
  sa.Column(
33
31
  "pkg_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False
34
32
  ),
35
- sa.Column(
36
- "version", sqlmodel.sql.sqltypes.AutoString(), nullable=True
37
- ),
33
+ sa.Column("version", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
38
34
  sa.Column(
39
35
  "python_version", sqlmodel.sql.sqltypes.AutoString(), nullable=True
40
36
  ),
@@ -123,8 +119,8 @@ def upgrade() -> None:
123
119
  except BaseException as e:
124
120
  if op.get_bind().dialect.name != "sqlite":
125
121
  raise e
126
- import sqlite3
127
122
  import logging
123
+ import sqlite3
128
124
 
129
125
  logger = logging.getLogger("alembic.runtime.migration")
130
126
  logger.warning(
@@ -5,11 +5,11 @@ Revises: 5bf02391cfef
5
5
  Create Date: 2024-09-09 13:17:51.008231
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  import sqlmodel
10
11
  from alembic import op
11
12
 
12
-
13
13
  # revision identifiers, used by Alembic.
14
14
  revision = "091b01f51f88"
15
15
  down_revision = "5bf02391cfef"
@@ -5,6 +5,7 @@ Revises: 1a83a5260664
5
5
  Create Date: 2025-09-10 14:36:06.028179
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  from alembic import op
10
11
  from sqlalchemy.dialects import postgresql
@@ -5,11 +5,11 @@ Revises: 8e8f227a3e36
5
5
  Create Date: 2024-10-30 14:34:28.219355
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  import sqlmodel
10
11
  from alembic import op
11
12
 
12
-
13
13
  # revision identifiers, used by Alembic.
14
14
  revision = "19eca0dd47a9"
15
15
  down_revision = "8e8f227a3e36"
@@ -5,8 +5,8 @@ Revises: b3ffb095f973
5
5
  Create Date: 2025-09-10 14:16:51.202765
6
6
 
7
7
  """
8
- from alembic import op
9
8
 
9
+ from alembic import op
10
10
 
11
11
  # revision identifiers, used by Alembic.
12
12
  revision = "1a83a5260664"
@@ -5,6 +5,7 @@ Revises: af8673379a5c
5
5
  Create Date: 2025-01-10 13:17:47.838607
6
6
 
7
7
  """
8
+
8
9
  import logging
9
10
 
10
11
  from alembic import op
@@ -5,10 +5,10 @@ Revises: d256a7379ab8
5
5
  Create Date: 2024-12-03 10:15:53.255958
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  from alembic import op
10
11
 
11
-
12
12
  # revision identifiers, used by Alembic.
13
13
  revision = "316140ff7ee1"
14
14
  down_revision = "d256a7379ab8"
@@ -0,0 +1,47 @@
1
+ """add index to history models
2
+
3
+ Revision ID: 40d6d6511b20
4
+ Revises: 7673fe18c05d
5
+ Create Date: 2025-11-14 10:34:12.300920
6
+
7
+ """
8
+
9
+ from alembic import op
10
+
11
+ # revision identifiers, used by Alembic.
12
+ revision = "40d6d6511b20"
13
+ down_revision = "7673fe18c05d"
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ # ### commands auto generated by Alembic - please adjust! ###
20
+ with op.batch_alter_table("historyimagecache", schema=None) as batch_op:
21
+ batch_op.create_index(
22
+ batch_op.f("ix_historyimagecache_latest_history_unit_id"),
23
+ ["latest_history_unit_id"],
24
+ unique=False,
25
+ )
26
+
27
+ with op.batch_alter_table("historyunit", schema=None) as batch_op:
28
+ batch_op.create_index(
29
+ batch_op.f("ix_historyunit_history_run_id"),
30
+ ["history_run_id"],
31
+ unique=False,
32
+ )
33
+
34
+ # ### end Alembic commands ###
35
+
36
+
37
+ def downgrade() -> None:
38
+ # ### commands auto generated by Alembic - please adjust! ###
39
+ with op.batch_alter_table("historyunit", schema=None) as batch_op:
40
+ batch_op.drop_index(batch_op.f("ix_historyunit_history_run_id"))
41
+
42
+ with op.batch_alter_table("historyimagecache", schema=None) as batch_op:
43
+ batch_op.drop_index(
44
+ batch_op.f("ix_historyimagecache_latest_history_unit_id")
45
+ )
46
+
47
+ # ### end Alembic commands ###
@@ -5,10 +5,10 @@ Revises: caba9fb1ea5e
5
5
  Create Date: 2025-11-11 16:39:12.813766
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  from alembic import op
10
11
 
11
-
12
12
  # revision identifiers, used by Alembic.
13
13
  revision = "45fbb391d7af"
14
14
  down_revision = "caba9fb1ea5e"
@@ -5,6 +5,7 @@ Revises: fbce16ff4e47
5
5
  Create Date: 2025-03-26 11:10:17.869028
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  from alembic import op
10
11
  from sqlalchemy.dialects import postgresql
@@ -5,6 +5,7 @@ Revises: 45fbb391d7af
5
5
  Create Date: 2025-11-11 16:39:41.497832
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  from alembic import op
10
11
  from sqlalchemy.dialects import postgresql
@@ -34,9 +35,7 @@ def downgrade() -> None:
34
35
  autoincrement=False,
35
36
  nullable=False,
36
37
  ),
37
- sa.Column(
38
- "ssh_host", sa.VARCHAR(), autoincrement=False, nullable=True
39
- ),
38
+ sa.Column("ssh_host", sa.VARCHAR(), autoincrement=False, nullable=True),
40
39
  sa.Column(
41
40
  "ssh_username", sa.VARCHAR(), autoincrement=False, nullable=True
42
41
  ),
@@ -5,11 +5,11 @@ Revises: 50a13d6138fd
5
5
  Create Date: 2023-05-29 17:09:02.492639
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  import sqlmodel
10
11
  from alembic import op
11
12
 
12
-
13
13
  # revision identifiers, used by Alembic.
14
14
  revision = "4c308bcaea2b"
15
15
  down_revision = "50a13d6138fd"
@@ -5,10 +5,10 @@ Revises: efa89c30e0a4
5
5
  Create Date: 2024-01-16 13:57:47.891931
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  from alembic import op
10
11
 
11
-
12
12
  # revision identifiers, used by Alembic.
13
13
  revision = "4cedeb448a53"
14
14
  down_revision = "efa89c30e0a4"
@@ -5,6 +5,7 @@ Revises: 5bf02391cfef
5
5
  Create Date: 2024-09-09 14:15:34.415926
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  from alembic import op
10
11
 
@@ -36,8 +37,8 @@ def upgrade() -> None:
36
37
  # NOTE: in sqlite, the `drop_index` command fails if the existing table
37
38
  # has zero rows, while it succeeds if there are already some rows
38
39
  if op.get_bind().dialect.name == "sqlite":
39
- import sqlite3
40
40
  import logging
41
+ import sqlite3
41
42
 
42
43
  logger = logging.getLogger("alembic.runtime.migration")
43
44
  logger.warning(
@@ -5,11 +5,11 @@ Revises:
5
5
  Create Date: 2023-05-29 12:14:56.670243
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  import sqlmodel
10
11
  from alembic import op
11
12
 
12
-
13
13
  # revision identifiers, used by Alembic.
14
14
  revision = "50a13d6138fd"
15
15
  down_revision = None
@@ -37,9 +37,7 @@ def upgrade() -> None:
37
37
  "task",
38
38
  sa.Column("default_args", sa.JSON(), nullable=True),
39
39
  sa.Column("meta", sa.JSON(), nullable=True),
40
- sa.Column(
41
- "source", sqlmodel.sql.sqltypes.AutoString(), nullable=False
42
- ),
40
+ sa.Column("source", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
43
41
  sa.Column("id", sa.Integer(), nullable=False),
44
42
  sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
45
43
  sa.Column(
@@ -52,9 +50,7 @@ def upgrade() -> None:
52
50
  "output_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False
53
51
  ),
54
52
  sa.Column("owner", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
55
- sa.Column(
56
- "version", sqlmodel.sql.sqltypes.AutoString(), nullable=True
57
- ),
53
+ sa.Column("version", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
58
54
  sa.PrimaryKeyConstraint("id"),
59
55
  sa.UniqueConstraint("source"),
60
56
  )
@@ -163,9 +159,7 @@ def upgrade() -> None:
163
159
  )
164
160
  op.create_table(
165
161
  "applyworkflow",
166
- sa.Column(
167
- "start_timestamp", sa.DateTime(timezone=True), nullable=True
168
- ),
162
+ sa.Column("start_timestamp", sa.DateTime(timezone=True), nullable=True),
169
163
  sa.Column("end_timestamp", sa.DateTime(timezone=True), nullable=True),
170
164
  sa.Column(
171
165
  "worker_init", sqlmodel.sql.sqltypes.AutoString(), nullable=True
@@ -183,9 +177,7 @@ def upgrade() -> None:
183
177
  sqlmodel.sql.sqltypes.AutoString(),
184
178
  nullable=True,
185
179
  ),
186
- sa.Column(
187
- "status", sqlmodel.sql.sqltypes.AutoString(), nullable=False
188
- ),
180
+ sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
189
181
  sa.Column("log", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
190
182
  sa.ForeignKeyConstraint(
191
183
  ["input_dataset_id"],
@@ -243,12 +235,8 @@ def downgrade() -> None:
243
235
  op.drop_table("resource")
244
236
  op.drop_table("applyworkflow")
245
237
  op.drop_table("workflow")
246
- op.drop_index(
247
- op.f("ix_oauthaccount_oauth_name"), table_name="oauthaccount"
248
- )
249
- op.drop_index(
250
- op.f("ix_oauthaccount_account_id"), table_name="oauthaccount"
251
- )
238
+ op.drop_index(op.f("ix_oauthaccount_oauth_name"), table_name="oauthaccount")
239
+ op.drop_index(op.f("ix_oauthaccount_account_id"), table_name="oauthaccount")
252
240
  op.drop_table("oauthaccount")
253
241
  op.drop_table("linkuserproject")
254
242
  op.drop_table("dataset")
@@ -5,11 +5,11 @@ Revises: 9fd26a2b0de4
5
5
  Create Date: 2024-04-18 10:35:19.067833
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  import sqlmodel
10
11
  from alembic import op
11
12
 
12
-
13
13
  # revision identifiers, used by Alembic.
14
14
  revision = "5bf02391cfef"
15
15
  down_revision = "9fd26a2b0de4"
@@ -50,9 +50,7 @@ def upgrade() -> None:
50
50
  sqlmodel.sql.sqltypes.AutoString(),
51
51
  nullable=True,
52
52
  ),
53
- sa.Column(
54
- "source", sqlmodel.sql.sqltypes.AutoString(), nullable=False
55
- ),
53
+ sa.Column("source", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
56
54
  sa.Column(
57
55
  "meta_non_parallel", sa.JSON(), server_default="{}", nullable=False
58
56
  ),
@@ -60,9 +58,7 @@ def upgrade() -> None:
60
58
  "meta_parallel", sa.JSON(), server_default="{}", nullable=False
61
59
  ),
62
60
  sa.Column("owner", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
63
- sa.Column(
64
- "version", sqlmodel.sql.sqltypes.AutoString(), nullable=True
65
- ),
61
+ sa.Column("version", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
66
62
  sa.Column("args_schema_non_parallel", sa.JSON(), nullable=True),
67
63
  sa.Column("args_schema_parallel", sa.JSON(), nullable=True),
68
64
  sa.Column(
@@ -166,9 +162,7 @@ def upgrade() -> None:
166
162
  "start_timestamp", sa.DateTime(timezone=True), nullable=False
167
163
  ),
168
164
  sa.Column("end_timestamp", sa.DateTime(timezone=True), nullable=True),
169
- sa.Column(
170
- "status", sqlmodel.sql.sqltypes.AutoString(), nullable=False
171
- ),
165
+ sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
172
166
  sa.Column("log", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
173
167
  sa.ForeignKeyConstraint(
174
168
  ["dataset_id"],
@@ -5,6 +5,7 @@ Revises: f384e1c0cf5d
5
5
  Create Date: 2023-07-17 11:39:47.679582
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  from alembic import op
10
11
  from sqlalchemy.sql import column
@@ -5,11 +5,11 @@ Revises: d4fe3708d309
5
5
  Create Date: 2023-12-05 12:36:44.100065
6
6
 
7
7
  """
8
+
8
9
  import sqlalchemy as sa
9
10
  import sqlmodel
10
11
  from alembic import op
11
12
 
12
-
13
13
  # revision identifiers, used by Alembic.
14
14
  revision = "71eefd1dd202"
15
15
  down_revision = "d4fe3708d309"
@@ -5,8 +5,8 @@ Revises: 49d0856e9569
5
5
  Create Date: 2025-11-11 16:50:20.079193
6
6
 
7
7
  """
8
- from alembic import op
9
8
 
9
+ from alembic import op
10
10
 
11
11
  # revision identifiers, used by Alembic.
12
12
  revision = "7673fe18c05d"