broskill 0.1.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.
broskill/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ from broskill.data_specs.skill import Skill, SkillStatus
2
+ from broskill.data_specs.tool import Arg, Tool
3
+ from broskill.processing.skill import SkillControl
4
+ from broskill.processing.tool import ToolControl
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "Arg",
9
+ "Skill",
10
+ "SkillControl",
11
+ "SkillStatus",
12
+ "Tool",
13
+ "ToolControl"
14
+ ]
File without changes
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import StrEnum
3
+ from pathlib import Path
4
+
5
+
6
+ class SkillStatus(StrEnum):
7
+ STABLE = 'stable'
8
+ EXPERIMENT = 'experiment'
9
+ DEPRECATED = 'deprecated'
10
+
11
+ @dataclass
12
+ class Skill:
13
+ name:str = field(
14
+ metadata={'description': "a skill name in frontmatter"}
15
+ )
16
+ description:str = field(
17
+ metadata={'description': "a description of the skill in frontmatter"}
18
+ )
19
+ version:str = field(
20
+ metadata={'description': "a skill's version recommending as v0.1.0 (vMajor.Minor.Patch)"}
21
+ )
22
+ path:Path = field(
23
+ metadata={'description': "a directory path of the skill."}
24
+ )
25
+ tags:list[str] | None = field(
26
+ metadata={'description': "tags will use later for searching and filtering"},
27
+ default=None
28
+ )
29
+ keywords:list[str] | None = field(
30
+ metadata={'description': "keywords will use later for searching and filtering"},
31
+ default=None
32
+ )
33
+ default:bool = field(
34
+ metadata={'description': "default here tells a program that this skill must be loaded or display"},
35
+ default=False
36
+ )
37
+ status:SkillStatus | None = field(
38
+ metadata={'description': "status will be: stable if you test it and pass, experiment if you are in developing phase, deprecated if you plan not to use it."},
39
+ default=SkillStatus.EXPERIMENT
40
+ )
@@ -0,0 +1,54 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ DTYPE_MAP = {
6
+ str: "string",
7
+ int: "integer",
8
+ float: "number",
9
+ bool: "boolean",
10
+ }
11
+
12
+ @dataclass
13
+ class Arg:
14
+ name:str = field(
15
+ metadata={"description": "the argument's name, taken from its dest in get_args()"}
16
+ )
17
+ type:Any = field(
18
+ metadata={"description": "the argument's Python type (e.g. str, int) from get_args(), converted to its JSON-schema type string (e.g. 'string', 'integer') on init"}
19
+ )
20
+ description:str | None = field(
21
+ metadata={"description": "the argument's help text, taken from get_args()"},
22
+ default=None
23
+ )
24
+ required:bool | None = field(
25
+ metadata={"description": "whether the argument is required to call the tool"},
26
+ default=True
27
+ )
28
+
29
+ def __post_init__(self):
30
+ """Resolve `type` to its JSON-schema string, if given as a Python type.
31
+
32
+ Raises:
33
+ ValueError: If `type` is a Python type not in `DTYPE_MAP`.
34
+ """
35
+ if isinstance(self.type, str):
36
+ return
37
+ if self.type not in DTYPE_MAP:
38
+ raise ValueError(f"Unsupported type for arg '{self.name}': {self.type}")
39
+ self.type = DTYPE_MAP[self.type]
40
+
41
+ @dataclass
42
+ class Tool:
43
+ name:str = field(
44
+ metadata={"description": "the tool's name, taken from the script's filename"}
45
+ )
46
+ description:str = field(
47
+ metadata={"description": "the tool's description, taken from get_args()'s parser description"}
48
+ )
49
+ args:list[Arg] = field(
50
+ metadata={"description": "the tool's arguments"}
51
+ )
52
+ path:Path = field(
53
+ metadata={"description": "the path to the script that implements this tool"}
54
+ )
File without changes
@@ -0,0 +1,38 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+
5
+ def strip_path(path: str) -> str:
6
+ """Stripping the path in to the bare path
7
+ Args:
8
+ path (str): The path to be stripped
9
+ Returns:
10
+ str: The stripped path
11
+ Examples:
12
+ >>> path = strip_path(path='*/references/*')
13
+ >>> print(path)
14
+ references
15
+ """
16
+ path = re.sub(r'^[*/]+', '', path)
17
+ path = re.sub(r'[*/]+$', '', path)
18
+ return path
19
+
20
+ def find_root(start: Path | None = None, marker: str = "skills") -> Path:
21
+ """Walk upward from `start` (default: cwd) looking for a folder
22
+ containing `marker`, the same trick git uses for `.git`.
23
+
24
+ Args:
25
+ start (Path): Where to begin the search. Defaults to Path.cwd().
26
+ marker (str): Folder name that identifies the project root.
27
+
28
+ Returns:
29
+ Path: The first ancestor (including `start`) containing `marker/`.
30
+
31
+ Raises:
32
+ ValueError: If no ancestor contains `marker/`.
33
+ """
34
+ current = (start or Path.cwd()).resolve()
35
+ for candidate in [current, *current.parents]:
36
+ if (candidate / marker).is_dir():
37
+ return candidate
38
+ raise ValueError(f"No '{marker}/' folder found in {current} or any parent")
@@ -0,0 +1,169 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+
6
+ from broskill.data_specs.skill import Skill
7
+
8
+
9
+ def split_frontmatter(text: str) -> tuple[dict, str]:
10
+ """Split a SKILL.md file's text into its frontmatter and body.
11
+
12
+ Args:
13
+ text (str): Raw file contents, expected to start with a `---`
14
+ delimited YAML block followed by the markdown body.
15
+
16
+ Returns:
17
+ tuple[dict, str]: The parsed frontmatter as a dict, and the
18
+ remaining body text with surrounding whitespace stripped.
19
+
20
+ Raises:
21
+ ValueError: If no `---` delimited frontmatter block is found.
22
+ """
23
+ match = re.match(r'^---\n(.*?)\n---\n?(.*)', text, re.DOTALL)
24
+ if not match:
25
+ raise ValueError('No frontmatter block found')
26
+ metadata = yaml.safe_load(match.group(1)) or {}
27
+ body = match.group(2).strip()
28
+ return metadata, body
29
+
30
+ class SkillControl:
31
+ """Discovers, loads, and reads skills from a skills directory.
32
+
33
+ `root` is the skills directory itself (e.g. `project_root / "skills"`),
34
+ not the project root — every method searches directly under `root`,
35
+ with no implicit `"skills"` subfolder appended.
36
+
37
+ Skills loaded via `load_skill`/`list_skills` are cached on `self.skills`
38
+ (keyed by skill name), so later calls that only need a skill's path
39
+ (`load_skill_extension`) don't need to search the filesystem again.
40
+
41
+ Attributes:
42
+ root (Path): Default skills directory (e.g. `project_root / "skills"`),
43
+ not the project root.
44
+ skills (dict[str, Skill]): Skills loaded so far, keyed by name.
45
+ """
46
+
47
+ def __init__(self, root:Path):
48
+ """Initialize the controller.
49
+
50
+ Args:
51
+ root (Path): Default skills directory (e.g. `project_root / "skills"`),
52
+ not the project root.
53
+ """
54
+ self.root:Path = root
55
+ self.skills:dict[str, Skill] = {}
56
+
57
+ def get_skill_path(self, skill_name:str)->Path | None:
58
+ """Get a previously loaded skill's directory path.
59
+
60
+ Args:
61
+ skill_name (str): Name of the skill, as registered by `load_skill`.
62
+
63
+ Returns:
64
+ Path | None: The skill's directory, or None if it hasn't been
65
+ loaded yet (call `load_skill` first).
66
+ """
67
+ skill = self.skills.get(skill_name, None)
68
+ if skill:
69
+ return skill.path
70
+ return None
71
+
72
+ def get_root(self, root:Path|None)->Path|None:
73
+ """Resolve the skills directory to use for a call.
74
+
75
+ Args:
76
+ root (Path | None): An explicit skills directory to use, or None
77
+ to fall back to `self.root`.
78
+
79
+ Returns:
80
+ Path | None: `root` if given, otherwise `self.root`.
81
+ """
82
+ if root is None:
83
+ return self.root
84
+ return root
85
+
86
+ def list_skills(self, root:Path|None=None)->list[Skill]|list:
87
+ """List and load every skill under the skills directory.
88
+
89
+ Args:
90
+ root (Path | None): Skills directory to search. Defaults to `self.root`.
91
+
92
+ Returns:
93
+ list[Skill]: Every skill discovered by scanning `root` for
94
+ `SKILL.md` files.
95
+
96
+ Raises:
97
+ FileNotFoundError: If `root` doesn't exist.
98
+ """
99
+ root = self.get_root(root)
100
+ if not root.exists():
101
+ raise FileNotFoundError(f"Skills directory not found: {root}")
102
+ skills = list(root.rglob("SKILL.md"))
103
+ _ = [self.load_skill(s.parent.name, root) for s in skills]
104
+ return list(self.skills.values())
105
+
106
+ def load_skill(self, skill_name:str, root:Path|None=None)->str|None:
107
+ """Load a skill by name and register it into `self.skills`.
108
+
109
+ Args:
110
+ skill_name (str): Name of the skill's directory under the skills directory.
111
+ root (Path | None): Skills directory to search. Defaults to `self.root`.
112
+
113
+ Returns:
114
+ Skill | None: The loaded skill, or None if no directory named
115
+ `skill_name` exists under `root`.
116
+
117
+ Raises:
118
+ ValueError: If a path named `skill_name` exists but none of the
119
+ matches is a valid skill directory, or if its `SKILL.md`
120
+ frontmatter doesn't match the `Skill` dataclass fields.
121
+ FileNotFoundError: If the matched directory has no `SKILL.md`.
122
+ """
123
+ root = self.get_root(root)
124
+ skills = list(root.rglob(skill_name))
125
+ if len(skills) == 0:
126
+ return None
127
+ candidates = [s for s in skills if s.is_dir() and s.exists()]
128
+ if not candidates:
129
+ raise ValueError(f"No skill directory found for '{skill_name}' under {root}")
130
+ skill = candidates[0]
131
+ skill_md = skill / "SKILL.md"
132
+ if not skill_md.is_file():
133
+ raise FileNotFoundError(f"'{skill_md}' not found for skill '{skill_name}'")
134
+ text = skill_md.read_text(encoding='utf-8')
135
+ metadata, body = split_frontmatter(text)
136
+ try:
137
+ _skill = Skill(**metadata, path=skill)
138
+ except TypeError as e:
139
+ raise ValueError(f"'{skill_md}' frontmatter doesn't match the Skill schema: {e}") from e
140
+ self.skills[skill_name] = _skill
141
+ return body
142
+
143
+ def load_skill_extension(self, skill_name:str, path:str, root:Path|None=None)->str|None:
144
+ """Load any skill artifact under its `references/`, `scripts/`, or `assets/` folder.
145
+
146
+ Args:
147
+ skill_name (str): Name of the skill, as registered by `load_skill`.
148
+ path (str): Path to the artifact, relative to the skill's directory
149
+ (e.g. `references/script.md`, `scripts/read_file.py`).
150
+ root (Path | None): Project root, currently unused directly (the
151
+ skill's own registered path is used) but accepted for a
152
+ consistent signature with the other loaders.
153
+
154
+ Returns:
155
+ str | None: The artifact's text content, or None if `path` doesn't
156
+ exist under the skill's directory.
157
+
158
+ Raises:
159
+ ValueError: If `skill_name` hasn't been loaded yet — call
160
+ `load_skill` first.
161
+ """
162
+ root = self.get_root(root)
163
+ skill_path = self.get_skill_path(skill_name)
164
+ if skill_path is None:
165
+ raise ValueError(f"Skill '{skill_name}' is not loaded — call load_skill first")
166
+ target = skill_path / path
167
+ if target.is_file():
168
+ return target.read_text(encoding='utf-8')
169
+ return None
@@ -0,0 +1,108 @@
1
+ import importlib.util
2
+ from typing import Any
3
+
4
+ from broskill.data_specs.tool import Arg, Tool
5
+ from broskill.processing.skill import SkillControl
6
+
7
+
8
+ def to_args(args: dict) -> list[Any]:
9
+ """Convert a dict of tool arguments into a CLI argument list.
10
+
11
+ Args:
12
+ args (dict): Argument names mapped to their values, e.g. `{"path": "a.md"}`.
13
+
14
+ Returns:
15
+ list[Any]: A flat `--name value` list suitable for `subprocess.run`,
16
+ e.g. `["--path", "a.md"]`. Empty list if `args` is falsy.
17
+ """
18
+ if not args:
19
+ return []
20
+ args_list = []
21
+ for k, v in args.items():
22
+ args_list.extend([f"--{k}", v])
23
+ return args_list
24
+
25
+
26
+ class ToolControl:
27
+ """Loads a skill's `scripts/*.py` files as callable `Tool` schemas.
28
+
29
+ Composes with a `SkillControl` for skill_name -> directory resolution,
30
+ so both classes share one source of truth for where a skill lives.
31
+ Every script must define a module-level `get_args()` returning an
32
+ `argparse.ArgumentParser` (see `skills/create-skill/references/script.md`);
33
+ that parser is introspected to build the `Tool`/`Arg` schema.
34
+
35
+ Attributes:
36
+ skills (SkillControl): Used to resolve a skill name to its directory.
37
+ tools (dict[str, Tool]): Tools loaded so far, keyed by `"skill_name:path"`.
38
+ """
39
+
40
+ def __init__(self, skills: SkillControl):
41
+ """Initialize the controller.
42
+
43
+ Args:
44
+ skills (SkillControl): The skill controller to resolve skill
45
+ directories through.
46
+ """
47
+ self.skills = skills
48
+ self.tools: dict[str, Tool] = {}
49
+
50
+ def load_tool(self, skill_name: str, path: str) -> Tool:
51
+ """Load a skill's script as a `Tool` schema, caching the result.
52
+
53
+ Args:
54
+ skill_name (str): Name of the skill, as registered by `self.skills.load_skill`.
55
+ path (str): Path to the script, relative to the skill's directory
56
+ (e.g. `scripts/read_file.py`).
57
+
58
+ Returns:
59
+ Tool: The tool schema built from the script's `get_args()` parser.
60
+
61
+ Raises:
62
+ ValueError: If `skill_name` isn't loaded, the file can't be loaded
63
+ as a Python module, it has no `get_args()`, or one of its
64
+ arguments uses a type `Arg` doesn't recognize (see `DTYPE_MAP`
65
+ in `broskill.data_specs.tool`).
66
+ FileNotFoundError: If `path` doesn't exist under the skill's directory.
67
+ """
68
+ skill_path = self.skills.get_skill_path(skill_name)
69
+ if skill_path is None:
70
+ raise ValueError(f"Skill '{skill_name}' is not loaded — call load_skill first")
71
+
72
+ cache_key = f"{skill_name}:{path}"
73
+ if cache_key in self.tools:
74
+ return self.tools[cache_key]
75
+
76
+ script_path = skill_path / path
77
+ if not script_path.is_file():
78
+ raise FileNotFoundError(f"Script not found: {script_path}")
79
+
80
+ spec = importlib.util.spec_from_file_location(script_path.stem, script_path)
81
+ if spec is None or spec.loader is None:
82
+ raise ValueError(f"Could not load '{script_path}' as a Python module")
83
+ module = importlib.util.module_from_spec(spec)
84
+ spec.loader.exec_module(module)
85
+
86
+ if not hasattr(module, "get_args"):
87
+ raise ValueError(f"'{script_path}' must define a get_args() function")
88
+ parser = module.get_args()
89
+
90
+ args = []
91
+ for action in parser._actions:
92
+ if action.dest == "help":
93
+ continue
94
+ try:
95
+ args.append(
96
+ Arg(
97
+ name=action.dest,
98
+ type=action.type,
99
+ description=action.help,
100
+ required=action.required,
101
+ )
102
+ )
103
+ except ValueError as e:
104
+ raise ValueError(f"{e} (in '{script_path}')") from e
105
+
106
+ tool = Tool(name=script_path.stem, description=parser.description, args=args, path=script_path)
107
+ self.tools[cache_key] = tool
108
+ return tool
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: broskill
3
+ Version: 0.1.0
4
+ Summary: Lightweight, quick-loading skill loader for agentic projects — discovers skills (SKILL.md + references/scripts/assets), loads their instructions, and turns their scripts into callable tools.
5
+ Project-URL: Repository, https://github.com/datanooblol/broskill
6
+ Project-URL: Changelog, https://github.com/datanooblol/broskill/blob/main/VERSIONS.md
7
+ Author: datanooblol
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: pyyaml>=6.0.3
12
+ Description-Content-Type: text/markdown
13
+
14
+ # broskill
15
+
16
+ yo. **broskill** is a lightweight, quick-loading skill loader for agentic projects — a "skill" being a folder with instructions (`SKILL.md`) plus optional reference docs, python scripts as tools, and static assets. broskill's whole job is finding those folders, loading them, and turning their scripts into callable tools. That's it. That's the lib.
17
+
18
+ part of the **bro lib family** — small, opinionated, do-one-thing tools built to stop us from writing the same glue code every time we spin up a new agentic project.
19
+
20
+ ## why this exists
21
+
22
+ every time you start a new agent project you end up rebuilding the same boring plumbing: some way to discover "skills," some way to load their instructions, some way to turn a script into a tool the model can call. broskill is that plumbing, ripped out of real projects where we got tired of writing it again and again.
23
+
24
+ **heads up:** this is an opinionated lib. it works one way — SKILL.md + `references/` + `scripts/` + `assets/`, discovered off a `skills/` folder. if that convention fits your project, great, you just saved yourself a bunch of setup. if it doesn't, this probably isn't your lib, and that's fine — go build your own, that's basically how broskill got made in the first place. inspired by real work and real struggle. voilà, here it is.
25
+
26
+ ## install
27
+
28
+ ```bash
29
+ uv pip install -e .
30
+ ```
31
+
32
+ ## the shape of a skill
33
+
34
+ ```
35
+ skills/
36
+ read-file/
37
+ SKILL.md # frontmatter (name, description, version, ...) + instructions
38
+ references/
39
+ script.md # optional deep-dive docs, loaded on demand
40
+ scripts/
41
+ read_file.py # a get_args()-based python script -> becomes a callable tool
42
+ assets/
43
+ template.md # optional static files a skill hands out
44
+ ```
45
+
46
+ `SKILL.md`'s frontmatter has to match the [`Skill`](src/broskill/data_specs/skill.py) dataclass field-for-field (`name`, `description`, `version` required; `tags`, `keywords`, `default`, `status` optional). Want to build one from scratch? There's a skill for that: [`skills/create-skill`](skills/create-skill/SKILL.md).
47
+
48
+ ## quick start
49
+
50
+ ### load skills — `SkillControl`
51
+
52
+ ```python
53
+ from broskill.processing.path import find_root
54
+ from broskill.processing.skill import SkillControl
55
+
56
+ root = find_root() # walks up 'til it finds a skills/ folder
57
+ sc = SkillControl(root=root / "skills")
58
+
59
+ for skill in sc.list_skills():
60
+ print(skill.name, "-", skill.description)
61
+
62
+ body = sc.load_skill("read-file") # -> SKILL.md's instructions, as a str
63
+ ref = sc.load_skill_extension("read-file", "references/script.md")
64
+ ```
65
+
66
+ Full runnable version: [`examples/case1.py`](examples/case1.py).
67
+
68
+ ### turn a script into a tool — `ToolControl`
69
+
70
+ ```python
71
+ from broskill.processing.skill import SkillControl
72
+ from broskill.processing.tool import ToolControl, to_args
73
+ import subprocess, sys
74
+
75
+ sc = SkillControl(root=root / "skills")
76
+ sc.load_skill("read-file")
77
+
78
+ tc = ToolControl(sc)
79
+ tool = tc.load_tool("read-file", "scripts/list_files.py")
80
+ # tool.name, tool.description, tool.args -> ready to hand to an LLM's tool schema
81
+
82
+ result = subprocess.run(
83
+ [sys.executable, str(tool.path), *to_args({"path": "skills/**/*.md"})],
84
+ capture_output=True, text=True,
85
+ )
86
+ ```
87
+
88
+ A script only needs one thing to become a tool: a module-level `get_args()` returning an `argparse.ArgumentParser`. broskill reads that parser and builds the schema for you — no separate tool-definition file to keep in sync. See [`skills/create-skill/references/script.md`](skills/create-skill/references/script.md) for the full contract.
89
+
90
+ Full runnable version: [`examples/case2.py`](examples/case2.py).
91
+
92
+ ## running the tests
93
+
94
+ ```bash
95
+ uv run pytest
96
+ uv run ruff check
97
+ ```
98
+
99
+ ## more
100
+
101
+ - [`VERSIONS.md`](VERSIONS.md) — changelog
102
+ - [`LICENSE`](LICENSE) — MIT
103
+ - [`skills/create-skill`](skills/create-skill/SKILL.md) — a skill that helps you build more skills
104
+ - [`skills/read-file`](skills/read-file/SKILL.md) — the skill used in both examples above, a decent reference for what a real skill looks like
105
+
106
+ stay chill, ship skills. 🤙
@@ -0,0 +1,12 @@
1
+ broskill/__init__.py,sha256=VDKp94tx9alpwa69Ca1ZvHMxMncXAdUoc0z7cMcAlWI,334
2
+ broskill/data_specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ broskill/data_specs/skill.py,sha256=XmZAi4qhqD9pTsv7AeeSoZ9RjibAu3fyS-ZtQ2HzNnc,1375
4
+ broskill/data_specs/tool.py,sha256=dEuafTTAyf7cYlhYcn0Ir--_SjzV3gqA8ebetMQTINs,1739
5
+ broskill/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ broskill/processing/path.py,sha256=UbPo84sS7OcblBx4A3r0h9pN_K6ls4xD1Z4n9AeRwTE,1194
7
+ broskill/processing/skill.py,sha256=Mc0OCvkFkJ3lUuX7SILnc9_VNDa3UX3UtyEj59SR5jg,6462
8
+ broskill/processing/tool.py,sha256=i4iaEkybDHrOiO_I_Xhd5hRrBSi_lVLUVPZ3x309cTc,4066
9
+ broskill-0.1.0.dist-info/METADATA,sha256=y8cni_eMO6rx4qNn4yPsQb3aUoyHVRHG_XxUR7Jq8U8,4629
10
+ broskill-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
11
+ broskill-0.1.0.dist-info/licenses/LICENSE,sha256=n-AwFsgZAbsTeTLfRf93WkKsAgYP_qZUDlNmf5jwZlM,1067
12
+ broskill-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 doublebank
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.