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 +112 -0
- veep/auth.py +211 -0
- veep/client.py +456 -0
- veep/collections.py +210 -0
- veep/exceptions.py +140 -0
- veep/models.py +153 -0
- veep/schema.py +177 -0
- veep/vectors.py +892 -0
- veep-0.4.2.dist-info/METADATA +313 -0
- veep-0.4.2.dist-info/RECORD +12 -0
- veep-0.4.2.dist-info/WHEEL +4 -0
- veep-0.4.2.dist-info/licenses/LICENSE +21 -0
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
|
+
)
|