fractal-server 2.12.1__py3-none-any.whl → 2.13.1__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/app/models/security.py +9 -12
- fractal_server/app/models/v2/__init__.py +4 -0
- fractal_server/app/models/v2/accounting.py +35 -0
- 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/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/accounting.py +108 -0
- fractal_server/app/routes/admin/v2/impersonate.py +35 -0
- fractal_server/app/routes/admin/v2/job.py +5 -13
- fractal_server/app/routes/admin/v2/task.py +1 -1
- fractal_server/app/routes/admin/v2/task_group.py +5 -13
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
- fractal_server/app/routes/api/v2/dataset.py +4 -4
- fractal_server/app/routes/api/v2/images.py +11 -11
- 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 +9 -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 +4 -7
- 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/__init__.py +0 -20
- 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 -5
- fractal_server/app/runner/executors/slurm/ssh/executor.py +21 -27
- 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 +25 -24
- fractal_server/app/runner/v2/__init__.py +4 -9
- fractal_server/app/runner/v2/_local/__init__.py +3 -0
- 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_ssh/__init__.py +2 -0
- fractal_server/app/runner/v2/_slurm_sudo/__init__.py +4 -2
- fractal_server/app/runner/v2/deduplicate_list.py +1 -1
- fractal_server/app/runner/v2/runner.py +25 -10
- fractal_server/app/runner/v2/runner_functions.py +12 -11
- 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/__init__.py +1 -0
- fractal_server/app/schemas/v2/accounting.py +18 -0
- 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 +1 -1
- fractal_server/config.py +86 -73
- fractal_server/images/models.py +18 -12
- fractal_server/main.py +1 -1
- fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +57 -0
- fractal_server/tasks/v2/utils_background.py +2 -2
- fractal_server/tasks/v2/utils_database.py +1 -1
- {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/METADATA +9 -10
- {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/RECORD +83 -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.1.dist-info → fractal_server-2.13.1.dist-info}/LICENSE +0 -0
- {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/WHEEL +0 -0
- {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/entry_points.txt +0 -0
@@ -4,10 +4,10 @@ from typing import Optional
|
|
4
4
|
from typing import Union
|
5
5
|
|
6
6
|
from pydantic import BaseModel
|
7
|
-
from pydantic import
|
7
|
+
from pydantic import ConfigDict
|
8
8
|
from pydantic import Field
|
9
|
-
from pydantic import
|
10
|
-
from pydantic import
|
9
|
+
from pydantic import field_validator
|
10
|
+
from pydantic import model_validator
|
11
11
|
|
12
12
|
from .._filter_validators import validate_type_filters
|
13
13
|
from .._validators import root_validate_dict_keys
|
@@ -39,34 +39,35 @@ class WorkflowTaskStatusTypeV2(str, Enum):
|
|
39
39
|
FAILED = "failed"
|
40
40
|
|
41
41
|
|
42
|
-
class WorkflowTaskCreateV2(BaseModel
|
42
|
+
class WorkflowTaskCreateV2(BaseModel):
|
43
|
+
model_config = ConfigDict(extra="forbid")
|
43
44
|
|
44
|
-
meta_non_parallel: Optional[dict[str, Any]]
|
45
|
-
meta_parallel: Optional[dict[str, Any]]
|
46
|
-
args_non_parallel: Optional[dict[str, Any]]
|
47
|
-
args_parallel: Optional[dict[str, Any]]
|
45
|
+
meta_non_parallel: Optional[dict[str, Any]] = None
|
46
|
+
meta_parallel: Optional[dict[str, Any]] = None
|
47
|
+
args_non_parallel: Optional[dict[str, Any]] = None
|
48
|
+
args_parallel: Optional[dict[str, Any]] = None
|
48
49
|
type_filters: dict[str, bool] = Field(default_factory=dict)
|
49
50
|
|
50
51
|
# Validators
|
51
|
-
_dict_keys =
|
52
|
-
root_validate_dict_keys
|
52
|
+
_dict_keys = model_validator(mode="before")(
|
53
|
+
classmethod(root_validate_dict_keys)
|
53
54
|
)
|
54
|
-
_type_filters =
|
55
|
-
validate_type_filters
|
55
|
+
_type_filters = field_validator("type_filters")(
|
56
|
+
classmethod(validate_type_filters)
|
56
57
|
)
|
57
|
-
|
58
|
-
|
59
|
-
valdict_keys("meta_non_parallel")
|
58
|
+
_meta_non_parallel = field_validator("meta_non_parallel")(
|
59
|
+
classmethod(valdict_keys("meta_non_parallel"))
|
60
60
|
)
|
61
|
-
_meta_parallel =
|
62
|
-
valdict_keys("meta_parallel")
|
61
|
+
_meta_parallel = field_validator("meta_parallel")(
|
62
|
+
classmethod(valdict_keys("meta_parallel"))
|
63
63
|
)
|
64
64
|
|
65
|
-
@
|
65
|
+
@field_validator("args_non_parallel")
|
66
|
+
@classmethod
|
66
67
|
def validate_args_non_parallel(cls, value):
|
67
68
|
if value is None:
|
68
69
|
return
|
69
|
-
valdict_keys("args_non_parallel")(value)
|
70
|
+
valdict_keys("args_non_parallel")(cls, value)
|
70
71
|
args_keys = set(value.keys())
|
71
72
|
intersect_keys = RESERVED_ARGUMENTS.intersection(args_keys)
|
72
73
|
if intersect_keys:
|
@@ -76,11 +77,12 @@ class WorkflowTaskCreateV2(BaseModel, extra=Extra.forbid):
|
|
76
77
|
)
|
77
78
|
return value
|
78
79
|
|
79
|
-
@
|
80
|
+
@field_validator("args_parallel")
|
81
|
+
@classmethod
|
80
82
|
def validate_args_parallel(cls, value):
|
81
83
|
if value is None:
|
82
84
|
return
|
83
|
-
valdict_keys("args_parallel")(value)
|
85
|
+
valdict_keys("args_parallel")(cls, value)
|
84
86
|
args_keys = set(value.keys())
|
85
87
|
intersect_keys = RESERVED_ARGUMENTS.intersection(args_keys)
|
86
88
|
if intersect_keys:
|
@@ -99,16 +101,15 @@ class WorkflowTaskReplaceV2(BaseModel):
|
|
99
101
|
|
100
102
|
|
101
103
|
class WorkflowTaskReadV2(BaseModel):
|
102
|
-
|
103
104
|
id: int
|
104
105
|
|
105
106
|
workflow_id: int
|
106
|
-
order: Optional[int]
|
107
|
-
meta_non_parallel: Optional[dict[str, Any]]
|
108
|
-
meta_parallel: Optional[dict[str, Any]]
|
107
|
+
order: Optional[int] = None
|
108
|
+
meta_non_parallel: Optional[dict[str, Any]] = None
|
109
|
+
meta_parallel: Optional[dict[str, Any]] = None
|
109
110
|
|
110
|
-
args_non_parallel: Optional[dict[str, Any]]
|
111
|
-
args_parallel: Optional[dict[str, Any]]
|
111
|
+
args_non_parallel: Optional[dict[str, Any]] = None
|
112
|
+
args_parallel: Optional[dict[str, Any]] = None
|
112
113
|
|
113
114
|
type_filters: dict[str, bool]
|
114
115
|
|
@@ -121,34 +122,35 @@ class WorkflowTaskReadV2WithWarning(WorkflowTaskReadV2):
|
|
121
122
|
warning: Optional[str] = None
|
122
123
|
|
123
124
|
|
124
|
-
class WorkflowTaskUpdateV2(BaseModel
|
125
|
+
class WorkflowTaskUpdateV2(BaseModel):
|
126
|
+
model_config = ConfigDict(extra="forbid")
|
125
127
|
|
126
|
-
meta_non_parallel: Optional[dict[str, Any]]
|
127
|
-
meta_parallel: Optional[dict[str, Any]]
|
128
|
-
args_non_parallel: Optional[dict[str, Any]]
|
129
|
-
args_parallel: Optional[dict[str, Any]]
|
130
|
-
type_filters: Optional[dict[str, bool]]
|
128
|
+
meta_non_parallel: Optional[dict[str, Any]] = None
|
129
|
+
meta_parallel: Optional[dict[str, Any]] = None
|
130
|
+
args_non_parallel: Optional[dict[str, Any]] = None
|
131
|
+
args_parallel: Optional[dict[str, Any]] = None
|
132
|
+
type_filters: Optional[dict[str, bool]] = None
|
131
133
|
|
132
134
|
# Validators
|
133
|
-
_dict_keys =
|
134
|
-
root_validate_dict_keys
|
135
|
+
_dict_keys = model_validator(mode="before")(
|
136
|
+
classmethod(root_validate_dict_keys)
|
135
137
|
)
|
136
|
-
_type_filters =
|
137
|
-
validate_type_filters
|
138
|
+
_type_filters = field_validator("type_filters")(
|
139
|
+
classmethod(validate_type_filters)
|
138
140
|
)
|
139
|
-
|
140
|
-
|
141
|
-
valdict_keys("meta_non_parallel")
|
141
|
+
_meta_non_parallel = field_validator("meta_non_parallel")(
|
142
|
+
classmethod(valdict_keys("meta_non_parallel"))
|
142
143
|
)
|
143
|
-
_meta_parallel =
|
144
|
-
valdict_keys("meta_parallel")
|
144
|
+
_meta_parallel = field_validator("meta_parallel")(
|
145
|
+
classmethod(valdict_keys("meta_parallel"))
|
145
146
|
)
|
146
147
|
|
147
|
-
@
|
148
|
+
@field_validator("args_non_parallel")
|
149
|
+
@classmethod
|
148
150
|
def validate_args_non_parallel(cls, value):
|
149
151
|
if value is None:
|
150
152
|
return
|
151
|
-
valdict_keys("args_non_parallel")(value)
|
153
|
+
valdict_keys("args_non_parallel")(cls, value)
|
152
154
|
args_keys = set(value.keys())
|
153
155
|
intersect_keys = RESERVED_ARGUMENTS.intersection(args_keys)
|
154
156
|
if intersect_keys:
|
@@ -158,11 +160,12 @@ class WorkflowTaskUpdateV2(BaseModel, extra=Extra.forbid):
|
|
158
160
|
)
|
159
161
|
return value
|
160
162
|
|
161
|
-
@
|
163
|
+
@field_validator("args_parallel")
|
164
|
+
@classmethod
|
162
165
|
def validate_args_parallel(cls, value):
|
163
166
|
if value is None:
|
164
167
|
return
|
165
|
-
valdict_keys("args_parallel")(value)
|
168
|
+
valdict_keys("args_parallel")(cls, value)
|
166
169
|
args_keys = set(value.keys())
|
167
170
|
intersect_keys = RESERVED_ARGUMENTS.intersection(args_keys)
|
168
171
|
if intersect_keys:
|
@@ -173,7 +176,8 @@ class WorkflowTaskUpdateV2(BaseModel, extra=Extra.forbid):
|
|
173
176
|
return value
|
174
177
|
|
175
178
|
|
176
|
-
class WorkflowTaskImportV2(BaseModel
|
179
|
+
class WorkflowTaskImportV2(BaseModel):
|
180
|
+
model_config = ConfigDict(extra="forbid")
|
177
181
|
|
178
182
|
meta_non_parallel: Optional[dict[str, Any]] = None
|
179
183
|
meta_parallel: Optional[dict[str, Any]] = None
|
@@ -185,7 +189,8 @@ class WorkflowTaskImportV2(BaseModel, extra=Extra.forbid):
|
|
185
189
|
task: Union[TaskImportV2, TaskImportV2Legacy]
|
186
190
|
|
187
191
|
# Validators
|
188
|
-
@
|
192
|
+
@model_validator(mode="before")
|
193
|
+
@classmethod
|
189
194
|
def update_legacy_filters(cls, values: dict):
|
190
195
|
"""
|
191
196
|
Transform legacy filters (created with fractal-server<2.11.0)
|
@@ -197,7 +202,6 @@ class WorkflowTaskImportV2(BaseModel, extra=Extra.forbid):
|
|
197
202
|
"Cannot set filters both through the legacy field "
|
198
203
|
"('filters') and the new one ('type_filters')."
|
199
204
|
)
|
200
|
-
|
201
205
|
else:
|
202
206
|
# As of 2.11.0, WorkflowTask do not have attribute filters
|
203
207
|
# any more.
|
@@ -213,26 +217,24 @@ class WorkflowTaskImportV2(BaseModel, extra=Extra.forbid):
|
|
213
217
|
|
214
218
|
return values
|
215
219
|
|
216
|
-
_type_filters =
|
217
|
-
validate_type_filters
|
220
|
+
_type_filters = field_validator("type_filters")(
|
221
|
+
classmethod(validate_type_filters)
|
218
222
|
)
|
219
|
-
|
220
|
-
|
221
|
-
valdict_keys("meta_non_parallel")
|
223
|
+
_meta_non_parallel = field_validator("meta_non_parallel")(
|
224
|
+
classmethod(valdict_keys("meta_non_parallel"))
|
222
225
|
)
|
223
|
-
_meta_parallel =
|
224
|
-
valdict_keys("meta_parallel")
|
226
|
+
_meta_parallel = field_validator("meta_parallel")(
|
227
|
+
classmethod(valdict_keys("meta_parallel"))
|
225
228
|
)
|
226
|
-
_args_non_parallel =
|
227
|
-
valdict_keys("args_non_parallel")
|
229
|
+
_args_non_parallel = field_validator("args_non_parallel")(
|
230
|
+
classmethod(valdict_keys("args_non_parallel"))
|
228
231
|
)
|
229
|
-
_args_parallel =
|
230
|
-
valdict_keys("args_parallel")
|
232
|
+
_args_parallel = field_validator("args_parallel")(
|
233
|
+
classmethod(valdict_keys("args_parallel"))
|
231
234
|
)
|
232
235
|
|
233
236
|
|
234
237
|
class WorkflowTaskExportV2(BaseModel):
|
235
|
-
|
236
238
|
meta_non_parallel: Optional[dict[str, Any]] = None
|
237
239
|
meta_parallel: Optional[dict[str, Any]] = None
|
238
240
|
args_non_parallel: Optional[dict[str, Any]] = None
|
@@ -83,7 +83,7 @@ class SQLModelUserDatabaseAsync(Generic[UP, ID], BaseUserDatabase[UP, ID]):
|
|
83
83
|
|
84
84
|
session: AsyncSession
|
85
85
|
user_model: Type[UP]
|
86
|
-
oauth_account_model: Optional[Type[OAuthAccount]]
|
86
|
+
oauth_account_model: Optional[Type[OAuthAccount]] = None
|
87
87
|
|
88
88
|
def __init__(
|
89
89
|
self,
|
fractal_server/config.py
CHANGED
@@ -24,11 +24,12 @@ from typing import TypeVar
|
|
24
24
|
from cryptography.fernet import Fernet
|
25
25
|
from dotenv import load_dotenv
|
26
26
|
from pydantic import BaseModel
|
27
|
-
from pydantic import BaseSettings
|
28
27
|
from pydantic import EmailStr
|
29
28
|
from pydantic import Field
|
30
|
-
from pydantic import
|
31
|
-
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
|
32
33
|
from sqlalchemy.engine import URL
|
33
34
|
|
34
35
|
import fractal_server
|
@@ -50,7 +51,7 @@ class MailSettings(BaseModel):
|
|
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
|
@@ -97,10 +98,11 @@ class OAuthClientConfig(BaseModel):
|
|
97
98
|
CLIENT_NAME: str
|
98
99
|
CLIENT_ID: str
|
99
100
|
CLIENT_SECRET: str
|
100
|
-
OIDC_CONFIGURATION_ENDPOINT: Optional[str]
|
101
|
+
OIDC_CONFIGURATION_ENDPOINT: Optional[str] = None
|
101
102
|
REDIRECT_URL: Optional[str] = None
|
102
103
|
|
103
|
-
@
|
104
|
+
@model_validator(mode="before")
|
105
|
+
@classmethod
|
104
106
|
def check_configuration(cls, values):
|
105
107
|
if values.get("CLIENT_NAME") not in ["GOOGLE", "GITHUB"]:
|
106
108
|
if not values.get("OIDC_CONFIGURATION_ENDPOINT"):
|
@@ -118,8 +120,7 @@ class Settings(BaseSettings):
|
|
118
120
|
The attributes of this class are set from the environment.
|
119
121
|
"""
|
120
122
|
|
121
|
-
|
122
|
-
case_sensitive = True
|
123
|
+
model_config = SettingsConfigDict(case_sensitive=True)
|
123
124
|
|
124
125
|
PROJECT_NAME: str = "Fractal Server"
|
125
126
|
PROJECT_VERSION: str = fractal_server.__VERSION__
|
@@ -136,7 +137,7 @@ class Settings(BaseSettings):
|
|
136
137
|
JWT token lifetime, in seconds.
|
137
138
|
"""
|
138
139
|
|
139
|
-
JWT_SECRET_KEY: Optional[str]
|
140
|
+
JWT_SECRET_KEY: Optional[str] = None
|
140
141
|
"""
|
141
142
|
JWT secret
|
142
143
|
|
@@ -150,7 +151,8 @@ class Settings(BaseSettings):
|
|
150
151
|
Cookie token lifetime, in seconds.
|
151
152
|
"""
|
152
153
|
|
153
|
-
@
|
154
|
+
@model_validator(mode="before")
|
155
|
+
@classmethod
|
154
156
|
def collect_oauth_clients(cls, values):
|
155
157
|
"""
|
156
158
|
Automatic collection of OAuth Clients
|
@@ -198,11 +200,11 @@ class Settings(BaseSettings):
|
|
198
200
|
"""
|
199
201
|
If `True`, make database operations verbose.
|
200
202
|
"""
|
201
|
-
POSTGRES_USER: Optional[str]
|
203
|
+
POSTGRES_USER: Optional[str] = None
|
202
204
|
"""
|
203
205
|
User to use when connecting to the PostgreSQL database.
|
204
206
|
"""
|
205
|
-
POSTGRES_PASSWORD: Optional[str]
|
207
|
+
POSTGRES_PASSWORD: Optional[str] = None
|
206
208
|
"""
|
207
209
|
Password to use when connecting to the PostgreSQL database.
|
208
210
|
"""
|
@@ -214,7 +216,7 @@ class Settings(BaseSettings):
|
|
214
216
|
"""
|
215
217
|
Port number to use when connecting to the PostgreSQL server.
|
216
218
|
"""
|
217
|
-
POSTGRES_DB: Optional[str]
|
219
|
+
POSTGRES_DB: Optional[str] = None
|
218
220
|
"""
|
219
221
|
Name of the PostgreSQL database to connect to.
|
220
222
|
"""
|
@@ -266,13 +268,14 @@ class Settings(BaseSettings):
|
|
266
268
|
default admin credentials.
|
267
269
|
"""
|
268
270
|
|
269
|
-
FRACTAL_TASKS_DIR: Optional[Path]
|
271
|
+
FRACTAL_TASKS_DIR: Optional[Path] = None
|
270
272
|
"""
|
271
273
|
Directory under which all the tasks will be saved (either an absolute path
|
272
274
|
or a path relative to current working directory).
|
273
275
|
"""
|
274
276
|
|
275
|
-
@
|
277
|
+
@field_validator("FRACTAL_TASKS_DIR")
|
278
|
+
@classmethod
|
276
279
|
def make_FRACTAL_TASKS_DIR_absolute(cls, v):
|
277
280
|
"""
|
278
281
|
If `FRACTAL_TASKS_DIR` is a non-absolute path, make it absolute (based
|
@@ -289,7 +292,8 @@ class Settings(BaseSettings):
|
|
289
292
|
)
|
290
293
|
return FRACTAL_TASKS_DIR_path
|
291
294
|
|
292
|
-
@
|
295
|
+
@field_validator("FRACTAL_RUNNER_WORKING_BASE_DIR")
|
296
|
+
@classmethod
|
293
297
|
def make_FRACTAL_RUNNER_WORKING_BASE_DIR_absolute(cls, v):
|
294
298
|
"""
|
295
299
|
(Copy of make_FRACTAL_TASKS_DIR_absolute)
|
@@ -312,7 +316,6 @@ class Settings(BaseSettings):
|
|
312
316
|
|
313
317
|
FRACTAL_RUNNER_BACKEND: Literal[
|
314
318
|
"local",
|
315
|
-
"local_experimental",
|
316
319
|
"slurm",
|
317
320
|
"slurm_ssh",
|
318
321
|
] = "local"
|
@@ -320,7 +323,7 @@ class Settings(BaseSettings):
|
|
320
323
|
Select which runner backend to use.
|
321
324
|
"""
|
322
325
|
|
323
|
-
FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path]
|
326
|
+
FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path] = None
|
324
327
|
"""
|
325
328
|
Base directory for running jobs / workflows. All artifacts required to set
|
326
329
|
up, run and tear down jobs are placed in subdirs of this directory.
|
@@ -333,7 +336,7 @@ class Settings(BaseSettings):
|
|
333
336
|
Only logs of with this level (or higher) will appear in the console logs.
|
334
337
|
"""
|
335
338
|
|
336
|
-
FRACTAL_LOCAL_CONFIG_FILE: Optional[Path]
|
339
|
+
FRACTAL_LOCAL_CONFIG_FILE: Optional[Path] = None
|
337
340
|
"""
|
338
341
|
Path of JSON file with configuration for the local backend.
|
339
342
|
"""
|
@@ -349,7 +352,7 @@ class Settings(BaseSettings):
|
|
349
352
|
Waiting time for the shutdown phase of executors
|
350
353
|
"""
|
351
354
|
|
352
|
-
FRACTAL_SLURM_CONFIG_FILE: Optional[Path]
|
355
|
+
FRACTAL_SLURM_CONFIG_FILE: Optional[Path] = None
|
353
356
|
"""
|
354
357
|
Path of JSON file with configuration for the SLURM backend.
|
355
358
|
"""
|
@@ -360,7 +363,8 @@ class Settings(BaseSettings):
|
|
360
363
|
nodes. If not specified, the same interpreter that runs the server is used.
|
361
364
|
"""
|
362
365
|
|
363
|
-
@
|
366
|
+
@field_validator("FRACTAL_SLURM_WORKER_PYTHON")
|
367
|
+
@classmethod
|
364
368
|
def absolute_FRACTAL_SLURM_WORKER_PYTHON(cls, v):
|
365
369
|
"""
|
366
370
|
If `FRACTAL_SLURM_WORKER_PYTHON` is a relative path, fail.
|
@@ -407,7 +411,8 @@ class Settings(BaseSettings):
|
|
407
411
|
Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.12.
|
408
412
|
"""
|
409
413
|
|
410
|
-
@
|
414
|
+
@model_validator(mode="before")
|
415
|
+
@classmethod
|
411
416
|
def check_tasks_python(cls, values):
|
412
417
|
"""
|
413
418
|
Perform multiple checks of the Python-interpreter variables.
|
@@ -511,7 +516,8 @@ class Settings(BaseSettings):
|
|
511
516
|
`--no-cache-dir` is used.
|
512
517
|
"""
|
513
518
|
|
514
|
-
@
|
519
|
+
@field_validator("FRACTAL_PIP_CACHE_DIR")
|
520
|
+
@classmethod
|
515
521
|
def absolute_FRACTAL_PIP_CACHE_DIR(cls, v):
|
516
522
|
"""
|
517
523
|
If `FRACTAL_PIP_CACHE_DIR` is a relative path, fail.
|
@@ -605,73 +611,80 @@ class Settings(BaseSettings):
|
|
605
611
|
"""
|
606
612
|
Comma-separated list of recipients of the OAuth-signup emails.
|
607
613
|
"""
|
608
|
-
FRACTAL_EMAIL_USE_STARTTLS:
|
614
|
+
FRACTAL_EMAIL_USE_STARTTLS: Literal["true", "false"] = "true"
|
609
615
|
"""
|
610
616
|
Whether to use StartTLS when using the SMTP server.
|
617
|
+
Accepted values: 'true', 'false'.
|
611
618
|
"""
|
612
|
-
FRACTAL_EMAIL_USE_LOGIN:
|
619
|
+
FRACTAL_EMAIL_USE_LOGIN: Literal["true", "false"] = "true"
|
613
620
|
"""
|
614
621
|
Whether to use login when using the SMTP server.
|
622
|
+
If 'true', FRACTAL_EMAIL_PASSWORD and FRACTAL_EMAIL_PASSWORD_KEY must be
|
623
|
+
provided.
|
624
|
+
Accepted values: 'true', 'false'.
|
615
625
|
"""
|
616
626
|
email_settings: Optional[MailSettings] = None
|
617
627
|
|
618
|
-
@
|
619
|
-
def validate_email_settings(
|
620
|
-
email_values =
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
628
|
+
@model_validator(mode="after")
|
629
|
+
def validate_email_settings(self):
|
630
|
+
email_values = [
|
631
|
+
self.FRACTAL_EMAIL_SENDER,
|
632
|
+
self.FRACTAL_EMAIL_SMTP_SERVER,
|
633
|
+
self.FRACTAL_EMAIL_SMTP_PORT,
|
634
|
+
self.FRACTAL_EMAIL_INSTANCE_NAME,
|
635
|
+
self.FRACTAL_EMAIL_RECIPIENTS,
|
636
|
+
]
|
637
|
+
if len(set(email_values)) == 1:
|
638
|
+
# All required EMAIL attributes are None
|
639
|
+
pass
|
640
|
+
elif None in email_values:
|
641
|
+
# Not all required EMAIL attributes are set
|
642
|
+
error_msg = (
|
643
|
+
"Invalid FRACTAL_EMAIL configuration. "
|
644
|
+
f"Given values: {email_values}."
|
645
|
+
)
|
646
|
+
raise ValueError(error_msg)
|
647
|
+
else:
|
648
|
+
use_starttls = self.FRACTAL_EMAIL_USE_STARTTLS == "true"
|
649
|
+
use_login = self.FRACTAL_EMAIL_USE_LOGIN == "true"
|
634
650
|
|
635
|
-
if
|
636
|
-
if
|
651
|
+
if use_login:
|
652
|
+
if self.FRACTAL_EMAIL_PASSWORD is None:
|
637
653
|
raise ValueError(
|
638
|
-
"'FRACTAL_EMAIL_USE_LOGIN' is
|
654
|
+
"'FRACTAL_EMAIL_USE_LOGIN' is 'true' but "
|
639
655
|
"'FRACTAL_EMAIL_PASSWORD' is not provided."
|
640
656
|
)
|
641
|
-
|
657
|
+
if self.FRACTAL_EMAIL_PASSWORD_KEY is None:
|
642
658
|
raise ValueError(
|
643
|
-
"'FRACTAL_EMAIL_USE_LOGIN' is
|
659
|
+
"'FRACTAL_EMAIL_USE_LOGIN' is 'true' but "
|
644
660
|
"'FRACTAL_EMAIL_PASSWORD_KEY' is not provided."
|
645
661
|
)
|
646
|
-
|
647
|
-
|
648
|
-
(
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
)
|
662
|
+
try:
|
663
|
+
(
|
664
|
+
Fernet(self.FRACTAL_EMAIL_PASSWORD_KEY)
|
665
|
+
.decrypt(self.FRACTAL_EMAIL_PASSWORD)
|
666
|
+
.decode("utf-8")
|
667
|
+
)
|
668
|
+
except Exception as e:
|
669
|
+
raise ValueError(
|
670
|
+
"Invalid pair (FRACTAL_EMAIL_PASSWORD, "
|
671
|
+
"FRACTAL_EMAIL_PASSWORD_KEY). "
|
672
|
+
f"Original error: {str(e)}."
|
673
|
+
)
|
659
674
|
|
660
|
-
|
661
|
-
sender=
|
662
|
-
recipients=
|
663
|
-
smtp_server=
|
664
|
-
port=
|
665
|
-
encrypted_password=
|
666
|
-
encryption_key=
|
667
|
-
instance_name=
|
668
|
-
use_starttls=
|
669
|
-
|
670
|
-
),
|
671
|
-
use_login=email_values.get("FRACTAL_EMAIL_USE_LOGIN", True),
|
675
|
+
self.email_settings = MailSettings(
|
676
|
+
sender=self.FRACTAL_EMAIL_SENDER,
|
677
|
+
recipients=self.FRACTAL_EMAIL_RECIPIENTS.split(","),
|
678
|
+
smtp_server=self.FRACTAL_EMAIL_SMTP_SERVER,
|
679
|
+
port=self.FRACTAL_EMAIL_SMTP_PORT,
|
680
|
+
encrypted_password=self.FRACTAL_EMAIL_PASSWORD,
|
681
|
+
encryption_key=self.FRACTAL_EMAIL_PASSWORD_KEY,
|
682
|
+
instance_name=self.FRACTAL_EMAIL_INSTANCE_NAME,
|
683
|
+
use_starttls=use_starttls,
|
684
|
+
use_login=use_login,
|
672
685
|
)
|
673
686
|
|
674
|
-
return
|
687
|
+
return self
|
675
688
|
|
676
689
|
###########################################################################
|
677
690
|
# BUSINESS LOGIC
|
@@ -794,7 +807,7 @@ class Settings(BaseSettings):
|
|
794
807
|
return False
|
795
808
|
|
796
809
|
sanitized_settings = {}
|
797
|
-
for k, v in self.
|
810
|
+
for k, v in self.model_dump().items():
|
798
811
|
if _must_be_sanitized(k):
|
799
812
|
sanitized_settings[k] = "***"
|
800
813
|
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
@@ -67,7 +67,7 @@ def check_settings() -> None:
|
|
67
67
|
|
68
68
|
logger = set_logger("fractal_server_settings")
|
69
69
|
logger.debug("Fractal Settings:")
|
70
|
-
for key, value in settings.
|
70
|
+
for key, value in settings.model_dump().items():
|
71
71
|
if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
|
72
72
|
value = "*****"
|
73
73
|
logger.debug(f" {key}: {value}")
|