fractal-server 2.14.3a0__py3-none-any.whl → 2.14.4__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 (42) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/job.py +2 -2
  3. fractal_server/app/routes/api/v2/images.py +4 -20
  4. fractal_server/app/routes/api/v2/pre_submission_checks.py +2 -2
  5. fractal_server/app/runner/{run_subprocess.py → executors/slurm_ssh/run_subprocess.py} +11 -6
  6. fractal_server/app/runner/executors/slurm_ssh/runner.py +11 -56
  7. fractal_server/app/runner/executors/slurm_ssh/tar_commands.py +65 -0
  8. fractal_server/app/runner/v2/_local.py +2 -2
  9. fractal_server/app/runner/v2/_slurm_ssh.py +2 -2
  10. fractal_server/app/runner/v2/_slurm_sudo.py +2 -2
  11. fractal_server/app/runner/v2/runner.py +2 -2
  12. fractal_server/app/runner/v2/task_interface.py +3 -14
  13. fractal_server/app/schemas/user.py +11 -35
  14. fractal_server/app/schemas/user_group.py +3 -23
  15. fractal_server/app/schemas/user_settings.py +17 -43
  16. fractal_server/app/schemas/v2/dataset.py +10 -50
  17. fractal_server/app/schemas/v2/job.py +19 -60
  18. fractal_server/app/schemas/v2/manifest.py +10 -25
  19. fractal_server/app/schemas/v2/project.py +3 -11
  20. fractal_server/app/schemas/v2/task.py +36 -106
  21. fractal_server/app/schemas/v2/task_collection.py +31 -81
  22. fractal_server/app/schemas/v2/task_group.py +14 -34
  23. fractal_server/app/schemas/v2/workflow.py +13 -28
  24. fractal_server/app/schemas/v2/workflowtask.py +18 -126
  25. fractal_server/config.py +20 -73
  26. fractal_server/images/models.py +15 -81
  27. fractal_server/images/tools.py +3 -3
  28. fractal_server/ssh/_fabric.py +4 -1
  29. fractal_server/types/__init__.py +87 -0
  30. fractal_server/types/validators/__init__.py +6 -0
  31. fractal_server/types/validators/_common_validators.py +42 -0
  32. fractal_server/types/validators/_filter_validators.py +24 -0
  33. fractal_server/types/validators/_workflow_task_arguments_validators.py +10 -0
  34. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4.dist-info}/METADATA +1 -1
  35. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4.dist-info}/RECORD +38 -36
  36. fractal_server/app/runner/compress_folder.py +0 -144
  37. fractal_server/app/runner/extract_archive.py +0 -99
  38. fractal_server/app/schemas/_filter_validators.py +0 -46
  39. fractal_server/app/schemas/_validators.py +0 -86
  40. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4.dist-info}/LICENSE +0 -0
  41. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4.dist-info}/WHEEL +0 -0
  42. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4.dist-info}/entry_points.txt +0 -0
@@ -6,14 +6,12 @@ from pydantic import BaseModel
6
6
  from pydantic import ConfigDict
7
7
  from pydantic import Field
8
8
  from pydantic import field_serializer
9
- from pydantic import field_validator
10
9
  from pydantic.types import AwareDatetime
11
10
 
12
- from .._validators import cant_set_none
13
- from .._validators import NonEmptyString
14
- from .._validators import val_absolute_path
15
- from .._validators import valdict_keys
16
- from .task import TaskReadV2
11
+ from fractal_server.app.schemas.v2.task import TaskReadV2
12
+ from fractal_server.types import AbsolutePathStr
13
+ from fractal_server.types import DictStrStr
14
+ from fractal_server.types import NonEmptyStr
17
15
 
18
16
 
19
17
  class TaskGroupV2OriginEnum(str, Enum):
@@ -43,31 +41,13 @@ class TaskGroupCreateV2(BaseModel):
43
41
  origin: TaskGroupV2OriginEnum
