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.
Files changed (87) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/security.py +9 -12
  3. fractal_server/app/models/v2/__init__.py +4 -0
  4. fractal_server/app/models/v2/accounting.py +35 -0
  5. fractal_server/app/models/v2/dataset.py +2 -2
  6. fractal_server/app/models/v2/job.py +11 -9
  7. fractal_server/app/models/v2/task.py +2 -3
  8. fractal_server/app/models/v2/task_group.py +6 -2
  9. fractal_server/app/models/v2/workflowtask.py +15 -8
  10. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  11. fractal_server/app/routes/admin/v2/accounting.py +108 -0
  12. fractal_server/app/routes/admin/v2/impersonate.py +35 -0
  13. fractal_server/app/routes/admin/v2/job.py +5 -13
  14. fractal_server/app/routes/admin/v2/task.py +1 -1
  15. fractal_server/app/routes/admin/v2/task_group.py +5 -13
  16. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
  17. fractal_server/app/routes/api/v2/dataset.py +4 -4
  18. fractal_server/app/routes/api/v2/images.py +11 -11
  19. fractal_server/app/routes/api/v2/project.py +2 -2
  20. fractal_server/app/routes/api/v2/status.py +1 -1
  21. fractal_server/app/routes/api/v2/submit.py +9 -6
  22. fractal_server/app/routes/api/v2/task.py +4 -2
  23. fractal_server/app/routes/api/v2/task_collection.py +3 -2
  24. fractal_server/app/routes/api/v2/task_group.py +4 -7
  25. fractal_server/app/routes/api/v2/workflow.py +3 -3
  26. fractal_server/app/routes/api/v2/workflow_import.py +3 -3
  27. fractal_server/app/routes/api/v2/workflowtask.py +3 -1
  28. fractal_server/app/routes/auth/_aux_auth.py +4 -1
  29. fractal_server/app/routes/auth/current_user.py +3 -5
  30. fractal_server/app/routes/auth/group.py +1 -1
  31. fractal_server/app/routes/auth/users.py +2 -4
  32. fractal_server/app/routes/aux/__init__.py +0 -20
  33. fractal_server/app/routes/aux/_runner.py +1 -1
  34. fractal_server/app/routes/aux/validate_user_settings.py +1 -2
  35. fractal_server/app/runner/executors/_job_states.py +13 -0
  36. fractal_server/app/runner/executors/slurm/_slurm_config.py +26 -18
  37. fractal_server/app/runner/executors/slurm/ssh/__init__.py +0 -3
  38. fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +31 -22
  39. fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +2 -5
  40. fractal_server/app/runner/executors/slurm/ssh/executor.py +21 -27
  41. fractal_server/app/runner/executors/slurm/sudo/__init__.py +0 -3
  42. fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +1 -2
  43. fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +37 -47
  44. fractal_server/app/runner/executors/slurm/sudo/executor.py +25 -24
  45. fractal_server/app/runner/v2/__init__.py +4 -9
  46. fractal_server/app/runner/v2/_local/__init__.py +3 -0
  47. fractal_server/app/runner/v2/_local/_local_config.py +5 -4
  48. fractal_server/app/runner/v2/_slurm_common/get_slurm_config.py +4 -4
  49. fractal_server/app/runner/v2/_slurm_ssh/__init__.py +2 -0
  50. fractal_server/app/runner/v2/_slurm_sudo/__init__.py +4 -2
  51. fractal_server/app/runner/v2/deduplicate_list.py +1 -1
  52. fractal_server/app/runner/v2/runner.py +25 -10
  53. fractal_server/app/runner/v2/runner_functions.py +12 -11
  54. fractal_server/app/runner/v2/task_interface.py +15 -7
  55. fractal_server/app/schemas/_filter_validators.py +6 -3
  56. fractal_server/app/schemas/_validators.py +7 -5
  57. fractal_server/app/schemas/user.py +23 -18
  58. fractal_server/app/schemas/user_group.py +25 -11
  59. fractal_server/app/schemas/user_settings.py +31 -24
  60. fractal_server/app/schemas/v2/__init__.py +1 -0
  61. fractal_server/app/schemas/v2/accounting.py +18 -0
  62. fractal_server/app/schemas/v2/dataset.py +48 -35
  63. fractal_server/app/schemas/v2/dumps.py +16 -14
  64. fractal_server/app/schemas/v2/job.py +49 -29
  65. fractal_server/app/schemas/v2/manifest.py +32 -28
  66. fractal_server/app/schemas/v2/project.py +18 -8
  67. fractal_server/app/schemas/v2/task.py +86 -75
  68. fractal_server/app/schemas/v2/task_collection.py +41 -30
  69. fractal_server/app/schemas/v2/task_group.py +39 -20
  70. fractal_server/app/schemas/v2/workflow.py +24 -12
  71. fractal_server/app/schemas/v2/workflowtask.py +63 -61
  72. fractal_server/app/security/__init__.py +1 -1
  73. fractal_server/config.py +86 -73
  74. fractal_server/images/models.py +18 -12
  75. fractal_server/main.py +1 -1
  76. fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +57 -0
  77. fractal_server/tasks/v2/utils_background.py +2 -2
  78. fractal_server/tasks/v2/utils_database.py +1 -1
  79. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/METADATA +9 -10
  80. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/RECORD +83 -81
  81. fractal_server/app/runner/v2/_local_experimental/__init__.py +0 -121
  82. fractal_server/app/runner/v2/_local_experimental/_local_config.py +0 -108
  83. fractal_server/app/runner/v2/_local_experimental/_submit_setup.py +0 -42
  84. fractal_server/app/runner/v2/_local_experimental/executor.py +0 -157
  85. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/LICENSE +0 -0
  86. {fractal_server-2.12.1.dist-info → fractal_server-2.13.1.dist-info}/WHEEL +0 -0
  87. {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 Extra
7
+ from pydantic import ConfigDict
8
8
  from pydantic import Field
9
- from pydantic import root_validator
10
- from pydantic import validator
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, extra=Extra.forbid):
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 = root_validator(pre=True, allow_reuse=True)(
52
- root_validate_dict_keys
52
+ _dict_keys = model_validator(mode="before")(
53
+ classmethod(root_validate_dict_keys)
53
54
  )
54
- _type_filters = validator("type_filters", allow_reuse=True)(
55
- validate_type_filters
55
+ _type_filters = field_validator("type_filters")(
56
+ classmethod(validate_type_filters)
56
57
  )
57
-
58
- _meta_non_parallel = validator("meta_non_parallel", allow_reuse=True)(
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 = validator("meta_parallel", allow_reuse=True)(
62
- valdict_keys("meta_parallel")
61
+ _meta_parallel = field_validator("meta_parallel")(
62
+ classmethod(valdict_keys("meta_parallel"))
63
63
  )
64
64
 
65
- @validator("args_non_parallel")
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
- @validator("args_parallel")
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, extra=Extra.forbid):
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 = root_validator(pre=True, allow_reuse=True)(
134
- root_validate_dict_keys
135
+ _dict_keys = model_validator(mode="before")(
136
+ classmethod(root_validate_dict_keys)
135
137
  )
136
- _type_filters = validator("type_filters", allow_reuse=True)(
137
- validate_type_filters
138
+ _type_filters = field_validator("type_filters")(
139
+ classmethod(validate_type_filters)
138
140
  )
139
-
140
- _meta_non_parallel = validator("meta_non_parallel", allow_reuse=True)(
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 = validator("meta_parallel", allow_reuse=True)(
144
- valdict_keys("meta_parallel")
144
+ _meta_parallel = field_validator("meta_parallel")(
145
+ classmethod(valdict_keys("meta_parallel"))
145
146
  )
146
147
 
147
- @validator("args_non_parallel")
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
- @validator("args_parallel")
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, extra=Extra.forbid):
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
- @root_validator(pre=True)
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 = validator("type_filters", allow_reuse=True)(
217
- validate_type_filters
220
+ _type_filters = field_validator("type_filters")(
221
+ classmethod(validate_type_filters)
218
222
  )
219
-
220
- _meta_non_parallel = validator("meta_non_parallel", allow_reuse=True)(
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 = validator("meta_parallel", allow_reuse=True)(
224
- valdict_keys("meta_parallel")
226
+ _meta_parallel = field_validator("meta_parallel")(
227
+ classmethod(valdict_keys("meta_parallel"))
225
228
  )
226
- _args_non_parallel = validator("args_non_parallel", allow_reuse=True)(
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 = validator("args_parallel", allow_reuse=True)(
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 root_validator
31
- from pydantic import validator
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(min_items=1)
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
- @root_validator
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
- class Config:
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
- @root_validator(pre=True)
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
- @validator("FRACTAL_TASKS_DIR", always=True)
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
- @validator("FRACTAL_RUNNER_WORKING_BASE_DIR", always=True)
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
- @validator("FRACTAL_SLURM_WORKER_PYTHON", always=True)
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
- @root_validator(pre=True)
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
- @validator("FRACTAL_PIP_CACHE_DIR", always=True)
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: Optional[bool] = True
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: Optional[bool] = True
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
- @root_validator(pre=True)
619
- def validate_email_settings(cls, values):
620
- email_values = {
621
- k: v for k, v in values.items() if k.startswith("FRACTAL_EMAIL")
622
- }
623
- if email_values:
624
-
625
- def assert_key(key: str):
626
- if key not in email_values:
627
- raise ValueError(f"Missing '{key}'")
628
-
629
- assert_key("FRACTAL_EMAIL_SENDER")
630
- assert_key("FRACTAL_EMAIL_SMTP_SERVER")
631
- assert_key("FRACTAL_EMAIL_SMTP_PORT")
632
- assert_key("FRACTAL_EMAIL_INSTANCE_NAME")
633
- assert_key("FRACTAL_EMAIL_RECIPIENTS")
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 email_values.get("FRACTAL_EMAIL_USE_LOGIN", True):
636
- if "FRACTAL_EMAIL_PASSWORD" not in email_values:
651
+ if use_login:
652
+ if self.FRACTAL_EMAIL_PASSWORD is None:
637
653
  raise ValueError(
638
- "'FRACTAL_EMAIL_USE_LOGIN' is True but "
654
+ "'FRACTAL_EMAIL_USE_LOGIN' is 'true' but "
639
655
  "'FRACTAL_EMAIL_PASSWORD' is not provided."
640
656
  )
641
- elif "FRACTAL_EMAIL_PASSWORD_KEY" not in email_values:
657
+ if self.FRACTAL_EMAIL_PASSWORD_KEY is None:
642
658
  raise ValueError(
643
- "'FRACTAL_EMAIL_USE_LOGIN' is True but "
659
+ "'FRACTAL_EMAIL_USE_LOGIN' is 'true' but "
644
660
  "'FRACTAL_EMAIL_PASSWORD_KEY' is not provided."
645
661
  )
646
- else:
647
- try:
648
- (
649
- Fernet(email_values["FRACTAL_EMAIL_PASSWORD_KEY"])
650
- .decrypt(email_values["FRACTAL_EMAIL_PASSWORD"])
651
- .decode("utf-8")
652
- )
653
- except Exception as e:
654
- raise ValueError(
655
- "Invalid pair (FRACTAL_EMAIL_PASSWORD, "
656
- "FRACTAL_EMAIL_PASSWORD_KEY). "
657
- f"Original error: {str(e)}."
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
- values["email_settings"] = MailSettings(
661
- sender=email_values["FRACTAL_EMAIL_SENDER"],
662
- recipients=email_values["FRACTAL_EMAIL_RECIPIENTS"].split(","),
663
- smtp_server=email_values["FRACTAL_EMAIL_SMTP_SERVER"],
664
- port=email_values["FRACTAL_EMAIL_SMTP_PORT"],
665
- encrypted_password=email_values.get("FRACTAL_EMAIL_PASSWORD"),
666
- encryption_key=email_values.get("FRACTAL_EMAIL_PASSWORD_KEY"),
667
- instance_name=email_values["FRACTAL_EMAIL_INSTANCE_NAME"],
668
- use_starttls=email_values.get(
669
- "FRACTAL_EMAIL_USE_STARTTLS", True
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 values
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.dict().items():
810
+ for k, v in self.model_dump().items():
798
811
  if _must_be_sanitized(k):
799
812
  sanitized_settings[k] = "***"
800
813
  else:
@@ -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 validator
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 = validator("attributes", allow_reuse=True)(
34
- valdict_keys("attributes")
33
+ _attributes = field_validator("attributes")(
34
+ classmethod(valdict_keys("attributes"))
35
35
  )
36
- _types = validator("types", allow_reuse=True)(valdict_keys("types"))
36
+ _types = field_validator("types")(classmethod(valdict_keys("types")))
37
37
 
38
- @validator("zarr_url")
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
- @validator("origin")
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
- @validator("attributes")
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
- @validator("attributes")
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
- @validator("zarr_url")
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
- @validator("attributes")
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 = validator("types", allow_reuse=True)(valdict_keys("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.dict().items():
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}")