fractal-server 2.12.0a0__py3-none-any.whl → 2.12.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/__main__.py +17 -63
- fractal_server/app/routes/api/v2/images.py +0 -12
- fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +0 -1
- fractal_server/app/runner/executors/slurm/ssh/executor.py +23 -29
- fractal_server/app/runner/executors/slurm/sudo/executor.py +51 -20
- fractal_server/app/schemas/_validators.py +0 -19
- fractal_server/app/security/__init__.py +6 -3
- fractal_server/app/security/signup_email.py +21 -12
- fractal_server/config.py +92 -51
- fractal_server/main.py +12 -9
- fractal_server/migrations/env.py +16 -63
- fractal_server/tasks/utils.py +0 -4
- 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-2.12.0a0.dist-info → fractal_server-2.12.1.dist-info}/METADATA +2 -2
- {fractal_server-2.12.0a0.dist-info → fractal_server-2.12.1.dist-info}/RECORD +24 -24
- {fractal_server-2.12.0a0.dist-info → fractal_server-2.12.1.dist-info}/LICENSE +0 -0
- {fractal_server-2.12.0a0.dist-info → fractal_server-2.12.1.dist-info}/WHEEL +0 -0
- {fractal_server-2.12.0a0.dist-info → fractal_server-2.12.1.dist-info}/entry_points.txt +0 -0
fractal_server/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "2.12.
|
1
|
+
__VERSION__ = "2.12.1"
|
fractal_server/__main__.py
CHANGED
@@ -63,45 +63,18 @@ update_db_data_parser = subparsers.add_parser(
|
|
63
63
|
description="Apply data-migration script to an existing database.",
|
64
64
|
)
|
65
65
|
|
66
|
-
# fractalctl email-
|
67
|
-
|
68
|
-
"email-
|
66
|
+
# fractalctl encrypt-email-password
|
67
|
+
encrypt_email_password_parser = subparsers.add_parser(
|
68
|
+
"encrypt-email-password",
|
69
69
|
description=(
|
70
70
|
"Generate valid values for environment variables "
|
71
|
-
"
|
71
|
+
"FRACTAL_EMAIL_PASSWORD and FRACTAL_EMAIL_PASSWORD_KEY."
|
72
72
|
),
|
73
73
|
)
|
74
|
-
email_settings_parser.add_argument(
|
75
|
-
"sender",
|
76
|
-
type=str,
|
77
|
-
help="Email of the sender",
|
78
|
-
)
|
79
|
-
email_settings_parser.add_argument(
|
80
|
-
"server",
|
81
|
-
type=str,
|
82
|
-
help="SMPT server used to send emails",
|
83
|
-
)
|
84
|
-
email_settings_parser.add_argument(
|
85
|
-
"port",
|
86
|
-
type=int,
|
87
|
-
help="Port of the SMPT server",
|
88
|
-
)
|
89
|
-
email_settings_parser.add_argument(
|
90
|
-
"instance",
|
91
|
-
type=str,
|
92
|
-
help="Name of the Fractal instance sending emails",
|
93
|
-
)
|
94
|
-
email_settings_parser.add_argument(
|
95
|
-
"--skip-starttls",
|
96
|
-
action="store_true",
|
97
|
-
default=False,
|
98
|
-
help="If set, skip the execution of `starttls` when sending emails",
|
99
|
-
)
|
100
74
|
|
101
75
|
|
102
76
|
def save_openapi(dest="openapi.json"):
|
103
77
|
from fractal_server.main import start_application
|
104
|
-
import json
|
105
78
|
|
106
79
|
app = start_application()
|
107
80
|
openapi_schema = app.openapi()
|
@@ -129,6 +102,11 @@ def set_db(skip_init_data: bool = False):
|
|
129
102
|
from pathlib import Path
|
130
103
|
import fractal_server
|
131
104
|
|
105
|
+
# Check settings
|
106
|
+
settings = Inject(get_settings)
|
107
|
+
settings.check_db()
|
108
|
+
|
109
|
+
# Perform migrations
|
132
110
|
alembic_ini = Path(fractal_server.__file__).parent / "alembic.ini"
|
133
111
|
alembic_args = ["-c", alembic_ini.as_posix(), "upgrade", "head"]
|
134
112
|
print(f"START: Run alembic.config, with argv={alembic_args}")
|
@@ -138,12 +116,10 @@ def set_db(skip_init_data: bool = False):
|
|
138
116
|
if skip_init_data:
|
139
117
|
return
|
140
118
|
|
141
|
-
#
|
119
|
+
# Create default group and user
|
142
120
|
print()
|
143
121
|
_create_first_group()
|
144
122
|
print()
|
145
|
-
# NOTE: It will be fixed with #1739
|
146
|
-
settings = Inject(get_settings)
|
147
123
|
asyncio.run(
|
148
124
|
_create_first_user(
|
149
125
|
email=settings.FRACTAL_DEFAULT_ADMIN_EMAIL,
|
@@ -224,31 +200,15 @@ def update_db_data():
|
|
224
200
|
current_update_db_data_module.fix_db()
|
225
201
|
|
226
202
|
|
227
|
-
def
|
228
|
-
sender: str,
|
229
|
-
server: str,
|
230
|
-
port: int,
|
231
|
-
instance: str,
|
232
|
-
skip_starttls: bool,
|
233
|
-
):
|
203
|
+
def print_encrypted_password():
|
234
204
|
from cryptography.fernet import Fernet
|
235
205
|
|
236
|
-
password = input(
|
206
|
+
password = input("Insert email password: ").encode("utf-8")
|
237
207
|
key = Fernet.generate_key().decode("utf-8")
|
238
|
-
|
239
|
-
dict(
|
240
|
-
sender=sender,
|
241
|
-
password=password,
|
242
|
-
smtp_server=server,
|
243
|
-
port=port,
|
244
|
-
instance_name=instance,
|
245
|
-
use_starttls=(not skip_starttls),
|
246
|
-
)
|
247
|
-
).encode("utf-8")
|
248
|
-
email_settings = Fernet(key).encrypt(fractal_mail_settings).decode("utf-8")
|
208
|
+
encrypted_password = Fernet(key).encrypt(password).decode("utf-8")
|
249
209
|
|
250
|
-
print(f"\
|
251
|
-
print(f"
|
210
|
+
print(f"\nFRACTAL_EMAIL_PASSWORD={encrypted_password}")
|
211
|
+
print(f"FRACTAL_EMAIL_PASSWORD_KEY={key}")
|
252
212
|
|
253
213
|
|
254
214
|
def run():
|
@@ -267,14 +227,8 @@ def run():
|
|
267
227
|
port=args.port,
|
268
228
|
reload=args.reload,
|
269
229
|
)
|
270
|
-
elif args.cmd == "email-
|
271
|
-
|
272
|
-
sender=args.sender,
|
273
|
-
server=args.server,
|
274
|
-
port=args.port,
|
275
|
-
instance=args.instance,
|
276
|
-
skip_starttls=args.skip_starttls,
|
277
|
-
)
|
230
|
+
elif args.cmd == "encrypt-email-password":
|
231
|
+
print_encrypted_password()
|
278
232
|
else:
|
279
233
|
sys.exit(f"Error: invalid command '{args.cmd}'.")
|
280
234
|
|
@@ -118,7 +118,6 @@ async def post_new_image(
|
|
118
118
|
async def query_dataset_images(
|
119
119
|
project_id: int,
|
120
120
|
dataset_id: int,
|
121
|
-
use_dataset_filters: bool = False, # query param
|
122
121
|
page: int = 1, # query param
|
123
122
|
page_size: Optional[int] = None, # query param
|
124
123
|
query: Optional[ImageQuery] = None, # body
|
@@ -138,17 +137,6 @@ async def query_dataset_images(
|
|
138
137
|
dataset = output["dataset"]
|
139
138
|
images = dataset.images
|
140
139
|
|
141
|
-
if use_dataset_filters is True:
|
142
|
-
images = [
|
143
|
-
image
|
144
|
-
for image in images
|
145
|
-
if match_filter(
|
146
|
-
image=image,
|
147
|
-
type_filters=dataset.type_filters,
|
148
|
-
attribute_filters=dataset.attribute_filters,
|
149
|
-
)
|
150
|
-
]
|
151
|
-
|
152
140
|
attributes = {}
|
153
141
|
for image in images:
|
154
142
|
for k, v in image["attributes"].items():
|
@@ -88,7 +88,6 @@ class SlurmJob:
|
|
88
88
|
self,
|
89
89
|
num_tasks_tot: int,
|
90
90
|
slurm_config: SlurmConfig,
|
91
|
-
workflow_task_file_prefix: Optional[str] = None,
|
92
91
|
slurm_file_prefix: Optional[str] = None,
|
93
92
|
wftask_file_prefixes: Optional[tuple[str, ...]] = None,
|
94
93
|
single_task_submission: bool = False,
|
@@ -62,7 +62,6 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
62
62
|
shutdown_file:
|
63
63
|
python_remote: Equal to `settings.FRACTAL_SLURM_WORKER_PYTHON`
|
64
64
|
wait_thread_cls: Class for waiting thread
|
65
|
-
keep_pickle_files:
|
66
65
|
workflow_dir_local:
|
67
66
|
Directory for both the cfut/SLURM and fractal-server files and logs
|
68
67
|
workflow_dir_remote:
|
@@ -84,7 +83,6 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
84
83
|
python_remote: str
|
85
84
|
|
86
85
|
wait_thread_cls = FractalSlurmWaitThread
|
87
|
-
keep_pickle_files: bool
|
88
86
|
|
89
87
|
common_script_lines: list[str]
|
90
88
|
slurm_account: Optional[str]
|
@@ -100,8 +98,6 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
100
98
|
# Folders and files
|
101
99
|
workflow_dir_local: Path,
|
102
100
|
workflow_dir_remote: Path,
|
103
|
-
# Runner options
|
104
|
-
keep_pickle_files: bool = False,
|
105
101
|
# Monitoring options
|
106
102
|
slurm_poll_interval: Optional[int] = None,
|
107
103
|
# SLURM submission script options
|
@@ -120,7 +116,6 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
120
116
|
fractal_ssh:
|
121
117
|
workflow_dir_local:
|
122
118
|
workflow_dir_remote:
|
123
|
-
keep_pickle_files:
|
124
119
|
slurm_poll_interval:
|
125
120
|
common_script_lines:
|
126
121
|
slurm_account:
|
@@ -194,7 +189,6 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
194
189
|
raise e
|
195
190
|
|
196
191
|
# Set/initialize some more options
|
197
|
-
self.keep_pickle_files = keep_pickle_files
|
198
192
|
self.map_jobid_to_slurm_files_local = {}
|
199
193
|
|
200
194
|
def _validate_common_script_lines(self):
|
@@ -385,9 +379,7 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
385
379
|
args_batches = []
|
386
380
|
batch_size = tasks_per_job
|
387
381
|
for ind_chunk in range(0, tot_tasks, batch_size):
|
388
|
-
args_batches.append(
|
389
|
-
list_args[ind_chunk : ind_chunk + batch_size] # noqa
|
390
|
-
)
|
382
|
+
args_batches.append(list_args[ind_chunk : ind_chunk + batch_size])
|
391
383
|
if len(args_batches) != math.ceil(tot_tasks / tasks_per_job):
|
392
384
|
raise RuntimeError("Something wrong here while batching tasks")
|
393
385
|
|
@@ -536,10 +528,15 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
536
528
|
_prefixes = []
|
537
529
|
_subfolder_names = []
|
538
530
|
for component in components:
|
539
|
-
|
531
|
+
# In Fractal, `component` is `dict` by construction (e.g.
|
532
|
+
# `component = {"zarr_url": "/something", "param": 1}``). The
|
533
|
+
# try/except covers the case of e.g. `executor.map([1, 2])`,
|
534
|
+
# which is useful for testing.
|
535
|
+
try:
|
540
536
|
actual_component = component.get(_COMPONENT_KEY_, None)
|
541
|
-
|
542
|
-
actual_component = component
|
537
|
+
except AttributeError:
|
538
|
+
actual_component = str(component)
|
539
|
+
|
543
540
|
_task_file_paths = get_task_file_paths(
|
544
541
|
workflow_dir_local=task_files.workflow_dir_local,
|
545
542
|
workflow_dir_remote=task_files.workflow_dir_remote,
|
@@ -898,12 +895,11 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
898
895
|
pass
|
899
896
|
for job_id in remaining_job_ids:
|
900
897
|
self._cleanup(job_id)
|
901
|
-
|
902
|
-
for
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
path.unlink()
|
898
|
+
for job in remaining_jobs:
|
899
|
+
for path in job.output_pickle_files_local:
|
900
|
+
path.unlink()
|
901
|
+
for path in job.input_pickle_files_local:
|
902
|
+
path.unlink()
|
907
903
|
|
908
904
|
def _completion(self, job_ids: list[str]) -> None:
|
909
905
|
"""
|
@@ -998,8 +994,7 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
998
994
|
f"Future {future} (SLURM job ID: {job_id}) "
|
999
995
|
"was already cancelled."
|
1000
996
|
)
|
1001
|
-
|
1002
|
-
in_path.unlink()
|
997
|
+
in_path.unlink()
|
1003
998
|
self._cleanup(job_id)
|
1004
999
|
self._handle_remaining_jobs(
|
1005
1000
|
remaining_futures=remaining_futures,
|
@@ -1059,17 +1054,15 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
1059
1054
|
remaining_job_ids=remaining_job_ids,
|
1060
1055
|
)
|
1061
1056
|
return
|
1062
|
-
|
1063
|
-
out_path.unlink()
|
1057
|
+
out_path.unlink()
|
1064
1058
|
except InvalidStateError:
|
1065
1059
|
logger.warning(
|
1066
1060
|
f"Future {future} (SLURM job ID: {job_id}) was "
|
1067
1061
|
"already cancelled, exit from "
|
1068
1062
|
"FractalSlurmSSHExecutor._completion."
|
1069
1063
|
)
|
1070
|
-
|
1071
|
-
|
1072
|
-
in_path.unlink()
|
1064
|
+
out_path.unlink()
|
1065
|
+
in_path.unlink()
|
1073
1066
|
|
1074
1067
|
self._cleanup(job_id)
|
1075
1068
|
self._handle_remaining_jobs(
|
@@ -1079,8 +1072,7 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
1079
1072
|
return
|
1080
1073
|
|
1081
1074
|
# Clean up input pickle file
|
1082
|
-
|
1083
|
-
in_path.unlink()
|
1075
|
+
in_path.unlink()
|
1084
1076
|
self._cleanup(job_id)
|
1085
1077
|
if job.single_task_submission:
|
1086
1078
|
future.set_result(outputs[0])
|
@@ -1207,8 +1199,10 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
1207
1199
|
script_lines = slurm_config.sort_script_lines(script_lines)
|
1208
1200
|
logger.debug(script_lines)
|
1209
1201
|
|
1210
|
-
# Always print output of `pwd`
|
1211
|
-
script_lines.append(
|
1202
|
+
# Always print output of `uname -n` and `pwd`
|
1203
|
+
script_lines.append(
|
1204
|
+
'"Hostname: `uname -n`; current directory: `pwd`"\n'
|
1205
|
+
)
|
1212
1206
|
|
1213
1207
|
# Complete script preamble
|
1214
1208
|
script_lines.append("\n")
|
@@ -10,6 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
|
12
12
|
# University of Zurich
|
13
|
+
import json
|
13
14
|
import math
|
14
15
|
import shlex
|
15
16
|
import subprocess # nosec
|
@@ -161,7 +162,6 @@ class SlurmJob:
|
|
161
162
|
self,
|
162
163
|
num_tasks_tot: int,
|
163
164
|
slurm_config: SlurmConfig,
|
164
|
-
workflow_task_file_prefix: Optional[str] = None,
|
165
165
|
slurm_file_prefix: Optional[str] = None,
|
166
166
|
wftask_file_prefixes: Optional[tuple[str, ...]] = None,
|
167
167
|
single_task_submission: bool = False,
|
@@ -219,7 +219,6 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
219
219
|
workflow_dir_local: Path
|
220
220
|
workflow_dir_remote: Path
|
221
221
|
map_jobid_to_slurm_files: dict[str, tuple[str, str, str]]
|
222
|
-
keep_pickle_files: bool
|
223
222
|
slurm_account: Optional[str]
|
224
223
|
jobs: dict[str, tuple[Future, SlurmJob]]
|
225
224
|
|
@@ -232,7 +231,6 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
232
231
|
user_cache_dir: Optional[str] = None,
|
233
232
|
common_script_lines: Optional[list[str]] = None,
|
234
233
|
slurm_poll_interval: Optional[int] = None,
|
235
|
-
keep_pickle_files: bool = False,
|
236
234
|
slurm_account: Optional[str] = None,
|
237
235
|
*args,
|
238
236
|
**kwargs,
|
@@ -253,11 +251,18 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
253
251
|
# raised within `__init__`).
|
254
252
|
self.wait_thread.shutdown_callback = self.shutdown
|
255
253
|
|
256
|
-
self.keep_pickle_files = keep_pickle_files
|
257
254
|
self.slurm_user = slurm_user
|
258
255
|
self.slurm_account = slurm_account
|
259
256
|
|
260
257
|
self.common_script_lines = common_script_lines or []
|
258
|
+
settings = Inject(get_settings)
|
259
|
+
|
260
|
+
if settings.FRACTAL_SLURM_WORKER_PYTHON is not None:
|
261
|
+
try:
|
262
|
+
self.check_remote_python_interpreter()
|
263
|
+
except Exception as e:
|
264
|
+
self._stop_and_join_wait_thread()
|
265
|
+
raise RuntimeError(f"Original error {str(e)}")
|
261
266
|
|
262
267
|
# Check that SLURM account is not set here
|
263
268
|
try:
|
@@ -289,7 +294,6 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
289
294
|
# Set the attribute slurm_poll_interval for self.wait_thread (see
|
290
295
|
# cfut.SlurmWaitThread)
|
291
296
|
if not slurm_poll_interval:
|
292
|
-
settings = Inject(get_settings)
|
293
297
|
slurm_poll_interval = settings.FRACTAL_SLURM_POLL_INTERVAL
|
294
298
|
self.wait_thread.slurm_poll_interval = slurm_poll_interval
|
295
299
|
self.wait_thread.slurm_user = self.slurm_user
|
@@ -608,11 +612,14 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
608
612
|
_prefixes = []
|
609
613
|
_subfolder_names = []
|
610
614
|
for component in components:
|
611
|
-
|
612
|
-
|
615
|
+
# In Fractal, `component` is a `dict` by construction (e.g.
|
616
|
+
# `component = {"zarr_url": "/something", "param": 1}``). The
|
617
|
+
# try/except covers the case of e.g. `executor.map([1, 2])`,
|
618
|
+
# which is useful for testing.
|
619
|
+
try:
|
613
620
|
actual_component = component.get(_COMPONENT_KEY_, None)
|
614
|
-
|
615
|
-
actual_component = component
|
621
|
+
except AttributeError:
|
622
|
+
actual_component = str(component)
|
616
623
|
_task_file_paths = get_task_file_paths(
|
617
624
|
workflow_dir_local=task_files.workflow_dir_local,
|
618
625
|
workflow_dir_remote=task_files.workflow_dir_remote,
|
@@ -864,8 +871,7 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
864
871
|
" cancelled, exit from"
|
865
872
|
" FractalSlurmExecutor._completion."
|
866
873
|
)
|
867
|
-
|
868
|
-
in_path.unlink()
|
874
|
+
in_path.unlink()
|
869
875
|
self._cleanup(jobid)
|
870
876
|
return
|
871
877
|
|
@@ -907,23 +913,20 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
907
913
|
exc = TaskExecutionError(proxy.tb, **kwargs)
|
908
914
|
fut.set_exception(exc)
|
909
915
|
return
|
910
|
-
|
911
|
-
out_path.unlink()
|
916
|
+
out_path.unlink()
|
912
917
|
except InvalidStateError:
|
913
918
|
logger.warning(
|
914
919
|
f"Future {fut} (SLURM job ID: {jobid}) was already"
|
915
920
|
" cancelled, exit from"
|
916
921
|
" FractalSlurmExecutor._completion."
|
917
922
|
)
|
918
|
-
|
919
|
-
|
920
|
-
in_path.unlink()
|
923
|
+
out_path.unlink()
|
924
|
+
in_path.unlink()
|
921
925
|
self._cleanup(jobid)
|
922
926
|
return
|
923
927
|
|
924
928
|
# Clean up input pickle file
|
925
|
-
|
926
|
-
in_path.unlink()
|
929
|
+
in_path.unlink()
|
927
930
|
self._cleanup(jobid)
|
928
931
|
if job.single_task_submission:
|
929
932
|
fut.set_result(outputs[0])
|
@@ -1159,8 +1162,10 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
1159
1162
|
script_lines = slurm_config.sort_script_lines(script_lines)
|
1160
1163
|
logger.debug(script_lines)
|
1161
1164
|
|
1162
|
-
# Always print output of `pwd`
|
1163
|
-
script_lines.append(
|
1165
|
+
# Always print output of `uname -n` and `pwd`
|
1166
|
+
script_lines.append(
|
1167
|
+
'"Hostname: `uname -n`; current directory: `pwd`"\n'
|
1168
|
+
)
|
1164
1169
|
|
1165
1170
|
# Complete script preamble
|
1166
1171
|
script_lines.append("\n")
|
@@ -1247,3 +1252,29 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
1247
1252
|
)
|
1248
1253
|
self._stop_and_join_wait_thread()
|
1249
1254
|
logger.debug("[FractalSlurmExecutor.__exit__] End")
|
1255
|
+
|
1256
|
+
def check_remote_python_interpreter(self):
|
1257
|
+
"""
|
1258
|
+
Check fractal-server version on the _remote_ Python interpreter.
|
1259
|
+
"""
|
1260
|
+
settings = Inject(get_settings)
|
1261
|
+
output = _subprocess_run_or_raise(
|
1262
|
+
(
|
1263
|
+
f"{settings.FRACTAL_SLURM_WORKER_PYTHON} "
|
1264
|
+
"-m fractal_server.app.runner.versions"
|
1265
|
+
)
|
1266
|
+
)
|
1267
|
+
runner_version = json.loads(output.stdout.strip("\n"))[
|
1268
|
+
"fractal_server"
|
1269
|
+
]
|
1270
|
+
|
1271
|
+
if runner_version != __VERSION__:
|
1272
|
+
error_msg = (
|
1273
|
+
"Fractal-server version mismatch.\n"
|
1274
|
+
"Local interpreter: "
|
1275
|
+
f"({sys.executable}): {__VERSION__}.\n"
|
1276
|
+
"Remote interpreter: "
|
1277
|
+
f"({settings.FRACTAL_SLURM_WORKER_PYTHON}): {runner_version}."
|
1278
|
+
)
|
1279
|
+
logger.error(error_msg)
|
1280
|
+
raise ValueError(error_msg)
|
@@ -48,25 +48,6 @@ def valdict_keys(attribute: str):
|
|
48
48
|
return val
|
49
49
|
|
50
50
|
|
51
|
-
def valint(attribute: str, min_val: int = 1):
|
52
|
-
"""
|
53
|
-
Check that an integer attribute (e.g. if it is meant to be the ID of a
|
54
|
-
database entry) is greater or equal to min_val.
|
55
|
-
"""
|
56
|
-
|
57
|
-
def val(integer: Optional[int]) -> Optional[int]:
|
58
|
-
if integer is None:
|
59
|
-
raise ValueError(f"Integer attribute '{attribute}' cannot be None")
|
60
|
-
if integer < min_val:
|
61
|
-
raise ValueError(
|
62
|
-
f"Integer attribute '{attribute}' cannot be less than "
|
63
|
-
f"{min_val} (given {integer})"
|
64
|
-
)
|
65
|
-
return integer
|
66
|
-
|
67
|
-
return val
|
68
|
-
|
69
|
-
|
70
51
|
def val_absolute_path(attribute: str, accept_none: bool = False):
|
71
52
|
"""
|
72
53
|
Check that a string attribute is an absolute path
|
@@ -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
|
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.
|
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
|
-
|
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,
|
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(
|
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
|
23
|
+
for recipient in email_settings.recipients
|
20
24
|
]
|
21
25
|
)
|
22
26
|
mail_msg[
|
23
27
|
"Subject"
|
24
|
-
] = f"[Fractal, {
|
28
|
+
] = f"[Fractal, {email_settings.instance_name}] New OAuth signup"
|
25
29
|
|
26
|
-
with smtplib.SMTP(
|
30
|
+
with smtplib.SMTP(
|
31
|
+
email_settings.smtp_server, email_settings.port
|
32
|
+
) as server:
|
27
33
|
server.ehlo()
|
28
|
-
if
|
34
|
+
if email_settings.use_starttls:
|
29
35
|
server.starttls()
|
30
36
|
server.ehlo()
|
31
|
-
|
32
|
-
|
33
|
-
|
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=
|
37
|
-
to_addrs=
|
45
|
+
from_addr=email_settings.sender,
|
46
|
+
to_addrs=email_settings.recipients,
|
38
47
|
msg=mail_msg.as_string(),
|
39
48
|
)
|