44
42
  pkg_name: str
45
43
  version: Optional[str] = None
46
- python_version: Optional[NonEmptyString] = None
47
- path: Optional[str] = None
48
- venv_path: Optional[str] = None
49
- wheel_path: Optional[str] = None
50
- pip_extras: Optional[NonEmptyString] = None
44
+ python_version: NonEmptyStr = None
45
+ path: AbsolutePathStr = None
46
+ venv_path: AbsolutePathStr = None
47
+ wheel_path: AbsolutePathStr = None
48
+ pip_extras: NonEmptyStr = None
51
49
  pip_freeze: Optional[str] = None
52
- pinned_package_versions: dict[str, str] = Field(default_factory=dict)
53
-
54
- # Validators
55
-
56
- @field_validator("python_version", "pip_extras")
57
- @classmethod
58
- def _cant_set_none(cls, v):
59
- return cant_set_none(v)
60
-
61
- _path = field_validator("path")(classmethod(val_absolute_path("path")))
62
- _venv_path = field_validator("venv_path")(
63
- classmethod(val_absolute_path("venv_path"))
64
- )
65
- _wheel_path = field_validator("wheel_path")(
66
- classmethod(val_absolute_path("wheel_path"))
67
- )
68
- _pinned_package_versions = field_validator("pinned_package_versions")(
69
- valdict_keys("pinned_package_versions")
70
- )
50
+ pinned_package_versions: DictStrStr = Field(default_factory=dict)
71
51
 
72
52
 
73
53
  class TaskGroupCreateV2Strict(TaskGroupCreateV2):
@@ -75,10 +55,10 @@ class TaskGroupCreateV2Strict(TaskGroupCreateV2):
75
55
  A strict version of TaskGroupCreateV2, to be used for task collection.
76
56
  """
77
57
 
78
- path: str
79
- venv_path: str
80
- version: str
81
- python_version: str
58
+ path: AbsolutePathStr
59
+ venv_path: AbsolutePathStr
60
+ version: NonEmptyStr
61
+ python_version: NonEmptyStr
82
62
 
83
63
 
84
64
  class TaskGroupReadV2(BaseModel):
@@ -4,23 +4,24 @@ from typing import Optional
4
4
  from pydantic import BaseModel
5
5
  from pydantic import ConfigDict
6
6
  from pydantic import field_serializer
7
- from pydantic import field_validator
8
7
  from pydantic.types import AwareDatetime
9
8
 
10
- from .._validators import cant_set_none
11
- from .._validators import NonEmptyString
12
- from .project import ProjectReadV2
13
- from .workflowtask import WorkflowTaskExportV2
14
- from .workflowtask import WorkflowTaskImportV2
15
- from .workflowtask import WorkflowTaskReadV2
16
- from .workflowtask import WorkflowTaskReadV2WithWarning
9
+ from fractal_server.app.schemas.v2.project import ProjectReadV2
10
+ from fractal_server.app.schemas.v2.workflowtask import WorkflowTaskExportV2
11
+ from fractal_server.app.schemas.v2.workflowtask import WorkflowTaskImportV2
12
+ from fractal_server.app.schemas.v2.workflowtask import WorkflowTaskReadV2
13
+ from fractal_server.app.schemas.v2.workflowtask import (
14
+ WorkflowTaskReadV2WithWarning,
15
+ )
16
+ from fractal_server.types import ListUniqueNonNegativeInt
17
+ from fractal_server.types import NonEmptyStr
17
18
 
18
19
 
19
20
  class WorkflowCreateV2(BaseModel):
20
21
 
21
22
  model_config = ConfigDict(extra="forbid")
22
23
 
23
- name: NonEmptyString
24
+ name: NonEmptyStr
24
25
 
25
26
 
26
27
  class WorkflowReadV2(BaseModel):
@@ -45,24 +46,8 @@ class WorkflowUpdateV2(BaseModel):
45
46
 
46
47
  model_config = ConfigDict(extra="forbid")
47
48
 
48
- name: Optional[NonEmptyString] = None
49
- reordered_workflowtask_ids: Optional[list[int]] = None
50
-
51
- # Validators
52
-
53
- @field_validator("name")
54
- @classmethod
55
- def _cant_set_none(cls, v):
56
- return cant_set_none(v)
57
-
58
- @field_validator("reordered_workflowtask_ids")
59
- @classmethod
60
- def check_positive_and_unique(cls, value):
61
- if any(i < 0 for i in value):
62
- raise ValueError("Negative `id` in `reordered_workflowtask_ids`")
63
- if len(value) != len(set(value)):
64
- raise ValueError("`reordered_workflowtask_ids` has repetitions")
65
- return value
49
+ name: NonEmptyStr = None
50
+ reordered_workflowtask_ids: Optional[ListUniqueNonNegativeInt] = None
66
51
 
67
52
 
68
53
  class WorkflowImportV2(BaseModel):
@@ -74,7 +59,7 @@ class WorkflowImportV2(BaseModel):
74
59
  """
