fractal-server 2.16.6__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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +178 -52
- fractal_server/app/db/__init__.py +9 -11
- fractal_server/app/models/security.py +30 -22
- fractal_server/app/models/user_settings.py +5 -4
- fractal_server/app/models/v2/__init__.py +4 -0
- fractal_server/app/models/v2/profile.py +16 -0
- fractal_server/app/models/v2/project.py +5 -0
- fractal_server/app/models/v2/resource.py +130 -0
- fractal_server/app/models/v2/task_group.py +4 -0
- fractal_server/app/routes/admin/v2/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/_aux_functions.py +55 -0
- fractal_server/app/routes/admin/v2/accounting.py +3 -3
- fractal_server/app/routes/admin/v2/impersonate.py +2 -2
- fractal_server/app/routes/admin/v2/job.py +51 -15
- fractal_server/app/routes/admin/v2/profile.py +100 -0
- fractal_server/app/routes/admin/v2/project.py +2 -2
- fractal_server/app/routes/admin/v2/resource.py +222 -0
- fractal_server/app/routes/admin/v2/task.py +59 -32
- fractal_server/app/routes/admin/v2/task_group.py +17 -12
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +52 -86
- fractal_server/app/routes/api/__init__.py +45 -8
- fractal_server/app/routes/api/v2/_aux_functions.py +17 -1
- fractal_server/app/routes/api/v2/_aux_functions_history.py +2 -2
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +55 -19
- fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +21 -17
- fractal_server/app/routes/api/v2/dataset.py +10 -19
- fractal_server/app/routes/api/v2/history.py +8 -8
- fractal_server/app/routes/api/v2/images.py +5 -5
- fractal_server/app/routes/api/v2/job.py +8 -8
- fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
- fractal_server/app/routes/api/v2/project.py +15 -7
- fractal_server/app/routes/api/v2/status_legacy.py +2 -2
- fractal_server/app/routes/api/v2/submit.py +49 -42
- fractal_server/app/routes/api/v2/task.py +26 -8
- fractal_server/app/routes/api/v2/task_collection.py +39 -50
- fractal_server/app/routes/api/v2/task_collection_custom.py +10 -6
- fractal_server/app/routes/api/v2/task_collection_pixi.py +34 -42
- fractal_server/app/routes/api/v2/task_group.py +19 -9
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +43 -86
- fractal_server/app/routes/api/v2/task_version_update.py +3 -3
- fractal_server/app/routes/api/v2/workflow.py +9 -9
- fractal_server/app/routes/api/v2/workflow_import.py +25 -13
- fractal_server/app/routes/api/v2/workflowtask.py +5 -5
- fractal_server/app/routes/auth/__init__.py +34 -5
- fractal_server/app/routes/auth/_aux_auth.py +39 -20
- fractal_server/app/routes/auth/current_user.py +56 -67
- fractal_server/app/routes/auth/group.py +29 -46
- fractal_server/app/routes/auth/oauth.py +55 -38
- fractal_server/app/routes/auth/register.py +2 -2
- fractal_server/app/routes/auth/router.py +4 -2
- fractal_server/app/routes/auth/users.py +29 -53
- fractal_server/app/routes/aux/_runner.py +2 -1
- fractal_server/app/routes/aux/validate_user_profile.py +62 -0
- fractal_server/app/schemas/__init__.py +0 -1
- fractal_server/app/schemas/user.py +43 -13
- fractal_server/app/schemas/user_group.py +2 -1
- fractal_server/app/schemas/v2/__init__.py +12 -0
- fractal_server/app/schemas/v2/profile.py +78 -0
- fractal_server/app/schemas/v2/resource.py +137 -0
- fractal_server/app/schemas/v2/task_collection.py +11 -3
- fractal_server/app/schemas/v2/task_group.py +5 -0
- fractal_server/app/security/__init__.py +174 -75
- fractal_server/app/security/signup_email.py +52 -34
- fractal_server/config/__init__.py +27 -0
- fractal_server/config/_data.py +68 -0
- fractal_server/config/_database.py +59 -0
- fractal_server/config/_email.py +133 -0
- fractal_server/config/_main.py +78 -0
- fractal_server/config/_oauth.py +69 -0
- fractal_server/config/_settings_config.py +7 -0
- fractal_server/data_migrations/2_17_0.py +339 -0
- fractal_server/images/tools.py +3 -3
- fractal_server/logger.py +3 -3
- fractal_server/main.py +17 -23
- fractal_server/migrations/naming_convention.py +1 -1
- fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +195 -0
- fractal_server/runner/config/__init__.py +2 -0
- fractal_server/runner/config/_local.py +21 -0
- fractal_server/runner/config/_slurm.py +129 -0
- fractal_server/runner/config/slurm_mem_to_MB.py +63 -0
- fractal_server/runner/exceptions.py +4 -0
- fractal_server/runner/executors/base_runner.py +17 -7
- fractal_server/runner/executors/local/get_local_config.py +21 -86
- fractal_server/runner/executors/local/runner.py +48 -5
- fractal_server/runner/executors/slurm_common/_batching.py +2 -2
- fractal_server/runner/executors/slurm_common/base_slurm_runner.py +60 -26
- fractal_server/runner/executors/slurm_common/get_slurm_config.py +39 -55
- fractal_server/runner/executors/slurm_common/remote.py +1 -1
- fractal_server/runner/executors/slurm_common/slurm_config.py +214 -0
- fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -1
- fractal_server/runner/executors/slurm_ssh/runner.py +12 -14
- fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +2 -2
- fractal_server/runner/executors/slurm_sudo/runner.py +12 -12
- fractal_server/runner/v2/_local.py +36 -21
- fractal_server/runner/v2/_slurm_ssh.py +41 -4
- fractal_server/runner/v2/_slurm_sudo.py +42 -12
- fractal_server/runner/v2/db_tools.py +1 -1
- fractal_server/runner/v2/runner.py +3 -11
- fractal_server/runner/v2/runner_functions.py +42 -28
- fractal_server/runner/v2/submit_workflow.py +88 -109
- fractal_server/runner/versions.py +8 -3
- fractal_server/ssh/_fabric.py +6 -6
- fractal_server/tasks/config/__init__.py +3 -0
- fractal_server/tasks/config/_pixi.py +127 -0
- fractal_server/tasks/config/_python.py +51 -0
- fractal_server/tasks/v2/local/_utils.py +7 -7
- fractal_server/tasks/v2/local/collect.py +13 -5
- fractal_server/tasks/v2/local/collect_pixi.py +26 -10
- fractal_server/tasks/v2/local/deactivate.py +7 -1
- fractal_server/tasks/v2/local/deactivate_pixi.py +5 -1
- fractal_server/tasks/v2/local/delete.py +5 -1
- fractal_server/tasks/v2/local/reactivate.py +13 -5
- fractal_server/tasks/v2/local/reactivate_pixi.py +27 -9
- fractal_server/tasks/v2/ssh/_pixi_slurm_ssh.py +11 -10
- fractal_server/tasks/v2/ssh/_utils.py +6 -7
- fractal_server/tasks/v2/ssh/collect.py +19 -12
- fractal_server/tasks/v2/ssh/collect_pixi.py +34 -16
- fractal_server/tasks/v2/ssh/deactivate.py +12 -8
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +14 -10
- fractal_server/tasks/v2/ssh/delete.py +12 -9
- fractal_server/tasks/v2/ssh/reactivate.py +18 -12
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +36 -17
- fractal_server/tasks/v2/templates/4_pip_show.sh +4 -6
- fractal_server/tasks/v2/utils_database.py +2 -2
- fractal_server/tasks/v2/utils_pixi.py +3 -0
- fractal_server/tasks/v2/utils_python_interpreter.py +8 -16
- fractal_server/tasks/v2/utils_templates.py +7 -10
- fractal_server/utils.py +1 -1
- {fractal_server-2.16.6.dist-info → fractal_server-2.17.0.dist-info}/METADATA +4 -6
- {fractal_server-2.16.6.dist-info → fractal_server-2.17.0.dist-info}/RECORD +136 -117
- fractal_server/app/routes/aux/validate_user_settings.py +0 -73
- fractal_server/app/schemas/user_settings.py +0 -67
- fractal_server/app/user_settings.py +0 -42
- fractal_server/config.py +0 -906
- fractal_server/data_migrations/2_14_10.py +0 -48
- fractal_server/runner/executors/slurm_common/_slurm_config.py +0 -471
- /fractal_server/{runner → app}/shutdown.py +0 -0
- {fractal_server-2.16.6.dist-info → fractal_server-2.17.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.16.6.dist-info → fractal_server-2.17.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.16.6.dist-info → fractal_server-2.17.0.dist-info}/licenses/LICENSE +0 -0
fractal_server/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__VERSION__ = "2.
|
|
1
|
+
__VERSION__ = "2.17.0"
|
fractal_server/__main__.py
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
110
|
+
def set_db():
|
|
87
111
|
"""
|
|
88
|
-
Upgrade database
|
|
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
|
|
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
|
-
#
|
|
106
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
15
|
-
from
|
|
16
|
-
from
|
|
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
|
-
|
|
46
|
-
settings.check_db()
|
|
45
|
+
db_settings = Inject(get_db_settings)
|
|
47
46
|
|
|
48
47
|
cls._engine_async = create_async_engine(
|
|
49
|
-
|
|
50
|
-
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
|
-
|
|
64
|
-
settings.check_db()
|
|
62
|
+
db_settings = Inject(get_db_settings)
|
|
65
63
|
|
|
66
64
|
cls._engine_sync = create_engine(
|
|
67
|
-
|
|
68
|
-
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
|
-
|
|
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},
|
|
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
|
-
|
|
101
|
-
foreign_key="
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
]
|
|
@@ -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
|
|
@@ -6,7 +6,9 @@ from fastapi import APIRouter
|
|
|
6
6
|
from .accounting import router as accounting_router
|
|
7
7
|
from .impersonate import router as impersonate_router
|
|
8
8
|
from .job import router as job_router
|
|
9
|
+
from .profile import router as profile_router
|
|
9
10
|
from .project import router as project_router
|
|
11
|
+
from .resource import router as resource_router
|
|
10
12
|
from .task import router as task_router
|
|
11
13
|
from .task_group import router as task_group_router
|
|
12
14
|
from .task_group_lifecycle import router as task_group_lifecycle_router
|
|
@@ -22,3 +24,5 @@ router_admin_v2.include_router(
|
|
|
22
24
|
task_group_lifecycle_router, prefix="/task-group"
|
|
23
25
|
)
|
|
24
26
|
router_admin_v2.include_router(impersonate_router, prefix="/impersonate")
|
|
27
|
+
router_admin_v2.include_router(resource_router, prefix="/resource")
|
|
28
|
+
router_admin_v2.include_router(profile_router, prefix="/profile")
|