Skip to content

Commit dc26bc8

Browse files
committed
v0.2.0: release
1 parent f4ba4d3 commit dc26bc8

11 files changed

Lines changed: 226 additions & 16 deletions

File tree

.idea/secrets-cache.iml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

HISTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# History
22

3+
## 0.2.0 (2025-09-13)
4+
5+
* Add core caching logic.
6+
37
## 0.1.0 (2025-09-12)
48

59
* First release on PyPI.

README.md

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,108 @@
33
![PyPI version](https://img.shields.io/pypi/v/secrets-cache.svg)
44
[![Documentation Status](https://readthedocs.org/projects/secrets-cache/badge/?version=latest)](https://secrets-cache.readthedocs.io/en/latest/?version=latest)
55

6-
Cache secrets locally from AWS Secrets Manager and other secret stores.
6+
Cache secrets locally from AWS Secrets Manager and other secret stores, with optional local caching for development or Lambda-friendly usage.
77

88
* PyPI package: https://pypi.org/project/secrets-cache/
99
* Free software: MIT License
1010
* Documentation: https://secrets-cache.readthedocs.io.
1111

12+
## Installation
13+
14+
Install the base package (minimal, Lambda-friendly):
15+
16+
```bash
17+
pip install secrets-cache[lambda]
18+
````
19+
20+
Install with **local cache support** (TOML) for testing / development:
21+
22+
```bash
23+
pip install secrets-cache[local]
24+
```
25+
26+
Install with CLI support:
27+
28+
```bash
29+
pip install secrets-cache[cli]
30+
```
31+
32+
You can also combine extras:
33+
34+
```bash
35+
pip install "secrets-cache[local,cli]"
36+
```
37+
38+
## Usage
39+
40+
```python
41+
from secrets_cache import get_secret, get_param
42+
43+
# Get a secret from AWS Secrets Manager
44+
my_secret = get_secret("my-secret-name", region="us-east-1")
45+
46+
# Get a parameter from AWS SSM Parameter Store
47+
my_param = get_param("/my/parameter/name", region="us-east-1")
48+
```
49+
50+
**Notes:**
51+
52+
* By default, secrets are cached **in memory** to reduce repeated AWS calls.
53+
* If `local` extra is installed, secrets are also stored in `~/.secrets_cache.toml` for local caching.
54+
* Module-level caches persist across **warm AWS Lambda invocations**, so repeated calls in the same container are very fast.
55+
1256
## Features
1357

14-
* TODO
58+
* Fetch secrets and parameters from AWS Secrets Manager / SSM.
59+
* Module-level caching for in-process efficiency.
60+
* Optional TOML caching for development.
61+
* Lambda-friendly usage without extra dependencies.
62+
* Easy to extend to other secret stores in the future.
63+
64+
## Getting Started: AWS Lambda
65+
66+
When running in AWS Lambda, you usually don’t want file-based caching. Use the `lambda` extra:
67+
68+
```bash
69+
pip install secrets-cache[lambda]
70+
````
71+
72+
### Example Lambda handler
73+
74+
```python
75+
import json
76+
from secrets_cache import get_secret, get_param
77+
78+
def lambda_handler(event, context):
79+
# Get a secret from AWS Secrets Manager
80+
db_password = get_secret("my-db-password", region="us-east-1")
81+
82+
# Get a parameter from AWS SSM Parameter Store
83+
api_key = get_param("/my/api/key", region="us-east-1")
84+
85+
# Do something with your secrets
86+
return {
87+
"statusCode": 200,
88+
"body": json.dumps({
89+
"db_password_length": len(db_password),
90+
"api_key_length": len(api_key)
91+
})
92+
}
93+
```
94+
95+
### Notes for Lambda
96+
97+
* **Module-level caching** ensures repeated calls in the same container are very fast.
98+
* No TOML or local file access is required — perfect for ephemeral Lambda environments.
99+
* Secrets are cached **in memory only**, and each new container start fetches them from AWS.
100+
* If you want local development caching, install the `local` extra:
101+
102+
```bash
103+
pip install secrets-cache[local]
104+
```
105+
106+
This enables optional `~/.secrets_cache.toml` caching for local testing.
15107

16108
## Credits
17109

18-
This package was created with [Cookiecutter](https://github.com/audreyfeldroy/cookiecutter) and the [rnag/cookiecutter-pypackage](https://github.com/rnag/cookiecutter-pypackage) project template.
110+
Created with [Cookiecutter](https://github.com/audreyfeldroy/cookiecutter) and the [rnag/cookiecutter-pypackage](https://github.com/rnag/cookiecutter-pypackage) template.

pyproject.toml

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "secrets-cache"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
description = "Cache secrets locally from AWS Secrets Manager and other secret stores."
55
readme = "README.md"
66
authors = [
@@ -9,15 +9,15 @@ authors = [
99
maintainers = [
1010
{name = "Ritvik Nag", email = "me@ritviknag.com"}
1111
]
12+
keywords = ["aws", "secrets", "ssm", "cache", "lambda"]
1213
classifiers = [
1314
# TODO
1415
# Ref: https://pypi.org/classifiers/
1516
# 'Development Status :: 5 - Production/Stable',
16-
# 'Development Status :: 4 - Beta',
17-
'Development Status :: 2 - Pre-Alpha',
17+
'Development Status :: 4 - Beta',
1818
'Intended Audience :: Developers',
19-
'License :: OSI Approved :: MIT License',
2019
'Natural Language :: English',
20+
'License :: OSI Approved :: MIT License',
2121
'Programming Language :: Python',
2222
'Programming Language :: Python :: 3',
2323
'Programming Language :: Python :: 3.10',
@@ -29,12 +29,20 @@ classifiers = [
2929
'Topic :: Software Development',
3030
]
3131
license = {text = "MIT"}
32-
dependencies = [
33-
"typer"
34-
]
32+
# Base dependencies (always installed)
33+
dependencies = []
3534
requires-python = ">= 3.10"
3635

3736
[project.optional-dependencies]
37+
cli = [
38+
'typer', # CLI / optional but handy
39+
]
40+
local = [
41+
'boto3>=1.26', # AWS SDK for fetching secrets
42+
'tomli>=2.0; python_version<"3.11"', # TOML reader for Python <3.11
43+
'tomli-w>=1.0; python_version<"3.11"', # TOML writer for Python <3.11
44+
]
45+
lambda = [] # Lambda-friendly, skips boto3
3846
test = [
3947
"coverage", # testing
4048
"pytest", # testing

src/secrets_cache/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
"""Top-level package for Secrets Cache."""
1+
"""Secrets Cache Library - get AWS secrets and SSM parameters with optional caching."""
2+
from ._aws import get_secret, get_param
3+
4+
__all__ = ["get_secret", "get_param"]
25

36
__author__ = """Ritvik Nag"""
47
__email__ = 'me@ritviknag.com'

src/secrets_cache/_aws.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Main module."""
2+
3+
from pathlib import Path
4+
5+
import boto3
6+
7+
from ._cache import cached_fetch
8+
9+
10+
# Optional: import TOML only if available
11+
try:
12+
import tomllib # Python 3.11+
13+
except ImportError:
14+
try:
15+
import tomli as tomllib # Python 3.10
16+
except ImportError:
17+
tomllib = None
18+
19+
try:
20+
import tomli_w
21+
except ImportError:
22+
tomli_w = None
23+
24+
25+
# Module-level caches
26+
_secret_cache = {}
27+
_param_cache = {}
28+
_boto_clients = {}
29+
30+
# TOML cache file (optional)
31+
_CACHE_FILE = Path.home() / '.secrets_cache.toml'
32+
33+
34+
def get_boto_client(service: str, region: str = 'us-east-1'):
35+
"""Cache boto3 clients per service/region."""
36+
key = service, region
37+
_client = _boto_clients.get(key)
38+
if _client is None:
39+
_boto_clients[key] = _client = boto3.client(service, region_name=region)
40+
return _client
41+
42+
43+
def _read_toml_cache():
44+
if not tomllib or not _CACHE_FILE.exists():
45+
return {}
46+
with _CACHE_FILE.open('rb') as f:
47+
return tomllib.load(f)
48+
49+
50+
def _write_toml_cache(data):
51+
if not tomli_w or not _CACHE_FILE.parent.exists():
52+
return
53+
with _CACHE_FILE.open('wb') as f:
54+
tomli_w.dump(data, f)
55+
56+
57+
def get_secret(name: str, region: str = 'us-east-1', ttl: int = 7 * 24 * 3600):
58+
"""Get secret from AWS Secrets Manager with optional caching."""
59+
return cached_fetch(_secret_cache, name, region, _fetch_secret, ttl)
60+
61+
62+
def get_param(name: str, region: str = 'us-east-1', ttl: int = 7 * 24 * 3600):
63+
"""Get parameter from AWS SSM Parameter Store with optional caching."""
64+
return cached_fetch(_param_cache, name, region, _fetch_param, ttl)
65+
66+
67+
def _fetch_secret(name: str, region: str = 'us-east-1'):
68+
client = get_boto_client('secretsmanager', region)
69+
resp = client.get_secret_value(SecretId=name)
70+
value = resp['SecretString']
71+
72+
# Update local TOML cache if available
73+
data = _read_toml_cache()
74+
data[name] = value
75+
_write_toml_cache(data)
76+
return value
77+
78+
79+
def _fetch_param(name: str, region: str = 'us-east-1'):
80+
client = get_boto_client('ssm', region)
81+
resp = client.get_parameter(Name=name, WithDecryption=True)
82+
value = resp['Parameter']['Value']
83+
84+
# Update local TOML cache if available
85+
data = _read_toml_cache()
86+
data[name] = value
87+
_write_toml_cache(data)
88+
return value

src/secrets_cache/_cache.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from time import time
2+
3+
4+
def cached_fetch(cache: dict, key: str, region: str, fetcher, ttl: int):
5+
"""Fetch from cache or call fetcher."""
6+
now = time()
7+
entry = cache.get(key)
8+
if entry and (now - entry['fetched_at'] < ttl):
9+
return entry['value']
10+
11+
value = fetcher(key, region)
12+
cache[key] = {'value': value, 'fetched_at': now}
13+
return value

src/secrets_cache/cli.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import typer
44
from rich.console import Console
55

6-
from secrets_cache import utils
76

87
app = typer.Typer()
98
console = Console()
@@ -15,7 +14,7 @@ def main():
1514
console.print("Replace this message by putting your code into "
1615
"secrets_cache.cli.main")
1716
console.print("See Typer documentation at https://typer.tiangolo.com/")
18-
utils.do_something_useful()
17+
# utils.do_something_useful()
1918

2019

2120
if __name__ == "__main__":

src/secrets_cache/secrets_cache.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/secrets_cache/utils.py

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)