fractal-server 2.14.4a0__py3-none-any.whl → 2.14.6__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 (110) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +2 -2
  3. fractal_server/app/models/security.py +8 -8
  4. fractal_server/app/models/user_settings.py +8 -10
  5. fractal_server/app/models/v2/accounting.py +2 -3
  6. fractal_server/app/models/v2/dataset.py +1 -2
  7. fractal_server/app/models/v2/history.py +3 -4
  8. fractal_server/app/models/v2/job.py +10 -11
  9. fractal_server/app/models/v2/project.py +1 -2
  10. fractal_server/app/models/v2/task.py +13 -14
  11. fractal_server/app/models/v2/task_group.py +15 -16
  12. fractal_server/app/models/v2/workflow.py +1 -2
  13. fractal_server/app/models/v2/workflowtask.py +6 -7
  14. fractal_server/app/routes/admin/v2/accounting.py +3 -4
  15. fractal_server/app/routes/admin/v2/job.py +13 -14
  16. fractal_server/app/routes/admin/v2/project.py +2 -4
  17. fractal_server/app/routes/admin/v2/task.py +11 -13
  18. fractal_server/app/routes/admin/v2/task_group.py +15 -17
  19. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +5 -8
  20. fractal_server/app/routes/api/v2/__init__.py +2 -0
  21. fractal_server/app/routes/api/v2/_aux_functions.py +7 -9
  22. fractal_server/app/routes/api/v2/_aux_functions_history.py +1 -1
  23. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +1 -3
  24. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +5 -6
  25. fractal_server/app/routes/api/v2/dataset.py +6 -8
  26. fractal_server/app/routes/api/v2/history.py +5 -8
  27. fractal_server/app/routes/api/v2/images.py +2 -3
  28. fractal_server/app/routes/api/v2/job.py +5 -6
  29. fractal_server/app/routes/api/v2/pre_submission_checks.py +1 -3
  30. fractal_server/app/routes/api/v2/project.py +2 -4
  31. fractal_server/app/routes/api/v2/status_legacy.py +2 -4
  32. fractal_server/app/routes/api/v2/submit.py +3 -4
  33. fractal_server/app/routes/api/v2/task.py +6 -7
  34. fractal_server/app/routes/api/v2/task_collection.py +11 -13
  35. fractal_server/app/routes/api/v2/task_collection_custom.py +4 -4
  36. fractal_server/app/routes/api/v2/task_group.py +6 -8
  37. fractal_server/app/routes/api/v2/task_group_lifecycle.py +6 -9
  38. fractal_server/app/routes/api/v2/task_version_update.py +270 -0
  39. fractal_server/app/routes/api/v2/workflow.py +5 -6
  40. fractal_server/app/routes/api/v2/workflow_import.py +3 -5
  41. fractal_server/app/routes/api/v2/workflowtask.py +2 -114
  42. fractal_server/app/routes/auth/current_user.py +2 -2
  43. fractal_server/app/routes/pagination.py +2 -3
  44. fractal_server/app/runner/exceptions.py +16 -22
  45. fractal_server/app/runner/executors/base_runner.py +19 -7
  46. fractal_server/app/runner/executors/call_command_wrapper.py +52 -0
  47. fractal_server/app/runner/executors/local/get_local_config.py +2 -3
  48. fractal_server/app/runner/executors/local/runner.py +52 -13
  49. fractal_server/app/runner/executors/slurm_common/_batching.py +2 -3
  50. fractal_server/app/runner/executors/slurm_common/_slurm_config.py +27 -29
  51. fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +95 -63
  52. fractal_server/app/runner/executors/slurm_common/get_slurm_config.py +2 -3
  53. fractal_server/app/runner/executors/slurm_common/remote.py +47 -92
  54. fractal_server/app/runner/executors/slurm_common/slurm_job_task_models.py +22 -22
  55. fractal_server/app/runner/executors/slurm_ssh/run_subprocess.py +2 -3
  56. fractal_server/app/runner/executors/slurm_ssh/runner.py +4 -6
  57. fractal_server/app/runner/executors/slurm_sudo/_subprocess_run_as_user.py +2 -6
  58. fractal_server/app/runner/executors/slurm_sudo/runner.py +9 -18
  59. fractal_server/app/runner/set_start_and_last_task_index.py +2 -5
  60. fractal_server/app/runner/shutdown.py +5 -11
  61. fractal_server/app/runner/task_files.py +3 -13
  62. fractal_server/app/runner/v2/_local.py +3 -4
  63. fractal_server/app/runner/v2/_slurm_ssh.py +5 -7
  64. fractal_server/app/runner/v2/_slurm_sudo.py +8 -10
  65. fractal_server/app/runner/v2/runner.py +4 -5
  66. fractal_server/app/runner/v2/runner_functions.py +20 -35
  67. fractal_server/app/runner/v2/submit_workflow.py +7 -10
  68. fractal_server/app/runner/v2/task_interface.py +2 -3
  69. fractal_server/app/runner/versions.py +3 -13
  70. fractal_server/app/schemas/user.py +2 -4
  71. fractal_server/app/schemas/user_group.py +1 -2
  72. fractal_server/app/schemas/user_settings.py +19 -21
  73. fractal_server/app/schemas/v2/dataset.py +2 -3
  74. fractal_server/app/schemas/v2/dumps.py +13 -15
  75. fractal_server/app/schemas/v2/history.py +6 -7
  76. fractal_server/app/schemas/v2/job.py +17 -18
  77. fractal_server/app/schemas/v2/manifest.py +12 -13
  78. fractal_server/app/schemas/v2/status_legacy.py +2 -2
  79. fractal_server/app/schemas/v2/task.py +29 -30
  80. fractal_server/app/schemas/v2/task_collection.py +8 -9
  81. fractal_server/app/schemas/v2/task_group.py +22 -23
  82. fractal_server/app/schemas/v2/workflow.py +1 -2
  83. fractal_server/app/schemas/v2/workflowtask.py +27 -29
  84. fractal_server/app/security/__init__.py +10 -12
  85. fractal_server/config.py +32 -42
  86. fractal_server/images/models.py +2 -4
  87. fractal_server/images/tools.py +4 -7
  88. fractal_server/logger.py +3 -5
  89. fractal_server/ssh/_fabric.py +41 -13
  90. fractal_server/string_tools.py +2 -2
  91. fractal_server/syringe.py +1 -1
  92. fractal_server/tasks/v2/local/collect.py +2 -3
  93. fractal_server/tasks/v2/local/deactivate.py +1 -1
  94. fractal_server/tasks/v2/local/reactivate.py +1 -1
  95. fractal_server/tasks/v2/ssh/collect.py +256 -245
  96. fractal_server/tasks/v2/ssh/deactivate.py +210 -187
  97. fractal_server/tasks/v2/ssh/reactivate.py +154 -146
  98. fractal_server/tasks/v2/utils_background.py +2 -3
  99. fractal_server/types/__init__.py +1 -2
  100. fractal_server/types/validators/_filter_validators.py +1 -2
  101. fractal_server/utils.py +4 -5
  102. fractal_server/zip_tools.py +1 -1
  103. {fractal_server-2.14.4a0.dist-info → fractal_server-2.14.6.dist-info}/METADATA +2 -9
  104. {fractal_server-2.14.4a0.dist-info → fractal_server-2.14.6.dist-info}/RECORD +107 -108
  105. fractal_server/app/history/__init__.py +0 -0
  106. fractal_server/app/runner/executors/slurm_common/utils_executors.py +0 -58
  107. fractal_server/app/runner/v2/runner_functions_low_level.py +0 -122
  108. {fractal_server-2.14.4a0.dist-info → fractal_server-2.14.6.dist-info}/LICENSE +0 -0
  109. {fractal_server-2.14.4a0.dist-info → fractal_server-2.14.6.dist-info}/WHEEL +0 -0
  110. {fractal_server-2.14.4a0.dist-info → fractal_server-2.14.6.dist-info}/entry_points.txt +0 -0
