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.
Files changed (82) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +17 -63
  3. fractal_server/app/models/security.py +9 -12
  4. fractal_server/app/models/v2/dataset.py +2 -2
  5. fractal_server/app/models/v2/job.py +11 -9
  6. fractal_server/app/models/v2/task.py +2 -3
  7. fractal_server/app/models/v2/task_group.py +6 -2
  8. fractal_server/app/models/v2/workflowtask.py +15 -8
  9. fractal_server/app/routes/admin/v2/task.py +1 -1
  10. fractal_server/app/routes/admin/v2/task_group.py +1 -1
  11. fractal_server/app/routes/api/v2/dataset.py +4 -4
  12. fractal_server/app/routes/api/v2/images.py +11 -23
  13. fractal_server/app/routes/api/v2/project.py +2 -2
  14. fractal_server/app/routes/api/v2/status.py +1 -1
  15. fractal_server/app/routes/api/v2/submit.py +8 -6
  16. fractal_server/app/routes/api/v2/task.py +4 -2
  17. fractal_server/app/routes/api/v2/task_collection.py +3 -2
  18. fractal_server/app/routes/api/v2/task_group.py +2 -2
  19. fractal_server/app/routes/api/v2/workflow.py +3 -3
  20. fractal_server/app/routes/api/v2/workflow_import.py +3 -3
  21. fractal_server/app/routes/api/v2/workflowtask.py +3 -1
  22. fractal_server/app/routes/auth/_aux_auth.py +4 -1
  23. fractal_server/app/routes/auth/current_user.py +3 -5
  24. fractal_server/app/routes/auth/group.py +1 -1
  25. fractal_server/app/routes/auth/users.py +2 -4
  26. fractal_server/app/routes/aux/_runner.py +1 -1
  27. fractal_server/app/routes/aux/validate_user_settings.py +1 -2
  28. fractal_server/app/runner/executors/_job_states.py +13 -0
  29. fractal_server/app/runner/executors/slurm/_slurm_config.py +26 -18
  30. fractal_server/app/runner/executors/slurm/ssh/__init__.py +0 -3
  31. fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +31 -22
  32. fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +2 -6
  33. fractal_server/app/runner/executors/slurm/ssh/executor.py +35 -50
  34. fractal_server/app/runner/executors/slurm/sudo/__init__.py +0 -3
  35. fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +1 -2
  36. fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +37 -47
  37. fractal_server/app/runner/executors/slurm/sudo/executor.py +77 -41
  38. fractal_server/app/runner/v2/__init__.py +0 -9
  39. fractal_server/app/runner/v2/_local/_local_config.py +5 -4
  40. fractal_server/app/runner/v2/_slurm_common/get_slurm_config.py +4 -4
  41. fractal_server/app/runner/v2/_slurm_sudo/__init__.py +2 -2
  42. fractal_server/app/runner/v2/deduplicate_list.py +1 -1
  43. fractal_server/app/runner/v2/runner.py +9 -4
  44. fractal_server/app/runner/v2/task_interface.py +15 -7
  45. fractal_server/app/schemas/_filter_validators.py +6 -3
  46. fractal_server/app/schemas/_validators.py +7 -5
  47. fractal_server/app/schemas/user.py +23 -18
  48. fractal_server/app/schemas/user_group.py +25 -11
  49. fractal_server/app/schemas/user_settings.py +31 -24
  50. fractal_server/app/schemas/v2/dataset.py +48 -35
  51. fractal_server/app/schemas/v2/dumps.py +16 -14
  52. fractal_server/app/schemas/v2/job.py +49 -29
  53. fractal_server/app/schemas/v2/manifest.py +32 -28
  54. fractal_server/app/schemas/v2/project.py +18 -8
  55. fractal_server/app/schemas/v2/task.py +86 -75
  56. fractal_server/app/schemas/v2/task_collection.py +41 -30
  57. fractal_server/app/schemas/v2/task_group.py +39 -20
  58. fractal_server/app/schemas/v2/workflow.py +24 -12
  59. fractal_server/app/schemas/v2/workflowtask.py +63 -61
  60. fractal_server/app/security/__init__.py +7 -4
  61. fractal_server/app/security/signup_email.py +21 -12
  62. fractal_server/config.py +123 -75
  63. fractal_server/images/models.py +18 -12
  64. fractal_server/main.py +13 -10
  65. fractal_server/migrations/env.py +16 -63
  66. fractal_server/tasks/v2/local/collect.py +9 -8
  67. fractal_server/tasks/v2/local/deactivate.py +3 -0
  68. fractal_server/tasks/v2/local/reactivate.py +3 -0
  69. fractal_server/tasks/v2/ssh/collect.py +8 -8
  70. fractal_server/tasks/v2/ssh/deactivate.py +3 -0
  71. fractal_server/tasks/v2/ssh/reactivate.py +9 -6
  72. fractal_server/tasks/v2/utils_background.py +1 -1
  73. fractal_server/tasks/v2/utils_database.py +1 -1
  74. {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/METADATA +10 -11
  75. {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/RECORD +78 -81
  76. fractal_server/app/runner/v2/_local_experimental/__init__.py +0 -121
  77. fractal_server/app/runner/v2/_local_experimental/_local_config.py +0 -108
  78. fractal_server/app/runner/v2/_local_experimental/_submit_setup.py +0 -42
  79. fractal_server/app/runner/v2/_local_experimental/executor.py +0 -157
  80. {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/LICENSE +0 -0
  81. {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/WHEEL +0 -0
  82. {fractal_server-2.12.0a1.dist-info → fractal_server-2.13.0.dist-info}/entry_points.txt +0 -0
@@ -3,9 +3,11 @@ from enum import Enum
3
3
  from typing import Optional
4
4
 
5
5
  from pydantic import BaseModel
6
- from pydantic import Extra
6
+ from pydantic import ConfigDict
7
7
  from pydantic import Field
8
- from pydantic import validator
8
+ from pydantic import field_serializer
9
+ from pydantic import field_validator
10
+ from pydantic.types import AwareDatetime
9
11
 
10
12
  from .._validators import val_absolute_path
11
13
  from .._validators import valdict_keys
@@ -32,7 +34,8 @@ class TaskGroupActivityActionV2(str, Enum):
32
34
  REACTIVATE = "reactivate"
33
35
 
34
36
 
35
- class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
37
+ class TaskGroupCreateV2(BaseModel):
38
+ model_config = ConfigDict(extra="forbid")
36
39
  user_id: int
37
40
  user_group_id: Optional[int] = None
38
41
  active: bool = True
@@ -48,21 +51,21 @@ class TaskGroupCreateV2(BaseModel, extra=Extra.forbid):
48
51
  pinned_package_versions: dict[str, str] = Field(default_factory=dict)
49
52
 
50
53
  # Validators
51
- _path = validator("path", allow_reuse=True)(val_absolute_path("path"))
52
- _venv_path = validator("venv_path", allow_reuse=True)(
53
- val_absolute_path("venv_path")
54
+ _path = field_validator("path")(classmethod(val_absolute_path("path")))
55
+ _venv_path = field_validator("venv_path")(
56
+ classmethod(val_absolute_path("venv_path"))
54
57
  )
55
- _wheel_path = validator("wheel_path", allow_reuse=True)(
56
- val_absolute_path("wheel_path")
58
+ _wheel_path = field_validator("wheel_path")(
59
+ classmethod(val_absolute_path("wheel_path"))
57
60
  )
58
- _pinned_package_versions = validator(
59
- "pinned_package_versions", allow_reuse=True
60
- )(valdict_keys("pinned_package_versions"))
61
- _pip_extras = validator("pip_extras", allow_reuse=True)(
62
- valstr("pip_extras")
61
+ _pinned_package_versions = field_validator("pinned_package_versions")(
62
+ valdict_keys("pinned_package_versions")
63
63
  )
64
- _python_version = validator("python_version", allow_reuse=True)(
65
- valstr("python_version")
64
+ _pip_extras = field_validator("pip_extras")(
65
+ classmethod(valstr("pip_extras"))
66
+ )
67
+ _python_version = field_validator("python_version")(
68
+ classmethod(valstr("python_version"))
66
69
  )
67
70
 
68
71
 
@@ -99,11 +102,16 @@ class TaskGroupReadV2(BaseModel):
99
102
  venv_file_number: Optional[int] = None
100
103
 
101
104
  active: bool
102
- timestamp_created: datetime
103
- timestamp_last_used: datetime
105
+ timestamp_created: AwareDatetime
106
+ timestamp_last_used: AwareDatetime
107
+
108
+ @field_serializer("timestamp_created", "timestamp_last_used")
109
+ def serialize_datetime(v: datetime) -> str:
110
+ return v.isoformat()
104
111
 
105
112
 
106
- class TaskGroupUpdateV2(BaseModel, extra=Extra.forbid):
113
+ class TaskGroupUpdateV2(BaseModel):
114
+ model_config = ConfigDict(extra="forbid")
107
115
  user_group_id: Optional[int] = None
108
116
 
109
117
 
@@ -111,10 +119,21 @@ class TaskGroupActivityV2Read(BaseModel):
111
119
  id: int
112
120
  user_id: int
113
121
  taskgroupv2_id: Optional[int] = None
114
- timestamp_started: datetime
115
- timestamp_ended: Optional[datetime] = None
122
+ timestamp_started: AwareDatetime
123
+ timestamp_ended: Optional[AwareDatetime] = None
116
124
  pkg_name: str
117
125
  version: str
118
126
  status: TaskGroupActivityStatusV2
119
127
  action: TaskGroupActivityActionV2
120
128
  log: Optional[str] = None
129
+
130
+ @field_serializer("timestamp_started")
131
+ def serialize_datetime_start(v: datetime) -> str:
132
+ return v.isoformat()
133
+
134
+ @field_serializer("timestamp_ended")
135
+ def serialize_datetime_end(v: Optional[datetime]) -> Optional[str]:
136
+ if v is None:
137
+ return None
138
+ else:
139
+ return v.isoformat()
@@ -2,8 +2,10 @@ from datetime import datetime
2
2
  from typing import Optional
3
3
 
4
4
  from pydantic import BaseModel
5
- from pydantic import Extra
6
- from pydantic import validator
5
+ from pydantic import ConfigDict
6
+ from pydantic import field_serializer
7
+ from pydantic import field_validator
8
+ from pydantic.types import AwareDatetime
7
9
 
8
10
  from .._validators import valstr
9
11
  from .project import ProjectReadV2
@@ -13,12 +15,14 @@ from .workflowtask import WorkflowTaskReadV2
13
15
  from .workflowtask import WorkflowTaskReadV2WithWarning
14
16
 
15
17
 
16
- class WorkflowCreateV2(BaseModel, extra=Extra.forbid):
18
+ class WorkflowCreateV2(BaseModel):
19
+
20
+ model_config = ConfigDict(extra="forbid")
17
21
 
18
22
  name: str
19
23
 
20
24
  # Validators
21
- _name = validator("name", allow_reuse=True)(valstr("name"))
25
+ _name = field_validator("name")(classmethod(valstr("name")))
22
26
 
23
27
 
24
28
  class WorkflowReadV2(BaseModel):
@@ -28,22 +32,29 @@ class WorkflowReadV2(BaseModel):
28
32
  project_id: int
29
33
  task_list: list[WorkflowTaskReadV2]
30
34
  project: ProjectReadV2
31
- timestamp_created: datetime
35
+ timestamp_created: AwareDatetime
36
+
37
+ @field_serializer("timestamp_created")
38
+ def serialize_datetime(v: datetime) -> str:
39
+ return v.isoformat()
32
40
 
33
41
 
34
42
  class WorkflowReadV2WithWarnings(WorkflowReadV2):
35
43
  task_list: list[WorkflowTaskReadV2WithWarning]
36
44
 
37
45
 
38
- class WorkflowUpdateV2(BaseModel, extra=Extra.forbid):
46
+ class WorkflowUpdateV2(BaseModel):
47
+
48
+ model_config = ConfigDict(extra="forbid")
39
49
 
40
- name: Optional[str]
41
- reordered_workflowtask_ids: Optional[list[int]]
50
+ name: Optional[str] = None
51
+ reordered_workflowtask_ids: Optional[list[int]] = None
42
52
 
43
53
  # Validators
44
- _name = validator("name", allow_reuse=True)(valstr("name"))
54
+ _name = field_validator("name")(classmethod(valstr("name")))
45
55
 
46
- @validator("reordered_workflowtask_ids")
56
+ @field_validator("reordered_workflowtask_ids")
57
+ @classmethod
47
58
  def check_positive_and_unique(cls, value):
48
59
  if any(i < 0 for i in value):
49
60
  raise ValueError("Negative `id` in `reordered_workflowtask_ids`")
@@ -52,7 +63,7 @@ class WorkflowUpdateV2(BaseModel, extra=Extra.forbid):
52
63
  return value
53
64
 
54
65
 
55
- class WorkflowImportV2(BaseModel, extra=Extra.forbid):
66
+ class WorkflowImportV2(BaseModel):
56
67
  """
57
68
  Class for `Workflow` import.
58
69
 
@@ -60,11 +71,12 @@ class WorkflowImportV2(BaseModel, extra=Extra.forbid):
60
71
  task_list:
61
72
  """
62
73
 
74
+ model_config = ConfigDict(extra="forbid")
63
75
  name: str
64
76
  task_list: list[WorkflowTaskImportV2]
65
77
 
66
78
  # Validators
67
- _name = validator("name", allow_reuse=True)(valstr("name"))
79
+ _name = field_validator("name")(classmethod(valstr("name")))
68
80
 
69
81
 
70
82
  class WorkflowExportV2(BaseModel):
@@ -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,
@@ -252,15 +252,18 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
252
252
  # Send mail section
253
253
  settings = Inject(get_settings)
254
254
 
255
- if this_user.oauth_accounts and settings.MAIL_SETTINGS is not None:
255
+ if (
256
+ this_user.oauth_accounts
257
+ and settings.email_settings is not None
258
+ ):
256
259
  try:
257
260
  logger.info(
258
261
  "START sending email about new signup to "
259
- f"{settings.MAIL_SETTINGS.recipients}."
262
+ f"{settings.email_settings.recipients}."
260
263
  )
261
264
  mail_new_oauth_signup(
262
265
  msg=f"New user registered: '{this_user.email}'.",
263
- mail_settings=settings.MAIL_SETTINGS,
266
+ email_settings=settings.email_settings,
264
267
  )
265
268
  logger.info("END sending email about new signup.")
266
269
  except Exception as e:
@@ -2,38 +2,47 @@ import smtplib
2
2
  from email.message import EmailMessage
3
3
  from email.utils import formataddr
4
4
 
5
+ from cryptography.fernet import Fernet
6
+
5
7
  from fractal_server.config import MailSettings
6
8
 
7
9
 
8
- def mail_new_oauth_signup(msg: str, mail_settings: MailSettings):
10
+ def mail_new_oauth_signup(msg: str, email_settings: MailSettings):
9
11
  """
10
12
  Send an email using the specified settings to notify a new OAuth signup.
11
13
  """
12
14
 
13
15
  mail_msg = EmailMessage()
14
16
  mail_msg.set_content(msg)
15
- mail_msg["From"] = formataddr((mail_settings.sender, mail_settings.sender))
17
+ mail_msg["From"] = formataddr(
18
+ (email_settings.sender, email_settings.sender)
19
+ )
16
20
  mail_msg["To"] = ", ".join(
17
21
  [
18
22
  formataddr((recipient, recipient))
19
- for recipient in mail_settings.recipients
23
+ for recipient in email_settings.recipients
20
24
  ]
21
25
  )
22
26
  mail_msg[
23
27
  "Subject"
24
- ] = f"[Fractal, {mail_settings.instance_name}] New OAuth signup"
28
+ ] = f"[Fractal, {email_settings.instance_name}] New OAuth signup"
25
29
 
26
- with smtplib.SMTP(mail_settings.smtp_server, mail_settings.port) as server:
30
+ with smtplib.SMTP(
31
+ email_settings.smtp_server, email_settings.port
32
+ ) as server:
27
33
  server.ehlo()
28
- if mail_settings.use_starttls:
34
+ if email_settings.use_starttls:
29
35
  server.starttls()
30
36
  server.ehlo()
31
-
32
- server.login(
33
- user=mail_settings.sender, password=mail_settings.password
34
- )
37
+ if email_settings.use_login:
38
+ password = (
39
+ Fernet(email_settings.encryption_key)
40
+ .decrypt(email_settings.encrypted_password)
41
+ .decode("utf-8")
42
+ )
43
+ server.login(user=email_settings.sender, password=password)
35
44
  server.sendmail(
36
- from_addr=mail_settings.sender,
37
- to_addrs=mail_settings.recipients,
45
+ from_addr=email_settings.sender,
46
+ to_addrs=email_settings.recipients,
38
47
  msg=mail_msg.as_string(),
39
48
  )