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
fractal_server/config.py CHANGED
@@ -11,7 +11,6 @@
11
11
  # <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
12
12
  # Institute for Biomedical Research and Pelkmans Lab from the University of
13
13
  # Zurich.
14
- import json
15
14
  import logging
16
15
  import shutil
17
16
  import sys
@@ -25,11 +24,12 @@ from typing import TypeVar
25
24
  from cryptography.fernet import Fernet
26
25
  from dotenv import load_dotenv
27
26
  from pydantic import BaseModel
28
- from pydantic import BaseSettings
29
27
  from pydantic import EmailStr
30
28
  from pydantic import Field
31
- from pydantic import root_validator
32
- 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
33
33
  from sqlalchemy.engine import URL
34
34
 
35
35
  import fractal_server
@@ -46,16 +46,19 @@ class MailSettings(BaseModel):
46
46
  port: SMTP server port
47
47
  password: Sender password
48
48
  instance_name: Name of SMTP server instance
49
- use_starttls: Using or not security protocol
49
+ use_starttls: Whether to use the security protocol
50
+ use_login: Whether to use login
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
- password: str
57
+ encrypted_password: Optional[str] = None
58
+ encryption_key: Optional[str] = None
57
59
  instance_name: str
58
60
  use_starttls: bool
61
+ use_login: bool
59
62
 
60
63
 
61
64
  class FractalConfigurationError(RuntimeError):
@@ -95,10 +98,11 @@ class OAuthClientConfig(BaseModel):
95
98
  CLIENT_NAME: str
96
99
  CLIENT_ID: str
97
100
  CLIENT_SECRET: str
98
- OIDC_CONFIGURATION_ENDPOINT: Optional[str]
101
+ OIDC_CONFIGURATION_ENDPOINT: Optional[str] = None
99
102
  REDIRECT_URL: Optional[str] = None
100
103
 
101
- @root_validator
104
+ @model_validator(mode="before")
105
+ @classmethod
102
106
  def check_configuration(cls, values):
103
107
  if values.get("CLIENT_NAME") not in ["GOOGLE", "GITHUB"]:
104
108
  if not values.get("OIDC_CONFIGURATION_ENDPOINT"):
@@ -116,8 +120,7 @@ class Settings(BaseSettings):
116
120
  The attributes of this class are set from the environment.
117
121
  """
118
122
 
119
- class Config:
120
- case_sensitive = True
123
+ model_config = SettingsConfigDict(case_sensitive=True)
121
124
 
122
125
  PROJECT_NAME: str = "Fractal Server"
123
126
  PROJECT_VERSION: str = fractal_server.__VERSION__
@@ -134,7 +137,7 @@ class Settings(BaseSettings):
134
137
  JWT token lifetime, in seconds.
135
138
  """
136
139
 
137
- JWT_SECRET_KEY: Optional[str]
140
+ JWT_SECRET_KEY: Optional[str] = None
138
141
  """
139
142
  JWT secret
140
143
 
@@ -148,7 +151,8 @@ class Settings(BaseSettings):
148
151
  Cookie token lifetime, in seconds.
149
152
  """
150
153
 
151
- @root_validator(pre=True)
154
+ @model_validator(mode="before")
155
+ @classmethod
152
156
  def collect_oauth_clients(cls, values):
153
157
  """
154
158
  Automatic collection of OAuth Clients
@@ -196,11 +200,11 @@ class Settings(BaseSettings):
196
200
  """
197
201
  If `True`, make database operations verbose.
198
202
  """
199
- POSTGRES_USER: Optional[str]
203
+ POSTGRES_USER: Optional[str] = None
200
204
  """
201
205
  User to use when connecting to the PostgreSQL database.
202
206
  """
203
- POSTGRES_PASSWORD: Optional[str]
207
+ POSTGRES_PASSWORD: Optional[str] = None
204
208
  """
205
209
  Password to use when connecting to the PostgreSQL database.
206
210
  """
@@ -212,7 +216,7 @@ class Settings(BaseSettings):
212
216
  """
213
217
  Port number to use when connecting to the PostgreSQL server.