75
60
 
76
61
  model_config = ConfigDict(extra="forbid")
77
- name: NonEmptyString
62
+ name: NonEmptyStr
78
63
  task_list: list[WorkflowTaskImportV2]
79
64
 
80
65
 
@@ -5,73 +5,26 @@ from typing import Union
5
5
  from pydantic import BaseModel
6
6
  from pydantic import ConfigDict
7
7
  from pydantic import Field
8
- from pydantic import field_validator
9
8
  from pydantic import model_validator
10
9
 
11
- from .._filter_validators import validate_type_filters
12
- from .._validators import root_validate_dict_keys
13
- from .._validators import valdict_keys
14
10
  from .task import TaskExportV2
15
11
  from .task import TaskImportV2
16
12
  from .task import TaskImportV2Legacy
17
13
  from .task import TaskReadV2
18
14
  from .task import TaskTypeType
19
-
20
- RESERVED_ARGUMENTS = {"zarr_dir", "zarr_url", "zarr_urls", "init_args"}
15
+ from fractal_server.types import DictStrAny
16
+ from fractal_server.types import TypeFilters
17
+ from fractal_server.types import WorkflowTaskArgument
21
18
 
22
19
 
23
20
  class WorkflowTaskCreateV2(BaseModel):
24
21
  model_config = ConfigDict(extra="forbid")
25
22
 
26
- meta_non_parallel: Optional[dict[str, Any]] = None
27
- meta_parallel: Optional[dict[str, Any]] = None
28
- args_non_parallel: Optional[dict[str, Any]] = None
29
- args_parallel: Optional[dict[str, Any]] = None
30
- type_filters: dict[str, bool] = Field(default_factory=dict)
31
-
32
- # Validators
33
- _dict_keys = model_validator(mode="before")(
34
- classmethod(root_validate_dict_keys)
35
- )
36
- _type_filters = field_validator("type_filters")(
37
- classmethod(validate_type_filters)
38
- )
39
- _meta_non_parallel = field_validator("meta_non_parallel")(
40
- classmethod(valdict_keys("meta_non_parallel"))
41
- )
42
- _meta_parallel = field_validator("meta_parallel")(
43
- classmethod(valdict_keys("meta_parallel"))
44
- )
45
-
46
- @field_validator("args_non_parallel")
47
- @classmethod
48
- def validate_args_non_parallel(cls, value):
49
- if value is None:
50
- return
51
- valdict_keys("args_non_parallel")(cls, value)
52
- args_keys = set(value.keys())
53
- intersect_keys = RESERVED_ARGUMENTS.intersection(args_keys)
54
- if intersect_keys:
55
- raise ValueError(
56
- "`args` contains the following forbidden keys: "
57
- f"{intersect_keys}"
58
- )
59
- return value
60
-
61
- @field_validator("args_parallel")
62
- @classmethod
63
- def validate_args_parallel(cls, value):
64
- if value is None:
65
- return
66
- valdict_keys("args_parallel")(cls, value)
67
- args_keys = set(value.keys())
68
- intersect_keys = RESERVED_ARGUMENTS.intersection(args_keys)
69
- if intersect_keys:
70
- raise ValueError(
71
- "`args` contains the following forbidden keys: "
72
- f"{intersect_keys}"
73
- )
74
- return value
23
+ meta_non_parallel: Optional[DictStrAny] = None
24
+ meta_parallel: Optional[DictStrAny] = None
25
+ args_non_parallel: Optional[WorkflowTaskArgument] = None
26
+ args_parallel: Optional[WorkflowTaskArgument] = None
27
+ type_filters: TypeFilters = Field(default_factory=dict)
75
28
 
