fractal-task-tools 0.0.8__py3-none-any.whl → 0.0.10__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.

Potentially problematic release.


This version of fractal-task-tools might be problematic. Click here for more details.

@@ -2,4 +2,4 @@ import logging
2
2
 
3
3
  logging.basicConfig(level=logging.INFO)
4
4
 
5
- __VERSION__ = "0.0.8"
5
+ __VERSION__ = "0.0.10"
@@ -7,9 +7,10 @@ from typing import Any
7
7
 
8
8
  from ._args_schemas import create_schema_for_single_task
9
9
  from ._package_name_tools import normalize_package_name
10
+ from ._task_arguments import validate_arguments
10
11
  from ._task_docs import create_docs_info
11
12
  from ._task_docs import read_docs_info_from_file
12
-
13
+ from .task_models import _BaseTask
13
14
 
14
15
  ARGS_SCHEMA_VERSION = "pydantic_v2"
15
16
  MANIFEST_FILENAME = "__FRACTAL_MANIFEST__.json"
@@ -60,7 +61,7 @@ def create_manifest(
60
61
  task_list_module = import_module(f"{package_name}.{task_list_path}")
61
62
 
62
63
  # Load TASK_LIST
63
- TASK_LIST = getattr(task_list_module, "TASK_LIST")
64
+ TASK_LIST: list[_BaseTask] = getattr(task_list_module, "TASK_LIST")
64
65
 
65
66
  # Load INPUT_MODELS
66
67
  try:
@@ -89,9 +90,15 @@ def create_manifest(
89
90
  for task_obj in TASK_LIST:
90
91
  # Convert Pydantic object to dictionary
91
92
  task_dict = task_obj.model_dump(
92
- exclude={"meta_init", "executable_init", "meta", "executable"},
93
+ exclude={
94
+ "meta_init",
95
+ "executable_init",
96
+ "meta",
97
+ "executable",
98
+ },
93
99
  exclude_unset=True,
94
100
  )
101
+ task_dict["type"] = task_obj.type
95
102
 
96
103
  # Copy some properties from `task_obj` to `task_dict`
97
104
  if task_obj.executable_non_parallel is not None:
@@ -115,6 +122,13 @@ def create_manifest(
115
122
  package=package_name,
116
123
  pydantic_models=INPUT_MODELS,
117
124
  )
125
+
126
+ validate_arguments(
127
+ task_type=task_obj.type,
128
+ schema=schema,
129
+ executable_kind=kind,
130
+ )
131
+
118
132
  logging.info(f"[{executable}] END (new schema)")
119
133
  task_dict[f"args_schema_{kind}"] = schema
120
134
 
@@ -17,7 +17,7 @@ def deepdiff(
17
17
  if type(old_object) is not type(new_object):
18
18
  raise ValueError(
19
19
  f"[{path}] Type difference:\n"
20
- f"\tOld: {type(old_object)}\n\tNew:{type(new_object)}"
20
+ f"\tOld: {type(old_object)}\n\tNew: {type(new_object)}"
21
21
  )
22
22
 
23
23
  if type(old_object) not in [list, dict, str, int, float, bool, type(None)]:
@@ -64,5 +64,5 @@ def deepdiff(
64
64
  if old_object != new_object:
65
65
  raise ValueError(
66
66
  f"{path} Values are different:\n"
67
- f"\tOld: '{old_object}'\n\tNew:'{new_object}'"
67
+ f"\tOld: '{old_object}'\n\tNew: '{new_object}'"
68
68
  )
@@ -122,24 +122,18 @@ def _get_function_args_descriptions(
122
122
  return descriptions
123
123
 
124
124
 
125
- def _get_class_attrs_descriptions(
126
- package_name: str, module_relative_path: str, class_name: str
125
+ def _get_class_attrs_descriptions_from_file(
126
+ *,
127
+ module_path: Path,
128
+ class_name: str,
127
129
  ) -> dict[str, str]:
128
130
  """
129
- Extract attribute descriptions from a class.
131
+ Extract class-attribute descriptions from a Python script
130
132
 
131
133
  Args:
132
- package_name: Example `fractal_tasks_core`.
133
- module_relative_path: Example `lib_channels.py`.
134
+ module_path: Example `/something/my_class.py`.
134
135
  class_name: Example `OmeroChannel`.
135
136
  """
136
-
137
- if not module_relative_path.endswith(".py"):
138
- raise ValueError(f"Module {module_relative_path} must end with '.py'")
139
-
140
- # Get the class ast.ClassDef object
141
- package_path = Path(import_module(package_name).__file__).parent
142
- module_path = package_path / module_relative_path
143
137
  tree = ast.parse(module_path.read_text())
144
138
  try:
145
139
  _class = next(
@@ -148,10 +142,7 @@ def _get_class_attrs_descriptions(
148
142
  if (isinstance(c, ast.ClassDef) and c.name == class_name)
149
143
  )
150
144
  except StopIteration:
151
- raise RuntimeError(
152
- f"Cannot find {class_name=} for {package_name=} "
153
- f"and {module_relative_path=}"
154
- )
145
+ raise RuntimeError(f"Cannot find {class_name=} in {module_path}.")
155
146
  docstring = ast.get_docstring(_class)
156
147
  parsed_docstring = docparse(docstring)
157
148
  descriptions = {
@@ -160,6 +151,31 @@ def _get_class_attrs_descriptions(
160
151
  else "Missing description"
161
152
  for x in parsed_docstring.params
162
153
  }
154
+ return descriptions
155
+
156
+
157
+ def _get_class_attrs_descriptions(
158
+ package_name: str, module_relative_path: str, class_name: str
159
+ ) -> dict[str, str]:
160
+ """
161
+ Extract class-attribute descriptions from an imported module
162
+
163
+ Args:
164
+ package_name: Example `fractal_tasks_core`.
165
+ module_relative_path: Example `lib_channels.py`.
166
+ class_name: Example `OmeroChannel`.
167
+ """
168
+
169
+ if not module_relative_path.endswith(".py"):
170
+ raise ValueError(f"Module {module_relative_path} must end with '.py'")
171
+
172
+ # Get the class ast.ClassDef object
173
+ package_path = Path(import_module(package_name).__file__).parent
174
+ module_path = package_path / module_relative_path
175
+ descriptions = _get_class_attrs_descriptions_from_file(
176
+ module_path=module_path,
177
+ class_name=class_name,
178
+ )
163
179
  logging.info(f"[_get_class_attrs_descriptions] END ({class_name=})")
164
180
  return descriptions
165
181
 
@@ -12,7 +12,7 @@ def normalize_package_name(pkg_name: str) -> str:
12
12
  imported-module name.
13
13
 
14
14
  Args:
15
- name: The non-normalized package name.
15
+ pkg_name: The non-normalized package name.
16
16
 
17
17
  Returns:
18
18
  The normalized package name.
@@ -0,0 +1,75 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Literal
4
+
5
+
6
+ REQUIRED_ARGUMENTS: dict[tuple[str, str], set[str]] = {
7
+ ("non_parallel", "non_parallel"): {"zarr_urls", "zarr_dir"},
8
+ ("compound", "non_parallel"): {"zarr_urls", "zarr_dir"},
9
+ ("parallel", "parallel"): {"zarr_url"},
10
+ ("compound", "parallel"): {"zarr_url", "init_args"},
11
+ ("converter_non_parallel", "non_parallel"): {"zarr_dir"},
12
+ ("converter_compound", "non_parallel"): {"zarr_dir"},
13
+ ("converter_compound", "parallel"): {"zarr_url", "init_args"},
14
+ }
15
+ FORBIDDEN_ARGUMENTS: dict[tuple[str, str], set[str]] = {
16
+ ("non_parallel", "non_parallel"): {"zarr_url"},
17
+ ("compound", "non_parallel"): {"zarr_url"},
18
+ ("parallel", "parallel"): {"zarr_urls", "zarr_dir"},
19
+ ("compound", "parallel"): {"zarr_urls", "zarr_dir"},
20
+ ("converter_non_parallel", "non_parallel"): {"zarr_url", "zarr_urls"},
21
+ ("converter_compound", "non_parallel"): {"zarr_url", "zarr_urls"},
22
+ ("converter_compound", "parallel"): {"zarr_urls", "zarr_dir"},
23
+ }
24
+
25
+
26
+ def validate_arguments(
27
+ *,
28
+ task_type: Literal["parallel", "non_parallel", "compound"],
29
+ executable_kind: Literal["parallel", "non_parallel"],
30
+ schema: dict[str, Any],
31
+ ) -> None:
32
+ """
33
+ Validate schema arguments against required/forbidden ones.
34
+
35
+ Arguments:
36
+ task_type:
37
+ executable_kind: The `parallel`/`non_parallel` part of the task.
38
+ schema:
39
+ """
40
+
41
+ key = (task_type, executable_kind)
42
+ if not (key in REQUIRED_ARGUMENTS and key in FORBIDDEN_ARGUMENTS):
43
+ logging.error(f"Invalid {task_type=}, {executable_kind=}.")
44
+ raise ValueError(f"Invalid {task_type=}, {executable_kind=}.")
45
+
46
+ required_args = REQUIRED_ARGUMENTS[key]
47
+ forbidden_args = FORBIDDEN_ARGUMENTS[key]
48
+
49
+ schema_properties = set(schema["properties"].keys())
50
+
51
+ logging.info(
52
+ f"[validate_arguments] Task has arguments: {schema_properties}"
53
+ )
54
+ logging.info(f"[validate_arguments] Required arguments: {required_args}")
55
+ logging.info(f"[validate_arguments] Forbidden arguments: {forbidden_args}")
56
+
57
+ missing_required_arguments = {
58
+ arg for arg in required_args if arg not in schema_properties
59
+ }
60
+ if missing_required_arguments:
61
+ error_msg = (
62
+ "[validate_arguments] Required arguments "
63
+ f"{missing_required_arguments} are missing."
64
+ )
65
+ logging.error(error_msg)
66
+ raise ValueError(error_msg)
67
+
68
+ present_forbidden_args = forbidden_args.intersection(schema_properties)
69
+ if present_forbidden_args:
70
+ error_msg = (
71
+ "[validate_arguments] Forbidden arguments "
72
+ f"{present_forbidden_args} are present."
73
+ )
74
+ logging.error(error_msg)
75
+ raise ValueError(error_msg)
@@ -1,4 +1,5 @@
1
1
  from typing import Any
2
+ from typing import Literal
2
3
  from typing import Optional
3
4
 
4
5
  from pydantic import BaseModel
@@ -18,6 +19,23 @@ class _BaseTask(BaseModel):
18
19
  modality: Optional[str] = None
19
20
  tags: list[str] = Field(default_factory=list)
20
21
  docs_info: Optional[str] = None
22
+ type: str
23
+
24
+ @property
25
+ def executable_non_parallel(self) -> str:
26
+ raise NotImplementedError()
27
+
28
+ @property
29
+ def meta_non_parallel(self) -> Optional[dict[str, Any]]:
30
+ raise NotImplementedError()
31
+
32
+ @property
33
+ def executable_parallel(self) -> str:
34
+ raise NotImplementedError()
35
+
36
+ @property
37
+ def meta_parallel(self) -> Optional[dict[str, Any]]:
38
+ raise NotImplementedError()
21
39
 
22
40
 
23
41
  class CompoundTask(_BaseTask):
@@ -29,6 +47,34 @@ class CompoundTask(_BaseTask):
29
47
 
30
48
  executable_init: str
31
49
  meta_init: Optional[dict[str, Any]] = None
50
+ type: Literal["compound"] = "compound"
51
+
52
+ @property
53
+ def executable_non_parallel(self) -> str:
54
+ return self.executable_init
55
+
56
+ @property
57
+ def meta_non_parallel(self) -> Optional[dict[str, Any]]:
58
+ return self.meta_init
59
+
60
+ @property
61
+ def executable_parallel(self) -> str:
62
+ return self.executable
63
+
64
+ @property
65
+ def meta_parallel(self) -> Optional[dict[str, Any]]:
66
+ return self.meta
67
+
68
+
69
+ class ConverterCompoundTask(_BaseTask):
70
+ """
71
+ A `ConverterCompoundTask` task is the same as a `CompoundTask`, but it
72
+ is executed differently in the Fractal backend.
73
+ """
74
+
75
+ executable_init: str
76
+ meta_init: Optional[dict[str, Any]] = None
77
+ type: Literal["converter_compound"] = "converter_compound"
32
78
 
33
79
  @property
34
80
  def executable_non_parallel(self) -> str:
@@ -53,6 +99,33 @@ class NonParallelTask(_BaseTask):
53
99
  may include the `meta` attribute.
54
100
  """
55
101
 
102
+ type: Literal["non_parallel"] = "non_parallel"
103
+
104
+ @property
105
+ def executable_non_parallel(self) -> str:
106
+ return self.executable
107
+
108
+ @property
109
+ def meta_non_parallel(self) -> Optional[dict[str, Any]]:
110
+ return self.meta
111
+
112
+ @property
113
+ def executable_parallel(self) -> None:
114
+ return None
115
+
116
+ @property
117
+ def meta_parallel(self) -> None:
118
+ return None
119
+
120
+
121
+ class ConverterNonParallelTask(_BaseTask):
122
+ """
123
+ A `ConverterNonParallelTask` task is the same as a `NonParallelTask`, but
124
+ it is executed differently in the Fractal backend.
125
+ """
126
+
127
+ type: Literal["converter_non_parallel"] = "converter_non_parallel"
128
+
56
129
  @property
57
130
  def executable_non_parallel(self) -> str:
58
131
  return self.executable
@@ -76,6 +149,8 @@ class ParallelTask(_BaseTask):
76
149
  include the `meta` attribute.
77
150
  """
78
151
 
152
+ type: Literal["parallel"] = "parallel"
153
+
79
154
  @property
80
155
  def executable_non_parallel(self) -> None:
81
156
  return None
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: fractal-task-tools
3
- Version: 0.0.8
3
+ Version: 0.0.10
4
4
  Summary: Shared tools for Fractal tasks
5
5
  Author-email: Tommaso Comparin <tommaso.comparin@exact-lab.it>
6
6
  License: BSD-3-Clause
@@ -11,12 +11,21 @@ Requires-Python: <3.13,>=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Requires-Dist: pydantic<=2.8.2,>=2.0.0
14
- Requires-Dist: docstring-parser==0.15
14
+ Requires-Dist: docstring-parser<=0.16,>=0.15
15
15
  Provides-Extra: dev
16
16
  Requires-Dist: bumpver==2024.1130; extra == "dev"
17
17
  Requires-Dist: devtools==0.12.2; extra == "dev"
18
18
  Requires-Dist: pytest<9.0.0,>=8.3.0; extra == "dev"
19
19
  Requires-Dist: coverage<7.7.0,>=7.6.0; extra == "dev"
20
+ Provides-Extra: docs
21
+ Requires-Dist: mkdocs<1.7.0,>=1.6.0; extra == "docs"
22
+ Requires-Dist: mkdocs-material<9.7.0,>=9.6.0; extra == "docs"
23
+ Requires-Dist: mkdocs-literate-nav<0.7.0,>=0.6.2; extra == "docs"
24
+ Requires-Dist: mkdocs-gen-files<0.6.0,>=0.5.0; extra == "docs"
25
+ Requires-Dist: mkdocs-section-index<0.4.0,>=0.3.9; extra == "docs"
26
+ Requires-Dist: mkdocstrings[python]<0.30.0,>=0.29.0; extra == "docs"
27
+ Requires-Dist: mkdocs-include-markdown-plugin<8.0.0,>=7.0.0; extra == "docs"
28
+ Dynamic: license-file
20
29
 
21
30
  # Fractal task tools
22
31
 
@@ -0,0 +1,21 @@
1
+ fractal_task_tools/__init__.py,sha256=ufFS3IZgAiGp8iACpujoOBeszPyOxqWtQJOLjbp6IcQ,80
2
+ fractal_task_tools/_args_schemas.py,sha256=Ka5IAAEdC7xg03ZHMl5jFbv4Sk8JR1QKbxFl7soevlQ,7829
3
+ fractal_task_tools/_cli.py,sha256=2yxGcktEwHYXrRg_KxhO94xqovYkapBSQZNes94v_so,2458
4
+ fractal_task_tools/_cli_tools.py,sha256=6G7zXvL6o5t7n4HvVAqP6an0o7qx5fEau5KjtoDXFuc,2622
5
+ fractal_task_tools/_create_manifest.py,sha256=kN_WPJMYz-4vPQ8RYUcLbGW6g8kWjs24J-rtrSXU1Eo,5219
6
+ fractal_task_tools/_deepdiff.py,sha256=Or9wO0E9Icb_ki_DTjIdgJKd4JpSJrk8UL3DqqUf-5g,2241
7
+ fractal_task_tools/_descriptions.py,sha256=qYwmpN11UYUmW-PH85ZrHJ3gB6bVjJZ52V9I9Rad3-w,7975
8
+ fractal_task_tools/_package_name_tools.py,sha256=9CyzFwOw4akvQc5GViBm4g-vU8pCc0ULh9hs3b_7VCY,823
9
+ fractal_task_tools/_pydantic_generatejsonschema.py,sha256=qZuID7YUXOdAcL8OqsWjNFNumOIAgdJillc1lA2cHIY,3136
10
+ fractal_task_tools/_signature_constraints.py,sha256=RzJwoL-NMD0HXEACR4_4aD8OOc59lxqEreT7oTc6xJo,3188
11
+ fractal_task_tools/_task_arguments.py,sha256=gw9xN3E50pyyvyIqkWfd-WE5D6CRIRIk618Nw96WmDg,2753
12
+ fractal_task_tools/_task_docs.py,sha256=aEXozSKf3a7weOwJMHyTVJTvHlCKgDr1qoU-AAO3bZI,3401
13
+ fractal_task_tools/_titles.py,sha256=GLWn-06fgQD6qzOM75H59EV0MMCXc8jVpHqGanYzNbw,3000
14
+ fractal_task_tools/task_models.py,sha256=4qBDUAoqIgdfAV-7CKkVO5--Y_2Y6CflNVMIiwJxn9g,4177
15
+ fractal_task_tools/task_wrapper.py,sha256=dhphKgxDm4EUxZnsrAy20hJPD6bGdqx7tNg9N_QzlCo,1878
16
+ fractal_task_tools-0.0.10.dist-info/licenses/LICENSE,sha256=1SGAsQ3Jm_nIU7c2TgtTZe_IOKjm9BDsrcf2r98xrdk,1584
17
+ fractal_task_tools-0.0.10.dist-info/METADATA,sha256=1RHx8ueC74a-H6W8yChJJXweP1ZkOdhoKi3yzUSv15I,4716
18
+ fractal_task_tools-0.0.10.dist-info/WHEEL,sha256=DK49LOLCYiurdXXOXwGJm6U4DkHkg4lcxjhqwRa0CP4,91
19
+ fractal_task_tools-0.0.10.dist-info/entry_points.txt,sha256=zE4qv7QhuiqN6DaPkmJV18X1xyYoUi0HIJ-uAg2M6TU,66
20
+ fractal_task_tools-0.0.10.dist-info/top_level.txt,sha256=2VBpiMDIBMJGOEPiHHX3njYEZGLhr4L0nu8vfkcNVzw,19
21
+ fractal_task_tools-0.0.10.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.1.0)
2
+ Generator: setuptools (78.0.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,20 +0,0 @@
1
- fractal_task_tools/__init__.py,sha256=W-V3ooDhwNplYF0EiHc51q41yNuCEZkXXYg5Yxha_SA,79
2
- fractal_task_tools/_args_schemas.py,sha256=Ka5IAAEdC7xg03ZHMl5jFbv4Sk8JR1QKbxFl7soevlQ,7829
3
- fractal_task_tools/_cli.py,sha256=2yxGcktEwHYXrRg_KxhO94xqovYkapBSQZNes94v_so,2458
4
- fractal_task_tools/_cli_tools.py,sha256=6G7zXvL6o5t7n4HvVAqP6an0o7qx5fEau5KjtoDXFuc,2622
5
- fractal_task_tools/_create_manifest.py,sha256=p5Z7ZZy8EkwWGPZHsqfNtkmLEAmHfL5QDfuvqXWwWEY,4821
6
- fractal_task_tools/_deepdiff.py,sha256=M3YZbAhZZx6iiAFvORZpxNENBpWmdEqkG29UJ7dFAcA,2239
7
- fractal_task_tools/_descriptions.py,sha256=n7WcoIBQ4FEd16BI26iL-Sh6bEQ76jlU32JfJBF4U0A,7566
8
- fractal_task_tools/_package_name_tools.py,sha256=_UT2cThh742V6M0XT9m0_aByhj5fXRSjATKt_MIXFVg,819
9
- fractal_task_tools/_pydantic_generatejsonschema.py,sha256=qZuID7YUXOdAcL8OqsWjNFNumOIAgdJillc1lA2cHIY,3136
10
- fractal_task_tools/_signature_constraints.py,sha256=RzJwoL-NMD0HXEACR4_4aD8OOc59lxqEreT7oTc6xJo,3188
11
- fractal_task_tools/_task_docs.py,sha256=aEXozSKf3a7weOwJMHyTVJTvHlCKgDr1qoU-AAO3bZI,3401
12
- fractal_task_tools/_titles.py,sha256=GLWn-06fgQD6qzOM75H59EV0MMCXc8jVpHqGanYzNbw,3000
13
- fractal_task_tools/task_models.py,sha256=kgvEv5NQD8e8I9tyaFrnvxUxbKm6vH63r-cnID1jHE8,2269
14
- fractal_task_tools/task_wrapper.py,sha256=dhphKgxDm4EUxZnsrAy20hJPD6bGdqx7tNg9N_QzlCo,1878
15
- fractal_task_tools-0.0.8.dist-info/LICENSE,sha256=1SGAsQ3Jm_nIU7c2TgtTZe_IOKjm9BDsrcf2r98xrdk,1584
16
- fractal_task_tools-0.0.8.dist-info/METADATA,sha256=sPGtC0eh1daDZMpjVl_H1OPszRhPbPSWP7t6rw4PRpA,4208
17
- fractal_task_tools-0.0.8.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
18
- fractal_task_tools-0.0.8.dist-info/entry_points.txt,sha256=zE4qv7QhuiqN6DaPkmJV18X1xyYoUi0HIJ-uAg2M6TU,66
19
- fractal_task_tools-0.0.8.dist-info/top_level.txt,sha256=2VBpiMDIBMJGOEPiHHX3njYEZGLhr4L0nu8vfkcNVzw,19
20
- fractal_task_tools-0.0.8.dist-info/RECORD,,