jevq 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.
jevq/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("jevq")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0+unknown"
jevq/cli.py ADDED
@@ -0,0 +1,297 @@
1
+ """Command-line entry point for jevq."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import math
8
+ import os
9
+ import sys
10
+ from collections.abc import Callable, Iterator, Mapping
11
+ from typing import Any, BinaryIO, Protocol, TextIO
12
+
13
+ from jevq.client import DEFAULT_MODEL, DEFAULT_URL, JevqAPIError, SystemOneClient
14
+
15
+ DESCRIPTION = """\
16
+ Filter JSON values from stdin (JSONL from `jq -c`) with a System One noul.
17
+ One System One call per value. QUESTION is a yes/no claim about the current
18
+ object, not a search query."""
19
+
20
+ EPILOG = """\
21
+ environment:
22
+ TYPESAFE_API_KEY API key (required except --pass)
23
+ JEV_MODEL default model (default jev-1.13.0)
24
+ JEV_BASE_URL API base URL (default https://api.typesafe.ai/v1/systemone)
25
+ JEV_THRESHOLD default threshold (default 0.5)
26
+
27
+ examples:
28
+ jq -c '.[] | select(.status == "open")' tickets.json | jevq "the customer is asking for a refund" | jq -c '{id, subject}'
29
+ jq -c '.[]' tickets.json | jevq --score "the customer is angry" | jq -c 'select(.score >= 0.8) | .value.id'
30
+ jq -c '.[]' tickets.json | jevq --pass"""
31
+
32
+
33
+ def build_parser() -> argparse.ArgumentParser:
34
+ parser = argparse.ArgumentParser(
35
+ prog="jevq",
36
+ usage="jevq [options] QUESTION",
37
+ description=DESCRIPTION,
38
+ epilog=EPILOG,
39
+ formatter_class=argparse.RawDescriptionHelpFormatter,
40
+ )
41
+ parser.add_argument(
42
+ "question",
43
+ nargs="?",
44
+ metavar="QUESTION",
45
+ help="yes/no claim about the current object",
46
+ )
47
+ mode = parser.add_mutually_exclusive_group()
48
+ mode.add_argument(
49
+ "--score",
50
+ action="store_true",
51
+ help='emit every row as {"score": <noul>, "value": <original>}',
52
+ )
53
+ mode.add_argument(
54
+ "--pass",
55
+ dest="pass_",
56
+ action="store_true",
57
+ help="emit every input value unchanged. No API call. No key required",
58
+ )
59
+ parser.add_argument(
60
+ "-t",
61
+ "--threshold",
62
+ metavar="N",
63
+ default=None,
64
+ help="cutoff in [0,1]. Default 0.5 or $JEV_THRESHOLD",
65
+ )
66
+ parser.add_argument(
67
+ "-f",
68
+ "--fields",
69
+ metavar="a,b",
70
+ default=None,
71
+ help="slim state only. Output is still the full original value",
72
+ )
73
+ parser.add_argument(
74
+ "--model",
75
+ metavar="NAME",
76
+ default=None,
77
+ help="default jev-1.13.0 (pinned) or $JEV_MODEL",
78
+ )
79
+ parser.add_argument(
80
+ "-v",
81
+ "--verbose",
82
+ action="store_true",
83
+ help="print end-of-run counts (read/emitted) and the answering model to stderr",
84
+ )
85
+ return parser
86
+
87
+
88
+ class InputError(Exception):
89
+ """A non-blank input line that is not exactly one JSON value."""
90
+
91
+ def __init__(self, line_no: int, message: str) -> None:
92
+ super().__init__(f"line {line_no}: invalid JSON: {message}")
93
+ self.line_no = line_no
94
+ self.message = message
95
+
96
+
97
+ def iter_values(stream: BinaryIO) -> Iterator[tuple[int, bytes, Any]]:
98
+ """Yield (line_no, raw_bytes, value) per non-blank JSONL line, lazily."""
99
+ for line_no, line in enumerate(stream, start=1):
100
+ raw = line.strip()
101
+ if not raw:
102
+ continue
103
+ try:
104
+ value = json.loads(raw)
105
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
106
+ raise InputError(line_no, str(exc)) from None
107
+ yield line_no, raw, value
108
+
109
+
110
+ def emit(stdout: BinaryIO, raw: bytes) -> None:
111
+ stdout.write(raw + b"\n")
112
+ stdout.flush()
113
+
114
+
115
+ def format_score_line(score: float, raw: bytes) -> bytes:
116
+ """The only wrap: compact ``{"score":<noul>,"value":<raw>}`` plus newline."""
117
+ return b'{"score":' + json.dumps(float(score)).encode() + b',"value":' + raw + b"}\n"
118
+
119
+
120
+ def report(stderr: TextIO, read: int, emitted: int) -> None:
121
+ stderr.write(f"jevq: read {read}, emitted {emitted}\n")
122
+
123
+
124
+ def report_model(stderr: TextIO, client: object) -> None:
125
+ """Name the model(s) that answered, once any System One response succeeded."""
126
+ models = getattr(client, "answered_models", None)
127
+ if models is None or not getattr(client, "responses", 0):
128
+ return
129
+ stderr.write(f"jevq: model: {', '.join(models) if models else 'not reported'}\n")
130
+
131
+
132
+ class UsageError(Exception):
133
+ """Bad flags, environment or missing key; exit 2 before reading stdin."""
134
+
135
+
136
+ class NoulClient(Protocol):
137
+ def noul(self, state: Any, question: str) -> float: ...
138
+
139
+
140
+ ClientFactory = Callable[[str, str, str], NoulClient]
141
+
142
+
143
+ def resolve_threshold(arg: str | None, env: Mapping[str, str]) -> float:
144
+ raw = arg if arg is not None else (env.get("JEV_THRESHOLD") or None)
145
+ if raw is None:
146
+ return 0.5
147
+ try:
148
+ value = float(raw)
149
+ except ValueError:
150
+ value = math.nan
151
+ if not math.isfinite(value) or not 0.0 <= value <= 1.0:
152
+ raise UsageError("threshold must be a number in [0, 1]")
153
+ return value
154
+
155
+
156
+ def parse_fields(arg: str | None) -> list[str] | None:
157
+ if arg is None:
158
+ return None
159
+ names = [name.strip() for name in arg.split(",")]
160
+ names = [name for name in names if name]
161
+ if not names:
162
+ raise UsageError("--fields needs at least one key name")
163
+ return names
164
+
165
+
166
+ def build_state(value: Any, fields: list[str] | None) -> Any:
167
+ """The state sent to System One; emitted output is always the raw line."""
168
+ if not isinstance(value, dict):
169
+ return {"value": value}
170
+ if fields is None:
171
+ return value
172
+ return {k: value[k] for k in fields if k in value}
173
+
174
+
175
+ def _default_client_factory(api_key: str, model: str, url: str) -> NoulClient:
176
+ return SystemOneClient(api_key, model, url)
177
+
178
+
179
+ def run(
180
+ argv: list[str] | None,
181
+ stdin: BinaryIO,
182
+ stdout: BinaryIO,
183
+ stderr: TextIO,
184
+ env: Mapping[str, str],
185
+ *,
186
+ client_factory: ClientFactory | None = None,
187
+ ) -> int:
188
+ parser = build_parser()
189
+ try:
190
+ args = parser.parse_args(argv)
191
+ except SystemExit as exc:
192
+ code = exc.code
193
+ return code if isinstance(code, int) else (0 if code is None else 2)
194
+ if args.pass_:
195
+ return _run_pass(stdin, stdout, stderr, verbose=args.verbose)
196
+ if not (args.question or "").strip():
197
+ stderr.write("jevq: QUESTION is required unless --pass\n")
198
+ return 2
199
+ try:
200
+ threshold = resolve_threshold(args.threshold, env)
201
+ fields = parse_fields(args.fields)
202
+ api_key = (env.get("TYPESAFE_API_KEY") or "").strip()
203
+ if not api_key:
204
+ raise UsageError("TYPESAFE_API_KEY is not set")
205
+ except UsageError as exc:
206
+ stderr.write(f"jevq: {exc}\n")
207
+ return 2
208
+ model = args.model or env.get("JEV_MODEL") or DEFAULT_MODEL
209
+ url = env.get("JEV_BASE_URL") or DEFAULT_URL
210
+ client = (client_factory or _default_client_factory)(api_key, model, url)
211
+ try:
212
+ return _run_filter(
213
+ client,
214
+ args.question,
215
+ threshold,
216
+ fields,
217
+ stdin,
218
+ stdout,
219
+ stderr,
220
+ score_mode=args.score,
221
+ verbose=args.verbose,
222
+ )
223
+ finally:
224
+ close = getattr(client, "close", None)
225
+ if close is not None:
226
+ close()
227
+
228
+
229
+ def _run_filter(
230
+ client: NoulClient,
231
+ question: str,
232
+ threshold: float,
233
+ fields: list[str] | None,
234
+ stdin: BinaryIO,
235
+ stdout: BinaryIO,
236
+ stderr: TextIO,
237
+ *,
238
+ score_mode: bool = False,
239
+ verbose: bool = False,
240
+ ) -> int:
241
+ read = emitted = 0
242
+ try:
243
+ for line_no, raw, value in iter_values(stdin):
244
+ read += 1
245
+ try:
246
+ score = client.noul(build_state(value, fields), question)
247
+ except JevqAPIError as exc:
248
+ stderr.write(f"jevq: line {line_no}: API error: {exc}\n")
249
+ if verbose:
250
+ report(stderr, read, emitted)
251
+ report_model(stderr, client)
252
+ return 1
253
+ if score_mode:
254
+ stdout.write(format_score_line(score, raw))
255
+ stdout.flush()
256
+ emitted += 1
257
+ elif score >= threshold:
258
+ emit(stdout, raw)
259
+ emitted += 1
260
+ except InputError as exc:
261
+ stderr.write(f"jevq: {exc}\n")
262
+ if verbose:
263
+ report(stderr, read, emitted)
264
+ report_model(stderr, client)
265
+ return 1
266
+ if verbose:
267
+ report(stderr, read, emitted)
268
+ report_model(stderr, client)
269
+ return 0
270
+
271
+
272
+ def _run_pass(stdin: BinaryIO, stdout: BinaryIO, stderr: TextIO, *, verbose: bool = False) -> int:
273
+ read = emitted = 0
274
+ try:
275
+ for _line_no, raw, _value in iter_values(stdin):
276
+ read += 1
277
+ emit(stdout, raw)
278
+ emitted += 1
279
+ except InputError as exc:
280
+ stderr.write(f"jevq: {exc}\n")
281
+ if verbose:
282
+ report(stderr, read, emitted)
283
+ return 1
284
+ if verbose:
285
+ report(stderr, read, emitted)
286
+ return 0
287
+
288
+
289
+ def main() -> None:
290
+ try:
291
+ code = run(sys.argv[1:], sys.stdin.buffer, sys.stdout.buffer, sys.stderr, os.environ)
292
+ except BrokenPipeError:
293
+ # Downstream closed early (e.g. `| head -1`): silence the flush at exit.
294
+ devnull = os.open(os.devnull, os.O_WRONLY)
295
+ os.dup2(devnull, sys.stdout.fileno())
296
+ sys.exit(0)
297
+ sys.exit(code)
jevq/client.py ADDED
@@ -0,0 +1,137 @@
1
+ """httpx client for the TypeSafe System One ``noul`` endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import time
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from jevq import __version__
13
+
14
+ DEFAULT_URL = "https://api.typesafe.ai/v1/systemone"
15
+ DEFAULT_MODEL = "jev-1.13.0"
16
+
17
+ _MAX_RETRY_AFTER = 10.0
18
+ _BASE_BACKOFF = 0.5
19
+
20
+
21
+ class JevqAPIError(Exception):
22
+ """System One call failed; never a "no" answer."""
23
+
24
+
25
+ def _retry_after(resp: httpx.Response | None) -> float | None:
26
+ if resp is None:
27
+ return None
28
+ raw = resp.headers.get("Retry-After")
29
+ if raw is None:
30
+ return None
31
+ try:
32
+ value = float(raw)
33
+ except ValueError:
34
+ return None
35
+ if not math.isfinite(value) or value < 0:
36
+ return None
37
+ return min(value, _MAX_RETRY_AFTER)
38
+
39
+
40
+ def _is_retryable(status: int) -> bool:
41
+ return status == 429 or 500 <= status <= 599
42
+
43
+
44
+ class SystemOneClient:
45
+ """Ask System One one ``noul`` question about one state per call."""
46
+
47
+ def __init__(
48
+ self,
49
+ api_key: str,
50
+ model: str = DEFAULT_MODEL,
51
+ url: str = DEFAULT_URL,
52
+ *,
53
+ timeout: float = 30.0,
54
+ max_retries: int = 3,
55
+ transport: httpx.BaseTransport | None = None,
56
+ sleep: Callable[[float], None] = time.sleep,
57
+ ) -> None:
58
+ self.model = model
59
+ self.url = url
60
+ self.max_retries = max_retries
61
+ self._sleep = sleep
62
+ self.responses = 0
63
+ self.answered_models: list[str] = []
64
+ self._http = httpx.Client(
65
+ timeout=timeout,
66
+ transport=transport,
67
+ headers={
68
+ "Authorization": f"Bearer {api_key}",
69
+ "User-Agent": f"jevq/{__version__}",
70
+ },
71
+ )
72
+
73
+ def build_body(self, state: Any, question: str) -> dict:
74
+ return {
75
+ "model": self.model,
76
+ "state": state,
77
+ "questions": {"q": {"type": "noul", "instructions": question}},
78
+ }
79
+
80
+ def noul(self, state: Any, question: str) -> float:
81
+ body = self.build_body(state, question)
82
+ attempts = self.max_retries + 1
83
+ last_error = ""
84
+ for attempt in range(attempts):
85
+ resp: httpx.Response | None = None
86
+ try:
87
+ resp = self._http.post(self.url, json=body)
88
+ except httpx.TransportError as exc:
89
+ last_error = f"transport error: {type(exc).__name__}"
90
+ else:
91
+ if resp.is_success:
92
+ score = _parse_noul(resp)
93
+ self._record_model(resp)
94
+ return score
95
+ if not _is_retryable(resp.status_code):
96
+ raise JevqAPIError(f"HTTP {resp.status_code}: {resp.text[:200]}")
97
+ last_error = f"HTTP {resp.status_code}"
98
+ if attempt < self.max_retries:
99
+ delay = _retry_after(resp)
100
+ if delay is None:
101
+ delay = _BASE_BACKOFF * 2**attempt
102
+ self._sleep(delay)
103
+ raise JevqAPIError(f"{last_error} after {attempts} attempts")
104
+
105
+ def _record_model(self, resp: httpx.Response) -> None:
106
+ """Note the answering model from the response's top-level ``model``."""
107
+ self.responses += 1
108
+ model = resp.json().get("model")
109
+ if isinstance(model, str) and model and model not in self.answered_models:
110
+ self.answered_models.append(model)
111
+
112
+ def close(self) -> None:
113
+ self._http.close()
114
+
115
+ def __enter__(self) -> SystemOneClient:
116
+ return self
117
+
118
+ def __exit__(self, *exc_info: object) -> None:
119
+ self.close()
120
+
121
+
122
+ def _parse_noul(resp: httpx.Response) -> float:
123
+ try:
124
+ value = resp.json()["answers"]["q"]["noul"]
125
+ except ValueError as exc:
126
+ raise JevqAPIError(f"invalid response: not JSON ({exc})") from None
127
+ except (KeyError, TypeError, IndexError):
128
+ raise JevqAPIError("invalid response: missing answers.q.noul") from None
129
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
130
+ raise JevqAPIError(f"invalid response: noul is {type(value).__name__}, not a number")
131
+ try:
132
+ score = float(value)
133
+ except OverflowError:
134
+ score = math.inf
135
+ if not math.isfinite(score):
136
+ raise JevqAPIError("invalid response: noul is not finite")
137
+ return score
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.5
2
+ Name: jevq
3
+ Version: 0.1.0
4
+ Summary: jq-compatible System One noul filter
5
+ Project-URL: Homepage, https://github.com/who/jevq
6
+ Project-URL: Repository, https://github.com/who/jevq
7
+ Project-URL: Issues, https://github.com/who/jevq/issues
8
+ Author-email: who <githubwho@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,filter,jq,json,jsonl,llm,typesafe
12
+ Classifier: Development Status :: 3 - Alpha
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.12
19
+ Classifier: Topic :: Text Processing :: Filters
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: httpx>=0.27
23
+ Description-Content-Type: text/markdown
24
+
25
+ # jevq
26
+
27
+ [![CI](https://github.com/who/jevq/actions/workflows/ci.yml/badge.svg)](https://github.com/who/jevq/actions/workflows/ci.yml)
28
+
29
+ `jq | jevq | jq`. jq handles structure; jevq reads one JSON value per line from stdin, asks
30
+ Jev (a TypeSafe System One model) a yes/no question about each, and passes through the yeses.
31
+ QUESTION is a yes/no claim about the current value, not a search query.
32
+
33
+ ![jevq demo: filtering support tickets to open refund requests](https://raw.githubusercontent.com/who/jevq/main/docs/assets/jevq-demo.gif)
34
+
35
+ Video: [docs/assets/jevq-demo.mp4](https://github.com/who/jevq/blob/main/docs/assets/jevq-demo.mp4)
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ uv tool install jevq # from PyPI (or: pipx install jevq)
41
+ uv tool install --editable . # from a source checkout
42
+ export TYPESAFE_API_KEY=...
43
+ ```
44
+
45
+ `scripts/reinstall-cli.sh` forces a rebuild (`--dry-run`, `--no-editable`, `--skip-smoke`).
46
+
47
+ ## Options
48
+
49
+ `jevq [options] QUESTION` reads JSONL on stdin. QUESTION is required unless `--pass`.
50
+
51
+ | Flag | Default | Description |
52
+ | --- | --- | --- |
53
+ | `QUESTION` | none | Yes/no claim about each value |
54
+ | `-t N`, `--threshold N` | `0.5` (`$JEV_THRESHOLD`) | Keep values scoring at least N |
55
+ | `-f a,b`, `--fields a,b` | whole value | Send only these keys; output stays the full value |
56
+ | `--score` | off | Emit every value as `{"score":<noul>,"value":<original>}` |
57
+ | `--pass` | off | Emit every value unchanged; no API call, no key |
58
+ | `--model NAME` | `jev-1.13.0` (`$JEV_MODEL`) | Model to ask |
59
+ | `-v`, `--verbose` | off | Print counts and the answering model to stderr |
60
+ | `-h`, `--help` | | Show help and exit |
61
+
62
+ `--score` and `--pass` are mutually exclusive. A flag beats its env var.
63
+
64
+ ## Environment
65
+
66
+ | Variable | Default | Description |
67
+ | --- | --- | --- |
68
+ | `TYPESAFE_API_KEY` | none | API key; required except with `--pass` |
69
+ | `JEV_MODEL` | `jev-1.13.0` | Default model |
70
+ | `JEV_BASE_URL` | `https://api.typesafe.ai/v1/systemone` | API base URL |
71
+ | `JEV_THRESHOLD` | `0.5` | Default threshold |
72
+
73
+ ## Use cases
74
+
75
+ ```bash
76
+ jq -c '.[] | select(.status == "open")' samples/tickets.json |
77
+ jevq "the customer is asking for a refund" |
78
+ jq -c '{id, subject}'
79
+ ```
80
+
81
+ Open tickets asking for money back, printed as `{id, subject}`.
82
+
83
+ ```bash
84
+ jq -c 'select(.level == "error")' samples/app.ndjson |
85
+ jevq "this error is caused by a network timeout, not a bug in our code"
86
+ ```
87
+
88
+ Error log lines that look like a timeout rather than a code bug.
89
+
90
+ ```bash
91
+ gh api 'repos/itchyny/gojq/issues?state=open&per_page=100' |
92
+ jq -c '.[] | select(.pull_request | not) | {number, title, body}' |
93
+ jevq --score "reports a crash or wrong output, not a feature request" |
94
+ jq -rs 'sort_by(-.score) | .[:5][] | .value | "\(.number)\t\(.title)"'
95
+ ```
96
+
97
+ The five most bug-like open issues, as `number<TAB>title`.
98
+
99
+ More examples: [docs/examples.md](https://github.com/who/jevq/blob/main/docs/examples.md)
100
+
101
+ Full contract (output rules, exit codes, HTTP, retries): [docs/jevq.md](https://github.com/who/jevq/blob/main/docs/jevq.md)
102
+
103
+ ## Testing
104
+
105
+ ```bash
106
+ uv run pytest -q
107
+ bash tests/samples/run.sh
108
+ ```
109
+
110
+ `JEVQ_LIVE=1 TYPESAFE_API_KEY=... bash tests/samples/run.sh` runs the sample pipes live.
111
+
112
+ Not supported in v1: `--path`, a `jev()` builtin, concurrency, caching.
@@ -0,0 +1,8 @@
1
+ jevq/__init__.py,sha256=Wnlf-_59Qwn66xZUiA2oyHLtF7izD-gZyHiaCnvG6Uo,164
2
+ jevq/cli.py,sha256=33Ugf3KDG4RGu14qN1zaLVE8Nze6xDSGi47A4Uo6rTY,9226
3
+ jevq/client.py,sha256=qIzzG_MMjMdp7aBs4vCePkJehVoH2J7gDVpgkC9pNIw,4348
4
+ jevq-0.1.0.dist-info/METADATA,sha256=b88CHQgeltL7hqrKbWasf6CoUmyNpSARjTy9zjl4KnM,4116
5
+ jevq-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ jevq-0.1.0.dist-info/entry_points.txt,sha256=Ce5-FLbUDQsGnzPZm-JlOnIXCwz2xISYoR1R8_zEAyo,39
7
+ jevq-0.1.0.dist-info/licenses/LICENSE,sha256=7g1B9RFIOYnUT-dx6FnYMn6HEA9bqDDEVLtji__nRLE,1060
8
+ jevq-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jevq = jevq.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 who
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.