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.
- litewave_cache_lib-0.1.4/.env.sample +24 -0
- {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/.github/workflows/ci-trigger.yml +25 -1
- {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/.gitignore +2 -1
- litewave_cache_lib-0.1.4/PKG-INFO +324 -0
- litewave_cache_lib-0.1.4/README.md +307 -0
- litewave_cache_lib-0.1.4/cache_manager/README.md +399 -0
- litewave_cache_lib-0.1.4/cache_manager/__init__.py +25 -0
- litewave_cache_lib-0.1.4/cache_manager/cache_manager.py +289 -0
- litewave_cache_lib-0.1.4/cache_manager/cache_refresh_ticker.py +248 -0
- litewave_cache_lib-0.1.4/cache_manager/redis_client.py +245 -0
- litewave_cache_lib-0.1.4/cache_manager/tenant_manager.py +131 -0
- litewave_cache_lib-0.1.4/cache_manager.png +0 -0
- litewave_cache_lib-0.1.4/cache_manager_ticker.png +0 -0
- litewave_cache_lib-0.1.4/examples/example_cache_manager.py +32 -0
- litewave_cache_lib-0.1.2/cache_reader.py → litewave_cache_lib-0.1.4/examples/example_tenant_mgr_cache.py +8 -3
- {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/pyproject.toml +3 -2
- {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/requirements.txt +2 -0
- {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/setup.py +15 -6
- {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/tenant_mgr_cache/cache_client.py +5 -17
- litewave_cache_lib-0.1.4/tenant_mgr_cache.png +0 -0
- litewave_cache_lib-0.1.4/tests/__init__.py +0 -0
- litewave_cache_lib-0.1.4/tests/conftest.py +25 -0
- litewave_cache_lib-0.1.4/tests/test_cache_manager.py +541 -0
- litewave_cache_lib-0.1.4/tests/test_cache_refresh_ticker.py +275 -0
- litewave_cache_lib-0.1.4/tests/test_redis_client.py +344 -0
- litewave_cache_lib-0.1.4/tests/test_tenant_manager.py +281 -0
- litewave_cache_lib-0.1.2/PKG-INFO +0 -208
- litewave_cache_lib-0.1.2/README.md +0 -192
- {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/.github/workflows/publish.yml +0 -0
- {litewave_cache_lib-0.1.2 → litewave_cache_lib-0.1.4}/tenant_mgr_cache/__init__.py +0 -0
- {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
|
|
@@ -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
|
+

|
|
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
|
+

|
|
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.
|