@@ -27,11 +27,9 @@ registers the client and the relative routes.
27
27
  All routes are registered under the `auth/` prefix.
28
28
  """
29
29
  import contextlib
30
+ from collections.abc import AsyncGenerator
30
31
  from typing import Any
31
- from typing import AsyncGenerator
32
32
  from typing import Generic
33
- from typing import Optional
34
- from typing import Type
35
33
 
36
34
  from fastapi import Depends
37
35
  from fastapi import Request
@@ -82,24 +80,24 @@ class SQLModelUserDatabaseAsync(Generic[UP, ID], BaseUserDatabase[UP, ID]):
82
80
  """
83
81
 
84
82
  session: AsyncSession
85
- user_model: Type[UP]
86
- oauth_account_model: Optional[Type[OAuthAccount]] = None
83
+ user_model: type[UP]
84
+ oauth_account_model: type[OAuthAccount] | None = None
87
85
 
88
86
  def __init__(
89
87
  self,
90
88
  session: AsyncSession,
91
- user_model: Type[UP],
92
- oauth_account_model: Optional[Type[OAuthAccount]] = None,
89
+ user_model: type[UP],
90
+ oauth_account_model: type[OAuthAccount] | None = None,
93
91
  ):
94
92
  self.session = session
95
93
  self.user_model = user_model
