fractal-server 2.16.5__py3-none-any.whl → 2.17.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 (143) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +178 -52
  3. fractal_server/app/db/__init__.py +9 -11
  4. fractal_server/app/models/security.py +30 -22
  5. fractal_server/app/models/user_settings.py +5 -4
  6. fractal_server/app/models/v2/__init__.py +4 -0
  7. fractal_server/app/models/v2/job.py +3 -4
  8. fractal_server/app/models/v2/profile.py +16 -0
  9. fractal_server/app/models/v2/project.py +5 -0
  10. fractal_server/app/models/v2/resource.py +130 -0
  11. fractal_server/app/models/v2/task_group.py +4 -0
  12. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  13. fractal_server/app/routes/admin/v2/_aux_functions.py +55 -0
  14. fractal_server/app/routes/admin/v2/accounting.py +3 -3
  15. fractal_server/app/routes/admin/v2/impersonate.py +2 -2
  16. fractal_server/app/routes/admin/v2/job.py +51 -15
  17. fractal_server/app/routes/admin/v2/profile.py +100 -0
  18. fractal_server/app/routes/admin/v2/project.py +2 -2
  19. fractal_server/app/routes/admin/v2/resource.py +222 -0
  20. fractal_server/app/routes/admin/v2/task.py +59 -32
  21. fractal_server/app/routes/admin/v2/task_group.py +17 -12
  22. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +52 -86
  23. fractal_server/app/routes/api/__init__.py +45 -8
  24. fractal_server/app/routes/api/v2/_aux_functions.py +17 -1
  25. fractal_server/app/routes/api/v2/_aux_functions_history.py +2 -2
  26. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
  27. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +55 -19
  28. fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +21 -17
  29. fractal_server/app/routes/api/v2/dataset.py +10 -19
  30. fractal_server/app/routes/api/v2/history.py +8 -8
  31. fractal_server/app/routes/api/v2/images.py +5 -5
  32. fractal_server/app/routes/api/v2/job.py +8 -8
  33. fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
  34. fractal_server/app/routes/api/v2/project.py +15 -7
  35. fractal_server/app/routes/api/v2/status_legacy.py +2 -2
  36. fractal_server/app/routes/api/v2/submit.py +49 -42
  37. fractal_server/app/routes/api/v2/task.py +26 -8
  38. fractal_server/app/routes/api/v2/task_collection.py +39 -50
  39. fractal_server/app/routes/api/v2/task_collection_custom.py +10 -6
  40. fractal_server/app/routes/api/v2/task_collection_pixi.py +34 -42
  41. fractal_server/app/routes/api/v2/task_group.py +19 -9
  42. fractal_server/app/routes/api/v2/task_group_lifecycle.py +43 -86
  43. fractal_server/app/routes/api/v2/task_version_update.py +3 -3
  44. fractal_server/app/routes/api/v2/workflow.py +9 -9
  45. fractal_server/app/routes/api/v2/workflow_import.py +29 -16
  46. fractal_server/app/routes/api/v2/workflowtask.py +5 -5
  47. fractal_server/app/routes/auth/__init__.py +34 -5
  48. fractal_server/app/routes/auth/_aux_auth.py +39 -20
  49. fractal_server/app/routes/auth/current_user.py +56 -67
  50. fractal_server/app/routes/auth/group.py +29 -46
  51. fractal_server/app/routes/auth/oauth.py +55 -38
  52. fractal_server/app/routes/auth/register.py +2 -2
  53. fractal_server/app/routes/auth/router.py +4 -2
  54. fractal_server/app/routes/auth/users.py +29 -53
  55. fractal_server/app/routes/aux/_runner.py +2 -1
  56. fractal_server/app/routes/aux/validate_user_profile.py +62 -0
  57. fractal_server/app/schemas/__init__.py +0 -1
  58. fractal_server/app/schemas/user.py +43 -13
  59. fractal_server/app/schemas/user_group.py +2 -1
  60. fractal_server/app/schemas/v2/__init__.py +12 -0
  61. fractal_server/app/schemas/v2/profile.py +78 -0
  62. fractal_server/app/schemas/v2/resource.py +137 -0
  63. fractal_server/app/schemas/v2/task_collection.py +11 -3
  64. fractal_server/app/schemas/v2/task_group.py +5 -0
  65. fractal_server/app/security/__init__.py +174 -75
  66. fractal_server/app/security/signup_email.py +52 -34
  67. fractal_server/config/__init__.py +27 -0
  68. fractal_server/config/_data.py +68 -0
  69. fractal_server/config/_database.py +59 -0
  70. fractal_server/config/_email.py +133 -0
  71. fractal_server/config/_main.py +78 -0
  72. fractal_server/config/_oauth.py +69 -0
  73. fractal_server/config/_settings_config.py +7 -0
  74. fractal_server/data_migrations/2_17_0.py +339 -0
  75. fractal_server/images/tools.py +3 -3
  76. fractal_server/logger.py +3 -3
  77. fractal_server/main.py +17 -23
  78. fractal_server/migrations/naming_convention.py +1 -1
  79. fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +195 -0
  80. fractal_server/runner/config/__init__.py +2 -0
  81. fractal_server/runner/config/_local.py +21 -0
  82. fractal_server/runner/config/_slurm.py +129 -0
  83. fractal_server/runner/config/slurm_mem_to_MB.py +63 -0
  84. fractal_server/runner/exceptions.py +4 -0
  85. fractal_server/runner/executors/base_runner.py +17 -7
  86. fractal_server/runner/executors/local/get_local_config.py +21 -86
  87. fractal_server/runner/executors/local/runner.py +48 -5
  88. fractal_server/runner/executors/slurm_common/_batching.py +2 -2
  89. fractal_server/runner/executors/slurm_common/base_slurm_runner.py +60 -26
  90. fractal_server/runner/executors/slurm_common/get_slurm_config.py +39 -55
  91. fractal_server/runner/executors/slurm_common/remote.py +1 -1
  92. fractal_server/runner/executors/slurm_common/slurm_config.py +214 -0
  93. fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -1
  94. fractal_server/runner/executors/slurm_ssh/runner.py +12 -14
  95. fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +2 -2
  96. fractal_server/runner/executors/slurm_sudo/runner.py +12 -12
  97. fractal_server/runner/v2/_local.py +36 -21
  98. fractal_server/runner/v2/_slurm_ssh.py +41 -4
  99. fractal_server/runner/v2/_slurm_sudo.py +42 -12
  100. fractal_server/runner/v2/db_tools.py +1 -1
  101. fractal_server/runner/v2/runner.py +3 -11
  102. fractal_server/runner/v2/runner_functions.py +42 -28
  103. fractal_server/runner/v2/submit_workflow.py +88 -109
  104. fractal_server/runner/versions.py +8 -3
  105. fractal_server/ssh/_fabric.py +6 -6
  106. fractal_server/tasks/config/__init__.py +3 -0
  107. fractal_server/tasks/config/_pixi.py +127 -0
  108. fractal_server/tasks/config/_python.py +51 -0
  109. fractal_server/tasks/v2/local/_utils.py +7 -7
  110. fractal_server/tasks/v2/local/collect.py +13 -5
  111. fractal_server/tasks/v2/local/collect_pixi.py +26 -10
  112. fractal_server/tasks/v2/local/deactivate.py +7 -1
  113. fractal_server/tasks/v2/local/deactivate_pixi.py +5 -1
  114. fractal_server/tasks/v2/local/delete.py +5 -1
  115. fractal_server/tasks/v2/local/reactivate.py +13 -5
  116. fractal_server/tasks/v2/local/reactivate_pixi.py +27 -9
  117. fractal_server/tasks/v2/ssh/_pixi_slurm_ssh.py +11 -10
  118. fractal_server/tasks/v2/ssh/_utils.py +6 -7
  119. fractal_server/tasks/v2/ssh/collect.py +19 -12
  120. fractal_server/tasks/v2/ssh/collect_pixi.py +34 -16
  121. fractal_server/tasks/v2/ssh/deactivate.py +12 -8
  122. fractal_server/tasks/v2/ssh/deactivate_pixi.py +14 -10
  123. fractal_server/tasks/v2/ssh/delete.py +12 -9
  124. fractal_server/tasks/v2/ssh/reactivate.py +18 -12
  125. fractal_server/tasks/v2/ssh/reactivate_pixi.py +36 -17
  126. fractal_server/tasks/v2/templates/4_pip_show.sh +4 -6
  127. fractal_server/tasks/v2/utils_database.py +2 -2
  128. fractal_server/tasks/v2/utils_pixi.py +3 -0
  129. fractal_server/tasks/v2/utils_python_interpreter.py +8 -16
  130. fractal_server/tasks/v2/utils_templates.py +7 -10
  131. fractal_server/utils.py +1 -1
  132. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/METADATA +8 -10
  133. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/RECORD +137 -118
  134. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/WHEEL +1 -1
  135. fractal_server/app/routes/aux/validate_user_settings.py +0 -73
  136. fractal_server/app/schemas/user_settings.py +0 -67
  137. fractal_server/app/user_settings.py +0 -42
  138. fractal_server/config.py +0 -906
  139. fractal_server/data_migrations/2_14_10.py +0 -48
  140. fractal_server/runner/executors/slurm_common/_slurm_config.py +0 -471
  141. /fractal_server/{runner → app}/shutdown.py +0 -0
  142. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/entry_points.txt +0 -0
  143. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,339 @@
