envv 1.0.0__tar.gz
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.
- envv-1.0.0/PKG-INFO +80 -0
- envv-1.0.0/README.md +57 -0
- envv-1.0.0/envv.egg-info/PKG-INFO +80 -0
- envv-1.0.0/envv.egg-info/SOURCES.txt +12 -0
- envv-1.0.0/envv.egg-info/dependency_links.txt +1 -0
- envv-1.0.0/envv.egg-info/requires.txt +1 -0
- envv-1.0.0/envv.egg-info/top_level.txt +1 -0
- envv-1.0.0/envvault/__init__.py +24 -0
- envv-1.0.0/envvault/client.py +163 -0
- envv-1.0.0/envvault/config.py +59 -0
- envv-1.0.0/envvault/errors.py +53 -0
- envv-1.0.0/pyproject.toml +37 -0
- envv-1.0.0/setup.cfg +4 -0
- envv-1.0.0/tests/test_validation.py +78 -0
envv-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: envv
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official EnvVault Python SDK — centralized secrets management
|
|
5
|
+
Author: EnvVault Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://git.netsphere360.com/envvault/envvault-sdk
|
|
8
|
+
Project-URL: Repository, https://git.netsphere360.com/envvault/envvault-sdk
|
|
9
|
+
Keywords: envvault,secrets,management,sdk,vault
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: requests>=2.28.0
|
|
23
|
+
|
|
24
|
+
# envvault (Python SDK)
|
|
25
|
+
|
|
26
|
+
Official Python SDK for [EnvVault](https://git.netsphere360.com/envvault) — centralized secrets management.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install envvault
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Requires Python >= 3.10.**
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from envvault import EnvVaultClient
|
|
40
|
+
|
|
41
|
+
client = EnvVaultClient(
|
|
42
|
+
api_url="https://api.envvault.example.com",
|
|
43
|
+
token="your-token",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Or rely on env vars: ENVVAULT_API_URL, ENVVAULT_TOKEN
|
|
47
|
+
# Or config file: ~/.envv/config.json
|
|
48
|
+
|
|
49
|
+
value = client.get_secret("DATABASE_URL")
|
|
50
|
+
secrets = client.list_secrets()
|
|
51
|
+
client.set_secret("API_KEY", "sk-...")
|
|
52
|
+
client.delete_secret("OLD_KEY")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration Priority
|
|
56
|
+
|
|
57
|
+
| Priority | Source |
|
|
58
|
+
|----------|--------|
|
|
59
|
+
| 1 (highest) | Constructor args (`api_url`, `token`) |
|
|
60
|
+
| 2 | Environment variables (`ENVVAULT_API_URL`, `ENVVAULT_TOKEN`) |
|
|
61
|
+
| 3 (lowest) | Config file (`~/.envv/config.json`) |
|
|
62
|
+
|
|
63
|
+
## Error Handling
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from envvault import EnvVaultClient, AuthenticationError, NotFoundError
|
|
67
|
+
|
|
68
|
+
client = EnvVaultClient(api_url="...", token="...")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
value = client.get_secret("MY_KEY")
|
|
72
|
+
except AuthenticationError:
|
|
73
|
+
print("Invalid token")
|
|
74
|
+
except NotFoundError:
|
|
75
|
+
print("Secret not found")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
[MIT](../../LICENSE)
|
envv-1.0.0/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# envvault (Python SDK)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [EnvVault](https://git.netsphere360.com/envvault) — centralized secrets management.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install envvault
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Requires Python >= 3.10.**
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from envvault import EnvVaultClient
|
|
17
|
+
|
|
18
|
+
client = EnvVaultClient(
|
|
19
|
+
api_url="https://api.envvault.example.com",
|
|
20
|
+
token="your-token",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Or rely on env vars: ENVVAULT_API_URL, ENVVAULT_TOKEN
|
|
24
|
+
# Or config file: ~/.envv/config.json
|
|
25
|
+
|
|
26
|
+
value = client.get_secret("DATABASE_URL")
|
|
27
|
+
secrets = client.list_secrets()
|
|
28
|
+
client.set_secret("API_KEY", "sk-...")
|
|
29
|
+
client.delete_secret("OLD_KEY")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration Priority
|
|
33
|
+
|
|
34
|
+
| Priority | Source |
|
|
35
|
+
|----------|--------|
|
|
36
|
+
| 1 (highest) | Constructor args (`api_url`, `token`) |
|
|
37
|
+
| 2 | Environment variables (`ENVVAULT_API_URL`, `ENVVAULT_TOKEN`) |
|
|
38
|
+
| 3 (lowest) | Config file (`~/.envv/config.json`) |
|
|
39
|
+
|
|
40
|
+
## Error Handling
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from envvault import EnvVaultClient, AuthenticationError, NotFoundError
|
|
44
|
+
|
|
45
|
+
client = EnvVaultClient(api_url="...", token="...")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
value = client.get_secret("MY_KEY")
|
|
49
|
+
except AuthenticationError:
|
|
50
|
+
print("Invalid token")
|
|
51
|
+
except NotFoundError:
|
|
52
|
+
print("Secret not found")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: envv
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official EnvVault Python SDK — centralized secrets management
|
|
5
|
+
Author: EnvVault Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://git.netsphere360.com/envvault/envvault-sdk
|
|
8
|
+
Project-URL: Repository, https://git.netsphere360.com/envvault/envvault-sdk
|
|
9
|
+
Keywords: envvault,secrets,management,sdk,vault
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: requests>=2.28.0
|
|
23
|
+
|
|
24
|
+
# envvault (Python SDK)
|
|
25
|
+
|
|
26
|
+
Official Python SDK for [EnvVault](https://git.netsphere360.com/envvault) — centralized secrets management.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install envvault
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Requires Python >= 3.10.**
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from envvault import EnvVaultClient
|
|
40
|
+
|
|
41
|
+
client = EnvVaultClient(
|
|
42
|
+
api_url="https://api.envvault.example.com",
|
|
43
|
+
token="your-token",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Or rely on env vars: ENVVAULT_API_URL, ENVVAULT_TOKEN
|
|
47
|
+
# Or config file: ~/.envv/config.json
|
|
48
|
+
|
|
49
|
+
value = client.get_secret("DATABASE_URL")
|
|
50
|
+
secrets = client.list_secrets()
|
|
51
|
+
client.set_secret("API_KEY", "sk-...")
|
|
52
|
+
client.delete_secret("OLD_KEY")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration Priority
|
|
56
|
+
|
|
57
|
+
| Priority | Source |
|
|
58
|
+
|----------|--------|
|
|
59
|
+
| 1 (highest) | Constructor args (`api_url`, `token`) |
|
|
60
|
+
| 2 | Environment variables (`ENVVAULT_API_URL`, `ENVVAULT_TOKEN`) |
|
|
61
|
+
| 3 (lowest) | Config file (`~/.envv/config.json`) |
|
|
62
|
+
|
|
63
|
+
## Error Handling
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from envvault import EnvVaultClient, AuthenticationError, NotFoundError
|
|
67
|
+
|
|
68
|
+
client = EnvVaultClient(api_url="...", token="...")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
value = client.get_secret("MY_KEY")
|
|
72
|
+
except AuthenticationError:
|
|
73
|
+
print("Invalid token")
|
|
74
|
+
except NotFoundError:
|
|
75
|
+
print("Secret not found")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
[MIT](../../LICENSE)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
envv.egg-info/PKG-INFO
|
|
4
|
+
envv.egg-info/SOURCES.txt
|
|
5
|
+
envv.egg-info/dependency_links.txt
|
|
6
|
+
envv.egg-info/requires.txt
|
|
7
|
+
envv.egg-info/top_level.txt
|
|
8
|
+
envvault/__init__.py
|
|
9
|
+
envvault/client.py
|
|
10
|
+
envvault/config.py
|
|
11
|
+
envvault/errors.py
|
|
12
|
+
tests/test_validation.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
envvault
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""EnvVault Python SDK — centralized secrets management."""
|
|
2
|
+
|
|
3
|
+
from envvault.client import EnvVaultClient
|
|
4
|
+
from envvault.errors import (
|
|
5
|
+
EnvVaultError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
AuthorizationError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
NetworkError,
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
__all__ = [
|
|
16
|
+
"EnvVaultClient",
|
|
17
|
+
"EnvVaultError",
|
|
18
|
+
"AuthenticationError",
|
|
19
|
+
"AuthorizationError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"NetworkError",
|
|
22
|
+
"ConfigurationError",
|
|
23
|
+
"ValidationError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""EnvVault HTTP client with typed error handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from envvault.config import resolve_config
|
|
11
|
+
from envvault.errors import (
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
AuthorizationError,
|
|
14
|
+
ConfigurationError,
|
|
15
|
+
EnvVaultError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
ValidationError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_DEFAULT_TIMEOUT = 30 # seconds
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EnvVaultClient:
|
|
25
|
+
"""EnvVault SDK client for managing secrets.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
api_url: The EnvVault API base URL.
|
|
29
|
+
token: Bearer authentication token.
|
|
30
|
+
timeout: Request timeout in seconds (default 30).
|
|
31
|
+
|
|
32
|
+
Configuration priority: constructor args > env vars > ~/.envv/config.json
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
api_url: Optional[str] = None,
|
|
38
|
+
token: Optional[str] = None,
|
|
39
|
+
timeout: int = _DEFAULT_TIMEOUT,
|
|
40
|
+
):
|
|
41
|
+
config = resolve_config(api_url=api_url, token=token)
|
|
42
|
+
self._api_url = (config["api_url"] or "").rstrip("/")
|
|
43
|
+
self._token = config["token"]
|
|
44
|
+
self._timeout = timeout
|
|
45
|
+
|
|
46
|
+
if not self._api_url:
|
|
47
|
+
raise ConfigurationError(
|
|
48
|
+
"API URL is not configured. Provide it via constructor, "
|
|
49
|
+
"ENVVAULT_API_URL, or ~/.envv/config.json."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self._session = requests.Session()
|
|
53
|
+
self._session.headers.update({
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
})
|
|
56
|
+
if self._token:
|
|
57
|
+
self._session.headers["Authorization"] = f"Bearer {self._token}"
|
|
58
|
+
|
|
59
|
+
# ── Public API ──────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def get_secret(self, key: str) -> str | None:
|
|
62
|
+
"""Fetch a single secret's decrypted value.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
key: The secret key.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The decrypted secret value, or None if not present in response.
|
|
69
|
+
"""
|
|
70
|
+
_validate_key(key)
|
|
71
|
+
data = self._request("GET", f"/cli/secrets/{quote(key, safe='')}")
|
|
72
|
+
return (data.get("data") or {}).get("value") or data.get("value")
|
|
73
|
+
|
|
74
|
+
def list_secrets(self) -> list[dict[str, Any]]:
|
|
75
|
+
"""List all secrets (metadata only, values not included).
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of secret metadata dicts.
|
|
79
|
+
"""
|
|
80
|
+
data = self._request("GET", "/cli/secrets")
|
|
81
|
+
return data.get("data", data) if isinstance(data, dict) else data
|
|
82
|
+
|
|
83
|
+
def set_secret(self, key: str, value: str) -> bool:
|
|
84
|
+
"""Create or update a secret.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
key: The secret key.
|
|
88
|
+
value: The secret value.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True on success.
|
|
92
|
+
"""
|
|
93
|
+
_validate_key(key)
|
|
94
|
+
_validate_value(value)
|
|
95
|
+
data = self._request("POST", "/cli/secrets", json={"key": key, "value": value})
|
|
96
|
+
return data.get("success", False)
|
|
97
|
+
|
|
98
|
+
def delete_secret(self, key: str) -> bool:
|
|
99
|
+
"""Delete a secret by key.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
key: The secret key.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True on success.
|
|
106
|
+
"""
|
|
107
|
+
_validate_key(key)
|
|
108
|
+
data = self._request("DELETE", f"/cli/secrets/{quote(key, safe='')}")
|
|
109
|
+
return data.get("success", False)
|
|
110
|
+
|
|
111
|
+
# ── Internal ────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
114
|
+
"""Execute an HTTP request and handle errors."""
|
|
115
|
+
url = f"{self._api_url}{path}"
|
|
116
|
+
try:
|
|
117
|
+
resp = self._session.request(
|
|
118
|
+
method, url, timeout=self._timeout, **kwargs
|
|
119
|
+
)
|
|
120
|
+
except requests.ConnectionError as exc:
|
|
121
|
+
raise NetworkError(str(exc)) from exc
|
|
122
|
+
except requests.Timeout as exc:
|
|
123
|
+
raise NetworkError(f"Request timed out: {exc}") from exc
|
|
124
|
+
except requests.RequestException as exc:
|
|
125
|
+
raise NetworkError(str(exc)) from exc
|
|
126
|
+
|
|
127
|
+
if resp.status_code >= 400:
|
|
128
|
+
self._raise_for_status(resp)
|
|
129
|
+
|
|
130
|
+
return resp.json()
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _raise_for_status(resp: requests.Response) -> None:
|
|
134
|
+
"""Map HTTP error responses to typed SDK errors."""
|
|
135
|
+
try:
|
|
136
|
+
body = resp.json()
|
|
137
|
+
except ValueError:
|
|
138
|
+
body = {}
|
|
139
|
+
|
|
140
|
+
message = body.get("error") or body.get("message") or resp.text
|
|
141
|
+
status = resp.status_code
|
|
142
|
+
|
|
143
|
+
if status == 401:
|
|
144
|
+
raise AuthenticationError(message)
|
|
145
|
+
if status == 403:
|
|
146
|
+
raise AuthorizationError(message)
|
|
147
|
+
if status == 404:
|
|
148
|
+
raise NotFoundError(message)
|
|
149
|
+
|
|
150
|
+
raise EnvVaultError(f"API Error ({status}): {message}", "API_ERROR", status)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── Validators ──────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _validate_key(key: Any) -> None:
|
|
157
|
+
if not isinstance(key, str) or not key.strip():
|
|
158
|
+
raise ValidationError("Secret key must be a non-empty string.")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _validate_value(value: Any) -> None:
|
|
162
|
+
if not isinstance(value, str):
|
|
163
|
+
raise ValidationError("Secret value must be a string.")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Configuration resolution for the EnvVault SDK.
|
|
2
|
+
|
|
3
|
+
Priority: Programmatic (constructor) > Environment Variables > Config File (~/.envv/config.json)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_config(
|
|
13
|
+
api_url: Optional[str] = None,
|
|
14
|
+
token: Optional[str] = None,
|
|
15
|
+
) -> dict:
|
|
16
|
+
"""Resolve SDK configuration from multiple sources.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
api_url: Programmatic API URL override (highest priority).
|
|
20
|
+
token: Programmatic token override (highest priority).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
dict with 'api_url' and 'token' keys.
|
|
24
|
+
"""
|
|
25
|
+
resolved_api_url = ""
|
|
26
|
+
resolved_token = None
|
|
27
|
+
|
|
28
|
+
# 1. Config file (lowest priority)
|
|
29
|
+
config_path = Path.home() / ".envv" / "config.json"
|
|
30
|
+
try:
|
|
31
|
+
if config_path.is_file():
|
|
32
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
33
|
+
file_config = json.load(f)
|
|
34
|
+
|
|
35
|
+
resolved_token = (
|
|
36
|
+
file_config.get("token")
|
|
37
|
+
or (file_config.get("config") or {}).get("token")
|
|
38
|
+
)
|
|
39
|
+
if file_config.get("api_base_url"):
|
|
40
|
+
resolved_api_url = file_config["api_base_url"]
|
|
41
|
+
except (json.JSONDecodeError, OSError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# 2. Environment variables override config file
|
|
45
|
+
env_token = os.environ.get("ENVVAULT_TOKEN")
|
|
46
|
+
if env_token:
|
|
47
|
+
resolved_token = env_token
|
|
48
|
+
|
|
49
|
+
env_api_url = os.environ.get("ENVVAULT_API_URL")
|
|
50
|
+
if env_api_url:
|
|
51
|
+
resolved_api_url = env_api_url
|
|
52
|
+
|
|
53
|
+
# 3. Programmatic overrides (highest priority)
|
|
54
|
+
if token:
|
|
55
|
+
resolved_token = token
|
|
56
|
+
if api_url:
|
|
57
|
+
resolved_api_url = api_url
|
|
58
|
+
|
|
59
|
+
return {"api_url": resolved_api_url, "token": resolved_token}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Typed error hierarchy for the EnvVault SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EnvVaultError(Exception):
|
|
5
|
+
"""Base error for all EnvVault SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, code: str, status_code: int | None = None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.message = message
|
|
10
|
+
self.code = code
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthenticationError(EnvVaultError):
|
|
15
|
+
"""Raised when authentication fails (HTTP 401)."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str = "Invalid or expired token."):
|
|
18
|
+
super().__init__(message, "UNAUTHORIZED", 401)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthorizationError(EnvVaultError):
|
|
22
|
+
"""Raised when access is denied (HTTP 403)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, message: str = "You do not have access to this resource."):
|
|
25
|
+
super().__init__(message, "FORBIDDEN", 403)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NotFoundError(EnvVaultError):
|
|
29
|
+
"""Raised when the requested resource is not found (HTTP 404)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str = "The requested resource was not found."):
|
|
32
|
+
super().__init__(message, "NOT_FOUND", 404)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NetworkError(EnvVaultError):
|
|
36
|
+
"""Raised on network-level failures."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str = "A network error occurred."):
|
|
39
|
+
super().__init__(message, "NETWORK_ERROR")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConfigurationError(EnvVaultError):
|
|
43
|
+
"""Raised when required SDK configuration is missing."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str = "SDK is not configured. Provide api_url and token."):
|
|
46
|
+
super().__init__(message, "CONFIGURATION_ERROR")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ValidationError(EnvVaultError):
|
|
50
|
+
"""Raised when a function receives invalid arguments."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str):
|
|
53
|
+
super().__init__(message, "VALIDATION_ERROR")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "envv"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official EnvVault Python SDK — centralized secrets management"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "EnvVault Team"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["envvault", "secrets", "management", "sdk", "vault"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Security",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"requests>=2.28.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://git.netsphere360.com/envvault/envvault-sdk"
|
|
34
|
+
Repository = "https://git.netsphere360.com/envvault/envvault-sdk"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["envvault*"]
|
envv-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Tests for input validation and SDK exports."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from envvault import (
|
|
6
|
+
EnvVaultClient,
|
|
7
|
+
EnvVaultError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
AuthorizationError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
ConfigurationError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestExports:
|
|
18
|
+
"""Verify all expected symbols are importable."""
|
|
19
|
+
|
|
20
|
+
def test_client_class_exists(self):
|
|
21
|
+
assert callable(EnvVaultClient)
|
|
22
|
+
|
|
23
|
+
def test_error_classes_exist(self):
|
|
24
|
+
for cls in (
|
|
25
|
+
EnvVaultError,
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
AuthorizationError,
|
|
28
|
+
NotFoundError,
|
|
29
|
+
NetworkError,
|
|
30
|
+
ConfigurationError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
):
|
|
33
|
+
assert issubclass(cls, Exception)
|
|
34
|
+
|
|
35
|
+
def test_error_hierarchy(self):
|
|
36
|
+
assert issubclass(AuthenticationError, EnvVaultError)
|
|
37
|
+
assert issubclass(AuthorizationError, EnvVaultError)
|
|
38
|
+
assert issubclass(NotFoundError, EnvVaultError)
|
|
39
|
+
assert issubclass(NetworkError, EnvVaultError)
|
|
40
|
+
assert issubclass(ConfigurationError, EnvVaultError)
|
|
41
|
+
assert issubclass(ValidationError, EnvVaultError)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestValidation:
|
|
45
|
+
"""Verify input validation raises ValidationError."""
|
|
46
|
+
|
|
47
|
+
def _make_client(self):
|
|
48
|
+
"""Create a client that will fail on HTTP but pass validation."""
|
|
49
|
+
return EnvVaultClient(api_url="http://localhost:9999", token="test")
|
|
50
|
+
|
|
51
|
+
def test_get_secret_empty_key(self):
|
|
52
|
+
client = self._make_client()
|
|
53
|
+
with pytest.raises(ValidationError):
|
|
54
|
+
client.get_secret("")
|
|
55
|
+
|
|
56
|
+
def test_get_secret_non_string_key(self):
|
|
57
|
+
client = self._make_client()
|
|
58
|
+
with pytest.raises(ValidationError):
|
|
59
|
+
client.get_secret(123)
|
|
60
|
+
|
|
61
|
+
def test_set_secret_empty_key(self):
|
|
62
|
+
client = self._make_client()
|
|
63
|
+
with pytest.raises(ValidationError):
|
|
64
|
+
client.set_secret("", "value")
|
|
65
|
+
|
|
66
|
+
def test_set_secret_non_string_value(self):
|
|
67
|
+
client = self._make_client()
|
|
68
|
+
with pytest.raises(ValidationError):
|
|
69
|
+
client.set_secret("key", 123)
|
|
70
|
+
|
|
71
|
+
def test_delete_secret_whitespace_key(self):
|
|
72
|
+
client = self._make_client()
|
|
73
|
+
with pytest.raises(ValidationError):
|
|
74
|
+
client.delete_secret(" ")
|
|
75
|
+
|
|
76
|
+
def test_missing_api_url_raises_config_error(self):
|
|
77
|
+
with pytest.raises(ConfigurationError):
|
|
78
|
+
EnvVaultClient(api_url="", token="t")
|