96
94
  self.oauth_account_model = oauth_account_model
97
95
 
98
- async def get(self, id: ID) -> Optional[UP]:
96
+ async def get(self, id: ID) -> UP | None:
99
97
  """Get a single user by id."""
100
98
  return await self.session.get(self.user_model, id)
101
99
 
102
- async def get_by_email(self, email: str) -> Optional[UP]:
100
+ async def get_by_email(self, email: str) -> UP | None:
103
101
  """Get a single user by email."""
104
102
  statement = select(self.user_model).where(
105
103
  func.lower(self.user_model.email) == func.lower(email)
@@ -112,7 +110,7 @@ class SQLModelUserDatabaseAsync(Generic[UP, ID], BaseUserDatabase[UP, ID]):
112
110
 
113
111
  async def get_by_oauth_account(
114
112
  self, oauth: str, account_id: str
115
- ) -> Optional[UP]: # noqa
113
+ ) -> UP | None: # noqa
116
114
  """Get a single user by OAuth account id."""
117
115
  if self.oauth_account_model is None:
118
116
  raise NotImplementedError()
@@ -212,7 +210,7 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
212
210
  )
213
211
 
214
212
  async def on_after_register(
215
- self, user: UserOAuth, request: Optional[Request] = None
213
+ self, user: UserOAuth, request: Request | None = None
216
214
  ):
