fractal-server 2.14.3a0__py3-none-any.whl → 2.14.4a0__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 (41) 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/types/__init__.py +87 -0
  29. fractal_server/types/validators/__init__.py +6 -0
  30. fractal_server/types/validators/_common_validators.py +42 -0
  31. fractal_server/types/validators/_filter_validators.py +24 -0
  32. fractal_server/types/validators/_workflow_task_arguments_validators.py +10 -0
  33. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4a0.dist-info}/METADATA +1 -1
  34. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4a0.dist-info}/RECORD +37 -35
  35. fractal_server/app/runner/compress_folder.py +0 -144
  36. fractal_server/app/runner/extract_archive.py +0 -99
  37. fractal_server/app/schemas/_filter_validators.py +0 -46
  38. fractal_server/app/schemas/_validators.py +0 -86
  39. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4a0.dist-info}/LICENSE +0 -0
  40. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4a0.dist-info}/WHEEL +0 -0
  41. {fractal_server-2.14.3a0.dist-info → fractal_server-2.14.4a0.dist-info}/entry_points.txt +0 -0
@@ -5,40 +5,23 @@ from pydantic import BaseModel
5
5
  from pydantic import ConfigDict
6
6
  from pydantic import Field
7
7
  from pydantic import field_serializer
8
- from pydantic import field_validator
9
- from pydantic import model_validator
10
8
  from pydantic.types import AwareDatetime
11
9
 
12
- from .._validators import cant_set_none
13
- from .._validators import NonEmptyString
14
- from .._validators import root_validate_dict_keys
15
- from .project import ProjectReadV2
10
+ from fractal_server.app.schemas.v2.project import ProjectReadV2
16
11
  from fractal_server.images import SingleImage
17
- from fractal_server.images.models import AttributeFiltersType
18
- from fractal_server.urls import normalize_url
12
+ from fractal_server.types import AttributeFilters
13
+ from fractal_server.types import NonEmptyStr
14
+ from fractal_server.types import ZarrDirStr
19
15
 
20
16
 
21
17
  class DatasetCreateV2(BaseModel):
22
18
  model_config = ConfigDict(extra="forbid")
23
19
 
24
- name: NonEmptyString
20
+ name: NonEmptyStr
25
21
 
26
- zarr_dir: Optional[str] = None
22
+ zarr_dir: Optional[ZarrDirStr] = None
27
23
 
28
- attribute_filters: AttributeFiltersType = Field(default_factory=dict)
29
-
30
- # Validators
31
-
32
- _dict_keys = model_validator(mode="before")(
33
- classmethod(root_validate_dict_keys)
34
- )
35
-
36
- @field_validator("zarr_dir")
37
- @classmethod
38
- def normalize_zarr_dir(cls, v: Optional[str]) -> Optional[str]:
39
- if v is not None:
40
- return normalize_url(v)
41
- return v
24
+ attribute_filters: AttributeFilters = Field(default_factory=dict)
42
25
 
43
26
 
44
27
  class DatasetReadV2(BaseModel):
@@ -60,26 +43,8 @@ class DatasetReadV2(BaseModel):
60
43
  class DatasetUpdateV2(BaseModel):
61
44
  model_config = ConfigDict(extra="forbid")
62
45
 
63
- name: Optional[NonEmptyString] = None
64
- zarr_dir: Optional[str] = None
65
-
66
- # Validators
67
-
68
- _dict_keys = model_validator(mode="before")(
69
- classmethod(root_validate_dict_keys)
70
- )
71
-
72
- @field_validator("name")
73
- @classmethod
74
- def _cant_set_none(cls, v):
75
- return cant_set_none(v)
76
-
77
- @field_validator("zarr_dir")
78
- @classmethod
79
- def normalize_zarr_dir(cls, v: Optional[str]) -> Optional[str]:
80
- if v is not None:
81
- return normalize_url(v)
82
- return v
46
+ name: NonEmptyStr = None
47
+ zarr_dir: Optional[ZarrDirStr] = None
83
48
 