76
29
 
77
30
  class WorkflowTaskReplaceV2(BaseModel):
@@ -106,70 +59,25 @@ class WorkflowTaskReadV2WithWarning(WorkflowTaskReadV2):
106
59
  class WorkflowTaskUpdateV2(BaseModel):
107
60
  model_config = ConfigDict(extra="forbid")
108
61
 
109
- meta_non_parallel: Optional[dict[str, Any]] = None
110
- meta_parallel: Optional[dict[str, Any]] = None
111
- args_non_parallel: Optional[dict[str, Any]] = None
112
- args_parallel: Optional[dict[str, Any]] = None
113
- type_filters: Optional[dict[str, bool]] = None
114
-
115
- # Validators
116
- _dict_keys = model_validator(mode="before")(
117
- classmethod(root_validate_dict_keys)
118
- )
119
- _type_filters = field_validator("type_filters")(
120
- classmethod(validate_type_filters)
121
- )
122
- _meta_non_parallel = field_validator("meta_non_parallel")(
123
- classmethod(valdict_keys("meta_non_parallel"))
124
- )
125
- _meta_parallel = field_validator("meta_parallel")(
126
- classmethod(valdict_keys("meta_parallel"))
127
- )
128
-
129
- @field_validator("args_non_parallel")
130
- @classmethod
131
- def validate_args_non_parallel(cls, value):
132
- if value is None:
133
- return
134
- valdict_keys("args_non_parallel")(cls, value)
135
- args_keys = set(value.keys())
136
- intersect_keys = RESERVED_ARGUMENTS.intersection(args_keys)
137
- if intersect_keys:
138
- raise ValueError(
139
- "`args` contains the following forbidden keys: "
140
- f"{intersect_keys}"
141
- )
142
- return value
143
-
144
- @field_validator("args_parallel")
145
- @classmethod
146
- def validate_args_parallel(cls, value):
147
- if value is None:
148
- return
149
- valdict_keys("args_parallel")(cls, value)
150
- args_keys = set(value.keys())
151
- intersect_keys = RESERVED_ARGUMENTS.intersection(args_keys)
152
- if intersect_keys:
153
- raise ValueError(
154
- "`args` contains the following forbidden keys: "
155
- f"{intersect_keys}"
156
- )
157
- return value
62
+ meta_non_parallel: Optional[DictStrAny] = None
63
+ meta_parallel: Optional[DictStrAny] = None
64
+ args_non_parallel: Optional[WorkflowTaskArgument] = None
65
+ args_parallel: Optional[WorkflowTaskArgument] = None
66
+ type_filters: TypeFilters = None
158
67
 
159
68
 
160
69
  class WorkflowTaskImportV2(BaseModel):
161
70
  model_config = ConfigDict(extra="forbid")
162
71
 
