aws-util 0.1.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.
- aws_util-0.1.0/PKG-INFO +10 -0
- aws_util-0.1.0/pyproject.toml +23 -0
- aws_util-0.1.0/setup.cfg +4 -0
- aws_util-0.1.0/src/__init__.py +1 -0
- aws_util-0.1.0/src/aws_util.egg-info/PKG-INFO +10 -0
- aws_util-0.1.0/src/aws_util.egg-info/SOURCES.txt +10 -0
- aws_util-0.1.0/src/aws_util.egg-info/dependency_links.txt +1 -0
- aws_util-0.1.0/src/aws_util.egg-info/requires.txt +1 -0
- aws_util-0.1.0/src/aws_util.egg-info/top_level.txt +4 -0
- aws_util-0.1.0/src/parameter_store.py +18 -0
- aws_util-0.1.0/src/placeholder.py +86 -0
- aws_util-0.1.0/src/secrets_manager.py +56 -0
aws_util-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aws-util
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A utility library for AWS services
|
|
5
|
+
Author-email: Masrik Dahir <info@masrikdahir.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.6
|
|
10
|
+
Requires-Dist: boto3
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aws-util"
|
|
7
|
+
authors = [ { name = "Masrik Dahir", email = "info@masrikdahir.com" }]
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "A utility library for AWS services"
|
|
10
|
+
requires-python = ">=3.6"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"boto3"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# If you have an email, uncomment the next line:
|
|
16
|
+
# email = "your_email@example.com" # Make sure no classification here.
|
|
17
|
+
|
|
18
|
+
# Define classifiers here, completely separate:
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent"
|
|
23
|
+
]
|
aws_util-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .placeholder import retrieve
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aws-util
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A utility library for AWS services
|
|
5
|
+
Author-email: Masrik Dahir <info@masrikdahir.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.6
|
|
10
|
+
Requires-Dist: boto3
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/__init__.py
|
|
3
|
+
src/parameter_store.py
|
|
4
|
+
src/placeholder.py
|
|
5
|
+
src/secrets_manager.py
|
|
6
|
+
src/aws_util.egg-info/PKG-INFO
|
|
7
|
+
src/aws_util.egg-info/SOURCES.txt
|
|
8
|
+
src/aws_util.egg-info/dependency_links.txt
|
|
9
|
+
src/aws_util.egg-info/requires.txt
|
|
10
|
+
src/aws_util.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
boto3
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
from botocore.exceptions import ClientError
|
|
5
|
+
|
|
6
|
+
ssm_client = boto3.client("ssm")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_parameter(name: str, with_decryption: bool = True) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Single SSM call, no caching here.
|
|
12
|
+
Caching will be handled in src.placeholder via lru_cache.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
resp = ssm_client.get_parameter(Name=name, WithDecryption=with_decryption)
|
|
16
|
+
return resp["Parameter"]["Value"]
|
|
17
|
+
except ClientError as e:
|
|
18
|
+
raise RuntimeError(f"Error resolving SSM parameter {name!r}: {e}") from e
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from src.parameter_store import get_parameter
|
|
8
|
+
from src.secrets_manager import get_secret
|
|
9
|
+
|
|
10
|
+
# Matches ${ssm:/myapp/db/username}
|
|
11
|
+
SSM_PATTERN = re.compile(r"\$\{ssm:([^}]+)\}")
|
|
12
|
+
|
|
13
|
+
# Matches ${secret:myapp/db-credentials:password}
|
|
14
|
+
# or ${secret:myapp/db-credentials}
|
|
15
|
+
# or ${secret:${ssm:secret_name}:password} AFTER SSM phase
|
|
16
|
+
SECRET_PATTERN = re.compile(r"\$\{secret:([^}]+)\}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache(maxsize=256)
|
|
20
|
+
def _resolve_ssm(name: str) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Cached wrapper around parameter_store.get_parameter.
|
|
23
|
+
"""
|
|
24
|
+
return get_parameter(name, with_decryption=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@lru_cache(maxsize=256)
|
|
28
|
+
def _resolve_secret(inner: str) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Cached wrapper around secret_manager.get_secret.
|
|
31
|
+
"""
|
|
32
|
+
return get_secret(inner)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# -------- cache clear helpers (NO return type changes) --------
|
|
36
|
+
def clear_ssm_cache() -> None:
|
|
37
|
+
"""Clear cached SSM resolutions in this warm Lambda container."""
|
|
38
|
+
_resolve_ssm.cache_clear()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clear_secret_cache() -> None:
|
|
42
|
+
"""Clear cached Secret resolutions in this warm Lambda container."""
|
|
43
|
+
_resolve_secret.cache_clear()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def clear_all_caches() -> None:
|
|
47
|
+
"""Clear both SSM and Secret caches."""
|
|
48
|
+
_resolve_ssm.cache_clear()
|
|
49
|
+
_resolve_secret.cache_clear()
|
|
50
|
+
# -------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def retrieve(value: Any) -> Any:
|
|
54
|
+
"""
|
|
55
|
+
Replace placeholders in the given string:
|
|
56
|
+
|
|
57
|
+
${ssm:/myapp/db/username}
|
|
58
|
+
${secret:myapp/db-credentials:password}
|
|
59
|
+
|
|
60
|
+
Order is:
|
|
61
|
+
1) Resolve ALL ${ssm:...}
|
|
62
|
+
2) Then resolve ALL ${secret:...}
|
|
63
|
+
|
|
64
|
+
This allows nested patterns like:
|
|
65
|
+
${secret:${ssm:secret_name}:password}
|
|
66
|
+
|
|
67
|
+
Non-string values are returned as-is.
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(value, str):
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
# ---------- 1) SSM phase ----------
|
|
73
|
+
def ssm_replacer(match: re.Match) -> str:
|
|
74
|
+
name = match.group(1)
|
|
75
|
+
return _resolve_ssm(name)
|
|
76
|
+
|
|
77
|
+
value = SSM_PATTERN.sub(ssm_replacer, value)
|
|
78
|
+
|
|
79
|
+
# ---------- 2) Secrets Manager phase ----------
|
|
80
|
+
def secret_replacer(match: re.Match) -> str:
|
|
81
|
+
inner = match.group(1)
|
|
82
|
+
return _resolve_secret(inner)
|
|
83
|
+
|
|
84
|
+
value = SECRET_PATTERN.sub(secret_replacer, value)
|
|
85
|
+
|
|
86
|
+
return value
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
# Reuse client (good for Lambda cold starts)
|
|
9
|
+
secrets_client = boto3.client("secretsmanager")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_secret(inner: str) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Single Secrets Manager call, no caching here.
|
|
15
|
+
Caching will be handled in src.placeholder via lru_cache.
|
|
16
|
+
|
|
17
|
+
`inner` can be:
|
|
18
|
+
- "myapp/db-credentials"
|
|
19
|
+
- "myapp/db-credentials:password"
|
|
20
|
+
- "arn:aws:secretsmanager:...:secret:myapp/db-credentials:password"
|
|
21
|
+
|
|
22
|
+
We split on the *last* ':' so ARNs still work.
|
|
23
|
+
"""
|
|
24
|
+
json_key = None
|
|
25
|
+
|
|
26
|
+
if ":" in inner:
|
|
27
|
+
secret_id, json_key = inner.rsplit(":", 1)
|
|
28
|
+
else:
|
|
29
|
+
secret_id = inner
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
resp = secrets_client.get_secret_value(SecretId=secret_id)
|
|
33
|
+
except ClientError as e:
|
|
34
|
+
raise RuntimeError(f"Error resolving secret {secret_id!r}: {e}") from e
|
|
35
|
+
|
|
36
|
+
if "SecretString" in resp:
|
|
37
|
+
secret_str = resp["SecretString"]
|
|
38
|
+
else:
|
|
39
|
+
secret_str = resp["SecretBinary"].decode("utf-8")
|
|
40
|
+
|
|
41
|
+
# If a JSON key was specified, treat secret as JSON and extract that field
|
|
42
|
+
if json_key:
|
|
43
|
+
try:
|
|
44
|
+
data = json.loads(secret_str)
|
|
45
|
+
except json.JSONDecodeError as e:
|
|
46
|
+
raise RuntimeError(
|
|
47
|
+
f"Secret {secret_id!r} is not valid JSON so key {json_key!r} cannot be used"
|
|
48
|
+
) from e
|
|
49
|
+
|
|
50
|
+
if json_key not in data:
|
|
51
|
+
raise KeyError(f"Key {json_key!r} not found in secret {secret_id!r}")
|
|
52
|
+
|
|
53
|
+
return str(data[json_key])
|
|
54
|
+
|
|
55
|
+
# No key → return the whole secret string
|
|
56
|
+
return secret_str
|