214
218
  """
215
- POSTGRES_DB: Optional[str]
219
+ POSTGRES_DB: Optional[str] = None
216
220
  """
217
221
  Name of the PostgreSQL database to connect to.
218
222
  """
@@ -264,13 +268,14 @@ class Settings(BaseSettings):
264
268
  default admin credentials.
265
269
  """
266
270
 
267
- FRACTAL_TASKS_DIR: Optional[Path]
271
+ FRACTAL_TASKS_DIR: Optional[Path] = None
268
272
  """
269
273
  Directory under which all the tasks will be saved (either an absolute path
270
274
  or a path relative to current working directory).
271
275
  """
272
276
 
273
- @validator("FRACTAL_TASKS_DIR", always=True)
277
+ @field_validator("FRACTAL_TASKS_DIR")
278
+ @classmethod
274
279
  def make_FRACTAL_TASKS_DIR_absolute(cls, v):
275
280
  """
276
281
  If `FRACTAL_TASKS_DIR` is a non-absolute path, make it absolute (based
@@ -287,7 +292,8 @@ class Settings(BaseSettings):
287
292
  )
288
293
  return FRACTAL_TASKS_DIR_path
289
294
 
290
- @validator("FRACTAL_RUNNER_WORKING_BASE_DIR", always=True)
295
+ @field_validator("FRACTAL_RUNNER_WORKING_BASE_DIR")
296
+ @classmethod
291
297
  def make_FRACTAL_RUNNER_WORKING_BASE_DIR_absolute(cls, v):
292
298
  """
293
299
  (Copy of make_FRACTAL_TASKS_DIR_absolute)
@@ -310,7 +316,6 @@ class Settings(BaseSettings):
310
316
 
311
317
  FRACTAL_RUNNER_BACKEND: Literal[
312
318
  "local",
313
- "local_experimental",
314
319
  "slurm",
315
320
  "slurm_ssh",
316
321
  ] = "local"
@@ -318,7 +323,7 @@ class Settings(BaseSettings):
318
323
  Select which runner backend to use.
319
324
  """
320
325
 
321
- FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path]
326
+ FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path] = None
322
327
  """
323
328
  Base directory for running jobs / workflows. All artifacts required to set
324
329
  up, run and tear down jobs are placed in subdirs of this directory.
@@ -331,7 +336,7 @@ class Settings(BaseSettings):
331
336
  Only logs of with this level (or higher) will appear in the console logs.
332
337
  """
333
338
 
334
- FRACTAL_LOCAL_CONFIG_FILE: Optional[Path]
339
+ FRACTAL_LOCAL_CONFIG_FILE: Optional[Path] = None
335
340
  """
336
341
  Path of JSON file with configuration for the local backend.
337
342
  """
@@ -347,7 +352,7 @@ class Settings(BaseSettings):
347
352
  Waiting time for the shutdown phase of executors
348
353
  """
349
354
 
350
- FRACTAL_SLURM_CONFIG_FILE: Optional[Path]
355
+ FRACTAL_SLURM_CONFIG_FILE: Optional[Path] = None
351
356
  """
352
357
  Path of JSON file with configuration for the SLURM backend.
353
358
  """
@@ -358,7 +363,8 @@ class Settings(BaseSettings):
358
363
  nodes. If not specified, the same interpreter that runs the server is used.
359
364
  """
360
365
 
361
- @validator("FRACTAL_SLURM_WORKER_PYTHON", always=True)
366
+ @field_validator("FRACTAL_SLURM_WORKER_PYTHON")
367
+ @classmethod
362
368
  def absolute_FRACTAL_SLURM_WORKER_PYTHON(cls, v):
363
369
  """
364
370
  If `FRACTAL_SLURM_WORKER_PYTHON` is a relative path, fail.
@@ -405,8 +411,9 @@ class Settings(BaseSettings):
405
411
  Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.12.
406
412
  """
407
413
 
408
- @root_validator(pre=True)
409
- def check_tasks_python(cls, values) -> None:
414
+ @model_validator(mode="before")
415
+ @classmethod
416
+ def check_tasks_python(cls, values):
410
417
  """