84
49
 
85
50
  class DatasetImportV2(BaseModel):
@@ -97,14 +62,9 @@ class DatasetImportV2(BaseModel):
97
62
  """
98
63
 
99
64
  name: str
100
- zarr_dir: str
65
+ zarr_dir: ZarrDirStr
101
66
  images: list[SingleImage] = Field(default_factory=list)
102
67
 
103
- @field_validator("zarr_dir")
104
- @classmethod
105
- def normalize_zarr_dir(cls, v: str) -> str:
106
- return normalize_url(v)
107
-
108
68
 
109
69
  class DatasetExportV2(BaseModel):
110
70
  """
@@ -6,21 +6,17 @@ 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 import model_validator
11
- from pydantic import ValidationInfo
12
10
  from pydantic.types import AwareDatetime
11
+ from pydantic.types import NonNegativeInt
13
12
  from pydantic.types import StrictStr
14
13
 
15
- from .._filter_validators import validate_attribute_filters
16
- from .._filter_validators import validate_type_filters
17
- from .._validators import cant_set_none
18
- from .._validators import NonEmptyString
19
- from .._validators import root_validate_dict_keys
20
- from .dumps import DatasetDumpV2
21
- from .dumps import ProjectDumpV2
22
- from .dumps import WorkflowDumpV2
23
- from fractal_server.images.models import AttributeFiltersType
14
+ from fractal_server.app.schemas.v2.dumps import DatasetDumpV2
15
+ from fractal_server.app.schemas.v2.dumps import ProjectDumpV2
16
+ from fractal_server.app.schemas.v2.dumps import WorkflowDumpV2
17
+ from fractal_server.types import AttributeFilters
18
+ from fractal_server.types import NonEmptyStr
19
+ from fractal_server.types import TypeFilters
24
20
 
25
21
 
26
22
  class JobStatusTypeV2(str, Enum):
@@ -47,64 +43,27 @@ class JobStatusTypeV2(str, Enum):
47
43
  class JobCreateV2(BaseModel):
48
44
  model_config = ConfigDict(extra="forbid")
49
45
 
50
- first_task_index: Optional[int] = None
51
- last_task_index: Optional[int] = None
46
+ first_task_index: Optional[NonNegativeInt] = None
47
+ last_task_index: Optional[NonNegativeInt] = None
52
48
  slurm_account: Optional[StrictStr] = None
53
- worker_init: Optional[NonEmptyString] = None
54
-
55
- attribute_filters: AttributeFiltersType = Field(default_factory=dict)
56
- type_filters: dict[str, bool] = Field(default_factory=dict)
49
+ worker_init: NonEmptyStr = None
57
50
 
58
- # Validators
51
+ attribute_filters: AttributeFilters = Field(default_factory=dict)
52
+ type_filters: TypeFilters = Field(default_factory=dict)
59
53
 
60
- @field_validator("worker_init")
54
+ @model_validator(mode="before")
61
55
  @classmethod
62
- def _cant_set_none(cls, v):
63
- return cant_set_none(v)
64
-
65
- _dict_keys = model_validator(mode="before")(
66
- classmethod(root_validate_dict_keys)
67
- )
68
- _attribute_filters = field_validator("attribute_filters")(
69
- classmethod(validate_attribute_filters)
70
- )
71
- _type_filters = field_validator("type_filters")(
72
- classmethod(validate_type_filters)
73
- )
74
-
75
- @field_validator("first_task_index")
76
- @classmethod
77
- def first_task_index_non_negative(cls, v):
78
- """
79
- Check that `first_task_index` is non-negative.
80
- """
81
- if v is not None and v < 0:
82
- raise ValueError(
83
- f"first_task_index cannot be negative (given: {v})"
84
- )
85
- return v
86
-
87
- @field_validator("last_task_index")
88
- @classmethod
89
- def first_last_task_indices(cls, v, info: ValidationInfo):
90
- """
91
- Check that `last_task_index` is non-negative, and that it is not
92
- smaller than `first_task_index`.
93
- """
94
- if v is not None and v < 0:
95
- raise ValueError(
96
- f"last_task_index cannot be negative (given: {v})"
97
- )
98
-
99
- first_task_index = info.data.get("first_task_index")
100
- last_task_index = v
56
+ def validate_first_last_indices(cls, values):
57
+ first_task_index = values.get("first_task_index")
58
+ last_task_index = values.get("last_task_index")
59
+
101
60
  if first_task_index is not None and last_task_index is not None:
102
61
  if first_task_index > last_task_index:
103
62
  raise ValueError(
104
63
  f"{first_task_index=} cannot be larger than "
105
64
  f"{last_task_index=}"
106
65
  )
107
- return v
66
+ return values
108
67
 
109
68
 
110
69
  class JobReadV2(BaseModel):
@@ -126,7 +85,7 @@ class JobReadV2(BaseModel):
126
85
  first_task_index: Optional[int] = None
127
86
  last_task_index: Optional[int] = None
128
87
  worker_init: Optional[str] = None
129
- attribute_filters: AttributeFiltersType
88
+ attribute_filters: AttributeFilters
130
89
  type_filters: dict[str, bool]
131
90
 
132
91
  @field_serializer("start_timestamp")
@@ -1,14 +1,13 @@
1
- from typing import Any
2
1
  from typing import Literal
3
2
  from typing import Optional
4
3
 
5
4
  from pydantic import BaseModel
6
5
  from pydantic import Field
7
- from pydantic import field_validator
8
- from pydantic import HttpUrl
9
6
  from pydantic import model_validator
10
7
 
11
- from .._validators import NonEmptyString
8
+ from fractal_server.types import DictStrAny
9
+ from fractal_server.types import HttpUrlStr
10
+ from fractal_server.types import NonEmptyStr
12
11
 
13
12
 
14
13
  class TaskManifestV2(BaseModel):
@@ -46,12 +45,12 @@ class TaskManifestV2(BaseModel):
46
45
  executable_parallel: Optional[str] = None
47
46
  input_types: dict[str, bool] = Field(default_factory=dict)
48
47
  output_types: dict[str, bool] = Field(default_factory=dict)
49
- meta_non_parallel: dict[str, Any] = Field(default_factory=dict)
50
- meta_parallel: dict[str, Any] = Field(default_factory=dict)
51
- args_schema_non_parallel: Optional[dict[str, Any]] = None
52
- args_schema_parallel: Optional[dict[str, Any]] = None
48
+ meta_non_parallel: DictStrAny = Field(default_factory=dict)
49
+ meta_parallel: DictStrAny = Field(default_factory=dict)
50
+ args_schema_non_parallel: Optional[DictStrAny] = None
51
+ args_schema_parallel: Optional[DictStrAny] = None
53
52
  docs_info: Optional[str] = None
54
- docs_link: Optional[str] = None
53
+ docs_link: Optional[HttpUrlStr] = None
55
54
 
56
55
  category: Optional[str] = None
57
56
  modality: Optional[str] = None
@@ -113,13 +112,6 @@ class TaskManifestV2(BaseModel):
113
112
 
114
113
  return self
115
114
 
116
- @field_validator("docs_link", mode="after")
117
- @classmethod
118
- def validate_docs_link(cls, value):
119
- if value is not None:
120
- HttpUrl(value)
121
- return value
122
-
123
115
 
124
116
  class ManifestV2(BaseModel):
125
117
  """
@@ -145,11 +137,11 @@ class ManifestV2(BaseModel):
145
137
  Label of how `args_schema`s were generated (e.g. `pydantic_v1`).
146
138
  """
147
139
 