163
- meta_non_parallel: Optional[dict[str, Any]] = None
164
- meta_parallel: Optional[dict[str, Any]] = None
165
- args_non_parallel: Optional[dict[str, Any]] = None
166
- args_parallel: Optional[dict[str, Any]] = None
167
- type_filters: Optional[dict[str, bool]] = None
72
+ meta_non_parallel: Optional[DictStrAny] = None
73
+ meta_parallel: Optional[DictStrAny] = None
74
+ args_non_parallel: Optional[DictStrAny] = None
75
+ args_parallel: Optional[DictStrAny] = None
76
+ type_filters: Optional[TypeFilters] = None
168
77
  input_filters: Optional[dict[str, Any]] = None
169
78
 
170
79
  task: Union[TaskImportV2, TaskImportV2Legacy]
171
80
 
172
- # Validators
173
81
  @model_validator(mode="before")
174
82
  @classmethod
175
83
  def update_legacy_filters(cls, values: dict):
@@ -198,22 +106,6 @@ class WorkflowTaskImportV2(BaseModel):
198
106
 
199
107
  return values
200
108
 
201
- _type_filters = field_validator("type_filters")(
202
- classmethod(validate_type_filters)
203
- )
204
- _meta_non_parallel = field_validator("meta_non_parallel")(
205
- classmethod(valdict_keys("meta_non_parallel"))
206
- )
207
- _meta_parallel = field_validator("meta_parallel")(
208
- classmethod(valdict_keys("meta_parallel"))
209
- )
210
- _args_non_parallel = field_validator("args_non_parallel")(
211
- classmethod(valdict_keys("args_non_parallel"))
212
- )
213
- _args_parallel = field_validator("args_parallel")(
214
- classmethod(valdict_keys("args_parallel"))
215
- )
216
-
217
109
 
218
110
  class WorkflowTaskExportV2(BaseModel):
219
111
  meta_non_parallel: Optional[dict[str, Any]] = None
fractal_server/config.py CHANGED
@@ -34,6 +34,7 @@ from pydantic_settings import SettingsConfigDict
34
34
  from sqlalchemy.engine import URL
35
35
 
36
36
  import fractal_server
37
+ from fractal_server.types import AbsolutePathStr
37
38
 
38
39
 
39
40
  class MailSettings(BaseModel):
@@ -280,45 +281,27 @@ class Settings(BaseSettings):
280
281
  or a path relative to current working directory).
281
282
  """
282
283
 
283
- @field_validator("FRACTAL_TASKS_DIR")
284
- @classmethod
285
- def make_FRACTAL_TASKS_DIR_absolute(cls, v):
286
- """
287
- If `FRACTAL_TASKS_DIR` is a non-absolute path, make it absolute (based
288
- on the current working directory).
289
- """
290
- if v is None:
291
- return None
292
- FRACTAL_TASKS_DIR_path = Path(v)
293
- if not FRACTAL_TASKS_DIR_path.is_absolute():
294
- FRACTAL_TASKS_DIR_path = FRACTAL_TASKS_DIR_path.resolve()
295
- logging.warning(
296
- f'FRACTAL_TASKS_DIR="{v}" is not an absolute path; '
297
- f'converting it to "{str(FRACTAL_TASKS_DIR_path)}"'
298
- )
299
- return FRACTAL_TASKS_DIR_path
284
+ FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path] = None
285
+ """
286
+ Base directory for job files (either an absolute path or a path relative to
287
+ current working directory).
288
+ """
300
289
 
301
- @field_validator("FRACTAL_RUNNER_WORKING_BASE_DIR")
290
+ @field_validator(
291
+ "FRACTAL_TASKS_DIR",
292
+ "FRACTAL_RUNNER_WORKING_BASE_DIR",
293
+ mode="after",
294
+ )
302
295
  @classmethod
303
- def make_FRACTAL_RUNNER_WORKING_BASE_DIR_absolute(cls, v):
304
- """
305
- (Copy of make_FRACTAL_TASKS_DIR_absolute)
306
- If `FRACTAL_RUNNER_WORKING_BASE_DIR` is a non-absolute path,
307
- make it absolute (based on the current working directory).
308
- """
309
- if v is None:
310
- return None
311
- FRACTAL_RUNNER_WORKING_BASE_DIR_path = Path(v)
312
- if not FRACTAL_RUNNER_WORKING_BASE_DIR_path.is_absolute():
313
- FRACTAL_RUNNER_WORKING_BASE_DIR_path = (
314
- FRACTAL_RUNNER_WORKING_BASE_DIR_path.resolve()
315
- )
296
+ def make_paths_absolute(cls, path: Optional[Path]) -> Optional[Path]:
297
+ if path is None or path.is_absolute():
298
+ return path
299
+ else:
316
300
  logging.warning(
317
- f'FRACTAL_RUNNER_WORKING_BASE_DIR="{v}" is not an absolute '
318
- "path; converting it to "
319
- f'"{str(FRACTAL_RUNNER_WORKING_BASE_DIR_path)}"'
301
+ f"'{path}' is not an absolute path; "
302
+ f"converting it to '{path.resolve()}'"
320
303
  )
321
- return FRACTAL_RUNNER_WORKING_BASE_DIR_path
304
+ return path.resolve()
322
305
 
323
306
  FRACTAL_RUNNER_BACKEND: Literal[
324
307
  "local",
@@ -329,12 +312,6 @@ class Settings(BaseSettings):
329
312
  Select which runner backend to use.
330
313
  """
