diffjudge 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.
diffjudge/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """diffjudge — AI-powered pull request reviewer."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
diffjudge/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
diffjudge/cli.py ADDED
@@ -0,0 +1,117 @@
1
+ """Command-line interface for diffjudge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from typing import List, Optional
9
+
10
+ from . import __version__, github
11
+ from .llm import LLMError
12
+ from .github import GitHubError
13
+ from .review import DEFAULT_MAX_DIFF_BYTES, format_comment, review_pr
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(
18
+ prog="diffjudge",
19
+ description="AI-powered pull request reviewer.",
20
+ )
21
+ parser.add_argument(
22
+ "--repo",
23
+ required=True,
24
+ metavar="OWNER/NAME",
25
+ help="GitHub repository slug, e.g. octocat/hello-world.",
26
+ )
27
+ parser.add_argument(
28
+ "--pr",
29
+ required=True,
30
+ type=int,
31
+ metavar="N",
32
+ help="pull request number to review.",
33
+ )
34
+ parser.add_argument(
35
+ "--model",
36
+ default=os.environ.get("DIFFJUDGE_MODEL", "gpt-4o-mini"),
37
+ metavar="NAME",
38
+ help="model name (default: gpt-4o-mini, or $DIFFJUDGE_MODEL).",
39
+ )
40
+ parser.add_argument(
41
+ "--base-url",
42
+ default=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
43
+ metavar="URL",
44
+ help="OpenAI-compatible base URL (default: $OPENAI_BASE_URL or OpenAI).",
45
+ )
46
+ parser.add_argument(
47
+ "--api-key",
48
+ default=os.environ.get("OPENAI_API_KEY"),
49
+ metavar="KEY",
50
+ help="LLM API key (default: $OPENAI_API_KEY).",
51
+ )
52
+ parser.add_argument(
53
+ "--github-token",
54
+ default=os.environ.get("GITHUB_TOKEN"),
55
+ metavar="TOKEN",
56
+ help="GitHub token for fetching the diff and posting (default: $GITHUB_TOKEN).",
57
+ )
58
+ parser.add_argument(
59
+ "--max-diff-bytes",
60
+ type=int,
61
+ default=DEFAULT_MAX_DIFF_BYTES,
62
+ metavar="N",
63
+ help="truncate diffs larger than this many bytes (default: {0}).".format(
64
+ DEFAULT_MAX_DIFF_BYTES
65
+ ),
66
+ )
67
+ parser.add_argument(
68
+ "--post",
69
+ action="store_true",
70
+ help="post the review as a PR comment instead of printing it.",
71
+ )
72
+ parser.add_argument(
73
+ "--version",
74
+ action="version",
75
+ version="%(prog)s {0}".format(__version__),
76
+ )
77
+ return parser
78
+
79
+
80
+ def main(argv: Optional[List[str]] = None) -> int:
81
+ parser = build_parser()
82
+ args = parser.parse_args(argv)
83
+
84
+ if not args.api_key:
85
+ parser.exit(2, "diffjudge: no API key (set $OPENAI_API_KEY or --api-key)\n")
86
+ if args.post and not args.github_token:
87
+ parser.exit(2, "diffjudge: --post needs a GitHub token (set $GITHUB_TOKEN)\n")
88
+
89
+ try:
90
+ review = review_pr(
91
+ args.repo,
92
+ args.pr,
93
+ model=args.model,
94
+ api_key=args.api_key,
95
+ base_url=args.base_url,
96
+ github_token=args.github_token,
97
+ max_diff_bytes=args.max_diff_bytes,
98
+ )
99
+ except (LLMError, GitHubError) as exc:
100
+ parser.exit(1, "diffjudge: {0}\n".format(exc))
101
+
102
+ if args.post:
103
+ try:
104
+ url = github.post_comment(
105
+ args.repo, args.pr, format_comment(review), token=args.github_token
106
+ )
107
+ except GitHubError as exc:
108
+ parser.exit(1, "diffjudge: {0}\n".format(exc))
109
+ sys.stderr.write("diffjudge: posted review -> {0}\n".format(url))
110
+ else:
111
+ sys.stdout.write(review.rstrip() + "\n")
112
+
113
+ return 0
114
+
115
+
116
+ if __name__ == "__main__":
117
+ raise SystemExit(main())
diffjudge/github.py ADDED
@@ -0,0 +1,97 @@
1
+ """Tiny GitHub REST client (stdlib only): fetch a PR diff and post a comment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+ from typing import Any, Callable, Optional
9
+
10
+ API_ROOT = "https://api.github.com"
11
+ Opener = Callable[..., Any]
12
+
13
+
14
+ class GitHubError(RuntimeError):
15
+ """Raised when a GitHub API request fails."""
16
+
17
+
18
+ def pr_url(repo: str, pr: int) -> str:
19
+ return "{0}/repos/{1}/pulls/{2}".format(API_ROOT, repo, pr)
20
+
21
+
22
+ def comments_url(repo: str, pr: int) -> str:
23
+ return "{0}/repos/{1}/issues/{2}/comments".format(API_ROOT, repo, pr)
24
+
25
+
26
+ def _request(
27
+ url: str,
28
+ *,
29
+ token: Optional[str] = None,
30
+ method: str = "GET",
31
+ accept: str = "application/vnd.github+json",
32
+ data: Optional[bytes] = None,
33
+ opener: Optional[Opener] = None,
34
+ timeout: int = 60,
35
+ ) -> bytes:
36
+ req = urllib.request.Request(url, data=data, method=method)
37
+ req.add_header("Accept", accept)
38
+ req.add_header("User-Agent", "diffjudge")
39
+ req.add_header("X-GitHub-Api-Version", "2022-11-28")
40
+ if token:
41
+ req.add_header("Authorization", "Bearer " + token)
42
+ if data is not None:
43
+ req.add_header("Content-Type", "application/json")
44
+
45
+ do_open = opener or urllib.request.urlopen
46
+ try:
47
+ with do_open(req, timeout=timeout) as resp:
48
+ return resp.read()
49
+ except urllib.error.HTTPError as exc:
50
+ body = exc.read().decode("utf-8", "replace")
51
+ raise GitHubError(
52
+ "GitHub API error (HTTP {0}): {1}".format(exc.code, body[:300])
53
+ )
54
+ except urllib.error.URLError as exc:
55
+ raise GitHubError("GitHub request failed: {0}".format(exc.reason))
56
+
57
+
58
+ def get_pr_diff(
59
+ repo: str, pr: int, *, token: Optional[str] = None, opener: Optional[Opener] = None
60
+ ) -> str:
61
+ """Return the unified diff for a pull request."""
62
+ raw = _request(
63
+ pr_url(repo, pr),
64
+ token=token,
65
+ accept="application/vnd.github.v3.diff",
66
+ opener=opener,
67
+ )
68
+ return raw.decode("utf-8", "replace")
69
+
70
+
71
+ def get_pr_title(
72
+ repo: str, pr: int, *, token: Optional[str] = None, opener: Optional[Opener] = None
73
+ ) -> str:
74
+ """Return the pull request title (best effort, empty string on absence)."""
75
+ raw = _request(pr_url(repo, pr), token=token, opener=opener)
76
+ data = json.loads(raw.decode("utf-8"))
77
+ return data.get("title", "") or ""
78
+
79
+
80
+ def post_comment(
81
+ repo: str,
82
+ pr: int,
83
+ body: str,
84
+ *,
85
+ token: str,
86
+ opener: Optional[Opener] = None,
87
+ ) -> str:
88
+ """Post a comment on a pull request, returning the created comment URL."""
89
+ payload = json.dumps({"body": body}).encode("utf-8")
90
+ raw = _request(
91
+ comments_url(repo, pr),
92
+ token=token,
93
+ method="POST",
94
+ data=payload,
95
+ opener=opener,
96
+ )
97
+ return json.loads(raw.decode("utf-8")).get("html_url", "")
diffjudge/llm.py ADDED
@@ -0,0 +1,67 @@
1
+ """Minimal client for OpenAI-compatible chat-completion APIs, using only stdlib.
2
+
3
+ Works against the OpenAI API, OpenRouter, a local llama.cpp server, or anything
4
+ else exposing a ``/chat/completions`` endpoint.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import urllib.error
11
+ import urllib.request
12
+ from typing import Any, Callable, Dict, List, Optional
13
+
14
+ Message = Dict[str, str]
15
+ Opener = Callable[..., Any]
16
+
17
+
18
+ class LLMError(RuntimeError):
19
+ """Raised when the LLM request fails or returns an unexpected shape."""
20
+
21
+
22
+ def extract_message(data: Dict[str, Any]) -> str:
23
+ """Pull the assistant message text out of a chat-completions response."""
24
+ try:
25
+ content = data["choices"][0]["message"]["content"]
26
+ except (KeyError, IndexError, TypeError):
27
+ raise LLMError(
28
+ "unexpected LLM response shape: {0}".format(json.dumps(data)[:300])
29
+ )
30
+ if not isinstance(content, str):
31
+ raise LLMError("LLM returned non-text content")
32
+ return content.strip()
33
+
34
+
35
+ def chat_completion(
36
+ messages: List[Message],
37
+ *,
38
+ model: str,
39
+ api_key: str,
40
+ base_url: str = "https://api.openai.com/v1",
41
+ temperature: float = 0.2,
42
+ timeout: int = 120,
43
+ opener: Optional[Opener] = None,
44
+ ) -> str:
45
+ """Call an OpenAI-compatible chat-completions endpoint and return the text."""
46
+ url = base_url.rstrip("/") + "/chat/completions"
47
+ payload = json.dumps(
48
+ {"model": model, "messages": messages, "temperature": temperature}
49
+ ).encode("utf-8")
50
+
51
+ req = urllib.request.Request(url, data=payload, method="POST")
52
+ req.add_header("Content-Type", "application/json")
53
+ req.add_header("Authorization", "Bearer " + api_key)
54
+
55
+ do_open = opener or urllib.request.urlopen
56
+ try:
57
+ with do_open(req, timeout=timeout) as resp:
58
+ raw = resp.read().decode("utf-8")
59
+ except urllib.error.HTTPError as exc:
60
+ body = exc.read().decode("utf-8", "replace")
61
+ raise LLMError(
62
+ "LLM request failed (HTTP {0}): {1}".format(exc.code, body[:500])
63
+ )
64
+ except urllib.error.URLError as exc:
65
+ raise LLMError("LLM request failed: {0}".format(exc.reason))
66
+
67
+ return extract_message(json.loads(raw))
diffjudge/prompts.py ADDED
@@ -0,0 +1,54 @@
1
+ """Prompt construction for the reviewer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, List, Optional
6
+
7
+ Message = Dict[str, str]
8
+
9
+ SYSTEM_PROMPT = (
10
+ "You are a meticulous senior software engineer reviewing a pull request. "
11
+ "Review the unified diff and give a focused, useful review.\n\n"
12
+ "Priorities, in order:\n"
13
+ "1. Correctness bugs and logic errors introduced by the change.\n"
14
+ "2. Security issues (injection, auth, secrets, unsafe input handling).\n"
15
+ "3. Missing edge cases, error handling, or tests for new behavior.\n"
16
+ "4. Clear, concrete improvements.\n\n"
17
+ "Rules:\n"
18
+ "- Be specific: reference file names and, where you can, the relevant lines.\n"
19
+ "- Do not nitpick formatting or style unless it affects correctness.\n"
20
+ "- Do not invent issues. If the change looks good, say so plainly.\n"
21
+ "- Keep it concise. No filler.\n\n"
22
+ "Respond in GitHub-flavored markdown with exactly these sections:\n"
23
+ "## Summary\n"
24
+ "A two or three sentence overview of what the PR does.\n\n"
25
+ "## Findings\n"
26
+ "A bullet list of concrete issues, each prefixed with a severity "
27
+ "(**blocker** / **major** / **minor** / **nit**). Write 'No significant "
28
+ "issues found.' if there are none.\n\n"
29
+ "## Suggestions\n"
30
+ "Optional improvements that are nice-to-have, or 'None.'"
31
+ )
32
+
33
+
34
+ def build_messages(
35
+ diff: str, *, title: Optional[str] = None, truncated: bool = False
36
+ ) -> List[Message]:
37
+ """Build the chat messages for reviewing a diff."""
38
+ header = "Pull request"
39
+ if title:
40
+ header += ': "{0}"'.format(title)
41
+
42
+ note = ""
43
+ if truncated:
44
+ note = (
45
+ "\n\nNote: the diff was truncated because it is large. Review what is "
46
+ "shown and say so if you cannot assess the whole change."
47
+ )
48
+
49
+ user = "{0}\n\nUnified diff:\n\n```diff\n{1}\n```{2}".format(header, diff, note)
50
+
51
+ return [
52
+ {"role": "system", "content": SYSTEM_PROMPT},
53
+ {"role": "user", "content": user},
54
+ ]
diffjudge/review.py ADDED
@@ -0,0 +1,50 @@
1
+ """Orchestration: fetch a PR diff, ask the model, return the review markdown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional, Tuple
6
+
7
+ from . import github, llm, prompts
8
+
9
+ DEFAULT_MAX_DIFF_BYTES = 60_000
10
+ COMMENT_HEADER = "## diffjudge review\n\n"
11
+
12
+
13
+ def truncate_diff(diff: str, max_bytes: int) -> Tuple[str, bool]:
14
+ """Truncate a diff to at most ``max_bytes`` UTF-8 bytes.
15
+
16
+ Returns the (possibly shortened) text and whether truncation happened.
17
+ """
18
+ encoded = diff.encode("utf-8")
19
+ if len(encoded) <= max_bytes:
20
+ return diff, False
21
+ clipped = encoded[:max_bytes].decode("utf-8", "ignore")
22
+ return clipped, True
23
+
24
+
25
+ def review_pr(
26
+ repo: str,
27
+ pr: int,
28
+ *,
29
+ model: str,
30
+ api_key: str,
31
+ base_url: str = "https://api.openai.com/v1",
32
+ github_token: Optional[str] = None,
33
+ max_diff_bytes: int = DEFAULT_MAX_DIFF_BYTES,
34
+ ) -> str:
35
+ """Produce a markdown review for a pull request."""
36
+ diff = github.get_pr_diff(repo, pr, token=github_token)
37
+ if not diff.strip():
38
+ return "_diffjudge: this pull request has an empty diff, nothing to review._"
39
+
40
+ title = github.get_pr_title(repo, pr, token=github_token)
41
+ text, truncated = truncate_diff(diff, max_diff_bytes)
42
+ messages = prompts.build_messages(text, title=title, truncated=truncated)
43
+ return llm.chat_completion(
44
+ messages, model=model, api_key=api_key, base_url=base_url
45
+ )
46
+
47
+
48
+ def format_comment(review: str) -> str:
49
+ """Wrap a review in the comment header used when posting to a PR."""
50
+ return COMMENT_HEADER + review.strip() + "\n"
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffjudge
3
+ Version: 0.1.0
4
+ Summary: AI-powered pull request reviewer. Reviews diffs with any OpenAI-compatible model and posts the review. Zero dependencies.
5
+ Project-URL: Homepage, https://github.com/Sev7nOfNine/diffjudge
6
+ Project-URL: Repository, https://github.com/Sev7nOfNine/diffjudge
7
+ Project-URL: Issues, https://github.com/Sev7nOfNine/diffjudge/issues
8
+ Author: Seven Of Nine
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,automation,cli,code-review,github,github-action,llm,maintainer,openai,pull-request
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Classifier: Topic :: Software Development :: Version Control :: Git
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.8
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest>=7; extra == 'test'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # diffjudge
28
+
29
+ **AI-powered pull request reviewer — with zero dependencies.**
30
+
31
+ `diffjudge` fetches a pull request's diff, asks a language model to review it, and
32
+ either prints the review or posts it back as a PR comment. It works with any
33
+ OpenAI-compatible API: the OpenAI platform, OpenRouter, or a local
34
+ `llama.cpp` / Ollama server. The whole thing is built on the Python standard
35
+ library — no `requests`, no SDKs, nothing to audit but a few hundred lines.
36
+
37
+ Use it as a CLI for ad-hoc reviews, or drop the bundled GitHub Action into a
38
+ workflow to get an automatic review on every pull request.
39
+
40
+ ## What a review looks like
41
+
42
+ ```markdown
43
+ ## diffjudge review
44
+
45
+ ## Summary
46
+ Adds retry handling to the upload client and a backoff helper.
47
+
48
+ ## Findings
49
+ - **major** `client.py`: the retry loop never breaks on a 4xx response, so a bad
50
+ request is retried 5 times before failing.
51
+ - **minor** `client.py`: `backoff()` ignores the `jitter` argument.
52
+
53
+ ## Suggestions
54
+ - Consider adding a test for the 4xx-no-retry path.
55
+ ```
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install diffjudge
61
+ # or, for an isolated CLI:
62
+ pipx install diffjudge
63
+ ```
64
+
65
+ Requires Python 3.8+.
66
+
67
+ ## CLI usage
68
+
69
+ ```bash
70
+ export OPENAI_API_KEY=sk-...
71
+ export GITHUB_TOKEN=ghp_... # only needed to fetch private diffs or to --post
72
+
73
+ # Print a review to your terminal
74
+ diffjudge --repo octocat/hello-world --pr 42
75
+
76
+ # Post the review as a comment on the PR
77
+ diffjudge --repo octocat/hello-world --pr 42 --post
78
+
79
+ # Use a different model or provider
80
+ diffjudge --repo octocat/hello-world --pr 42 --model gpt-4o
81
+ diffjudge --repo me/proj --pr 7 --base-url http://localhost:8080/v1 --model local-model
82
+ ```
83
+
84
+ ### Options
85
+
86
+ | Flag | Description |
87
+ | --- | --- |
88
+ | `--repo OWNER/NAME` | Repository to review (required). |
89
+ | `--pr N` | Pull request number (required). |
90
+ | `--model NAME` | Model name (default `gpt-4o-mini`, or `$DIFFJUDGE_MODEL`). |
91
+ | `--base-url URL` | OpenAI-compatible base URL (default `$OPENAI_BASE_URL` or OpenAI). |
92
+ | `--api-key KEY` | LLM API key (default `$OPENAI_API_KEY`). |
93
+ | `--github-token TOKEN` | GitHub token (default `$GITHUB_TOKEN`). |
94
+ | `--max-diff-bytes N` | Truncate diffs larger than this (default 60000). |
95
+ | `--post` | Post the review as a PR comment instead of printing it. |
96
+
97
+ ## GitHub Action
98
+
99
+ Add a workflow that reviews every pull request. Store your provider key as a
100
+ repository secret (e.g. `OPENAI_API_KEY`):
101
+
102
+ ```yaml
103
+ name: diffjudge
104
+ on:
105
+ pull_request:
106
+
107
+ permissions:
108
+ contents: read
109
+ pull-requests: write
110
+
111
+ jobs:
112
+ review:
113
+ runs-on: ubuntu-latest
114
+ steps:
115
+ - uses: Sev7nOfNine/diffjudge@v0.1.0
116
+ with:
117
+ api-key: ${{ secrets.OPENAI_API_KEY }}
118
+ model: gpt-4o-mini
119
+ ```
120
+
121
+ Point `base-url` at any OpenAI-compatible endpoint to use a different provider or
122
+ a self-hosted model.
123
+
124
+ ## Privacy note
125
+
126
+ `diffjudge` sends the pull request diff to whatever provider you configure. Only
127
+ point it at providers you trust with your code, and prefer a self-hosted model
128
+ (`--base-url`) for private repositories where that matters.
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ pip install -e ".[test]"
134
+ python -m pytest
135
+ ```
136
+
137
+ The HTTP layers accept an injectable opener, so the whole suite runs offline
138
+ with no real API calls.
139
+
140
+ ## License
141
+
142
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,12 @@
1
+ diffjudge/__init__.py,sha256=jLI64M6OleZILOtN76jMhuqSnO7n50REF8CiA9v1OCI,104
2
+ diffjudge/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
3
+ diffjudge/cli.py,sha256=b9bjNjl9__OdLwuD8XGh-m6z20JApMKaD1Ybq_-Bv4Y,3437
4
+ diffjudge/github.py,sha256=ai075cduqajw2TusVn6-48gqLxd1oi2H21HcYHg0Hp0,2848
5
+ diffjudge/llm.py,sha256=3cqvwLpp_3z7cCxtM0m09nT31nLS92PbQ_DQZy69DlY,2199
6
+ diffjudge/prompts.py,sha256=l372yd7Qc-SxLZPTH69x7EDm6ox-bRGSkIjb2a_arvc,2025
7
+ diffjudge/review.py,sha256=upUT1GD-c6CF2SG8WWQit55s7RqY9_-jNJbsj5I0xOA,1589
8
+ diffjudge-0.1.0.dist-info/METADATA,sha256=W-qWjQjj7BnuzGK8Z7PK3lYhTSbZ18B503wnHpWTqSg,4381
9
+ diffjudge-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ diffjudge-0.1.0.dist-info/entry_points.txt,sha256=7_Ji9ft4HAH4d0LbyFzyQoLeNd3ENob_Ag45H_lLodQ,49
11
+ diffjudge-0.1.0.dist-info/licenses/LICENSE,sha256=z_MXotUe6DP_vu70fPgxIdzHdFq5mHzyHp7p3j22NRA,1070
12
+ diffjudge-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ diffjudge = diffjudge.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seven Of Nine
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.