148
- manifest_version: str
140
+ manifest_version: Literal["2"]
149
141
  task_list: list[TaskManifestV2]
150
142
  has_args_schemas: bool = False
151
143
  args_schema_version: Optional[str] = None
152
- authors: Optional[NonEmptyString] = None
144
+ authors: Optional[NonEmptyStr] = None
153
145
 
154
146
  @model_validator(mode="after")
155
147
  def _check_args_schemas_are_present(self):
@@ -185,10 +177,3 @@ class ManifestV2(BaseModel):
185
177
  )
186
178
  )
187
179
  return self
188
-
189
- @field_validator("manifest_version")
190
- @classmethod
191
- def manifest_version_2(cls, value):
192
- if value != "2":
193
- raise ValueError(f"Wrong manifest version (given {value})")
194
- return value
@@ -1,21 +1,18 @@
1
1
  from datetime import datetime
2
- from typing import Optional
3
2
 
4
3
  from pydantic import BaseModel
5
4
  from pydantic import ConfigDict
6
5
  from pydantic import field_serializer
7
- from pydantic import field_validator
8
6
  from pydantic.types import AwareDatetime
9
7
 
10
- from .._validators import cant_set_none
11
- from .._validators import NonEmptyString
8
+ from fractal_server.types import NonEmptyStr
12
9
 
13
10
 
14
11
  class ProjectCreateV2(BaseModel):
15
12
 
16
13
  model_config = ConfigDict(extra="forbid")
17
14
 
18
- name: NonEmptyString
15
+ name: NonEmptyStr
19
16
 
20
17
 
21
18
  class ProjectReadV2(BaseModel):
@@ -33,9 +30,4 @@ class ProjectUpdateV2(BaseModel):
33
30
 
34
31
  model_config = ConfigDict(extra="forbid")
35
32
 
36
- name: Optional[NonEmptyString] = None
37
-
38
- @field_validator("name")
39
- @classmethod
40
- def _cant_set_none(cls, v):
41
- return cant_set_none(v)
33
+ name: NonEmptyStr = None
@@ -5,16 +5,15 @@ from typing import Optional
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
- from pydantic import HttpUrl
10
8
  from pydantic import model_validator
11
9
 
12
- from .._validators import cant_set_none
13
- from fractal_server.app.schemas._validators import NonEmptyString
14
- from fractal_server.app.schemas._validators import val_unique_list
15
- from fractal_server.app.schemas._validators import valdict_keys
16
10
  from fractal_server.logger import set_logger
17
11
  from fractal_server.string_tools import validate_cmd
12
+ from fractal_server.types import DictStrAny
13
+ from fractal_server.types import HttpUrlStr
14
+ from fractal_server.types import ListUniqueNonEmptyString
15
+ from fractal_server.types import NonEmptyStr
16
+ from fractal_server.types import TypeFilters
18
17
 
