nui-python-shared-utils 2.4.1__tar.gz → 2.4.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_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/AGENTS.md +5 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/CHANGELOG.md +28 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/PKG-INFO +2 -2
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/README.md +1 -1
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-004-credential-resolution-precedence.md +20 -5
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/getting-started/configuration.md +18 -2
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/powertools-integration.md +2 -2
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/config.py +3 -1
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/db_client.py +7 -86
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/es_client.py +15 -17
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/powertools_helpers.py +2 -2
- nui_python_shared_utils-2.4.2/nui_shared_utils/secrets_helper.py +334 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_client.py +7 -8
- nui_python_shared_utils-2.4.2/tests/test_aws_utils.py +119 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_db_client.py +97 -23
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_es_client.py +142 -4
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_powertools_helpers.py +17 -7
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_secrets_helper.py +148 -0
- nui_python_shared_utils-2.4.1/nui_shared_utils/secrets_helper.py +0 -208
- nui_python_shared_utils-2.4.1/tests/test_aws_utils.py +0 -325
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/.editorconfig +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/.github/workflows/ci.yml +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/.github/workflows/publish.yml +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/.github/workflows/test.yml +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/.markdownlint-base.yaml +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/.markdownlint-cli2.yaml +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/CLAUDE.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/CONTRIBUTING.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/LICENSE +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/MANIFEST.in +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/README.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-001-structured-logging-field-vocabulary.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-002-keep-shared-utilities-generic.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-003-lazy-load-integrations.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-005-explicit-secrets-manager-region.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-006-snowflake-sql-api-client.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-007-elasticsearch-client-version-range.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-008-stdlib-zoneinfo-over-pytz.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/decisions/README.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/development/testing.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/getting-started/installation.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/getting-started/quickstart.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/cli-tools.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/elasticsearch-integration.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/jwt-authentication.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/lambda-utilities.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/llm-integration.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/log-processing.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/shared-types.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/slack-integration.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/snowflake-integration.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/docs/guides/structured-logging-contract.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/mypy.ini +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_python_shared_utils.egg-info/SOURCES.txt +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/__init__.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/base_client.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/cli.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/cloudwatch_metrics.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/error_handler.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/es_query_builder.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/jwt_auth.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/lambda_helpers.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/llm.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/log_processors.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/logging/__init__.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/logging/binding.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/logging/fields.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/logging/redaction.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_formatter.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_setup/__init__.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/snowflake_client.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/timezone.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/utils.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/pyproject.toml +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/pytest.ini +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/redirect/README.md +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/redirect/pyproject.toml +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/redirect/setup.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/requirements-test.txt +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/scripts/bench_imports.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/setup.cfg +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/setup.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/__init__.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_base_client.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_cloudwatch_metrics.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_config.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_error_handler.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_es_client_compat.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_es_query_builder.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_jwt_auth.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_lambda_helpers.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_lazy_imports.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_llm.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_log_processors.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_logging_binding.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_logging_fields.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_redaction.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_slack_client.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_slack_formatter.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_snowflake_client.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_timezone.py +0 -0
- {nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/tests/test_utils.py +0 -0
|
@@ -194,6 +194,11 @@ Clients resolve credentials in this order (first match wins):
|
|
|
194
194
|
2. **Environment variables** — per-client patterns, skips Secrets Manager
|
|
195
195
|
3. **AWS Secrets Manager** — default behavior
|
|
196
196
|
|
|
197
|
+
The `get_*_credentials` helpers in `secrets_helper.py` follow tiers 2 and 3 through the same
|
|
198
|
+
per-service env resolver and secret normaliser the clients use. Elasticsearch host:
|
|
199
|
+
`host` argument > `ES_HOST`/`ELASTICSEARCH_HOST` > `configure(es_host=...)` > secret `host` >
|
|
200
|
+
`localhost:9200`.
|
|
201
|
+
|
|
197
202
|
### Environment Variables
|
|
198
203
|
|
|
199
204
|
Runtime configuration:
|
|
@@ -10,11 +10,39 @@ that contained them.
|
|
|
10
10
|
|
|
11
11
|
### Changed
|
|
12
12
|
|
|
13
|
+
- **BREAKING**: `get_database_credentials` and `get_slack_credentials` check environment
|
|
14
|
+
variables before Secrets Manager, matching `DatabaseClient`, `SlackClient` and
|
|
15
|
+
`get_elasticsearch_credentials`. With `DB_HOST` and `DB_PASSWORD` set, or `SLACK_BOT_TOKEN`
|
|
16
|
+
set, they return the env values (MySQL defaults for unset `DB_PORT`, `DB_USERNAME`,
|
|
17
|
+
`DB_DATABASE`) and make no AWS call, even when a `secret_name` is passed, and they no longer
|
|
18
|
+
raise for a missing secret name in that case. A caller that reads a second database or
|
|
19
|
+
workspace through `get_database_credentials("<other secret>")` or
|
|
20
|
+
`get_slack_credentials("<other secret>")` while the `DB_*` or `SLACK_BOT_TOKEN` env vars
|
|
21
|
+
describe the first gets the first; read that secret with `get_secret` and map its fields.
|
|
22
|
+
- `ElasticsearchClient` reads the `host` field of its resolved credentials. Host order on the
|
|
23
|
+
client and `get_elasticsearch_credentials` is: `host` argument (client only), `ES_HOST` or
|
|
24
|
+
`ELASTICSEARCH_HOST`, a host set with `configure(es_host=...)`, the credentials' `host`, then
|
|
25
|
+
`localhost:9200`. A client built from a secret that carries a host, with neither `ES_HOST` nor
|
|
26
|
+
`configure(es_host=...)` set, connects to that host instead of `localhost:9200`. The helper
|
|
27
|
+
appends `:9200` to any host that is not a URL and names no port, including one that starts
|
|
28
|
+
with `http` (`httpd-es`), as the client does.
|
|
29
|
+
- `ElasticsearchClient` and `SlackClient` add the normalised fields the helpers return to
|
|
30
|
+
credentials fetched from Secrets Manager (`host`/`username`/`password`, and
|
|
31
|
+
`bot_token`/`webhook_url`), keeping the secret's other keys. A legacy `token` field is used
|
|
32
|
+
when `bot_token` is missing, null or empty; the helper used to keep a null or empty
|
|
33
|
+
`bot_token`. `SlackClient.credentials` from env vars always carries a `webhook_url` key,
|
|
34
|
+
`None` when `SLACK_WEBHOOK_URL` is unset.
|
|
35
|
+
|
|
13
36
|
- **BREAKING**: require Python 3.10 or newer in both the main package and the legacy
|
|
14
37
|
redirect package. Python 3.9 is no longer supported or included in CI test matrices.
|
|
38
|
+
- Powertools docstring and guide examples use generic service and namespace names
|
|
39
|
+
(`order-processor`, `Ecommerce/Orders`) instead of internal repo names. Docs only, no
|
|
40
|
+
behaviour change.
|
|
15
41
|
|
|
16
42
|
### Fixed
|
|
17
43
|
|
|
44
|
+
- `PostgreSQLClient` given a secret with no `port` or `database` fills PostgreSQL defaults
|
|
45
|
+
(`5432`, `postgres`) instead of the MySQL ones (`3306`, `app`).
|
|
18
46
|
- Use canonical IANA names `America/New_York` and `America/Los_Angeles` for US timezone
|
|
19
47
|
constants and option labels, so importing timezone utilities and Slack formatting works
|
|
20
48
|
on systems without legacy timezone links.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nui-python-shared-utils
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: Shared Python utilities for AWS Lambda, CLI tools, and agents with Slack, Elasticsearch, and monitoring integrations
|
|
5
5
|
Home-page: https://github.com/nuimarkets/nui-python-shared-utils
|
|
6
6
|
Author: NUI Markets
|
|
@@ -221,7 +221,7 @@ from nui_shared_utils import get_secret, get_slack_credentials
|
|
|
221
221
|
api_keys = get_secret("my-service/api-keys")
|
|
222
222
|
|
|
223
223
|
# Specialized getters with normalized field names
|
|
224
|
-
slack_creds = get_slack_credentials() #
|
|
224
|
+
slack_creds = get_slack_credentials() # SLACK_BOT_TOKEN if set, else the configured secret
|
|
225
225
|
```
|
|
226
226
|
|
|
227
227
|
**[→ See full secrets management guide](docs/getting-started/configuration.md#aws-secrets-manager)**
|
|
@@ -121,7 +121,7 @@ from nui_shared_utils import get_secret, get_slack_credentials
|
|
|
121
121
|
api_keys = get_secret("my-service/api-keys")
|
|
122
122
|
|
|
123
123
|
# Specialized getters with normalized field names
|
|
124
|
-
slack_creds = get_slack_credentials() #
|
|
124
|
+
slack_creds = get_slack_credentials() # SLACK_BOT_TOKEN if set, else the configured secret
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
**[→ See full secrets management guide](docs/getting-started/configuration.md#aws-secrets-manager)**
|
|
@@ -24,14 +24,19 @@ The function-based helper historically undercut this. `get_elasticsearch_credent
|
|
|
24
24
|
(`secrets_helper.py`) called Secrets Manager first even when `ES_PASSWORD` was set, so CLI and
|
|
25
25
|
local-dev paths paid an avoidable AWS call. Commit `0d3f8e8` (PR #23) added the env-var check
|
|
26
26
|
ahead of the store lookup, aligning the helper with the precedence the clients already enforced.
|
|
27
|
+
The database and Slack helpers kept calling the store first until they were routed through the
|
|
28
|
+
clients' own env resolvers, and the Elasticsearch client ignored a secret's `host` field that the
|
|
29
|
+
helper honoured.
|
|
27
30
|
|
|
28
31
|
## Decision
|
|
29
32
|
|
|
30
33
|
Credentials resolve in a fixed order, first match wins, owned by `BaseClient._resolve_credentials`
|
|
31
34
|
in `base_client.py`:
|
|
32
35
|
|
|
33
|
-
1. **Explicit `credentials` dict** passed to the constructor. Returned as-is;
|
|
34
|
-
consulted. The caller owns these values and the package does not merge or
|
|
36
|
+
1. **Explicit `credentials` dict** passed to the constructor. Returned as-is; no other credential
|
|
37
|
+
source is consulted. The caller owns these values and the package does not merge or
|
|
38
|
+
second-guess them. An Elasticsearch `host` key in the dict is the one exception: it follows
|
|
39
|
+
the host order below.
|
|
35
40
|
2. **Per-client environment variables**, via each client's `_resolve_credentials_from_env` override
|
|
36
41
|
(Elasticsearch reads `ES_PASSWORD`/`ES_PASS` in `es_client.py`; database requires
|
|
37
42
|
`DB_HOST` + `DB_PASSWORD` in `db_client.py`; Slack requires `SLACK_BOT_TOKEN` in
|
|
@@ -42,9 +47,19 @@ in `base_client.py`:
|
|
|
42
47
|
|
|
43
48
|
A later tier fires only when every earlier tier is absent. An AWS call happens only when neither
|
|
44
49
|
an explicit dict nor env vars resolve, so local-dev, CLI, and test paths that supply explicit or
|
|
45
|
-
environment credentials make **zero** Secrets Manager calls. The direct function
|
|
46
|
-
the tail of this ladder for callers that bypass the client classes:
|
|
47
|
-
`
|
|
50
|
+
environment credentials make **zero** Secrets Manager calls. The direct function helpers mirror
|
|
51
|
+
the tail of this ladder for callers that bypass the client classes: `get_elasticsearch_credentials`,
|
|
52
|
+
`get_database_credentials` and `get_slack_credentials` each check the same env vars as their
|
|
53
|
+
client, then the store. Client hook and helper call the same per-service functions in
|
|
54
|
+
`secrets_helper.py` (an env resolver and a secret normaliser per service, with per-engine defaults
|
|
55
|
+
for MySQL and PostgreSQL), so the two paths cannot drift apart.
|
|
56
|
+
|
|
57
|
+
The Elasticsearch host follows its own order on both paths: the client's `host` argument, then
|
|
58
|
+
`ES_HOST` (or `ELASTICSEARCH_HOST`), then the `host` field of the resolved credentials (the
|
|
59
|
+
secret's, or an explicit dict's), then `localhost:9200`. A host set with `configure(es_host=...)`
|
|
60
|
+
ranks between the env var and the credentials' `host`, because a caller that set it chose it; an
|
|
61
|
+
`ES_HOST` the config picked up from the environment at startup does not. To pin the host of one
|
|
62
|
+
client whatever the environment holds, pass its `host` argument.
|
|
48
63
|
|
|
49
64
|
## Alternatives considered
|
|
50
65
|
|
|
@@ -18,6 +18,18 @@ All clients (Slack, Elasticsearch, Database) resolve credentials in this order:
|
|
|
18
18
|
2. **Credential environment variables** - Per-client env vars, skips Secrets Manager
|
|
19
19
|
3. **AWS Secrets Manager** - Default behavior (unchanged)
|
|
20
20
|
|
|
21
|
+
The helper functions `get_elasticsearch_credentials`, `get_database_credentials` and
|
|
22
|
+
`get_slack_credentials` follow the same order from step 2: when the client's env vars are set
|
|
23
|
+
they return those values without calling Secrets Manager. `get_database_credentials` applies the
|
|
24
|
+
MySQL defaults; `PostgreSQLClient` applies the PostgreSQL ones on both the env and the secret path.
|
|
25
|
+
|
|
26
|
+
The Elasticsearch host resolves as: the client's `host` argument, then `ES_HOST` (or
|
|
27
|
+
`ELASTICSEARCH_HOST`), then a host set with `nui.configure(es_host=...)`, then the secret's
|
|
28
|
+
`host` field, then the built-in default
|
|
29
|
+
(`localhost:9200`). The client and `get_elasticsearch_credentials` use the same order. A `host`
|
|
30
|
+
key in an explicit `credentials` dict ranks where the secret's does, so to pin one client's host
|
|
31
|
+
pass `host=` instead.
|
|
32
|
+
|
|
21
33
|
## Environment Variables
|
|
22
34
|
|
|
23
35
|
### Core Configuration
|
|
@@ -163,7 +175,8 @@ The package expects AWS secrets in specific JSON formats:
|
|
|
163
175
|
|
|
164
176
|
**Optional Fields:**
|
|
165
177
|
|
|
166
|
-
- `host` -
|
|
178
|
+
- `host` - Used when the credentials come from this secret (no `ES_PASSWORD` or `ES_PASS`) and
|
|
179
|
+
no `host=` argument, `ES_HOST`, `ELASTICSEARCH_HOST` or `configure(es_host=...)` names one
|
|
167
180
|
- `username` - Defaults to "elastic" if not provided
|
|
168
181
|
|
|
169
182
|
#### Database Credentials
|
|
@@ -178,7 +191,8 @@ The package expects AWS secrets in specific JSON formats:
|
|
|
178
191
|
}
|
|
179
192
|
```
|
|
180
193
|
|
|
181
|
-
|
|
194
|
+
`host`, `username` and `password` are required. A missing `port` or `database` takes the
|
|
195
|
+
engine's default: `3306`/`app` for MySQL, `5432`/`postgres` for `PostgreSQLClient`.
|
|
182
196
|
|
|
183
197
|
#### Slack Credentials
|
|
184
198
|
|
|
@@ -274,6 +288,8 @@ slack = nui.SlackClient(secret_name="custom-slack-secret")
|
|
|
274
288
|
es_creds = nui.get_elasticsearch_credentials("custom-es-secret")
|
|
275
289
|
db_creds = nui.get_database_credentials("custom-db-secret")
|
|
276
290
|
slack_creds = nui.get_slack_credentials("custom-slack-secret")
|
|
291
|
+
# These three return the env values instead, ignoring the secret name, when the
|
|
292
|
+
# client's credential env vars are set (ES_PASSWORD, DB_HOST + DB_PASSWORD, SLACK_BOT_TOKEN).
|
|
277
293
|
|
|
278
294
|
# Generic secret retrieval
|
|
279
295
|
api_keys = nui.get_secret("my-service/api-keys")
|
|
@@ -80,7 +80,7 @@ def get_powertools_logger(
|
|
|
80
80
|
Create AWS Powertools Logger with Elasticsearch-compatible formatting.
|
|
81
81
|
|
|
82
82
|
Args:
|
|
83
|
-
service_name: Service identifier (e.g., "
|
|
83
|
+
service_name: Service identifier (e.g., "order-processor")
|
|
84
84
|
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
85
85
|
local_dev_colors: Enable coloredlogs for local development
|
|
86
86
|
|
|
@@ -177,7 +177,7 @@ def powertools_handler(
|
|
|
177
177
|
|
|
178
178
|
Args:
|
|
179
179
|
service_name: Service identifier for logging and metrics dimensions
|
|
180
|
-
metrics_namespace: CloudWatch namespace (e.g., "
|
|
180
|
+
metrics_namespace: CloudWatch namespace (e.g., "Ecommerce/Orders")
|
|
181
181
|
slack_alert_channel: Slack channel for error alerts (e.g., "#alerts")
|
|
182
182
|
slack_account_names: AWS account ID to display-name mapping for alerts
|
|
183
183
|
slack_account_names_config: YAML file containing account-name defaults
|
|
@@ -34,7 +34,9 @@ class Config:
|
|
|
34
34
|
slack_credentials_secret: AWS secret name for Slack credentials
|
|
35
35
|
aws_region: AWS region for secrets/services
|
|
36
36
|
"""
|
|
37
|
-
# Elasticsearch settings
|
|
37
|
+
# Elasticsearch settings. An es_host passed here outranks a host carried in a
|
|
38
|
+
# credentials secret; the env-derived and built-in values do not.
|
|
39
|
+
self._es_host_explicit = es_host is not None
|
|
38
40
|
if es_host is not None:
|
|
39
41
|
self.es_host = es_host
|
|
40
42
|
else:
|
{nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/db_client.py
RENAMED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Refactored Database client using BaseClient for DRY code patterns.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import os
|
|
6
5
|
import time
|
|
7
6
|
import logging
|
|
8
7
|
from typing import Dict, List, Optional, Any
|
|
@@ -11,7 +10,7 @@ import pymysql
|
|
|
11
10
|
|
|
12
11
|
from .base_client import BaseClient, ServiceHealthMixin
|
|
13
12
|
from .utils import handle_client_errors, safe_close_connection
|
|
14
|
-
from .secrets_helper import get_database_credentials
|
|
13
|
+
from .secrets_helper import get_database_credentials, _db_credentials_from_env, _normalise_db_secret
|
|
15
14
|
|
|
16
15
|
# Optional PostgreSQL support
|
|
17
16
|
try:
|
|
@@ -27,47 +26,6 @@ log = logging.getLogger(__name__)
|
|
|
27
26
|
_connection_pool = {}
|
|
28
27
|
|
|
29
28
|
|
|
30
|
-
def _clean_expired_connections(pool_key: str, pool_recycle: int) -> None:
|
|
31
|
-
"""
|
|
32
|
-
Clean expired connections from the pool.
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
pool_key: Pool identifier
|
|
36
|
-
pool_recycle: Maximum connection age in seconds (0 disables recycling)
|
|
37
|
-
"""
|
|
38
|
-
if not pool_recycle or pool_recycle <= 0:
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
if pool_key not in _connection_pool:
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
current_time = time.time()
|
|
45
|
-
pool_entries = _connection_pool[pool_key]
|
|
46
|
-
|
|
47
|
-
# Find expired connections
|
|
48
|
-
expired_connections = []
|
|
49
|
-
fresh_connections = []
|
|
50
|
-
|
|
51
|
-
for entry in pool_entries:
|
|
52
|
-
age = current_time - entry["timestamp"]
|
|
53
|
-
if age > pool_recycle:
|
|
54
|
-
expired_connections.append(entry["connection"])
|
|
55
|
-
else:
|
|
56
|
-
fresh_connections.append(entry)
|
|
57
|
-
|
|
58
|
-
# Close expired connections
|
|
59
|
-
for conn in expired_connections:
|
|
60
|
-
safe_close_connection(conn)
|
|
61
|
-
|
|
62
|
-
# Update pool with only fresh connections
|
|
63
|
-
_connection_pool[pool_key] = fresh_connections
|
|
64
|
-
|
|
65
|
-
if expired_connections:
|
|
66
|
-
log.debug(
|
|
67
|
-
f"Cleaned {len(expired_connections)} expired connections from pool {pool_key}"
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
29
|
def get_pool_stats() -> Dict[str, Any]:
|
|
72
30
|
"""
|
|
73
31
|
Get current connection pool statistics.
|
|
@@ -158,26 +116,8 @@ class DatabaseClient(BaseClient, ServiceHealthMixin):
|
|
|
158
116
|
return None
|
|
159
117
|
|
|
160
118
|
def _resolve_credentials_from_env(self) -> Optional[Dict[str, Any]]:
|
|
161
|
-
"""Resolve MySQL credentials from
|
|
162
|
-
|
|
163
|
-
Requires both DB_HOST and DB_PASSWORD to trigger.
|
|
164
|
-
"""
|
|
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
|
-
}
|
|
119
|
+
"""Resolve MySQL credentials from DB_* env vars (DB_HOST + DB_PASSWORD required)."""
|
|
120
|
+
return _db_credentials_from_env("mysql")
|
|
181
121
|
|
|
182
122
|
def _fetch_credentials_from_sm(self, secret_name: Optional[str]) -> Dict[str, Any]:
|
|
183
123
|
"""Override to use get_database_credentials for normalized field names."""
|
|
@@ -511,29 +451,11 @@ class PostgreSQLClient(BaseClient, ServiceHealthMixin):
|
|
|
511
451
|
return None
|
|
512
452
|
|
|
513
453
|
def _resolve_credentials_from_env(self) -> Optional[Dict[str, Any]]:
|
|
514
|
-
"""Resolve PostgreSQL credentials from
|
|
515
|
-
|
|
516
|
-
Requires both DB_HOST and DB_PASSWORD to trigger.
|
|
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
|
-
}
|
|
454
|
+
"""Resolve PostgreSQL credentials from DB_* env vars (DB_HOST + DB_PASSWORD required)."""
|
|
455
|
+
return _db_credentials_from_env("postgresql")
|
|
534
456
|
|
|
535
457
|
def _fetch_credentials_from_sm(self, secret_name: Optional[str]) -> Dict[str, Any]:
|
|
536
|
-
"""
|
|
458
|
+
"""Fetch from Secrets Manager, preferring the auth_* keys when requested and present."""
|
|
537
459
|
from .secrets_helper import get_secret
|
|
538
460
|
|
|
539
461
|
resolved_secret_name = self._resolve_secret_name(secret_name)
|
|
@@ -548,8 +470,7 @@ class PostgreSQLClient(BaseClient, ServiceHealthMixin):
|
|
|
548
470
|
"password": raw_creds.get("auth_password"),
|
|
549
471
|
"database": raw_creds.get("auth_database", "auth-service-db"),
|
|
550
472
|
}
|
|
551
|
-
|
|
552
|
-
return get_database_credentials(resolved_secret_name)
|
|
473
|
+
return _normalise_db_secret(raw_creds, "postgresql")
|
|
553
474
|
|
|
554
475
|
@contextmanager
|
|
555
476
|
def get_connection(self, database: Optional[str] = None):
|
{nui_python_shared_utils-2.4.1 → nui_python_shared_utils-2.4.2}/nui_shared_utils/es_client.py
RENAMED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Refactored Elasticsearch client using BaseClient for DRY code patterns.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import os
|
|
6
5
|
import logging
|
|
7
6
|
from datetime import datetime, timedelta
|
|
8
7
|
from typing import Dict, Iterator, List, Optional, Any, Tuple
|
|
@@ -11,7 +10,8 @@ from elasticsearch import Elasticsearch
|
|
|
11
10
|
from elasticsearch.helpers import streaming_bulk as es_streaming_bulk
|
|
12
11
|
|
|
13
12
|
from .base_client import BaseClient, ServiceHealthMixin
|
|
14
|
-
from .utils import handle_client_errors
|
|
13
|
+
from .utils import handle_client_errors
|
|
14
|
+
from .secrets_helper import _es_credentials_from_env, _es_host_with_port, _normalise_es_secret, _resolve_es_host
|
|
15
15
|
|
|
16
16
|
log = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -55,29 +55,27 @@ class ElasticsearchClient(BaseClient, ServiceHealthMixin):
|
|
|
55
55
|
Accepts both conventions: ES_PASSWORD/ES_USERNAME and ES_PASS/ES_USER.
|
|
56
56
|
Password is required to trigger. Username defaults to "elastic".
|
|
57
57
|
"""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
58
|
+
return _es_credentials_from_env()
|
|
59
|
+
|
|
60
|
+
def _fetch_credentials_from_sm(self, secret_name: Optional[str]) -> Dict[str, Any]:
|
|
61
|
+
"""Fetch from Secrets Manager; normalise host, username, password and keep any other keys."""
|
|
62
|
+
raw = super()._fetch_credentials_from_sm(secret_name)
|
|
63
|
+
return {**raw, **_normalise_es_secret(raw)}
|
|
65
64
|
|
|
66
65
|
def _create_service_client(self) -> Elasticsearch:
|
|
67
66
|
"""Create Elasticsearch client with resolved configuration."""
|
|
68
|
-
#
|
|
69
|
-
host
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
# Host precedence: constructor arg > ES_HOST/ELASTICSEARCH_HOST > configure(es_host=...)
|
|
68
|
+
# > credentials "host" (e.g. from the secret) > default. Shared with get_elasticsearch_credentials.
|
|
69
|
+
if self._host_override is not None:
|
|
70
|
+
host = self._host_override
|
|
71
|
+
else:
|
|
72
|
+
host = _resolve_es_host(self.credentials.get("host"))
|
|
73
|
+
host = _es_host_with_port(host)
|
|
74
74
|
|
|
75
75
|
parsed = urlparse(host)
|
|
76
76
|
if parsed.scheme and parsed.netloc: # Valid URL with scheme://netloc
|
|
77
77
|
es_url = host
|
|
78
78
|
else:
|
|
79
|
-
if ":" not in host:
|
|
80
|
-
host = f"{host}:9200"
|
|
81
79
|
scheme = self.client_config.get("scheme", "http")
|
|
82
80
|
es_url = f"{scheme}://{host}"
|
|
83
81
|
|
|
@@ -75,7 +75,7 @@ def get_powertools_logger(
|
|
|
75
75
|
and enforces UTC timezone for consistency with log aggregation systems.
|
|
76
76
|
|
|
77
77
|
Args:
|
|
78
|
-
service_name: Service identifier (e.g., "
|
|
78
|
+
service_name: Service identifier (e.g., "order-processor", "email-ingest")
|
|
79
79
|
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Default: INFO
|
|
80
80
|
local_dev_colors: Enable coloredlogs for local development. Default: True
|
|
81
81
|
|
|
@@ -182,7 +182,7 @@ def powertools_handler(
|
|
|
182
182
|
|
|
183
183
|
Args:
|
|
184
184
|
service_name: Service identifier for logging and metrics dimensions
|
|
185
|
-
metrics_namespace: CloudWatch namespace for metrics (e.g., "
|
|
185
|
+
metrics_namespace: CloudWatch namespace for metrics (e.g., "Ecommerce/Orders").
|
|
186
186
|
If None, metrics publishing is disabled.
|
|
187
187
|
slack_alert_channel: Slack channel for error alerts (e.g., "#alerts", "#errors").
|
|
188
188
|
If None, Slack alerting is disabled.
|