ripv-py 1.0.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.
- pv/__init__.py +48 -0
- pv/client.py +220 -0
- ripv_py-1.0.0.dist-info/METADATA +125 -0
- ripv_py-1.0.0.dist-info/RECORD +6 -0
- ripv_py-1.0.0.dist-info/WHEEL +5 -0
- ripv_py-1.0.0.dist-info/top_level.txt +1 -0
pv/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""PersonalVault Python SDK - secure API key retrieval for backend applications.
|
|
2
|
+
|
|
3
|
+
Usage (sync):
|
|
4
|
+
import pv
|
|
5
|
+
pv.init()
|
|
6
|
+
openai_key = pv("OPENAI_API_KEY")
|
|
7
|
+
|
|
8
|
+
Usage (async):
|
|
9
|
+
from pv import pv_async
|
|
10
|
+
openai_key = await pv_async("OPENAI_API_KEY")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import types
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from pv.client import (
|
|
17
|
+
pv,
|
|
18
|
+
pv_async,
|
|
19
|
+
init,
|
|
20
|
+
PVAuthError,
|
|
21
|
+
PVDeniedError,
|
|
22
|
+
PVKeyNotFoundError,
|
|
23
|
+
PVNotInitializedError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"pv",
|
|
28
|
+
"pv_async",
|
|
29
|
+
"init",
|
|
30
|
+
"PVAuthError",
|
|
31
|
+
"PVDeniedError",
|
|
32
|
+
"PVKeyNotFoundError",
|
|
33
|
+
"PVNotInitializedError",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "1.0.0"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _CallableModule(types.ModuleType):
|
|
40
|
+
"""Makes ``import pv; pv("KEY")`` work by forwarding calls to ``pv.pv``."""
|
|
41
|
+
|
|
42
|
+
def __call__(self, key_name: str) -> str:
|
|
43
|
+
return pv(key_name)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Replace this module's class so the module itself is callable.
|
|
47
|
+
_self = sys.modules[__name__]
|
|
48
|
+
_self.__class__ = _CallableModule
|
pv/client.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Core PersonalVault SDK client with sync and async key retrieval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"pv",
|
|
13
|
+
"pv_async",
|
|
14
|
+
"init",
|
|
15
|
+
"PVAuthError",
|
|
16
|
+
"PVDeniedError",
|
|
17
|
+
"PVKeyNotFoundError",
|
|
18
|
+
"PVNotInitializedError",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
_SDK_VERSION = "1.0.0"
|
|
22
|
+
_USER_AGENT = f"pv-python-sdk/{_SDK_VERSION}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Exceptions
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
class PVAuthError(Exception):
|
|
30
|
+
"""Raised when the PV_API_KEY is missing, invalid, or expired."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PVDeniedError(Exception):
|
|
34
|
+
"""Raised when the server rejects the request (403)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PVKeyNotFoundError(Exception):
|
|
38
|
+
"""Raised when a requested key is not present in the vault cache."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PVNotInitializedError(Exception):
|
|
42
|
+
"""Raised when pv() is called before the SDK has been initialized."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Internal state
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
_key_cache: Dict[str, str] = {}
|
|
50
|
+
_initialized: bool = False
|
|
51
|
+
_cache_expiry: float = 0.0
|
|
52
|
+
_cache_ttl: int = int(os.environ.get("PV_CACHE_TTL", "3600"))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_cache_valid() -> bool:
|
|
56
|
+
return _initialized and time.time() < _cache_expiry
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_api_key() -> str:
|
|
60
|
+
key = os.environ.get("PV_API_KEY")
|
|
61
|
+
if not key:
|
|
62
|
+
raise PVAuthError(
|
|
63
|
+
"PersonalVault: PV_API_KEY not found in environment.\n"
|
|
64
|
+
"Generate one at https://pv.sprkt.xyz/dashboard -> Backend Keys"
|
|
65
|
+
)
|
|
66
|
+
return key
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_api_url() -> str:
|
|
70
|
+
return os.environ.get("PV_API_URL", "https://pv.sprkt.xyz")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_headers(api_key: str) -> Dict[str, str]:
|
|
74
|
+
return {
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"Authorization": f"Bearer {api_key}",
|
|
77
|
+
"User-Agent": _USER_AGENT,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _handle_error_response(status_code: int, data: dict) -> None:
|
|
82
|
+
error_msg = data.get("error", "")
|
|
83
|
+
if status_code == 401:
|
|
84
|
+
raise PVAuthError(error_msg or "Invalid or expired backend key")
|
|
85
|
+
if status_code == 403:
|
|
86
|
+
raise PVDeniedError(error_msg or "Access denied")
|
|
87
|
+
raise Exception(error_msg or f"Request failed with status {status_code}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _apply_fetched_keys(keys: Dict[str, str]) -> None:
|
|
91
|
+
global _key_cache, _initialized, _cache_expiry
|
|
92
|
+
_key_cache = keys
|
|
93
|
+
_initialized = True
|
|
94
|
+
_cache_expiry = time.time() + _cache_ttl
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Sync fetch (uses httpx.Client)
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def _fetch_keys_sync(key_names: Optional[List[str]] = None) -> Dict[str, str]:
|
|
102
|
+
api_key = _get_api_key()
|
|
103
|
+
url = f"{_get_api_url()}/api/backend/keys"
|
|
104
|
+
body: dict = {}
|
|
105
|
+
if key_names:
|
|
106
|
+
body["keyNames"] = key_names
|
|
107
|
+
|
|
108
|
+
with httpx.Client() as client:
|
|
109
|
+
resp = client.post(url, json=body, headers=_build_headers(api_key))
|
|
110
|
+
|
|
111
|
+
if resp.status_code != 200:
|
|
112
|
+
try:
|
|
113
|
+
data = resp.json()
|
|
114
|
+
except Exception:
|
|
115
|
+
data = {}
|
|
116
|
+
_handle_error_response(resp.status_code, data)
|
|
117
|
+
|
|
118
|
+
return resp.json()["keys"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Async fetch (uses httpx.AsyncClient)
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
async def _fetch_keys_async(key_names: Optional[List[str]] = None) -> Dict[str, str]:
|
|
126
|
+
api_key = _get_api_key()
|
|
127
|
+
url = f"{_get_api_url()}/api/backend/keys"
|
|
128
|
+
body: dict = {}
|
|
129
|
+
if key_names:
|
|
130
|
+
body["keyNames"] = key_names
|
|
131
|
+
|
|
132
|
+
async with httpx.AsyncClient() as client:
|
|
133
|
+
resp = await client.post(url, json=body, headers=_build_headers(api_key))
|
|
134
|
+
|
|
135
|
+
if resp.status_code != 200:
|
|
136
|
+
try:
|
|
137
|
+
data = resp.json()
|
|
138
|
+
except Exception:
|
|
139
|
+
data = {}
|
|
140
|
+
_handle_error_response(resp.status_code, data)
|
|
141
|
+
|
|
142
|
+
return resp.json()["keys"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Public API
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def init() -> None:
|
|
150
|
+
"""Synchronously initialize the SDK by fetching keys from PersonalVault.
|
|
151
|
+
|
|
152
|
+
Use this in synchronous frameworks (Django, Flask) that don't have an
|
|
153
|
+
event loop. Blocks until the fetch completes.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
PVAuthError: If PV_API_KEY is missing or invalid.
|
|
157
|
+
PVDeniedError: If the server rejects the request.
|
|
158
|
+
"""
|
|
159
|
+
if _is_cache_valid():
|
|
160
|
+
return
|
|
161
|
+
keys = _fetch_keys_sync()
|
|
162
|
+
_apply_fetched_keys(keys)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def pv(key_name: str) -> str:
|
|
166
|
+
"""Synchronously retrieve a key from the in-memory cache.
|
|
167
|
+
|
|
168
|
+
The SDK must be initialized first via ``init()`` or ``pv_async()``.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
key_name: The name of the key to retrieve.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
The key value as a string.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
PVNotInitializedError: If the SDK has not been initialized or the
|
|
178
|
+
cache has expired.
|
|
179
|
+
PVKeyNotFoundError: If the key is not in the cache.
|
|
180
|
+
"""
|
|
181
|
+
if not _is_cache_valid():
|
|
182
|
+
raise PVNotInitializedError(
|
|
183
|
+
"PersonalVault: not initialized. "
|
|
184
|
+
"Use pv_async() for the first call or call init()."
|
|
185
|
+
)
|
|
186
|
+
value = _key_cache.get(key_name)
|
|
187
|
+
if value is None:
|
|
188
|
+
raise PVKeyNotFoundError(
|
|
189
|
+
f"Key '{key_name}' not found in vault or not allowed for this backend key"
|
|
190
|
+
)
|
|
191
|
+
return value
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def pv_async(key_name: str) -> str:
|
|
195
|
+
"""Asynchronously retrieve a key, initializing the SDK if needed.
|
|
196
|
+
|
|
197
|
+
On the first call (or after cache expiry), this fetches keys from the
|
|
198
|
+
PersonalVault server. Subsequent calls return from the in-memory cache.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
key_name: The name of the key to retrieve.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
The key value as a string.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
PVAuthError: If PV_API_KEY is missing or invalid.
|
|
208
|
+
PVDeniedError: If the server rejects the request.
|
|
209
|
+
PVKeyNotFoundError: If the key is not in the fetched results.
|
|
210
|
+
"""
|
|
211
|
+
if not _is_cache_valid():
|
|
212
|
+
keys = await _fetch_keys_async()
|
|
213
|
+
_apply_fetched_keys(keys)
|
|
214
|
+
|
|
215
|
+
value = _key_cache.get(key_name)
|
|
216
|
+
if value is None:
|
|
217
|
+
raise PVKeyNotFoundError(
|
|
218
|
+
f"Key '{key_name}' not found in vault or not allowed for this backend key"
|
|
219
|
+
)
|
|
220
|
+
return value
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ripv-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for PersonalVault - secure API key retrieval
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx>=0.24.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
12
|
+
Requires-Dist: respx>=0.20; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# PersonalVault Python SDK
|
|
15
|
+
|
|
16
|
+
Secure API key retrieval for Python backend applications.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install personalvault
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Environment Variables
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
export PV_API_KEY="pvault_bk_..." # Required - your backend key
|
|
30
|
+
export PV_API_URL="https://pv.example.com" # Optional - defaults to http://localhost:8923
|
|
31
|
+
export PV_CACHE_TTL="3600" # Optional - cache TTL in seconds (default: 3600)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Synchronous (Django, Flask)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from personalvault import init, pv
|
|
38
|
+
|
|
39
|
+
# Initialize once at startup
|
|
40
|
+
init()
|
|
41
|
+
|
|
42
|
+
# Retrieve keys instantly from cache
|
|
43
|
+
openai_key = pv("OPENAI_API_KEY")
|
|
44
|
+
db_password = pv("DB_PASS")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Asynchronous (FastAPI, AIOHTTP)
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from personalvault import pv_async
|
|
51
|
+
|
|
52
|
+
# Auto-initializes on first call, then serves from cache
|
|
53
|
+
openai_key = await pv_async("OPENAI_API_KEY")
|
|
54
|
+
db_password = await pv_async("DB_PASS")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### `init() -> None`
|
|
60
|
+
Synchronously fetch and cache all allowed keys. Blocks until complete.
|
|
61
|
+
|
|
62
|
+
### `pv(key_name: str) -> str`
|
|
63
|
+
Synchronous cache lookup. Raises `PVNotInitializedError` if `init()` hasn't been called or cache has expired.
|
|
64
|
+
|
|
65
|
+
### `pv_async(key_name: str) -> str`
|
|
66
|
+
Async key retrieval. Automatically initializes/refreshes the cache when needed.
|
|
67
|
+
|
|
68
|
+
## Exceptions
|
|
69
|
+
|
|
70
|
+
| Exception | When |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `PVAuthError` | `PV_API_KEY` missing, invalid, or expired |
|
|
73
|
+
| `PVDeniedError` | Server rejected request (IP allowlist, etc.) |
|
|
74
|
+
| `PVKeyNotFoundError` | Key not in vault or not allowed for this backend key |
|
|
75
|
+
| `PVNotInitializedError` | `pv()` called before `init()` or cache expired |
|
|
76
|
+
|
|
77
|
+
## Framework Examples
|
|
78
|
+
|
|
79
|
+
### FastAPI
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from contextlib import asynccontextmanager
|
|
83
|
+
from fastapi import FastAPI
|
|
84
|
+
from personalvault import pv_async, pv
|
|
85
|
+
|
|
86
|
+
@asynccontextmanager
|
|
87
|
+
async def lifespan(app: FastAPI):
|
|
88
|
+
await pv_async("DB_URL") # warm the cache
|
|
89
|
+
yield
|
|
90
|
+
|
|
91
|
+
app = FastAPI(lifespan=lifespan)
|
|
92
|
+
|
|
93
|
+
@app.get("/")
|
|
94
|
+
async def root():
|
|
95
|
+
return {"db": pv("DB_URL")}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Flask
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from flask import Flask
|
|
102
|
+
from personalvault import init, pv
|
|
103
|
+
|
|
104
|
+
app = Flask(__name__)
|
|
105
|
+
init() # fetch keys at import time
|
|
106
|
+
|
|
107
|
+
@app.route("/")
|
|
108
|
+
def index():
|
|
109
|
+
return {"db": pv("DB_URL")}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Django (settings.py)
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from personalvault import init, pv
|
|
116
|
+
|
|
117
|
+
init()
|
|
118
|
+
|
|
119
|
+
DATABASES = {
|
|
120
|
+
"default": {
|
|
121
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
122
|
+
"PASSWORD": pv("DB_PASS"),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pv/__init__.py,sha256=XvuXioP7vIF7FOXBqj4OloPhKfgri_IMTUuprwzkcD4,976
|
|
2
|
+
pv/client.py,sha256=vbmrJdvyxAq5mFTfL9M7EdQtrYb2tz0ot1EQ9OCaxOY,6676
|
|
3
|
+
ripv_py-1.0.0.dist-info/METADATA,sha256=NELmvI0azMZIFObytSEGZK41hmO71y3fvWotUb8siL4,2909
|
|
4
|
+
ripv_py-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
ripv_py-1.0.0.dist-info/top_level.txt,sha256=EfDDilaSYq7eE6JeB7uEi0pH_wB4I-t4tmKu5rqnjf4,3
|
|
6
|
+
ripv_py-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pv
|