fractal-task-tools 0.2.0__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.
@@ -0,0 +1,100 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+
5
+ _Schema = dict[str, Any]
6
+
7
+
8
+ def _include_titles_for_properties(
9
+ properties: dict[str, dict],
10
+ verbose: bool = False,
11
+ ) -> dict[str, dict]:
12
+ """
13
+ Scan through properties of a JSON Schema, and set their title when it is
14
+ missing.
15
+
16
+ The title is set to `name.title()`, where `title` is a standard string
17
+ method - see https://docs.python.org/3/library/stdtypes.html#str.title.
18
+
19
+ Args:
20
+ properties: TBD
21
+ """
22
+ if verbose:
23
+ logging.info(
24
+ f"[_include_titles_for_properties] Original properties:\n"
25
+ f"{properties}"
26
+ )
27
+
28
+ new_properties = properties.copy()
29
+ for prop_name, prop in properties.items():
30
+ if "title" not in prop.keys():
31
+ new_prop = prop.copy()
32
+ new_prop["title"] = prop_name.title()
33
+ new_properties[prop_name] = new_prop
34
+ if verbose:
35
+ logging.info(
36
+ f"[_include_titles_for_properties] New properties:\n"
37
+ f"{new_properties}"
38
+ )
39
+ return new_properties
40
+
41
+
42
+ def _include_titles(
43
+ schema: _Schema,
44
+ definitions_key: str,
45
+ verbose: bool = False,
46
+ ) -> _Schema:
47
+ """
48
+ Include property titles, when missing.
49
+
50
+ This handles both:
51
+
52
+ - first-level JSON Schema properties (corresponding to task
53
+ arguments);
54
+ - properties of JSON Schema definitions (corresponding to
55
+ task-argument attributes).
56
+
57
+ Args:
58
+ schema: TBD
59
+ definitions_key: Either `"definitions"` (for Pydantic V1) or
60
+ `"$defs"` (for Pydantic V2)
61
+ verbose:
62
+ """
63
+ new_schema = schema.copy()
64
+
65
+ if verbose:
66
+ logging.info("[_include_titles] START")
67
+ logging.info(f"[_include_titles] Input schema:\n{schema}")
68
+
69
+ # Update first-level properties (that is, task arguments)
70
+ new_properties = _include_titles_for_properties(
71
+ schema["properties"], verbose=verbose
72
+ )
73
+ new_schema["properties"] = new_properties
74
+
75
+ if verbose:
76
+ logging.info("[_include_titles] Titles for properties now included.")
77
+
78
+ # Update properties of definitions
79
+ if definitions_key in schema.keys():
80
+ new_definitions = schema[definitions_key].copy()
81
+ for def_name, def_schema in new_definitions.items():
82
+ if "properties" not in def_schema.keys():
83
+ if verbose:
84
+ logging.info(
85
+ f"Definition schema {def_name} has no 'properties' "
86
+ "key. Skip."
87
+ )
88
+ else:
89
+ new_properties = _include_titles_for_properties(
90
+ def_schema["properties"], verbose=verbose
91
+ )
92
+ new_definitions[def_name]["properties"] = new_properties
93
+ new_schema[definitions_key] = new_definitions
94
+
95
+ if verbose:
96
+ logging.info(
97
+ "[_include_titles] Titles for definitions properties now included."
98
+ )
99
+ logging.info("[_include_titles] END")
100
+ return new_schema
@@ -0,0 +1,55 @@
1
+ import types
2
+ import typing
3
+
4
+ from pydantic.fields import FieldInfo
5
+
6
+ _UNION_TYPES = {typing.Union, types.UnionType}
7
+
8
+
9
+ def is_union(_type) -> bool:
10
+ """
11
+ Determine whether `_type` is a union.
12
+
13
+ Based on
14
+ https://docs.python.org/3/library/typing.html#typing.Union
15
+ https://discuss.python.org/t/how-to-check-if-a-type-annotation-represents-an-union/77692/2.
16
+ """
17
+ result = typing.get_origin(_type) in _UNION_TYPES
18
+ alternative_result = (
19
+ type(_type) is typing._UnionGenericAlias
20
+ or type(_type) is types.UnionType
21
+ )
22
+ if result != alternative_result:
23
+ # This is a safety check, which is meant to be unreachable
24
+ raise ValueError(
25
+ f"Could not determine whether {_type} is a union. Please report "
26
+ "this at https://github.com/fractal-analytics-platform/"
27
+ "fractal-task-tools/issues."
28
+ )
29
+ return result
30
+
31
+
32
+ def is_annotated_union(_type) -> bool:
33
+ """
34
+ Determine whether `_type` is `Annotated` and its wrapped type is a union.
35
+
36
+ See https://docs.python.org/3/library/typing.html#typing.Annotated
37
+ """
38
+ return typing.get_origin(_type) is typing.Annotated and is_union(
39
+ _type.__origin__
40
+ )
41
+
42
+
43
+ def is_tagged(_type) -> bool:
44
+ """
45
+ Determine whether annotations make an `Annotated` type a tagged union.
46
+
47
+ Note that this function only gets called after `is_annotated_union(_type)`
48
+ returned `True`.
49
+
50
+ See https://docs.python.org/3/library/typing.html#typing.Annotated
51
+ """
52
+ return any(
53
+ isinstance(_item, FieldInfo) and _item.discriminator is not None
54
+ for _item in _type.__metadata__
55
+ )
@@ -0,0 +1,168 @@
1
+ from typing import Any
2
+ from typing import Literal
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel
6
+ from pydantic import ConfigDict
7
+ from pydantic import Field
8
+
9
+
10
+ class _BaseTask(BaseModel):
11
+ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
12
+
13
+ name: str
14
+ executable: str
15
+ meta: Optional[dict[str, Any]] = None
16
+ input_types: Optional[dict[str, bool]] = None
17
+ output_types: Optional[dict[str, bool]] = None
18
+ category: Optional[str] = None
19
+ modality: Optional[str] = None
20
+ tags: list[str] = Field(default_factory=list)
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()
39
+
40
+
41
+ class CompoundTask(_BaseTask):
42
+ """
43
+ A `CompoundTask` object must include both `executable_init` and
44
+ `executable` attributes, and it may include the `meta_init` and `meta`
45
+ attributes.
46
+ """
47
+
48
+ executable_init: str
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"
78
+
79
+ @property
80
+ def executable_non_parallel(self) -> str:
81
+ return self.executable_init
82
+
83
+ @property
84
+ def meta_non_parallel(self) -> Optional[dict[str, Any]]:
85
+ return self.meta_init
86
+
87
+ @property
88
+ def executable_parallel(self) -> str:
89
+ return self.executable
90
+
91
+ @property
92
+ def meta_parallel(self) -> Optional[dict[str, Any]]:
93
+ return self.meta
94
+
95
+
96
+ class NonParallelTask(_BaseTask):
97
+ """
98
+ A `NonParallelTask` object must include the `executable` attribute, and it
99
+ may include the `meta` attribute.
100
+ """
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
+
129
+ @property
130
+ def executable_non_parallel(self) -> str:
131
+ return self.executable
132
+
133
+ @property
134
+ def meta_non_parallel(self) -> Optional[dict[str, Any]]:
135
+ return self.meta
136
+
137
+ @property
138
+ def executable_parallel(self) -> None:
139
+ return None
140
+
141
+ @property
142
+ def meta_parallel(self) -> None:
143
+ return None
144
+
145
+
146
+ class ParallelTask(_BaseTask):
147
+ """
148
+ A `ParallelTask` object must include the `executable` attribute, and it may
149
+ include the `meta` attribute.
150
+ """
151
+
152
+ type: Literal["parallel"] = "parallel"
153
+
154
+ @property
155
+ def executable_non_parallel(self) -> None:
156
+ return None
157
+
158
+ @property
159
+ def meta_non_parallel(self) -> None:
160
+ return None
161
+
162
+ @property
163
+ def executable_parallel(self) -> str:
164
+ return self.executable
165
+
166
+ @property
167
+ def meta_parallel(self) -> Optional[dict[str, Any]]:
168
+ return self.meta
@@ -0,0 +1,71 @@
1
+ """
2
+ Standard input/output interface for tasks.
3
+ """
4
+ import json
5
+ import logging
6
+ from argparse import ArgumentParser
7
+ from json import JSONEncoder
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+
12
+ class TaskParameterEncoder(JSONEncoder):
13
+ """
14
+ Custom JSONEncoder that transforms Path objects to strings.
15
+
16
+ Ref https://docs.python.org/3/library/json.html
17
+ """
18
+
19
+ def default(self, obj):
20
+ if isinstance(obj, Path):
21
+ return obj.as_posix()
22
+ return super().default(obj)
23
+
24
+
25
+ def run_fractal_task(
26
+ *,
27
+ task_function: callable,
28
+ logger_name: Optional[str] = None,
29
+ ):
30
+ """
31
+ Implement standard task interface and call task_function.
32
+
33
+ Args:
34
+ task_function: the callable function that runs the task.
35
+ logger_name: TBD
36
+ """
37
+
38
+ # Parse `-j` and `--metadata-out` arguments
39
+ parser = ArgumentParser()
40
+ parser.add_argument(
41
+ "--args-json", help="Read parameters from json file", required=True
42
+ )
43
+ parser.add_argument(
44
+ "--out-json",
45
+ help="Output file to redirect serialised returned data",
46
+ required=True,
47
+ )
48
+ parsed_args = parser.parse_args()
49
+
50
+ # Set logger
51
+ logger = logging.getLogger(logger_name)
52
+
53
+ # Preliminary check
54
+ if Path(parsed_args.out_json).exists():
55
+ logger.error(
56
+ f"Output file {parsed_args.out_json} already exists. Terminating"
57
+ )
58
+ exit(1)
59
+
60
+ # Read parameters dictionary
61
+ with open(parsed_args.args_json, "r") as f:
62
+ pars = json.load(f)
63
+
64
+ # Run task
65
+ logger.info(f"START {task_function.__name__} task")
66
+ metadata_update = task_function(**pars)
67
+ logger.info(f"END {task_function.__name__} task")
68
+
69
+ # Write output metadata to file, with custom JSON encoder
70
+ with open(parsed_args.out_json, "w") as fout:
71
+ json.dump(metadata_update, fout, cls=TaskParameterEncoder, indent=2)
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: fractal-task-tools
3
+ Version: 0.2.0
4
+ Summary: Shared tools for Fractal tasks
5
+ Author-email: Tommaso Comparin <tommaso.comparin@exact-lab.it>
6
+ License: BSD-3-Clause
7
+ Project-URL: homepage, https://github.com/fractal-analytics-platform/fractal-task-tools
8
+ Project-URL: repository, https://github.com/fractal-analytics-platform/fractal-task-tools
9
+ Project-URL: changelog, https://github.com/fractal-analytics-platform/fractal-task-tools/blob/main/CHANGELOG.md
10
+ Requires-Python: <3.15,>=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: pydantic<=2.13.0,>=2.0.0
14
+ Requires-Dist: docstring-parser<=0.16,>=0.15
15
+ Requires-Dist: packaging
16
+ Provides-Extra: dev
17
+ Requires-Dist: bumpver==2024.1130; extra == "dev"
18
+ Requires-Dist: devtools==0.12.2; extra == "dev"
19
+ Requires-Dist: pytest<9.0.0,>=8.3.0; extra == "dev"
20
+ Requires-Dist: coverage<7.7.0,>=7.6.0; extra == "dev"
21
+ Requires-Dist: jsonschema>=4; extra == "dev"
22
+ Requires-Dist: requests<2.33.0,>2.32.0; extra == "dev"
23
+ Provides-Extra: docs
24
+ Requires-Dist: mkdocs==1.6.1; extra == "docs"
25
+ Requires-Dist: mkdocstrings[python]==0.30.0; extra == "docs"
26
+ Requires-Dist: mkdocs-material==9.6.15; extra == "docs"
27
+ Requires-Dist: mkdocs-gen-files==0.5.0; extra == "docs"
28
+ Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "docs"
29
+ Requires-Dist: mkdocs-section-index==0.3.10; extra == "docs"
30
+ Requires-Dist: mkdocs-include-markdown-plugin==7.1.8; extra == "docs"
31
+ Dynamic: license-file
32
+
33
+ # Fractal task tools
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/fractal-task-tools?color=gree)](https://pypi.org/project/fractal-task-tools/)
36
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
37
+ [![CI Status](https://github.com/fractal-analytics-platform/fractal-task-tools/actions/workflows/ci.yml/badge.svg)](https://github.com/fractal-analytics-platform/fractal-task-tools/actions/workflows/ci.yml)
38
+ [![Coverage](https://raw.githubusercontent.com/fractal-analytics-platform/fractal-task-tools/python-coverage-comment-action-data/badge.svg)](https://htmlpreview.github.io/?https://github.com/fractal-analytics-platform/fractal-task-tools/blob/python-coverage-comment-action-data/htmlcov/index.html)
39
+
40
+ Fractal-task-tools provides some basic tools for building tasks for the [Fractal](https://fractal-analytics-platform.github.io/) framework.
41
+
42
+ ![Fractal_overview_small](https://github.com/user-attachments/assets/666c8797-2594-4b8e-b1d2-b43fca66d1df)
43
+
44
+ [Fractal](https://fractal-analytics-platform.github.io/) is a framework developed at the [BioVisionCenter](https://www.biovisioncenter.uzh.ch/en.html) to process bioimaging data at scale in the OME-Zarr format and prepare the images for interactive visualization.
45
+
46
+
47
+ # Get started
48
+ ```console
49
+ $ python -m venv venv
50
+
51
+ $ source venv/bin/activate
52
+
53
+ $ python -m pip install -e .
54
+ [...]
55
+ Successfully installed annotated-types-0.7.0 docstring-parser-0.15 fractal-task-tools-0.0.1 pydantic-2.8.2 pydantic-core-2.20.1 typing-extensions-4.12.2
56
+
57
+ $ fractal-manifest create --help
58
+ usage: fractal-manifest create [-h] --package PACKAGE [--task-list-path TASK_LIST_PATH]
59
+
60
+ Create new manifest file
61
+
62
+ options:
63
+ -h, --help show this help message and exit
64
+ --package PACKAGE Example: 'fractal_tasks_core'
65
+ --task-list-path TASK_LIST_PATH
66
+ Dot-separated path to the `task_list.py` module, relative to the package root (default value:
67
+ 'dev.task_list').
68
+
69
+ ```
70
+
71
+ # Development
72
+
73
+ ```console
74
+ $ python -m venv venv
75
+
76
+ $ source venv/bin/activate
77
+
78
+ $ python -m pip install -e .[dev]
79
+ [...]
80
+ Successfully installed asttokens-2.4.1 bumpver-2024.1130 click-8.1.8 colorama-0.4.6 coverage-7.6.12 devtools-0.12.2 exceptiongroup-1.2.2 executing-2.2.0 fractal-task-tools-0.0.1 iniconfig-2.0.0 lexid-2021.1006 packaging-24.2 pluggy-1.5.0 pygments-2.19.1 pytest-8.3.5 six-1.17.0 toml-0.10.2 tomli-2.2.1
81
+
82
+ $ pre-commit install
83
+ pre-commit installed at .git/hooks/pre-commit
84
+ ```
85
+
86
+ ## How to make a release
87
+ From the development environment:
88
+ ```
89
+ bumpver update --patch --dry
90
+ ```
91
+
92
+
93
+ ## Contributors and license
94
+
95
+ Fractal was conceived in the Liberali Lab at the Friedrich Miescher Institute for Biomedical Research and in the Pelkmans Lab at the University of Zurich by [@jluethi](https://github.com/jluethi) and [@gusqgm](https://github.com/gusqgm). The Fractal project is now developed at the [BioVisionCenter](https://www.biovisioncenter.uzh.ch/en.html) at the University of Zurich and the project lead is with [@jluethi](https://github.com/jluethi). The core development is done under contract by [eXact lab S.r.l.](https://www.exact-lab.it).
96
+
97
+ Unless otherwise specified, Fractal components are released under the BSD 3-Clause License, and copyright is with the BioVisionCenter at the University of Zurich.
@@ -0,0 +1,22 @@
1
+ fractal_task_tools/__init__.py,sha256=AwEt_j9THts4zcoSWRDJs6u1ZOkQO_ghLtEOccPI4UE,79
2
+ fractal_task_tools/_args_schemas.py,sha256=OGSFM6AZDFVXSzWJrZo1tQFvI6uHkmMYq6X-9GwQRiU,9102
3
+ fractal_task_tools/_cli.py,sha256=FdFkWj92wCQDaFm4b44yxsD1SbBH1qARbiH7DgiXpgM,2459
4
+ fractal_task_tools/_cli_tools.py,sha256=6G7zXvL6o5t7n4HvVAqP6an0o7qx5fEau5KjtoDXFuc,2622
5
+ fractal_task_tools/_create_manifest.py,sha256=NQAAqTLGgDECQEh4g8E5DzEBkVk_ywCOnWqNC640EVY,5431
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=69qs0yeckZAr37wo75qU_E0g-wPiMp_Ho3FFt6xgwjU,4566
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/_union_types.py,sha256=Dz7CiLY6sJM631bJJva0k-OBtA-jdjKO1lENnRL2TrI,1639
15
+ fractal_task_tools/task_models.py,sha256=4qBDUAoqIgdfAV-7CKkVO5--Y_2Y6CflNVMIiwJxn9g,4177
16
+ fractal_task_tools/task_wrapper.py,sha256=dhphKgxDm4EUxZnsrAy20hJPD6bGdqx7tNg9N_QzlCo,1878
17
+ fractal_task_tools-0.2.0.dist-info/licenses/LICENSE,sha256=1SGAsQ3Jm_nIU7c2TgtTZe_IOKjm9BDsrcf2r98xrdk,1584
18
+ fractal_task_tools-0.2.0.dist-info/METADATA,sha256=6gxxqP68TK0RnhKP03Q5Le7iR7ojiQ7JYaYPnUMNrT8,4793
19
+ fractal_task_tools-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ fractal_task_tools-0.2.0.dist-info/entry_points.txt,sha256=zE4qv7QhuiqN6DaPkmJV18X1xyYoUi0HIJ-uAg2M6TU,66
21
+ fractal_task_tools-0.2.0.dist-info/top_level.txt,sha256=2VBpiMDIBMJGOEPiHHX3njYEZGLhr4L0nu8vfkcNVzw,19
22
+ fractal_task_tools-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fractal-manifest = fractal_task_tools._cli:main
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright 2024 (C) Friedrich Miescher Institute for Biomedical Research and University of Zurich
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ fractal_task_tools