nveil 0.0.1.dev1__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.
- nveil/__init__.py +243 -0
- nveil/client.py +114 -0
- nveil/exceptions.py +25 -0
- nveil/session.py +137 -0
- nveil/spec.py +141 -0
- nveil/timing.py +76 -0
- nveil-0.0.1.dev1.dist-info/METADATA +107 -0
- nveil-0.0.1.dev1.dist-info/RECORD +11 -0
- nveil-0.0.1.dev1.dist-info/WHEEL +5 -0
- nveil-0.0.1.dev1.dist-info/licenses/LICENSE +105 -0
- nveil-0.0.1.dev1.dist-info/top_level.txt +1 -0
nveil/__init__.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""NVEIL Python SDK — no-code AI data visualization.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
import nveil
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
nveil.configure(api_key="nveil_...")
|
|
9
|
+
|
|
10
|
+
df = pd.read_csv("sales.csv")
|
|
11
|
+
spec = nveil.generate_spec("Show revenue by region", df)
|
|
12
|
+
|
|
13
|
+
fig = spec.render(df)
|
|
14
|
+
fig.show()
|
|
15
|
+
|
|
16
|
+
spec.save("revenue.nveil")
|
|
17
|
+
spec = nveil.load_spec("revenue.nveil")
|
|
18
|
+
fig = spec.render(new_df)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging as _logging
|
|
22
|
+
import os as _os
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from importlib.metadata import version as _pkg_version
|
|
26
|
+
try:
|
|
27
|
+
__version__ = _pkg_version("nveil")
|
|
28
|
+
except Exception:
|
|
29
|
+
__version__ = "0.0.0"
|
|
30
|
+
|
|
31
|
+
# Silence verbose internal logging by default.
|
|
32
|
+
if not _os.environ.get("NVEIL_VERBOSE"):
|
|
33
|
+
for _name in (
|
|
34
|
+
"kedro", "kedro.io", "kedro.runner", "kedro.pipeline",
|
|
35
|
+
"kedro.framework", "dive", "dive.builder", "choregraph",
|
|
36
|
+
):
|
|
37
|
+
_logging.getLogger(_name).setLevel(_logging.WARNING)
|
|
38
|
+
_logging.getLogger().setLevel(_logging.WARNING)
|
|
39
|
+
|
|
40
|
+
from .client import NveilClient
|
|
41
|
+
from .exceptions import (
|
|
42
|
+
AuthenticationError,
|
|
43
|
+
IncompatibleDataError,
|
|
44
|
+
NveilError,
|
|
45
|
+
QuotaExceededError,
|
|
46
|
+
ScopeError,
|
|
47
|
+
SpecGenerationError,
|
|
48
|
+
)
|
|
49
|
+
from .spec import NveilSpec, show, save_image, save_html
|
|
50
|
+
from .session import Session
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"configure",
|
|
54
|
+
"session",
|
|
55
|
+
"generate_spec",
|
|
56
|
+
"load_spec",
|
|
57
|
+
"show",
|
|
58
|
+
"save_image",
|
|
59
|
+
"save_html",
|
|
60
|
+
"NveilClient",
|
|
61
|
+
"NveilSpec",
|
|
62
|
+
"Session",
|
|
63
|
+
"NveilError",
|
|
64
|
+
"AuthenticationError",
|
|
65
|
+
"ScopeError",
|
|
66
|
+
"QuotaExceededError",
|
|
67
|
+
"SpecGenerationError",
|
|
68
|
+
"IncompatibleDataError",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
_client: NveilClient | None = None
|
|
72
|
+
_timing_enabled: bool = False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def configure(
|
|
76
|
+
api_key: str,
|
|
77
|
+
base_url: str = "https://app.nveil.com",
|
|
78
|
+
verify: bool = True,
|
|
79
|
+
verbose: bool = False,
|
|
80
|
+
timing: bool = False,
|
|
81
|
+
**kwargs,
|
|
82
|
+
):
|
|
83
|
+
"""Configure the global NVEIL client.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
api_key: Your NVEIL API key (starts with ``nveil_``).
|
|
87
|
+
base_url: NVEIL server URL (default: ``https://app.nveil.com``).
|
|
88
|
+
verify: Verify SSL certificates (set ``False`` for local dev with self-signed certs).
|
|
89
|
+
verbose: Enable internal library logging (default: silent).
|
|
90
|
+
timing: Enable timing instrumentation (default: ``False``).
|
|
91
|
+
"""
|
|
92
|
+
global _client, _timing_enabled
|
|
93
|
+
_timing_enabled = timing
|
|
94
|
+
if verbose:
|
|
95
|
+
for name in ("kedro", "kedro.io", "kedro.runner", "kedro.pipeline", "kedro.framework"):
|
|
96
|
+
_logging.getLogger(name).setLevel(_logging.INFO)
|
|
97
|
+
_client = NveilClient(api_key=api_key, base_url=base_url, verify=verify, **kwargs)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_client() -> NveilClient:
|
|
101
|
+
if _client is None:
|
|
102
|
+
raise NveilError(
|
|
103
|
+
"NVEIL not configured. Call nveil.configure(api_key='nveil_...') first."
|
|
104
|
+
)
|
|
105
|
+
return _client
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def session() -> Session:
|
|
109
|
+
"""Create a scoped session with a shared workspace.
|
|
110
|
+
|
|
111
|
+
Use as a context manager to keep the workspace alive across
|
|
112
|
+
``generate_spec`` and ``render`` calls — the data pipeline
|
|
113
|
+
runs once, not on every render.
|
|
114
|
+
|
|
115
|
+
Example::
|
|
116
|
+
|
|
117
|
+
with nveil.session() as s:
|
|
118
|
+
spec = s.generate_spec("bar chart of revenue", df)
|
|
119
|
+
fig = spec.render(df) # reuses pipeline — no re-run
|
|
120
|
+
nveil.show(fig)
|
|
121
|
+
# workspace cleaned up here
|
|
122
|
+
"""
|
|
123
|
+
return Session(client=_client, timing=_timing_enabled)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _normalize_to_dict(data) -> dict:
|
|
127
|
+
"""Normalize input to a dict of named DataFrames."""
|
|
128
|
+
import pandas as pd
|
|
129
|
+
import numpy as np
|
|
130
|
+
|
|
131
|
+
if isinstance(data, pd.DataFrame):
|
|
132
|
+
return {"dataset": data}
|
|
133
|
+
if isinstance(data, dict):
|
|
134
|
+
result = {}
|
|
135
|
+
for name, value in data.items():
|
|
136
|
+
if isinstance(value, pd.DataFrame):
|
|
137
|
+
result[name] = value
|
|
138
|
+
else:
|
|
139
|
+
result[name] = pd.DataFrame(value)
|
|
140
|
+
return result
|
|
141
|
+
if isinstance(data, np.ndarray):
|
|
142
|
+
return {"dataset": pd.DataFrame(data)}
|
|
143
|
+
if isinstance(data, list):
|
|
144
|
+
if data and isinstance(data[0], (list, tuple)):
|
|
145
|
+
return {"dataset": pd.DataFrame(data[1:], columns=data[0])}
|
|
146
|
+
return {"dataset": pd.DataFrame(data)}
|
|
147
|
+
raise TypeError(f"Cannot convert {type(data).__name__} to DataFrame")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_MAX_RETRIES = 2
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def generate_spec(prompt: str, data: Any) -> NveilSpec:
|
|
154
|
+
"""Generate a visualization specification from data and a prompt.
|
|
155
|
+
|
|
156
|
+
Only metadata leaves your machine — never raw data.
|
|
157
|
+
All internal processing is handled by the compiled engine.
|
|
158
|
+
|
|
159
|
+
If the server-generated data pipeline fails locally, the SDK retries
|
|
160
|
+
with a new server call (the plan is non-deterministic).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
prompt: Natural language description of the desired visualization.
|
|
164
|
+
data: pandas DataFrame, dict of DataFrames, numpy array, or list of lists.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
NveilSpec that can render locally and be saved/reused.
|
|
168
|
+
"""
|
|
169
|
+
import logging
|
|
170
|
+
import shutil
|
|
171
|
+
from dive._engine import prepare, apply_plan, finalize
|
|
172
|
+
|
|
173
|
+
log = logging.getLogger("nveil")
|
|
174
|
+
client = _get_client()
|
|
175
|
+
dataframes = _normalize_to_dict(data)
|
|
176
|
+
last_error = None
|
|
177
|
+
|
|
178
|
+
for attempt in range(_MAX_RETRIES + 1):
|
|
179
|
+
workspace = prepare(dataframes)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
# Step 1: Send request to server for processing plan
|
|
183
|
+
plan_response = client.processing_plan(
|
|
184
|
+
prompt=prompt,
|
|
185
|
+
request_blob=workspace["request_blob"],
|
|
186
|
+
catalogue_stats=workspace["catalogue_stats"],
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Step 2: Apply plan + run pipeline locally
|
|
190
|
+
pipeline = apply_plan(
|
|
191
|
+
server_plan_response=plan_response,
|
|
192
|
+
workspace_state=workspace,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Step 3: Send request to server for visualization
|
|
196
|
+
viz_response = client.visualization_generate(
|
|
197
|
+
session_id=plan_response.get("session_id", ""),
|
|
198
|
+
request_blob=pipeline["request_blob"],
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Surface server warnings
|
|
202
|
+
for warning in viz_response.get("warnings", []):
|
|
203
|
+
log.warning(warning)
|
|
204
|
+
|
|
205
|
+
# Step 4: Package final spec as opaque blob
|
|
206
|
+
spec_blob = finalize(
|
|
207
|
+
server_viz_response=viz_response,
|
|
208
|
+
pipeline_state_blob=pipeline["request_blob"],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return NveilSpec(spec_blob=spec_blob)
|
|
212
|
+
|
|
213
|
+
except RuntimeError as e:
|
|
214
|
+
last_error = e
|
|
215
|
+
if attempt < _MAX_RETRIES:
|
|
216
|
+
log.warning(
|
|
217
|
+
"Pipeline execution failed (attempt %d/%d): %s — retrying",
|
|
218
|
+
attempt + 1, _MAX_RETRIES + 1, e,
|
|
219
|
+
)
|
|
220
|
+
continue
|
|
221
|
+
finally:
|
|
222
|
+
# Clean up workspace (standalone flow — no session to keep it alive)
|
|
223
|
+
ws = workspace.get("_workspace")
|
|
224
|
+
if ws and ws.exists():
|
|
225
|
+
shutil.rmtree(ws, ignore_errors=True)
|
|
226
|
+
|
|
227
|
+
raise SpecGenerationError(
|
|
228
|
+
f"Pipeline execution failed after {_MAX_RETRIES + 1} attempts: {last_error}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def load_spec(path: str) -> NveilSpec:
|
|
233
|
+
"""Load a spec from an opaque .nveil file.
|
|
234
|
+
|
|
235
|
+
No API call — loaded specs can be rendered locally for free.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
path: Path to a ``.nveil`` file.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
NveilSpec ready to render.
|
|
242
|
+
"""
|
|
243
|
+
return NveilSpec.load(path)
|
nveil/client.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""NVEIL API client — handles HTTP communication with the NVEIL server."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
QuotaExceededError,
|
|
9
|
+
ScopeError,
|
|
10
|
+
SpecGenerationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://app.nveil.com"
|
|
14
|
+
DEFAULT_TIMEOUT = 120.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NveilClient:
|
|
18
|
+
"""HTTP client for the NVEIL public API."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
api_key: str,
|
|
23
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
24
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
25
|
+
verify: bool = True,
|
|
26
|
+
):
|
|
27
|
+
from . import __version__
|
|
28
|
+
|
|
29
|
+
if not verify:
|
|
30
|
+
logging.getLogger("nveil").warning(
|
|
31
|
+
"SSL verification disabled (verify=False). "
|
|
32
|
+
"Only use this for local development with self-signed certificates."
|
|
33
|
+
)
|
|
34
|
+
self._api_key = api_key
|
|
35
|
+
self._base_url = base_url.rstrip("/")
|
|
36
|
+
self._client = httpx.Client(
|
|
37
|
+
base_url=self._base_url,
|
|
38
|
+
headers={
|
|
39
|
+
"X-API-Key": api_key,
|
|
40
|
+
"X-Nveil-Schema-Version": __version__,
|
|
41
|
+
},
|
|
42
|
+
timeout=timeout,
|
|
43
|
+
verify=verify,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _handle_response(self, resp: httpx.Response) -> dict:
|
|
47
|
+
if resp.status_code == 401:
|
|
48
|
+
raise AuthenticationError(
|
|
49
|
+
"Invalid, expired, or revoked API key"
|
|
50
|
+
)
|
|
51
|
+
if resp.status_code == 403:
|
|
52
|
+
raise ScopeError(resp.json().get("detail", "Missing required scope"))
|
|
53
|
+
if resp.status_code == 429:
|
|
54
|
+
raise QuotaExceededError("Rate limit exceeded")
|
|
55
|
+
if resp.status_code >= 400:
|
|
56
|
+
detail = resp.text
|
|
57
|
+
try:
|
|
58
|
+
detail = resp.json().get("detail", resp.text)
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
raise SpecGenerationError(f"API error ({resp.status_code}): {detail}")
|
|
62
|
+
return resp.json()
|
|
63
|
+
|
|
64
|
+
def processing_plan(
|
|
65
|
+
self,
|
|
66
|
+
prompt: str,
|
|
67
|
+
request_blob: str,
|
|
68
|
+
catalogue_stats: str,
|
|
69
|
+
) -> dict:
|
|
70
|
+
"""Request a data processing plan from the server.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
prompt: User's natural language prompt.
|
|
74
|
+
request_blob: Base64-encoded encrypted request payload.
|
|
75
|
+
catalogue_stats: JSON string of dataset metadata.
|
|
76
|
+
"""
|
|
77
|
+
resp = self._client.post(
|
|
78
|
+
"/api/v1/processing/plan",
|
|
79
|
+
json={
|
|
80
|
+
"prompt": prompt,
|
|
81
|
+
"request_blob": request_blob,
|
|
82
|
+
"catalogue_stats": catalogue_stats,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
return self._handle_response(resp)
|
|
86
|
+
|
|
87
|
+
def visualization_generate(
|
|
88
|
+
self,
|
|
89
|
+
session_id: str,
|
|
90
|
+
request_blob: str,
|
|
91
|
+
) -> dict:
|
|
92
|
+
"""Request visualization generation from the server.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
session_id: Session ID from processing_plan response.
|
|
96
|
+
request_blob: Base64-encoded encrypted request payload.
|
|
97
|
+
"""
|
|
98
|
+
resp = self._client.post(
|
|
99
|
+
"/api/v1/visualization/generate",
|
|
100
|
+
json={
|
|
101
|
+
"session_id": session_id,
|
|
102
|
+
"request_blob": request_blob,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
return self._handle_response(resp)
|
|
106
|
+
|
|
107
|
+
def close(self):
|
|
108
|
+
self._client.close()
|
|
109
|
+
|
|
110
|
+
def __enter__(self):
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def __exit__(self, *args):
|
|
114
|
+
self.close()
|
nveil/exceptions.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""NVEIL SDK exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NveilError(Exception):
|
|
5
|
+
"""Base exception for all NVEIL SDK errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthenticationError(NveilError):
|
|
9
|
+
"""Raised when the API key is invalid, expired, or revoked."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ScopeError(NveilError):
|
|
13
|
+
"""Raised when the API key is missing a required scope."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QuotaExceededError(NveilError):
|
|
17
|
+
"""Raised when the rate limit or quota is exceeded."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SpecGenerationError(NveilError):
|
|
21
|
+
"""Raised when the server fails to generate a specification."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IncompatibleDataError(NveilError):
|
|
25
|
+
"""Raised when data columns don't match the spec's expected schema."""
|
nveil/session.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Scoped session — owns a temporary workspace for the duration of a ``with:`` block.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
with nveil.session() as s:
|
|
6
|
+
spec = s.generate_spec("bar chart of revenue", df)
|
|
7
|
+
fig = spec.render(df) # reuses the same workspace — no re-run
|
|
8
|
+
nveil.show(fig)
|
|
9
|
+
print(s.timer.summary())
|
|
10
|
+
# workspace cleaned up here
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from .exceptions import NveilError, SpecGenerationError
|
|
20
|
+
from .spec import NveilSpec
|
|
21
|
+
from .timing import Timer
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Session:
|
|
25
|
+
"""Scoped workspace session.
|
|
26
|
+
|
|
27
|
+
The session owns a temporary workspace and a pipeline instance.
|
|
28
|
+
``generate_spec`` builds and runs the pipeline once; subsequent
|
|
29
|
+
``render()`` calls reuse the already-computed outputs.
|
|
30
|
+
|
|
31
|
+
When ``timing=True``, all operations are tracked in ``self.timer``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, client=None, timing: bool = False):
|
|
35
|
+
self._client = client
|
|
36
|
+
self._pipeline = None # Pipeline instance — alive for the session
|
|
37
|
+
self._workspace: Optional[Path] = None
|
|
38
|
+
self._session_id: Optional[str] = None
|
|
39
|
+
self.timer = Timer(enabled=timing)
|
|
40
|
+
|
|
41
|
+
def __enter__(self):
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, *exc):
|
|
45
|
+
if self._workspace and self._workspace.exists():
|
|
46
|
+
shutil.rmtree(self._workspace, ignore_errors=True)
|
|
47
|
+
self._pipeline = None
|
|
48
|
+
self._workspace = None
|
|
49
|
+
self._session_id = None
|
|
50
|
+
|
|
51
|
+
def _get_client(self):
|
|
52
|
+
if self._client:
|
|
53
|
+
return self._client
|
|
54
|
+
from . import _get_client
|
|
55
|
+
return _get_client()
|
|
56
|
+
|
|
57
|
+
def generate_spec(self, prompt: str, data: Any) -> NveilSpec:
|
|
58
|
+
"""Generate a visualization specification.
|
|
59
|
+
|
|
60
|
+
All internal processing is done by the compiled engine.
|
|
61
|
+
The session keeps the pipeline instance alive for render() reuse.
|
|
62
|
+
|
|
63
|
+
If the server-generated data pipeline fails locally, retries
|
|
64
|
+
with a new server call (the plan is non-deterministic).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
prompt: Natural language visualization request.
|
|
68
|
+
data: pandas DataFrame, dict of DataFrames, or compatible input.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
NveilSpec bound to this session's workspace.
|
|
72
|
+
"""
|
|
73
|
+
import logging
|
|
74
|
+
import shutil
|
|
75
|
+
from dive._engine import prepare, apply_plan, finalize
|
|
76
|
+
from . import _normalize_to_dict, _MAX_RETRIES
|
|
77
|
+
|
|
78
|
+
log = logging.getLogger("nveil")
|
|
79
|
+
client = self._get_client()
|
|
80
|
+
dataframes = _normalize_to_dict(data)
|
|
81
|
+
last_error = None
|
|
82
|
+
|
|
83
|
+
for attempt in range(_MAX_RETRIES + 1):
|
|
84
|
+
with self.timer.measure("build workspace"):
|
|
85
|
+
workspace = prepare(dataframes)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
with self.timer.measure("API: processing plan"):
|
|
89
|
+
plan_response = client.processing_plan(
|
|
90
|
+
prompt=prompt,
|
|
91
|
+
request_blob=workspace["request_blob"],
|
|
92
|
+
catalogue_stats=workspace["catalogue_stats"],
|
|
93
|
+
)
|
|
94
|
+
self._session_id = plan_response.get("session_id", "")
|
|
95
|
+
|
|
96
|
+
with self.timer.measure("pipeline run"):
|
|
97
|
+
pipeline = apply_plan(
|
|
98
|
+
server_plan_response=plan_response,
|
|
99
|
+
workspace_state=workspace,
|
|
100
|
+
)
|
|
101
|
+
self._pipeline = pipeline.get("_choregraph")
|
|
102
|
+
self._workspace = pipeline.get("_workspace")
|
|
103
|
+
|
|
104
|
+
with self.timer.measure("API: visualization"):
|
|
105
|
+
viz_response = client.visualization_generate(
|
|
106
|
+
session_id=self._session_id,
|
|
107
|
+
request_blob=pipeline["request_blob"],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
for warning in viz_response.get("warnings", []):
|
|
111
|
+
log.warning(warning)
|
|
112
|
+
|
|
113
|
+
spec_blob = finalize(
|
|
114
|
+
server_viz_response=viz_response,
|
|
115
|
+
pipeline_state_blob=pipeline["request_blob"],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
spec = NveilSpec(spec_blob=spec_blob)
|
|
119
|
+
spec._session = self
|
|
120
|
+
return spec
|
|
121
|
+
|
|
122
|
+
except RuntimeError as e:
|
|
123
|
+
last_error = e
|
|
124
|
+
if attempt < _MAX_RETRIES:
|
|
125
|
+
log.warning(
|
|
126
|
+
"Pipeline execution failed (attempt %d/%d): %s — retrying",
|
|
127
|
+
attempt + 1, _MAX_RETRIES + 1, e,
|
|
128
|
+
)
|
|
129
|
+
# Clean up failed workspace before retry
|
|
130
|
+
ws = workspace.get("_workspace")
|
|
131
|
+
if ws and ws.exists():
|
|
132
|
+
shutil.rmtree(ws, ignore_errors=True)
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
raise SpecGenerationError(
|
|
136
|
+
f"Pipeline execution failed after {_MAX_RETRIES + 1} attempts: {last_error}"
|
|
137
|
+
)
|
nveil/spec.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""NveilSpec — opaque visualization specification with local rendering.
|
|
2
|
+
|
|
3
|
+
Once generated, a spec can be reused unlimited times on new data with
|
|
4
|
+
compatible columns — no API call, no cost.
|
|
5
|
+
|
|
6
|
+
All internal processing is handled by the compiled engine. The SDK
|
|
7
|
+
only stores and passes opaque byte blobs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NveilSpec:
|
|
16
|
+
"""Opaque visualization specification with local rendering.
|
|
17
|
+
|
|
18
|
+
Once generated, ``spec.render(new_data)`` is 100% local — no API call,
|
|
19
|
+
no cost. Reusable on any data with compatible columns.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, spec_blob: bytes):
|
|
23
|
+
self._blob = spec_blob
|
|
24
|
+
self._session = None # set by Session.generate_spec for workspace reuse
|
|
25
|
+
|
|
26
|
+
def render(self, data: Any = None) -> Any:
|
|
27
|
+
"""Render using the auto-detected best backend (Plotly, VTK, DeckGL).
|
|
28
|
+
|
|
29
|
+
100% local execution — no API call.
|
|
30
|
+
|
|
31
|
+
If called within a session context (``with nveil.session()``),
|
|
32
|
+
reuses the session's already-computed pipeline outputs —
|
|
33
|
+
no pipeline re-run.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
data: pandas DataFrame, dict of DataFrames, numpy array, or list.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Backend-specific figure object (plotly.graph_objects.Figure, etc.)
|
|
40
|
+
"""
|
|
41
|
+
from dive._engine import render as engine_render
|
|
42
|
+
from .timing import Timer
|
|
43
|
+
|
|
44
|
+
session = self._session
|
|
45
|
+
timer = session.timer if session else Timer()
|
|
46
|
+
|
|
47
|
+
pipeline_instance = None
|
|
48
|
+
if session and session._pipeline:
|
|
49
|
+
pipeline_instance = session._pipeline
|
|
50
|
+
|
|
51
|
+
with timer.measure("render"):
|
|
52
|
+
return engine_render(
|
|
53
|
+
spec_blob=self._blob,
|
|
54
|
+
data=data,
|
|
55
|
+
choregraph_instance=pipeline_instance,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def explanation(self) -> str:
|
|
60
|
+
"""Human-readable description of what was generated."""
|
|
61
|
+
from dive._engine import get_explanation
|
|
62
|
+
return get_explanation(self._blob)
|
|
63
|
+
|
|
64
|
+
def save(self, path: str) -> None:
|
|
65
|
+
"""Save to opaque .nveil file (encrypted binary)."""
|
|
66
|
+
from dive._engine import save_spec
|
|
67
|
+
save_spec(self._blob, path)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def load(path: str) -> NveilSpec:
|
|
71
|
+
"""Load from opaque .nveil file."""
|
|
72
|
+
from dive._engine import load_spec
|
|
73
|
+
blob = load_spec(path)
|
|
74
|
+
return NveilSpec(spec_blob=blob)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── Module-level display/export functions ──
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def show(fig: Any, theme: str = "dark") -> None:
|
|
81
|
+
"""Display a figure in the default browser.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
fig: Figure object returned by ``NveilSpec.render()``.
|
|
85
|
+
theme: Display theme ("dark" or "light").
|
|
86
|
+
"""
|
|
87
|
+
if fig is None:
|
|
88
|
+
raise RuntimeError("No figure to display")
|
|
89
|
+
|
|
90
|
+
import tempfile
|
|
91
|
+
import webbrowser
|
|
92
|
+
from dive.builder.export import export_image
|
|
93
|
+
|
|
94
|
+
html = export_image(fig, extension="html", theme=theme)
|
|
95
|
+
|
|
96
|
+
tmp = tempfile.NamedTemporaryFile(
|
|
97
|
+
suffix=".html", delete=False, mode="w", encoding="utf-8",
|
|
98
|
+
)
|
|
99
|
+
tmp.write(html)
|
|
100
|
+
tmp.close()
|
|
101
|
+
webbrowser.open(f"file://{tmp.name}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def save_image(
|
|
105
|
+
fig: Any,
|
|
106
|
+
path: str,
|
|
107
|
+
theme: str = "dark",
|
|
108
|
+
width: int = 1200,
|
|
109
|
+
height: int = 800,
|
|
110
|
+
scale: int = 1,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Save a figure as a static image.
|
|
113
|
+
|
|
114
|
+
Format is inferred from the file extension.
|
|
115
|
+
Supported: .png, .jpg, .svg, .pdf (require kaleido), .html
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
fig: Figure object returned by ``NveilSpec.render()``.
|
|
119
|
+
path: Output file path (e.g. ``"chart.png"``).
|
|
120
|
+
theme: Export theme ("dark" or "light").
|
|
121
|
+
width: Image width in pixels.
|
|
122
|
+
height: Image height in pixels.
|
|
123
|
+
scale: Font/margin scale factor.
|
|
124
|
+
"""
|
|
125
|
+
if fig is None:
|
|
126
|
+
raise RuntimeError("No figure to export")
|
|
127
|
+
|
|
128
|
+
from dive.builder.export import export_to_file
|
|
129
|
+
export_to_file(fig, path, theme=theme, width=width, height=height, scale=scale)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def save_html(fig: Any, path: str, theme: str = "dark") -> None:
|
|
133
|
+
"""Save a figure as an interactive HTML file.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
fig: Figure object returned by ``NveilSpec.render()``.
|
|
137
|
+
path: Output file path (e.g. ``"chart.html"``).
|
|
138
|
+
theme: Export theme ("dark" or "light").
|
|
139
|
+
"""
|
|
140
|
+
from dive.builder.export import export_to_file
|
|
141
|
+
export_to_file(fig, path, theme=theme)
|
nveil/timing.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Lightweight timing instrumentation for the NVEIL SDK.
|
|
2
|
+
|
|
3
|
+
Controlled via ``nveil.configure(timing=True)``. When disabled (default),
|
|
4
|
+
all operations are no-ops with zero overhead.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Timer:
|
|
11
|
+
"""Tracks named durations as an ordered list of (label, seconds) pairs."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, enabled: bool = False):
|
|
14
|
+
self.enabled = enabled
|
|
15
|
+
self._entries: list[tuple[str, float]] = []
|
|
16
|
+
|
|
17
|
+
def measure(self, label: str):
|
|
18
|
+
"""Context manager that records the duration of a block.
|
|
19
|
+
|
|
20
|
+
Usage::
|
|
21
|
+
|
|
22
|
+
with timer.measure("API call"):
|
|
23
|
+
response = client.post(...)
|
|
24
|
+
"""
|
|
25
|
+
if not self.enabled:
|
|
26
|
+
return _NOOP
|
|
27
|
+
return _TimerContext(self, label)
|
|
28
|
+
|
|
29
|
+
def record(self, label: str, duration: float):
|
|
30
|
+
"""Manually record a duration."""
|
|
31
|
+
if self.enabled:
|
|
32
|
+
self._entries.append((label, duration))
|
|
33
|
+
|
|
34
|
+
def summary(self) -> str:
|
|
35
|
+
"""Return a formatted terminal table of all recorded timings."""
|
|
36
|
+
if not self._entries:
|
|
37
|
+
return ""
|
|
38
|
+
max_label = max(len(label) for label, _ in self._entries)
|
|
39
|
+
total = sum(d for _, d in self._entries)
|
|
40
|
+
width = max_label + 14
|
|
41
|
+
lines = ["", "\u2500" * width]
|
|
42
|
+
for label, dur in self._entries:
|
|
43
|
+
lines.append(f" {label:<{max_label}} {dur:>6.2f}s")
|
|
44
|
+
lines.append("\u2500" * width)
|
|
45
|
+
lines.append(f" {'Total':<{max_label}} {total:>6.2f}s")
|
|
46
|
+
lines.append("")
|
|
47
|
+
return "\n".join(lines)
|
|
48
|
+
|
|
49
|
+
def clear(self):
|
|
50
|
+
"""Reset all recorded entries."""
|
|
51
|
+
self._entries.clear()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _TimerContext:
|
|
55
|
+
__slots__ = ("_timer", "_label", "_t0")
|
|
56
|
+
|
|
57
|
+
def __init__(self, timer: Timer, label: str):
|
|
58
|
+
self._timer = timer
|
|
59
|
+
self._label = label
|
|
60
|
+
|
|
61
|
+
def __enter__(self):
|
|
62
|
+
self._t0 = time.perf_counter()
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, *_):
|
|
66
|
+
self._timer.record(self._label, time.perf_counter() - self._t0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _NoOp:
|
|
70
|
+
"""Singleton no-op context manager — zero allocation when timing is off."""
|
|
71
|
+
__slots__ = ()
|
|
72
|
+
def __enter__(self): return self
|
|
73
|
+
def __exit__(self, *_): pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_NOOP = _NoOp()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nveil
|
|
3
|
+
Version: 0.0.1.dev1
|
|
4
|
+
Summary: NVEIL Python SDK — no-code AI data visualization with auditable, deterministic results
|
|
5
|
+
Author-email: NVEIL <contact@nveil.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: httpx>=0.25
|
|
11
|
+
Requires-Dist: pydantic>=2.0
|
|
12
|
+
Requires-Dist: nveil-dive[builder]
|
|
13
|
+
Requires-Dist: nveil-choregraph
|
|
14
|
+
Requires-Dist: lxml>=4.9
|
|
15
|
+
Requires-Dist: xmlschema>=3.0
|
|
16
|
+
Requires-Dist: natsort>=8.0
|
|
17
|
+
Requires-Dist: pandas>=2.0
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Requires-Dist: cryptography>=41.0
|
|
20
|
+
Requires-Dist: clingo>=5.6
|
|
21
|
+
Requires-Dist: plotly>=5.0
|
|
22
|
+
Requires-Dist: kaleido>=0.2
|
|
23
|
+
Requires-Dist: vtk>=9.0
|
|
24
|
+
Requires-Dist: pydeck>=0.8
|
|
25
|
+
Requires-Dist: scipy>=1.10
|
|
26
|
+
Requires-Dist: kedro>=0.19.0
|
|
27
|
+
Requires-Dist: kedro-datasets[pillow]>=3.0.0
|
|
28
|
+
Requires-Dist: pyarrow>=22.0.0
|
|
29
|
+
Requires-Dist: python-dotenv
|
|
30
|
+
Requires-Dist: geopandas
|
|
31
|
+
Requires-Dist: geonamescache
|
|
32
|
+
Requires-Dist: langdetect
|
|
33
|
+
Requires-Dist: simplemma
|
|
34
|
+
Requires-Dist: unidecode
|
|
35
|
+
Requires-Dist: rapidfuzz
|
|
36
|
+
Requires-Dist: scikit-learn>=1.3.0
|
|
37
|
+
Requires-Dist: openpyxl
|
|
38
|
+
Requires-Dist: pyexcel
|
|
39
|
+
Requires-Dist: pyexcel-xlsx
|
|
40
|
+
Requires-Dist: pyexcel-xls
|
|
41
|
+
Requires-Dist: pyexcel-ods3
|
|
42
|
+
Requires-Dist: statsmodels
|
|
43
|
+
Provides-Extra: agent
|
|
44
|
+
Requires-Dist: watchdog>=3.0; extra == "agent"
|
|
45
|
+
Requires-Dist: pystray>=0.19; extra == "agent"
|
|
46
|
+
Provides-Extra: docs
|
|
47
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
48
|
+
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
49
|
+
Requires-Dist: mkdocs-section-index>=0.3; extra == "docs"
|
|
50
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
Dynamic: requires-dist
|
|
53
|
+
|
|
54
|
+
# NVEIL Python SDK
|
|
55
|
+
|
|
56
|
+
**Turn your data into production-ready visualizations in seconds.**
|
|
57
|
+
|
|
58
|
+
NVEIL is a no-code AI data visualization platform. Describe what you want in plain language, and the SDK generates auditable, deterministic visualizations — no hallucinations, no manual chart building.
|
|
59
|
+
|
|
60
|
+
Your data stays on your machine. Only metadata (column names, types, and statistics) is sent to the NVEIL server.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install nveil
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quickstart
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import nveil
|
|
72
|
+
import pandas as pd
|
|
73
|
+
|
|
74
|
+
nveil.configure(api_key="nveil_...")
|
|
75
|
+
|
|
76
|
+
df = pd.read_csv("sales.csv")
|
|
77
|
+
spec = nveil.generate_spec("Revenue by region, colored by quarter", df)
|
|
78
|
+
|
|
79
|
+
fig = spec.render(df)
|
|
80
|
+
nveil.show(fig)
|
|
81
|
+
|
|
82
|
+
# Save for later — renders offline, no API call
|
|
83
|
+
spec.save("revenue.nveil")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Key Features
|
|
87
|
+
|
|
88
|
+
- **No-code AI** — describe your visualization in plain language.
|
|
89
|
+
- **Auditable results** — deterministic output, no hallucinations.
|
|
90
|
+
- **Data stays local** — only column names, types, and aggregate statistics are sent. Raw data never leaves your machine.
|
|
91
|
+
- **Offline rendering** — once generated, `render()` runs 100% locally with no API call.
|
|
92
|
+
- **Reusable specs** — save to `.nveil` files, load later, render on new data.
|
|
93
|
+
- **Multi-backend** — auto-detects the best rendering engine (Plotly, VTK, DeckGL).
|
|
94
|
+
|
|
95
|
+
## Getting an API Key
|
|
96
|
+
|
|
97
|
+
1. Create an account at [app.nveil.com](https://app.nveil.com)
|
|
98
|
+
2. Go to **Settings** in your account
|
|
99
|
+
3. Generate an API key
|
|
100
|
+
|
|
101
|
+
## Documentation
|
|
102
|
+
|
|
103
|
+
Full documentation: [https://docs.nveil.com](https://docs.nveil.com)
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
Proprietary. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
nveil/__init__.py,sha256=Sff2TT0v1DDLTWxXIrn02ukQLAsApOXOkzHG-03izp4,7500
|
|
2
|
+
nveil/client.py,sha256=STEy5MnVuP7Iwy9zcPuAskofpPBVRXHQ08pbMjxFG4I,3427
|
|
3
|
+
nveil/exceptions.py,sha256=H5uS3Yx8bHervtx3B3azJLSfm3Lf_cIkxQ12A8j6sRs,664
|
|
4
|
+
nveil/session.py,sha256=6ILOFJjGdVQPG6T0GrMJSL3gGs4QebtNTiGxGKPpNAQ,5018
|
|
5
|
+
nveil/spec.py,sha256=7swyjvBMC1awricqHKTUFvk7QU93bU4zke54OlwY8Q0,4453
|
|
6
|
+
nveil/timing.py,sha256=T2lATFPRgbKSg3tse-GbdnVp0Eo67Xn1X_uBdKvBEXM,2250
|
|
7
|
+
nveil-0.0.1.dev1.dist-info/licenses/LICENSE,sha256=OJQKdgGM9BWFndOy0ItoA20LjQDEzt-jfqyH-mq2trw,3228
|
|
8
|
+
nveil-0.0.1.dev1.dist-info/METADATA,sha256=Mm_FGJlNg6vL3CuNJFbZm8cK5cDBSjxkn0U5cyrNxtc,3350
|
|
9
|
+
nveil-0.0.1.dev1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
nveil-0.0.1.dev1.dist-info/top_level.txt,sha256=ZHHSexRTpJpasRnJJWnb3xytUMI4LWQhtEvbAGbrobs,6
|
|
11
|
+
nveil-0.0.1.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
NVEIL SOFTWARE LICENSE AGREEMENT
|
|
2
|
+
|
|
3
|
+
Version 1.0 — Effective April 2026
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2025-2026 NVEIL SAS. All rights reserved.
|
|
6
|
+
|
|
7
|
+
By installing or using the NVEIL SDK and its bundled libraries
|
|
8
|
+
("Software"), you agree to the following terms.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
1. LICENSE GRANT
|
|
12
|
+
|
|
13
|
+
NVEIL grants you a limited, non-exclusive, non-transferable,
|
|
14
|
+
revocable license to:
|
|
15
|
+
|
|
16
|
+
(a) Install and use the Software to develop applications that
|
|
17
|
+
connect to the NVEIL platform via authorized API keys.
|
|
18
|
+
|
|
19
|
+
(b) Distribute applications that include the Software as a
|
|
20
|
+
runtime dependency, for both commercial and non-commercial
|
|
21
|
+
purposes.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
2. RESTRICTIONS
|
|
25
|
+
|
|
26
|
+
You may not:
|
|
27
|
+
|
|
28
|
+
(a) Reverse engineer, decompile, disassemble, or attempt to
|
|
29
|
+
derive the source code of any compiled component of the
|
|
30
|
+
Software, including but not limited to the visualization
|
|
31
|
+
engine and data processing libraries.
|
|
32
|
+
|
|
33
|
+
(b) Modify, adapt, or create derivative works of the Software.
|
|
34
|
+
|
|
35
|
+
(c) Remove or alter any copyright, trademark, or other
|
|
36
|
+
proprietary notices contained in the Software.
|
|
37
|
+
|
|
38
|
+
(d) Redistribute the Software as a standalone package or
|
|
39
|
+
include it in a software distribution unrelated to your
|
|
40
|
+
application.
|
|
41
|
+
|
|
42
|
+
(e) Use the Software to develop a product or service that
|
|
43
|
+
competes with or substitutes for the NVEIL platform.
|
|
44
|
+
|
|
45
|
+
(f) Share, publish, or transfer your API key to unauthorized
|
|
46
|
+
third parties.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
3. API KEY AND USAGE
|
|
50
|
+
|
|
51
|
+
The Software requires a valid API key issued through the NVEIL
|
|
52
|
+
platform (https://app.nveil.com). Usage is subject to the rate
|
|
53
|
+
limits and terms associated with your account. NVEIL may revoke
|
|
54
|
+
API keys that violate these terms.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
4. INTELLECTUAL PROPERTY
|
|
58
|
+
|
|
59
|
+
The Software, including all algorithms, data formats, compiled
|
|
60
|
+
libraries, and documentation, is and remains the exclusive
|
|
61
|
+
intellectual property of NVEIL SAS. This license does not convey
|
|
62
|
+
any ownership rights.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
5. DATA PRIVACY
|
|
66
|
+
|
|
67
|
+
The Software is designed so that your raw data is processed
|
|
68
|
+
locally. Only metadata (column names, types, and aggregate
|
|
69
|
+
statistics) is transmitted to the NVEIL server. NVEIL does not
|
|
70
|
+
store or access your raw data.
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
6. DISCLAIMER OF WARRANTIES
|
|
74
|
+
|
|
75
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
|
76
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
77
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
|
|
78
|
+
NON-INFRINGEMENT. NVEIL DOES NOT WARRANT THAT THE SOFTWARE
|
|
79
|
+
WILL BE ERROR-FREE OR UNINTERRUPTED.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
7. LIMITATION OF LIABILITY
|
|
83
|
+
|
|
84
|
+
IN NO EVENT SHALL NVEIL BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
|
85
|
+
SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM YOUR
|
|
86
|
+
USE OF THE SOFTWARE, REGARDLESS OF THE CAUSE OF ACTION.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
8. TERMINATION
|
|
90
|
+
|
|
91
|
+
This license is effective until terminated. It terminates
|
|
92
|
+
automatically if you breach any of its terms. NVEIL may also
|
|
93
|
+
terminate it at any time by providing notice. Upon termination,
|
|
94
|
+
you must cease all use of the Software and destroy all copies.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
9. GOVERNING LAW
|
|
98
|
+
|
|
99
|
+
This agreement is governed by the laws of France. Any disputes
|
|
100
|
+
shall be subject to the exclusive jurisdiction of the courts
|
|
101
|
+
of Paris, France.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
Contact: contact@nveil.com
|
|
105
|
+
Website: https://nveil.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nveil
|