331
314
 
332
- FRACTAL_RUNNER_WORKING_BASE_DIR: Optional[Path] = None
333
- """
334
- Base directory for running jobs / workflows. All artifacts required to set
335
- up, run and tear down jobs are placed in subdirs of this directory.
336
- """
337
-
338
315
  FRACTAL_LOGGING_LEVEL: int = logging.INFO
339
316
  """
340
317
  Logging-level threshold for logging
@@ -363,27 +340,12 @@ class Settings(BaseSettings):
363
340
  Path of JSON file with configuration for the SLURM backend.
364
341
  """
365
342
 
366
- FRACTAL_SLURM_WORKER_PYTHON: Optional[str] = None
343
+ FRACTAL_SLURM_WORKER_PYTHON: Optional[AbsolutePathStr] = None
367
344
  """
368
345
  Absolute path to Python interpreter that will run the jobs on the SLURM
369
346
  nodes. If not specified, the same interpreter that runs the server is used.
370
347
  """
371
348
 
372
- @field_validator("FRACTAL_SLURM_WORKER_PYTHON")
373
- @classmethod
374
- def absolute_FRACTAL_SLURM_WORKER_PYTHON(cls, v):
375
- """
376
- If `FRACTAL_SLURM_WORKER_PYTHON` is a relative path, fail.
377
- """
378
- if v is None:
379
- return None
380
- elif not Path(v).is_absolute():
381
- raise FractalConfigurationError(
382
- f"Non-absolute value for FRACTAL_SLURM_WORKER_PYTHON={v}"
383
- )
384
- else:
385
- return v
386
-
387
349
  FRACTAL_TASKS_PYTHON_DEFAULT_VERSION: Optional[
388
350
  Literal["3.9", "3.10", "3.11", "3.12"]
389
351
  ] = None
@@ -507,27 +469,12 @@ class Settings(BaseSettings):
507
469
  `JobExecutionError`.
508
470
  """
509
471
 
510
- FRACTAL_PIP_CACHE_DIR: Optional[str] = None
472
+ FRACTAL_PIP_CACHE_DIR: Optional[AbsolutePathStr] = None
511
473
  """
512
474
  Absolute path to the cache directory for `pip`; if unset,
513
475
  `--no-cache-dir` is used.
