nui-lambda-shared-utils 1.2.0__tar.gz → 1.2.2__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.
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/CLAUDE.md +25 -7
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/PKG-INFO +1 -1
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/getting-started/configuration.md +79 -8
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/__init__.py +3 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/base_client.py +83 -14
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/db_client.py +59 -41
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/es_client.py +26 -4
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/jwt_auth.py +60 -1
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/slack_client.py +24 -7
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_base_client.py +137 -23
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_db_client.py +110 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_es_client.py +64 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_jwt_auth.py +112 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_slack_client.py +65 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/.editorconfig +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/.github/workflows/ci.yml +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/.github/workflows/publish.yml +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/.github/workflows/test.yml +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/.markdownlint-cli2.yaml +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/CONTRIBUTING.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/LICENSE +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/MANIFEST.in +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/README.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/README.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/development/testing.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/getting-started/installation.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/getting-started/quickstart.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/guides/cli-tools.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/guides/elasticsearch-integration.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/guides/jwt-authentication.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/guides/lambda-utilities.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/guides/log-processing.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/guides/powertools-integration.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/guides/shared-types.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/docs/guides/slack-integration.md +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/mypy.ini +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/cli.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/cloudwatch_metrics.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/config.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/error_handler.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/es_query_builder.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/lambda_helpers.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/log_processors.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/powertools_helpers.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/secrets_helper.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/slack_formatter.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/slack_setup/__init__.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/slack_setup/channel_creator.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/slack_setup/channel_definitions.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/slack_setup/setup_helpers.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/timezone.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/utils.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils.egg-info/SOURCES.txt +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/pyproject.toml +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/pytest.ini +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/requirements-test.txt +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/setup.cfg +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/setup.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/__init__.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_aws_utils.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_cloudwatch_metrics.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_config.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_error_handler.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_es_query_builder.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_lambda_helpers.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_log_processors.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_powertools_helpers.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_secrets_helper.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_slack_formatter.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_timezone.py +0 -0
- {nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/tests/test_utils.py +0 -0
|
@@ -182,16 +182,33 @@ python -m twine upload dist/*
|
|
|
182
182
|
|
|
183
183
|
## Configuration Patterns
|
|
184
184
|
|
|
185
|
+
### Credential Resolution
|
|
186
|
+
|
|
187
|
+
Clients resolve credentials in this order (first match wins):
|
|
188
|
+
|
|
189
|
+
1. **Explicit `credentials` dict** — constructor param, skips everything else
|
|
190
|
+
2. **Environment variables** — per-client patterns, skips Secrets Manager
|
|
191
|
+
3. **AWS Secrets Manager** — default behavior
|
|
192
|
+
|
|
185
193
|
### Environment Variables
|
|
186
194
|
|
|
187
|
-
|
|
195
|
+
Runtime configuration:
|
|
188
196
|
|
|
189
197
|
- `ES_HOST` - Elasticsearch endpoint
|
|
190
198
|
- `ES_CREDENTIALS_SECRET` - AWS secret name for ES credentials
|
|
191
|
-
- `DB_CREDENTIALS_SECRET` - AWS secret name for database credentials
|
|
199
|
+
- `DB_CREDENTIALS_SECRET` - AWS secret name for database credentials
|
|
192
200
|
- `SLACK_CREDENTIALS_SECRET` - AWS secret name for Slack token
|
|
193
201
|
- `AWS_REGION` - AWS region for services
|
|
194
202
|
|
|
203
|
+
Direct credential env vars (bypass Secrets Manager when set):
|
|
204
|
+
|
|
205
|
+
| Client | Required | Optional |
|
|
206
|
+
|--------|----------|----------|
|
|
207
|
+
| Slack | `SLACK_BOT_TOKEN` | `SLACK_WEBHOOK_URL` |
|
|
208
|
+
| Elasticsearch | `ES_PASSWORD` | `ES_USERNAME` (default: `elastic`) |
|
|
209
|
+
| Database (MySQL) | `DB_HOST` + `DB_PASSWORD` | `DB_PORT` (3306), `DB_USERNAME` (root), `DB_DATABASE` (app) |
|
|
210
|
+
| Database (PostgreSQL) | `DB_HOST` + `DB_PASSWORD` | `DB_PORT` (5432), `DB_USERNAME` (postgres), `DB_DATABASE` (postgres) |
|
|
211
|
+
|
|
195
212
|
### AWS Secrets Format
|
|
196
213
|
|
|
197
214
|
Secrets should follow standardized JSON structures:
|
|
@@ -212,15 +229,16 @@ Secrets should follow standardized JSON structures:
|
|
|
212
229
|
```python
|
|
213
230
|
import nui_lambda_shared_utils as nui
|
|
214
231
|
|
|
215
|
-
#
|
|
232
|
+
# Pass credentials directly (no Secrets Manager needed)
|
|
233
|
+
slack = nui.SlackClient(credentials={"bot_token": "xoxb-..."})
|
|
234
|
+
es = nui.ElasticsearchClient(credentials={"username": "elastic", "password": "pass"})
|
|
235
|
+
db = nui.DatabaseClient(credentials={"host": "localhost", "port": 3306, "username": "root", "password": "pass", "database": "app"})
|
|
236
|
+
|
|
237
|
+
# Or configure globally
|
|
216
238
|
nui.configure(
|
|
217
239
|
es_host="localhost:9200",
|
|
218
240
|
slack_credentials_secret="dev/slack-token"
|
|
219
241
|
)
|
|
220
|
-
|
|
221
|
-
# Or use Config object
|
|
222
|
-
config = nui.Config(es_host="prod:9200")
|
|
223
|
-
nui.set_config(config)
|
|
224
242
|
```
|
|
225
243
|
|
|
226
244
|
## Common Usage Patterns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nui-lambda-shared-utils
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Enterprise-grade utilities for AWS Lambda functions with Slack, Elasticsearch, and monitoring integrations
|
|
5
5
|
Home-page: https://github.com/nuimarkets/nui-lambda-shared-utils
|
|
6
6
|
Author: NUI Markets
|
|
@@ -10,6 +10,14 @@ The package uses a hierarchical configuration system with the following priority
|
|
|
10
10
|
2. **Environment Variables** - System environment variables
|
|
11
11
|
3. **Configuration Defaults** - Package default values
|
|
12
12
|
|
|
13
|
+
### Credential Resolution Precedence
|
|
14
|
+
|
|
15
|
+
All clients (Slack, Elasticsearch, Database) resolve credentials in this order:
|
|
16
|
+
|
|
17
|
+
1. **Explicit `credentials` dict** - Passed to constructor, skips everything else
|
|
18
|
+
2. **Credential environment variables** - Per-client env vars, skips Secrets Manager
|
|
19
|
+
3. **AWS Secrets Manager** - Default behavior (unchanged)
|
|
20
|
+
|
|
13
21
|
## Environment Variables
|
|
14
22
|
|
|
15
23
|
### Core Configuration
|
|
@@ -34,19 +42,60 @@ The package supports alternative environment variable names for compatibility:
|
|
|
34
42
|
| `DB_CREDENTIALS_SECRET` | `DATABASE_CREDENTIALS_SECRET` |
|
|
35
43
|
| `AWS_REGION` | `AWS_DEFAULT_REGION` |
|
|
36
44
|
|
|
45
|
+
### Credential Environment Variables
|
|
46
|
+
|
|
47
|
+
These environment variables provide credentials directly, bypassing AWS Secrets Manager:
|
|
48
|
+
|
|
49
|
+
#### Slack
|
|
50
|
+
|
|
51
|
+
| Variable | Required | Default |
|
|
52
|
+
|----------|----------|---------|
|
|
53
|
+
| `SLACK_BOT_TOKEN` | Yes (triggers env var path) | - |
|
|
54
|
+
| `SLACK_WEBHOOK_URL` | No | - |
|
|
55
|
+
|
|
56
|
+
#### Elasticsearch
|
|
57
|
+
|
|
58
|
+
| Variable | Required | Default |
|
|
59
|
+
|----------|----------|---------|
|
|
60
|
+
| `ES_PASSWORD` | Yes (triggers env var path) | - |
|
|
61
|
+
| `ES_USERNAME` | No | `elastic` |
|
|
62
|
+
|
|
63
|
+
#### Database (MySQL)
|
|
64
|
+
|
|
65
|
+
| Variable | Required | Default |
|
|
66
|
+
|----------|----------|---------|
|
|
67
|
+
| `DB_HOST` | Yes (both required) | - |
|
|
68
|
+
| `DB_PASSWORD` | Yes (both required) | - |
|
|
69
|
+
| `DB_PORT` | No | `3306` |
|
|
70
|
+
| `DB_USERNAME` | No | `root` |
|
|
71
|
+
| `DB_DATABASE` | No | `app` |
|
|
72
|
+
|
|
73
|
+
#### Database (PostgreSQL)
|
|
74
|
+
|
|
75
|
+
| Variable | Required | Default |
|
|
76
|
+
|----------|----------|---------|
|
|
77
|
+
| `DB_HOST` | Yes (both required) | - |
|
|
78
|
+
| `DB_PASSWORD` | Yes (both required) | - |
|
|
79
|
+
| `DB_PORT` | No | `5432` |
|
|
80
|
+
| `DB_USERNAME` | No | `postgres` |
|
|
81
|
+
| `DB_DATABASE` | No | `postgres` |
|
|
82
|
+
|
|
37
83
|
### Example Environment Setup
|
|
38
84
|
|
|
39
85
|
```bash
|
|
40
|
-
#
|
|
86
|
+
# Secrets Manager configuration (default)
|
|
41
87
|
export ES_HOST="prod-elasticsearch:9200"
|
|
42
88
|
export ES_CREDENTIALS_SECRET="prod/elasticsearch-creds"
|
|
43
89
|
export DB_CREDENTIALS_SECRET="prod/database-creds"
|
|
44
90
|
export SLACK_CREDENTIALS_SECRET="prod/slack-token"
|
|
45
91
|
export AWS_REGION="us-west-2"
|
|
46
92
|
|
|
47
|
-
#
|
|
48
|
-
export
|
|
49
|
-
export
|
|
93
|
+
# Direct credentials (bypass Secrets Manager)
|
|
94
|
+
export SLACK_BOT_TOKEN="xoxb-your-token"
|
|
95
|
+
export ES_PASSWORD="your-password"
|
|
96
|
+
export DB_HOST="localhost"
|
|
97
|
+
export DB_PASSWORD="your-password"
|
|
98
|
+
export DB_DATABASE="mydb"
|
|
50
99
|
```
|
|
51
100
|
|
|
52
101
|
## Programmatic Configuration
|
|
@@ -186,9 +235,21 @@ aws secretsmanager create-secret \
|
|
|
186
235
|
4. Enter the JSON values as key-value pairs
|
|
187
236
|
5. Name the secret according to your configuration
|
|
188
237
|
|
|
189
|
-
###
|
|
238
|
+
### Credential Access Patterns
|
|
190
239
|
|
|
191
|
-
####
|
|
240
|
+
#### Direct Credentials (No AWS Required)
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
# Pass credentials directly — Secrets Manager is never called
|
|
244
|
+
slack = nui.SlackClient(credentials={"bot_token": "xoxb-your-token"})
|
|
245
|
+
es = nui.ElasticsearchClient(credentials={"username": "elastic", "password": "secret"})
|
|
246
|
+
db = nui.DatabaseClient(credentials={
|
|
247
|
+
"host": "localhost", "port": 3306,
|
|
248
|
+
"username": "root", "password": "secret", "database": "mydb"
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Automatic Credential Loading (Secrets Manager)
|
|
192
253
|
|
|
193
254
|
```python
|
|
194
255
|
# Credentials are automatically loaded based on configuration
|
|
@@ -197,7 +258,7 @@ db = nui.DatabaseClient() # Uses DB_CREDENTIALS_SECRET
|
|
|
197
258
|
slack = nui.SlackClient() # Uses SLACK_CREDENTIALS_SECRET
|
|
198
259
|
```
|
|
199
260
|
|
|
200
|
-
#### Override
|
|
261
|
+
#### Override Secret Names
|
|
201
262
|
|
|
202
263
|
```python
|
|
203
264
|
# Override default secret names
|
|
@@ -223,7 +284,17 @@ api_keys = nui.get_secret("my-service/api-keys")
|
|
|
223
284
|
### Development Environment
|
|
224
285
|
|
|
225
286
|
```python
|
|
226
|
-
#
|
|
287
|
+
# Option 1: Direct credentials (no AWS needed)
|
|
288
|
+
slack = nui.SlackClient(credentials={"bot_token": "xoxb-dev-token"})
|
|
289
|
+
db = nui.DatabaseClient(credentials={
|
|
290
|
+
"host": "localhost", "port": 3306,
|
|
291
|
+
"username": "root", "password": "dev", "database": "app_dev"
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
# Option 2: Environment variables (set SLACK_BOT_TOKEN, DB_HOST, DB_PASSWORD, etc.)
|
|
295
|
+
slack = nui.SlackClient() # picks up SLACK_BOT_TOKEN from env
|
|
296
|
+
|
|
297
|
+
# Option 3: Secrets Manager
|
|
227
298
|
nui.configure(
|
|
228
299
|
es_host="localhost:9200",
|
|
229
300
|
es_credentials_secret="dev/elasticsearch",
|
{nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/__init__.py
RENAMED
|
@@ -137,6 +137,7 @@ try:
|
|
|
137
137
|
from .jwt_auth import (
|
|
138
138
|
validate_jwt,
|
|
139
139
|
require_auth,
|
|
140
|
+
check_auth,
|
|
140
141
|
get_jwt_public_key,
|
|
141
142
|
JWTValidationError,
|
|
142
143
|
AuthenticationError,
|
|
@@ -144,6 +145,7 @@ try:
|
|
|
144
145
|
except ImportError:
|
|
145
146
|
validate_jwt = None # type: ignore
|
|
146
147
|
require_auth = None # type: ignore
|
|
148
|
+
check_auth = None # type: ignore
|
|
147
149
|
get_jwt_public_key = None # type: ignore
|
|
148
150
|
JWTValidationError = None # type: ignore
|
|
149
151
|
AuthenticationError = None # type: ignore
|
|
@@ -243,6 +245,7 @@ __all__ = [
|
|
|
243
245
|
# JWT authentication
|
|
244
246
|
"validate_jwt",
|
|
245
247
|
"require_auth",
|
|
248
|
+
"check_auth",
|
|
246
249
|
"get_jwt_public_key",
|
|
247
250
|
"JWTValidationError",
|
|
248
251
|
"AuthenticationError",
|
|
@@ -23,25 +23,32 @@ class BaseClient(ABC):
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
def __init__(
|
|
26
|
-
self,
|
|
26
|
+
self,
|
|
27
27
|
secret_name: Optional[str] = None,
|
|
28
28
|
config_key_prefix: Optional[str] = None,
|
|
29
|
+
credentials: Optional[Dict[str, Any]] = None,
|
|
29
30
|
**kwargs
|
|
30
31
|
):
|
|
31
32
|
"""
|
|
32
33
|
Initialize base client with standardized credential and config resolution.
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
Credential resolution precedence:
|
|
36
|
+
1. Explicit ``credentials`` dict — skip everything else
|
|
37
|
+
2. Environment variables (per-client patterns) — skip Secrets Manager
|
|
38
|
+
3. Secrets Manager (existing behavior) — unchanged
|
|
39
|
+
|
|
34
40
|
Args:
|
|
35
41
|
secret_name: Override secret name for credentials
|
|
36
42
|
config_key_prefix: Prefix for config keys (e.g., 'slack', 'es', 'db')
|
|
43
|
+
credentials: Direct credentials dict, bypasses Secrets Manager entirely
|
|
37
44
|
**kwargs: Additional client-specific parameters
|
|
38
45
|
"""
|
|
39
46
|
self.config = get_config()
|
|
40
47
|
self.config_key_prefix = config_key_prefix or self._get_default_config_prefix()
|
|
41
|
-
|
|
48
|
+
|
|
42
49
|
# Resolve and store credentials
|
|
43
|
-
self.credentials = self._resolve_credentials(secret_name)
|
|
44
|
-
|
|
50
|
+
self.credentials = self._resolve_credentials(secret_name, credentials)
|
|
51
|
+
|
|
45
52
|
# Store additional configuration
|
|
46
53
|
self.client_config = kwargs
|
|
47
54
|
|
|
@@ -72,18 +79,66 @@ class BaseClient(ABC):
|
|
|
72
79
|
"""Return the default secret name for this client type."""
|
|
73
80
|
pass
|
|
74
81
|
|
|
75
|
-
def
|
|
82
|
+
def _resolve_credentials_from_env(self) -> Optional[Dict[str, Any]]:
|
|
83
|
+
"""
|
|
84
|
+
Resolve credentials from environment variables.
|
|
85
|
+
|
|
86
|
+
Subclasses override this to check client-specific env vars
|
|
87
|
+
(e.g. SLACK_BOT_TOKEN, ES_PASSWORD). Return a credentials dict
|
|
88
|
+
if the required env vars are present, or ``None`` to fall through
|
|
89
|
+
to Secrets Manager.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Credentials dict or None
|
|
93
|
+
"""
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def _resolve_credentials(
|
|
97
|
+
self,
|
|
98
|
+
secret_name: Optional[str],
|
|
99
|
+
explicit_credentials: Optional[Dict[str, Any]] = None,
|
|
100
|
+
) -> Dict[str, Any]:
|
|
76
101
|
"""
|
|
77
102
|
Resolve credentials using standardized precedence.
|
|
78
|
-
|
|
103
|
+
|
|
104
|
+
Precedence:
|
|
105
|
+
1. Explicit credentials dict (constructor param)
|
|
106
|
+
2. Environment variables (per-client ``_resolve_credentials_from_env``)
|
|
107
|
+
3. AWS Secrets Manager (via ``_fetch_credentials_from_sm``)
|
|
108
|
+
|
|
79
109
|
Args:
|
|
80
110
|
secret_name: Optional override for secret name
|
|
81
|
-
|
|
111
|
+
explicit_credentials: Direct credentials dict from constructor
|
|
112
|
+
|
|
82
113
|
Returns:
|
|
83
114
|
Dictionary containing resolved credentials
|
|
84
115
|
"""
|
|
85
|
-
#
|
|
86
|
-
|
|
116
|
+
# 1. Explicit credentials dict — skip everything
|
|
117
|
+
if explicit_credentials is not None:
|
|
118
|
+
log.debug("Using explicitly provided credentials")
|
|
119
|
+
return explicit_credentials
|
|
120
|
+
|
|
121
|
+
# 2. Environment variables — skip Secrets Manager
|
|
122
|
+
env_credentials = self._resolve_credentials_from_env()
|
|
123
|
+
if env_credentials is not None:
|
|
124
|
+
log.debug("Using credentials from environment variables")
|
|
125
|
+
return env_credentials
|
|
126
|
+
|
|
127
|
+
# 3. Secrets Manager
|
|
128
|
+
return self._fetch_credentials_from_sm(secret_name)
|
|
129
|
+
|
|
130
|
+
def _resolve_secret_name(self, secret_name: Optional[str]) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Resolve the Secrets Manager secret name using standard precedence:
|
|
133
|
+
explicit param > env var > config default.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
secret_name: Optional explicit override
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Resolved secret name string
|
|
140
|
+
"""
|
|
141
|
+
resolved = resolve_config_value(
|
|
87
142
|
secret_name,
|
|
88
143
|
[
|
|
89
144
|
f"{self.config_key_prefix.upper()}_CREDENTIALS_SECRET",
|
|
@@ -91,10 +146,24 @@ class BaseClient(ABC):
|
|
|
91
146
|
],
|
|
92
147
|
getattr(self.config, f"{self.config_key_prefix}_credentials_secret", self._get_default_secret_name())
|
|
93
148
|
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
validate_required_param(resolved, "secret_name")
|
|
150
|
+
return resolved
|
|
151
|
+
|
|
152
|
+
def _fetch_credentials_from_sm(self, secret_name: Optional[str]) -> Dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Fetch credentials from AWS Secrets Manager.
|
|
155
|
+
|
|
156
|
+
Subclasses override this to customize the SM fetch (e.g. DB clients
|
|
157
|
+
use ``get_database_credentials`` for field normalization).
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
secret_name: Optional override for secret name
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary containing resolved credentials
|
|
164
|
+
"""
|
|
165
|
+
resolved_secret_name = self._resolve_secret_name(secret_name)
|
|
166
|
+
|
|
98
167
|
try:
|
|
99
168
|
credentials = get_secret(resolved_secret_name)
|
|
100
169
|
log.debug(f"Retrieved credentials from secret: {resolved_secret_name}")
|
{nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/db_client.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Refactored Database client using BaseClient for DRY code patterns.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import time
|
|
6
7
|
import logging
|
|
7
8
|
from typing import Dict, List, Optional, Any
|
|
@@ -120,23 +121,26 @@ class DatabaseClient(BaseClient, ServiceHealthMixin):
|
|
|
120
121
|
use_pool: bool = True,
|
|
121
122
|
pool_size: int = 5,
|
|
122
123
|
pool_recycle: int = 3600,
|
|
124
|
+
credentials: Optional[Dict[str, Any]] = None,
|
|
123
125
|
**kwargs
|
|
124
126
|
):
|
|
125
127
|
"""
|
|
126
128
|
Initialize database client.
|
|
127
|
-
|
|
129
|
+
|
|
128
130
|
Args:
|
|
129
131
|
secret_name: Override secret name
|
|
130
132
|
use_pool: Enable connection pooling
|
|
131
133
|
pool_size: Maximum pooled connections
|
|
132
134
|
pool_recycle: Recycle connections after seconds
|
|
135
|
+
credentials: Direct credentials dict (keys: host, port, username, password, database),
|
|
136
|
+
bypasses Secrets Manager
|
|
133
137
|
**kwargs: Additional configuration
|
|
134
138
|
"""
|
|
135
139
|
self.use_pool = use_pool
|
|
136
140
|
self.pool_size = pool_size
|
|
137
141
|
self.pool_recycle = pool_recycle
|
|
138
|
-
|
|
139
|
-
super().__init__(secret_name=secret_name, **kwargs)
|
|
142
|
+
|
|
143
|
+
super().__init__(secret_name=secret_name, credentials=credentials, **kwargs)
|
|
140
144
|
|
|
141
145
|
# Build pool key for connection management
|
|
142
146
|
self._pool_key = f"{self.credentials['host']}:{self.credentials['port']}"
|
|
@@ -153,32 +157,31 @@ class DatabaseClient(BaseClient, ServiceHealthMixin):
|
|
|
153
157
|
"""Database client doesn't have a single service client - uses connections."""
|
|
154
158
|
return None
|
|
155
159
|
|
|
156
|
-
def
|
|
157
|
-
"""
|
|
158
|
-
Override to use get_database_credentials for normalized field names.
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
secret_name: Optional secret name override
|
|
160
|
+
def _resolve_credentials_from_env(self) -> Optional[Dict[str, Any]]:
|
|
161
|
+
"""Resolve MySQL credentials from environment variables.
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
Normalized database credentials
|
|
163
|
+
Requires both DB_HOST and DB_PASSWORD to trigger.
|
|
165
164
|
"""
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
165
|
+
host = os.environ.get("DB_HOST")
|
|
166
|
+
password = os.environ.get("DB_PASSWORD")
|
|
167
|
+
if not host or not password:
|
|
168
|
+
return None
|
|
169
|
+
port_str = os.environ.get("DB_PORT", "3306")
|
|
170
|
+
try:
|
|
171
|
+
port = int(port_str)
|
|
172
|
+
except ValueError:
|
|
173
|
+
raise ValueError(f"DB_PORT must be an integer, got: {port_str!r}")
|
|
174
|
+
return {
|
|
175
|
+
"host": host,
|
|
176
|
+
"port": port,
|
|
177
|
+
"username": os.environ.get("DB_USERNAME", "root"),
|
|
178
|
+
"password": password,
|
|
179
|
+
"database": os.environ.get("DB_DATABASE", "app"),
|
|
180
|
+
}
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
def _fetch_credentials_from_sm(self, secret_name: Optional[str]) -> Dict[str, Any]:
|
|
183
|
+
"""Override to use get_database_credentials for normalized field names."""
|
|
184
|
+
return get_database_credentials(self._resolve_secret_name(secret_name))
|
|
182
185
|
|
|
183
186
|
def _clean_expired_connections(self, pool_key: str) -> None:
|
|
184
187
|
"""
|
|
@@ -476,21 +479,24 @@ class PostgreSQLClient(BaseClient, ServiceHealthMixin):
|
|
|
476
479
|
self,
|
|
477
480
|
secret_name: Optional[str] = None,
|
|
478
481
|
use_auth_credentials: bool = True,
|
|
482
|
+
credentials: Optional[Dict[str, Any]] = None,
|
|
479
483
|
**kwargs
|
|
480
484
|
):
|
|
481
485
|
"""
|
|
482
486
|
Initialize PostgreSQL client.
|
|
483
|
-
|
|
487
|
+
|
|
484
488
|
Args:
|
|
485
489
|
secret_name: Override secret name
|
|
486
490
|
use_auth_credentials: Use auth-specific credentials
|
|
491
|
+
credentials: Direct credentials dict (keys: host, port, username, password, database),
|
|
492
|
+
bypasses Secrets Manager
|
|
487
493
|
**kwargs: Additional configuration
|
|
488
494
|
"""
|
|
489
495
|
if not HAS_POSTGRESQL:
|
|
490
496
|
raise ImportError("psycopg2 is not installed. Install with: pip install psycopg2-binary")
|
|
491
497
|
|
|
492
498
|
self.use_auth_credentials = use_auth_credentials
|
|
493
|
-
super().__init__(secret_name=secret_name, **kwargs)
|
|
499
|
+
super().__init__(secret_name=secret_name, credentials=credentials, **kwargs)
|
|
494
500
|
|
|
495
501
|
def _get_default_config_prefix(self) -> str:
|
|
496
502
|
"""Return configuration prefix for PostgreSQL."""
|
|
@@ -504,20 +510,33 @@ class PostgreSQLClient(BaseClient, ServiceHealthMixin):
|
|
|
504
510
|
"""PostgreSQL client doesn't have a single service client - uses connections."""
|
|
505
511
|
return None
|
|
506
512
|
|
|
507
|
-
def
|
|
508
|
-
"""
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
Args:
|
|
512
|
-
secret_name: Optional secret name override
|
|
513
|
-
|
|
514
|
-
Returns:
|
|
515
|
-
PostgreSQL credentials
|
|
513
|
+
def _resolve_credentials_from_env(self) -> Optional[Dict[str, Any]]:
|
|
514
|
+
"""Resolve PostgreSQL credentials from environment variables.
|
|
515
|
+
|
|
516
|
+
Requires both DB_HOST and DB_PASSWORD to trigger.
|
|
516
517
|
"""
|
|
518
|
+
host = os.environ.get("DB_HOST")
|
|
519
|
+
password = os.environ.get("DB_PASSWORD")
|
|
520
|
+
if not host or not password:
|
|
521
|
+
return None
|
|
522
|
+
port_str = os.environ.get("DB_PORT", "5432")
|
|
523
|
+
try:
|
|
524
|
+
port = int(port_str)
|
|
525
|
+
except ValueError:
|
|
526
|
+
raise ValueError(f"DB_PORT must be an integer, got: {port_str!r}")
|
|
527
|
+
return {
|
|
528
|
+
"host": host,
|
|
529
|
+
"port": port,
|
|
530
|
+
"username": os.environ.get("DB_USERNAME", "postgres"),
|
|
531
|
+
"password": password,
|
|
532
|
+
"database": os.environ.get("DB_DATABASE", "postgres"),
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
def _fetch_credentials_from_sm(self, secret_name: Optional[str]) -> Dict[str, Any]:
|
|
536
|
+
"""Override with auth-specific credential handling."""
|
|
517
537
|
from .secrets_helper import get_secret
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
resolved_secret_name = secret_name or self._get_default_secret_name()
|
|
538
|
+
|
|
539
|
+
resolved_secret_name = self._resolve_secret_name(secret_name)
|
|
521
540
|
raw_creds = get_secret(resolved_secret_name)
|
|
522
541
|
|
|
523
542
|
# Use auth-specific credentials if available and requested
|
|
@@ -530,7 +549,6 @@ class PostgreSQLClient(BaseClient, ServiceHealthMixin):
|
|
|
530
549
|
"database": raw_creds.get("auth_database", "auth-service-db"),
|
|
531
550
|
}
|
|
532
551
|
else:
|
|
533
|
-
# Fall back to normalized credentials
|
|
534
552
|
return get_database_credentials(resolved_secret_name)
|
|
535
553
|
|
|
536
554
|
@contextmanager
|
{nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/es_client.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Refactored Elasticsearch client using BaseClient for DRY code patterns.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import logging
|
|
6
7
|
from datetime import datetime, timedelta
|
|
7
8
|
from typing import Dict, Iterator, List, Optional, Any, Tuple
|
|
@@ -20,18 +21,25 @@ class ElasticsearchClient(BaseClient, ServiceHealthMixin):
|
|
|
20
21
|
Refactored Elasticsearch client with standardized patterns.
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
|
-
def __init__(
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
host: Optional[str] = None,
|
|
27
|
+
secret_name: Optional[str] = None,
|
|
28
|
+
credentials: Optional[Dict[str, Any]] = None,
|
|
29
|
+
**kwargs,
|
|
30
|
+
):
|
|
24
31
|
"""
|
|
25
32
|
Initialize Elasticsearch client.
|
|
26
|
-
|
|
33
|
+
|
|
27
34
|
Args:
|
|
28
35
|
host: Override ES host
|
|
29
36
|
secret_name: Override secret name
|
|
37
|
+
credentials: Direct credentials dict (keys: username, password), bypasses Secrets Manager
|
|
30
38
|
**kwargs: Additional ES client configuration
|
|
31
39
|
"""
|
|
32
40
|
# Store host for later use in service client creation
|
|
33
41
|
self._host_override = host
|
|
34
|
-
super().__init__(secret_name=secret_name, **kwargs)
|
|
42
|
+
super().__init__(secret_name=secret_name, credentials=credentials, **kwargs)
|
|
35
43
|
|
|
36
44
|
def _get_default_config_prefix(self) -> str:
|
|
37
45
|
"""Return configuration prefix for Elasticsearch."""
|
|
@@ -41,6 +49,20 @@ class ElasticsearchClient(BaseClient, ServiceHealthMixin):
|
|
|
41
49
|
"""Return default secret name for ES credentials."""
|
|
42
50
|
return "elasticsearch-credentials"
|
|
43
51
|
|
|
52
|
+
def _resolve_credentials_from_env(self) -> Optional[Dict[str, Any]]:
|
|
53
|
+
"""Resolve Elasticsearch credentials from environment variables.
|
|
54
|
+
|
|
55
|
+
Checks for ES_PASSWORD (required to trigger).
|
|
56
|
+
ES_USERNAME defaults to "elastic" if not set.
|
|
57
|
+
"""
|
|
58
|
+
password = os.environ.get("ES_PASSWORD")
|
|
59
|
+
if not password:
|
|
60
|
+
return None
|
|
61
|
+
return {
|
|
62
|
+
"username": os.environ.get("ES_USERNAME", "elastic"),
|
|
63
|
+
"password": password,
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
def _create_service_client(self) -> Elasticsearch:
|
|
45
67
|
"""Create Elasticsearch client with resolved configuration."""
|
|
46
68
|
# Resolve host using utility
|
|
@@ -132,7 +154,7 @@ class ElasticsearchClient(BaseClient, ServiceHealthMixin):
|
|
|
132
154
|
)
|
|
133
155
|
|
|
134
156
|
@handle_client_errors(default_return=0)
|
|
135
|
-
def count(self, index: str, body: Optional[Dict] = None) -> int:
|
|
157
|
+
def count(self, index: str, body: Optional[Dict[str, Any]] = None) -> int:
|
|
136
158
|
"""
|
|
137
159
|
Count documents with error handling.
|
|
138
160
|
|
{nui_lambda_shared_utils-1.2.0 → nui_lambda_shared_utils-1.2.2}/nui_lambda_shared_utils/jwt_auth.py
RENAMED
|
@@ -9,10 +9,12 @@ Install: pip install nui-lambda-shared-utils[jwt]
|
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
11
|
import json
|
|
12
|
+
import re
|
|
12
13
|
import base64
|
|
13
14
|
import time
|
|
14
15
|
import logging
|
|
15
|
-
from typing import TYPE_CHECKING, Optional
|
|
16
|
+
from typing import TYPE_CHECKING, AbstractSet, Any, Dict, Optional, Tuple
|
|
17
|
+
from urllib.parse import unquote
|
|
16
18
|
|
|
17
19
|
from .secrets_helper import get_secret
|
|
18
20
|
|
|
@@ -216,3 +218,60 @@ def require_auth(event: dict, secret_name: Optional[str] = None) -> dict:
|
|
|
216
218
|
return validate_jwt(token, public_key)
|
|
217
219
|
except JWTValidationError as e:
|
|
218
220
|
raise AuthenticationError(f"Authentication failed: {e}") from e
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _normalize_path(path: str) -> str:
|
|
224
|
+
"""Normalize a URL path for safe comparison.
|
|
225
|
+
|
|
226
|
+
URL-decodes, collapses duplicate slashes, ensures a single leading slash,
|
|
227
|
+
and strips any trailing slash (except for root "/").
|
|
228
|
+
"""
|
|
229
|
+
path = unquote(path)
|
|
230
|
+
path = re.sub(r"/+", "/", path)
|
|
231
|
+
return "/" + path.strip("/") if path.strip("/") else "/"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def check_auth(
|
|
235
|
+
event: dict,
|
|
236
|
+
public_paths: AbstractSet[str] = frozenset(),
|
|
237
|
+
secret_name: Optional[str] = None,
|
|
238
|
+
) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
|
|
239
|
+
"""Check JWT authentication on an API Gateway event, skipping public paths.
|
|
240
|
+
|
|
241
|
+
Combines path normalization, public-path bypass, JWT validation,
|
|
242
|
+
and a standard JSON:API 401 error response in one call.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
event: API Gateway Lambda proxy integration event.
|
|
246
|
+
public_paths: Set of normalized paths that skip auth (e.g. {"/health"}).
|
|
247
|
+
secret_name: Optional Secrets Manager secret name for the public key.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
(claims, None) on success — claims is the decoded JWT dict,
|
|
251
|
+
or None if the path is public.
|
|
252
|
+
(None, response) on auth failure — response is a 401 dict
|
|
253
|
+
ready to return from your Lambda handler.
|
|
254
|
+
"""
|
|
255
|
+
raw_path = event.get("path") or event.get("rawPath") or ""
|
|
256
|
+
if _normalize_path(raw_path) in public_paths:
|
|
257
|
+
return None, None
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
claims = require_auth(event, secret_name=secret_name)
|
|
261
|
+
return claims, None
|
|
262
|
+
except AuthenticationError as e:
|
|
263
|
+
log.warning("Authentication failed: %s", e)
|
|
264
|
+
return None, {
|
|
265
|
+
"statusCode": 401,
|
|
266
|
+
"headers": {
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
"Access-Control-Allow-Origin": "*",
|
|
269
|
+
},
|
|
270
|
+
"body": json.dumps({
|
|
271
|
+
"errors": [{
|
|
272
|
+
"status": "401",
|
|
273
|
+
"title": "Unauthorized",
|
|
274
|
+
"detail": "Authentication required",
|
|
275
|
+
}]
|
|
276
|
+
}),
|
|
277
|
+
}
|