tmpkit 1.0.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,506 @@
1
+ Metadata-Version: 2.5
2
+ Name: tmpkit
3
+ Version: 1.0.0
4
+ Summary: Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes, async support, and zero dependencies.
5
+ Project-URL: Homepage, https://github.com/MathiasPaulenko/tmpkit
6
+ Project-URL: Repository, https://github.com/MathiasPaulenko/tmpkit
7
+ Project-URL: Issues, https://github.com/MathiasPaulenko/tmpkit/issues
8
+ Project-URL: Changelog, https://github.com/MathiasPaulenko/tmpkit/blob/main/CHANGELOG.md
9
+ Author: Mathias Paulenko
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: async,atomic,cleanup,context manager,filesystem,temp,tempfile,temporary,tmp
13
+ Classifier: Development Status :: 4 - Beta
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.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: System :: Filesystems
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Provides-Extra: dev
26
+ Requires-Dist: build>=1.0; extra == 'dev'
27
+ Requires-Dist: mypy>=1.11.0; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # tmpkit
35
+
36
+ Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes, async support, and zero dependencies.
37
+
38
+ [![CI](https://github.com/MathiasPaulenko/tmpkit/actions/workflows/ci.yml/badge.svg)](https://github.com/MathiasPaulenko/tmpkit/actions/workflows/ci.yml)
39
+ [![PyPI](https://img.shields.io/pypi/v/tmpkit)](https://pypi.org/project/tmpkit/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/tmpkit)](https://pypi.org/project/tmpkit/)
41
+ [![License](https://img.shields.io/pypi/l/tmpkit)](https://github.com/MathiasPaulenko/tmpkit/blob/main/LICENSE)
42
+ [![Coverage](https://img.shields.io/codecov/c/github/MathiasPaulenko/tmpkit)](https://codecov.io/gh/MathiasPaulenko/tmpkit)
43
+
44
+ ---
45
+
46
+ ## Table of Contents
47
+
48
+ - [Why tmpkit?](#why-tmpkit)
49
+ - [Installation](#installation)
50
+ - [Quick Start](#quick-start)
51
+ - [Features](#features)
52
+ - [API Reference](#api-reference)
53
+ - [`temp_file()`](#temp_file)
54
+ - [`temp_dir()`](#temp_dir)
55
+ - [`atomic_write()`](#atomic_write)
56
+ - [`@temp_dir()` Decorator](#temp_dir-decorator)
57
+ - [`@temp_file()` Decorator](#temp_file-decorator)
58
+ - [`temp_registry`](#temp_registry)
59
+ - [Async API](#async-api)
60
+ - [Environment Variables](#environment-variables)
61
+ - [Keep Control: Precedence](#keep-control-precedence)
62
+ - [Cleanup Hooks](#cleanup-hooks)
63
+ - [Comparison](#comparison)
64
+ - [Contributing](#contributing)
65
+ - [Changelog](#changelog)
66
+ - [Acknowledgements](#acknowledgements)
67
+ - [License](#license)
68
+
69
+ ---
70
+
71
+ ## Why tmpkit?
72
+
73
+ Python's `tempfile` gives you the pieces but forces you to write cleanup boilerplate every time. tmpkit wraps it in ergonomic context managers that **guarantee cleanup** — with features nobody else offers.
74
+
75
+ ```python
76
+ # stdlib — verbose, easy to forget cleanup
77
+ import os, tempfile, shutil
78
+ tmpdir = tempfile.mkdtemp()
79
+ try:
80
+ with open(os.path.join(tmpdir, "data.csv"), "w") as f:
81
+ f.write(data)
82
+ finally:
83
+ shutil.rmtree(tmpdir, ignore_errors=True)
84
+
85
+ # tmpkit — one line, always cleans up
86
+ from tmpkit import temp_file
87
+ with temp_file(suffix=".csv") as f:
88
+ f.write(data)
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Installation
94
+
95
+ ```bash
96
+ pip install tmpkit
97
+ ```
98
+
99
+ **Requirements:** Python >= 3.11. Zero runtime dependencies.
100
+
101
+ For development:
102
+
103
+ ```bash
104
+ pip install -e ".[dev]"
105
+ ```
106
+
107
+ This installs `pytest`, `pytest-asyncio`, `pytest-cov`, `ruff`, `mypy`, and `build`.
108
+
109
+ ---
110
+
111
+ ## Quick Start
112
+
113
+ ```python
114
+ from tmpkit import temp_file, temp_dir, atomic_write, async_temp_file
115
+
116
+ # Temp file
117
+ with temp_file(suffix=".csv", prefix="myapp_") as f:
118
+ f.write(data)
119
+ # deleted on exit
120
+
121
+ # Temp dir with auto-chdir
122
+ with temp_dir(cwd=True) as d:
123
+ (d / "output.txt").write_text("hello")
124
+ # cwd restored, dir removed on exit
125
+
126
+ # Keep on error — the killer debugging feature
127
+ with temp_file(keep_on_error=True) as f:
128
+ f.write(data)
129
+ risky_operation(f) # if this raises, file stays
130
+ # if no exception, file is deleted
131
+
132
+ # Atomic write
133
+ with atomic_write("config.json") as f:
134
+ f.write(data)
135
+ # on success: atomically renamed to config.json
136
+ # on error: config.json untouched, temp cleaned up
137
+
138
+ # Promote temp to permanent location
139
+ with temp_file(dest="output.csv") as f:
140
+ f.write(data)
141
+ # on success: moved to output.csv
142
+
143
+ # Async
144
+ async def main() -> None:
145
+ async with async_temp_file(suffix=".json") as f:
146
+ await f.write(data)
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Features
152
+
153
+ - **`keep_on_error=True`** — keep temp only on exception, delete on success. The #1 most requested tempfile feature. Nobody else has it.
154
+ - **`atomic_write()`** — temp file + atomic rename. The most reimplemented pattern, now built-in.
155
+ - **`dest=` parameter** — promote temp to a permanent location on success.
156
+ - **`DEBUG=1` env var** — keep all temps for debugging, no code changes.
157
+ - **`keep=True` per-call** — keep a specific temp without global DEBUG.
158
+ - **`.keep()` method** — decide at runtime whether to keep.
159
+ - **`cwd=True`** — auto-chdir into temp dir, restore on exit.
160
+ - **`content=`** — pre-populate file with content.
161
+ - **`cleanup_hook=`** — custom hook called before standard cleanup.
162
+ - **`temp_registry`** — track all temps globally, cleanup on demand.
163
+ - **`@temp_dir()` / `@temp_file()` decorators** — inject temps into functions and test classes.
164
+ - **Close without delete** — file survives `close()`, deleted on context exit (Windows subprocess friendly).
165
+ - **`.path` attribute** — `Path` object, no more `Path(f.name)` boilerplate.
166
+ - **Async support** — `async with temp_file() as f:` with async I/O methods.
167
+ - **Windows-safe by default** — no `O_TEMPORARY` lock, `ignore_cleanup_errors=True`.
168
+ - **Zero dependencies** — stdlib only.
169
+
170
+ ---
171
+
172
+ ## API Reference
173
+
174
+ ### `temp_file()`
175
+
176
+ ```python
177
+ from tmpkit import temp_file
178
+
179
+ with temp_file(
180
+ suffix: str | None = None, # e.g. ".csv"
181
+ prefix: str | None = None, # e.g. "myapp_"
182
+ dir: str | Path | None = None, # parent directory
183
+ mode: str = "w+b", # open mode
184
+ content: str | bytes | None = None, # pre-populate
185
+ dest: str | Path | None = None, # move here on success
186
+ keep: bool = False, # always keep
187
+ keep_on_error: bool = False, # keep only on exception
188
+ ignore_cleanup_errors: bool = True,
189
+ cleanup_hook: Callable[[Path], None] | None = None,
190
+ ) as f:
191
+ f.write(data) # file-like I/O
192
+ f.read()
193
+ f.seek(0)
194
+ f.path # Path object
195
+ f.keep() # runtime decision to keep
196
+ ```
197
+
198
+ **Returns:** A file-like object with `.path` (Path), `.keep()`, and all standard file methods (`read`, `write`, `seek`, `tell`, `flush`, `close`).
199
+
200
+ **`dest=` behavior:**
201
+
202
+ - On success: temp is moved to `dest` via `os.replace()` (same filesystem) or `shutil.move()` (cross-filesystem).
203
+ - On error: temp is deleted, `dest` is untouched.
204
+ - If `dest` already exists, it is overwritten.
205
+
206
+ ### `temp_dir()`
207
+
208
+ ```python
209
+ from tmpkit import temp_dir
210
+
211
+ with temp_dir(
212
+ suffix: str | None = None,
213
+ prefix: str | None = None,
214
+ dir: str | Path | None = None,
215
+ cwd: bool = False, # auto-chdir into temp dir
216
+ keep: bool = False,
217
+ keep_on_error: bool = False,
218
+ ignore_cleanup_errors: bool = True,
219
+ cleanup_hook: Callable[[Path], None] | None = None,
220
+ ) as d:
221
+ (d / "file.txt").write_text("hello")
222
+ d # Path object
223
+
224
+ # To call .keep(), use the context manager object directly:
225
+ td = temp_dir()
226
+ with td as d:
227
+ (d / "file.txt").write_text("hello")
228
+ td.keep() # runtime decision to keep
229
+ ```
230
+
231
+ **Returns:** A `Path` object (the temp directory path) with `/` operator support. To call `.keep()`, use the context manager object directly (see example above).
232
+
233
+ **`cwd=True`:** Changes the working directory to the temp dir on `__enter__`, restores the original on `__exit__`.
234
+
235
+ ### `atomic_write()`
236
+
237
+ ```python
238
+ from tmpkit import atomic_write
239
+
240
+ with atomic_write(
241
+ dest: str | Path, # final destination
242
+ mode: str = "w", # "w" (text) or "wb" (binary)
243
+ encoding: str | None = None,
244
+ newline: str | None = None,
245
+ prefix: str | None = None,
246
+ suffix: str = ".tmp",
247
+ fsync: bool = True, # fsync before rename
248
+ keep_on_error: bool = False,
249
+ ignore_cleanup_errors: bool = True,
250
+ ) as f:
251
+ f.write(data)
252
+ # on success: atomically renamed to dest
253
+ # on error: dest untouched, temp deleted
254
+ ```
255
+
256
+ Writes to a temp file in `dest`'s parent directory, then atomically replaces `dest` via `os.replace()` on success. On error, the temp is cleaned up and `dest` is left untouched.
257
+
258
+ ### `@temp_dir()` Decorator
259
+
260
+ ```python
261
+ from tmpkit import temp_dir_decorator
262
+
263
+ # On a function — temp dir injected as first arg
264
+ @temp_dir_decorator()
265
+ def process(tmp: Path, data: str) -> None:
266
+ (tmp / "output.txt").write_text(data)
267
+
268
+ # cwd=True by default for the decorator
269
+ @temp_dir_decorator(prefix="test_")
270
+ def my_func(tmp: Path) -> str:
271
+ return str(tmp)
272
+
273
+ # On a test class — each test_ method gets a fresh temp dir
274
+ @temp_dir_decorator()
275
+ class TestMyApp:
276
+ def test_writes_file(self) -> None:
277
+ assert self.tmpdir.exists()
278
+ (self.tmpdir / "data.txt").write_text("test")
279
+
280
+ async def test_async(self) -> None:
281
+ assert self.tmpdir.exists()
282
+ ```
283
+
284
+ **Decorator defaults:** `cwd=True` (unlike the context manager where `cwd=False` by default).
285
+
286
+ **Class decoration:** Each method starting with `test_` is wrapped. The temp dir is available as `self.tmpdir`. Works with both sync and async test methods.
287
+
288
+ ### `@temp_file()` Decorator
289
+
290
+ ```python
291
+ from tmpkit import temp_file_decorator
292
+
293
+ @temp_file_decorator(mode="w+")
294
+ def process(f, data: str) -> str:
295
+ f.write(data)
296
+ f.seek(0)
297
+ return f.read()
298
+
299
+ result = process("hello world")
300
+
301
+ # Async functions supported
302
+ @temp_file_decorator(suffix=".json")
303
+ async def process_async(f, data: str) -> None:
304
+ await f.write(data)
305
+ ```
306
+
307
+ The temp file object is injected as the **first positional argument**.
308
+
309
+ ### `temp_registry`
310
+
311
+ ```python
312
+ from tmpkit import temp_registry
313
+
314
+ # Enable tracking
315
+ temp_registry.enable()
316
+
317
+ # Or via env var: TMPKIT_REGISTRY=1
318
+
319
+ with temp_file() as f:
320
+ assert len(temp_registry.active) == 1
321
+ assert temp_registry.active[0].path == f.path
322
+ # After exit:
323
+ assert len(temp_registry.active) == 0
324
+ assert len(temp_registry.cleaned) == 1
325
+
326
+ # Inspect all records
327
+ for record in temp_registry.all:
328
+ print(f"{record.kind} at {record.path} (cleaned={record.cleaned}, kept={record.kept})")
329
+
330
+ # Emergency cleanup
331
+ count = temp_registry.cleanup_all() # deletes all active temps
332
+
333
+ # Mark all as kept
334
+ temp_registry.keep_all()
335
+
336
+ # Clear cleaned records from history
337
+ temp_registry.clear_history()
338
+
339
+ # Disable
340
+ temp_registry.disable()
341
+ ```
342
+
343
+ **`TempRecord` fields:**
344
+
345
+ | Field | Type | Description |
346
+ | --- | --- | --- |
347
+ | `path` | `Path` | Filesystem path |
348
+ | `kind` | `"file"` or `"dir"` | Resource type |
349
+ | `created_at` | `float` | Epoch timestamp |
350
+ | `cleaned` | `bool` | Whether it was cleaned up |
351
+ | `kept` | `bool` | Whether it was kept |
352
+
353
+ **Thread-safe:** All operations are protected by `threading.Lock`.
354
+
355
+ ### Async API
356
+
357
+ All sync APIs have async counterparts with identical parameters:
358
+
359
+ ```python
360
+ from tmpkit import async_temp_file, async_temp_dir, async_atomic_write
361
+
362
+ # Async temp file
363
+ async with async_temp_file(suffix=".csv") as f:
364
+ await f.write(data)
365
+ await f.seek(0)
366
+ content = await f.read()
367
+
368
+ # Async temp dir
369
+ async with async_temp_dir(cwd=True) as d:
370
+ ...
371
+
372
+ # Async atomic write
373
+ async with async_atomic_write("config.json") as f:
374
+ await f.write(data)
375
+ ```
376
+
377
+ Async file objects support `await f.read()`, `await f.write()`, `await f.seek()`, `await f.tell()`, `await f.flush()`, `await f.close()`.
378
+
379
+ ### Environment Variables
380
+
381
+ | Variable | Value | Effect |
382
+ | --- | --- | --- |
383
+ | `TMPKIT_DEBUG` | `1` | Keep all temps (overrides `keep=False`) |
384
+ | `DEBUG` | `1` | Same as `TMPKIT_DEBUG=1` (fallback) |
385
+ | `TMPKIT_REGISTRY` | `1` | Enable `temp_registry` at import time |
386
+
387
+ `TMPKIT_DEBUG` takes precedence over `DEBUG`.
388
+
389
+ ---
390
+
391
+ ## Keep Control: Precedence
392
+
393
+ When multiple keep signals are present, precedence is:
394
+
395
+ 1. **`.keep()` method** — highest priority, always keeps.
396
+ 2. **`keep=True` parameter** — always keeps.
397
+ 3. **`DEBUG=1` / `TMPKIT_DEBUG=1` env var** — keeps all temps globally.
398
+ 4. **`keep_on_error=True` + exception** — keeps only on error.
399
+ 5. **`dest=` move** — if none of the above trigger, temp is moved to dest on success.
400
+ 6. **Standard cleanup** — temp is deleted.
401
+
402
+ ```python
403
+ # .keep() wins over everything
404
+ with temp_file(keep=False) as f:
405
+ f.write(data)
406
+ f.keep() # file is kept despite keep=False
407
+
408
+ # DEBUG=1 overrides keep=False
409
+ # $ TMPKIT_DEBUG=1 python my_script.py
410
+ with temp_file() as f: # file is kept
411
+ f.write(data)
412
+
413
+ # keep_on_error keeps only on exception
414
+ with temp_file(keep_on_error=True) as f:
415
+ f.write(data)
416
+ raise RuntimeError("oops") # file is kept
417
+ ```
418
+
419
+ ---
420
+
421
+ ## Cleanup Hooks
422
+
423
+ The `cleanup_hook` parameter lets you run custom logic before standard cleanup:
424
+
425
+ ```python
426
+ def my_hook(path: Path) -> None:
427
+ print(f"Cleaning up {path}")
428
+ # e.g. log, collect metrics, copy to backup, etc.
429
+
430
+ with temp_file(cleanup_hook=my_hook) as f:
431
+ f.write(data)
432
+ # hook is called, then standard cleanup runs
433
+
434
+ # Hook is called even on exceptions
435
+ with temp_file(cleanup_hook=my_hook) as f:
436
+ raise RuntimeError("oops")
437
+ # hook is still called, then temp is deleted
438
+
439
+ # Hook errors are swallowed if ignore_cleanup_errors=True (default)
440
+ def bad_hook(path: Path) -> None:
441
+ raise OSError("hook failed")
442
+
443
+ with temp_file(cleanup_hook=bad_hook) as f: # no error raised
444
+ f.write(data)
445
+
446
+ # Hook is NOT called when temp is kept
447
+ with temp_file(keep=True, cleanup_hook=my_hook) as f:
448
+ f.write(data)
449
+ # hook is NOT called
450
+ ```
451
+
452
+ ---
453
+
454
+ ## Comparison
455
+
456
+ | Feature | tmpkit | stdlib `tempfile` | `temporary` | `tdir` | `temppathlib` | `ephemdir` |
457
+ | --- | --- | --- | --- | --- | --- | --- |
458
+ | `keep_on_error` | Yes | No | No | No | No | No |
459
+ | `atomic_write()` | Yes | No | No | No | No | No |
460
+ | `dest=` promote | Yes | No | No | No | No | No |
461
+ | `cleanup_hook` | Yes | No | No | No | No | No |
462
+ | `temp_registry` | Yes | No | No | No | No | No |
463
+ | Decorators | Yes | No | No | No | No | No |
464
+ | `DEBUG=1` env var | Yes | No | No | No | No | No |
465
+ | `keep=True` per-call | Yes | No | No | No | No | No |
466
+ | `.keep()` method | Yes | No | No | No | No | No |
467
+ | Close without delete | Yes | No | No | No | No | No |
468
+ | `.path` attribute | Yes | No | No | No | Yes | No |
469
+ | `cwd=True` | Yes | No | Yes | Yes | No | No |
470
+ | `content=` | Yes | No | Yes | No | No | No |
471
+ | Async support | Yes | No | No | No | No | No |
472
+ | Windows-safe by default | Yes | No | No | No | No | No |
473
+ | `ignore_cleanup_errors` | Yes (default) | Yes (opt-in) | No | No | No | No |
474
+ | Returns `Path` | Yes | No | Yes | No | Yes | No |
475
+ | Zero deps | Yes | Yes | No | Yes | Yes | Yes |
476
+ | Python >=3.11 | Yes | Yes | No | Yes | No | Yes |
477
+
478
+ ---
479
+
480
+ ## Contributing
481
+
482
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code style, testing, and pull request guidelines.
483
+
484
+ Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing.
485
+
486
+ To report a security vulnerability, see [SECURITY.md](SECURITY.md).
487
+
488
+ ---
489
+
490
+ ## Changelog
491
+
492
+ See [CHANGELOG.md](CHANGELOG.md) for a full list of changes.
493
+
494
+ ---
495
+
496
+ ## Acknowledgements
497
+
498
+ - Python's [`tempfile`](https://docs.python.org/3/library/tempfile.html) module — the foundation tmpkit builds upon.
499
+ - [`contextlib`](https://docs.python.org/3/library/contextlib.html) — inspiration for the context manager patterns.
500
+ - Every developer who has written `try/finally/shutil.rmtree` boilerplate — you deserved better.
501
+
502
+ ---
503
+
504
+ ## License
505
+
506
+ [MIT](LICENSE) — Copyright (c) 2025 Mathias Paulenko
@@ -0,0 +1,13 @@
1
+ tmpkit/__init__.py,sha256=ZhWHe5nff6A-80cvecYAS8rcZQrXmpTzhXrIwMHL4YU,795
2
+ tmpkit/_async.py,sha256=n-nEOxQvibobnxJeq7HAlJrNce9pqHb2ETM4J6g3WFk,9256
3
+ tmpkit/_atomic.py,sha256=xPcGSv1A_74uCX5eLXsVFSQiX47iOQr5jZtEHZZT6fU,9028
4
+ tmpkit/_config.py,sha256=-SVxmwvRgf8U4Qx4rx8l7CBH8rQ0Z5hXxn7UqLdeVbU,908
5
+ tmpkit/_decorators.py,sha256=GSdJeQkVL5PDH7GEGdMyA82dUXX92uAdY2q2d7LfIkY,8616
6
+ tmpkit/_registry.py,sha256=ibzwODy0bLMbFbl3MpAroNIwTo0tmLpEYAtT7wteB_Q,4960
7
+ tmpkit/_sync.py,sha256=scn5nwwOYzcTSsv4sILHz6ImYHZUlS_jCQ5PXIA08k8,18878
8
+ tmpkit/_types.py,sha256=BWnRLP6mOgyFUwHL16p-B62Md5pVIeBH0CjTCozxz9Y,1360
9
+ tmpkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ tmpkit-1.0.0.dist-info/METADATA,sha256=CRMmzVbnm21HpiRBnMkwBFpnDAMzPeMqzfR40SCbvpc,16050
11
+ tmpkit-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
12
+ tmpkit-1.0.0.dist-info/licenses/LICENSE,sha256=LLr8AP65kT83seFEcknE3-76LL7oikrP87U33tZKYro,1073
13
+ tmpkit-1.0.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) 2025 Mathias Paulenko
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.