litewave-cache-lib 0.1.2__tar.gz → 0.1.4__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.
Files changed (31) hide show
  1. litewave_cache_lib-0.1.4/.env.sample +24 -0
  2. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/.github/workflows/ci-trigger.yml +25 -1
  3. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/.gitignore +2 -1
  4. litewave_cache_lib-0.1.4/PKG-INFO +324 -0
  5. litewave_cache_lib-0.1.4/README.md +307 -0
  6. litewave_cache_lib-0.1.4/cache_manager/README.md +399 -0
  7. litewave_cache_lib-0.1.4/cache_manager/__init__.py +25 -0
  8. litewave_cache_lib-0.1.4/cache_manager/cache_manager.py +289 -0
  9. litewave_cache_lib-0.1.4/cache_manager/cache_refresh_ticker.py +248 -0
  10. litewave_cache_lib-0.1.4/cache_manager/redis_client.py +245 -0
  11. litewave_cache_lib-0.1.4/cache_manager/tenant_manager.py +131 -0
  12. litewave_cache_lib-0.1.4/cache_manager.png +0 -0
  13. litewave_cache_lib-0.1.4/cache_manager_ticker.png +0 -0
  14. litewave_cache_lib-0.1.4/examples/example_cache_manager.py +32 -0
  15. litewave_cache_lib-0.1.2/cache_reader.py → litewave_cache_lib-0.1.4/examples/example_tenant_mgr_cache.py +8 -3
  16. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/pyproject.toml +3 -2
  17. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/requirements.txt +2 -0
  18. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/setup.py +15 -6
  19. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/tenant_mgr_cache/cache_client.py +5 -17
  20. litewave_cache_lib-0.1.4/tenant_mgr_cache.png +0 -0
  21. litewave_cache_lib-0.1.4/tests/__init__.py +0 -0
  22. litewave_cache_lib-0.1.4/tests/conftest.py +25 -0
  23. litewave_cache_lib-0.1.4/tests/test_cache_manager.py +541 -0
  24. litewave_cache_lib-0.1.4/tests/test_cache_refresh_ticker.py +275 -0
  25. litewave_cache_lib-0.1.4/tests/test_redis_client.py +344 -0
  26. litewave_cache_lib-0.1.4/tests/test_tenant_manager.py +281 -0
  27. litewave_cache_lib-0.1.2/PKG-INFO +0 -208
  28. litewave_cache_lib-0.1.2/README.md +0 -192
  29. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/.github/workflows/publish.yml +0 -0
  30. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/tenant_mgr_cache/__init__.py +0 -0
  31. {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/tenant_mgr_cache/config.py +0 -0
@@ -0,0 +1,24 @@
1
+ # ── Redis connection ──────────────────────────────────────────────────────────
2
+ REDIS_HOST=localhost
3
+ REDIS_PORT=6379
4
+ REDIS_PASSWORD=
5
+ REDIS_TENANT_CONFIG_DB=0
6
+
7
+ # Optional tuning (defaults shown)
8
+ REDIS_MAX_CONNECTIONS=50
9
+ REDIS_SOCKET_CONNECT_TIMEOUT=2.0
10
+ REDIS_SOCKET_TIMEOUT=2.0
11
+
12
+ # ── Tenant-manager API ────────────────────────────────────────────────────────
13
+ TENANT_MANAGER_BASE_URL=https://tenant-manager.example.com
14
+ SEED_TENANT_TOKEN=your-seed-bearer-token
15
+ SEED_TENANT_ID=your-seed-tenant-id
16
+
17
+ # ── CacheManager behaviour ────────────────────────────────────────────────────
18
+ # Set to "true" to enable automatic cache population on cache misses.
19
+ SELF_MANAGED_CACHE=false
20
+
21
+ # Pre-register tenant/secret keys with the background ticker on startup.
22
+ # Only used when SELF_MANAGED_CACHE=true.
23
+ # Format: JSON object — { "<tenant_id>": ["<secret>", ...], ... }
24
+ # TENANT_SECRET_MAP={"15": ["satoken", "azureConfiguration"]}
@@ -30,4 +30,28 @@ jobs:
30
30
  with:
31
31
  scan_path: "."
32
32
  severity: "CRITICAL,HIGH"
33
- fail_on_severity: true
33
+ fail_on_severity: true
34
+
35
+ test:
36
+ runs-on: ubuntu-latest
37
+ strategy:
38
+ matrix:
39
+ python-version: ["3.10", "3.11", "3.12"]
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+
43
+ - name: Set up Python ${{ matrix.python-version }}
44
+ uses: actions/setup-python@v5
45
+ with:
46
+ python-version: ${{ matrix.python-version }}
47
+
48
+ - name: Install dependencies
49
+ run: pip install -e ".[dev]"
50
+
51
+ - name: Run tests with coverage
52
+ run: |
53
+ python -m pytest tests/ \
54
+ --cov=cache_manager \
55
+ --cov-report=term-missing \
56
+ --cov-fail-under=90 \
57
+ -v
@@ -205,4 +205,5 @@ cython_debug/
205
205
  marimo/_static/
206
206
  marimo/_lsp/
207
207
  __marimo__/
208
- .env
208
+ .env
209
+ */**/.env
@@ -0,0 +1,324 @@
1
+ Metadata-Version: 2.4
2
+ Name: litewave-cache-lib
3
+ Version: 0.1.4
4
+ Summary: A lightweight Python library for Redis-backed tenant configuration storage
5
+ Project-URL: Homepage, https://github.com/aiorch/litewave-cache-lib
6
+ Project-URL: Repository, https://github.com/aiorch/litewave-cache-lib
7
+ Author-email: Nagarjuna Sarvepalli <nagarjuna.s@litewave.ai>
8
+ License: MIT
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx>=0.25.0
11
+ Requires-Dist: python-dotenv>=1.0.0
12
+ Requires-Dist: redis>=5.0.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
15
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # litewave-cache-lib
19
+
20
+ A lightweight Python library providing two complementary packages for Redis-backed tenant configuration storage:
21
+
22
+ | Package | Purpose |
23
+ |---|---|
24
+ | [`tenant_mgr_cache`](#tenant_mgr_cache) | Read-only cache reader — look up tenant secrets already stored in Redis |
25
+ | [`cache_manager`](#cache_manager) | Full cache manager — read from Redis and optionally auto-fetch from the tenant-manager API on a cache miss |
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install git+https://github.com/aiorch/litewave-cache-lib
33
+ ```
34
+
35
+ Or install from source:
36
+
37
+ ```bash
38
+ git clone https://github.com/aiorch/litewave-cache-lib.git
39
+ cd litewave-cache-lib
40
+ pip install .
41
+ ```
42
+
43
+ **Requirements:** Python >= 3.10, `redis >= 5.0.0`, `httpx >= 0.25.0`, `python-dotenv >= 1.0.0`
44
+
45
+ ---
46
+
47
+ ## `tenant_mgr_cache`
48
+
49
+ A lightweight, read-only cache reader for tenant secrets stored in Redis. Provides three simple functions covering all lookup patterns.
50
+
51
+ ![tenant_mgr_cache package diagram](tenant_mgr_cache.png)
52
+
53
+ ### Features
54
+
55
+ - Connection-pooled Redis client with automatic retry on failure (5-minute cooldown)
56
+ - Fetch values by raw Redis key, or by `(tenant_id, secret_name)` pair via `get_secret`
57
+ - Attribute-level extraction from stored JSON objects via `get_attribute_value_by_key`
58
+ - Automatic JSON deserialization of stored values
59
+ - Standardised tenant key format: `tenant:{tenant_id}:secret:{secret_name}`
60
+ - Zero-boilerplate logging setup
61
+
62
+ ### Configuration
63
+
64
+ Settings are read from environment variables **at import time** (`CacheSettings` is instantiated on import). Always call `load_dotenv()` **before** importing from `tenant_mgr_cache`.
65
+
66
+ | Environment Variable | Required | Default | Description |
67
+ |---|---|---|---|
68
+ | `REDIS_HOST` | yes | — | Redis server hostname |
69
+ | `REDIS_PORT` | yes | — | Redis server port |
70
+ | `REDIS_PASSWORD` | yes | — | Redis password |
71
+ | `REDIS_TENANT_CONFIG_DB` | no | `0` | Redis database index for tenant config |
72
+ | `REDIS_MAX_CONNECTIONS` | no | `50` | Connection pool size |
73
+ | `REDIS_SOCKET_CONNECT_TIMEOUT` | no | `2` | Socket connect timeout (seconds) |
74
+ | `REDIS_SOCKET_TIMEOUT` | no | `2` | Socket read/write timeout (seconds) |
75
+ | `LOG_LEVEL` | no | `INFO` | Logging level (`DEBUG`, `INFO`, `WARNING`, …) |
76
+
77
+ ### Usage
78
+
79
+ > **Important:** `load_dotenv()` must be called before importing `tenant_mgr_cache` because environment variables are resolved at module load time.
80
+
81
+ ```python
82
+ from dotenv import load_dotenv
83
+
84
+ load_dotenv(".env", override=True)
85
+
86
+ from tenant_mgr_cache import (
87
+ get_attribute_value_by_key,
88
+ get_secret,
89
+ get_value_by_key,
90
+ )
91
+ ```
92
+
93
+ #### Case 1 — fetch by fully-formed Redis key
94
+
95
+ Pass a complete, pre-built Redis key directly:
96
+
97
+ ```python
98
+ key = "tenant:15:secret:satoken"
99
+ value = get_value_by_key(key)
100
+ if value is not None:
101
+ print(f"{key} → {value}")
102
+ else:
103
+ print("Key not found or Redis unavailable.")
104
+ ```
105
+
106
+ #### Case 2 — fetch by tenant ID and secret name
107
+
108
+ The canonical key `tenant:{tenant_id}:secret:{secret_name}` is built internally:
109
+
110
+ ```python
111
+ value = get_secret("15", "satoken")
112
+ if value is not None:
113
+ print(f"secret → {value}")
114
+ else:
115
+ print("Not found.")
116
+ ```
117
+
118
+ JSON objects stored in Redis are deserialized automatically; plain strings are returned as-is.
119
+
120
+ #### Case 3 — fetch a single attribute from a stored JSON object
121
+
122
+ Builds the key, fetches the JSON object, and returns the value of the requested attribute:
123
+
124
+ ```python
125
+ value = get_attribute_value_by_key("15", "satoken", "api_key")
126
+ if value is not None:
127
+ print(f"api_key → {value}")
128
+ else:
129
+ print("Attribute not found.")
130
+ ```
131
+
132
+ ### API Reference
133
+
134
+ #### `get_value_by_key(key: str) -> Optional[Any]`
135
+
136
+ Fetches the value stored at `key` and JSON-decodes it where possible. Returns `None` when the key does not exist, Redis is unavailable, or an error occurs.
137
+
138
+ #### `get_secret(tenant_id: str, secret_name: str) -> Optional[Any]`
139
+
140
+ Builds the canonical key `tenant:{tenant_id}:secret:{secret_name}` and returns the stored value. JSON is deserialized automatically. Returns `None` if the key does not exist or Redis is unavailable.
141
+
142
+ #### `get_attribute_value_by_key(tenant_id: str, secret_name: str, attribute_name: str) -> Optional[Any]`
143
+
144
+ Builds the canonical key, fetches the stored JSON object, and returns `data.get(attribute_name)`. Falls back to `getattr` for non-dict objects. Returns `None` if the key is missing, the value is not dict-like, or the attribute is absent.
145
+
146
+ #### `get_redis_client() -> Optional[redis.Redis]`
147
+
148
+ Returns the shared, connection-pooled Redis client. Returns `None` if the connection is unavailable. A failed connection is retried automatically after a 5-minute cooldown; subsequent calls reuse the existing client.
149
+
150
+ #### `prepare_key(tenant_id: str, secret_name: str) -> str`
151
+
152
+ Builds the canonical Redis key:
153
+
154
+ ```python
155
+ from tenant_mgr_cache.cache_client import prepare_key
156
+
157
+ key = prepare_key("15", "satoken")
158
+ print(key) # "tenant:15:secret:satoken"
159
+ ```
160
+
161
+ #### `settings` — `CacheSettings`
162
+
163
+ A dataclass instance holding all resolved configuration values:
164
+
165
+ ```python
166
+ from tenant_mgr_cache import settings
167
+
168
+ print(settings.REDIS_HOST)
169
+ print(settings.REDIS_PORT)
170
+ print(settings.REDIS_TENANT_CONFIG_DB)
171
+ ```
172
+
173
+ #### `logger` — `logging.Logger`
174
+
175
+ A pre-configured logger (`litewave_cache`) that respects `LOG_LEVEL`:
176
+
177
+ ```python
178
+ from tenant_mgr_cache import logger
179
+
180
+ logger.info("Custom message from application code")
181
+ ```
182
+
183
+ ---
184
+
185
+ ## `cache_manager`
186
+
187
+ A higher-level cache manager that wraps a `RedisClient` and an optional `TenantService`. When `self_managed_cache` is enabled, a cache miss automatically fetches the secret from the tenant-manager HTTP API and populates Redis (default TTL: 48 hours).
188
+
189
+ ### Features
190
+
191
+ - Reads from Redis first; falls back to tenant-manager API on a cache miss (when enabled)
192
+ - Auto-populates Redis after a successful tenant-manager fetch (48-hour TTL by default)
193
+ - Fully configurable via constructor arguments or environment variables
194
+ - Low-level `RedisClient` and `TenantService` classes are also exported for direct use
195
+
196
+ ### Configuration
197
+
198
+ #### Redis connection (`RedisClient`)
199
+
200
+ | Environment Variable | Required | Default | Description |
201
+ |---|---|---|---|
202
+ | `REDIS_HOST` | yes | — | Redis server hostname |
203
+ | `REDIS_PORT` | yes | — | Redis server port |
204
+ | `REDIS_PASSWORD` | no | `None` | Redis password |
205
+ | `REDIS_TENANT_CONFIG_DB` | no | `0` | Redis database index |
206
+ | `REDIS_MAX_CONNECTIONS` | no | `50` | Connection pool size |
207
+ | `REDIS_SOCKET_CONNECT_TIMEOUT` | no | `2.0` | Socket connect timeout (seconds) |
208
+ | `REDIS_SOCKET_TIMEOUT` | no | `2.0` | Socket read/write timeout (seconds) |
209
+
210
+ #### Cache manager behaviour
211
+
212
+ | Environment Variable | Required | Default | Description |
213
+ |---|---|---|---|
214
+ | `SELF_MANAGED_CACHE` | no | `false` | Set to `true` to enable auto-fetch from tenant-manager |
215
+
216
+ #### Tenant-manager API (`TenantService`)
217
+
218
+ Only required when `self_managed_cache` is `True`:
219
+
220
+ | Environment Variable | Required | Description |
221
+ |---|---|---|
222
+ | `TENANT_MANAGER_BASE_URL` | yes | Base URL of the tenant-manager API |
223
+ | `SEED_TENANT_TOKEN` | yes | Bearer token used to authenticate requests |
224
+ | `SEED_TENANT_ID` | yes | Tenant ID sent as the `x-tenant-id` header |
225
+
226
+ ### Usage
227
+
228
+ ```python
229
+ from dotenv import load_dotenv
230
+ from cache_manager import CacheManager
231
+
232
+ load_dotenv()
233
+
234
+ manager = CacheManager(self_managed_cache=True, ttl=10 * 60)
235
+ data = manager.get_secret_data("15", "azureConfiguration")
236
+
237
+ if data is None:
238
+ print("Secret not found or tenant-manager unavailable")
239
+ else:
240
+ print(f"Secret: {data}")
241
+ ```
242
+
243
+ #### Resolution order inside `get_secret_data`
244
+
245
+ 1. **Redis cache** — returns immediately on a hit.
246
+ 2. **Tenant-manager API** — called only when `self_managed_cache=True`; the response is stored in Redis with the configured TTL before being returned.
247
+ 3. Returns `None` when both sources are unavailable or the secret does not exist.
248
+
249
+ ### API Reference
250
+
251
+ #### `CacheManager(redis_client?, tenant_service?, self_managed_cache?, ttl?)`
252
+
253
+ | Parameter | Type | Default | Description |
254
+ |---|---|---|---|
255
+ | `redis_client` | `RedisClient` | auto-constructed from env | Pre-built Redis client |
256
+ | `tenant_service` | `TenantService` | auto-constructed when `self_managed_cache=True` | Pre-built tenant service |
257
+ | `self_managed_cache` | `bool` | `SELF_MANAGED_CACHE` env var | Enable auto-fetch on cache miss |
258
+ | `ttl` | `int` (seconds) | `172800` (48 h) | TTL applied when writing to Redis |
259
+
260
+ #### `CacheManager.get_secret_data(tenant_id: str, secret_name: str) -> Optional[Any]`
261
+
262
+ Retrieves the secret for the given `tenant_id` / `secret_name` pair following the resolution order described above.
263
+
264
+ #### `RedisClient` — low-level Redis wrapper
265
+
266
+ Can be used independently when direct Redis access is needed:
267
+
268
+ ```python
269
+ from cache_manager import RedisClient
270
+
271
+ client = RedisClient() # all settings from env vars
272
+ client = RedisClient(host="localhost", port=6379, db=1) # override specific params
273
+
274
+ client.set("my:key", {"foo": "bar"}, ttl=3600) # JSON-serialized, 1-hour TTL
275
+ data = client.get("my:key") # {"foo": "bar"}
276
+
277
+ client.set("config:flag", "enabled")
278
+ flag = client.get("config:flag") # "enabled"
279
+
280
+ missing = client.get("does:not:exist") # None
281
+ ```
282
+
283
+ #### `TenantService` — HTTP client for the tenant-manager API
284
+
285
+ Can be used independently when direct API access is needed:
286
+
287
+ ```python
288
+ from cache_manager import TenantService
289
+
290
+ service = TenantService(
291
+ base_url="https://tenant-manager.internal",
292
+ seed_token="my-token",
293
+ seed_tenant_id="1",
294
+ )
295
+ secret = service.get_secret("15", "azureConfiguration")
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Development
301
+
302
+ ### Setup
303
+
304
+ ```bash
305
+ pip install -r requirements.txt
306
+ ```
307
+
308
+ ### Running Tests
309
+
310
+ ```bash
311
+ pytest
312
+ ```
313
+
314
+ With coverage:
315
+
316
+ ```bash
317
+ pytest --cov=tenant_mgr_cache --cov-report=term-missing
318
+ ```
319
+
320
+ ---
321
+
322
+ ## License
323
+
324
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,307 @@
1
+ # litewave-cache-lib
2
+
3
+ A lightweight Python library providing two complementary packages for Redis-backed tenant configuration storage:
4
+
5
+ | Package | Purpose |
6
+ |---|---|
7
+ | [`tenant_mgr_cache`](#tenant_mgr_cache) | Read-only cache reader — look up tenant secrets already stored in Redis |
8
+ | [`cache_manager`](#cache_manager) | Full cache manager — read from Redis and optionally auto-fetch from the tenant-manager API on a cache miss |
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install git+https://github.com/aiorch/litewave-cache-lib
16
+ ```
17
+
18
+ Or install from source:
19
+
20
+ ```bash
21
+ git clone https://github.com/aiorch/litewave-cache-lib.git
22
+ cd litewave-cache-lib
23
+ pip install .
24
+ ```
25
+
26
+ **Requirements:** Python >= 3.10, `redis >= 5.0.0`, `httpx >= 0.25.0`, `python-dotenv >= 1.0.0`
27
+
28
+ ---
29
+
30
+ ## `tenant_mgr_cache`
31
+
32
+ A lightweight, read-only cache reader for tenant secrets stored in Redis. Provides three simple functions covering all lookup patterns.
33
+
34
+ ![tenant_mgr_cache package diagram](tenant_mgr_cache.png)
35
+
36
+ ### Features
37
+
38
+ - Connection-pooled Redis client with automatic retry on failure (5-minute cooldown)
39
+ - Fetch values by raw Redis key, or by `(tenant_id, secret_name)` pair via `get_secret`
40
+ - Attribute-level extraction from stored JSON objects via `get_attribute_value_by_key`
41
+ - Automatic JSON deserialization of stored values
42
+ - Standardised tenant key format: `tenant:{tenant_id}:secret:{secret_name}`
43
+ - Zero-boilerplate logging setup
44
+
45
+ ### Configuration
46
+
47
+ Settings are read from environment variables **at import time** (`CacheSettings` is instantiated on import). Always call `load_dotenv()` **before** importing from `tenant_mgr_cache`.
48
+
49
+ | Environment Variable | Required | Default | Description |
50
+ |---|---|---|---|
51
+ | `REDIS_HOST` | yes | — | Redis server hostname |
52
+ | `REDIS_PORT` | yes | — | Redis server port |
53
+ | `REDIS_PASSWORD` | yes | — | Redis password |
54
+ | `REDIS_TENANT_CONFIG_DB` | no | `0` | Redis database index for tenant config |
55
+ | `REDIS_MAX_CONNECTIONS` | no | `50` | Connection pool size |
56
+ | `REDIS_SOCKET_CONNECT_TIMEOUT` | no | `2` | Socket connect timeout (seconds) |
57
+ | `REDIS_SOCKET_TIMEOUT` | no | `2` | Socket read/write timeout (seconds) |
58
+ | `LOG_LEVEL` | no | `INFO` | Logging level (`DEBUG`, `INFO`, `WARNING`, …) |
59
+
60
+ ### Usage
61
+
62
+ > **Important:** `load_dotenv()` must be called before importing `tenant_mgr_cache` because environment variables are resolved at module load time.
63
+
64
+ ```python
65
+ from dotenv import load_dotenv
66
+
67
+ load_dotenv(".env", override=True)
68
+
69
+ from tenant_mgr_cache import (
70
+ get_attribute_value_by_key,
71
+ get_secret,
72
+ get_value_by_key,
73
+ )
74
+ ```
75
+
76
+ #### Case 1 — fetch by fully-formed Redis key
77
+
78
+ Pass a complete, pre-built Redis key directly:
79
+
80
+ ```python
81
+ key = "tenant:15:secret:satoken"
82
+ value = get_value_by_key(key)
83
+ if value is not None:
84
+ print(f"{key} → {value}")
85
+ else:
86
+ print("Key not found or Redis unavailable.")
87
+ ```
88
+
89
+ #### Case 2 — fetch by tenant ID and secret name
90
+
91
+ The canonical key `tenant:{tenant_id}:secret:{secret_name}` is built internally:
92
+
93
+ ```python
94
+ value = get_secret("15", "satoken")
95
+ if value is not None:
96
+ print(f"secret → {value}")
97
+ else:
98
+ print("Not found.")
99
+ ```
100
+
101
+ JSON objects stored in Redis are deserialized automatically; plain strings are returned as-is.
102
+
103
+ #### Case 3 — fetch a single attribute from a stored JSON object
104
+
105
+ Builds the key, fetches the JSON object, and returns the value of the requested attribute:
106
+
107
+ ```python
108
+ value = get_attribute_value_by_key("15", "satoken", "api_key")
109
+ if value is not None:
110
+ print(f"api_key → {value}")
111
+ else:
112
+ print("Attribute not found.")
113
+ ```
114
+
115
+ ### API Reference
116
+
117
+ #### `get_value_by_key(key: str) -> Optional[Any]`
118
+
119
+ Fetches the value stored at `key` and JSON-decodes it where possible. Returns `None` when the key does not exist, Redis is unavailable, or an error occurs.
120
+
121
+ #### `get_secret(tenant_id: str, secret_name: str) -> Optional[Any]`
122
+
123
+ Builds the canonical key `tenant:{tenant_id}:secret:{secret_name}` and returns the stored value. JSON is deserialized automatically. Returns `None` if the key does not exist or Redis is unavailable.
124
+
125
+ #### `get_attribute_value_by_key(tenant_id: str, secret_name: str, attribute_name: str) -> Optional[Any]`
126
+
127
+ Builds the canonical key, fetches the stored JSON object, and returns `data.get(attribute_name)`. Falls back to `getattr` for non-dict objects. Returns `None` if the key is missing, the value is not dict-like, or the attribute is absent.
128
+
129
+ #### `get_redis_client() -> Optional[redis.Redis]`
130
+
131
+ Returns the shared, connection-pooled Redis client. Returns `None` if the connection is unavailable. A failed connection is retried automatically after a 5-minute cooldown; subsequent calls reuse the existing client.
132
+
133
+ #### `prepare_key(tenant_id: str, secret_name: str) -> str`
134
+
135
+ Builds the canonical Redis key:
136
+
137
+ ```python
138
+ from tenant_mgr_cache.cache_client import prepare_key
139
+
140
+ key = prepare_key("15", "satoken")
141
+ print(key) # "tenant:15:secret:satoken"
142
+ ```
143
+
144
+ #### `settings` — `CacheSettings`
145
+
146
+ A dataclass instance holding all resolved configuration values:
147
+
148
+ ```python
149
+ from tenant_mgr_cache import settings
150
+
151
+ print(settings.REDIS_HOST)
152
+ print(settings.REDIS_PORT)
153
+ print(settings.REDIS_TENANT_CONFIG_DB)
154
+ ```
155
+
156
+ #### `logger` — `logging.Logger`
157
+
158
+ A pre-configured logger (`litewave_cache`) that respects `LOG_LEVEL`:
159
+
160
+ ```python
161
+ from tenant_mgr_cache import logger
162
+
163
+ logger.info("Custom message from application code")
164
+ ```
165
+
166
+ ---
167
+
168
+ ## `cache_manager`
169
+
170
+ A higher-level cache manager that wraps a `RedisClient` and an optional `TenantService`. When `self_managed_cache` is enabled, a cache miss automatically fetches the secret from the tenant-manager HTTP API and populates Redis (default TTL: 48 hours).
171
+
172
+ ### Features
173
+
174
+ - Reads from Redis first; falls back to tenant-manager API on a cache miss (when enabled)
175
+ - Auto-populates Redis after a successful tenant-manager fetch (48-hour TTL by default)
176
+ - Fully configurable via constructor arguments or environment variables
177
+ - Low-level `RedisClient` and `TenantService` classes are also exported for direct use
178
+
179
+ ### Configuration
180
+
181
+ #### Redis connection (`RedisClient`)
182
+
183
+ | Environment Variable | Required | Default | Description |
184
+ |---|---|---|---|
185
+ | `REDIS_HOST` | yes | — | Redis server hostname |
186
+ | `REDIS_PORT` | yes | — | Redis server port |
187
+ | `REDIS_PASSWORD` | no | `None` | Redis password |
188
+ | `REDIS_TENANT_CONFIG_DB` | no | `0` | Redis database index |
189
+ | `REDIS_MAX_CONNECTIONS` | no | `50` | Connection pool size |
190
+ | `REDIS_SOCKET_CONNECT_TIMEOUT` | no | `2.0` | Socket connect timeout (seconds) |
191
+ | `REDIS_SOCKET_TIMEOUT` | no | `2.0` | Socket read/write timeout (seconds) |
192
+
193
+ #### Cache manager behaviour
194
+
195
+ | Environment Variable | Required | Default | Description |
196
+ |---|---|---|---|
197
+ | `SELF_MANAGED_CACHE` | no | `false` | Set to `true` to enable auto-fetch from tenant-manager |
198
+
199
+ #### Tenant-manager API (`TenantService`)
200
+
201
+ Only required when `self_managed_cache` is `True`:
202
+
203
+ | Environment Variable | Required | Description |
204
+ |---|---|---|
205
+ | `TENANT_MANAGER_BASE_URL` | yes | Base URL of the tenant-manager API |
206
+ | `SEED_TENANT_TOKEN` | yes | Bearer token used to authenticate requests |
207
+ | `SEED_TENANT_ID` | yes | Tenant ID sent as the `x-tenant-id` header |
208
+
209
+ ### Usage
210
+
211
+ ```python
212
+ from dotenv import load_dotenv
213
+ from cache_manager import CacheManager
214
+
215
+ load_dotenv()
216
+
217
+ manager = CacheManager(self_managed_cache=True, ttl=10 * 60)
218
+ data = manager.get_secret_data("15", "azureConfiguration")
219
+
220
+ if data is None:
221
+ print("Secret not found or tenant-manager unavailable")
222
+ else:
223
+ print(f"Secret: {data}")
224
+ ```
225
+
226
+ #### Resolution order inside `get_secret_data`
227
+
228
+ 1. **Redis cache** — returns immediately on a hit.
229
+ 2. **Tenant-manager API** — called only when `self_managed_cache=True`; the response is stored in Redis with the configured TTL before being returned.
230
+ 3. Returns `None` when both sources are unavailable or the secret does not exist.
231
+
232
+ ### API Reference
233
+
234
+ #### `CacheManager(redis_client?, tenant_service?, self_managed_cache?, ttl?)`
235
+
236
+ | Parameter | Type | Default | Description |
237
+ |---|---|---|---|
238
+ | `redis_client` | `RedisClient` | auto-constructed from env | Pre-built Redis client |
239
+ | `tenant_service` | `TenantService` | auto-constructed when `self_managed_cache=True` | Pre-built tenant service |
240
+ | `self_managed_cache` | `bool` | `SELF_MANAGED_CACHE` env var | Enable auto-fetch on cache miss |
241
+ | `ttl` | `int` (seconds) | `172800` (48 h) | TTL applied when writing to Redis |
242
+
243
+ #### `CacheManager.get_secret_data(tenant_id: str, secret_name: str) -> Optional[Any]`
244
+
245
+ Retrieves the secret for the given `tenant_id` / `secret_name` pair following the resolution order described above.
246
+
247
+ #### `RedisClient` — low-level Redis wrapper
248
+
249
+ Can be used independently when direct Redis access is needed:
250
+
251
+ ```python
252
+ from cache_manager import RedisClient
253
+
254
+ client = RedisClient() # all settings from env vars
255
+ client = RedisClient(host="localhost", port=6379, db=1) # override specific params
256
+
257
+ client.set("my:key", {"foo": "bar"}, ttl=3600) # JSON-serialized, 1-hour TTL
258
+ data = client.get("my:key") # {"foo": "bar"}
259
+
260
+ client.set("config:flag", "enabled")
261
+ flag = client.get("config:flag") # "enabled"
262
+
263
+ missing = client.get("does:not:exist") # None
264
+ ```
265
+
266
+ #### `TenantService` — HTTP client for the tenant-manager API
267
+
268
+ Can be used independently when direct API access is needed:
269
+
270
+ ```python
271
+ from cache_manager import TenantService
272
+
273
+ service = TenantService(
274
+ base_url="https://tenant-manager.internal",
275
+ seed_token="my-token",
276
+ seed_tenant_id="1",
277
+ )
278
+ secret = service.get_secret("15", "azureConfiguration")
279
+ ```
280
+
281
+ ---
282
+
283
+ ## Development
284
+
285
+ ### Setup
286
+
287
+ ```bash
288
+ pip install -r requirements.txt
289
+ ```
290
+
291
+ ### Running Tests
292
+
293
+ ```bash
294
+ pytest
295
+ ```
296
+
297
+ With coverage:
298
+
299
+ ```bash
300
+ pytest --cov=tenant_mgr_cache --cov-report=term-missing
301
+ ```
302
+
303
+ ---
304
+
305
+ ## License
306
+
307
+ MIT License. See [LICENSE](LICENSE) for details.