1
+ import json
2
+ import logging
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from dotenv.main import DotEnv
8
+ from pydantic import BaseModel
9
+ from sqlalchemy.orm import Session
10
+ from sqlalchemy.sql.operators import is_
11
+ from sqlalchemy.sql.operators import is_not
12
+ from sqlmodel import select
13
+
14
+ from fractal_server.app.db import get_sync_db
15
+ from fractal_server.app.models import Profile
16
+ from fractal_server.app.models import ProjectV2
17
+ from fractal_server.app.models import Resource
18
+ from fractal_server.app.models import TaskGroupV2
19
+ from fractal_server.app.models import UserOAuth
20
+ from fractal_server.app.models import UserSettings
21
+ from fractal_server.app.schemas.v2.profile import cast_serialize_profile
22
+ from fractal_server.app.schemas.v2.resource import cast_serialize_resource
23
+ from fractal_server.config import get_settings
24
+ from fractal_server.runner.config import JobRunnerConfigLocal
25
+ from fractal_server.runner.config import JobRunnerConfigSLURM
26
+ from fractal_server.tasks.config import TasksPixiSettings
27
+ from fractal_server.tasks.config import TasksPythonSettings
28
+ from fractal_server.types import AbsolutePathStr
29
+ from fractal_server.types import ListUniqueNonEmptyString
30
+ from fractal_server.urls import normalize_url
31
+
32
+ logging.basicConfig(level=logging.INFO)
33
+
34
+
35
+ class UserUpdateInfo(BaseModel):
36
+ user_id: int
37
+ project_dir: AbsolutePathStr
38
+ slurm_accounts: ListUniqueNonEmptyString
39
+
40
+
41
+ class ProfileUsersUpdateInfo(BaseModel):
42
+ data: dict[str, Any]
43
+ user_updates: list[UserUpdateInfo]
44
+
45
+
46
+ def _get_user_settings(user: UserOAuth, db: Session) -> UserSettings:
47
+ if user.user_settings_id is None:
48
+ sys.exit(f"User {user.email} is active but {user.user_settings_id=}.")
49
+ user_settings = db.get(UserSettings, user.user_settings_id)
50
+ return user_settings
51
+
52
+
53
+ def assert_user_setting_key(
54
+ user: UserOAuth,
55
+ user_settings: UserSettings,
56
+ keys: list[str],
57
+ ) -> None:
58
+ for key in keys:
59
+ if getattr(user_settings, key) is None:
60
+ sys.exit(
61
+ f"User {user.email} is active and verified but their "
62
+ f"user settings have {key}=None."
63
+ )
64
+
65
+
66
+ def prepare_profile_and_user_updates() -> dict[str, ProfileUsersUpdateInfo]:
67
+ settings = get_settings()
68
+ profiles_and_users: dict[str, ProfileUsersUpdateInfo] = {}
69
+ with next(get_sync_db()) as db:
70
+ # Get active&verified users
71
+ res = db.execute(
72
+ select(UserOAuth)
73
+ .where(is_(UserOAuth.is_active, True))
74
+ .where(is_(UserOAuth.is_verified, True))
75
+ .order_by(UserOAuth.id)
76
+ )
77
+ for user in res.unique().scalars().all():
78
+ # Get user settings
79
+ user_settings = _get_user_settings(user=user, db=db)
80
+ assert_user_setting_key(user, user_settings, ["project_dir"])
81
+
82
+ # Prepare profile data and user update
83
+ new_profile_data = dict()
84
+ if settings.FRACTAL_RUNNER_BACKEND == "local":
85
+ username = None
86
+ if settings.FRACTAL_RUNNER_BACKEND == "slurm_sudo":
87
+ assert_user_setting_key(user, user_settings, ["slurm_user"])
88
+ username = user_settings.slurm_user
89
+ elif settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
90
+ assert_user_setting_key(
91
+ user,
92
+ user_settings,
93
+ [
94
+ "ssh_username",
95
+ "ssh_private_key_path",
96
+ "ssh_tasks_dir",
97
+ "ssh_jobs_dir",
98
+ ],
99
+ )
100
+ username = user_settings.ssh_username
101
+ new_profile_data.update(
102
+ ssh_key_path=user_settings.ssh_private_key_path,
103
+ tasks_remote_dir=normalize_url(
104
+ user_settings.ssh_tasks_dir
105
+ ),
106
+ jobs_remote_dir=normalize_url(user_settings.ssh_jobs_dir),
107
+ )
108
+
109
+ new_profile_data.update(
110
+ name=f"Profile {username}",
111
+ username=username,
112
+ resource_type=settings.FRACTAL_RUNNER_BACKEND,
113
+ )
114
+ cast_serialize_profile(new_profile_data)
115
+
116
+ user_update_info = UserUpdateInfo(
117
+ user_id=user.id,
118
+ project_dir=normalize_url(user_settings.project_dir),
119
+ slurm_accounts=user_settings.slurm_accounts or [],
120
+ )
121
+
122
+ if username in profiles_and_users.keys():
123
+ if profiles_and_users[username].data != new_profile_data:
124
+ error_msg = (
125
+ "Profile data mismatch.\n"
126
+ f"{profiles_and_users[username].data=}\n"
127
+ f"{new_profile_data=}"
128
+ )
129
+ logging.error(error_msg)
130
+ sys.exit(error_msg)
131
+ profiles_and_users[username].user_updates.append(
132
+ user_update_info
133
+ )
134
+ else:
135
+ profiles_and_users[username] = ProfileUsersUpdateInfo(
136
+ data=new_profile_data,
137
+ user_updates=[user_update_info],
138
+ )
139
+
140
+ return profiles_and_users
141
+
142
+
143
+ def get_old_dotenv_variables() -> dict[str, str | None]:
144
+ """
145
+ See
146
+ https://github.com/fractal-analytics-platform/fractal-server/blob/2.16.x/fractal_server/config.py
147
+ """
148
+ OLD_DOTENV_FILE = ".fractal_server.env.old"
149
+ return dict(
150
+ **DotEnv(
151
+ dotenv_path=OLD_DOTENV_FILE,
152
+ override=False,
153
+ ).dict()
154
+ )
155
+
156
+
157
+ def get_TasksPythonSettings(
158
+ old_config: dict[str, str | None]
159
+ ) -> dict[str, Any]:
160
+ versions = {}
161
+ for version_underscore in ["3_9", "3_10", "3_11", "3_12"]:
162
+ key = f"FRACTAL_TASKS_PYTHON_{version_underscore}"
163
+ version_dot = version_underscore.replace("_", ".")
164
+ value = old_config.get(key, None)
165
+ if value is not None:
166
+ versions[version_dot] = value
167
+ obj = TasksPythonSettings(
168
+ default_version=old_config["FRACTAL_TASKS_PYTHON_DEFAULT_VERSION"],
169
+ versions=versions,
170
+ pip_cache_dir=old_config.get("FRACTAL_PIP_CACHE_DIR", None),
171
+ )
172
+ return obj.model_dump()
173
+
174
+
175
+ def get_TasksPixiSettings(old_config: dict[str, str | None]) -> dict[str, Any]:
176
+ pixi_file = old_config.get("FRACTAL_PIXI_CONFIG_FILE", None)
177
+ if pixi_file is None:
178
+ return {}
179
+ with open(pixi_file) as f:
180
+ old_pixi_config = json.load(f)
181
+ TasksPixiSettings(**old_pixi_config)
182
+ return old_pixi_config
183
+
184
+
185
+ def get_JobRunnerConfigSLURM(
186
+ old_config: dict[str, str | None]
187
+ ) -> dict[str, Any]:
188
+ slurm_file = old_config["FRACTAL_SLURM_CONFIG_FILE"]
189
+ with open(slurm_file) as f:
190
+ old_slurm_config = json.load(f)
191
+ JobRunnerConfigSLURM(**old_slurm_config)
192
+ return old_slurm_config
193
+
194
+
195
+ def get_JobRunnerConfigLocal(
196
+ old_config: dict[str, str | None]
197
+ ) -> dict[str, Any]:
198
+ local_file = old_config.get("FRACTAL_LOCAL_CONFIG_FILE", None)
199
+ if local_file is None or not Path(local_file).exists():
200
+ return JobRunnerConfigLocal().model_dump()
201
+ else:
202
+ with open(local_file) as f:
203
+ old_local_config = json.load(f)
204
+ JobRunnerConfigLocal(**old_local_config)
205
+ return old_local_config
206
+
207
+
208
+ def get_ssh_host() -> str:
209
+ with next(get_sync_db()) as db:
210
+ res = db.execute(
211
+ select(UserSettings.ssh_host).where(
212
+ is_not(UserSettings.ssh_host, None)
213
+ )
214
+ )
215
+ hosts = res.scalars().all()
216
+ if len(set(hosts)) > 1:
217
+ host = max(set(hosts), key=hosts.count)
218
+ print(f"MOST FREQUENT HOST: {host}")
219
+ else:
220
+ host = hosts[0]
221
+ return host
222
+
223
+
224
+ def prepare_resource_data(old_config: dict[str, str | None]) -> dict[str, Any]:
225
+ settings = get_settings()
226
+
227
+ resource_data = dict(
228
+ type=settings.FRACTAL_RUNNER_BACKEND,
229
+ name="Resource Name",
230
+ tasks_python_config=get_TasksPythonSettings(old_config),
231
+ tasks_pixi_config=get_TasksPixiSettings(old_config),
232
+ tasks_local_dir=old_config["FRACTAL_TASKS_DIR"],
233
+ jobs_local_dir=old_config["FRACTAL_RUNNER_WORKING_BASE_DIR"],
234
+ jobs_poll_interval=int(
235
+ old_config.get("FRACTAL_SLURM_POLL_INTERVAL", 15)
236
+ ),
237
+ )
238
+ if settings.FRACTAL_RUNNER_BACKEND == "local":
239
+ resource_data["jobs_runner_config"] = get_JobRunnerConfigLocal(
240
+ old_config
241
+ )
242
+ elif settings.FRACTAL_RUNNER_BACKEND == "slurm_sudo":
243
+ resource_data["jobs_slurm_python_worker"] = old_config[
244
+ "FRACTAL_SLURM_WORKER_PYTHON"
245
+ ]
246
+ resource_data["jobs_runner_config"] = get_JobRunnerConfigSLURM(
247
+ old_config
248
+ )
249
+ else:
250
+ resource_data["jobs_slurm_python_worker"] = old_config[
251
+ "FRACTAL_SLURM_WORKER_PYTHON"
252
+ ]
253
+ resource_data["jobs_runner_config"] = get_JobRunnerConfigSLURM(
254
+ old_config
255
+ )
256
+ resource_data["host"] = get_ssh_host()
257
+
258
+ resource_data = cast_serialize_resource(resource_data)
259
+
260
+ return resource_data
261
+
262
+
263
+ def fix_db():
264
+ logging.info("START preliminary checks.")
265
+
266
+ # Read old env file
267
+ old_config = get_old_dotenv_variables()
268
+
269
+ # Prepare resource data
270
+ logging.info("START prepare_resource_data")
271
+ resource_data = prepare_resource_data(old_config)
272
+ logging.info("END prepare_resource_data")
273
+
274
+ # Prepare profile/users data
275
+ logging.info("START prepare_profile_and_user_updates")
276
+ profile_and_user_updates = prepare_profile_and_user_updates()
277
+ logging.info("END prepare_profile_and_user_updates")
278
+
279
+ logging.info("END preliminary checks.")
280
+ print()
281
+
282
+ with next(get_sync_db()) as db:
283
+ # Create new resource
284
+ resource = Resource(**resource_data)
285
+ db.add(resource)
286
+ db.commit()
287
+ db.refresh(resource)
288
+ db.expunge(resource)
289
+ resource_id = resource.id
290
+ logging.info(f"Created resource with {resource_id=}.")
291
+
292
+ # Update task groups
293
+ res = db.execute(select(TaskGroupV2).order_by(TaskGroupV2.id))
294
+ for taskgroup in res.scalars().all():
295
+ taskgroup.resource_id = resource_id
296
+ db.add(taskgroup)
297
+ db.commit()
298
+ logging.info(f"Set {resource_id=} foreign key for all task groups.")
299
+
300
+ # Update projects
301
+ res = db.execute(select(ProjectV2).order_by(ProjectV2.id))
302
+ for project in res.scalars().all():
303
+ project.resource_id = resource_id
304
+ db.add(project)
305
+ db.commit()
306
+ logging.info(f"Set {resource_id=} foreign key for all projects.")
307
+ print()
308
+
309
+ db.expunge_all()
310
+
311
+ for _, info in profile_and_user_updates.items():
312
+ # Create profile
313
+ profile_data = info.data
314
+ profile_data["resource_id"] = resource_id
315
+ profile = Profile(**profile_data)
316
+ db.add(profile)
317
+ db.commit()
318
+ db.refresh(profile)
319
+ db.expunge(profile)
320
+ profile_id = profile.id
321
+ logging.info(
322
+ f"Created profile '{profile.name}', with {profile.id=}."
323
+ )
324
+
325
+ # Update users
326
+ for user_update in info.user_updates:
327
+ user = db.get(UserOAuth, user_update.user_id)
328
+ user.profile_id = profile_id
329
+ user.project_dir = user_update.project_dir
330
+ user.slurm_accounts = user_update.slurm_accounts
331
+ db.add(user)
332
+ logging.info(f"Updated {user.email} with {user.project_dir=}.")
333
+ logging.info(
334
+ f"Associated {user.email} to profile {profile.name}."
335
+ )
336
+ print()
337
+ db.commit()
338
+
339
+ logging.info("END - all ok.")
@@ -16,7 +16,7 @@ def find_image_by_zarr_url(
16
16
  """
17
17
  Return a copy of the image with a given zarr_url, and its positional index.
18
18
 
19
- Arguments:
19
+ Args:
20
20
  images: List of images.
21
21
  zarr_url: Path that the returned image must have.
22
22
 
@@ -40,7 +40,7 @@ def match_filter(
40
40
  """
41
41
  Find whether an image matches a filter set.
42
42
 
43
- Arguments:
43
+ Args:
44
44
  image: A single image.
45
45
  type_filters:
46
46
  attribute_filters:
@@ -70,7 +70,7 @@ def filter_image_list(
70
70
  """
71
71
  Compute a sublist with images that match a filter set.
72
72
 
73
- Arguments:
73
+ Args:
74
74
  images: A list of images.
75
75
  type_filters:
76
76
  attribute_filters:
fractal_server/logger.py CHANGED
@@ -44,7 +44,7 @@ def get_logger(logger_name: str | None = None) -> logging.Logger:
44
44
  close_logger(logger)
45
45
  ```
46
46
 
47
- Arguments:
47
+ Args:
48
48
  logger_name: Name of logger
49
49
  Returns:
50
50
  Logger with name `logger_name`
@@ -124,7 +124,7 @@ def close_logger(logger: logging.Logger) -> None:
124
124
  """
125
125
  Close all handlers associated to a `logging.Logger` object
126
126
 
127
- Arguments:
127
+ Args:
128
128
  logger: The actual logger
129
129
  """
130
130
  for handle in logger.handlers:
@@ -135,7 +135,7 @@ def reset_logger_handlers(logger: logging.Logger) -> None:
135
135
  """
136
136
  Close and remove all handlers associated to a `logging.Logger` object
137
137
 
138
- Arguments:
138
+ Args:
139
139
  logger: The actual logger
140
140
  """
141
141
  close_logger(logger)
fractal_server/main.py CHANGED
@@ -1,34 +1,22 @@
1
- # Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
2
- # University of Zurich
3
- #
4
- # Original authors:
5
- # Jacopo Nespolo <jacopo.nespolo@exact-lab.it>
6
- # Marco Franzon <marco.franzon@exact-lab.it>
7
- # Tommaso Comaprin <tommaso.comparin@exact-lab.it>
8
- #
9
- # This file is part of Fractal and was originally developed by eXact lab S.r.l.
10
- # <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
11
- # Institute for Biomedical Research and Pelkmans Lab from the University of
12
- # Zurich.
13
- """
14
- # Application factory
15
-
16
- This module sets up the FastAPI application that serves the Fractal Server.
17
- """
18
1
  import os
19
2
  from contextlib import asynccontextmanager
3
+ from itertools import chain
20
4
 
21
5
  from fastapi import FastAPI
22
6
 
23
7
  from .app.routes.aux._runner import _backend_supports_shutdown
8
+ from .app.shutdown import cleanup_after_shutdown
9
+ from .config import get_data_settings
10
+ from .config import get_db_settings
11
+ from .config import get_email_settings
24
12
  from .config import get_settings
25
13
  from .logger import config_uvicorn_loggers
26
14
  from .logger import get_logger
27
15
  from .logger import reset_logger_handlers
28
16
  from .logger import set_logger
29
- from .runner.shutdown import cleanup_after_shutdown
30
17
  from .syringe import Inject
31
18
  from fractal_server import __VERSION__
19
+ from fractal_server.app.schemas.v2 import ResourceType
32
20
 
33
21
 
34
22
  def collect_routers(app: FastAPI) -> None:
@@ -63,11 +51,17 @@ def check_settings() -> None:
63
51
  ValidationError: If the configuration is invalid.
64
52
  """
65
53
  settings = Inject(get_settings)
66
- settings.check()
67
-
54
+ db_settings = Inject(get_db_settings)
55
+ email_settings = Inject(get_email_settings)
56
+ data_settings = Inject(get_data_settings)
68
57
  logger = set_logger("fractal_server_settings")
69
58
  logger.debug("Fractal Settings:")
70
- for key, value in settings.model_dump().items():
59
+ for key, value in chain(
60
+ db_settings.model_dump().items(),
61
+ settings.model_dump().items(),
62
+ email_settings.model_dump().items(),
63
+ data_settings.model_dump().items(),
64
+ ):
71
65
  if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
72
66
  value = "*****"
73
67
  logger.debug(f" {key}: {value}")
@@ -82,7 +76,7 @@ async def lifespan(app: FastAPI):
82
76
  check_settings()
83
77
  settings = Inject(get_settings)
84
78
 
85
- if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
79
+ if settings.FRACTAL_RUNNER_BACKEND == ResourceType.SLURM_SSH:
86
80
  from fractal_server.ssh._fabric import FractalSSHList
87
81
 
88
82
  app.state.fractal_ssh_list = FractalSSHList()
@@ -103,7 +97,7 @@ async def lifespan(app: FastAPI):
103
97
  logger = get_logger("fractal_server.lifespan")
104
98
  logger.info("[teardown] START")
105
99
 
106
- if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
100
+ if settings.FRACTAL_RUNNER_BACKEND == ResourceType.SLURM_SSH:
107
101
  logger.info(
108
102
  "[teardown] Close FractalSSH connections "
109
103
  f"(current size: {app.state.fractal_ssh_list.size})."
@@ -1,7 +1,7 @@
1
1
  NAMING_CONVENTION = {
2
2
  "ix": "ix_%(column_0_label)s",
3
3
  "uq": "uq_%(table_name)s_%(column_0_name)s",
4
- "ck": "ck_%(table_name)s_`%(constraint_name)s`",
4
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
5
5
  "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
6
6
  "pk": "pk_%(table_name)s",
7
7
  }
@@ -0,0 +1,195 @@
1
+ """2.17.0
2
+
3
+ Revision ID: 83bc2ad3ffcc
4
+ Revises: 981d588fe248
5
+ Create Date: 2025-10-30 14:16:53.639006
6
+
7
+ """
8
+ import sqlalchemy as sa
9
+ import sqlmodel
10
+ from alembic import op
11
+ from sqlalchemy.dialects import postgresql
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = "83bc2ad3ffcc"
15
+ down_revision = "981d588fe248"
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ # ### commands auto generated by Alembic - please adjust! ###
22
+ op.create_table(
23
+ "resource",
24
+ sa.Column("id", sa.Integer(), nullable=False),
25
+ sa.Column("type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
26
+ sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
27
+ sa.Column(
28
+ "timestamp_created", sa.DateTime(timezone=True), nullable=False
29
+ ),
30
+ sa.Column("host", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
31
+ sa.Column(
32
+ "jobs_local_dir",
33
+ sqlmodel.sql.sqltypes.AutoString(),
34
+ nullable=False,
35
+ ),
36
+ sa.Column(
37
+ "jobs_runner_config",
38
+ postgresql.JSONB(astext_type=sa.Text()),
39
+ server_default="{}",
40
+ nullable=False,
41
+ ),
42
+ sa.Column(
43
+ "jobs_slurm_python_worker",
44
+ sqlmodel.sql.sqltypes.AutoString(),
45
+ nullable=True,
46
+ ),
47
+ sa.Column("jobs_poll_interval", sa.Integer(), nullable=False),
48
+ sa.Column(
49
+ "tasks_local_dir",
50
+ sqlmodel.sql.sqltypes.AutoString(),
51
+ nullable=False,
52
+ ),
53
+ sa.Column(
54
+ "tasks_python_config",
55
+ postgresql.JSONB(astext_type=sa.Text()),
56
+ server_default="{}",
57
+ nullable=False,
58
+ ),
59
+ sa.Column(
60
+ "tasks_pixi_config",
61
+ postgresql.JSONB(astext_type=sa.Text()),
62
+ server_default="{}",
63
+ nullable=False,
64
+ ),
65
+ sa.CheckConstraint(
66
+ "(type = 'local') OR (jobs_slurm_python_worker IS NOT NULL)",
67
+ name=op.f("ck_resource_jobs_slurm_python_worker_set"),
68
+ ),
69
+ sa.CheckConstraint(
70
+ "type IN ('local', 'slurm_sudo', 'slurm_ssh')",
71
+ name=op.f("ck_resource_correct_type"),
72
+ ),
73
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_resource")),
74
+ sa.UniqueConstraint("name", name=op.f("uq_resource_name")),
75
+ )
76
+ op.create_table(
77
+ "profile",
78
+ sa.Column("id", sa.Integer(), nullable=False),
79
+ sa.Column("resource_id", sa.Integer(), nullable=False),
80
+ sa.Column(
81
+ "resource_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False
82
+ ),
83
+ sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
84
+ sa.Column(
85
+ "username", sqlmodel.sql.sqltypes.AutoString(), nullable=True
86
+ ),
87
+ sa.Column(
88
+ "ssh_key_path", sqlmodel.sql.sqltypes.AutoString(), nullable=True
89
+ ),
90
+ sa.Column(
91
+ "jobs_remote_dir",
92
+ sqlmodel.sql.sqltypes.AutoString(),
93
+ nullable=True,
94
+ ),
95
+ sa.Column(
96
+ "tasks_remote_dir",
97
+ sqlmodel.sql.sqltypes.AutoString(),
98
+ nullable=True,
99
+ ),
100
+ sa.ForeignKeyConstraint(
101
+ ["resource_id"],
102
+ ["resource.id"],
103
+ name=op.f("fk_profile_resource_id_resource"),
104
+ ondelete="RESTRICT",
105
+ ),
106
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_profile")),
107
+ sa.UniqueConstraint("name", name=op.f("uq_profile_name")),
108
+ )
109
+ with op.batch_alter_table("projectv2", schema=None) as batch_op:
110
+ batch_op.add_column(
111
+ sa.Column("resource_id", sa.Integer(), nullable=True)
112
+ )
113
+ batch_op.create_foreign_key(
114
+ batch_op.f("fk_projectv2_resource_id_resource"),
115
+ "resource",
116
+ ["resource_id"],
117
+ ["id"],
118
+ ondelete="RESTRICT",
119
+ )
120
+
121
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
122
+ batch_op.add_column(
123
+ sa.Column("resource_id", sa.Integer(), nullable=True)
124
+ )
125
+ batch_op.create_foreign_key(
126
+ batch_op.f("fk_taskgroupv2_resource_id_resource"),
127
+ "resource",
128
+ ["resource_id"],
129
+ ["id"],
130
+ ondelete="RESTRICT",
131
+ )
132
+
133
+ with op.batch_alter_table("user_oauth", schema=None) as batch_op:
134
+ batch_op.add_column(
135
+ sa.Column("profile_id", sa.Integer(), nullable=True)
136
+ )
137
+ batch_op.add_column(
138
+ sa.Column(
139
+ "project_dir",
140
+ sa.String(),
141
+ server_default="/PLACEHOLDER",
142
+ nullable=False,
143
+ )
144
+ )
145
+ batch_op.add_column(
146
+ sa.Column(
147
+ "slurm_accounts",
148
+ postgresql.ARRAY(sa.String()),
149
+ server_default="{}",
150
+ nullable=True,
151
+ )
152
+ )
153
+ batch_op.create_foreign_key(
154
+ batch_op.f("fk_user_oauth_profile_id_profile"),
155
+ "profile",
156
+ ["profile_id"],
157
+ ["id"],
158
+ ondelete="RESTRICT",
159
+ )
160
+ batch_op.drop_column("username")
161
+
162
+ # ### end Alembic commands ###
163
+
164
+
165
+ def downgrade() -> None:
166
+ # ### commands auto generated by Alembic - please adjust! ###
167
+ with op.batch_alter_table("user_oauth", schema=None) as batch_op:
168
+ batch_op.add_column(
169
+ sa.Column(
170
+ "username", sa.VARCHAR(), autoincrement=False, nullable=True
171
+ )
172
+ )
173
+ batch_op.drop_constraint(
174
+ batch_op.f("fk_user_oauth_profile_id_profile"), type_="foreignkey"
175
+ )
176
+ batch_op.drop_column("slurm_accounts")
177
+ batch_op.drop_column("project_dir")
178
+ batch_op.drop_column("profile_id")
179
+
180
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
181
+ batch_op.drop_constraint(
182
+ batch_op.f("fk_taskgroupv2_resource_id_resource"),
183
+ type_="foreignkey",
184
+ )
185
+ batch_op.drop_column("resource_id")
186
+
187
+ with op.batch_alter_table("projectv2", schema=None) as batch_op:
188
+ batch_op.drop_constraint(
189
+ batch_op.f("fk_projectv2_resource_id_resource"), type_="foreignkey"
190
+ )
191
+ batch_op.drop_column("resource_id")
192
+
193
+ op.drop_table("profile")
194
+ op.drop_table("resource")
195
+ # ### end Alembic commands ###
@@ -0,0 +1,2 @@
1
+ from ._local import JobRunnerConfigLocal # noqa F401
2
+ from ._slurm import JobRunnerConfigSLURM # noqa F401