fractal-server 2.5.0a1__py3-none-any.whl → 2.5.2__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/app/routes/api/v2/submit.py +13 -1
- fractal_server/app/routes/api/v2/task_collection.py +10 -1
- fractal_server/app/routes/api/v2/task_collection_custom.py +2 -1
- fractal_server/app/runner/compress_folder.py +1 -1
- fractal_server/app/runner/executors/slurm/ssh/executor.py +24 -4
- fractal_server/app/runner/executors/slurm/sudo/_subprocess_run_as_user.py +2 -0
- fractal_server/app/runner/executors/slurm/sudo/executor.py +9 -2
- fractal_server/app/runner/run_subprocess.py +5 -1
- fractal_server/app/runner/v1/_common.py +2 -0
- fractal_server/app/runner/v2/runner_functions_low_level.py +5 -0
- fractal_server/app/schemas/user.py +15 -10
- fractal_server/app/schemas/v2/dumps.py +11 -2
- fractal_server/app/schemas/v2/task.py +6 -0
- fractal_server/main.py +12 -10
- fractal_server/ssh/_fabric.py +185 -34
- fractal_server/string_tools.py +25 -0
- {fractal_server-2.5.0a1.dist-info → fractal_server-2.5.2.dist-info}/METADATA +1 -1
- {fractal_server-2.5.0a1.dist-info → fractal_server-2.5.2.dist-info}/RECORD +22 -22
- {fractal_server-2.5.0a1.dist-info → fractal_server-2.5.2.dist-info}/LICENSE +0 -0
- {fractal_server-2.5.0a1.dist-info → fractal_server-2.5.2.dist-info}/WHEEL +0 -0
- {fractal_server-2.5.0a1.dist-info → fractal_server-2.5.2.dist-info}/entry_points.txt +0 -0
fractal_server/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "2.5.
|
1
|
+
__VERSION__ = "2.5.2"
|
@@ -238,6 +238,18 @@ async def apply_workflow(
|
|
238
238
|
await db.merge(job)
|
239
239
|
await db.commit()
|
240
240
|
|
241
|
+
# User appropriate FractalSSH object
|
242
|
+
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
243
|
+
ssh_credentials = dict(
|
244
|
+
user=settings.FRACTAL_SLURM_SSH_USER,
|
245
|
+
host=settings.FRACTAL_SLURM_SSH_HOST,
|
246
|
+
key_path=settings.FRACTAL_SLURM_SSH_PRIVATE_KEY_PATH,
|
247
|
+
)
|
248
|
+
fractal_ssh_list = request.app.state.fractal_ssh_list
|
249
|
+
fractal_ssh = fractal_ssh_list.get(**ssh_credentials)
|
250
|
+
else:
|
251
|
+
fractal_ssh = None
|
252
|
+
|
241
253
|
background_tasks.add_task(
|
242
254
|
submit_workflow,
|
243
255
|
workflow_id=workflow.id,
|
@@ -246,7 +258,7 @@ async def apply_workflow(
|
|
246
258
|
worker_init=job.worker_init,
|
247
259
|
slurm_user=user.slurm_user,
|
248
260
|
user_cache_dir=user.cache_dir,
|
249
|
-
fractal_ssh=
|
261
|
+
fractal_ssh=fractal_ssh,
|
250
262
|
)
|
251
263
|
request.app.state.jobsV2.append(job.id)
|
252
264
|
logger.info(
|
@@ -124,11 +124,20 @@ async def collect_tasks_pip(
|
|
124
124
|
db.add(state)
|
125
125
|
await db.commit()
|
126
126
|
|
127
|
+
# User appropriate FractalSSH object
|
128
|
+
ssh_credentials = dict(
|
129
|
+
user=settings.FRACTAL_SLURM_SSH_USER,
|
130
|
+
host=settings.FRACTAL_SLURM_SSH_HOST,
|
131
|
+
key_path=settings.FRACTAL_SLURM_SSH_PRIVATE_KEY_PATH,
|
132
|
+
)
|
133
|
+
fractal_ssh_list = request.app.state.fractal_ssh_list
|
134
|
+
fractal_ssh = fractal_ssh_list.get(**ssh_credentials)
|
135
|
+
|
127
136
|
background_tasks.add_task(
|
128
137
|
background_collect_pip_ssh,
|
129
138
|
state.id,
|
130
139
|
task_pkg,
|
131
|
-
|
140
|
+
fractal_ssh,
|
132
141
|
)
|
133
142
|
|
134
143
|
response.status_code = status.HTTP_201_CREATED
|
@@ -20,12 +20,12 @@ from ....schemas.v2 import TaskCreateV2
|
|
20
20
|
from ....schemas.v2 import TaskReadV2
|
21
21
|
from fractal_server.app.models import UserOAuth
|
22
22
|
from fractal_server.app.routes.auth import current_active_verified_user
|
23
|
+
from fractal_server.string_tools import validate_cmd
|
23
24
|
from fractal_server.tasks.v2.background_operations import _insert_tasks
|
24
25
|
from fractal_server.tasks.v2.background_operations import (
|
25
26
|
_prepare_tasks_metadata,
|
26
27
|
)
|
27
28
|
|
28
|
-
|
29
29
|
router = APIRouter()
|
30
30
|
|
31
31
|
logger = set_logger(__name__)
|
@@ -74,6 +74,7 @@ async def collect_task_custom(
|
|
74
74
|
package_name_underscore = task_collect.package_name.replace("-", "_")
|
75
75
|
# Note that python_command is then used as part of a subprocess.run
|
76
76
|
# statement: be careful with mixing `'` and `"`.
|
77
|
+
validate_cmd(package_name_underscore)
|
77
78
|
python_command = (
|
78
79
|
"import importlib.util; "
|
79
80
|
"from pathlib import Path; "
|
@@ -48,7 +48,7 @@ def create_tar_archive(
|
|
48
48
|
"."
|
49
49
|
)
|
50
50
|
logger.debug(f"cmd tar:\n{cmd_tar}")
|
51
|
-
run_subprocess(cmd=cmd_tar, logger_name=logger_name)
|
51
|
+
run_subprocess(cmd=cmd_tar, logger_name=logger_name, allow_char="*")
|
52
52
|
|
53
53
|
|
54
54
|
def remove_temp_subfolder(subfolder_path_tmp_copy: Path, logger_name: str):
|
@@ -163,17 +163,34 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
163
163
|
settings = Inject(get_settings)
|
164
164
|
self.python_remote = settings.FRACTAL_SLURM_WORKER_PYTHON
|
165
165
|
if self.python_remote is None:
|
166
|
+
self._stop_and_join_wait_thread()
|
166
167
|
raise ValueError("FRACTAL_SLURM_WORKER_PYTHON is not set. Exit.")
|
167
168
|
|
168
169
|
# Initialize connection and perform handshake
|
169
170
|
self.fractal_ssh = fractal_ssh
|
170
171
|
logger.warning(self.fractal_ssh)
|
171
|
-
|
172
|
+
try:
|
173
|
+
self.handshake()
|
174
|
+
except Exception as e:
|
175
|
+
logger.warning(
|
176
|
+
"Stop/join waiting thread and then "
|
177
|
+
f"re-raise original error {str(e)}"
|
178
|
+
)
|
179
|
+
self._stop_and_join_wait_thread()
|
180
|
+
raise e
|
172
181
|
|
173
182
|
# Set/validate parameters for SLURM submission scripts
|
174
183
|
self.slurm_account = slurm_account
|
175
184
|
self.common_script_lines = common_script_lines or []
|
176
|
-
|
185
|
+
try:
|
186
|
+
self._validate_common_script_lines()
|
187
|
+
except Exception as e:
|
188
|
+
logger.warning(
|
189
|
+
"Stop/join waiting thread and then "
|
190
|
+
f"re-raise original error {str(e)}"
|
191
|
+
)
|
192
|
+
self._stop_and_join_wait_thread()
|
193
|
+
raise e
|
177
194
|
|
178
195
|
# Set/initialize some more options
|
179
196
|
self.keep_pickle_files = keep_pickle_files
|
@@ -1385,6 +1402,10 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
1385
1402
|
self.fractal_ssh.run_command(cmd=scancel_command)
|
1386
1403
|
logger.debug("Executor shutdown: end")
|
1387
1404
|
|
1405
|
+
def _stop_and_join_wait_thread(self):
|
1406
|
+
self.wait_thread.stop()
|
1407
|
+
self.wait_thread.join()
|
1408
|
+
|
1388
1409
|
def __exit__(self, *args, **kwargs):
|
1389
1410
|
"""
|
1390
1411
|
See
|
@@ -1393,8 +1414,7 @@ class FractalSlurmSSHExecutor(SlurmExecutor):
|
|
1393
1414
|
logger.debug(
|
1394
1415
|
"[FractalSlurmSSHExecutor.__exit__] Stop and join `wait_thread`"
|
1395
1416
|
)
|
1396
|
-
self.
|
1397
|
-
self.wait_thread.join()
|
1417
|
+
self._stop_and_join_wait_thread()
|
1398
1418
|
logger.debug("[FractalSlurmSSHExecutor.__exit__] End")
|
1399
1419
|
|
1400
1420
|
def run_squeue(self, job_ids):
|
@@ -20,6 +20,7 @@ import subprocess # nosec
|
|
20
20
|
from typing import Optional
|
21
21
|
|
22
22
|
from ......logger import set_logger
|
23
|
+
from fractal_server.string_tools import validate_cmd
|
23
24
|
|
24
25
|
logger = set_logger(__name__)
|
25
26
|
|
@@ -47,6 +48,7 @@ def _run_command_as_user(
|
|
47
48
|
Returns:
|
48
49
|
res: The return value from `subprocess.run`.
|
49
50
|
"""
|
51
|
+
validate_cmd(cmd)
|
50
52
|
logger.debug(f'[_run_command_as_user] {user=}, cmd="{cmd}"')
|
51
53
|
if user:
|
52
54
|
new_cmd = f"sudo --set-home --non-interactive -u {user} {cmd}"
|
@@ -47,6 +47,7 @@ from ._subprocess_run_as_user import _path_exists_as_user
|
|
47
47
|
from ._subprocess_run_as_user import _run_command_as_user
|
48
48
|
from fractal_server import __VERSION__
|
49
49
|
from fractal_server.app.runner.components import _COMPONENT_KEY_
|
50
|
+
from fractal_server.string_tools import validate_cmd
|
50
51
|
|
51
52
|
|
52
53
|
logger = set_logger(__name__)
|
@@ -65,6 +66,7 @@ def _subprocess_run_or_raise(full_command: str) -> Optional[CompletedProcess]:
|
|
65
66
|
Returns:
|
66
67
|
The actual `CompletedProcess` output of `subprocess.run`.
|
67
68
|
"""
|
69
|
+
validate_cmd(full_command)
|
68
70
|
try:
|
69
71
|
output = subprocess.run( # nosec
|
70
72
|
shlex.split(full_command),
|
@@ -257,6 +259,7 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
257
259
|
for line in self.common_script_lines
|
258
260
|
if line.startswith("#SBATCH --account=")
|
259
261
|
)
|
262
|
+
self._stop_and_join_wait_thread()
|
260
263
|
raise RuntimeError(
|
261
264
|
"Invalid line in `FractalSlurmExecutor.common_script_lines`: "
|
262
265
|
f"'{invalid_line}'.\n"
|
@@ -1266,6 +1269,7 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
1266
1269
|
pre_command = f"sudo --non-interactive -u {self.slurm_user}"
|
1267
1270
|
submit_command = f"scancel {scancel_string}"
|
1268
1271
|
full_command = f"{pre_command} {submit_command}"
|
1272
|
+
validate_cmd(full_command)
|
1269
1273
|
logger.debug(f"Now execute `{full_command}`")
|
1270
1274
|
try:
|
1271
1275
|
subprocess.run( # nosec
|
@@ -1284,6 +1288,10 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
1284
1288
|
|
1285
1289
|
logger.debug("Executor shutdown: end")
|
1286
1290
|
|
1291
|
+
def _stop_and_join_wait_thread(self):
|
1292
|
+
self.wait_thread.stop()
|
1293
|
+
self.wait_thread.join()
|
1294
|
+
|
1287
1295
|
def __exit__(self, *args, **kwargs):
|
1288
1296
|
"""
|
1289
1297
|
See
|
@@ -1292,6 +1300,5 @@ class FractalSlurmExecutor(SlurmExecutor):
|
|
1292
1300
|
logger.debug(
|
1293
1301
|
"[FractalSlurmExecutor.__exit__] Stop and join `wait_thread`"
|
1294
1302
|
)
|
1295
|
-
self.
|
1296
|
-
self.wait_thread.join()
|
1303
|
+
self._stop_and_join_wait_thread()
|
1297
1304
|
logger.debug("[FractalSlurmExecutor.__exit__] End")
|
@@ -3,11 +3,15 @@ import subprocess # nosec
|
|
3
3
|
from typing import Optional
|
4
4
|
|
5
5
|
from fractal_server.logger import get_logger
|
6
|
+
from fractal_server.string_tools import validate_cmd
|
6
7
|
|
7
8
|
|
8
9
|
def run_subprocess(
|
9
|
-
cmd: str,
|
10
|
+
cmd: str,
|
11
|
+
allow_char: Optional[str] = None,
|
12
|
+
logger_name: Optional[str] = None,
|
10
13
|
) -> subprocess.CompletedProcess:
|
14
|
+
validate_cmd(cmd, allow_char=allow_char)
|
11
15
|
logger = get_logger(logger_name)
|
12
16
|
try:
|
13
17
|
res = subprocess.run( # nosec
|
@@ -31,6 +31,7 @@ from .common import write_args_file
|
|
31
31
|
from fractal_server.app.runner.filenames import HISTORY_FILENAME
|
32
32
|
from fractal_server.app.runner.filenames import METADATA_FILENAME
|
33
33
|
from fractal_server.app.runner.task_files import get_task_file_paths
|
34
|
+
from fractal_server.string_tools import validate_cmd
|
34
35
|
|
35
36
|
|
36
37
|
def no_op_submit_setup_call(
|
@@ -77,6 +78,7 @@ def _call_command_wrapper(cmd: str, stdout: Path, stderr: Path) -> None:
|
|
77
78
|
TERM or KILL signal)
|
78
79
|
"""
|
79
80
|
|
81
|
+
validate_cmd(cmd)
|
80
82
|
# Verify that task command is executable
|
81
83
|
if shutil.which(shlex_split(cmd)[0]) is None:
|
82
84
|
msg = (
|
@@ -12,6 +12,7 @@ from ..exceptions import JobExecutionError
|
|
12
12
|
from ..exceptions import TaskExecutionError
|
13
13
|
from fractal_server.app.models.v2 import WorkflowTaskV2
|
14
14
|
from fractal_server.app.runner.task_files import get_task_file_paths
|
15
|
+
from fractal_server.string_tools import validate_cmd
|
15
16
|
|
16
17
|
|
17
18
|
def _call_command_wrapper(cmd: str, log_path: Path) -> None:
|
@@ -25,6 +26,10 @@ def _call_command_wrapper(cmd: str, log_path: Path) -> None:
|
|
25
26
|
exit code (e.g. due to the subprocess receiving a
|
26
27
|
TERM or KILL signal)
|
27
28
|
"""
|
29
|
+
try:
|
30
|
+
validate_cmd(cmd)
|
31
|
+
except ValueError as e:
|
32
|
+
raise TaskExecutionError(f"Invalid command. Original error: {str(e)}")
|
28
33
|
|
29
34
|
# Verify that task command is executable
|
30
35
|
if shutil.which(shlex_split(cmd)[0]) is None:
|
@@ -10,7 +10,7 @@ from pydantic.types import StrictStr
|
|
10
10
|
from ._validators import val_absolute_path
|
11
11
|
from ._validators import val_unique_list
|
12
12
|
from ._validators import valstr
|
13
|
-
|
13
|
+
from fractal_server.string_tools import validate_cmd
|
14
14
|
|
15
15
|
__all__ = (
|
16
16
|
"UserRead",
|
@@ -77,14 +77,16 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
77
77
|
valstr("slurm_user")
|
78
78
|
)
|
79
79
|
_username = validator("username", allow_reuse=True)(valstr("username"))
|
80
|
-
_cache_dir = validator("cache_dir", allow_reuse=True)(
|
81
|
-
val_absolute_path("cache_dir")
|
82
|
-
)
|
83
80
|
|
84
81
|
_slurm_accounts = validator("slurm_accounts", allow_reuse=True)(
|
85
82
|
val_unique_list("slurm_accounts")
|
86
83
|
)
|
87
84
|
|
85
|
+
@validator("cache_dir")
|
86
|
+
def cache_dir_validator(cls, value):
|
87
|
+
validate_cmd(value)
|
88
|
+
return val_absolute_path("cache_dir")(value)
|
89
|
+
|
88
90
|
@validator(
|
89
91
|
"is_active",
|
90
92
|
"is_verified",
|
@@ -115,9 +117,10 @@ class UserUpdateStrict(BaseModel, extra=Extra.forbid):
|
|
115
117
|
val_unique_list("slurm_accounts")
|
116
118
|
)
|
117
119
|
|
118
|
-
|
119
|
-
|
120
|
-
|
120
|
+
@validator("cache_dir")
|
121
|
+
def cache_dir_validator(cls, value):
|
122
|
+
validate_cmd(value)
|
123
|
+
return val_absolute_path("cache_dir")(value)
|
121
124
|
|
122
125
|
|
123
126
|
class UserUpdateWithNewGroupIds(UserUpdate):
|
@@ -157,6 +160,8 @@ class UserCreate(schemas.BaseUserCreate):
|
|
157
160
|
valstr("slurm_user")
|
158
161
|
)
|
159
162
|
_username = validator("username", allow_reuse=True)(valstr("username"))
|
160
|
-
|
161
|
-
|
162
|
-
)
|
163
|
+
|
164
|
+
@validator("cache_dir")
|
165
|
+
def cache_dir_validator(cls, value):
|
166
|
+
validate_cmd(value)
|
167
|
+
return val_absolute_path("cache_dir")(value)
|
@@ -39,14 +39,23 @@ class TaskDumpV2(BaseModel):
|
|
39
39
|
|
40
40
|
|
41
41
|
class WorkflowTaskDumpV2(BaseModel):
|
42
|
+
"""
|
43
|
+
Before v2.5.0, WorkflowTaskV2 could have `task_id=task=None` and
|
44
|
+
non-`None` `task_legacy_id` and `task_legacy`. Since these objects
|
45
|
+
may still exist in the database after version updates, we are setting
|
46
|
+
`task_id` and `task` to `Optional` to avoid response-validation errors
|
47
|
+
for the endpoints that GET datasets.
|
48
|
+
Ref issue #1783.
|
49
|
+
"""
|
50
|
+
|
42
51
|
id: int
|
43
52
|
workflow_id: int
|
44
53
|
order: Optional[int]
|
45
54
|
|
46
55
|
input_filters: Filters
|
47
56
|
|
48
|
-
task_id: int
|
49
|
-
task: TaskDumpV2
|
57
|
+
task_id: Optional[int]
|
58
|
+
task: Optional[TaskDumpV2]
|
50
59
|
|
51
60
|
|
52
61
|
class WorkflowDumpV2(BaseModel, extra=Extra.forbid):
|
@@ -11,6 +11,7 @@ from pydantic import validator
|
|
11
11
|
|
12
12
|
from .._validators import valdictkeys
|
13
13
|
from .._validators import valstr
|
14
|
+
from fractal_server.string_tools import validate_cmd
|
14
15
|
|
15
16
|
|
16
17
|
class TaskCreateV2(BaseModel, extra=Extra.forbid):
|
@@ -43,6 +44,11 @@ class TaskCreateV2(BaseModel, extra=Extra.forbid):
|
|
43
44
|
"Task must have at least one valid command "
|
44
45
|
"(parallel and/or non_parallel)"
|
45
46
|
)
|
47
|
+
if command_parallel is not None:
|
48
|
+
validate_cmd(command_parallel)
|
49
|
+
if command_non_parallel is not None:
|
50
|
+
validate_cmd(command_non_parallel)
|
51
|
+
|
46
52
|
return values
|
47
53
|
|
48
54
|
_name = validator("name", allow_reuse=True)(valstr("name"))
|
fractal_server/main.py
CHANGED
@@ -92,32 +92,34 @@ async def lifespan(app: FastAPI):
|
|
92
92
|
settings = Inject(get_settings)
|
93
93
|
|
94
94
|
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
95
|
-
from fractal_server.ssh._fabric import get_ssh_connection
|
96
|
-
from fractal_server.ssh._fabric import FractalSSH
|
97
95
|
|
98
|
-
|
99
|
-
|
96
|
+
from fractal_server.ssh._fabric import FractalSSHList
|
97
|
+
|
98
|
+
app.state.fractal_ssh_list = FractalSSHList()
|
99
|
+
|
100
100
|
logger.info(
|
101
|
-
|
102
|
-
f"({app.state.
|
101
|
+
"Added empty FractalSSHList to app.state "
|
102
|
+
f"(id={id(app.state.fractal_ssh_list)})."
|
103
103
|
)
|
104
104
|
else:
|
105
|
-
app.state.
|
105
|
+
app.state.fractal_ssh_list = None
|
106
106
|
|
107
107
|
config_uvicorn_loggers()
|
108
108
|
logger.info("End application startup")
|
109
109
|
reset_logger_handlers(logger)
|
110
|
+
|
110
111
|
yield
|
112
|
+
|
111
113
|
logger = get_logger("fractal_server.lifespan")
|
112
114
|
logger.info("Start application shutdown")
|
113
115
|
|
114
116
|
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
115
117
|
logger.info(
|
116
|
-
|
117
|
-
f"(current: {app.state.
|
118
|
+
"Close FractalSSH connections "
|
119
|
+
f"(current size: {app.state.fractal_ssh_list.size})."
|
118
120
|
)
|
119
121
|
|
120
|
-
app.state.
|
122
|
+
app.state.fractal_ssh_list.close_all()
|
121
123
|
|
122
124
|
logger.info(
|
123
125
|
f"Current worker with pid {os.getpid()} is shutting down. "
|
fractal_server/ssh/_fabric.py
CHANGED
@@ -16,19 +16,21 @@ from paramiko.ssh_exception import NoValidConnectionsError
|
|
16
16
|
|
17
17
|
from ..logger import get_logger
|
18
18
|
from ..logger import set_logger
|
19
|
-
from fractal_server.
|
20
|
-
from fractal_server.syringe import Inject
|
19
|
+
from fractal_server.string_tools import validate_cmd
|
21
20
|
|
22
21
|
|
23
22
|
class FractalSSHTimeoutError(RuntimeError):
|
24
23
|
pass
|
25
24
|
|
26
25
|
|
26
|
+
class FractalSSHListTimeoutError(RuntimeError):
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
27
30
|
logger = set_logger(__name__)
|
28
31
|
|
29
32
|
|
30
33
|
class FractalSSH(object):
|
31
|
-
|
32
34
|
"""
|
33
35
|
FIXME SSH: Fix docstring
|
34
36
|
|
@@ -110,7 +112,6 @@ class FractalSSH(object):
|
|
110
112
|
def run(
|
111
113
|
self, *args, lock_timeout: Optional[float] = None, **kwargs
|
112
114
|
) -> Any:
|
113
|
-
|
114
115
|
actual_lock_timeout = self.default_lock_timeout
|
115
116
|
if lock_timeout is not None:
|
116
117
|
actual_lock_timeout = lock_timeout
|
@@ -137,12 +138,25 @@ class FractalSSH(object):
|
|
137
138
|
)
|
138
139
|
|
139
140
|
def close(self) -> None:
|
140
|
-
|
141
|
+
"""
|
142
|
+
Aggressively close `self._connection`.
|
143
|
+
|
144
|
+
When `Connection.is_connected` is `False`, `Connection.close()` does
|
145
|
+
not call `Connection.client.close()`. Thus we do this explicitly here,
|
146
|
+
because we observed cases where `is_connected=False` but the underlying
|
147
|
+
`Transport` object was not closed.
|
148
|
+
"""
|
149
|
+
|
150
|
+
self._connection.close()
|
151
|
+
|
152
|
+
if self._connection.client is not None:
|
153
|
+
self._connection.client.close()
|
141
154
|
|
142
155
|
def run_command(
|
143
156
|
self,
|
144
157
|
*,
|
145
158
|
cmd: str,
|
159
|
+
allow_char: Optional[str] = None,
|
146
160
|
max_attempts: Optional[int] = None,
|
147
161
|
base_interval: Optional[int] = None,
|
148
162
|
lock_timeout: Optional[int] = None,
|
@@ -152,6 +166,7 @@ class FractalSSH(object):
|
|
152
166
|
|
153
167
|
Args:
|
154
168
|
cmd: Command to be run
|
169
|
+
allow_char: Forbidden chars to allow for this command
|
155
170
|
max_attempts:
|
156
171
|
base_interval:
|
157
172
|
lock_timeout:
|
@@ -159,6 +174,9 @@ class FractalSSH(object):
|
|
159
174
|
Returns:
|
160
175
|
Standard output of the command, if successful.
|
161
176
|
"""
|
177
|
+
|
178
|
+
validate_cmd(cmd, allow_char=allow_char)
|
179
|
+
|
162
180
|
actual_max_attempts = self.default_max_attempts
|
163
181
|
if max_attempts is not None:
|
164
182
|
actual_max_attempts = max_attempts
|
@@ -329,36 +347,169 @@ class FractalSSH(object):
|
|
329
347
|
f.write(content)
|
330
348
|
|
331
349
|
|
332
|
-
|
333
|
-
*,
|
334
|
-
host: Optional[str] = None,
|
335
|
-
user: Optional[str] = None,
|
336
|
-
key_filename: Optional[str] = None,
|
337
|
-
) -> Connection:
|
350
|
+
class FractalSSHList(object):
|
338
351
|
"""
|
339
|
-
|
340
|
-
or explicit arguments.
|
352
|
+
Collection of `FractalSSH` objects
|
341
353
|
|
342
|
-
|
343
|
-
|
344
|
-
user:
|
345
|
-
key_filename:
|
354
|
+
Attributes are all private, and access to this collection must be
|
355
|
+
through methods (mostly the `get` one).
|
346
356
|
|
347
|
-
|
348
|
-
|
357
|
+
Attributes:
|
358
|
+
_data:
|
359
|
+
Mapping of unique keys (the SSH-credentials tuples) to
|
360
|
+
`FractalSSH` objects.
|
361
|
+
_lock:
|
362
|
+
A `threading.Lock object`, to be acquired when changing `_data`.
|
363
|
+
_timeout: Timeout for `_lock` acquisition.
|
364
|
+
_logger_name: Logger name.
|
349
365
|
"""
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
366
|
+
|
367
|
+
_data: dict[tuple[str, str, str], FractalSSH]
|
368
|
+
_lock: Lock
|
369
|
+
_timeout: float
|
370
|
+
_logger_name: str
|
371
|
+
|
372
|
+
def __init__(
|
373
|
+
self,
|
374
|
+
*,
|
375
|
+
timeout: float = 5.0,
|
376
|
+
logger_name: str = "fractal_server.FractalSSHList",
|
377
|
+
):
|
378
|
+
self._lock = Lock()
|
379
|
+
self._data = {}
|
380
|
+
self._timeout = timeout
|
381
|
+
self._logger_name = logger_name
|
382
|
+
set_logger(self._logger_name)
|
383
|
+
|
384
|
+
@property
|
385
|
+
def logger(self) -> logging.Logger:
|
386
|
+
"""
|
387
|
+
This property exists so that we never have to propagate the
|
388
|
+
`Logger` object.
|
389
|
+
"""
|
390
|
+
return get_logger(self._logger_name)
|
391
|
+
|
392
|
+
@property
|
393
|
+
def size(self) -> int:
|
394
|
+
"""
|
395
|
+
Number of current key-value pairs in `self._data`.
|
396
|
+
"""
|
397
|
+
return len(self._data.values())
|
398
|
+
|
399
|
+
def get(self, *, host: str, user: str, key_path: str) -> FractalSSH:
|
400
|
+
"""
|
401
|
+
Get the `FractalSSH` for the current credentials, or create one.
|
402
|
+
|
403
|
+
Note: Changing `_data` requires acquiring `_lock`.
|
404
|
+
|
405
|
+
Arguments:
|
406
|
+
host:
|
407
|
+
user:
|
408
|
+
key_path:
|
409
|
+
"""
|
410
|
+
key = (host, user, key_path)
|
411
|
+
fractal_ssh = self._data.get(key, None)
|
412
|
+
if fractal_ssh is not None:
|
413
|
+
self.logger.info(
|
414
|
+
f"Return existing FractalSSH object for {user}@{host}"
|
415
|
+
)
|
416
|
+
return fractal_ssh
|
417
|
+
else:
|
418
|
+
self.logger.info(f"Add new FractalSSH object for {user}@{host}")
|
419
|
+
connection = Connection(
|
420
|
+
host=host,
|
421
|
+
user=user,
|
422
|
+
forward_agent=False,
|
423
|
+
connect_kwargs={
|
424
|
+
"key_filename": key_path,
|
425
|
+
"look_for_keys": False,
|
426
|
+
},
|
427
|
+
)
|
428
|
+
with self.acquire_lock_with_timeout():
|
429
|
+
self._data[key] = FractalSSH(connection=connection)
|
430
|
+
return self._data[key]
|
431
|
+
|
432
|
+
def contains(
|
433
|
+
self,
|
434
|
+
*,
|
435
|
+
host: str,
|
436
|
+
user: str,
|
437
|
+
key_path: str,
|
438
|
+
) -> bool:
|
439
|
+
"""
|
440
|
+
Return whether a given key is present in the collection.
|
441
|
+
|
442
|
+
Arguments:
|
443
|
+
host:
|
444
|
+
user:
|
445
|
+
key_path:
|
446
|
+
"""
|
447
|
+
key = (host, user, key_path)
|
448
|
+
return key in self._data.keys()
|
449
|
+
|
450
|
+
def remove(
|
451
|
+
self,
|
452
|
+
*,
|
453
|
+
host: str,
|
454
|
+
user: str,
|
455
|
+
key_path: str,
|
456
|
+
) -> None:
|
457
|
+
"""
|
458
|
+
Remove a key from `_data` and close the corresponding connection.
|
459
|
+
|
460
|
+
Note: Changing `_data` requires acquiring `_lock`.
|
461
|
+
|
462
|
+
Arguments:
|
463
|
+
host:
|
464
|
+
user:
|
465
|
+
key_path:
|
466
|
+
"""
|
467
|
+
key = (host, user, key_path)
|
468
|
+
with self.acquire_lock_with_timeout():
|
469
|
+
self.logger.info(
|
470
|
+
f"Removing FractalSSH object for {user}@{host} "
|
471
|
+
"from collection."
|
472
|
+
)
|
473
|
+
fractal_ssh_obj = self._data.pop(key)
|
474
|
+
self.logger.info(
|
475
|
+
f"Closing FractalSSH object for {user}@{host} "
|
476
|
+
f"({fractal_ssh_obj.is_connected=})."
|
477
|
+
)
|
478
|
+
fractal_ssh_obj.close()
|
479
|
+
|
480
|
+
def close_all(self, *, timeout: float = 5.0):
|
481
|
+
"""
|
482
|
+
Close all `FractalSSH` objects in the collection.
|
483
|
+
|
484
|
+
Arguments:
|
485
|
+
timeout:
|
486
|
+
Timeout for `FractalSSH._lock` acquisition, to be obtained
|
487
|
+
before closing.
|
488
|
+
"""
|
489
|
+
for key, fractal_ssh_obj in self._data.items():
|
490
|
+
host, user, _ = key[:]
|
491
|
+
self.logger.info(
|
492
|
+
f"Closing FractalSSH object for {user}@{host} "
|
493
|
+
f"({fractal_ssh_obj.is_connected=})."
|
494
|
+
)
|
495
|
+
with fractal_ssh_obj.acquire_timeout(timeout=timeout):
|
496
|
+
fractal_ssh_obj.close()
|
497
|
+
|
498
|
+
@contextmanager
|
499
|
+
def acquire_lock_with_timeout(self) -> Generator[Literal[True], Any, None]:
|
500
|
+
self.logger.debug(
|
501
|
+
f"Trying to acquire lock, with timeout {self._timeout} s"
|
502
|
+
)
|
503
|
+
result = self._lock.acquire(timeout=self._timeout)
|
504
|
+
try:
|
505
|
+
if not result:
|
506
|
+
self.logger.error("Lock was *NOT* acquired.")
|
507
|
+
raise FractalSSHListTimeoutError(
|
508
|
+
f"Failed to acquire lock within {self._timeout} ss"
|
509
|
+
)
|
510
|
+
self.logger.debug("Lock was acquired.")
|
511
|
+
yield result
|
512
|
+
finally:
|
513
|
+
if result:
|
514
|
+
self._lock.release()
|
515
|
+
self.logger.debug("Lock was released")
|
fractal_server/string_tools.py
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
import string
|
2
|
+
from typing import Optional
|
2
3
|
|
3
4
|
__SPECIAL_CHARACTERS__ = f"{string.punctuation}{string.whitespace}"
|
4
5
|
|
6
|
+
# List of invalid characters discussed here:
|
7
|
+
# https://github.com/fractal-analytics-platform/fractal-server/issues/1647
|
8
|
+
__NOT_ALLOWED_FOR_COMMANDS__ = r"`#$&*()\|[]{};<>?!"
|
9
|
+
|
5
10
|
|
6
11
|
def sanitize_string(value: str) -> str:
|
7
12
|
"""
|
@@ -43,3 +48,23 @@ def slugify_task_name_for_source(task_name: str) -> str:
|
|
43
48
|
Slug-ified task name.
|
44
49
|
"""
|
45
50
|
return task_name.replace(" ", "_").lower()
|
51
|
+
|
52
|
+
|
53
|
+
def validate_cmd(command: str, allow_char: Optional[str] = None):
|
54
|
+
"""
|
55
|
+
Assert that the provided `command` does not contain any of the forbidden
|
56
|
+
characters for commands
|
57
|
+
(fractal_server.string_tools.__NOT_ALLOWED_FOR_COMMANDS__)
|
58
|
+
|
59
|
+
Args:
|
60
|
+
command: command to validate.
|
61
|
+
allow_char: chars to accept among the forbidden ones
|
62
|
+
"""
|
63
|
+
forbidden = set(__NOT_ALLOWED_FOR_COMMANDS__)
|
64
|
+
if allow_char is not None:
|
65
|
+
forbidden = forbidden - set(allow_char)
|
66
|
+
if not forbidden.isdisjoint(set(command)):
|
67
|
+
raise ValueError(
|
68
|
+
f"Command must not contain any of this characters: '{forbidden}'\n"
|
69
|
+
f"Provided command: '{command}'."
|
70
|
+
)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
fractal_server/__init__.py,sha256=
|
1
|
+
fractal_server/__init__.py,sha256=zW_UoRr0gmuphO3yp_Nmzq1qV6ZNQIt_3zHJMXwFtIM,22
|
2
2
|
fractal_server/__main__.py,sha256=upYBkGYrkBnkS1rp4D_nb_1LS37QT4j-wxGX1ZMvR4A,5704
|
3
3
|
fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
|
4
4
|
fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -43,10 +43,10 @@ fractal_server/app/routes/api/v2/images.py,sha256=JR1rR6qEs81nacjriOXAOBQjAbCXF4
|
|
43
43
|
fractal_server/app/routes/api/v2/job.py,sha256=Bga2Kz1OjvDIdxZObWaaXVhNIhC_5JKhKRjEH2_ayEE,5157
|
44
44
|
fractal_server/app/routes/api/v2/project.py,sha256=eWYFJ7F2ZYQcpi-_n-rhPF-Q4gJhzYBsVGYFhHZZXAE,6653
|
45
45
|
fractal_server/app/routes/api/v2/status.py,sha256=6N9DSZ4iFqbZImorWfEAPoyoFUgEruo4Hweqo0x0xXU,6435
|
46
|
-
fractal_server/app/routes/api/v2/submit.py,sha256=
|
46
|
+
fractal_server/app/routes/api/v2/submit.py,sha256=tyaeEpGMEkazdmltlnJxJYfD9Y9_t9mP2MUmx3s1Ato,9223
|
47
47
|
fractal_server/app/routes/api/v2/task.py,sha256=XgRnGBvSoI9VNJHtWZQ2Ide99f6elo7a2FN3GQkf0dU,8376
|
48
|
-
fractal_server/app/routes/api/v2/task_collection.py,sha256=
|
49
|
-
fractal_server/app/routes/api/v2/task_collection_custom.py,sha256=
|
48
|
+
fractal_server/app/routes/api/v2/task_collection.py,sha256=SirU4yiE4pGfW68cyopMLgHSevIzaepQXLZJeIdaoDE,12697
|
49
|
+
fractal_server/app/routes/api/v2/task_collection_custom.py,sha256=CbeC7xYYF8K9JVOOunL3Y_3wXBEGGGoiJcoPa2hEftI,6127
|
50
50
|
fractal_server/app/routes/api/v2/workflow.py,sha256=rMCcclz9aJAMSVLncUdSDGrgkKbn4KOCZTqZtqs2HDY,10428
|
51
51
|
fractal_server/app/routes/api/v2/workflowtask.py,sha256=-3-c8DDnxGjMwWbX_h5V5OLaC_iCLXYzwWKBUaL-5wE,7060
|
52
52
|
fractal_server/app/routes/auth/__init__.py,sha256=fao6CS0WiAjHDTvBzgBVV_bSXFpEAeDBF6Z6q7rRkPc,1658
|
@@ -66,7 +66,7 @@ fractal_server/app/runner/.gitignore,sha256=ytzN_oyHWXrGU7iFAtoHSTUbM6Rn6kG0Zkdd
|
|
66
66
|
fractal_server/app/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
67
67
|
fractal_server/app/runner/async_wrap.py,sha256=_O6f8jftKYXG_DozkmlrDBhoiK9QhE9MablOyECq2_M,829
|
68
68
|
fractal_server/app/runner/components.py,sha256=ZF8ct_Ky5k8IAcrmpYOZ-bc6OBgdELEighYVqFDEbZg,119
|
69
|
-
fractal_server/app/runner/compress_folder.py,sha256=
|
69
|
+
fractal_server/app/runner/compress_folder.py,sha256=HSc1tv7x2DBjBoXwugZlC79rm9GNBIWtQKK9yWn5ZBI,3991
|
70
70
|
fractal_server/app/runner/exceptions.py,sha256=_qZ_t8O4umAdJ1ikockiF5rDJuxnEskrGrLjZcnQl7A,4159
|
71
71
|
fractal_server/app/runner/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
72
72
|
fractal_server/app/runner/executors/slurm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -76,20 +76,20 @@ fractal_server/app/runner/executors/slurm/remote.py,sha256=wLziIsGdSMiO-jIXM8x77
|
|
76
76
|
fractal_server/app/runner/executors/slurm/ssh/__init__.py,sha256=Cjn1rYvljddi96tAwS-qqGkNfOcfPzjChdaEZEObCcM,65
|
77
77
|
fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py,sha256=bKo5Ja0IGxJWpPWyh9dN0AG-PwzTDZzD5LyaEHB3YU4,3742
|
78
78
|
fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py,sha256=rwlqZzoGo4SAb4nSlFjsQJdaCgfM1J6YGcjb8yYxlqc,4506
|
79
|
-
fractal_server/app/runner/executors/slurm/ssh/executor.py,sha256=
|
79
|
+
fractal_server/app/runner/executors/slurm/ssh/executor.py,sha256=K1lKsn40PqQb-GZKwJkzeJk2Q8OGqy6YR-IxcV0E0Pw,57705
|
80
80
|
fractal_server/app/runner/executors/slurm/sudo/__init__.py,sha256=Cjn1rYvljddi96tAwS-qqGkNfOcfPzjChdaEZEObCcM,65
|
81
81
|
fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py,sha256=wAgwpVcr6JIslKHOuS0FhRa_6T1KCManyRJqA-fifzw,1909
|
82
82
|
fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py,sha256=z5LlhaiqAb8pHsF1WwdzXN39C5anQmwjo1rSQgtRAYE,4422
|
83
|
-
fractal_server/app/runner/executors/slurm/sudo/_subprocess_run_as_user.py,sha256=
|
84
|
-
fractal_server/app/runner/executors/slurm/sudo/executor.py,sha256=
|
83
|
+
fractal_server/app/runner/executors/slurm/sudo/_subprocess_run_as_user.py,sha256=g8wqUjSicN17UZVXlfaMomYZ-xOIbBu1oE7HdJTzfvw,5218
|
84
|
+
fractal_server/app/runner/executors/slurm/sudo/executor.py,sha256=6OPe9t70gLyuC2JhWt2o1f0e7zhQPBtrbMHQkDd6RAQ,48725
|
85
85
|
fractal_server/app/runner/extract_archive.py,sha256=tLpjDrX47OjTNhhoWvm6iNukg8KoieWyTb7ZfvE9eWU,2483
|
86
86
|
fractal_server/app/runner/filenames.py,sha256=9lwu3yB4C67yiijYw8XIKaLFn3mJUt6_TCyVFM_aZUQ,206
|
87
|
-
fractal_server/app/runner/run_subprocess.py,sha256=
|
87
|
+
fractal_server/app/runner/run_subprocess.py,sha256=c3JbYXq3hX2aaflQU19qJ5Xs6J6oXGNvnTEoAfv2bxc,959
|
88
88
|
fractal_server/app/runner/set_start_and_last_task_index.py,sha256=-q4zVybAj8ek2XlbENKlfOAJ39hT_zoJoZkqzDqiAMY,1254
|
89
89
|
fractal_server/app/runner/shutdown.py,sha256=I_o2iYKJwzku0L3E85ETjrve3QPECygR5xhhsAo5huM,2910
|
90
90
|
fractal_server/app/runner/task_files.py,sha256=sd_MpJ01C8c9QTO8GzGMidFGdlq_hXX_ARDRhd_YMnI,3762
|
91
91
|
fractal_server/app/runner/v1/__init__.py,sha256=VvJFk4agX2X3fQfDcoNmOB2ouNCaQU7dAqaFmpcdP8I,15063
|
92
|
-
fractal_server/app/runner/v1/_common.py,sha256=
|
92
|
+
fractal_server/app/runner/v1/_common.py,sha256=EiSfp-PvhtTD3uijSec5CNKxe50ITts2DyGCFcjfVBw,21619
|
93
93
|
fractal_server/app/runner/v1/_local/__init__.py,sha256=KlSML4LqF4p1IfhSd8tAkiu3aeDzifeanuNXjATDsYE,6929
|
94
94
|
fractal_server/app/runner/v1/_local/_local_config.py,sha256=hM7SPxR07luXPcXdrWXRpEB2uOyjSSRUdqW3QBKJn9c,3147
|
95
95
|
fractal_server/app/runner/v1/_local/_submit_setup.py,sha256=XyBDPb4IYdKEEnzLYdcYteIHWVWofJxKMmQCyRkn5Bc,1509
|
@@ -119,12 +119,12 @@ fractal_server/app/runner/v2/handle_failed_job.py,sha256=fipRJT5Y8UY0US4bXUX-4OR
|
|
119
119
|
fractal_server/app/runner/v2/merge_outputs.py,sha256=IHuHqbKmk97K35BFvTrKVBs60z3e_--OzXTnsvmA02c,1281
|
120
120
|
fractal_server/app/runner/v2/runner.py,sha256=nw9oYt3cFItHWVoevJyMI63K0kWHCTAriAQ_KINo_F8,13039
|
121
121
|
fractal_server/app/runner/v2/runner_functions.py,sha256=BLREIcQaE6FSc2AEJyZuiYk6rGazEz_9gprUqUZDljs,9488
|
122
|
-
fractal_server/app/runner/v2/runner_functions_low_level.py,sha256=
|
122
|
+
fractal_server/app/runner/v2/runner_functions_low_level.py,sha256=1fWvQ6YZUUnDhO_mipXC5hnaT-zK-GHxg8ayoxZX82k,3648
|
123
123
|
fractal_server/app/runner/v2/task_interface.py,sha256=myS-kT0DsJ8xIJZBVEzgD8g54VbiwL6i7Im3e1zcVHQ,1866
|
124
124
|
fractal_server/app/runner/versions.py,sha256=dSaPRWqmFPHjg20kTCHmi_dmGNcCETflDtDLronNanU,852
|
125
125
|
fractal_server/app/schemas/__init__.py,sha256=jiIf54owztXupv3PO6Ilh0qcrkh2RUzKq4bcEFqEfc4,40
|
126
126
|
fractal_server/app/schemas/_validators.py,sha256=1dTOYr1IZykrxuQSV2-zuEMZbKe_nGwrfS7iUrsh-sE,3461
|
127
|
-
fractal_server/app/schemas/user.py,sha256=
|
127
|
+
fractal_server/app/schemas/user.py,sha256=OJutfwMR1JPEmdFzqA4vHMZO-mhB4Mb9Yyx_G24XTCM,4081
|
128
128
|
fractal_server/app/schemas/user_group.py,sha256=2f9XQ6kIar6NMY4UCN0yOnve6ZDHUVZaHv1dna1Vfjg,1446
|
129
129
|
fractal_server/app/schemas/v1/__init__.py,sha256=CrBGgBhoemCvmZ70ZUchM-jfVAICnoa7AjZBAtL2UB0,1852
|
130
130
|
fractal_server/app/schemas/v1/applyworkflow.py,sha256=uuIh7fHlHEL4yLqL-dePI6-nfCsqgBYATmht7w_KITw,4302
|
@@ -138,12 +138,12 @@ fractal_server/app/schemas/v1/task_collection.py,sha256=uvq9bcMaGD_qHsh7YtcpoSAk
|
|
138
138
|
fractal_server/app/schemas/v1/workflow.py,sha256=tuOs5E5Q_ozA8if7YPZ07cQjzqB_QMkBS4u92qo4Ro0,4618
|
139
139
|
fractal_server/app/schemas/v2/__init__.py,sha256=kmM4NfSGIL0I4xVtnmMST20kfVo3nBBG-Ssk8vJAvLs,1979
|
140
140
|
fractal_server/app/schemas/v2/dataset.py,sha256=dLT52tV4dSf2HrFNak4vdQEn8PT_04IUrGnd2z-AXIU,2599
|
141
|
-
fractal_server/app/schemas/v2/dumps.py,sha256=
|
141
|
+
fractal_server/app/schemas/v2/dumps.py,sha256=ZrJCHTv9oU2QMNjPUSBO3DIPRO3qDvbxpAGpernpf-Q,1720
|
142
142
|
fractal_server/app/schemas/v2/job.py,sha256=zfF9K3v4jWUJ7M482ta2CkqUJ4tVT4XfVt60p9IRhP0,3250
|
143
143
|
fractal_server/app/schemas/v2/manifest.py,sha256=N37IWohcfO3_y2l8rVM0h_1nZq7m4Izxk9iL1vtwBJw,6243
|
144
144
|
fractal_server/app/schemas/v2/project.py,sha256=u7S4B-bote1oGjzAGiZ-DuQIyeRAGqJsI71Tc1EtYE0,736
|
145
145
|
fractal_server/app/schemas/v2/status.py,sha256=SQaUpQkjFq5c5k5J4rOjNhuQaDOEg8lksPhkKmPU5VU,332
|
146
|
-
fractal_server/app/schemas/v2/task.py,sha256=
|
146
|
+
fractal_server/app/schemas/v2/task.py,sha256=XsN8w1Szs8BrxxRtKyWCHKjN4Od-Kmlhi769JEplL-M,4804
|
147
147
|
fractal_server/app/schemas/v2/task_collection.py,sha256=8PG1bOqkfQqORMN0brWf6mHDmijt0bBW-mZsF7cSxUs,6129
|
148
148
|
fractal_server/app/schemas/v2/workflow.py,sha256=Zzx3e-qgkH8le0FUmAx9UrV5PWd7bj14PPXUh_zgZXM,1827
|
149
149
|
fractal_server/app/schemas/v2/workflowtask.py,sha256=TN-mdkuE_EWet9Wk-xFrUwIt_tXYcw88WOKMnUcchKk,5665
|
@@ -156,7 +156,7 @@ fractal_server/images/__init__.py,sha256=xO6jTLE4EZKO6cTDdJsBmK9cdeh9hFTaSbSuWgQ
|
|
156
156
|
fractal_server/images/models.py,sha256=9ipU5h4N6ogBChoB-2vHoqtL0TXOHCv6kRR-fER3mkM,4167
|
157
157
|
fractal_server/images/tools.py,sha256=gxeniYy4Z-cp_ToK2LHPJUTVVUUrdpogYdcBUvBuLiY,2209
|
158
158
|
fractal_server/logger.py,sha256=56wfka6fHaa3Rx5qO009nEs_y8gx5wZ2NUNZZ1I-uvc,5130
|
159
|
-
fractal_server/main.py,sha256=
|
159
|
+
fractal_server/main.py,sha256=68rVybnviF28yFgRhfHZXwnf6LyzkcmeYYZZppboU4M,4925
|
160
160
|
fractal_server/migrations/README,sha256=4rQvyDfqodGhpJw74VYijRmgFP49ji5chyEemWGHsuw,59
|
161
161
|
fractal_server/migrations/env.py,sha256=mEiX0TRa_8KAYBrUGJTx1cFJ5YAq_oNHHsFCp1raegk,2543
|
162
162
|
fractal_server/migrations/naming_convention.py,sha256=htbKrVdetx3pklowb_9Cdo5RqeF0fJ740DNecY5de_M,265
|
@@ -181,8 +181,8 @@ fractal_server/migrations/versions/efa89c30e0a4_add_project_timestamp_created.py
|
|
181
181
|
fractal_server/migrations/versions/f384e1c0cf5d_drop_task_default_args_columns.py,sha256=9BwqUS9Gf7UW_KjrzHbtViC880qhD452KAytkHWWZyk,746
|
182
182
|
fractal_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
183
183
|
fractal_server/ssh/__init__.py,sha256=sVUmzxf7_DuXG1xoLQ1_00fo5NPhi2LJipSmU5EAkPs,124
|
184
|
-
fractal_server/ssh/_fabric.py,sha256=
|
185
|
-
fractal_server/string_tools.py,sha256=
|
184
|
+
fractal_server/ssh/_fabric.py,sha256=B9C7Cj9ibrT1_OGlu38Jz94elNofLvkP1pxf0Tv8Eic,16140
|
185
|
+
fractal_server/string_tools.py,sha256=YyopB2ZZ8iL9JLDXUA8PI0hahivqLiohir2HsAlEzqE,2170
|
186
186
|
fractal_server/syringe.py,sha256=3qSMW3YaMKKnLdgnooAINOPxnCOxP7y2jeAQYB21Gdo,2786
|
187
187
|
fractal_server/tasks/__init__.py,sha256=kadmVUoIghl8s190_Tt-8f-WBqMi8u8oU4Pvw39NHE8,23
|
188
188
|
fractal_server/tasks/utils.py,sha256=wucz57I7G0Vd8hvtmvonlryACx9zIVlqfxG5I87MJ80,1820
|
@@ -207,8 +207,8 @@ fractal_server/tasks/v2/utils.py,sha256=JOyCacb6MNvrwfLNTyLwcz8y79J29YuJeJ2MK5kq
|
|
207
207
|
fractal_server/urls.py,sha256=5o_qq7PzKKbwq12NHSQZDmDitn5RAOeQ4xufu-2v9Zk,448
|
208
208
|
fractal_server/utils.py,sha256=b7WwFdcFZ8unyT65mloFToYuEDXpQoHRcmRNqrhd_dQ,2115
|
209
209
|
fractal_server/zip_tools.py,sha256=xYpzBshysD2nmxkD5WLYqMzPYUcCRM3kYy-7n9bJL-U,4426
|
210
|
-
fractal_server-2.5.
|
211
|
-
fractal_server-2.5.
|
212
|
-
fractal_server-2.5.
|
213
|
-
fractal_server-2.5.
|
214
|
-
fractal_server-2.5.
|
210
|
+
fractal_server-2.5.2.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
|
211
|
+
fractal_server-2.5.2.dist-info/METADATA,sha256=Q_lBgfC8qNcsp8ll6WrxkYOvt8P0YAVJCaWPuOIgFTY,4628
|
212
|
+
fractal_server-2.5.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
213
|
+
fractal_server-2.5.2.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
|
214
|
+
fractal_server-2.5.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|