19
18
  TaskTypeType = Literal[
20
19
  "compound",
@@ -31,42 +30,30 @@ logger = set_logger(__name__)
31
30
  class TaskCreateV2(BaseModel):
32
31
  model_config = ConfigDict(extra="forbid")
33
32
 
34
- name: NonEmptyString
33
+ name: NonEmptyStr
35
34
 
36
- command_non_parallel: Optional[NonEmptyString] = None
37
- command_parallel: Optional[NonEmptyString] = None
35
+ command_non_parallel: NonEmptyStr = None
36
+ command_parallel: NonEmptyStr = None
38
37
 
39
- meta_non_parallel: Optional[dict[str, Any]] = None
40
- meta_parallel: Optional[dict[str, Any]] = None
41
- version: Optional[NonEmptyString] = None
42
- args_schema_non_parallel: Optional[dict[str, Any]] = None
43
- args_schema_parallel: Optional[dict[str, Any]] = None
44
- args_schema_version: Optional[NonEmptyString] = None
38
+ meta_non_parallel: Optional[DictStrAny] = None
39
+ meta_parallel: Optional[DictStrAny] = None
40
+ version: NonEmptyStr = None
41
+ args_schema_non_parallel: Optional[DictStrAny] = None
42
+ args_schema_parallel: Optional[DictStrAny] = None
43
+ args_schema_version: NonEmptyStr = None
45
44
  docs_info: Optional[str] = None
46
- docs_link: Optional[str] = None
45
+ docs_link: Optional[HttpUrlStr] = None
47
46
 
48
- input_types: dict[str, bool] = Field(default={})
49
- output_types: dict[str, bool] = Field(default={})
47
+ input_types: TypeFilters = Field(default={})
48
+ output_types: TypeFilters = Field(default={})
50
49
 
51
- category: Optional[NonEmptyString] = None
52
- modality: Optional[NonEmptyString] = None
53
- tags: list[NonEmptyString] = Field(default_factory=list)
54
- authors: Optional[NonEmptyString] = None
50
+ category: Optional[NonEmptyStr] = None
51
+ modality: Optional[NonEmptyStr] = None
52
+ tags: ListUniqueNonEmptyString = Field(default_factory=list)
53
+ authors: Optional[NonEmptyStr] = None
55
54
 
56
55
  type: Optional[TaskTypeType] = None
57
56
 
58
- # Validators
59
-
60
- @field_validator(
61
- "command_non_parallel",
62
- "command_parallel",
63
- "version",
64
- "args_schema_version",
65
- )
66
- @classmethod
67
- def _cant_set_none(cls, v):
68
- return cant_set_none(v)
69
-
70
57
  @model_validator(mode="after")
71
58
  def validate_commands(self):
72
59
  command_parallel = self.command_parallel
@@ -100,37 +87,6 @@ class TaskCreateV2(BaseModel):
100
87
 
101
88
  return self
102
89
 
103
- _meta_non_parallel = field_validator("meta_non_parallel")(
104
- classmethod(valdict_keys("meta_non_parallel"))
105
- )
106
- _meta_parallel = field_validator("meta_parallel")(
107
- classmethod(valdict_keys("meta_parallel"))
108
- )
109
- _args_schema_non_parallel = field_validator("args_schema_non_parallel")(
110
- classmethod(valdict_keys("args_schema_non_parallel"))
111
- )
112
- _args_schema_parallel = field_validator("args_schema_parallel")(
113
- classmethod(valdict_keys("args_schema_parallel"))
114
- )
115
- _input_types = field_validator("input_types")(
116
- classmethod(valdict_keys("input_types"))
117
- )
118
- _output_types = field_validator("output_types")(
119
- classmethod(valdict_keys("output_types"))
120
- )
121
-
122
- @field_validator("tags")
123
- @classmethod
124
- def validate_list_of_strings(cls, value):
125
- return val_unique_list("tags")(cls, value)
126
-
127
- @field_validator("docs_link", mode="after")
128
- @classmethod
129
- def validate_docs_link(cls, value):
130
- if value is not None:
131
- HttpUrl(value)
132
- return value
133
-
134
90
 
135
91
  class TaskReadV2(BaseModel):
136
92
  id: int
@@ -162,56 +118,30 @@ class TaskReadV2(BaseModel):
162
118
  class TaskUpdateV2(BaseModel):
163
119
  model_config = ConfigDict(extra="forbid")
164
120
 
165
- command_parallel: Optional[NonEmptyString] = None
166
- command_non_parallel: Optional[NonEmptyString] = None
167
- input_types: Optional[dict[str, bool]] = None
168
- output_types: Optional[dict[str, bool]] = None
169
-
170
- category: Optional[NonEmptyString] = None
171
- modality: Optional[NonEmptyString] = None
172
- authors: Optional[NonEmptyString] = None
173
- tags: Optional[list[NonEmptyString]] = None
174
-
175
- # Validators
176
-
177
- @field_validator("command_parallel", "command_non_parallel")
178
- @classmethod
179
- def _cant_set_none(cls, v):
180
- return cant_set_none(v)
181
-
182
- @field_validator("input_types", "output_types")
183
- @classmethod
184
- def val_is_dict(cls, v):
185
- if not isinstance(v, dict):
186
- raise ValueError
187
- return v
188
-
189
- _input_types = field_validator("input_types")(
190
- classmethod(valdict_keys("input_types"))
191
- )
192
- _output_types = field_validator("output_types")(
193
- classmethod(valdict_keys("output_types"))
194
- )
121
+ command_parallel: NonEmptyStr = None
122
+ command_non_parallel: NonEmptyStr = None
123
+ input_types: TypeFilters = None
124
+ output_types: TypeFilters = None
195
125
 
196
- @field_validator("tags")
197
- @classmethod
198
- def validate_tags(cls, value):
199
- return val_unique_list("tags")(cls, value)
126
+ category: Optional[NonEmptyStr] = None
127
+ modality: Optional[NonEmptyStr] = None
128
+ authors: Optional[NonEmptyStr] = None
129
+ tags: Optional[ListUniqueNonEmptyString] = None
200
130
 
201
131
 
202
132
  class TaskImportV2(BaseModel):
203
133
  model_config = ConfigDict(extra="forbid")
204
134
 
205
- pkg_name: NonEmptyString
206
- version: Optional[NonEmptyString] = None
207
- name: NonEmptyString
135
+ pkg_name: NonEmptyStr
136
+ version: Optional[NonEmptyStr] = None
137
+ name: NonEmptyStr
208
138
 
209
139
 
210
140
  class TaskImportV2Legacy(BaseModel):
211
- source: NonEmptyString
141
+ source: NonEmptyStr
212
142
 
213
143
 
214
144
  class TaskExportV2(BaseModel):
215
- pkg_name: NonEmptyString
216
- version: Optional[NonEmptyString] = None
217
- name: NonEmptyString
145
+ pkg_name: NonEmptyStr
146
+ version: Optional[NonEmptyStr] = None
147
+ name: NonEmptyStr
@@ -1,4 +1,3 @@
1
- from pathlib import Path
2
1
  from typing import Literal
3
2
  from typing import Optional
4
3
 
@@ -7,9 +6,11 @@ from pydantic import ConfigDict
7
6
  from pydantic import field_validator
8
7
  from pydantic import model_validator
9
8
 
10
- from .._validators import NonEmptyString
11
9
  from fractal_server.app.schemas.v2 import ManifestV2
12
10
  from fractal_server.string_tools import validate_cmd
11
+ from fractal_server.types import AbsolutePathStr
12
+ from fractal_server.types import DictStrStr
13
+ from fractal_server.types import NonEmptyStr
13
14
 
14
15
 
15
16
  class WheelFile(BaseModel):
@@ -46,57 +47,28 @@ class TaskCollectPipV2(BaseModel):
46
47
  """
47
48
 
48
49
  model_config = ConfigDict(extra="forbid")
49
- package: Optional[NonEmptyString] = None
50
- package_version: Optional[NonEmptyString] = None
51
- package_extras: Optional[NonEmptyString] = None
50
+ package: Optional[NonEmptyStr] = None
51
+ package_version: Optional[NonEmptyStr] = None
52
+ package_extras: Optional[NonEmptyStr] = None
52
53
  python_version: Optional[Literal["3.9", "3.10", "3.11", "3.12"]] = None
53
- pinned_package_versions: Optional[dict[str, str]] = None
54
+ pinned_package_versions: Optional[DictStrStr] = None
54
55
 
55
- @field_validator("package")
56
+ @field_validator(
57
+ "package", "package_version", "package_extras", mode="after"
58
+ )
56
59
  @classmethod
57
- def package_validator(cls, value: Optional[str]) -> Optional[str]:
58
- if value is None:
59
- return value
60
- validate_cmd(value, attribute_name="package")
61
- return value
62
-
63
- @field_validator("package_version")
64
- @classmethod
65
- def package_version_validator(cls, value: Optional[str]) -> Optional[str]:
66
- if value is None:
67
- return value
68
- validate_cmd(value, attribute_name="package_version")
69
- return value
70
-
71
- @field_validator("pinned_package_versions")
72
- @classmethod
73
- def pinned_package_versions_validator(cls, value):
74
- if value is None:
75
- return value
76
-
77
- old_keys = list(value.keys())
78
- new_keys = [key.strip() for key in old_keys]
79
- if any(k == "" for k in new_keys):
80
- raise ValueError(f"Empty string in {new_keys}.")
81
- if len(new_keys) != len(set(new_keys)):
82
- raise ValueError(
83
- f"Dictionary contains multiple identical keys: {value}."
84
- )
85
- for old_key, new_key in zip(old_keys, new_keys):
86
- if new_key != old_key:
87
- value[new_key] = value.pop(old_key)
88
-
89
- for pkg, version in value.items():
90
- validate_cmd(pkg)
91
- validate_cmd(version)
60
+ def validate_commands(cls, value):
61
+ if value is not None:
62
+ validate_cmd(value)
92
63
  return value
93
64
 
94
- @field_validator("package_extras")
65
+ @field_validator("pinned_package_versions", mode="after")
95
66
  @classmethod
96
- def package_extras_validator(cls, value: Optional[str]) -> Optional[str]:
97
- if value is None:
98
- return value
99
- validate_cmd(value, attribute_name="package_extras")
67
+ def validate_pinned_package_versions(cls, value):
68
+ if value is not None:
69
+ for pkg, version in value.items():
70
+ validate_cmd(pkg)
71
+ validate_cmd(version)
100
72
  return value
101
73
 
102
74
 
@@ -120,11 +92,18 @@ class TaskCollectCustomV2(BaseModel):
120
92
 
121
93
  model_config = ConfigDict(extra="forbid")
122
94
  manifest: ManifestV2
123
- python_interpreter: NonEmptyString
124
- label: NonEmptyString
125
- package_root: Optional[NonEmptyString] = None
126
- package_name: Optional[NonEmptyString] = None
127
- version: Optional[NonEmptyString] = None
95
+ python_interpreter: AbsolutePathStr
96
+ label: NonEmptyStr
97
+ package_root: Optional[AbsolutePathStr] = None
98
+ package_name: Optional[NonEmptyStr] = None
99
+ version: Optional[NonEmptyStr] = None
100
+
101
+ @field_validator("package_name", mode="after")
102
+ @classmethod
103
+ def validate_package_name(cls, value):
104
+ if value is not None:
105
+ validate_cmd(value)
106
+ return value
128
107
 
129
108
  @model_validator(mode="before")
130
109
  @classmethod
@@ -139,32 +118,3 @@ class TaskCollectCustomV2(BaseModel):
139
118
  "'package_root' and 'package_name'"
140
119
  )
141
120
  return values
142
-
143
- @field_validator("package_name")
144
- @classmethod
145
- def package_name_validator(cls, value: str):
146
- """
147
- Remove all whitespace characters, then check for invalid code.
148
- """
149
- if value is not None:
150
- validate_cmd(value)
151
- value = value.replace(" ", "")
152
- return value
153
-
154
- @field_validator("package_root")
155
- @classmethod
156
- def package_root_validator(cls, value):
157
- if (value is not None) and (not Path(value).is_absolute()):
158
- raise ValueError(
159
- f"'package_root' must be an absolute path: (given {value})."
160
- )
161
- return value
162
-
163
- @field_validator("python_interpreter")
164
- @classmethod
165
- def python_interpreter_validator(cls, value):
166
- if not Path(value).is_absolute():
167
- raise ValueError(
168
- f"Python interpreter path must be absolute: (given {value})."
169
- )
170
- return value