217
215
  logger.info(
218
216
  f"New-user registration completed ({user.id=}, {user.email=})."
@@ -290,7 +288,7 @@ async def _create_first_user(
290
288
  password: str,
291
289
  is_superuser: bool = False,
292
290
  is_verified: bool = False,
293
- username: Optional[str] = None,
291
+ username: str | None = None,
294
292
  ) -> None:
295
293
  """
296
294
  Private method to create the first fractal-server user
fractal_server/config.py CHANGED
@@ -18,7 +18,6 @@ from os import environ
18
18
  from os import getenv
19
19
  from pathlib import Path
20
20
  from typing import Literal
21
- from typing import Optional
22
21
  from typing import TypeVar
23
22
 
24
23
  from cryptography.fernet import Fernet
@@ -56,8 +55,8 @@ class MailSettings(BaseModel):
56
55
  recipients: list[EmailStr] = Field(min_length=1)
57
56
  smtp_server: str
58
57
  port: int
59
- encrypted_password: Optional[SecretStr] = None
60
- encryption_key: Optional[SecretStr] = None
58
+ encrypted_password: SecretStr | None = None
59
+ encryption_key: SecretStr | None = None
61
60
  instance_name: str
62
61
  use_starttls: bool
63
62
  use_login: bool
@@ -100,8 +99,8 @@ class OAuthClientConfig(BaseModel):
100
99
  CLIENT_NAME: str
101
100
  CLIENT_ID: str
102
101
  CLIENT_SECRET: SecretStr
103
- OIDC_CONFIGURATION_ENDPOINT: Optional[str] = None
104
- REDIRECT_URL: Optional[str] = None
102
+ OIDC_CONFIGURATION_ENDPOINT: str | None = None
103
+ REDIRECT_URL: str | None = None
105
104
 
106
105
  @model_validator(mode="before")
107
106
  @classmethod
@@ -139,7 +138,7 @@ class Settings(BaseSettings):
139
138
  JWT token lifetime, in seconds.
140
139
  """
141
140
 
142
- JWT_SECRET_KEY: Optional[SecretStr] = None
141
+ JWT_SECRET_KEY: SecretStr | None = None
143
142
  """
144
143
  JWT secret
145
144
 
@@ -202,23 +201,23 @@ class Settings(BaseSettings):
202
201
  """
203
202
  If `True`, make database operations verbose.
204
203
  """
205
- POSTGRES_USER: Optional[str] = None
204
+ POSTGRES_USER: str | None = None
206
205
  """
207
206
  User to use when connecting to the PostgreSQL database.
208
207
  """
209
- POSTGRES_PASSWORD: Optional[SecretStr] = None
208
+ POSTGRES_PASSWORD: SecretStr | None = None
210
209
  """
211
210
  Password to use when connecting to the PostgreSQL database.
212
211
  """
213
- POSTGRES_HOST: Optional[str] = "localhost"
212
+ POSTGRES_HOST: str | None = "localhost"
214
213
  """
215
214
  URL to the PostgreSQL server or path to a UNIX domain socket.
216
215
  """
217
- POSTGRES_PORT: Optional[str] = "5432"
216
+ POSTGRES_PORT: str | None = "5432"
218
217
  """
219
218
  Port number to use when connecting to the PostgreSQL server.
220
219
  """
221
- POSTGRES_DB: Optional[str] = None
220
+ POSTGRES_DB: str | None = None
222
221
  """
223
222
  Name of the PostgreSQL database to connect to.
224
223
  """
@@ -275,13 +274,13 @@ class Settings(BaseSettings):
275
274
  default admin credentials.
276
275
  """
277
276
 
278
- FRACTAL_TASKS_DIR: Optional[Path] = None
277
+ FRACTAL_TASKS_DIR: Path | None = None
279
278
  """
280
279
  Directory under which all the tasks will be saved (either an absolute path
281
280
  or a path relative to current working directory).
282
281
  """
283
282
 
284
- FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path] = None
283
+ FRACTAL_RUNNER_WORKING_BASE_DIR: Path | None = None
285
284
  """
286
285
  Base directory for job files (either an absolute path or a path relative to
287
286
  current working directory).
@@ -293,7 +292,7 @@ class Settings(BaseSettings):
293
292
  mode="after",
294
293
  )
295
294
  @classmethod
296
- def make_paths_absolute(cls, path: Optional[Path]) -> Optional[Path]:
295
+ def make_paths_absolute(cls, path: Path | None) -> Path | None:
297
296
  if path is None or path.is_absolute():
298
297
  return path
299
298
  else:
@@ -319,7 +318,7 @@ class Settings(BaseSettings):
319
318
  Only logs of with this level (or higher) will appear in the console logs.
320
319
  """
321
320
 
322
- FRACTAL_LOCAL_CONFIG_FILE: Optional[Path] = None
321
+ FRACTAL_LOCAL_CONFIG_FILE: Path | None = None
323
322
  """
324
323
  Path of JSON file with configuration for the local backend.
325
324
  """
@@ -335,27 +334,27 @@ class Settings(BaseSettings):
335
334
  Waiting time for the shutdown phase of executors
336
335
  """
337
336
 
338
- FRACTAL_SLURM_CONFIG_FILE: Optional[Path] = None
337
+ FRACTAL_SLURM_CONFIG_FILE: Path | None = None
339
338
  """
340
339
  Path of JSON file with configuration for the SLURM backend.
341
340
  """
342
341
 
343
- FRACTAL_SLURM_WORKER_PYTHON: Optional[AbsolutePathStr] = None
342
+ FRACTAL_SLURM_WORKER_PYTHON: AbsolutePathStr | None = None
344
343
  """
345
344
  Absolute path to Python interpreter that will run the jobs on the SLURM
346
345
  nodes. If not specified, the same interpreter that runs the server is used.
347
346
  """
348
347
 
349
- FRACTAL_TASKS_PYTHON_DEFAULT_VERSION: Optional[
348
+ FRACTAL_TASKS_PYTHON_DEFAULT_VERSION: None | (
350
349
  Literal["3.9", "3.10", "3.11", "3.12"]
351
- ] = None
350
+ ) = None
352
351
  """
353
352
  Default Python version to be used for task collection. Defaults to the
354
353
  current version. Requires the corresponding variable (e.g
355
354
  `FRACTAL_TASKS_PYTHON_3_10`) to be set.