411
418
  Perform multiple checks of the Python-interpreter variables.
412
419
 
@@ -416,7 +423,6 @@ class Settings(BaseSettings):
416
423
  `sys.executable` and set the corresponding
417
424
  `FRACTAL_TASKS_PYTHON_X_Y` (and unset all others).
418
425
  """
419
-
420
426
  # `FRACTAL_TASKS_PYTHON_X_Y` variables can only be absolute paths
421
427
  for version in ["3_9", "3_10", "3_11", "3_12"]:
422
428
  key = f"FRACTAL_TASKS_PYTHON_{version}"
@@ -510,7 +516,8 @@ class Settings(BaseSettings):
510
516
  `--no-cache-dir` is used.
511
517
  """
512
518
 
513
- @validator("FRACTAL_PIP_CACHE_DIR", always=True)
519
+ @field_validator("FRACTAL_PIP_CACHE_DIR")
520
+ @classmethod
514
521
  def absolute_FRACTAL_PIP_CACHE_DIR(cls, v):
515
522
  """
516
523
  If `FRACTAL_PIP_CACHE_DIR` is a relative path, fail.
@@ -575,66 +582,108 @@ class Settings(BaseSettings):
575
582
  ###########################################################################
576
583
  # SMTP SERVICE
577
584
  ###########################################################################
578
- FRACTAL_EMAIL_SETTINGS: Optional[str] = None
585
+
586
+ FRACTAL_EMAIL_SENDER: Optional[EmailStr] = None
587
+ """
588
+ Address of the OAuth-signup email sender.
589
+ """
590
+ FRACTAL_EMAIL_PASSWORD: Optional[str] = None
579
591
  """
580
- Encrypted version of settings dictionary, with keys `sender`, `password`,
581
- `smtp_server`, `port`, `instance_name`, `use_starttls`.
592
+ Password for the OAuth-signup email sender.
582
593
  """
583
- FRACTAL_EMAIL_SETTINGS_KEY: Optional[str] = None
594
+ FRACTAL_EMAIL_PASSWORD_KEY: Optional[str] = None
584
595
  """
585
596
  Key value for `cryptography.fernet` decrypt
586
597
  """
598
+ FRACTAL_EMAIL_SMTP_SERVER: Optional[str] = None
599
+ """
600
+ SMPT server for the OAuth-signup emails.
601
+ """
602
+ FRACTAL_EMAIL_SMTP_PORT: Optional[int] = None
603
+ """
604
+ SMPT server port for the OAuth-signup emails.
605
+ """
606
+ FRACTAL_EMAIL_INSTANCE_NAME: Optional[str] = None
607
+ """
608
+ Fractal instance name, to be included in the OAuth-signup emails.
609
+ """
587
610
  FRACTAL_EMAIL_RECIPIENTS: Optional[str] = None
588
611
  """
