fractal-server 2.12.0a1__py3-none-any.whl → 2.13.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 +17 -63
- fractal_server/app/models/security.py +9 -12
- fractal_server/app/models/v2/dataset.py +2 -2
- fractal_server/app/models/v2/job.py +11 -9
- fractal_server/app/models/v2/task.py +2 -3
- fractal_server/app/models/v2/task_group.py +6 -2
- fractal_server/app/models/v2/workflowtask.py +15 -8
- fractal_server/app/routes/admin/v2/task.py +1 -1
- fractal_server/app/routes/admin/v2/task_group.py +1 -1
- fractal_server/app/routes/api/v2/dataset.py +4 -4
- fractal_server/app/routes/api/v2/images.py +11 -23
- fractal_server/app/routes/api/v2/project.py +2 -2
- fractal_server/app/routes/api/v2/status.py +1 -1
- fractal_server/app/routes/api/v2/submit.py +8 -6
- fractal_server/app/routes/api/v2/task.py +4 -2
- fractal_server/app/routes/api/v2/task_collection.py +3 -2
- fractal_server/app/routes/api/v2/task_group.py +2 -2
- fractal_server/app/routes/api/v2/workflow.py +3 -3
- fractal_server/app/routes/api/v2/workflow_import.py +3 -3
- fractal_server/app/routes/api/v2/workflowtask.py +3 -1
- fractal_server/app/routes/auth/_aux_auth.py +4 -1
- fractal_server/app/routes/auth/current_user.py +3 -5
- fractal_server/app/routes/auth/group.py +1 -1
- fractal_server/app/routes/auth/users.py +2 -4
- fractal_server/app/routes/aux/_runner.py +1 -1
- fractal_server/app/routes/aux/validate_user_settings.py +1 -2
- fractal_server/app/runner/executors/_job_states.py +13 -0
- fractal_server/app/runner/executors/slurm/_slurm_config.py +26 -18
- fractal_server/app/runner/executors/slurm/ssh/__init__.py +0 -3
- fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +31 -22
- fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +2 -6
- fractal_server/app/runner/executors/slurm/ssh/executor.py +35 -50
- fractal_server/app/runner/executors/slurm/sudo/__init__.py +0 -3
- fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +1 -2
- fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +37 -47
- fractal_server/app/runner/executors/slurm/sudo/executor.py +77 -41
- fractal_server/app/runner/v2/__init__.py +0 -9
- fractal_server/app/runner/v2/_local/_local_config.py +5 -4
- fractal_server/app/runner/v2/_slurm_common/get_slurm_config.py +4 -4
- fractal_server/app/runner/v2/_slurm_sudo/__init__.py +2 -2
- fractal_server/app/runner/v2/deduplicate_list.py +1 -1
- fractal_server/app/runner/v2/runner.py +9 -4
- fractal_server/app/runner/v2/task_interface.py +15 -7
- fractal_server/app/schemas/_filter_validators.py +6 -3
- fractal_server/app/schemas/_validators.py +7 -5
- fractal_server/app/schemas/user.py +23 -18
- fractal_server/app/schemas/user_group.py +25 -11
- fractal_server/app/schemas/user_settings.py +31 -24
- fractal_server/app/schemas/v2/dataset.py +48 -35
- fractal_server/app/schemas/v2/dumps.py +16 -14
- fractal_server/app/schemas/v2/job.py +49 -29
- fractal_server/app/schemas/v2/manifest.py +32 -28
- fractal_server/app/schemas/v2/project.py +18 -8
- fractal_server/app/schemas/v2/task.py +86 -75
- fractal_server/app/schemas/v2/task_collection.py +41 -30
- fractal_server/app/schemas/v2/task_group.py +39 -20
- fractal_server/app/schemas/v2/workflow.py +24 -12
- fractal_server/app/schemas/v2/workflowtask.py +63 -61
- fractal_server/app/security/__init__.py +7 -4
- fractal_server/app/security/signup_email.py +21 -12
- fractal_server/config.py +123 -75
- fractal_server/images/models.py +18 -12
- fractal_server/main.py +13 -10
- fractal_server/migrations/env.py +16 -63
- fractal_server/tasks/v2/local/collect.py +9 -8
- fractal_server/tasks/v2/local/deactivate.py +3 -0
- fractal_server/tasks/v2/local/reactivate.py +3 -0
- fractal_server/tasks/v2/ssh/collect.py +8 -8
- fractal_server/tasks/v2/ssh/deactivate.py +3 -0
- fractal_server/tasks/v2/ssh/reactivate.py +9 -6
- fractal_server/tasks/v2/utils_background.py +1 -1
- fractal_server/tasks/v2/utils_database.py +1 -1
- {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/METADATA +10 -11
- {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/RECORD +78 -81
- fractal_server/app/runner/v2/_local_experimental/__init__.py +0 -121
- fractal_server/app/runner/v2/_local_experimental/_local_config.py +0 -108
- fractal_server/app/runner/v2/_local_experimental/_submit_setup.py +0 -42
- fractal_server/app/runner/v2/_local_experimental/executor.py +0 -157
- {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/entry_points.txt +0 -0
fractal_server/config.py
CHANGED
@@ -11,7 +11,6 @@
|
|
11
11
|
# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
|
12
12
|
# Institute for Biomedical Research and Pelkmans Lab from the University of
|
13
13
|
# Zurich.
|
14
|
-
import json
|
15
14
|
import logging
|
16
15
|
import shutil
|
17
16
|
import sys
|
@@ -25,11 +24,12 @@ from typing import TypeVar
|
|
25
24
|
from cryptography.fernet import Fernet
|
26
25
|
from dotenv import load_dotenv
|
27
26
|
from pydantic import BaseModel
|
28
|
-
from pydantic import BaseSettings
|
29
27
|
from pydantic import EmailStr
|
30
28
|
from pydantic import Field
|
31
|
-
from pydantic import
|
32
|
-
from pydantic import
|
29
|
+
from pydantic import field_validator
|
30
|
+
from pydantic import model_validator
|
31
|
+
from pydantic_settings import BaseSettings
|
32
|
+
from pydantic_settings import SettingsConfigDict
|
33
33
|
from sqlalchemy.engine import URL
|
34
34
|
|
35
35
|
import fractal_server
|
@@ -46,16 +46,19 @@ class MailSettings(BaseModel):
|
|
46
46
|
port: SMTP server port
|
47
47
|
password: Sender password
|
48
48
|
instance_name: Name of SMTP server instance
|
49
|
-
use_starttls:
|
49
|
+
use_starttls: Whether to use the security protocol
|
50
|
+
use_login: Whether to use login
|
50
51
|
"""
|
51
52
|
|
52
53
|
sender: EmailStr
|
53
|
-
recipients: list[EmailStr] = Field(
|
54
|
+
recipients: list[EmailStr] = Field(min_length=1)
|
54
55
|
smtp_server: str
|
55
56
|
port: int
|
56
|
-
|
57
|
+
encrypted_password: Optional[str] = None
|
58
|
+
encryption_key: Optional[str] = None
|
57
59
|
instance_name: str
|
58
60
|
use_starttls: bool
|
61
|
+
use_login: bool
|
59
62
|
|
60
63
|
|
61
64
|
class FractalConfigurationError(RuntimeError):
|
@@ -95,10 +98,11 @@ class OAuthClientConfig(BaseModel):
|
|
95
98
|
CLIENT_NAME: str
|
96
99
|
CLIENT_ID: str
|
97
100
|
CLIENT_SECRET: str
|
98
|
-
OIDC_CONFIGURATION_ENDPOINT: Optional[str]
|
101
|
+
OIDC_CONFIGURATION_ENDPOINT: Optional[str] = None
|
99
102
|
REDIRECT_URL: Optional[str] = None
|
100
103
|
|
101
|
-
@
|
104
|
+
@model_validator(mode="before")
|
105
|
+
@classmethod
|
102
106
|
def check_configuration(cls, values):
|
103
107
|
if values.get("CLIENT_NAME") not in ["GOOGLE", "GITHUB"]:
|
104
108
|
if not values.get("OIDC_CONFIGURATION_ENDPOINT"):
|
@@ -116,8 +120,7 @@ class Settings(BaseSettings):
|
|
116
120
|
The attributes of this class are set from the environment.
|
117
121
|
"""
|
118
122
|
|
119
|
-
|
120
|
-
case_sensitive = True
|
123
|
+
model_config = SettingsConfigDict(case_sensitive=True)
|
121
124
|
|
122
125
|
PROJECT_NAME: str = "Fractal Server"
|
123
126
|
PROJECT_VERSION: str = fractal_server.__VERSION__
|
@@ -134,7 +137,7 @@ class Settings(BaseSettings):
|
|
134
137
|
JWT token lifetime, in seconds.
|
135
138
|
"""
|
136
139
|
|
137
|
-
JWT_SECRET_KEY: Optional[str]
|
140
|
+
JWT_SECRET_KEY: Optional[str] = None
|
138
141
|
"""
|
139
142
|
JWT secret
|
140
143
|
|
@@ -148,7 +151,8 @@ class Settings(BaseSettings):
|
|
148
151
|
Cookie token lifetime, in seconds.
|
149
152
|
"""
|
150
153
|
|
151
|
-
@
|
154
|
+
@model_validator(mode="before")
|
155
|
+
@classmethod
|
152
156
|
def collect_oauth_clients(cls, values):
|
153
157
|
"""
|
154
158
|
Automatic collection of OAuth Clients
|
@@ -196,11 +200,11 @@ class Settings(BaseSettings):
|
|
196
200
|
"""
|
197
201
|
If `True`, make database operations verbose.
|
198
202
|
"""
|
199
|
-
POSTGRES_USER: Optional[str]
|
203
|
+
POSTGRES_USER: Optional[str] = None
|
200
204
|
"""
|
201
205
|
User to use when connecting to the PostgreSQL database.
|
202
206
|
"""
|
203
|
-
POSTGRES_PASSWORD: Optional[str]
|
207
|
+
POSTGRES_PASSWORD: Optional[str] = None
|
204
208
|
"""
|
205
209
|
Password to use when connecting to the PostgreSQL database.
|
206
210
|
"""
|
@@ -212,7 +216,7 @@ class Settings(BaseSettings):
|
|
212
216
|
"""
|
213
217
|
Port number to use when connecting to the PostgreSQL server.
|
214
218
|
"""
|
215
|
-
POSTGRES_DB: Optional[str]
|
219
|
+
POSTGRES_DB: Optional[str] = None
|
216
220
|
"""
|
217
221
|
Name of the PostgreSQL database to connect to.
|
218
222
|
"""
|
@@ -264,13 +268,14 @@ class Settings(BaseSettings):
|
|
264
268
|
default admin credentials.
|
265
269
|
"""
|
266
270
|
|
267
|
-
FRACTAL_TASKS_DIR: Optional[Path]
|
271
|
+
FRACTAL_TASKS_DIR: Optional[Path] = None
|
268
272
|
"""
|
269
273
|
Directory under which all the tasks will be saved (either an absolute path
|
270
274
|
or a path relative to current working directory).
|
271
275
|
"""
|
272
276
|
|
273
|
-
@
|
277
|
+
@field_validator("FRACTAL_TASKS_DIR")
|
278
|
+
@classmethod
|
274
279
|
def make_FRACTAL_TASKS_DIR_absolute(cls, v):
|
275
280
|
"""
|
276
281
|
If `FRACTAL_TASKS_DIR` is a non-absolute path, make it absolute (based
|
@@ -287,7 +292,8 @@ class Settings(BaseSettings):
|
|
287
292
|
)
|
288
293
|
return FRACTAL_TASKS_DIR_path
|
289
294
|
|
290
|
-
@
|
295
|
+
@field_validator("FRACTAL_RUNNER_WORKING_BASE_DIR")
|
296
|
+
@classmethod
|
291
297
|
def make_FRACTAL_RUNNER_WORKING_BASE_DIR_absolute(cls, v):
|
292
298
|
"""
|
293
299
|
(Copy of make_FRACTAL_TASKS_DIR_absolute)
|
@@ -310,7 +316,6 @@ class Settings(BaseSettings):
|
|
310
316
|
|
311
317
|
FRACTAL_RUNNER_BACKEND: Literal[
|
312
318
|
"local",
|
313
|
-
"local_experimental",
|
314
319
|
"slurm",
|
315
320
|
"slurm_ssh",
|
316
321
|
] = "local"
|
@@ -318,7 +323,7 @@ class Settings(BaseSettings):
|
|
318
323
|
Select which runner backend to use.
|
319
324
|
"""
|
320
325
|
|
321
|
-
FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path]
|
326
|
+
FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path] = None
|
322
327
|
"""
|
323
328
|
Base directory for running jobs / workflows. All artifacts required to set
|
324
329
|
up, run and tear down jobs are placed in subdirs of this directory.
|
@@ -331,7 +336,7 @@ class Settings(BaseSettings):
|
|
331
336
|
Only logs of with this level (or higher) will appear in the console logs.
|
332
337
|
"""
|
333
338
|
|
334
|
-
FRACTAL_LOCAL_CONFIG_FILE: Optional[Path]
|
339
|
+
FRACTAL_LOCAL_CONFIG_FILE: Optional[Path] = None
|
335
340
|
"""
|
336
341
|
Path of JSON file with configuration for the local backend.
|
337
342
|
"""
|
@@ -347,7 +352,7 @@ class Settings(BaseSettings):
|
|
347
352
|
Waiting time for the shutdown phase of executors
|
348
353
|
"""
|
349
354
|
|
350
|
-
FRACTAL_SLURM_CONFIG_FILE: Optional[Path]
|
355
|
+
FRACTAL_SLURM_CONFIG_FILE: Optional[Path] = None
|
351
356
|
"""
|
352
357
|
Path of JSON file with configuration for the SLURM backend.
|
353
358
|
"""
|
@@ -358,7 +363,8 @@ class Settings(BaseSettings):
|
|
358
363
|
nodes. If not specified, the same interpreter that runs the server is used.
|
359
364
|
"""
|
360
365
|
|
361
|
-
@
|
366
|
+
@field_validator("FRACTAL_SLURM_WORKER_PYTHON")
|
367
|
+
@classmethod
|
362
368
|
def absolute_FRACTAL_SLURM_WORKER_PYTHON(cls, v):
|
363
369
|
"""
|
364
370
|
If `FRACTAL_SLURM_WORKER_PYTHON` is a relative path, fail.
|
@@ -405,8 +411,9 @@ class Settings(BaseSettings):
|
|
405
411
|
Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.12.
|
406
412
|
"""
|
407
413
|
|
408
|
-
@
|
409
|
-
|
414
|
+
@model_validator(mode="before")
|
415
|
+
@classmethod
|
416
|
+
def check_tasks_python(cls, values):
|
410
417
|
"""
|
411
418
|
Perform multiple checks of the Python-interpreter variables.
|
412
419
|
|
@@ -416,7 +423,6 @@ class Settings(BaseSettings):
|
|
416
423
|
`sys.executable` and set the corresponding
|
417
424
|
`FRACTAL_TASKS_PYTHON_X_Y` (and unset all others).
|
418
425
|
"""
|
419
|
-
|
420
426
|
# `FRACTAL_TASKS_PYTHON_X_Y` variables can only be absolute paths
|
421
427
|
for version in ["3_9", "3_10", "3_11", "3_12"]:
|
422
428
|
key = f"FRACTAL_TASKS_PYTHON_{version}"
|
@@ -510,7 +516,8 @@ class Settings(BaseSettings):
|
|
510
516
|
`--no-cache-dir` is used.
|
511
517
|
"""
|
512
518
|
|
513
|
-
@
|
519
|
+
@field_validator("FRACTAL_PIP_CACHE_DIR")
|
520
|
+
@classmethod
|
514
521
|
def absolute_FRACTAL_PIP_CACHE_DIR(cls, v):
|
515
522
|
"""
|
516
523
|
If `FRACTAL_PIP_CACHE_DIR` is a relative path, fail.
|
@@ -575,66 +582,108 @@ class Settings(BaseSettings):
|
|
575
582
|
###########################################################################
|
576
583
|
# SMTP SERVICE
|
577
584
|
###########################################################################
|
578
|
-
|
585
|
+
|
586
|
+
FRACTAL_EMAIL_SENDER: Optional[EmailStr] = None
|
587
|
+
"""
|
588
|
+
Address of the OAuth-signup email sender.
|
589
|
+
"""
|
590
|
+
FRACTAL_EMAIL_PASSWORD: Optional[str] = None
|
579
591
|
"""
|
580
|
-
|
581
|
-
`smtp_server`, `port`, `instance_name`, `use_starttls`.
|
592
|
+
Password for the OAuth-signup email sender.
|
582
593
|
"""
|
583
|
-
|
594
|
+
FRACTAL_EMAIL_PASSWORD_KEY: Optional[str] = None
|
584
595
|
"""
|
585
596
|
Key value for `cryptography.fernet` decrypt
|
586
597
|
"""
|
598
|
+
FRACTAL_EMAIL_SMTP_SERVER: Optional[str] = None
|
599
|
+
"""
|
600
|
+
SMPT server for the OAuth-signup emails.
|
601
|
+
"""
|
602
|
+
FRACTAL_EMAIL_SMTP_PORT: Optional[int] = None
|
603
|
+
"""
|
604
|
+
SMPT server port for the OAuth-signup emails.
|
605
|
+
"""
|
606
|
+
FRACTAL_EMAIL_INSTANCE_NAME: Optional[str] = None
|
607
|
+
"""
|
608
|
+
Fractal instance name, to be included in the OAuth-signup emails.
|
609
|
+
"""
|
587
610
|
FRACTAL_EMAIL_RECIPIENTS: Optional[str] = None
|
588
611
|
"""
|
589
|
-
|
612
|
+
Comma-separated list of recipients of the OAuth-signup emails.
|
613
|
+
"""
|
614
|
+
FRACTAL_EMAIL_USE_STARTTLS: Optional[bool] = True
|
615
|
+
"""
|
616
|
+
Whether to use StartTLS when using the SMTP server.
|
590
617
|
"""
|
618
|
+
FRACTAL_EMAIL_USE_LOGIN: Optional[bool] = True
|
619
|
+
"""
|
620
|
+
Whether to use login when using the SMTP server.
|
621
|
+
"""
|
622
|
+
email_settings: Optional[MailSettings] = None
|
591
623
|
|
592
|
-
@
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
)
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
624
|
+
@model_validator(mode="before")
|
625
|
+
@classmethod
|
626
|
+
def validate_email_settings(cls, values):
|
627
|
+
email_values = {
|
628
|
+
k: v for k, v in values.items() if k.startswith("FRACTAL_EMAIL")
|
629
|
+
}
|
630
|
+
if email_values:
|
631
|
+
|
632
|
+
def assert_key(key: str):
|
633
|
+
if key not in email_values:
|
634
|
+
raise ValueError(f"Missing '{key}'")
|
635
|
+
|
636
|
+
assert_key("FRACTAL_EMAIL_SENDER")
|
637
|
+
assert_key("FRACTAL_EMAIL_SMTP_SERVER")
|
638
|
+
assert_key("FRACTAL_EMAIL_SMTP_PORT")
|
639
|
+
assert_key("FRACTAL_EMAIL_INSTANCE_NAME")
|
640
|
+
assert_key("FRACTAL_EMAIL_RECIPIENTS")
|
641
|
+
|
642
|
+
if email_values.get("FRACTAL_EMAIL_USE_LOGIN", True):
|
643
|
+
if "FRACTAL_EMAIL_PASSWORD" not in email_values:
|
644
|
+
raise ValueError(
|
645
|
+
"'FRACTAL_EMAIL_USE_LOGIN' is True but "
|
646
|
+
"'FRACTAL_EMAIL_PASSWORD' is not provided."
|
647
|
+
)
|
648
|
+
elif "FRACTAL_EMAIL_PASSWORD_KEY" not in email_values:
|
649
|
+
raise ValueError(
|
650
|
+
"'FRACTAL_EMAIL_USE_LOGIN' is True but "
|
651
|
+
"'FRACTAL_EMAIL_PASSWORD_KEY' is not provided."
|
652
|
+
)
|
653
|
+
else:
|
654
|
+
try:
|
655
|
+
(
|
656
|
+
Fernet(email_values["FRACTAL_EMAIL_PASSWORD_KEY"])
|
657
|
+
.decrypt(email_values["FRACTAL_EMAIL_PASSWORD"])
|
658
|
+
.decode("utf-8")
|
659
|
+
)
|
660
|
+
except Exception as e:
|
661
|
+
raise ValueError(
|
662
|
+
"Invalid pair (FRACTAL_EMAIL_PASSWORD, "
|
663
|
+
"FRACTAL_EMAIL_PASSWORD_KEY). "
|
664
|
+
f"Original error: {str(e)}."
|
665
|
+
)
|
666
|
+
|
667
|
+
values["email_settings"] = MailSettings(
|
668
|
+
sender=email_values["FRACTAL_EMAIL_SENDER"],
|
669
|
+
recipients=email_values["FRACTAL_EMAIL_RECIPIENTS"].split(","),
|
670
|
+
smtp_server=email_values["FRACTAL_EMAIL_SMTP_SERVER"],
|
671
|
+
port=email_values["FRACTAL_EMAIL_SMTP_PORT"],
|
672
|
+
encrypted_password=email_values.get("FRACTAL_EMAIL_PASSWORD"),
|
673
|
+
encryption_key=email_values.get("FRACTAL_EMAIL_PASSWORD_KEY"),
|
674
|
+
instance_name=email_values["FRACTAL_EMAIL_INSTANCE_NAME"],
|
675
|
+
use_starttls=email_values.get(
|
676
|
+
"FRACTAL_EMAIL_USE_STARTTLS", True
|
677
|
+
),
|
678
|
+
use_login=email_values.get("FRACTAL_EMAIL_USE_LOGIN", True),
|
621
679
|
)
|
622
680
|
|
681
|
+
return values
|
682
|
+
|
623
683
|
###########################################################################
|
624
684
|
# BUSINESS LOGIC
|
625
685
|
###########################################################################
|
626
686
|
|
627
|
-
def check_fractal_mail_settings(self):
|
628
|
-
"""
|
629
|
-
Checks that the mail settings are properly set.
|
630
|
-
"""
|
631
|
-
try:
|
632
|
-
self.MAIL_SETTINGS
|
633
|
-
except Exception as e:
|
634
|
-
raise FractalConfigurationError(
|
635
|
-
f"Invalid email configuration settings. Original error: {e}"
|
636
|
-
)
|
637
|
-
|
638
687
|
def check_db(self) -> None:
|
639
688
|
"""
|
640
689
|
Checks that db environment variables are properly set.
|
@@ -740,7 +789,6 @@ class Settings(BaseSettings):
|
|
740
789
|
|
741
790
|
self.check_db()
|
742
791
|
self.check_runner()
|
743
|
-
self.check_fractal_mail_settings()
|
744
792
|
|
745
793
|
def get_sanitized(self) -> dict:
|
746
794
|
def _must_be_sanitized(string) -> bool:
|
@@ -753,7 +801,7 @@ class Settings(BaseSettings):
|
|
753
801
|
return False
|
754
802
|
|
755
803
|
sanitized_settings = {}
|
756
|
-
for k, v in self.
|
804
|
+
for k, v in self.model_dump().items():
|
757
805
|
if _must_be_sanitized(k):
|
758
806
|
sanitized_settings[k] = "***"
|
759
807
|
else:
|
fractal_server/images/models.py
CHANGED
@@ -4,7 +4,7 @@ from typing import Union
|
|
4
4
|
|
5
5
|
from pydantic import BaseModel
|
6
6
|
from pydantic import Field
|
7
|
-
from pydantic import
|
7
|
+
from pydantic import field_validator
|
8
8
|
|
9
9
|
from fractal_server.app.schemas._validators import valdict_keys
|
10
10
|
from fractal_server.urls import normalize_url
|
@@ -30,16 +30,18 @@ class _SingleImageBase(BaseModel):
|
|
30
30
|
types: dict[str, bool] = Field(default_factory=dict)
|
31
31
|
|
32
32
|
# Validators
|
33
|
-
_attributes =
|
34
|
-
valdict_keys("attributes")
|
33
|
+
_attributes = field_validator("attributes")(
|
34
|
+
classmethod(valdict_keys("attributes"))
|
35
35
|
)
|
36
|
-
_types =
|
36
|
+
_types = field_validator("types")(classmethod(valdict_keys("types")))
|
37
37
|
|
38
|
-
@
|
38
|
+
@field_validator("zarr_url")
|
39
|
+
@classmethod
|
39
40
|
def normalize_zarr_url(cls, v: str) -> str:
|
40
41
|
return normalize_url(v)
|
41
42
|
|
42
|
-
@
|
43
|
+
@field_validator("origin")
|
44
|
+
@classmethod
|
43
45
|
def normalize_orig(cls, v: Optional[str]) -> Optional[str]:
|
44
46
|
if v is not None:
|
45
47
|
return normalize_url(v)
|
@@ -50,7 +52,8 @@ class SingleImageTaskOutput(_SingleImageBase):
|
|
50
52
|
`SingleImageBase`, with scalar `attributes` values (`None` included).
|
51
53
|
"""
|
52
54
|
|
53
|
-
@
|
55
|
+
@field_validator("attributes")
|
56
|
+
@classmethod
|
54
57
|
def validate_attributes(
|
55
58
|
cls, v: dict[str, Any]
|
56
59
|
) -> dict[str, Union[int, float, str, bool, None]]:
|
@@ -69,7 +72,8 @@ class SingleImage(_SingleImageBase):
|
|
69
72
|
`SingleImageBase`, with scalar `attributes` values (`None` excluded).
|
70
73
|
"""
|
71
74
|
|
72
|
-
@
|
75
|
+
@field_validator("attributes")
|
76
|
+
@classmethod
|
73
77
|
def validate_attributes(
|
74
78
|
cls, v: dict[str, Any]
|
75
79
|
) -> dict[str, Union[int, float, str, bool]]:
|
@@ -87,17 +91,19 @@ class SingleImageUpdate(BaseModel):
|
|
87
91
|
attributes: Optional[dict[str, Any]] = None
|
88
92
|
types: Optional[dict[str, bool]] = None
|
89
93
|
|
90
|
-
@
|
94
|
+
@field_validator("zarr_url")
|
95
|
+
@classmethod
|
91
96
|
def normalize_zarr_url(cls, v: str) -> str:
|
92
97
|
return normalize_url(v)
|
93
98
|
|
94
|
-
@
|
99
|
+
@field_validator("attributes")
|
100
|
+
@classmethod
|
95
101
|
def validate_attributes(
|
96
102
|
cls, v: dict[str, Any]
|
97
103
|
) -> dict[str, Union[int, float, str, bool]]:
|
98
104
|
if v is not None:
|
99
105
|
# validate keys
|
100
|
-
valdict_keys("attributes")(v)
|
106
|
+
valdict_keys("attributes")(cls, v)
|
101
107
|
# validate values
|
102
108
|
for key, value in v.items():
|
103
109
|
if not isinstance(value, (int, float, str, bool)):
|
@@ -108,4 +114,4 @@ class SingleImageUpdate(BaseModel):
|
|
108
114
|
)
|
109
115
|
return v
|
110
116
|
|
111
|
-
_types =
|
117
|
+
_types = field_validator("types")(classmethod(valdict_keys("types")))
|
fractal_server/main.py
CHANGED
@@ -28,6 +28,7 @@ from .logger import get_logger
|
|
28
28
|
from .logger import reset_logger_handlers
|
29
29
|
from .logger import set_logger
|
30
30
|
from .syringe import Inject
|
31
|
+
from fractal_server import __VERSION__
|
31
32
|
|
32
33
|
|
33
34
|
def collect_routers(app: FastAPI) -> None:
|
@@ -66,8 +67,8 @@ def check_settings() -> None:
|
|
66
67
|
|
67
68
|
logger = set_logger("fractal_server_settings")
|
68
69
|
logger.debug("Fractal Settings:")
|
69
|
-
for key, value in settings.
|
70
|
-
if any(s in key.upper() for s in ["PASSWORD", "SECRET"]):
|
70
|
+
for key, value in settings.model_dump().items():
|
71
|
+
if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
|
71
72
|
value = "*****"
|
72
73
|
logger.debug(f" {key}: {value}")
|
73
74
|
reset_logger_handlers(logger)
|
@@ -77,7 +78,7 @@ def check_settings() -> None:
|
|
77
78
|
async def lifespan(app: FastAPI):
|
78
79
|
app.state.jobsV2 = []
|
79
80
|
logger = set_logger("fractal_server.lifespan")
|
80
|
-
logger.info("
|
81
|
+
logger.info(f"[startup] START (fractal-server {__VERSION__})")
|
81
82
|
check_settings()
|
82
83
|
settings = Inject(get_settings)
|
83
84
|
|
@@ -88,31 +89,31 @@ async def lifespan(app: FastAPI):
|
|
88
89
|
app.state.fractal_ssh_list = FractalSSHList()
|
89
90
|
|
90
91
|
logger.info(
|
91
|
-
"Added empty FractalSSHList to app.state "
|
92
|
+
"[startup] Added empty FractalSSHList to app.state "
|
92
93
|
f"(id={id(app.state.fractal_ssh_list)})."
|
93
94
|
)
|
94
95
|
else:
|
95
96
|
app.state.fractal_ssh_list = None
|
96
97
|
|
97
98
|
config_uvicorn_loggers()
|
98
|
-
logger.info("
|
99
|
+
logger.info("[startup] END")
|
99
100
|
reset_logger_handlers(logger)
|
100
101
|
|
101
102
|
yield
|
102
103
|
|
103
104
|
logger = get_logger("fractal_server.lifespan")
|
104
|
-
logger.info("
|
105
|
+
logger.info("[teardown] START")
|
105
106
|
|
106
107
|
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
107
108
|
logger.info(
|
108
|
-
"Close FractalSSH connections "
|
109
|
+
"[teardown] Close FractalSSH connections "
|
109
110
|
f"(current size: {app.state.fractal_ssh_list.size})."
|
110
111
|
)
|
111
112
|
|
112
113
|
app.state.fractal_ssh_list.close_all()
|
113
114
|
|
114
115
|
logger.info(
|
115
|
-
f"Current worker with pid {os.getpid()} is shutting down. "
|
116
|
+
f"[teardown] Current worker with pid {os.getpid()} is shutting down. "
|
116
117
|
f"Current jobs: {app.state.jobsV2=}"
|
117
118
|
)
|
118
119
|
if _backend_supports_shutdown(settings.FRACTAL_RUNNER_BACKEND):
|
@@ -128,9 +129,11 @@ async def lifespan(app: FastAPI):
|
|
128
129
|
f"Original error: {e}"
|
129
130
|
)
|
130
131
|
else:
|
131
|
-
logger.info(
|
132
|
+
logger.info(
|
133
|
+
"[teardown] Shutdown not available for this backend runner."
|
134
|
+
)
|
132
135
|
|
133
|
-
logger.info("
|
136
|
+
logger.info("[teardown] END")
|
134
137
|
reset_logger_handlers(logger)
|
135
138
|
|
136
139
|
|
fractal_server/migrations/env.py
CHANGED
@@ -1,16 +1,11 @@
|
|
1
|
-
import asyncio
|
2
1
|
from logging.config import fileConfig
|
3
2
|
|
4
3
|
from alembic import context
|
5
|
-
from sqlalchemy.engine import Connection
|
6
4
|
from sqlmodel import SQLModel
|
7
5
|
|
8
|
-
from fractal_server.config import get_settings
|
9
6
|
from fractal_server.migrations.naming_convention import NAMING_CONVENTION
|
10
|
-
from fractal_server.syringe import Inject
|
11
7
|
|
12
|
-
#
|
13
|
-
# access to the values within the .ini file in use.
|
8
|
+
# Alembic Config object (provides access to the values within the .ini file)
|
14
9
|
config = context.config
|
15
10
|
|
16
11
|
|
@@ -20,77 +15,35 @@ if config.config_file_name is not None:
|
|
20
15
|
fileConfig(config.config_file_name)
|
21
16
|
|
22
17
|
|
23
|
-
# add your model's MetaData object here
|
24
|
-
# for 'autogenerate' support
|
25
|
-
# from myapp import mymodel
|
26
|
-
# target_metadata = mymodel.Base.metadata
|
27
18
|
target_metadata = SQLModel.metadata
|
28
19
|
target_metadata.naming_convention = NAMING_CONVENTION
|
29
|
-
# Importing `fractal_server.app.models` after defining
|
20
|
+
# Importing `fractal_server.app.models` *after* defining
|
30
21
|
# `SQLModel.metadata.naming_convention` in order to apply the naming convention
|
31
22
|
# when autogenerating migrations (see issue #1819).
|
32
23
|
from fractal_server.app import models # noqa
|
33
24
|
|
34
|
-
# other values from the config, defined by the needs of env.py,
|
35
|
-
# can be acquired:
|
36
|
-
# my_important_option = config.get_main_option("my_important_option")
|
37
|
-
# ... etc.
|
38
|
-
|
39
|
-
|
40
|
-
def run_migrations_offline() -> None:
|
41
|
-
"""Run migrations in 'offline' mode.
|
42
|
-
|
43
|
-
This configures the context with just a URL
|
44
|
-
and not an Engine, though an Engine is acceptable
|
45
|
-
here as well. By skipping the Engine creation
|
46
|
-
we don't even need a DBAPI to be available.
|
47
|
-
|
48
|
-
Calls to context.execute() here emit the given string to the
|
49
|
-
script output.
|
50
25
|
|
26
|
+
def run_migrations_online() -> None:
|
51
27
|
"""
|
52
|
-
|
53
|
-
settings.check_db()
|
54
|
-
context.configure(
|
55
|
-
url=settings.DATABASE_ASYNC_URL,
|
56
|
-
target_metadata=target_metadata,
|
57
|
-
literal_binds=True,
|
58
|
-
dialect_opts={"paramstyle": "named"},
|
59
|
-
render_as_batch=True,
|
60
|
-
)
|
61
|
-
|
62
|
-
with context.begin_transaction():
|
63
|
-
context.run_migrations()
|
64
|
-
|
65
|
-
|
66
|
-
def do_run_migrations(connection: Connection) -> None:
|
67
|
-
context.configure(
|
68
|
-
connection=connection,
|
69
|
-
target_metadata=target_metadata,
|
70
|
-
render_as_batch=True,
|
71
|
-
)
|
72
|
-
|
73
|
-
with context.begin_transaction():
|
74
|
-
context.run_migrations()
|
75
|
-
|
76
|
-
|
77
|
-
async def run_migrations_online() -> None:
|
78
|
-
"""Run migrations in 'online' mode.
|
28
|
+
Run migrations in 'online' mode.
|
79
29
|
|
80
30
|
In this scenario we need to create an Engine
|
81
31
|
and associate a connection with the context.
|
82
|
-
|
83
32
|
"""
|
84
33
|
from fractal_server.app.db import DB
|
85
34
|
|
86
|
-
engine = DB.
|
87
|
-
|
88
|
-
|
35
|
+
engine = DB.engine_sync()
|
36
|
+
with engine.connect() as connection:
|
37
|
+
context.configure(
|
38
|
+
connection=connection,
|
39
|
+
target_metadata=target_metadata,
|
40
|
+
render_as_batch=True,
|
41
|
+
)
|
42
|
+
|
43
|
+
with context.begin_transaction():
|
44
|
+
context.run_migrations()
|
89
45
|
|
90
|
-
|
46
|
+
engine.dispose()
|
91
47
|
|
92
48
|
|
93
|
-
|
94
|
-
run_migrations_offline()
|
95
|
-
else:
|
96
|
-
asyncio.run(run_migrations_online())
|
49
|
+
run_migrations_online()
|