veep 0.4.2__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.
veep/__init__.py ADDED
@@ -0,0 +1,112 @@
1
+ """veep -- Python SDK for Vector Panda vector search.
2
+
3
+ Module structure and method signatures:
4
+
5
+ from veep import VP
6
+
7
+ vp = VP(api_key="...", verbose=True)
8
+
9
+ # Collections (resource-based)
10
+ vp.collections.create("my_collection", tier="hot") -> Collection
11
+ vp.collections.get("my_collection") -> Collection
12
+ vp.collections.list() -> list[Collection]
13
+ vp.collections.delete("my_collection") -> None
14
+ vp.collections.status("my_collection") -> str
15
+
16
+ # Vectors / files
17
+ vp.vectors.upsert("col", "data.parquet") -> UploadResult (file)
18
+ vp.vectors.upsert("col", vectors=[{...}]) -> UploadResult (inline)
19
+ vp.vectors.upsert("col", dataframe=df) -> UploadResult (DataFrame)
20
+ vp.vectors.replace("col", "data.parquet") -> UploadResult
21
+ vp.vectors.query("col", [0.1, ...]) -> QueryResults
22
+ vp.vectors.query("col", [0.1, ...], filter={...}) -> QueryResults (filtered)
23
+ vp.vectors.query_batch([...]) -> list[QueryResults]
24
+ vp.vectors.delete("col", "data.parquet") -> None (file delete)
25
+ vp.vectors.delete("col", ids=["k1", "k2"]) -> dict (id delete)
26
+ vp.vectors.list_files("col") -> list[FileInfo]
27
+
28
+ # Schema
29
+ vp.schema.get("my_collection") -> SchemaInfo
30
+ vp.schema.confirm("my_collection", id_field, vec_field) -> dict
31
+
32
+ # Health
33
+ vp.ping() -> bool
34
+
35
+ # Authentication (device flow + credential persistence)
36
+ vp = VP.login() # interactive OAuth via browser
37
+ vp = VP.from_creds() # load ~/.veep/credentials.json
38
+ vp.save() # persist api_key for later
39
+
40
+ API endpoints (all through consumer-site .120):
41
+ POST /api/v1/collections create collection
42
+ GET /api/v1/collections list collections
43
+ GET /api/v1/collections/:name get collection detail
44
+ GET /api/v1/collections/:name/status lightweight status
45
+ DELETE /api/v1/collections/:name delete collection
46
+ POST /api/v1/collections/:name/files/:filename upload file (multipart)
47
+ PUT /api/v1/collections/:name/files/:filename replace file (multipart)
48
+ DELETE /api/v1/collections/:name/files/:filename delete file
49
+ GET /api/v1/collections/:name/files list files
50
+ GET /api/v1/collections/:name/schema get schema
51
+ POST /api/v1/collections/:name/schema/confirm confirm schema
52
+ POST /api/v1/query single query
53
+ POST /api/v1/query/batch batch query
54
+ GET /api/v1/health health check
55
+ """
56
+
57
+ from __future__ import annotations
58
+
59
+ from .client import VP
60
+ from .collections import Collections
61
+ from .exceptions import (
62
+ AuthError,
63
+ CollectionAlreadyExistsError,
64
+ CollectionNotFoundError,
65
+ CollectionNotReadyError,
66
+ FileAlreadyExistsError,
67
+ NotFoundError,
68
+ QueryError,
69
+ ServerError,
70
+ TimeoutError,
71
+ UploadError,
72
+ ValidationError,
73
+ VeepError,
74
+ )
75
+ from .models import (
76
+ Collection,
77
+ FetchResult,
78
+ FileInfo,
79
+ QueryResults,
80
+ Result,
81
+ SchemaInfo,
82
+ UploadResult,
83
+ )
84
+ from .vectors import Vectors
85
+
86
+ __version__ = "0.4.2"
87
+
88
+ __all__ = [
89
+ "VP",
90
+ "Collections",
91
+ "Vectors",
92
+ "AuthError",
93
+ "Collection",
94
+ "CollectionAlreadyExistsError",
95
+ "CollectionNotFoundError",
96
+ "CollectionNotReadyError",
97
+ "FetchResult",
98
+ "FileAlreadyExistsError",
99
+ "FileInfo",
100
+ "NotFoundError",
101
+ "QueryError",
102
+ "QueryResults",
103
+ "Result",
104
+ "SchemaInfo",
105
+ "ServerError",
106
+ "TimeoutError",
107
+ "UploadError",
108
+ "UploadResult",
109
+ "ValidationError",
110
+ "VeepError",
111
+ "__version__",
112
+ ]
veep/auth.py ADDED
@@ -0,0 +1,211 @@
1
+ """Device authorization flow and credential persistence for veep.
2
+
3
+ Enables fully programmatic login from Python/Jupyter without
4
+ manually copying API keys:
5
+
6
+ from veep import VP
7
+ vp = VP.login() # opens browser, completes OAuth, returns client
8
+
9
+ Credentials are saved to ~/.veep/credentials.json and reused
10
+ on subsequent sessions via VP.from_creds().
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ import os
18
+ import time
19
+ import webbrowser
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ import requests
24
+
25
+ logger = logging.getLogger("veep")
26
+
27
+ CREDENTIALS_DIR = Path.home() / ".veep"
28
+ CREDENTIALS_FILE = CREDENTIALS_DIR / "credentials.json"
29
+
30
+ DEFAULT_HOST = "https://api.vectorpanda.com"
31
+
32
+
33
+ def save_credentials(api_key: str, host: str | None = None, **extra: Any) -> Path:
34
+ """Save API key and host to ~/.veep/credentials.json.
35
+
36
+ Args:
37
+ api_key: The API key to save.
38
+ host: The API host URL. Defaults to the Vector Panda cloud.
39
+ **extra: Additional fields to store (e.g., client_id).
40
+
41
+ Returns:
42
+ Path to the credentials file.
43
+ """
44
+ CREDENTIALS_DIR.mkdir(parents=True, exist_ok=True)
45
+ data = {"api_key": api_key, "host": host or DEFAULT_HOST, **extra}
46
+ CREDENTIALS_FILE.write_text(json.dumps(data, indent=2) + "\n")
47
+ CREDENTIALS_FILE.chmod(0o600)
48
+ logger.info("Credentials saved to %s", CREDENTIALS_FILE)
49
+ return CREDENTIALS_FILE
50
+
51
+
52
+ def load_credentials() -> dict | None:
53
+ """Load saved credentials from ~/.veep/credentials.json.
54
+
55
+ Returns:
56
+ Dict with at least 'api_key' and 'host', or None if not found.
57
+ """
58
+ if not CREDENTIALS_FILE.exists():
59
+ return None
60
+ try:
61
+ data = json.loads(CREDENTIALS_FILE.read_text())
62
+ if "api_key" in data:
63
+ return data
64
+ except (json.JSONDecodeError, OSError):
65
+ pass
66
+ return None
67
+
68
+
69
+ def clear_credentials() -> None:
70
+ """Remove saved credentials."""
71
+ if CREDENTIALS_FILE.exists():
72
+ CREDENTIALS_FILE.unlink()
73
+ logger.info("Credentials cleared")
74
+
75
+
76
+ def _is_notebook() -> bool:
77
+ """Detect if running in a Jupyter/IPython notebook."""
78
+ try:
79
+ from IPython import get_ipython
80
+ shell = get_ipython()
81
+ if shell is None:
82
+ return False
83
+ return shell.__class__.__name__ == "ZMQInteractiveShell"
84
+ except ImportError:
85
+ return False
86
+
87
+
88
+ def _display_link(url: str, user_code: str) -> None:
89
+ """Display the verification URL — clickable in notebooks, printed in terminals."""
90
+ if _is_notebook():
91
+ try:
92
+ from IPython.display import HTML, display
93
+ display(HTML(
94
+ f'<p>Open this link to sign in: <a href="{url}" target="_blank">{url}</a></p>'
95
+ f'<p>Your confirmation code: <b>{user_code}</b></p>'
96
+ ))
97
+ return
98
+ except ImportError:
99
+ pass
100
+ print(f"\nOpen this URL to sign in:\n {url}\n")
101
+ print(f"Your confirmation code: {user_code}\n")
102
+
103
+
104
+ def device_login(
105
+ host: str | None = None,
106
+ open_browser: bool = True,
107
+ timeout_s: int = 300,
108
+ ) -> dict:
109
+ """Run the device authorization flow.
110
+
111
+ 1. Requests a device code from the server
112
+ 2. Opens the verification URL in a browser (or prints it)
113
+ 3. Polls until the user completes OAuth in the browser
114
+ 4. Returns {"api_key": ..., "client_id": ..., "host": ...}
115
+
116
+ Args:
117
+ host: API base URL. Defaults to VEEP_HOST env var or Vector Panda cloud.
118
+ open_browser: Whether to automatically open the browser. Default True.
119
+ timeout_s: How long to wait for the user to complete login. Default 300s.
120
+
121
+ Returns:
122
+ Dict with api_key, client_id, and host.
123
+
124
+ Raises:
125
+ TimeoutError: If the user doesn't complete login in time.
126
+ ServerError: If the device flow is not supported or fails.
127
+ """
128
+ from .exceptions import ServerError, TimeoutError
129
+
130
+ host = (host or os.environ.get("VEEP_HOST", DEFAULT_HOST)).rstrip("/")
131
+
132
+ # Step 1: Request device code
133
+ try:
134
+ resp = requests.post(
135
+ f"{host}/api/v1/auth/device",
136
+ json={},
137
+ timeout=10,
138
+ )
139
+ except requests.exceptions.ConnectionError:
140
+ raise ServerError(
141
+ f"Could not connect to {host}. "
142
+ f"Check that the host is correct and the service is running."
143
+ ) from None
144
+
145
+ if resp.status_code != 200:
146
+ raise ServerError(
147
+ f"Device login not available at {host}. "
148
+ f"You may need to update your server or use VP(api_key=...) instead.",
149
+ status_code=resp.status_code,
150
+ )
151
+
152
+ data = resp.json()
153
+ device_code = data["device_code"]
154
+ user_code = data["user_code"]
155
+ verification_url = data["verification_url"]
156
+ interval = data.get("interval", 5)
157
+ expires_in = data.get("expires_in", timeout_s)
158
+
159
+ # Step 2: Show the URL to the user
160
+ _display_link(verification_url, user_code)
161
+
162
+ if open_browser:
163
+ try:
164
+ webbrowser.open(verification_url)
165
+ print("Browser opened. Complete sign-in there, then return here.")
166
+ except Exception:
167
+ print("Could not open browser automatically. Please open the URL above.")
168
+
169
+ print("Waiting for sign-in...", end="", flush=True)
170
+
171
+ # Step 3: Poll for completion
172
+ deadline = time.time() + min(expires_in, timeout_s)
173
+ while time.time() < deadline:
174
+ time.sleep(interval)
175
+ print(".", end="", flush=True)
176
+
177
+ try:
178
+ poll_resp = requests.post(
179
+ f"{host}/api/v1/auth/device/token",
180
+ json={"device_code": device_code},
181
+ timeout=10,
182
+ )
183
+ except requests.exceptions.RequestException:
184
+ continue
185
+
186
+ if poll_resp.status_code == 200:
187
+ result = poll_resp.json()
188
+ print(" done!\n")
189
+ logger.info("Login successful. Client ID: %s", result.get("client_id"))
190
+ return {
191
+ "api_key": result["api_key"],
192
+ "client_id": result.get("client_id", ""),
193
+ "host": host,
194
+ }
195
+
196
+ if poll_resp.status_code == 400:
197
+ body = poll_resp.json()
198
+ error = body.get("error", "")
199
+ if error == "authorization_pending":
200
+ continue
201
+ if error == "expired_token":
202
+ break
203
+ if error == "access_denied":
204
+ print(" denied.\n")
205
+ raise ServerError("Login was denied by the user.")
206
+
207
+ print(" timed out.\n")
208
+ raise TimeoutError(
209
+ f"Login was not completed within {timeout_s} seconds. "
210
+ f"Run VP.login() again to retry."
211
+ )