589
- List of email receivers, separated with commas
612
+ Comma-separated list of recipients of the OAuth-signup emails.
613
+ """
614
+ FRACTAL_EMAIL_USE_STARTTLS: Optional[bool] = True
615
+ """
616
+ Whether to use StartTLS when using the SMTP server.
590
617
  """
618
+ FRACTAL_EMAIL_USE_LOGIN: Optional[bool] = True
619
+ """
620
+ Whether to use login when using the SMTP server.
621
+ """
622
+ email_settings: Optional[MailSettings] = None
591
623
 
592
- @property
593
- def MAIL_SETTINGS(self) -> Optional[MailSettings]:
594
- if (
595
- self.FRACTAL_EMAIL_SETTINGS is not None
596
- and self.FRACTAL_EMAIL_SETTINGS_KEY is not None
597
- and self.FRACTAL_EMAIL_RECIPIENTS is not None
598
- ):
599
- smpt_settings = (
600
- Fernet(self.FRACTAL_EMAIL_SETTINGS_KEY)
601
- .decrypt(self.FRACTAL_EMAIL_SETTINGS)
602
- .decode("utf-8")
603
- )
604
- recipients = self.FRACTAL_EMAIL_RECIPIENTS.split(",")
605
- mail_settings = MailSettings(
606
- **json.loads(smpt_settings), recipients=recipients
607
- )
608
- return mail_settings
609
- elif not all(
610
- [
611
- self.FRACTAL_EMAIL_RECIPIENTS is None,
612
- self.FRACTAL_EMAIL_SETTINGS_KEY is None,
613
- self.FRACTAL_EMAIL_SETTINGS is None,
614
- ]
615
- ):
616
- raise ValueError(
617
- "You must set all SMPT config variables: "
618
- f"{self.FRACTAL_EMAIL_SETTINGS=}, "
619
- f"{self.FRACTAL_EMAIL_RECIPIENTS=}, "
620
- f"{self.FRACTAL_EMAIL_SETTINGS_KEY=}, "
624
+ @model_validator(mode="before")
625
+ @classmethod
626
+ def validate_email_settings(cls, values):
627
+ email_values = {
628
+ k: v for k, v in values.items() if k.startswith("FRACTAL_EMAIL")
629
+ }
630
+ if email_values:
631
+
632
+ def assert_key(key: str):
633
+ if key not in email_values:
634
+ raise ValueError(f"Missing '{key}'")
635
+
636
+ assert_key("FRACTAL_EMAIL_SENDER")
637
+ assert_key("FRACTAL_EMAIL_SMTP_SERVER")
638
+ assert_key("FRACTAL_EMAIL_SMTP_PORT")
639
+ assert_key("FRACTAL_EMAIL_INSTANCE_NAME")
640
+ assert_key("FRACTAL_EMAIL_RECIPIENTS")
641
+
642
+ if email_values.get("FRACTAL_EMAIL_USE_LOGIN", True):
643
+ if "FRACTAL_EMAIL_PASSWORD" not in email_values:
644
+ raise ValueError(
645
+ "'FRACTAL_EMAIL_USE_LOGIN' is True but "
646
+ "'FRACTAL_EMAIL_PASSWORD' is not provided."
647
+ )
648
+ elif "FRACTAL_EMAIL_PASSWORD_KEY" not in email_values:
649
+ raise ValueError(
650
+ "'FRACTAL_EMAIL_USE_LOGIN' is True but "
651
+ "'FRACTAL_EMAIL_PASSWORD_KEY' is not provided."
652
+ )
653
+ else:
654
+ try:
655
+ (
656
+ Fernet(email_values["FRACTAL_EMAIL_PASSWORD_KEY"])
657
+ .decrypt(email_values["FRACTAL_EMAIL_PASSWORD"])
658
+ .decode("utf-8")
659
+ )
660
+ except Exception as e:
661
+ raise ValueError(
662
+ "Invalid pair (FRACTAL_EMAIL_PASSWORD, "
663
+ "FRACTAL_EMAIL_PASSWORD_KEY). "
664
+ f"Original error: {str(e)}."
665
+ )
666
+
667
+ values["email_settings"] = MailSettings(
668
+ sender=email_values["FRACTAL_EMAIL_SENDER"],
669
+ recipients=email_values["FRACTAL_EMAIL_RECIPIENTS"].split(","),
670
+ smtp_server=email_values["FRACTAL_EMAIL_SMTP_SERVER"],
671
+ port=email_values["FRACTAL_EMAIL_SMTP_PORT"],
672
+ encrypted_password=email_values.get("FRACTAL_EMAIL_PASSWORD"),
673
+ encryption_key=email_values.get("FRACTAL_EMAIL_PASSWORD_KEY"),
674
+ instance_name=email_values["FRACTAL_EMAIL_INSTANCE_NAME"],
675
+ use_starttls=email_values.get(
676
+ "FRACTAL_EMAIL_USE_STARTTLS", True
677
+ ),
678
+ use_login=email_values.get("FRACTAL_EMAIL_USE_LOGIN", True),
621
679
  )
622
680
 
681
+ return values
682
+
623
683
  ###########################################################################
624
684
  # BUSINESS LOGIC
625
685
  ###########################################################################
626
686
 
627
- def check_fractal_mail_settings(self):
628
- """
629
- Checks that the mail settings are properly set.
630
- """
631
- try:
632
- self.MAIL_SETTINGS
633
- except Exception as e:
634
- raise FractalConfigurationError(
635
- f"Invalid email configuration settings. Original error: {e}"
636
- )
637
-
638
687
  def check_db(self) -> None:
639
688
  """
