sqlite2duckdb 0.4.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.
- sqlite2duckdb/__init__.py +14 -0
- sqlite2duckdb/__main__.py +76 -0
- sqlite2duckdb/sqlite_to_duckdb.py +174 -0
- sqlite2duckdb-0.4.0.dist-info/METADATA +144 -0
- sqlite2duckdb-0.4.0.dist-info/RECORD +8 -0
- sqlite2duckdb-0.4.0.dist-info/WHEEL +4 -0
- sqlite2duckdb-0.4.0.dist-info/entry_points.txt +2 -0
- sqlite2duckdb-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
from sqlite2duckdb.sqlite_to_duckdb import ConversionResult, sqlite_to_duckdb
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = importlib.metadata.version("sqlite2duckdb")
|
|
7
|
+
except importlib.metadata.PackageNotFoundError:
|
|
8
|
+
# Running from a source checkout that was never installed.
|
|
9
|
+
__version__ = "0.0.0.dev0"
|
|
10
|
+
|
|
11
|
+
# Deprecated alias, kept so that existing imports keep working.
|
|
12
|
+
__VERSION__ = __version__
|
|
13
|
+
|
|
14
|
+
__all__ = ["ConversionResult", "__version__", "sqlite_to_duckdb"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from sqlite2duckdb import __version__, sqlite_to_duckdb
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main_cli() -> int:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="sqlite2duckdb",
|
|
14
|
+
description="Convert Sqlite database to Duckdb database",
|
|
15
|
+
usage="sqlite2duckdb [-f] <sqlite_path> <duckdb_path>",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
parser.add_argument("sqlite_path", type=str, help="sqlite file path")
|
|
19
|
+
parser.add_argument("duckdb_path", type=str, help="duckdb file path")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-f",
|
|
22
|
+
"--force",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help="overwrite the duckdb file if it already exists",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="only report errors")
|
|
27
|
+
parser.add_argument("--verbose", action="store_true", help="report every step")
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-v", "--version", action="version", version=f"sqlite2duckdb {__version__}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
if args.quiet:
|
|
35
|
+
level = logging.WARNING
|
|
36
|
+
elif args.verbose:
|
|
37
|
+
level = logging.DEBUG
|
|
38
|
+
else:
|
|
39
|
+
level = logging.INFO
|
|
40
|
+
# Progress goes to stderr so that stdout stays free for pipelines.
|
|
41
|
+
logging.basicConfig(level=level, format="%(message)s", stream=sys.stderr)
|
|
42
|
+
|
|
43
|
+
overwrite = args.force
|
|
44
|
+
if not overwrite and os.path.exists(args.duckdb_path):
|
|
45
|
+
if not sys.stdin.isatty():
|
|
46
|
+
print(
|
|
47
|
+
f"{args.duckdb_path} already exists. Use --force to overwrite it.",
|
|
48
|
+
file=sys.stderr,
|
|
49
|
+
)
|
|
50
|
+
return 1
|
|
51
|
+
try:
|
|
52
|
+
answer = (
|
|
53
|
+
input(
|
|
54
|
+
f"{args.duckdb_path} already exists. do you want to delete this file ? (yes/no): "
|
|
55
|
+
)
|
|
56
|
+
.strip()
|
|
57
|
+
.lower()
|
|
58
|
+
)
|
|
59
|
+
except EOFError:
|
|
60
|
+
print(f"{args.duckdb_path} already exists.", file=sys.stderr)
|
|
61
|
+
return 1
|
|
62
|
+
if answer not in ("yes", "y"):
|
|
63
|
+
return 1
|
|
64
|
+
overwrite = True
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
sqlite_to_duckdb(args.sqlite_path, args.duckdb_path, overwrite=overwrite)
|
|
68
|
+
except (FileNotFoundError, FileExistsError) as error:
|
|
69
|
+
print(f"error: {error}", file=sys.stderr)
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
sys.exit(main_cli())
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
import duckdb
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ConversionResult:
|
|
17
|
+
"""What a successful conversion produced."""
|
|
18
|
+
|
|
19
|
+
target: str
|
|
20
|
+
tables: int
|
|
21
|
+
elapsed: float
|
|
22
|
+
"""Wall clock duration of the conversion, in seconds."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _quote_identifier(name: str) -> str:
|
|
26
|
+
return '"' + name.replace('"', '""') + '"'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_duration(seconds: float) -> str:
|
|
30
|
+
if seconds < 1:
|
|
31
|
+
return f"{seconds * 1000:.2f} ms"
|
|
32
|
+
return f"{seconds:.2f} s"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _remove_quietly(path: str) -> None:
|
|
36
|
+
for candidate in (path, path + ".wal"):
|
|
37
|
+
try:
|
|
38
|
+
os.remove(candidate)
|
|
39
|
+
except OSError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _brackets_to_quotes(sql: str) -> str:
|
|
44
|
+
"""Rewrite sqlite's [bracket] identifiers into standard "double quoted" ones.
|
|
45
|
+
|
|
46
|
+
Quoted runs are copied verbatim so that a `[` inside a string literal survives.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
out = []
|
|
50
|
+
index = 0
|
|
51
|
+
length = len(sql)
|
|
52
|
+
|
|
53
|
+
while index < length:
|
|
54
|
+
char = sql[index]
|
|
55
|
+
|
|
56
|
+
if char in "'\"`":
|
|
57
|
+
end = index + 1
|
|
58
|
+
while end < length:
|
|
59
|
+
if sql[end] == char:
|
|
60
|
+
if end + 1 < length and sql[end + 1] == char:
|
|
61
|
+
end += 2
|
|
62
|
+
continue
|
|
63
|
+
break
|
|
64
|
+
end += 1
|
|
65
|
+
out.append(sql[index : end + 1])
|
|
66
|
+
index = end + 1
|
|
67
|
+
|
|
68
|
+
elif char == "[":
|
|
69
|
+
end = sql.find("]", index)
|
|
70
|
+
if end == -1:
|
|
71
|
+
out.append(char)
|
|
72
|
+
index += 1
|
|
73
|
+
else:
|
|
74
|
+
out.append(_quote_identifier(sql[index + 1 : end]))
|
|
75
|
+
index = end + 1
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
out.append(char)
|
|
79
|
+
index += 1
|
|
80
|
+
|
|
81
|
+
return "".join(out)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _copy_indexes(conn: duckdb.DuckDBPyConnection, sqlite_path: str) -> None:
|
|
85
|
+
"""Recreate the source indexes, translating sqlite quoting on the way."""
|
|
86
|
+
|
|
87
|
+
with contextlib.closing(sqlite3.connect(sqlite_path)) as source:
|
|
88
|
+
indexes = source.execute(
|
|
89
|
+
"SELECT name, sql FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL"
|
|
90
|
+
).fetchall()
|
|
91
|
+
|
|
92
|
+
for name, sql in indexes:
|
|
93
|
+
try:
|
|
94
|
+
conn.sql(_brackets_to_quotes(sql))
|
|
95
|
+
except duckdb.Error as error:
|
|
96
|
+
logger.warning("Could not recreate index %s: %s", name, error)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _copy_tables(
|
|
100
|
+
conn: duckdb.DuckDBPyConnection, tables: list[tuple[str, str]]
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Recreate each table then fill it from the attached database."""
|
|
103
|
+
|
|
104
|
+
for table_name, ddl in tables:
|
|
105
|
+
# This DDL is regenerated by duckdb from the attached catalog, so unlike
|
|
106
|
+
# sqlite's own it always parses, and it still carries the primary keys
|
|
107
|
+
# and the NOT NULL constraints.
|
|
108
|
+
conn.sql(ddl)
|
|
109
|
+
quoted = _quote_identifier(table_name)
|
|
110
|
+
conn.sql(f"INSERT INTO {quoted} SELECT * FROM __other.{quoted}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def sqlite_to_duckdb(
|
|
114
|
+
sqlite_db: str | os.PathLike[str],
|
|
115
|
+
duck_db: str | os.PathLike[str],
|
|
116
|
+
*,
|
|
117
|
+
overwrite: bool = False,
|
|
118
|
+
) -> ConversionResult:
|
|
119
|
+
"""Copy a sqlite database into a new duckdb database.
|
|
120
|
+
|
|
121
|
+
Tables, data, primary keys, NOT NULL constraints and indexes are copied.
|
|
122
|
+
Views and UNIQUE / FOREIGN KEY / CHECK constraints are not: duckdb's sqlite
|
|
123
|
+
extension does not expose them on the attached database.
|
|
124
|
+
|
|
125
|
+
Raises FileNotFoundError if `sqlite_db` does not exist, and FileExistsError
|
|
126
|
+
if `duck_db` already exists and `overwrite` is False.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
sqlite_path = os.fspath(sqlite_db)
|
|
130
|
+
duck_path = os.fspath(duck_db)
|
|
131
|
+
|
|
132
|
+
if not os.path.exists(sqlite_path):
|
|
133
|
+
raise FileNotFoundError(f"File {sqlite_path} doesn't exist")
|
|
134
|
+
|
|
135
|
+
if os.path.exists(duck_path):
|
|
136
|
+
if not overwrite:
|
|
137
|
+
raise FileExistsError(f"Database {duck_path} already exists")
|
|
138
|
+
_remove_quietly(duck_path)
|
|
139
|
+
|
|
140
|
+
logger.info("Create %s database", duck_path)
|
|
141
|
+
start_time = time.perf_counter()
|
|
142
|
+
|
|
143
|
+
conn = duckdb.connect(duck_path)
|
|
144
|
+
try:
|
|
145
|
+
## Install sqlite
|
|
146
|
+
conn.sql("INSTALL sqlite; LOAD sqlite;")
|
|
147
|
+
|
|
148
|
+
# Bound parameters are not allowed in ATTACH, so escape the quotes ourselves
|
|
149
|
+
source_path = sqlite_path.replace("'", "''")
|
|
150
|
+
conn.sql(f"ATTACH '{source_path}' AS __other (TYPE SQLITE, READ_ONLY)")
|
|
151
|
+
|
|
152
|
+
## Get sqlite Names, along with the CREATE statement duckdb derives for them
|
|
153
|
+
tables = conn.sql(
|
|
154
|
+
"SELECT table_name, sql FROM duckdb_tables WHERE database_name = '__other'"
|
|
155
|
+
).fetchall()
|
|
156
|
+
logger.info("%d table(s) found", len(tables))
|
|
157
|
+
|
|
158
|
+
_copy_tables(conn, tables)
|
|
159
|
+
_copy_indexes(conn, sqlite_path)
|
|
160
|
+
|
|
161
|
+
conn.sql("DETACH __other")
|
|
162
|
+
except BaseException:
|
|
163
|
+
# Never leave a half-written database behind: it would trip the
|
|
164
|
+
# "already exists" guard on the next run.
|
|
165
|
+
conn.close()
|
|
166
|
+
_remove_quietly(duck_path)
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
conn.close()
|
|
170
|
+
|
|
171
|
+
elapsed = time.perf_counter() - start_time
|
|
172
|
+
logger.info("Done in %s !", _format_duration(elapsed))
|
|
173
|
+
|
|
174
|
+
return ConversionResult(target=duck_path, tables=len(tables), elapsed=elapsed)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sqlite2duckdb
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A tool to convert sqlite database to duckdb database
|
|
5
|
+
Project-URL: Homepage, https://github.com/dridk/sqlite2duckdb
|
|
6
|
+
Project-URL: Issues, https://github.com/dridk/sqlite2duckdb/issues
|
|
7
|
+
Author-email: Sacha Schutz <sacha.schutz@pm.me>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: database,duckdb,olap,oltp,sqlite
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: duckdb>=1.1.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# sqlite2duckdb
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
A tool for converting a [sqlite](https://www.sqlite.org/) database into a [duckdb](https://duckdb.org/) database
|
|
29
|
+
|
|
30
|
+
## Description
|
|
31
|
+
|
|
32
|
+
Sqlite is an embedded online database designed for transactional reading and writing.
|
|
33
|
+
Duckdb is also an embedded database, but column-oriented, designed for analytical process with a very high reading efficiency.
|
|
34
|
+
|
|
35
|
+
For more details [https://towardsdatascience.com/forget-about-sqlite-use-duckdb-instead-and-thank-me-later-df76ee9bb777](https://towardsdatascience.com/forget-about-sqlite-use-duckdb-instead-and-thank-me-later-df76ee9bb777)
|
|
36
|
+
|
|
37
|
+
Requires Python >= 3.9 and duckdb >= 1.1.0 (indexes are only copied from that version on).
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
With [uv](https://docs.astral.sh/uv/), no installation is required. `uvx` downloads and runs the tool in one go:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uvx sqlite2duckdb source.db target.db
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
To keep it around:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv tool install sqlite2duckdb
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or with pip:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install sqlite2duckdb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### As a command line
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
usage: sqlite2duckdb [-f] <sqlite_path> <duckdb_path>
|
|
65
|
+
|
|
66
|
+
Convert Sqlite database to Duckdb database
|
|
67
|
+
|
|
68
|
+
positional arguments:
|
|
69
|
+
sqlite_path sqlite file path
|
|
70
|
+
duckdb_path duckdb file path
|
|
71
|
+
|
|
72
|
+
options:
|
|
73
|
+
-h, --help show this help message and exit
|
|
74
|
+
-f, --force overwrite the duckdb file if it already exists
|
|
75
|
+
-q, --quiet only report errors
|
|
76
|
+
--verbose report every step
|
|
77
|
+
-v, --version show program's version number and exit
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The tool never overwrites an existing target silently. On a terminal it asks for
|
|
81
|
+
confirmation; anywhere else (a script, a CI job, a pipe) it exits with code 1 and tells you
|
|
82
|
+
to pass `--force`. Progress is written to stderr, so stdout stays free for pipelines.
|
|
83
|
+
|
|
84
|
+
### Examples
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uvx sqlite2duckdb source.db target.db
|
|
88
|
+
uvx sqlite2duckdb --force source.db target.db # overwrite target.db without asking
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### From python
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from sqlite2duckdb import sqlite_to_duckdb
|
|
95
|
+
|
|
96
|
+
result = sqlite_to_duckdb("source.sqlite", "target.duckdb")
|
|
97
|
+
print(result.tables, result.elapsed)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`sqlite_to_duckdb(sqlite_db, duck_db, *, overwrite=False)` accepts `str` or `pathlib.Path`
|
|
101
|
+
and returns a `ConversionResult` (`target`, `tables`, `elapsed`). It raises
|
|
102
|
+
`FileNotFoundError` if the source is missing and `FileExistsError` if the target already
|
|
103
|
+
exists and `overwrite` is False. If the conversion fails halfway, the partially written
|
|
104
|
+
target file is removed rather than left behind. Progress is reported through the standard
|
|
105
|
+
`logging` module (logger `sqlite2duckdb.sqlite_to_duckdb`), never printed.
|
|
106
|
+
|
|
107
|
+
## What is converted
|
|
108
|
+
|
|
109
|
+
| | |
|
|
110
|
+
|---|---|
|
|
111
|
+
| Tables and data | ✅ |
|
|
112
|
+
| Primary keys, NOT NULL constraints, indexes | ✅ |
|
|
113
|
+
| UNIQUE, FOREIGN KEY and CHECK constraints | ❌ |
|
|
114
|
+
| Views | ❌ (silently dropped) |
|
|
115
|
+
|
|
116
|
+
Duckdb's sqlite extension does not expose the last two on the attached database, so they
|
|
117
|
+
cannot be copied. Reading them back from `sqlite_master` would be needed.
|
|
118
|
+
|
|
119
|
+
Tables are recreated from the DDL duckdb derives for the attached database, then filled
|
|
120
|
+
from it, and the indexes are read back from `sqlite_master`. This is what makes sqlite
|
|
121
|
+
files that quote their DDL with `[brackets]` (chinook.db, MS Access exports) convert
|
|
122
|
+
correctly: duckdb's own parser rejects that syntax, so the quoting is translated first.
|
|
123
|
+
|
|
124
|
+
## Todo
|
|
125
|
+
|
|
126
|
+
- [ ] Custom type mapping
|
|
127
|
+
- [x] Primary keys, NOT NULL constraints and indexes
|
|
128
|
+
- [ ] Views, and UNIQUE / FOREIGN KEY / CHECK constraints
|
|
129
|
+
|
|
130
|
+
## Contributing
|
|
131
|
+
|
|
132
|
+
The project uses [uv](https://docs.astral.sh/uv/) for everything:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
make dev # uv sync — installs duckdb plus the test deps (pytest, faker, ruff)
|
|
136
|
+
make test # uv run pytest
|
|
137
|
+
make lint # uv run ruff check . && uv run ruff format --check .
|
|
138
|
+
make build # uv build
|
|
139
|
+
make publish # uv publish (PyPI trusted publishing, also run on tags by CI)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### See also
|
|
143
|
+
|
|
144
|
+
- [Harlequin](https://github.com/tconbeer/harlequin): A nice duckdb IDE for your terminal
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sqlite2duckdb/__init__.py,sha256=GJEB0tNxkIzTIHMFTpUyywaGqLRMp6zWYCiniVMc74I,473
|
|
2
|
+
sqlite2duckdb/__main__.py,sha256=o4BxA4togX5XVZGtqwxebtLK0Sk3K8W9sqEccflKVAo,2319
|
|
3
|
+
sqlite2duckdb/sqlite_to_duckdb.py,sha256=E_8RumKshUB_XqePhNQ2okPKUpJmUnKQB7znb5Sn6Fk,5166
|
|
4
|
+
sqlite2duckdb-0.4.0.dist-info/METADATA,sha256=MQjtJBMAoU1FtiQNotPpW0FiR4QSg-ggRKilmdYzMs4,4951
|
|
5
|
+
sqlite2duckdb-0.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
6
|
+
sqlite2duckdb-0.4.0.dist-info/entry_points.txt,sha256=HXDOw1q3RRjrTvb8pZ9cfwp41b0AQXaSwCF7CQPlgQA,66
|
|
7
|
+
sqlite2duckdb-0.4.0.dist-info/licenses/LICENSE,sha256=m02ByVVkLjRHEV9sN02uVqtLRTfaO74CBS_s8eA_UII,1069
|
|
8
|
+
sqlite2duckdb-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 sacha schutz
|
|
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.
|