redenv 0.2.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.
@@ -0,0 +1,9 @@
1
+ # Python
2
+ example*.py
3
+
4
+ # Build
5
+ build
6
+ dist
7
+ .eggs
8
+ __pycache__
9
+ .pytest_cache
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.2.0] - 2026-01-22
6
+
7
+ ### Added
8
+
9
+ - **Synchronous Client:** Added `RedenvSync` for blocking contexts (scripts, legacy apps).
10
+ - **Write Support:** Implemented `client.set(key, value)` with full version history management.
11
+ - **Smart Secrets Object:**
12
+ - `secrets.get(key, cast=int)`: Auto-convert types.
13
+ - `secrets.scope("PREFIX_")`: Create namespaced configuration subsets.
14
+ - `secrets.require("KEY")`: Fail-fast validation for missing keys.
15
+ - **Time Travel:** Added `client.get_version(key, v)` to fetch historical secrets. Supports both absolute IDs and relative indexing (0=Latest, 1=Previous).
16
+ - **Security Hardening:** `Secrets` object now masks values (`********`) in logs/print statements to prevent accidental leakage.
17
+ - **Override Protection:** Added `env.override` option to prevent overwriting existing environment variables.
18
+
19
+ ## [0.1.0] - 2026-01-22
20
+
21
+ ### Added
22
+
23
+ - **Initial Release:** First public beta release of the `redenv` Python SDK.
24
+ - **Zero-Knowledge Security:** All cryptographic operations (AES-256-GCM, PBKDF2) are performed locally.
25
+ - **Async Support:** Built on `asyncio` and `upstash-redis` for high-performance non-blocking operations.
26
+ - **SWR Caching:** Implemented a robust `Stale-While-Revalidate` caching strategy using `cachetools.LRUCache`.
27
+ - **Environment Injection:** Automatically populates `os.environ` with decrypted secrets on `load()`.
redenv-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PRAS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
redenv-0.2.0/Makefile ADDED
@@ -0,0 +1,24 @@
1
+ .PHONY: install test lint build clean example example-sync
2
+
3
+ # Install the package in editable mode with dev dependencies
4
+ install:
5
+ pip install -e ".[dev]"
6
+
7
+ # Run tests
8
+ test:
9
+ PYTHONPATH=src python -m pytest
10
+
11
+ # Run linting and type checking
12
+ lint:
13
+ python -m ruff check src tests
14
+ python -m pyright src
15
+
16
+ # Build the package artifacts
17
+ build:
18
+ rm -rf dist/ build/
19
+ python -m build
20
+
21
+ # Clean up build artifacts and caches
22
+ clean:
23
+ rm -rf dist/ build/ *.egg-info .pytest_cache .ruff_cache
24
+ find . -type d -name "__pycache__" -exec rm -rf {} +
redenv-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: redenv
3
+ Version: 0.2.0
4
+ Summary: A zero-knowledge, end-to-end encrypted secret management SDK for Python.
5
+ Project-URL: Homepage, https://github.com/redenv-labs/redenv
6
+ Project-URL: Documentation, https://github.com/redenv-labs/redenv/tree/main/packages/python-client
7
+ Project-URL: Repository, https://github.com/redenv-labs/redenv
8
+ Project-URL: Issues, https://github.com/redenv-labs/redenv/issues
9
+ Author-email: PRAS <prassamin@gmail.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 PRAS
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: dotenv,encryption,redis,sdk,secrets,security,upstash
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.8
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Topic :: Security
44
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
45
+ Requires-Python: >=3.8
46
+ Requires-Dist: cachetools>=5.0.0
47
+ Requires-Dist: cryptography>=41.0.0
48
+ Requires-Dist: upstash-redis>=1.0.0
49
+ Provides-Extra: dev
50
+ Requires-Dist: build; extra == 'dev'
51
+ Requires-Dist: pyright; extra == 'dev'
52
+ Requires-Dist: pytest; extra == 'dev'
53
+ Requires-Dist: python-dotenv; extra == 'dev'
54
+ Requires-Dist: ruff; extra == 'dev'
55
+ Requires-Dist: twine; extra == 'dev'
56
+ Description-Content-Type: text/markdown
57
+
58
+ # Redenv Python SDK
59
+
60
+ The official, zero-knowledge Python client for [Redenv](https://github.com/redenv-labs/redenv). Securely fetch, cache, and manage your environment variables at runtime.
61
+
62
+ ![PyPI - Version](https://img.shields.io/pypi/v/redenv)
63
+ ![PyPI - License](https://img.shields.io/pypi/l/redenv)
64
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/redenv)
65
+
66
+ ## Features
67
+
68
+ - **🔒 Zero-Knowledge:** End-to-End Encryption. Secrets are decrypted locally using your Project Encryption Key (PEK).
69
+ - **⚡ High Performance:** In-memory `LRUCache` with `Stale-While-Revalidate` strategy for zero-latency reads.
70
+ - **🔄 Universal:** Native **Async** (`asyncio`) and **Synchronous** clients included.
71
+ - **🛠️ Developer Experience:**
72
+ - **Smart Casting:** `secrets.get("PORT", cast=int)`
73
+ - **Scoping:** `secrets.scope("STRIPE_")` for namespaced configs.
74
+ - **Validation:** `secrets.require("API_KEY")` fail-fast checks.
75
+ - **Time Travel:** Fetch historical versions of secrets.
76
+ - **🛡️ Secure by Default:** Secrets are masked (`********`) in logs to prevent accidental leaks.
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ pip install redenv
82
+ ```
83
+
84
+ ## Quick Start
85
+
86
+ ### Async Client (FastAPI / Modern Apps)
87
+
88
+ ```python
89
+ import asyncio
90
+ import os
91
+ from redenv import Redenv
92
+
93
+ async def main():
94
+ client = Redenv({
95
+ "project": os.getenv("REDENV_PROJECT"),
96
+ "token_id": os.getenv("REDENV_TOKEN_ID"),
97
+ "token": os.getenv("REDENV_TOKEN_KEY"),
98
+ "upstash": {
99
+ "url": os.getenv("UPSTASH_REDIS_URL"),
100
+ "token": os.getenv("UPSTASH_REDIS_TOKEN")
101
+ }
102
+ })
103
+
104
+ # 1. Load Secrets (Populates os.environ by default)
105
+ secrets = await client.load()
106
+
107
+ # 2. Access Secrets
108
+ print(f"Database URL: {secrets['DATABASE_URL']}")
109
+
110
+ # 3. Smart Casting
111
+ port = secrets.get("PORT", cast=int)
112
+ debug = secrets.get("DEBUG", cast=bool)
113
+
114
+ if __name__ == "__main__":
115
+ asyncio.run(main())
116
+ ```
117
+
118
+ ### Synchronous Client (Flask / Scripts / Legacy)
119
+
120
+ Perfect for scripts or frameworks where `async/await` is not available at the top level.
121
+
122
+ ```python
123
+ from redenv import RedenvSync
124
+
125
+ client = RedenvSync({ ... }) # Same config as above
126
+
127
+ # Blocks until secrets are fetched
128
+ secrets = client.load()
129
+
130
+ print(secrets["API_KEY"])
131
+ ```
132
+
133
+ ## Advanced Usage
134
+
135
+ ### 1. Scoping & Validation
136
+ Organize large configurations and ensure critical keys exist.
137
+
138
+ ```python
139
+ secrets = await client.load()
140
+
141
+ # Fail if these keys are missing
142
+ secrets.require("STRIPE_KEY", "STRIPE_WEBHOOK")
143
+
144
+ # Create a subset of keys (e.g., keys starting with "STRIPE_")
145
+ # The prefix is automatically stripped.
146
+ stripe_config = secrets.scope("STRIPE_")
147
+
148
+ print(stripe_config["KEY"]) # Maps to STRIPE_KEY
149
+ print(stripe_config["WEBHOOK"]) # Maps to STRIPE_WEBHOOK
150
+ ```
151
+
152
+ ### 2. Time Travel (Version History)
153
+ Redenv stores a history of every secret change. You can access older versions for rollbacks or auditing.
154
+
155
+ ```python
156
+ # Get the absolute version 5
157
+ v5 = await client.get_version("API_KEY", 5)
158
+
159
+ # Get the previous version (1 version older than latest)
160
+ # Mode="index": 0=Latest, 1=Previous, -1=Oldest
161
+ prev = await client.get_version("API_KEY", 1, mode="index")
162
+
163
+ # Get the oldest version ever created
164
+ first = await client.get_version("API_KEY", -1)
165
+ ```
166
+
167
+ ### 3. Writing Secrets
168
+ You can update secrets programmatically. This automatically encrypts the value, increments the version, and updates the history.
169
+
170
+ ```python
171
+ await client.set("FEATURE_FLAG", "true")
172
+ ```
173
+
174
+ ### 4. Configuration Options
175
+
176
+ | Option | Type | Description | Default |
177
+ |:---|:---|:---|:---|
178
+ | `project` | str | Your Project ID | Required |
179
+ | `token_id` | str | Service Token Public ID | Required |
180
+ | `token` | str | Service Token Secret Key | Required |
181
+ | `upstash` | dict | `{ url: ..., token: ... }` | Required |
182
+ | `environment` | str | Target environment (dev, prod) | `development` |
183
+ | `log` | str | Log level (`none`, `low`, `high`) | `low` |
184
+ | `cache` | dict | `{ ttl: 300, swr: 86400 }` (seconds) | 5min / 24h |
185
+ | `env.override` | bool | Overwrite existing `os.environ` keys | `True` |
186
+
187
+ ```python
188
+ client = Redenv({
189
+ # ...
190
+ "env": {
191
+ "override": False # Protects local env vars from being overwritten
192
+ }
193
+ })
194
+ ```
195
+
196
+ ## Security
197
+
198
+ - **Masking:** If you accidentally print the `secrets` object, values are hidden: `Secrets({'API_KEY': '********'})`.
199
+ - **Zero-Knowledge:** The server (Upstash) never sees the plaintext. Decryption happens only in your application's memory.
200
+
201
+ ## License
202
+
203
+ MIT
redenv-0.2.0/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Redenv Python SDK
2
+
3
+ The official, zero-knowledge Python client for [Redenv](https://github.com/redenv-labs/redenv). Securely fetch, cache, and manage your environment variables at runtime.
4
+
5
+ ![PyPI - Version](https://img.shields.io/pypi/v/redenv)
6
+ ![PyPI - License](https://img.shields.io/pypi/l/redenv)
7
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/redenv)
8
+
9
+ ## Features
10
+
11
+ - **🔒 Zero-Knowledge:** End-to-End Encryption. Secrets are decrypted locally using your Project Encryption Key (PEK).
12
+ - **⚡ High Performance:** In-memory `LRUCache` with `Stale-While-Revalidate` strategy for zero-latency reads.
13
+ - **🔄 Universal:** Native **Async** (`asyncio`) and **Synchronous** clients included.
14
+ - **🛠️ Developer Experience:**
15
+ - **Smart Casting:** `secrets.get("PORT", cast=int)`
16
+ - **Scoping:** `secrets.scope("STRIPE_")` for namespaced configs.
17
+ - **Validation:** `secrets.require("API_KEY")` fail-fast checks.
18
+ - **Time Travel:** Fetch historical versions of secrets.
19
+ - **🛡️ Secure by Default:** Secrets are masked (`********`) in logs to prevent accidental leaks.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install redenv
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### Async Client (FastAPI / Modern Apps)
30
+
31
+ ```python
32
+ import asyncio
33
+ import os
34
+ from redenv import Redenv
35
+
36
+ async def main():
37
+ client = Redenv({
38
+ "project": os.getenv("REDENV_PROJECT"),
39
+ "token_id": os.getenv("REDENV_TOKEN_ID"),
40
+ "token": os.getenv("REDENV_TOKEN_KEY"),
41
+ "upstash": {
42
+ "url": os.getenv("UPSTASH_REDIS_URL"),
43
+ "token": os.getenv("UPSTASH_REDIS_TOKEN")
44
+ }
45
+ })
46
+
47
+ # 1. Load Secrets (Populates os.environ by default)
48
+ secrets = await client.load()
49
+
50
+ # 2. Access Secrets
51
+ print(f"Database URL: {secrets['DATABASE_URL']}")
52
+
53
+ # 3. Smart Casting
54
+ port = secrets.get("PORT", cast=int)
55
+ debug = secrets.get("DEBUG", cast=bool)
56
+
57
+ if __name__ == "__main__":
58
+ asyncio.run(main())
59
+ ```
60
+
61
+ ### Synchronous Client (Flask / Scripts / Legacy)
62
+
63
+ Perfect for scripts or frameworks where `async/await` is not available at the top level.
64
+
65
+ ```python
66
+ from redenv import RedenvSync
67
+
68
+ client = RedenvSync({ ... }) # Same config as above
69
+
70
+ # Blocks until secrets are fetched
71
+ secrets = client.load()
72
+
73
+ print(secrets["API_KEY"])
74
+ ```
75
+
76
+ ## Advanced Usage
77
+
78
+ ### 1. Scoping & Validation
79
+ Organize large configurations and ensure critical keys exist.
80
+
81
+ ```python
82
+ secrets = await client.load()
83
+
84
+ # Fail if these keys are missing
85
+ secrets.require("STRIPE_KEY", "STRIPE_WEBHOOK")
86
+
87
+ # Create a subset of keys (e.g., keys starting with "STRIPE_")
88
+ # The prefix is automatically stripped.
89
+ stripe_config = secrets.scope("STRIPE_")
90
+
91
+ print(stripe_config["KEY"]) # Maps to STRIPE_KEY
92
+ print(stripe_config["WEBHOOK"]) # Maps to STRIPE_WEBHOOK
93
+ ```
94
+
95
+ ### 2. Time Travel (Version History)
96
+ Redenv stores a history of every secret change. You can access older versions for rollbacks or auditing.
97
+
98
+ ```python
99
+ # Get the absolute version 5
100
+ v5 = await client.get_version("API_KEY", 5)
101
+
102
+ # Get the previous version (1 version older than latest)
103
+ # Mode="index": 0=Latest, 1=Previous, -1=Oldest
104
+ prev = await client.get_version("API_KEY", 1, mode="index")
105
+
106
+ # Get the oldest version ever created
107
+ first = await client.get_version("API_KEY", -1)
108
+ ```
109
+
110
+ ### 3. Writing Secrets
111
+ You can update secrets programmatically. This automatically encrypts the value, increments the version, and updates the history.
112
+
113
+ ```python
114
+ await client.set("FEATURE_FLAG", "true")
115
+ ```
116
+
117
+ ### 4. Configuration Options
118
+
119
+ | Option | Type | Description | Default |
120
+ |:---|:---|:---|:---|
121
+ | `project` | str | Your Project ID | Required |
122
+ | `token_id` | str | Service Token Public ID | Required |
123
+ | `token` | str | Service Token Secret Key | Required |
124
+ | `upstash` | dict | `{ url: ..., token: ... }` | Required |
125
+ | `environment` | str | Target environment (dev, prod) | `development` |
126
+ | `log` | str | Log level (`none`, `low`, `high`) | `low` |
127
+ | `cache` | dict | `{ ttl: 300, swr: 86400 }` (seconds) | 5min / 24h |
128
+ | `env.override` | bool | Overwrite existing `os.environ` keys | `True` |
129
+
130
+ ```python
131
+ client = Redenv({
132
+ # ...
133
+ "env": {
134
+ "override": False # Protects local env vars from being overwritten
135
+ }
136
+ })
137
+ ```
138
+
139
+ ## Security
140
+
141
+ - **Masking:** If you accidentally print the `secrets` object, values are hidden: `Secrets({'API_KEY': '********'})`.
142
+ - **Zero-Knowledge:** The server (Upstash) never sees the plaintext. Decryption happens only in your application's memory.
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "redenv"
7
+ version = "0.2.0"
8
+ description = "A zero-knowledge, end-to-end encrypted secret management SDK for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "PRAS", email = "prassamin@gmail.com" },
14
+ ]
15
+ keywords = ["secrets", "security", "dotenv", "upstash", "redis", "encryption", "sdk"]
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.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Security",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Operating System :: OS Independent",
29
+ ]
30
+ dependencies = [
31
+ "upstash-redis>=1.0.0",
32
+ "cryptography>=41.0.0",
33
+ "cachetools>=5.0.0",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/redenv-labs/redenv"
38
+ Documentation = "https://github.com/redenv-labs/redenv/tree/main/packages/python-client"
39
+ Repository = "https://github.com/redenv-labs/redenv"
40
+ Issues = "https://github.com/redenv-labs/redenv/issues"
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "pytest",
45
+ "python-dotenv",
46
+ "build",
47
+ "twine",
48
+ "ruff",
49
+ "pyright"
50
+ ]
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/redenv"]
@@ -0,0 +1,7 @@
1
+ from .client import Redenv
2
+ from .errors import RedenvError
3
+ from .secrets import Secrets
4
+ from .sync import Redenv as RedenvSync
5
+
6
+ __version__ = "0.2.0"
7
+ __all__ = ["Redenv", "RedenvSync", "RedenvError", "Secrets"]
@@ -0,0 +1,137 @@
1
+ import asyncio
2
+ import time
3
+ from typing import Dict, Any, Optional, Literal
4
+ from upstash_redis.asyncio import Redis
5
+ from cachetools import LRUCache
6
+ from .types import RedenvOptions, CacheEntry
7
+ from .secrets import Secrets
8
+ from .utils import fetch_and_decrypt, populate_env, log, error, set_secret, get_secret_version
9
+ from .errors import RedenvError
10
+
11
+ class Redenv:
12
+ def __init__(self, options: Dict[str, Any]):
13
+ self.options = RedenvOptions.from_dict(options)
14
+ self.validate_options()
15
+
16
+ self._cache = LRUCache(maxsize=1000)
17
+
18
+ self.redis = Redis(
19
+ url=self.options.upstash.url,
20
+ token=self.options.upstash.token
21
+ )
22
+
23
+ def validate_options(self):
24
+ if not self.options.project:
25
+ raise RedenvError("Missing required configuration option: project", "MISSING_CONFIG")
26
+ if not self.options.token_id:
27
+ raise RedenvError("Missing required configuration option: token_id", "MISSING_CONFIG")
28
+ if not self.options.token:
29
+ raise RedenvError("Missing required configuration option: token", "MISSING_CONFIG")
30
+ if not self.options.upstash.url or not self.options.upstash.token:
31
+ raise RedenvError("Missing required configuration option: upstash", "MISSING_CONFIG")
32
+
33
+ def _get_cache_key(self) -> str:
34
+ return f"redenv:{self.options.project}:{self.options.environment}"
35
+
36
+ async def _get_secrets(self) -> Secrets:
37
+ key = self._get_cache_key()
38
+ entry = self._cache.get(key)
39
+ now = time.time()
40
+
41
+ ttl_seconds = self.options.cache.ttl
42
+ swr_seconds = self.options.cache.swr
43
+
44
+ # Function to fetch fresh value
45
+ async def fetch_fresh() -> Secrets:
46
+ try:
47
+ log("Fetching fresh secrets...", self.options.log)
48
+ secrets = await fetch_and_decrypt(self.redis, self.options)
49
+
50
+ # Update cache with new entry
51
+ self._cache[key] = CacheEntry(secrets, time.time())
52
+
53
+ # Side effect: populate environment
54
+ await populate_env(secrets, self.options)
55
+
56
+ return secrets
57
+ except Exception as e:
58
+ error(f"Failed to fetch secrets: {e}", self.options.log)
59
+ raise e
60
+
61
+ if entry:
62
+ age = now - entry.created_at
63
+
64
+ if age < ttl_seconds:
65
+ # Case 1: Fresh
66
+ log("Cache hit (Fresh).", self.options.log)
67
+ return entry.value
68
+
69
+ elif age < (ttl_seconds + swr_seconds):
70
+ # Case 2: Stale (SWR)
71
+ log("Cache hit (Stale). Revalidating in background...", self.options.log)
72
+ # Return stale value immediately
73
+ # Spawn background refresh
74
+ asyncio.create_task(fetch_fresh())
75
+ return entry.value
76
+ else:
77
+ # Case 3: Expired
78
+ log("Cache expired. Fetching fresh...", self.options.log)
79
+ return await fetch_fresh()
80
+ else:
81
+ # Case 4: Miss
82
+ log("Cache miss. Fetching fresh...", self.options.log)
83
+ return await fetch_fresh()
84
+
85
+ async def init(self):
86
+ """
87
+ Initializes the environment with secrets.
88
+ Alias for load().
89
+ """
90
+ await self.load()
91
+
92
+ async def load(self) -> Secrets:
93
+ """
94
+ Fetches, caches, and injects secrets into the environment.
95
+
96
+ Returns:
97
+ The Secrets object.
98
+ """
99
+ secrets = await self._get_secrets()
100
+
101
+ # Ensure env is populated
102
+ await populate_env(secrets, self.options)
103
+
104
+ return secrets
105
+
106
+ async def set(self, key: str, value: str):
107
+ """
108
+ Adds or updates a secret.
109
+ """
110
+ if not key or not value:
111
+ raise RedenvError("Key and value are required.", "INVALID_INPUT")
112
+
113
+ try:
114
+ await set_secret(self.redis, self.options, key, value)
115
+ log(f'Successfully set secret for key "{key}".', self.options.log)
116
+
117
+ # Invalidate cache
118
+ cache_key = self._get_cache_key()
119
+ if cache_key in self._cache:
120
+ del self._cache[cache_key]
121
+
122
+ except Exception as e:
123
+ msg = str(e)
124
+ error(f"Failed to set secret: {msg}", self.options.log)
125
+ raise RedenvError(f"Failed to set secret: {msg}", "UNKNOWN_ERROR")
126
+
127
+ async def get_version(self, key: str, version: int, mode: Literal["id", "index"] = "id") -> Optional[str]:
128
+ """
129
+ Fetches a specific version of a secret with caching.
130
+
131
+ Args:
132
+ key: The secret key.
133
+ version: The version ID or index.
134
+ mode: "id" (default) uses positive version numbers, negative for index from end.
135
+ "index" treats version as a 0-based array index (0=latest).
136
+ """
137
+ return await get_secret_version(self.redis, self.options, self._cache, key, version, mode)