356
355
  """
357
356
 
358
- FRACTAL_TASKS_PYTHON_3_9: Optional[str] = None
357
+ FRACTAL_TASKS_PYTHON_3_9: str | None = None
359
358
  """
360
359
  Absolute path to the Python 3.9 interpreter that serves as base for virtual
361
360
  environments tasks. Note that this interpreter must have the `venv` module
@@ -364,17 +363,17 @@ class Settings(BaseSettings):
364
363
  unset, `sys.executable` is used as a default.
365
364
  """
366
365
 
367
- FRACTAL_TASKS_PYTHON_3_10: Optional[str] = None
366
+ FRACTAL_TASKS_PYTHON_3_10: str | None = None
368
367
  """
369
368
  Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.10.
370
369
  """
371
370
 
372
- FRACTAL_TASKS_PYTHON_3_11: Optional[str] = None
371
+ FRACTAL_TASKS_PYTHON_3_11: str | None = None
373
372
  """
374
373
  Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.11.
375
374
  """
376
375
 
377
- FRACTAL_TASKS_PYTHON_3_12: Optional[str] = None
376
+ FRACTAL_TASKS_PYTHON_3_12: str | None = None
378
377
  """
379
378
  Same as `FRACTAL_TASKS_PYTHON_3_9`, for Python 3.12.
380
379
  """
@@ -460,16 +459,7 @@ class Settings(BaseSettings):
460
459
  running a task that produces multiple SLURM jobs.
461
460
  """
462
461
 
463
- FRACTAL_SLURM_ERROR_HANDLING_INTERVAL: int = 5
464
- """
465
- Interval to wait (in seconds) when the SLURM backend does not find an
466
- output pickle file - which could be due to several reasons (e.g. the SLURM
467
- job was cancelled or failed, or writing the file is taking long). If the
468
- file is still missing after this time interval, this leads to a
469
- `JobExecutionError`.
470
- """
471
-
472
- FRACTAL_PIP_CACHE_DIR: Optional[AbsolutePathStr] = None
462
+ FRACTAL_PIP_CACHE_DIR: AbsolutePathStr | None = None
473
463
  """
474
464
  Absolute path to the cache directory for `pip`; if unset,
475
465
  `--no-cache-dir` is used.
@@ -516,7 +506,7 @@ class Settings(BaseSettings):
516
506
  viewer paths. Useful when vizarr viewer is not used.
517
507
  """
518
508
 
519
- FRACTAL_VIEWER_BASE_FOLDER: Optional[str] = None
509
+ FRACTAL_VIEWER_BASE_FOLDER: str | None = None
520
510
  """
521
511
  Base path to Zarr files that will be served by fractal-vizarr-viewer;
522
512
  This variable is required and used only when
@@ -527,31 +517,31 @@ class Settings(BaseSettings):
527
517
  # SMTP SERVICE
528
518
  ###########################################################################
529
519
 
530
- FRACTAL_EMAIL_SENDER: Optional[EmailStr] = None
520
+ FRACTAL_EMAIL_SENDER: EmailStr | None = None
531
521
  """
532
522
  Address of the OAuth-signup email sender.
533
523
  """
534
- FRACTAL_EMAIL_PASSWORD: Optional[SecretStr] = None
524
+ FRACTAL_EMAIL_PASSWORD: SecretStr | None = None
535
525
  """
536
526
  Password for the OAuth-signup email sender.
537
527
  """
538
- FRACTAL_EMAIL_PASSWORD_KEY: Optional[SecretStr] = None
528
+ FRACTAL_EMAIL_PASSWORD_KEY: SecretStr | None = None
539
529
  """
540
530
  Key value for `cryptography.fernet` decrypt
541
531
  """
542
- FRACTAL_EMAIL_SMTP_SERVER: Optional[str] = None
532
+ FRACTAL_EMAIL_SMTP_SERVER: str | None = None
543
533
  """
544
534
  SMTP server for the OAuth-signup emails.
545
535
  """
546
- FRACTAL_EMAIL_SMTP_PORT: Optional[int] = None
536
+ FRACTAL_EMAIL_SMTP_PORT: int | None = None
547
537
  """
548
538
  SMTP server port for the OAuth-signup emails.
549
539
  """
550
- FRACTAL_EMAIL_INSTANCE_NAME: Optional[str] = None
540
+ FRACTAL_EMAIL_INSTANCE_NAME: str | None = None
551
541
  """