514
476
  """
515
477
 
516
- @field_validator("FRACTAL_PIP_CACHE_DIR")
517
- @classmethod
518
- def absolute_FRACTAL_PIP_CACHE_DIR(cls, v):
519
- """
520
- If `FRACTAL_PIP_CACHE_DIR` is a relative path, fail.
521
- """
522
- if v is None:
523
- return None
524
- elif not Path(v).is_absolute():
525
- raise FractalConfigurationError(
526
- f"Non-absolute value for FRACTAL_PIP_CACHE_DIR={v}"
527
- )
528
- else:
529
- return v
530
-
531
478
  @property
532
479
  def PIP_CACHE_DIR_ARG(self) -> str:
533
480
  """
@@ -1,15 +1,14 @@
1
- from typing import Any
2
1
  from typing import Optional
3
- from typing import Union
4
2
 
5
3
  from pydantic import BaseModel
6
4
  from pydantic import Field
7
- from pydantic import field_validator
8
5
 
9
- from fractal_server.app.schemas._validators import valdict_keys
10
- from fractal_server.urls import normalize_url
11
-
12
- AttributeFiltersType = dict[str, list[Any]]
6
+ from fractal_server.types import DictStrAny
7
+ from fractal_server.types import ImageAttributes
8
+ from fractal_server.types import ImageAttributesWithNone
9
+ from fractal_server.types import ImageTypes
10
+ from fractal_server.types import ZarrDirStr
11
+ from fractal_server.types import ZarrUrlStr
13
12
 
14
13
 
15
14
  class _SingleImageBase(BaseModel):
@@ -23,28 +22,11 @@ class _SingleImageBase(BaseModel):
23
22
  types:
24
23
  """
25
24
 
26
- zarr_url: str
27
- origin: Optional[str] = None
28
-
29
- attributes: dict[str, Any] = Field(default_factory=dict)
30
- types: dict[str, bool] = Field(default_factory=dict)
31
-
32
- # Validators
33
- _attributes = field_validator("attributes")(
34
- classmethod(valdict_keys("attributes"))
35
- )
36
- _types = field_validator("types")(classmethod(valdict_keys("types")))
37
-
38
- @field_validator("zarr_url")
39
- @classmethod
40
- def normalize_zarr_url(cls, v: str) -> str:
41
- return normalize_url(v)
25
+ zarr_url: ZarrUrlStr
26
+ origin: Optional[ZarrDirStr] = None
42
27
 
43
- @field_validator("origin")
44
- @classmethod
45
- def normalize_orig(cls, v: Optional[str]) -> Optional[str]:
46
- if v is not None:
47
- return normalize_url(v)
28
+ attributes: DictStrAny = Field(default_factory=dict)
29
+ types: ImageTypes = Field(default_factory=dict)
48
30
 
49
31
 
50
32
  class SingleImageTaskOutput(_SingleImageBase):
@@ -52,19 +34,7 @@ class SingleImageTaskOutput(_SingleImageBase):
52
34
  `SingleImageBase`, with scalar `attributes` values (`None` included).
53
35
  """
54
36
 
55
- @field_validator("attributes")
56
- @classmethod
57
- def validate_attributes(
58
- cls, v: dict[str, Any]
59
- ) -> dict[str, Union[int, float, str, bool, None]]:
60
- for key, value in v.items():
61
- if not isinstance(value, (int, float, str, bool, type(None))):
62
- raise ValueError(
63
- f"SingleImageTaskOutput.attributes[{key}] must be a "
64
- "scalar (int, float, str or bool). "
65
- f"Given {value} ({type(value)})"
66
- )
67
- return v
37
+ attributes: ImageAttributesWithNone = Field(default_factory=dict)
68
38
 
69
39
 
70
40
  class SingleImage(_SingleImageBase):
@@ -72,46 +42,10 @@ class SingleImage(_SingleImageBase):
72
42
  `SingleImageBase`, with scalar `attributes` values (`None` excluded).
