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.
@@ -1 +1 @@
1
- __VERSION__ = "2.12.0a0"
1
+ __VERSION__ = "2.12.1"
@@ -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-settings
67
- email_settings_parser = subparsers.add_parser(
68
- "email-settings",
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
- "`FRACTAL_EMAIL_SETTINGS` and `FRACTAL_EMAIL_SETTINGS_KEY`."
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
- # Insert default group
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 print_mail_settings(
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(f"Insert email password for sender '{sender}': ")
206
+ password = input("Insert email password: ").encode("utf-8")
237
207
  key = Fernet.generate_key().decode("utf-8")
238
- fractal_mail_settings = json.dumps(
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"\nFRACTAL_EMAIL_SETTINGS: {email_settings}")
251
- print(f"FRACTAL_EMAIL_SETTINGS_KEY: {key}")
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-settings":
271
- print_mail_settings(
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
- if isinstance(component, dict):
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
- else:
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
- if not self.keep_pickle_files:
902
- for job in remaining_jobs:
903
- for path in job.output_pickle_files_local:
904
- path.unlink()
905
- for path in job.input_pickle_files_local:
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
- if not self.keep_pickle_files:
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
- if not self.keep_pickle_files:
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
- if not self.keep_pickle_files:
1071
- out_path.unlink()
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
- if not self.keep_pickle_files:
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('echo "Working directory (pwd): `pwd`"\n')
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
- if isinstance(component, dict):
612
- # This is needed for V2
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
- else:
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
- if not self.keep_pickle_files:
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
- if not self.keep_pickle_files:
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
- if not self.keep_pickle_files:
919
- out_path.unlink()
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
- if not self.keep_pickle_files:
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('echo "Working directory (pwd): `pwd`"\n')
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 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
  )