552
542
  Fractal instance name, to be included in the OAuth-signup emails.
553
543
  """
554
- FRACTAL_EMAIL_RECIPIENTS: Optional[str] = None
544
+ FRACTAL_EMAIL_RECIPIENTS: str | None = None
555
545
  """
556
546
  Comma-separated list of recipients of the OAuth-signup emails.
557
547
  """
@@ -567,7 +557,7 @@ class Settings(BaseSettings):
567
557
  provided.
568
558
  Accepted values: 'true', 'false'.
569
559
  """
570
- email_settings: Optional[MailSettings] = None
560
+ email_settings: MailSettings | None = None
571
561
 
572
562
  @model_validator(mode="after")
573
563
  def validate_email_settings(self):
@@ -1,5 +1,3 @@
1
- from typing import Optional
2
-
3
1
  from pydantic import BaseModel
4
2
  from pydantic import Field
5
3
 
@@ -23,7 +21,7 @@ class _SingleImageBase(BaseModel):
23
21
  """
24
22
 
25
23
  zarr_url: ZarrUrlStr
26
- origin: Optional[ZarrDirStr] = None
24
+ origin: ZarrDirStr | None = None
27
25
 
28
26
  attributes: DictStrAny = Field(default_factory=dict)
29
27
  types: ImageTypes = Field(default_factory=dict)
@@ -48,4 +46,4 @@ class SingleImage(_SingleImageBase):
48
46
  class SingleImageUpdate(BaseModel):
49
47
  zarr_url: ZarrUrlStr
50
48
  attributes: ImageAttributes = None
51
- types: Optional[ImageTypes] = None
49
+ types: ImageTypes | None = None
@@ -1,7 +1,6 @@
1
1
  from copy import copy
2
2
  from typing import Any
3
3
  from typing import Literal
4
- from typing import Optional
5
4
  from typing import Union
6
5
 
7
6
  from fractal_server.types import AttributeFilters
@@ -13,7 +12,7 @@ def find_image_by_zarr_url(
13
12
  *,
14
13
  images: list[dict[str, Any]],
15
14
  zarr_url: str,
16
- ) -> Optional[ImageSearch]:
15
+ ) -> ImageSearch | None:
17
16
  """
18
17
  Return a copy of the image with a given zarr_url, and its positional index.
19
18
 