73
43
  """
74
44
 
75
- @field_validator("attributes")
76
- @classmethod
77
- def validate_attributes(
78
- cls, v: dict[str, Any]
79
- ) -> dict[str, Union[int, float, str, bool]]:
80
- for key, value in v.items():
81
- if not isinstance(value, (int, float, str, bool)):
82
- raise ValueError(
83
- f"SingleImage.attributes[{key}] must be a scalar "
84
- f"(int, float, str or bool). Given {value} ({type(value)})"
85
- )
86
- return v
45
+ attributes: ImageAttributes = Field(default_factory=dict)
87
46
 
88
47
 
89
48
  class SingleImageUpdate(BaseModel):
90
- zarr_url: str
91
- attributes: Optional[dict[str, Any]] = None
92
- types: Optional[dict[str, bool]] = None
93
-
94
- @field_validator("zarr_url")
95
- @classmethod
96
- def normalize_zarr_url(cls, v: str) -> str:
97
- return normalize_url(v)
98
-
99
- @field_validator("attributes")
100
- @classmethod
101
- def validate_attributes(
102
- cls, v: dict[str, Any]
103
- ) -> dict[str, Union[int, float, str, bool]]:
104
- if v is not None:
105
- # validate keys
106
- valdict_keys("attributes")(cls, v)
107
- # validate values
108
- for key, value in v.items():
109
- if not isinstance(value, (int, float, str, bool)):
110
- raise ValueError(
111
- f"SingleImageUpdate.attributes[{key}] must be a scalar"
112
- " (int, float, str or bool). "
113
- f"Given {value} ({type(value)})"
114
- )
115
- return v
116
-
117
- _types = field_validator("types")(classmethod(valdict_keys("types")))
49
+ zarr_url: ZarrUrlStr
50
+ attributes: ImageAttributes = None
51
+ types: Optional[ImageTypes] = None
@@ -4,7 +4,7 @@ from typing import Literal
4
4
  from typing import Optional
5
5
  from typing import Union
6
6
 
7
- from fractal_server.images.models import AttributeFiltersType
7
+ from fractal_server.types import AttributeFilters
8
8
 
9
9
  ImageSearch = dict[Literal["image", "index"], Union[int, dict[str, Any]]]
10
10
 
@@ -36,7 +36,7 @@ def match_filter(
36
36
  *,
37
37
  image: dict[str, Any],
38
38
  type_filters: dict[str, bool],
39
- attribute_filters: AttributeFiltersType,
39
+ attribute_filters: AttributeFilters,
40
40
  ) -> bool:
41
41
  """
42
42
  Find whether an image matches a filter set.
@@ -66,7 +66,7 @@ def match_filter(
66
66
  def filter_image_list(
67
67
  images: list[dict[str, Any]],
68
68
  type_filters: Optional[dict[str, bool]] = None,
69
- attribute_filters: Optional[AttributeFiltersType] = None,
69
+ attribute_filters: Optional[AttributeFilters] = None,
70
70
  ) -> list[dict[str, Any]]:
71
71
  """
72
72
  Compute a sublist with images that match a filter set.
@@ -56,6 +56,7 @@ def _acquire_lock_with_timeout(
56
56
  """
57
57
  logger = get_logger(logger_name)
58
58
  logger.info(f"Trying to acquire lock for '{label}', with {timeout=}")
59
+ t_start_lock_acquire = time.perf_counter()
59
60
  result = lock.acquire(timeout=timeout)
60
61
  try:
61
62
  if not result:
@@ -64,7 +65,9 @@ def _acquire_lock_with_timeout(
64
65
  f"Failed to acquire lock for '{label}' within "
65
66
  f"{timeout} seconds"
66
67
  )
67
- logger.info(f"Lock for '{label}' was acquired.")
68
+ t_end_lock_acquire = time.perf_counter()
69
+ elapsed = t_end_lock_acquire - t_start_lock_acquire
70
+ logger.info(f"Lock for '{label}' was acquired - {elapsed=:.4f} s")
68
71
  yield result
69
72
  finally:
70
73
  if result: