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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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