@@ -65,8 +64,8 @@ def match_filter(
65
64
 
66
65
  def filter_image_list(
67
66
  images: list[dict[str, Any]],
68
- type_filters: Optional[dict[str, bool]] = None,
69
- attribute_filters: Optional[AttributeFilters] = None,
67
+ type_filters: dict[str, bool] | None = None,
68
+ attribute_filters: AttributeFilters | None = None,
70
69
  ) -> list[dict[str, Any]]:
71
70
  """
72
71
  Compute a sublist with images that match a filter set.
@@ -141,6 +140,4 @@ def aggregate_types(images: list[dict[str, Any]]) -> list[str]:
141
140
  """
142
141
  Given a list of images, this function returns a list of all image types.
143
142
  """
144
- return list(
145
- set(type for image in images for type in image["types"].keys())
146
- )
143
+ return list({type for image in images for type in image["types"].keys()})
fractal_server/logger.py CHANGED
@@ -14,8 +14,6 @@ This module provides logging utilities
14
14
  """
15
15
  import logging
16
16
  from pathlib import Path
17
- from typing import Optional
18
- from typing import Union
19
17
 
20
18
  from .config import get_settings
21
19
  from .syringe import Inject
@@ -25,7 +23,7 @@ LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
25
23
  LOG_FORMATTER = logging.Formatter(LOG_FORMAT)
26
24
 
27
25
 
28
- def get_logger(logger_name: Optional[str] = None) -> logging.Logger:
26
+ def get_logger(logger_name: str | None = None) -> logging.Logger:
29
27
  """
30
28
  Wrap the
31
29
  [`logging.getLogger`](https://docs.python.org/3/library/logging.html#logging.getLogger)
@@ -57,8 +55,8 @@ def get_logger(logger_name: Optional[str] = None) -> logging.Logger:
57
55
  def set_logger(
58
56
  logger_name: str,
59
57
  *,
60
- log_file_path: Optional[Union[str, Path]] = None,
61
- default_logging_level: Optional[int] = None,
58
+ log_file_path: str | Path | None = None,
59
+ default_logging_level: int | None = None,
62
60
  ) -> logging.Logger:
63
61
  """
64
62
  Set up a `fractal-server` logger
@@ -1,18 +1,18 @@
1
1
  import json
2
2
  import logging
3
3
  import time
4
+ from collections.abc import Generator
4
5
  from contextlib import contextmanager
5
6
  from pathlib import Path
6
7
  from threading import Lock
7
8
  from typing import Any
8
- from typing import Generator
9
9
  from typing import Literal
10
- from typing import Optional
11
10
 
12
11
  import paramiko.sftp_client
13
12
  from fabric import Connection
14
13
  from invoke import UnexpectedExit
15
14
  from paramiko.ssh_exception import NoValidConnectionsError
15
+ from pydantic import BaseModel
16
16
 
17
17
  from ..logger import get_logger
18
18
  from ..logger import set_logger
@@ -35,6 +35,12 @@ class FractalSSHUnknownError(RuntimeError):
35
35
  pass
36
36
 
37
37
 
38
+ class SSHConfig(BaseModel):
39
+ host: str
40
+ user: str
41
+ key_path: str
42
+
43
+
38
44
  logger = set_logger(__name__)
39
45
 
40
46
 
@@ -56,6 +62,7 @@ def _acquire_lock_with_timeout(
56
62
  """
57
63
  logger = get_logger(logger_name)
58
64
  logger.info(f"Trying to acquire lock for '{label}', with {timeout=}")
65
+ t_start_lock_acquire = time.perf_counter()
59
66
  result = lock.acquire(timeout=timeout)
60
67
  try:
61
68
  if not result:
@@ -64,7 +71,9 @@ def _acquire_lock_with_timeout(
64
71
  f"Failed to acquire lock for '{label}' within "
65
72
  f"{timeout} seconds"
66
73
  )
67
- logger.info(f"Lock for '{label}' was acquired.")
74
+ t_end_lock_acquire = time.perf_counter()
75
+ elapsed = t_end_lock_acquire - t_start_lock_acquire
76
+ logger.info(f"Lock for '{label}' was acquired - {elapsed=:.4f} s")
68
77
  yield result
69
78
  finally:
70
79
  if result:
@@ -72,7 +81,7 @@ def _acquire_lock_with_timeout(
72
81
  logger.info(f"Lock for '{label}' was released.")
73
82
 
74
83
 
75
- class FractalSSH(object):
84
+ class FractalSSH:
76
85
  """
77
86
  Wrapper of `fabric.Connection` object, enriched with locks.
78
87
 
@@ -156,7 +165,7 @@ class FractalSSH(object):
156
165
  raise e
157
166
 
158
167
  def _run(
159
- self, *args, label: str, lock_timeout: Optional[float] = None, **kwargs
168
+ self, *args, label: str, lock_timeout: float | None = None, **kwargs
160
169
  ) -> Any:
161
170
  actual_lock_timeout = self.default_lock_timeout
162
171
  if lock_timeout is not None:
@@ -272,10 +281,10 @@ class FractalSSH(object):
272
281
  self,
273
282
  *,
274
283
  cmd: str,
275
- allow_char: Optional[str] = None,
276
- max_attempts: Optional[int] = None,
277
- base_interval: Optional[float] = None,
278
- lock_timeout: Optional[int] = None,
284
+ allow_char: str | None = None,
285
+ max_attempts: int | None = None,
286
+ base_interval: float | None = None,
287
+ lock_timeout: int | None = None,
279
288
  ) -> str:
280
289
  """
281
290
  Run a command within an open SSH connection.
@@ -372,7 +381,7 @@ class FractalSSH(object):
372
381
  *,
373
382
  local: str,
374
383
  remote: str,
375
- lock_timeout: Optional[float] = None,
384
+ lock_timeout: float | None = None,
376
385
  ) -> None:
377
386
  """
378
387
  Transfer a file via SSH
@@ -412,7 +421,7 @@ class FractalSSH(object):
412
421
  *,
413
422
  local: str,
414
423
  remote: str,
415
- lock_timeout: Optional[float] = None,
424
+ lock_timeout: float | None = None,
416
425
  ) -> None:
