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
@@ -1 +1 @@
1
- __VERSION__ = "2.16.5"
1
+ __VERSION__ = "2.17.0"
@@ -2,8 +2,10 @@ import argparse as ap
2
2
  import asyncio
3
3
  import json
4
4
  import sys
5
+ from pathlib import Path
5
6
 
6
7
  import uvicorn
8
+ from pydantic import ValidationError
7
9
 
8
10
 
9
11
  parser = ap.ArgumentParser(description="fractal-server commands")
@@ -50,11 +52,42 @@ set_db_parser = subparsers.add_parser(
50
52
  "Initialise/upgrade database schemas and create first group&user."
51
53
  ),
52
54
  )
53
- set_db_parser.add_argument(
54
- "--skip-init-data",
55
- action="store_true",
56
- help="If set, do not try creating first group and user.",
57
- default=False,
55
+
56
+ # fractalctl init-db-data
57
+ init_db_data_parser = subparsers.add_parser(
58
+ "init-db-data",
59
+ description="Populate database with initial data.",
60
+ )
61
+ init_db_data_parser.add_argument(
62
+ "--resource",
63
+ type=str,
64
+ help="Either `default` or path to the JSON file of the first resource.",
65
+ required=False,
66
+ )
67
+ init_db_data_parser.add_argument(
68
+ "--profile",
69
+ type=str,
70
+ help="Either `default` or path to the JSON file of the first profile.",
71
+ required=False,
72
+ )
73
+ init_db_data_parser.add_argument(
74
+ "--admin-email",
75
+ type=str,
76
+ help="Email of the first admin user.",
77
+ required=False,
78
+ )
79
+ init_db_data_parser.add_argument(
80
+ "--admin-pwd",
81
+ type=str,
82
+ help="Password for the first admin user.",
83
+ required=False,
84
+ )
85
+
86
+ init_db_data_parser.add_argument(
87
+ "--admin-project-dir",
88
+ type=str,
89
+ help="Project_dir for the first admin user.",
90
+ required=False,
58
91
  )
59
92
 
60
93
  # fractalctl update-db-data
@@ -63,15 +96,6 @@ update_db_data_parser = subparsers.add_parser(
63
96
  description="Apply data-migration script to an existing database.",
64
97
  )
65
98
 
66
- # fractalctl encrypt-email-password
67
- encrypt_email_password_parser = subparsers.add_parser(
68
- "encrypt-email-password",
69
- description=(
70
- "Generate valid values for environment variables "
71
- "FRACTAL_EMAIL_PASSWORD and FRACTAL_EMAIL_PASSWORD_KEY."
72
- ),
73
- )
74
-
75
99
 
76
100
  def save_openapi(dest="openapi.json"):
77
101
  from fractal_server.main import start_application
@@ -83,28 +107,22 @@ def save_openapi(dest="openapi.json"):
83
107
  json.dump(openapi_schema, f)
84
108
 
85
109
 
86
- def set_db(skip_init_data: bool = False):
110
+ def set_db():
87
111
  """
88
- Upgrade database schema *and* create first group/user
112
+ Upgrade database schemas.
89
113
 
90
114
  Call alembic to upgrade to the latest migration.
91
115
  Ref: https://stackoverflow.com/a/56683030/283972
92
-
93
- Arguments:
94
- skip_init_data: If `True`, skip creation of first group and user.
95
116
  """
96
- from fractal_server.app.security import _create_first_user
97
- from fractal_server.app.security import _create_first_group
98
117
  from fractal_server.syringe import Inject
99
- from fractal_server.config import get_settings
118
+ from fractal_server.config import get_db_settings
100
119
 
101
120
  import alembic.config
102
121
  from pathlib import Path
103
122
  import fractal_server
104
123
 
105
- # Check settings
106
- settings = Inject(get_settings)
107
- settings.check_db()
124
+ # Validate DB settings
125
+ Inject(get_db_settings)
108
126
 
109
127
  # Perform migrations
110
128
  alembic_ini = Path(fractal_server.__file__).parent / "alembic.ini"
@@ -113,25 +131,138 @@ def set_db(skip_init_data: bool = False):
113
131
  alembic.config.main(argv=alembic_args)
114
132
  print("END: alembic.config")
115
133
 
116
- if skip_init_data:
117
- return
134
+
135
+ def init_db_data(
136
+ *,
137
+ resource: str | None = None,
138
+ profile: str | None = None,
139
+ admin_email: str | None = None,
140
+ admin_password: str | None = None,
141
+ admin_project_dir: str | None = None,
142
+ ) -> None:
143
+ from fractal_server.app.security import _create_first_user
144
+ from fractal_server.app.security import _create_first_group
145
+ from fractal_server.app.db import get_sync_db
146
+ from sqlalchemy import select, func
147
+ from fractal_server.app.models.security import UserOAuth
148
+ from fractal_server.app.models import Resource, Profile
149
+ from fractal_server.app.schemas.v2.resource import cast_serialize_resource
150
+ from fractal_server.app.schemas.v2.profile import cast_serialize_profile
151
+ from fractal_server.app.schemas.v2 import ResourceType
118
152
 
119
153
  # Create default group and user
120
154
  print()
121
155
  _create_first_group()
122
156
  print()
123
- asyncio.run(
124
- _create_first_user(
125
- email=settings.FRACTAL_DEFAULT_ADMIN_EMAIL,
126
- password=(
127
- settings.FRACTAL_DEFAULT_ADMIN_PASSWORD.get_secret_value()
128
- ),
129
- username=settings.FRACTAL_DEFAULT_ADMIN_USERNAME,
130
- is_superuser=True,
131
- is_verified=True,
157
+
158
+ # Create admin user if requested
159
+ if not (
160
+ (admin_email is None)
161
+ == (admin_password is None)
162
+ == (admin_project_dir is None)
163
+ ):
164
+ print(
165
+ "You must provide either or or none of `--admin-email`, "
166
+ "`--admin-pwd` and `--admin-project-dir`. Exit."
132
167
  )
133
- )
134
- print()
168
+ sys.exit(1)
169
+ if admin_password and admin_email:
170
+ asyncio.run(
171
+ _create_first_user(
172
+ email=admin_email,
173
+ password=admin_password,
174
+ project_dir=admin_project_dir,
175
+ is_superuser=True,
176
+ is_verified=True,
177
+ )
178
+ )
179
+ print()
180
+
181
+ # Create resource and profile if requested
182
+ if (resource is None) != (profile is None):
183
+ print("You must provide both --resource and --profile. Exit.")
184
+ sys.exit(1)
185
+ if resource and profile:
186
+ with next(get_sync_db()) as db:
187
+ # Preliminary check
188
+ num_resources = db.execute(
189
+ select(func.count(Resource.id))
190
+ ).scalar()
191
+ if num_resources != 0:
192
+ print(f"There exist already {num_resources=} resources. Exit.")
193
+ sys.exit(1)
194
+
195
+ # Get resource/profile data
196
+ if resource == "default":
197
+ _python_version = (
198
+ f"{sys.version_info.major}.{sys.version_info.minor}"
199
+ )
200
+ resource_data = {
201
+ "name": "Local resource",
202
+ "type": ResourceType.LOCAL,
203
+ "jobs_local_dir": (Path.cwd() / "data-jobs").as_posix(),
204
+ "tasks_local_dir": (Path.cwd() / "data-tasks").as_posix(),
205
+ "tasks_python_config": {
206
+ "default_version": _python_version,
207
+ "versions": {
208
+ _python_version: sys.executable,
209
+ },
210
+ },
211
+ "jobs_poll_interval": 0,
212
+ "jobs_runner_config": {},
213
+ "tasks_pixi_config": {},
214
+ }
215
+ print("Prepared default resource data.")
216
+ else:
217
+ with open(resource) as f:
218
+ resource_data = json.load(f)
219
+ print(f"Read resource data from {resource}.")
220
+ if profile == "default":
221
+ profile_data = {
222
+ "resource_type": "local",
223
+ "name": "Local profile",
224
+ }
225
+ print("Prepared default profile data.")
226
+ else:
227
+ with open(profile) as f:
228
+ profile_data = json.load(f)
229
+ print(f"Read profile data from {profile}.")
230
+
231
+ # Validate resource/profile data
232
+ try:
233
+ resource_data = cast_serialize_resource(resource_data)
234
+ except ValidationError as e:
235
+ sys.exit(
236
+ f"ERROR: Invalid resource data.\nOriginal error:\n{str(e)}"
237
+ )
238
+ try:
239
+ profile_data = cast_serialize_profile(profile_data)
240
+ except ValidationError as e:
241
+ sys.exit(
242
+ f"ERROR: Invalid profile data.\nOriginal error:\n{str(e)}"
243
+ )
244
+
245
+ # Create resource/profile db objects
246
+ resource_obj = Resource(**resource_data)
247
+ db.add(resource_obj)
248
+ db.commit()
249
+ db.refresh(resource_obj)
250
+ profile_data["resource_id"] = resource_obj.id
251
+ profile_obj = Profile(**profile_data)
252
+ db.add(profile_obj)
253
+ db.commit()
254
+ db.refresh(profile_obj)
255
+
256
+ # Associate profile to users
257
+ res = db.execute(select(UserOAuth))
258
+ users = res.unique().scalars().all()
259
+ for user in users:
260
+ print(f"Now set profile_id={profile_obj.id} for {user.email}.")
261
+ user.profile_id = profile_obj.id
262
+ db.add(user)
263
+ db.commit()
264
+ db.expunge_all()
265
+ print()
135
266
 
136
267
 
137
268
  def update_db_data():
@@ -202,24 +333,21 @@ def update_db_data():
202
333
  current_update_db_data_module.fix_db()
203
334
 
204
335
 
205
- def print_encrypted_password():
206
- from cryptography.fernet import Fernet
207
-
208
- password = input("Insert email password: ").encode("utf-8")
209
- key = Fernet.generate_key().decode("utf-8")
210
- encrypted_password = Fernet(key).encrypt(password).decode("utf-8")
211
-
212
- print(f"\nFRACTAL_EMAIL_PASSWORD={encrypted_password}")
213
- print(f"FRACTAL_EMAIL_PASSWORD_KEY={key}")
214
-
215
-
216
336
  def run():
217
337
  args = parser.parse_args(sys.argv[1:])
218
338
 
219
339
  if args.cmd == "openapi":
220
340
  save_openapi(dest=args.openapi_file)
221
341
  elif args.cmd == "set-db":
222
- set_db(skip_init_data=args.skip_init_data)
342
+ set_db()
343
+ elif args.cmd == "init-db-data":
344
+ init_db_data(
345
+ resource=args.resource,
346
+ profile=args.profile,
347
+ admin_email=args.admin_email,
348
+ admin_password=args.admin_pwd,
349
+ admin_project_dir=args.admin_project_dir,
350
+ )
223
351
  elif args.cmd == "update-db-data":
224
352
  update_db_data()
225
353
  elif args.cmd == "start":
@@ -229,8 +357,6 @@ def run():
229
357
  port=args.port,
230
358
  reload=args.reload,
231
359
  )
232
- elif args.cmd == "encrypt-email-password":
233
- print_encrypted_password()
234
360
  else:
235
361
  sys.exit(f"Error: invalid command '{args.cmd}'.")
236
362
 
@@ -11,9 +11,9 @@ from sqlalchemy.ext.asyncio import create_async_engine
11
11
  from sqlalchemy.orm import Session as DBSyncSession
12
12
  from sqlalchemy.orm import sessionmaker
13
13
 
14
- from ...config import get_settings
15
- from ...logger import set_logger
16
- from ...syringe import Inject
14
+ from fractal_server.config import get_db_settings
15
+ from fractal_server.logger import set_logger
16
+ from fractal_server.syringe import Inject
17
17
 
18
18
 
19
19
  logger = set_logger(__name__)
@@ -42,12 +42,11 @@ class DB:
42
42
 
43
43
  @classmethod
44
44
  def set_async_db(cls):
45
- settings = Inject(get_settings)
46
- settings.check_db()
45
+ db_settings = Inject(get_db_settings)
47
46
 
48
47
  cls._engine_async = create_async_engine(
49
- settings.DATABASE_ASYNC_URL,
50
- echo=settings.DB_ECHO,
48
+ db_settings.DATABASE_URL,
49
+ echo=db_settings.DB_ECHO,
51
50
  future=True,
52
51
  pool_pre_ping=True,
53
52
  )
@@ -60,12 +59,11 @@ class DB:
60
59
 
61
60
  @classmethod
62
61
  def set_sync_db(cls):
63
- settings = Inject(get_settings)
64
- settings.check_db()
62
+ db_settings = Inject(get_db_settings)
65
63
 
66
64
  cls._engine_sync = create_engine(
67
- settings.DATABASE_SYNC_URL,
68
- echo=settings.DB_ECHO,
65
+ db_settings.DATABASE_URL,
66
+ echo=db_settings.DB_ECHO,
69
67
  future=True,
70
68
  pool_pre_ping=True,
71
69
  )
@@ -1,27 +1,17 @@
1
- # This is based on fastapi_users_db_sqlmodel
2
- # <https://github.com/fastapi-users/fastapi-users-db-sqlmodel>
3
- # Original Copyright
4
- # Copyright 2022 François Voron
5
- # License: MIT
6
- #
7
- # Modified by:
8
- # Tommaso Comparin <tommaso.comparin@exact-lab.it>
9
- #
10
- # Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
11
- # University of Zurich
12
1
  from datetime import datetime
13
2
  from typing import Optional
14
3
 
15
4
  from pydantic import ConfigDict
16
5
  from pydantic import EmailStr
17
6
  from sqlalchemy import Column
7
+ from sqlalchemy import String
8
+ from sqlalchemy.dialects.postgresql import ARRAY
18
9
  from sqlalchemy.dialects.postgresql import JSONB
19
10
  from sqlalchemy.types import DateTime
20
11
  from sqlmodel import Field
21
12
  from sqlmodel import Relationship
22
13
  from sqlmodel import SQLModel
23
14
 
24
- from .user_settings import UserSettings
25
15
  from fractal_server.utils import get_timestamp
26
16
 
27
17
 
@@ -73,37 +63,55 @@ class UserOAuth(SQLModel, table=True):
73
63
  is_active:
74
64
  is_superuser:
75
65
  is_verified:
76
- username:
77
66
  oauth_accounts:
78
- settings:
67
+ profile_id:
68
+ project_dir:
69
+ slurm_accounts:
79
70
  """
80
71
 
72
+ model_config = ConfigDict(from_attributes=True)
73
+
81
74
  __tablename__ = "user_oauth"
82
75
 
83
76
  id: int | None = Field(default=None, primary_key=True)
84
77
 
85
78
  email: EmailStr = Field(
86
- sa_column_kwargs={"unique": True, "index": True}, nullable=False
79
+ sa_column_kwargs={"unique": True, "index": True},
80
+ nullable=False,
87
81
  )
88
82
  hashed_password: str
89
83
  is_active: bool = Field(default=True, nullable=False)
90
84
  is_superuser: bool = Field(default=False, nullable=False)
91
85
  is_verified: bool = Field(default=False, nullable=False)
92
86
 
93
- username: str | None = None
94
-
95
87
  oauth_accounts: list["OAuthAccount"] = Relationship(
96
88
  back_populates="user",
97
89
  sa_relationship_kwargs={"lazy": "joined", "cascade": "all, delete"},
98
90
  )
99
91
 
100
- user_settings_id: int | None = Field(
101
- foreign_key="user_settings.id", default=None
92
+ profile_id: int | None = Field(
93
+ foreign_key="profile.id",
94
+ default=None,
95
+ ondelete="RESTRICT",
96
+ )
97
+
98
+ # TODO-2.17.1: update to `project_dir: str`
99
+ project_dir: str = Field(
100
+ sa_column=Column(
101
+ String,
102
+ server_default="/PLACEHOLDER",
103
+ nullable=False,
104
+ )
102
105
  )
103
- settings: UserSettings | None = Relationship(
104
- sa_relationship_kwargs=dict(lazy="selectin", cascade="all, delete")
106
+ slurm_accounts: list[str] = Field(
107
+ sa_column=Column(ARRAY(String), server_default="{}"),
108
+ )
109
+
110
+ # TODO-2.17.1: remove
111
+ user_settings_id: int | None = Field(
112
+ foreign_key="user_settings.id",
113
+ default=None,
105
114
  )
106
- model_config = ConfigDict(from_attributes=True)
107
115
 
108
116
 
109
117
  class UserGroup(SQLModel, table=True):
@@ -4,6 +4,7 @@ from sqlmodel import Field
4
4
  from sqlmodel import SQLModel
5
5
 
6
6
 
7
+ # TODO-2.17.1: Drop `UserSettings`
7
8
  class UserSettings(SQLModel, table=True):
8
9
  """
9
10
  Comprehensive list of user settings.
@@ -15,8 +16,6 @@ class UserSettings(SQLModel, table=True):
15
16
  ssh_host: SSH-reachable host where a SLURM client is available.
16
17
  ssh_username: User on `ssh_host`.
17
18
  ssh_private_key_path: Path of private SSH key for `ssh_username`.
18
- ssh_tasks_dir: Task-venvs base folder on `ssh_host`.
19
- ssh_jobs_dir: Jobs base folder on `ssh_host`.
20
19
  slurm_user: Local user, to be impersonated via `sudo -u`
21
20
  project_dir: Folder where `slurm_user` can write.
22
21
  """
@@ -30,7 +29,9 @@ class UserSettings(SQLModel, table=True):
30
29
  ssh_host: str | None = None
31
30
  ssh_username: str | None = None
32
31
  ssh_private_key_path: str | None = None
33
- ssh_tasks_dir: str | None = None
34
- ssh_jobs_dir: str | None = None
32
+
35
33
  slurm_user: str | None = None
36
34
  project_dir: str | None = None
35
+
36
+ ssh_tasks_dir: str | None = None
37
+ ssh_jobs_dir: str | None = None
@@ -9,7 +9,9 @@ from .history import HistoryImageCache
9
9
  from .history import HistoryRun
10
10
  from .history import HistoryUnit
11
11
  from .job import JobV2
12
+ from .profile import Profile
12
13
  from .project import ProjectV2
14
+ from .resource import Resource
13
15
  from .task import TaskV2
14
16
  from .task_group import TaskGroupActivityV2
15
17
  from .task_group import TaskGroupV2
@@ -31,4 +33,6 @@ __all__ = [
31
33
  "HistoryRun",
32
34
  "HistoryUnit",
33
35
  "HistoryImageCache",
36
+ "Resource",
37
+ "Profile",
34
38
  ]
@@ -8,9 +8,8 @@ from sqlalchemy.types import DateTime
8
8
  from sqlmodel import Field
9
9
  from sqlmodel import SQLModel
10
10
 
11
- from ....utils import get_timestamp
12
- from ...schemas.v2 import JobStatusTypeV2
13
- from fractal_server.types import AttributeFilters
11
+ from fractal_server.app.schemas.v2 import JobStatusTypeV2
12
+ from fractal_server.utils import get_timestamp
14
13
 
15
14
 
16
15
  class JobV2(SQLModel, table=True):
@@ -57,7 +56,7 @@ class JobV2(SQLModel, table=True):
57
56
  log: str | None = None
58
57
  executor_error_log: str | None = None
59
58
 
60
- attribute_filters: AttributeFilters = Field(
59
+ attribute_filters: dict[str, list[int | float | str | bool]] = Field(
61
60
  sa_column=Column(JSONB, nullable=False, server_default="{}")
62
61
  )
63
62
  type_filters: dict[str, bool] = Field(
@@ -0,0 +1,16 @@
1
+ from sqlmodel import Field
2
+ from sqlmodel import SQLModel
3
+
4
+
5
+ class Profile(SQLModel, table=True):
6
+ id: int | None = Field(default=None, primary_key=True)
7
+ resource_id: int = Field(foreign_key="resource.id", ondelete="RESTRICT")
8
+ resource_type: str
9
+
10
+ name: str = Field(unique=True)
11
+
12
+ username: str | None = None
13
+ ssh_key_path: str | None = None
14
+
15
+ jobs_remote_dir: str | None = None
16
+ tasks_remote_dir: str | None = None
@@ -14,6 +14,11 @@ from fractal_server.utils import get_timestamp
14
14
  class ProjectV2(SQLModel, table=True):
15
15
  id: int | None = Field(default=None, primary_key=True)
16
16
  name: str
17
+
18
+ # TODO-2.17.1: make `resource_id` not nullable
19
+ resource_id: int | None = Field(
20
+ foreign_key="resource.id", default=None, ondelete="RESTRICT"
21
+ )
17
22
  timestamp_created: datetime = Field(
18
23
  default_factory=get_timestamp,
19
24
  sa_column=Column(DateTime(timezone=True), nullable=False),
@@ -0,0 +1,130 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+ from typing import Self
4
+
5
+ from sqlalchemy import Column
6
+ from sqlalchemy.dialects.postgresql import JSONB
7
+ from sqlalchemy.types import DateTime
8
+ from sqlmodel import CheckConstraint
9
+ from sqlmodel import Field
10
+ from sqlmodel import SQLModel
11
+
12
+ from fractal_server.utils import get_timestamp
13
+
14
+
15
+ class Resource(SQLModel, table=True):
16
+ id: int | None = Field(default=None, primary_key=True)
17
+
18
+ type: str
19
+ """
20
+ One of `local`, `slurm_sudo` or `slurm_ssh` - matching with
21
+ `settings.FRACTAL_RUNNER_BACKEND`.
22
+ """
23
+
24
+ name: str = Field(unique=True)
25
+ """
26
+ Resource name.
27
+ """
28
+
29
+ timestamp_created: datetime = Field(
30
+ default_factory=get_timestamp,
31
+ sa_column=Column(DateTime(timezone=True), nullable=False),
32
+ )
33
+ """
34
+ Creation timestamp (autogenerated).
35
+ """
36
+
37
+ host: str | None = None
38
+ """
39
+ Address for ssh connections, when `type="slurm_ssh"`.
40
+ """
41
+
42
+ jobs_local_dir: str
43
+ """
44
+ Base local folder for job subfolders (containing artifacts and logs).
45
+ """
46
+
47
+ jobs_runner_config: dict[str, Any] = Field(
48
+ sa_column=Column(JSONB, nullable=False, server_default="{}")
49
+ )
50
+ """
51
+ Runner configuration, matching one of `JobRunnerConfigLocal` or
52
+ `JobRunnerConfigSLURM` schemas.
53
+ """
54
+
55
+ jobs_slurm_python_worker: str | None = None
56
+ """
57
+ On SLURM deloyments, this is the Python interpreter that runs the
58
+ `fractal-server` worker from within the SLURM jobs.
59
+ """
60
+
61
+ jobs_poll_interval: int
62
+ """
63
+ On SLURM resources: the interval to wait before new `squeue` calls.
64
+ On local resources: ignored.
65
+ """
66
+
67
+ # task_settings
68
+ tasks_local_dir: str
69
+ """
70
+ Base local folder for task-package subfolders.
71
+ """
72
+
73
+ tasks_python_config: dict[str, Any] = Field(
74
+ sa_column=Column(JSONB, nullable=False, server_default="{}")
75
+ )
76
+ """
77
+ Python configuration for task collection. Example:
78
+ ```json
79
+ {
80
+ "default_version": "3.10",
81
+ "versions:{
82
+ "3.10": "/xxx/venv-3.10/bin/python",
83
+ "3.11": "/xxx/venv-3.11/bin/python",
84
+ "3.12": "/xxx/venv-3.12/bin/python"
85
+ }
86
+ }
87
+ ```
88
+ """
89
+
90
+ tasks_pixi_config: dict[str, Any] = Field(
91
+ sa_column=Column(JSONB, nullable=False, server_default="{}")
92
+ )
93
+ """
94
+ Pixi configuration for task collection. Basic example:
95
+ ```json
96
+ {
97
+ "default_version": "0.41.0",
98
+ "versions": {
99
+ "0.40.0": "/xxx/pixi/0.40.0/",
100
+ "0.41.0": "/xxx/pixi/0.41.0/"
101
+ },
102
+ }
103
+ ```
104
+ """
105
+
106
+ @property
107
+ def pip_cache_dir_arg(self: Self) -> str:
108
+ """
109
+ If `pip_cache_dir` is set (in `self.tasks_python_config`), then
110
+ return `--cache_dir /something`; else return `--no-cache-dir`.
111
+ """
112
+ _pip_cache_dir = self.tasks_python_config.get("pip_cache_dir", None)
113
+ if _pip_cache_dir is not None:
114
+ return f"--cache-dir {_pip_cache_dir}"
115
+ else:
116
+ return "--no-cache-dir"
117
+
118
+ # Check constraints
119
+ __table_args__ = (
120
+ # `type` column must be one of "local", "slurm_sudo" or "slurm_ssh"
121
+ CheckConstraint(
122
+ "type IN ('local', 'slurm_sudo', 'slurm_ssh')",
123
+ name="correct_type",
124
+ ),
125
+ # If `type` is not "local", `jobs_slurm_python_worker` must be set
126
+ CheckConstraint(
127
+ "(type = 'local') OR (jobs_slurm_python_worker IS NOT NULL)",
128
+ name="jobs_slurm_python_worker_set",
129
+ ),
130
+ )
@@ -42,6 +42,10 @@ class TaskGroupV2(SQLModel, table=True):
42
42
  user_group_id: int | None = Field(
43
43
  foreign_key="usergroup.id", default=None, ondelete="SET NULL"
44
44
  )
45
+ # TODO-2.17.1: make `resource_id` not nullable
46
+ resource_id: int | None = Field(
47
+ foreign_key="resource.id", default=None, ondelete="RESTRICT"
48
+ )
45
49
 
46
50
  origin: str
47
51
  pkg_name: str