entropy-data 0.3.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.
- entropy_data/__init__.py +5 -0
- entropy_data/__main__.py +3 -0
- entropy_data/cli.py +133 -0
- entropy_data/client.py +163 -0
- entropy_data/commands/__init__.py +0 -0
- entropy_data/commands/access.py +128 -0
- entropy_data/commands/api_keys.py +65 -0
- entropy_data/commands/assets.py +80 -0
- entropy_data/commands/certifications.py +80 -0
- entropy_data/commands/connection.py +90 -0
- entropy_data/commands/costs.py +61 -0
- entropy_data/commands/datacontracts.py +115 -0
- entropy_data/commands/dataproducts.py +93 -0
- entropy_data/commands/definitions.py +80 -0
- entropy_data/commands/events.py +32 -0
- entropy_data/commands/example_data.py +83 -0
- entropy_data/commands/import_export.py +134 -0
- entropy_data/commands/lineage.py +107 -0
- entropy_data/commands/search.py +55 -0
- entropy_data/commands/settings.py +68 -0
- entropy_data/commands/sourcesystems.py +80 -0
- entropy_data/commands/tags.py +84 -0
- entropy_data/commands/teams.py +80 -0
- entropy_data/commands/test_results.py +85 -0
- entropy_data/commands/usage.py +99 -0
- entropy_data/config.py +141 -0
- entropy_data/output.py +119 -0
- entropy_data/util.py +22 -0
- entropy_data-0.3.0.dist-info/METADATA +23 -0
- entropy_data-0.3.0.dist-info/RECORD +33 -0
- entropy_data-0.3.0.dist-info/WHEEL +4 -0
- entropy_data-0.3.0.dist-info/entry_points.txt +2 -0
- entropy_data-0.3.0.dist-info/licenses/LICENSE +21 -0
entropy_data/config.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Connection configuration management for ~/.entropy-data/config.toml."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
import tomllib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import tomli_w
|
|
10
|
+
|
|
11
|
+
CONFIG_DIR = Path.home() / ".entropy-data"
|
|
12
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
13
|
+
DEFAULT_HOST = "https://api.entropy-data.com"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigurationError(Exception):
|
|
17
|
+
"""Missing or invalid configuration."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ConnectionConfig:
|
|
22
|
+
api_key: str
|
|
23
|
+
host: str = DEFAULT_HOST
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_config() -> dict:
|
|
27
|
+
"""Read ~/.entropy-data/config.toml, return empty dict if missing."""
|
|
28
|
+
if not CONFIG_FILE.exists():
|
|
29
|
+
return {}
|
|
30
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
31
|
+
return tomllib.load(f)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_config(config: dict) -> None:
|
|
35
|
+
"""Write config.toml with 0600 permissions."""
|
|
36
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
with open(CONFIG_FILE, "wb") as f:
|
|
38
|
+
tomli_w.dump(config, f)
|
|
39
|
+
CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_connection(
|
|
43
|
+
connection_name: str | None = None,
|
|
44
|
+
cli_api_key: str | None = None,
|
|
45
|
+
cli_host: str | None = None,
|
|
46
|
+
) -> ConnectionConfig:
|
|
47
|
+
"""Resolve connection with precedence: CLI options > env vars > config file."""
|
|
48
|
+
api_key = cli_api_key
|
|
49
|
+
host = cli_host
|
|
50
|
+
|
|
51
|
+
# Layer 2: environment variables
|
|
52
|
+
if api_key is None:
|
|
53
|
+
api_key = os.getenv("ENTROPY_DATA_API_KEY")
|
|
54
|
+
if host is None:
|
|
55
|
+
host = os.getenv("ENTROPY_DATA_HOST")
|
|
56
|
+
|
|
57
|
+
# Layer 3: config file
|
|
58
|
+
if api_key is None or host is None:
|
|
59
|
+
config = load_config()
|
|
60
|
+
connections = config.get("connections", {})
|
|
61
|
+
|
|
62
|
+
name = connection_name or config.get("default_connection_name")
|
|
63
|
+
if connection_name and connection_name not in connections:
|
|
64
|
+
raise ConfigurationError(f"Connection '{connection_name}' not found.")
|
|
65
|
+
if name and name in connections:
|
|
66
|
+
conn = connections[name]
|
|
67
|
+
if api_key is None:
|
|
68
|
+
api_key = conn.get("api_key")
|
|
69
|
+
if host is None:
|
|
70
|
+
host = conn.get("host")
|
|
71
|
+
|
|
72
|
+
# Default host
|
|
73
|
+
if host is None:
|
|
74
|
+
host = DEFAULT_HOST
|
|
75
|
+
|
|
76
|
+
if api_key is None:
|
|
77
|
+
raise ConfigurationError(
|
|
78
|
+
"No API key found. Set ENTROPY_DATA_API_KEY, use --api-key, or run: entropy-data connection add <name>"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return ConnectionConfig(api_key=api_key, host=host)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def add_connection(name: str, api_key: str, host: str = DEFAULT_HOST) -> None:
|
|
85
|
+
"""Add or update a named connection."""
|
|
86
|
+
if not name or not name.strip():
|
|
87
|
+
raise ConfigurationError("Connection name must not be empty.")
|
|
88
|
+
config = load_config()
|
|
89
|
+
if "connections" not in config:
|
|
90
|
+
config["connections"] = {}
|
|
91
|
+
config["connections"][name] = {"api_key": api_key, "host": host}
|
|
92
|
+
# Set as default if it's the first connection
|
|
93
|
+
if "default_connection_name" not in config:
|
|
94
|
+
config["default_connection_name"] = name
|
|
95
|
+
save_config(config)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def remove_connection(name: str) -> None:
|
|
99
|
+
"""Remove a named connection."""
|
|
100
|
+
config = load_config()
|
|
101
|
+
connections = config.get("connections", {})
|
|
102
|
+
if name not in connections:
|
|
103
|
+
raise ConfigurationError(f"Connection '{name}' not found.")
|
|
104
|
+
del connections[name]
|
|
105
|
+
# Clear default if we removed it
|
|
106
|
+
if config.get("default_connection_name") == name:
|
|
107
|
+
if connections:
|
|
108
|
+
config["default_connection_name"] = next(iter(connections))
|
|
109
|
+
else:
|
|
110
|
+
config.pop("default_connection_name", None)
|
|
111
|
+
save_config(config)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def set_default_connection(name: str) -> None:
|
|
115
|
+
"""Set the default connection."""
|
|
116
|
+
config = load_config()
|
|
117
|
+
connections = config.get("connections", {})
|
|
118
|
+
if name not in connections:
|
|
119
|
+
raise ConfigurationError(f"Connection '{name}' not found.")
|
|
120
|
+
config["default_connection_name"] = name
|
|
121
|
+
save_config(config)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def list_connections() -> list[dict]:
|
|
125
|
+
"""List all connections with masked API keys."""
|
|
126
|
+
config = load_config()
|
|
127
|
+
default_name = config.get("default_connection_name")
|
|
128
|
+
connections = config.get("connections", {})
|
|
129
|
+
result = []
|
|
130
|
+
for name, conn in connections.items():
|
|
131
|
+
api_key = conn.get("api_key", "")
|
|
132
|
+
masked = api_key[:4] + "..." + api_key[-4:] if len(api_key) > 8 else "****"
|
|
133
|
+
result.append(
|
|
134
|
+
{
|
|
135
|
+
"name": name,
|
|
136
|
+
"host": conn.get("host", DEFAULT_HOST),
|
|
137
|
+
"api_key": masked,
|
|
138
|
+
"default": name == default_name,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
return result
|
entropy_data/output.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Output formatting for CLI results."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
error_console = Console(stderr=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OutputFormat(str, Enum):
|
|
14
|
+
table = "table"
|
|
15
|
+
json = "json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Column definitions per resource type: list of (header, dict_key)
|
|
19
|
+
RESOURCE_COLUMNS: dict[str, list[tuple[str, str]]] = {
|
|
20
|
+
"dataproducts": [("ID", "id"), ("Title", "name"), ("Status", "status"), ("Owner", "team.name")],
|
|
21
|
+
"datacontracts": [("ID", "id"), ("Title", "name"), ("Version", "version"), ("Owner", "team.name")],
|
|
22
|
+
"access": [
|
|
23
|
+
("ID", "id"),
|
|
24
|
+
("Purpose", "info.purpose"),
|
|
25
|
+
("Status", "info.status"),
|
|
26
|
+
("Active", "info.active"),
|
|
27
|
+
("Provider", "provider.dataProductId"),
|
|
28
|
+
("Consumer", "consumer.teamId"),
|
|
29
|
+
],
|
|
30
|
+
"teams": [("ID", "id"), ("Name", "name"), ("Type", "type"), ("Parent", "parent")],
|
|
31
|
+
"sourcesystems": [("ID", "id"), ("Name", "name"), ("Owner", "owner")],
|
|
32
|
+
"definitions": [("ID", "id"), ("Name", "title"), ("Owner", "owner")],
|
|
33
|
+
"certifications": [("ID", "id"), ("Name", "name"), ("Rank", "rank"), ("Tag", "tag")],
|
|
34
|
+
"example-data": [("ID", "id"), ("Data Product", "dataProductId"), ("Schema", "schemaName")],
|
|
35
|
+
"test-results": [("ID", "id"), ("Data Contract", "dataContractId"), ("Result", "result")],
|
|
36
|
+
"events": [("ID", "id"), ("Type", "type"), ("Subject", "subject"), ("Time", "time")],
|
|
37
|
+
"costs": [("ID", "id"), ("Data Product", "dataProductId"), ("Amount", "amount"), ("Currency", "currency")],
|
|
38
|
+
"assets": [
|
|
39
|
+
("ID", "id"),
|
|
40
|
+
("Name", "info.name"),
|
|
41
|
+
("Type", "info.type"),
|
|
42
|
+
("Source", "info.source"),
|
|
43
|
+
("Owner", "info.owner"),
|
|
44
|
+
],
|
|
45
|
+
"tags": [("ID", "id"), ("Owner", "info.owner"), ("Description", "info.description")],
|
|
46
|
+
"lineage": [
|
|
47
|
+
("Event Type", "eventType"),
|
|
48
|
+
("Event Time", "eventTime"),
|
|
49
|
+
("Job", "job.name"),
|
|
50
|
+
("Namespace", "job.namespace"),
|
|
51
|
+
],
|
|
52
|
+
"usage": [],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_nested(data: dict, key: str) -> str:
|
|
57
|
+
"""Get a nested value from a dict using dot notation."""
|
|
58
|
+
parts = key.split(".")
|
|
59
|
+
current = data
|
|
60
|
+
for part in parts:
|
|
61
|
+
if isinstance(current, dict):
|
|
62
|
+
current = current.get(part)
|
|
63
|
+
else:
|
|
64
|
+
return ""
|
|
65
|
+
return str(current) if current is not None else ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def print_resource(data: dict, resource_type: str, fmt: OutputFormat) -> None:
|
|
69
|
+
"""Print a single resource."""
|
|
70
|
+
if fmt == OutputFormat.json:
|
|
71
|
+
console.print_json(json.dumps(data))
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
columns = RESOURCE_COLUMNS.get(resource_type, [])
|
|
75
|
+
if columns:
|
|
76
|
+
table = Table(show_header=True)
|
|
77
|
+
for header, _ in columns:
|
|
78
|
+
table.add_column(header)
|
|
79
|
+
table.add_row(*[_get_nested(data, key) for _, key in columns])
|
|
80
|
+
console.print(table)
|
|
81
|
+
else:
|
|
82
|
+
console.print_json(json.dumps(data))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def print_resource_list(
|
|
86
|
+
data: list[dict], resource_type: str, fmt: OutputFormat, has_next_page: bool = False, page: int = 0
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Print a list of resources."""
|
|
89
|
+
if fmt == OutputFormat.json:
|
|
90
|
+
console.print_json(json.dumps(data))
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
columns = RESOURCE_COLUMNS.get(resource_type, [])
|
|
94
|
+
if not columns:
|
|
95
|
+
console.print_json(json.dumps(data))
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
table = Table(show_header=True, title=f"{resource_type} (page {page})")
|
|
99
|
+
for header, _ in columns:
|
|
100
|
+
table.add_column(header)
|
|
101
|
+
for item in data:
|
|
102
|
+
table.add_row(*[_get_nested(item, key) for _, key in columns])
|
|
103
|
+
console.print(table)
|
|
104
|
+
|
|
105
|
+
if has_next_page:
|
|
106
|
+
console.print(f"\nMore results available. Use --page {page + 1} to see the next page.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def print_success(message: str) -> None:
|
|
110
|
+
console.print(f"[green]{message}[/green]")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def print_link(url: str) -> None:
|
|
114
|
+
if url:
|
|
115
|
+
console.print(f"Open {url}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def print_error(message: str) -> None:
|
|
119
|
+
error_console.print(f"[red]Error: {message}[/red]")
|
entropy_data/util.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Shared utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def read_body(file: Path) -> dict:
|
|
11
|
+
"""Read JSON or YAML from file path or stdin (-)."""
|
|
12
|
+
if str(file) == "-":
|
|
13
|
+
content = sys.stdin.read()
|
|
14
|
+
else:
|
|
15
|
+
content = file.read_text()
|
|
16
|
+
try:
|
|
17
|
+
data = json.loads(content)
|
|
18
|
+
except json.JSONDecodeError:
|
|
19
|
+
data = yaml.safe_load(content)
|
|
20
|
+
if not isinstance(data, dict):
|
|
21
|
+
raise ValueError(f"Expected a JSON/YAML object, got {type(data).__name__}.")
|
|
22
|
+
return data
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entropy-data
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: CLI for Entropy Data
|
|
5
|
+
Project-URL: Homepage, https://entropy-data.com
|
|
6
|
+
Project-URL: Documentation, https://docs.entropy-data.com
|
|
7
|
+
Project-URL: Repository, https://github.com/entropy-data/entropy-data-cli
|
|
8
|
+
Author-email: Entropy Data <support@entropy-data.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: pydantic<3.0,>=2.10
|
|
13
|
+
Requires-Dist: python-dotenv<2.0,>=1.0
|
|
14
|
+
Requires-Dist: pyyaml<7.0,>=6.0
|
|
15
|
+
Requires-Dist: requests<3.0,>=2.32
|
|
16
|
+
Requires-Dist: rich<16.0,>=14.0
|
|
17
|
+
Requires-Dist: tomli-w<2.0,>=1.0
|
|
18
|
+
Requires-Dist: typer<1.0,>=0.15.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: responses>=0.25; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
entropy_data/__init__.py,sha256=7gUBK-cksVtizDORiQOtbtPFmpRbZbMORdHsf0DwuuY,147
|
|
2
|
+
entropy_data/__main__.py,sha256=TPkjTet86UW21BUomzL6QcGH_jKFOFwM2iHApJ-sohk,40
|
|
3
|
+
entropy_data/cli.py,sha256=uZ7RUp1FNzVGm4u_dtWEYz2Ri5JgibAOZsA6gGZGEDc,5901
|
|
4
|
+
entropy_data/client.py,sha256=ADjeDNVH18R-DX-3aMxYdNdnq0Iz00mgC6T3WFkcCCg,6790
|
|
5
|
+
entropy_data/config.py,sha256=yYm5_y5dEs6uEwhey79FBeLwOOLqhm-hZ1kjtAcCuoc,4476
|
|
6
|
+
entropy_data/output.py,sha256=eesJqR_bxMqek9_bGqHaxFKQZcL5XLzzljb3zPrWqy0,3958
|
|
7
|
+
entropy_data/util.py,sha256=jtoyru6G0Ukroj_jQ7m1McAKEoroekxZnPAsoyJ95Yc,537
|
|
8
|
+
entropy_data/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
entropy_data/commands/access.py,sha256=-ymE9t6202zEK82S2wYJRug5mWTE9WJW9zvp_iQpDiI,4102
|
|
10
|
+
entropy_data/commands/api_keys.py,sha256=Jj1_lj3tUc1LhpK4H5rjfWYrqNZtZyO97aH4Q3VsWqs,2206
|
|
11
|
+
entropy_data/commands/assets.py,sha256=aPy652XANlOqG4eK0nXuhZgQ5Yiywn9EBQhKOOL_oSA,2566
|
|
12
|
+
entropy_data/commands/certifications.py,sha256=pIukkTX4k3-VHTF_VPU_e2-WcfkWw-UB9VKYAKK7N4s,2714
|
|
13
|
+
entropy_data/commands/connection.py,sha256=Os7QR0SGugMoHgxRYIBOFM14kGg5GkGsXhHSJ8Ycq98,2707
|
|
14
|
+
entropy_data/commands/costs.py,sha256=VABkHbv3NK_pfQFkl3EJktb-x5Oo40kT6r6KExg7q6U,1846
|
|
15
|
+
entropy_data/commands/datacontracts.py,sha256=Rg8b8zAlHfc2y4eL4ITfTih9vPiX5E_P5yb98vFy824,4032
|
|
16
|
+
entropy_data/commands/dataproducts.py,sha256=Esexadd3kQyDIztYt_LLyoLy0JSiq8yC9kH28QXrLf0,3280
|
|
17
|
+
entropy_data/commands/definitions.py,sha256=lWOegYFHRlFrl7rr8q0Amek67n3SOWEZ2x2QCQrPohU,2651
|
|
18
|
+
entropy_data/commands/events.py,sha256=dKfVuWCDJRTuyeJaMNuG1Ly_JbFIU_pPxhYCRzsO9D8,976
|
|
19
|
+
entropy_data/commands/example_data.py,sha256=D5ZZuUji-p6a6uJQCBrJwiUzb8VcW_nLHV0vhKybBGk,2807
|
|
20
|
+
entropy_data/commands/import_export.py,sha256=DuHNM11yfye1gymYH5n--sjqWpTE9wua0VVLnEfwsU8,4289
|
|
21
|
+
entropy_data/commands/lineage.py,sha256=NpUyaVMmDV7Jrkb66tNGHmJiDFnHP_AzMn34QrXB1Sc,3961
|
|
22
|
+
entropy_data/commands/search.py,sha256=YO0cGjDHCiQh9OEHPsNDTmYg4RHy0yP1bg8NIhHTiXc,1824
|
|
23
|
+
entropy_data/commands/settings.py,sha256=EBSNAV9AGpF5_4yXjxSLBD17-w361zG9CAU3Xipc6HM,2349
|
|
24
|
+
entropy_data/commands/sourcesystems.py,sha256=a5tzl69ntolVV_Nn94sdT1siPBMjtC8JHiARBcYJLgE,2703
|
|
25
|
+
entropy_data/commands/tags.py,sha256=IQ9HxljQ7r6bNweitGUxDLSslfmamztNHhV0TDB6bEA,2760
|
|
26
|
+
entropy_data/commands/teams.py,sha256=NeUAW59wHEQsrM1c-iw3bsJ3GQUVPV4g5ySls74p1m8,2525
|
|
27
|
+
entropy_data/commands/test_results.py,sha256=64uNhlXsIx5J-mfoVkO5j8Naox0-T_aF-00g7vOgWvk,2848
|
|
28
|
+
entropy_data/commands/usage.py,sha256=sE1JH5_3iNSRTwYFEZqHWBeRiAcjYMTv_M8nByiE6NA,3473
|
|
29
|
+
entropy_data-0.3.0.dist-info/METADATA,sha256=Hz9pp9brktLNN-BLY0d3vqIE_kV-7LCJ84_qD2c8sjQ,828
|
|
30
|
+
entropy_data-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
31
|
+
entropy_data-0.3.0.dist-info/entry_points.txt,sha256=2MI80plj9d4TOc2jMFpcyBGOidIQkH4-1jiV-oT1shk,54
|
|
32
|
+
entropy_data-0.3.0.dist-info/licenses/LICENSE,sha256=gn-OXYQ3yCus2bnJ2MHbmkOx7JUW5fBqzzN7U_Snbw4,1069
|
|
33
|
+
entropy_data-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Entropy Data
|
|
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.
|