417
426
  """
418
427
  Transfer a file via SSH
@@ -503,7 +512,7 @@ class FractalSSH(object):
503
512
  *,
504
513
  path: str,
505
514
  content: str,
506
- lock_timeout: Optional[float] = None,
515
+ lock_timeout: float | None = None,
507
516
  ) -> None:
508
517
  """
509
518
  Open a remote file via SFTP and write it.
@@ -557,7 +566,7 @@ class FractalSSH(object):
557
566
  )
558
567
 
559
568
 
560
- class FractalSSHList(object):
569
+ class FractalSSHList:
561
570
  """
562
571
  Collection of `FractalSSH` objects
563
572
 
@@ -711,3 +720,22 @@ class FractalSSHList(object):
711
720
  f"({fractal_ssh_obj.is_connected=})."
712
721
  )
713
722
  fractal_ssh_obj.close()
723
+
724
+
725
+ @contextmanager
726
+ def SingleUseFractalSSH(
727
+ *,
728
+ ssh_config: SSHConfig,
729
+ logger_name: str,
730
+ ) -> Generator[FractalSSH, Any, None]:
731
+ """
732
+ Get a new FractalSSH object (with a fresh connection).
733
+
734
+ Args:
735
+ ssh_config:
736
+ logger_name:
737
+ """
738
+ _fractal_ssh_list = FractalSSHList(logger_name=logger_name)
739
+ _fractal_ssh = _fractal_ssh_list.get(**ssh_config.model_dump())
740
+ yield _fractal_ssh
741
+ _fractal_ssh.close()
@@ -1,5 +1,5 @@
1
1
  import string
2
- from typing import Optional
2
+
3
3
 
4
4
  __SPECIAL_CHARACTERS__ = f"{string.punctuation}{string.whitespace}"
5
5
 
@@ -36,7 +36,7 @@ def sanitize_string(value: str) -> str:
36
36
  def validate_cmd(
37
37
  command: str,
38
38
  *,
39
- allow_char: Optional[str] = None,
39
+ allow_char: str | None = None,
40
40
  attribute_name: str = "Command",
41
41
  ):
42
42
  """
fractal_server/syringe.py CHANGED
@@ -34,8 +34,8 @@ or popped from the directory.
34
34
  >>> bar()
35
35
  42
36
36
  """
37
+ from collections.abc import Callable
37
38
  from typing import Any
38
- from typing import Callable
39
39
  from typing import TypeVar
40
40
 
41
41
 
@@ -4,7 +4,6 @@ import shutil
4
4
  import time
5
5
  from pathlib import Path
6
6
  from tempfile import TemporaryDirectory
7
- from typing import Optional
8
7
 
9
8
  from ..utils_database import create_db_tasks_and_update_task_group_sync
10
9
  from ._utils import _customize_and_run_template
@@ -39,7 +38,7 @@ def collect_local(
39
38
  *,
40
39
  task_group_activity_id: int,
41
40
  task_group_id: int,
42
- wheel_file: Optional[WheelFile] = None,
41
+ wheel_file: WheelFile | None = None,
43
42
  ) -> None:
44
43
  """
45
44
  Collect a task package.
@@ -132,7 +131,7 @@ def collect_local(
132
131
  ).as_posix(),
133
132
  prefix=(
134
133
  f"{int(time.time())}_"
135
- f"{TaskGroupActivityActionV2.COLLECT.value}_"
134
+ f"{TaskGroupActivityActionV2.COLLECT}_"
136
135
  ),
137
136
  logger_name=LOGGER_NAME,
138
137
  )
@@ -107,7 +107,7 @@ def deactivate_local(
107
107
  ).as_posix(),
108
108
  prefix=(
109
109
  f"{int(time.time())}_"
110
- f"{TaskGroupActivityActionV2.DEACTIVATE.value}_"
110
+ f"{TaskGroupActivityActionV2.DEACTIVATE}_"
111
111
  ),
112
112
  logger_name=LOGGER_NAME,
113
113
  )
@@ -107,7 +107,7 @@ def reactivate_local(
107
107
  ).as_posix(),
108
108
  prefix=(
109
109
  f"{int(time.time())}_"
110
- f"{TaskGroupActivityActionV2.REACTIVATE.value}_"
110
+ f"{TaskGroupActivityActionV2.REACTIVATE}_"
111
111
  ),
112
112
  logger_name=LOGGER_NAME,
113
113
  )