640
689
  Checks that db environment variables are properly set.
@@ -740,7 +789,6 @@ class Settings(BaseSettings):
740
789
 
741
790
  self.check_db()
742
791
  self.check_runner()
743
- self.check_fractal_mail_settings()
744
792
 
745
793
  def get_sanitized(self) -> dict:
746
794
  def _must_be_sanitized(string) -> bool:
@@ -753,7 +801,7 @@ class Settings(BaseSettings):
753
801
  return False
754
802
 
755
803
  sanitized_settings = {}
756
- for k, v in self.dict().items():
804
+ for k, v in self.model_dump().items():
757
805
  if _must_be_sanitized(k):
758
806
  sanitized_settings[k] = "***"
759
807
  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
@@ -28,6 +28,7 @@ from .logger import get_logger
28
28
  from .logger import reset_logger_handlers
29
29
  from .logger import set_logger
30
30
  from .syringe import Inject
31
+ from fractal_server import __VERSION__
31
32
 
32
33
 
33
34
  def collect_routers(app: FastAPI) -> None:
@@ -66,8 +67,8 @@ def check_settings() -> None:
66
67
 
67
68
  logger = set_logger("fractal_server_settings")
68
69
  logger.debug("Fractal Settings:")
69
- for key, value in settings.dict().items():
70
- if any(s in key.upper() for s in ["PASSWORD", "SECRET"]):
70
+ for key, value in settings.model_dump().items():
71
+ if any(s in key.upper() for s in ["PASSWORD", "SECRET", "KEY"]):
71
72
  value = "*****"
72
73
  logger.debug(f" {key}: {value}")
73
74
  reset_logger_handlers(logger)
@@ -77,7 +78,7 @@ def check_settings() -> None:
77
78
  async def lifespan(app: FastAPI):
78
79
  app.state.jobsV2 = []
79
80
  logger = set_logger("fractal_server.lifespan")
80
- logger.info("Start application startup")
81
+ logger.info(f"[startup] START (fractal-server {__VERSION__})")
81
82
  check_settings()
82
83
  settings = Inject(get_settings)
83
84
 
@@ -88,31 +89,31 @@ async def lifespan(app: FastAPI):
88
89
  app.state.fractal_ssh_list = FractalSSHList()
89
90
 
90
91
  logger.info(
91
- "Added empty FractalSSHList to app.state "
92
+ "[startup] Added empty FractalSSHList to app.state "
92
93
  f"(id={id(app.state.fractal_ssh_list)})."
93
94
  )
94
95
  else:
95
96
  app.state.fractal_ssh_list = None
96
97
 
97
98
  config_uvicorn_loggers()
98
- logger.info("End application startup")
99
+ logger.info("[startup] END")
99
100
  reset_logger_handlers(logger)
100
101
 
101
102
  yield
102
103
 
103
104
  logger = get_logger("fractal_server.lifespan")
104
- logger.info("Start application shutdown")
105
+ logger.info("[teardown] START")
105
106
 
106
107
  if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
107
108
  logger.info(
108
- "Close FractalSSH connections "
109
+ "[teardown] Close FractalSSH connections "
109
110
  f"(current size: {app.state.fractal_ssh_list.size})."
110
111
  )
111
112
 
112
113
  app.state.fractal_ssh_list.close_all()
113
114
 
114
115
  logger.info(
115
- f"Current worker with pid {os.getpid()} is shutting down. "
116
+ f"[teardown] Current worker with pid {os.getpid()} is shutting down. "
116
117
  f"Current jobs: {app.state.jobsV2=}"
117
118
  )
118
119
  if _backend_supports_shutdown(settings.FRACTAL_RUNNER_BACKEND):
@@ -128,9 +129,11 @@ async def lifespan(app: FastAPI):
128
129
  f"Original error: {e}"
129
130
  )
130
131
  else:
131
- logger.info("Shutdown not available for this backend runner.")
132
+ logger.info(
133
+ "[teardown] Shutdown not available for this backend runner."
134
+ )
132
135
 
133
- logger.info("End application shutdown")
136
+ logger.info("[teardown] END")
134
137
  reset_logger_handlers(logger)
135
138
 
136
139
 
@@ -1,16 +1,11 @@
1
- import asyncio
2
1
  from logging.config import fileConfig
3
2
 
4
3
  from alembic import context
5
- from sqlalchemy.engine import Connection
6
4
  from sqlmodel import SQLModel
7
5
 
8
- from fractal_server.config import get_settings
9
6
  from fractal_server.migrations.naming_convention import NAMING_CONVENTION
10
- from fractal_server.syringe import Inject
11
7
 
12
- # this is the Alembic Config object, which provides
13
- # access to the values within the .ini file in use.
8
+ # Alembic Config object (provides access to the values within the .ini file)
14
9
  config = context.config
15
10
 
16
11
 
@@ -20,77 +15,35 @@ if config.config_file_name is not None:
20
15
  fileConfig(config.config_file_name)
21
16
 
22
17
 
23
- # add your model's MetaData object here
24
- # for 'autogenerate' support
25
- # from myapp import mymodel
26
- # target_metadata = mymodel.Base.metadata
27
18
  target_metadata = SQLModel.metadata
28
19
  target_metadata.naming_convention = NAMING_CONVENTION
29
- # Importing `fractal_server.app.models` after defining
20
+ # Importing `fractal_server.app.models` *after* defining
30
21
  # `SQLModel.metadata.naming_convention` in order to apply the naming convention
31
22
  # when autogenerating migrations (see issue #1819).
32
23
  from fractal_server.app import models # noqa
33
24
 
34
- # other values from the config, defined by the needs of env.py,
35
- # can be acquired:
36
- # my_important_option = config.get_main_option("my_important_option")
37
- # ... etc.
38
-
39
-
40
- def run_migrations_offline() -> None:
41
- """Run migrations in 'offline' mode.
42
-
43
- This configures the context with just a URL
44
- and not an Engine, though an Engine is acceptable
45
- here as well. By skipping the Engine creation
46
- we don't even need a DBAPI to be available.
47
-
48
- Calls to context.execute() here emit the given string to the
49
- script output.
50
25
 
26
+ def run_migrations_online() -> None:
51
27
  """
52
- settings = Inject(get_settings)
53
- settings.check_db()
54
- context.configure(
55
- url=settings.DATABASE_ASYNC_URL,
56
- target_metadata=target_metadata,
57
- literal_binds=True,
58
- dialect_opts={"paramstyle": "named"},
59
- render_as_batch=True,
60
- )
61
-
62
- with context.begin_transaction():
63
- context.run_migrations()
64
-
65
-
66
- def do_run_migrations(connection: Connection) -> None:
67
- context.configure(
68
- connection=connection,
69
- target_metadata=target_metadata,
70
- render_as_batch=True,
71
- )
72
-
73
- with context.begin_transaction():
74
- context.run_migrations()
75
-
76
-
77
- async def run_migrations_online() -> None:
78
- """Run migrations in 'online' mode.
28
+ Run migrations in 'online' mode.
79
29
 
80
30
  In this scenario we need to create an Engine
81
31
  and associate a connection with the context.
82
-
83
32
  """
84
33
  from fractal_server.app.db import DB
85
34
 
86
- engine = DB.engine_async()
87
- async with engine.connect() as connection:
88
- await connection.run_sync(do_run_migrations)
35
+ engine = DB.engine_sync()
36
+ with engine.connect() as connection:
37
+ context.configure(
38
+ connection=connection,
39
+ target_metadata=target_metadata,
40
+ render_as_batch=True,
41
+ )
42
+
43
+ with context.begin_transaction():
44
+ context.run_migrations()
89
45
 
90
- await engine.dispose()
46
+ engine.dispose()
91
47
 
92
48
 
93
- if context.is_offline_mode():
94
- run_migrations_offline()
95
- else:
96
- asyncio.run(run_migrations_online())
49
+ run_migrations_online()