envis 0.0.1__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.
envis-0.0.1/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ .env
3
+ __pycache__
4
+ *_cache
5
+ .env.local
@@ -0,0 +1,3 @@
1
+ Copyright (c) 2026-present umairx25 <umairarhambd@gmail.com>
2
+
3
+ `envis` is released under a private source-available license: you can use the SDK to integrate with the Envisible service, but the backend platform remains proprietary.
envis-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: envis
3
+ Version: 0.0.1
4
+ Summary: Secure, simple, and powerful secret management.
5
+ Project-URL: Homepage, https://envisible.dev
6
+ Project-URL: Documentation, https://github.com/umairx25/envis#readme
7
+ Project-URL: Issues, https://github.com/umairx25/envis/issues
8
+ Project-URL: Source, https://github.com/umairx25/envis
9
+ Author-email: Umair Arham <contact@uarham.me>
10
+ License-Expression: MIT
11
+ License-File: LICENSE.txt
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+
15
+ # envis
16
+
17
+ [![PyPI - Version](https://img.shields.io/pypi/v/envis.svg)](https://pypi.org/project/envis)
18
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/envis.svg)](https://pypi.org/project/envis)
19
+
20
+ -----
21
+
22
+ ## Table of Contents
23
+
24
+ - [Installation](#installation)
25
+ - [License](#license)
26
+ - [Contact](#contact)
27
+
28
+ ## Installation
29
+
30
+ ```console
31
+ pip install envis
32
+ ```
33
+
34
+ ## License
35
+
36
+ `envis` is released under a private source-available license: you can use the SDK to integrate with https://envisible.dev, but the backend platform remains proprietary.
37
+
38
+ ## Contact
39
+ - [Website](https://envisible.dev)
40
+ - [Email](mailto:contact@uarham.me)
envis-0.0.1/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # envis
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/envis.svg)](https://pypi.org/project/envis)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/envis.svg)](https://pypi.org/project/envis)
5
+
6
+ -----
7
+
8
+ ## Table of Contents
9
+
10
+ - [Installation](#installation)
11
+ - [License](#license)
12
+ - [Contact](#contact)
13
+
14
+ ## Installation
15
+
16
+ ```console
17
+ pip install envis
18
+ ```
19
+
20
+ ## License
21
+
22
+ `envis` is released under a private source-available license: you can use the SDK to integrate with https://envisible.dev, but the backend platform remains proprietary.
23
+
24
+ ## Contact
25
+ - [Website](https://envisible.dev)
26
+ - [Email](mailto:contact@uarham.me)
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "envis"
7
+ dynamic = ["version"]
8
+ description = 'Secure, simple, and powerful secret management.'
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ keywords = []
13
+ [[project.authors]]
14
+ name = "Umair Arham"
15
+ email = "contact@uarham.me"
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Programming Language :: Python",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: Implementation :: CPython",
25
+ "Programming Language :: Python :: Implementation :: PyPy",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.31.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://envisible.dev"
33
+ Documentation = "https://github.com/umairx25/envis#readme"
34
+ Issues = "https://github.com/umairx25/envis/issues"
35
+ Source = "https://github.com/umairx25/envis"
36
+
37
+ [tool.hatch.version]
38
+ path = "src/envis/__about__.py"
39
+
40
+ [tool.hatch.envs.types]
41
+ extra-dependencies = [
42
+ "mypy>=1.0.0",
43
+ ]
44
+ [tool.hatch.envs.types.scripts]
45
+ check = "mypy --install-types --non-interactive {args:src/envis tests}"
46
+
47
+ [tool.coverage.run]
48
+ source_pkgs = ["envis", "tests"]
49
+ branch = true
50
+ parallel = true
51
+ omit = [
52
+ "src/envis/__about__.py",
53
+ ]
54
+
55
+ [tool.coverage.paths]
56
+ envis = ["src/envis", "*/envis/src/envis"]
57
+ tests = ["tests", "*/envis/tests"]
58
+
59
+ [tool.coverage.report]
60
+ exclude_lines = [
61
+ "no cov",
62
+ "if __name__ == .__main__.:",
63
+ "if TYPE_CHECKING:",
64
+ ]
envis-0.0.1/setup.py ADDED
@@ -0,0 +1,32 @@
1
+ from pathlib import Path
2
+
3
+ from setuptools import find_packages, setup
4
+
5
+ PACKAGE_ROOT = Path(__file__).parent
6
+
7
+ # Reuse the single source of truth for the package version.
8
+ ABOUT_PATH = PACKAGE_ROOT / "src" / "envis" / "__about__.py"
9
+ ABOUT: dict = {}
10
+ exec(ABOUT_PATH.read_text(encoding="utf-8"), ABOUT)
11
+
12
+ README_PATH = PACKAGE_ROOT / "README.md"
13
+ LONG_DESCRIPTION = README_PATH.read_text(encoding="utf-8")
14
+
15
+ setup(
16
+ name="envis",
17
+ version=ABOUT["__version__"],
18
+ description="Secure, simple, and powerful secret management.",
19
+ long_description=LONG_DESCRIPTION,
20
+ long_description_content_type="text/markdown",
21
+ author="Umair Arham",
22
+ author_email="contact@uarham.me",
23
+ url="https://github.com/umairx25/envis",
24
+ packages=find_packages(where="src"),
25
+ package_dir={"": "src"},
26
+ python_requires=">=3.8",
27
+ install_requires=[
28
+ "requests>=2.31.0",
29
+ "termcolor>=2.3.0",
30
+ ],
31
+ include_package_data=True
32
+ )
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2026-present umairx25 <umairarhambd@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.0.1"
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2026-present umairx25 <umairarhambd@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ from .main import get, logout
6
+
7
+ __all__ = ["get", "logout"]
8
+
@@ -0,0 +1,56 @@
1
+ from typing import Any
2
+ import time
3
+ import requests
4
+ import json
5
+ from uuid import UUID
6
+ from .session import write_session, BASE_URL, WAIT_TIME, POLL_DELAY_SECONDS
7
+
8
+ def wait_for_auth(device_id: str) -> dict[str, Any]:
9
+ """
10
+ Poll the backend until the device session is ready or the wait time expires.
11
+ """
12
+ try:
13
+ UUID(device_id)
14
+ except ValueError as exc:
15
+ raise RuntimeError("Device id must be a valid UUID.") from exc
16
+
17
+ url = f"{BASE_URL}/v1/auth/{device_id}"
18
+
19
+ deadline = time.monotonic() + WAIT_TIME
20
+
21
+ while time.monotonic() < deadline:
22
+ try:
23
+ response = requests.get(url, timeout=10)
24
+ except requests.RequestException:
25
+ time.sleep(POLL_DELAY_SECONDS)
26
+ continue
27
+
28
+ try:
29
+ payload = response.json()
30
+ except ValueError as exc:
31
+ raise RuntimeError("Auth endpoint returned invalid JSON.") from exc
32
+
33
+ if response.status_code == 200 and payload.get("is_auth"):
34
+ session_blob = payload.get("content")
35
+ if not session_blob:
36
+ raise RuntimeError("Auth endpoint returned an empty session payload.")
37
+ try:
38
+ session = json.loads(session_blob)
39
+ except json.JSONDecodeError as exc:
40
+ raise RuntimeError("Auth endpoint returned malformed session data.") from exc
41
+
42
+ write_session(session)
43
+ return session
44
+
45
+ if response.status_code == 202:
46
+ retry_header = response.headers.get("Retry-After")
47
+ try:
48
+ delay = max(1, int(retry_header)) if retry_header else POLL_DELAY_SECONDS
49
+ except ValueError:
50
+ delay = POLL_DELAY_SECONDS
51
+ time.sleep(delay)
52
+ continue
53
+
54
+ detail = payload.get("detail") if isinstance(payload, dict) else response.text
55
+ status_code = response.status_code
56
+ raise RuntimeError(f"Auth endpoint failed ({status_code}): {detail}")
@@ -0,0 +1,80 @@
1
+ from urllib.parse import urlparse
2
+ import requests
3
+ from .session import load_session, ensure_session, SESSION_PATH, BASE_URL
4
+ from termcolor import colored
5
+
6
+ """
7
+ Helpers
8
+ """
9
+
10
+ def _extract_user_id(session: dict) -> str | None:
11
+ """
12
+ Return the Supabase user ID from the cached session, if present.
13
+ """
14
+ user = session.get("user")
15
+ if isinstance(user, dict):
16
+ user_id = user.get("id")
17
+ if isinstance(user_id, str) and user_id.strip():
18
+ return user_id
19
+ return None
20
+
21
+
22
+ def _is_local_base_url(base_url: str) -> bool:
23
+ """
24
+ Determine if the API URL is targeting a local dev server.
25
+ """
26
+ parsed = urlparse(base_url)
27
+ host = parsed.hostname or ""
28
+ return host in {"localhost", "127.0.0.1", "0.0.0.0"}
29
+
30
+ """
31
+ Main functions
32
+ """
33
+
34
+ def logout() -> None:
35
+ """
36
+ Remove the cached session so the next call forces re-authentication.
37
+ """
38
+ if not SESSION_PATH.exists():
39
+ raise RuntimeError("Already logged out. Please authenticate again.")
40
+
41
+ try:
42
+ SESSION_PATH.unlink()
43
+ print(colored("Successfully logged out!", "green"))
44
+ except OSError as exc:
45
+ raise RuntimeError(f"Unable to remove session cache: {exc}") from exc
46
+
47
+
48
+ def get(project_id: str, secret_name: str) -> dict:
49
+ """
50
+ Fetch a secret value, enforcing that the caller has an authenticated session.
51
+ """
52
+ ensure_session()
53
+ session = load_session()
54
+ headers = {
55
+ "Authorization": f"Bearer {session['access_token']}",
56
+ "Content-Type": "application/json",
57
+ }
58
+ if _is_local_base_url(BASE_URL):
59
+ user_id = _extract_user_id(session)
60
+ if user_id:
61
+ headers["X-User-Id"] = user_id
62
+
63
+ url = f"{BASE_URL}/v1/projects/{project_id}/secrets/{secret_name}"
64
+ try:
65
+ resp = requests.get(url, headers=headers, timeout=10)
66
+ resp.raise_for_status()
67
+ except requests.HTTPError as exc:
68
+ detail = exc.response.text if exc.response is not None else str(exc)
69
+ raise RuntimeError(f"Failed to fetch secret ({exc.response.status_code if exc.response else 'HTTP error'}): {detail}") from exc
70
+ except requests.RequestException as exc:
71
+ raise RuntimeError(f"Failed to reach Envault API: {exc}") from exc
72
+
73
+ try:
74
+ res= resp.json()
75
+ return res["value"]
76
+ except ValueError:
77
+ text = resp.text.strip()
78
+ if text:
79
+ return {"raw": text}
80
+ raise RuntimeError("API returned an empty, non-JSON response when fetching secret.")
@@ -0,0 +1,89 @@
1
+ from uuid import uuid4
2
+ from pathlib import Path
3
+ from typing import Any
4
+ import json
5
+ import os
6
+ import webbrowser
7
+ from termcolor import colored
8
+
9
+ SESSION_PATH = Path.home() / ".envis" / "session.json"
10
+
11
+ def _get_env_url(var_name: str, default: str) -> str:
12
+ """
13
+ Read a URL override from environment variables with a safe fallback.
14
+ Trailing slashes are stripped so joins don't produce double slashes.
15
+ """
16
+ value = os.getenv(var_name)
17
+ if not value:
18
+ return default
19
+ return value.rstrip("/")
20
+
21
+ BASE_URL = _get_env_url("ENVIS_API_URL", "https://envis.onrender.com")
22
+ FRONTEND_URL = _get_env_url("ENVIS_DASH_URL", "https://envisible.netlify.app")
23
+ WAIT_TIME = 120
24
+ POLL_DELAY_SECONDS = 5
25
+
26
+ def load_session() -> dict:
27
+ """
28
+ Load the cached session tokens that the CLI stored after login.
29
+ """
30
+
31
+ if not SESSION_PATH.exists():
32
+ device_code = uuid4()
33
+ raise RuntimeError(
34
+ f"Not authenticated. Visit {FRONTEND_URL}/auth?device_code={device_code} to link this device."
35
+ )
36
+
37
+ try:
38
+ session = json.loads(SESSION_PATH.read_text(encoding="utf-8"))
39
+ except json.JSONDecodeError as exc:
40
+ raise RuntimeError("Session file is corrupt. Re-run `envis login`.") from exc
41
+
42
+ access_token = session.get("access_token")
43
+ if not access_token:
44
+ raise RuntimeError("Session missing access_token. Re-run `envis login`.")
45
+
46
+ return session
47
+
48
+
49
+ def write_session(session_info: dict[str, Any]) -> None:
50
+ """
51
+ Persist the Supabase session locally so future calls can reuse it.
52
+ """
53
+ if not isinstance(session_info, dict):
54
+ raise RuntimeError("Session payload must be a dictionary.")
55
+
56
+ try:
57
+ payload = json.dumps(session_info, indent=2)
58
+ except (TypeError, ValueError) as exc:
59
+ raise RuntimeError("Session payload is not JSON serializable.") from exc
60
+
61
+ try:
62
+ SESSION_PATH.parent.mkdir(parents=True, exist_ok=True)
63
+ SESSION_PATH.write_text(payload, encoding="utf-8")
64
+ os.chmod(SESSION_PATH, 0o600)
65
+ except OSError as exc:
66
+ raise RuntimeError(f"Failed to write session cache: {exc}") from exc
67
+
68
+ def ensure_session() -> None:
69
+
70
+ from .auth import wait_for_auth
71
+
72
+ if SESSION_PATH.exists():
73
+ return
74
+
75
+ device_code = str(uuid4())
76
+ auth_url = f"{FRONTEND_URL}/auth?device_code={device_code}"
77
+
78
+ print(colored("No cached session detected.", "red"))
79
+
80
+ try:
81
+ webbrowser.open(auth_url, new=2)
82
+ print(f"\nTrying to open:")
83
+ print(colored(f" {auth_url}", "blue"))
84
+ except Exception:
85
+ print(colored("Could not open browser automatically – open \n {auth_url}", "red"))
86
+
87
+ print("\nWaiting for the session to be approved...")
88
+ wait_for_auth(device_code)
89
+ print(colored("\nDevice approved and session saved locally.", "green"))
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2026-present umairx25 <umairarhambd@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT