nui-python-shared-utils 2.4__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.
Files changed (106) hide show
  1. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/.github/workflows/ci.yml +1 -1
  2. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/.github/workflows/publish.yml +1 -1
  3. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/.github/workflows/test.yml +1 -1
  4. nui_python_shared_utils-2.4.2/.markdownlint-base.yaml +66 -0
  5. nui_python_shared_utils-2.4.2/.markdownlint-cli2.yaml +17 -0
  6. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/AGENTS.md +5 -0
  7. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/CHANGELOG.md +43 -1
  8. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/CONTRIBUTING.md +1 -1
  9. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/PKG-INFO +4 -5
  10. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/README.md +2 -2
  11. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-004-credential-resolution-precedence.md +20 -5
  12. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-008-stdlib-zoneinfo-over-pytz.md +4 -3
  13. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/getting-started/configuration.md +18 -2
  14. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/getting-started/installation.md +3 -3
  15. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/powertools-integration.md +2 -2
  16. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/shared-types.md +15 -0
  17. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/mypy.ini +1 -1
  18. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_python_shared_utils.egg-info/SOURCES.txt +1 -0
  19. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/config.py +3 -1
  20. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/db_client.py +7 -86
  21. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/es_client.py +15 -17
  22. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/logging/binding.py +0 -1
  23. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/powertools_helpers.py +2 -2
  24. nui_python_shared_utils-2.4.2/nui_shared_utils/secrets_helper.py +334 -0
  25. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_client.py +7 -8
  26. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/timezone.py +4 -4
  27. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/pyproject.toml +3 -4
  28. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/redirect/pyproject.toml +1 -1
  29. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/setup.py +1 -2
  30. nui_python_shared_utils-2.4.2/tests/test_aws_utils.py +119 -0
  31. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_db_client.py +97 -23
  32. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_es_client.py +142 -4
  33. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_powertools_helpers.py +17 -7
  34. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_secrets_helper.py +148 -0
  35. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_slack_client.py +66 -51
  36. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_timezone.py +37 -0
  37. nui_python_shared_utils-2.4/.markdownlint-cli2.yaml +0 -47
  38. nui_python_shared_utils-2.4/nui_shared_utils/secrets_helper.py +0 -208
  39. nui_python_shared_utils-2.4/tests/test_aws_utils.py +0 -325
  40. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/.editorconfig +0 -0
  41. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/CLAUDE.md +0 -0
  42. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/LICENSE +0 -0
  43. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/MANIFEST.in +0 -0
  44. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/README.md +0 -0
  45. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-001-structured-logging-field-vocabulary.md +0 -0
  46. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-002-keep-shared-utilities-generic.md +0 -0
  47. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-003-lazy-load-integrations.md +0 -0
  48. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-005-explicit-secrets-manager-region.md +0 -0
  49. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-006-snowflake-sql-api-client.md +0 -0
  50. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/ADR-007-elasticsearch-client-version-range.md +0 -0
  51. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/decisions/README.md +0 -0
  52. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/development/testing.md +0 -0
  53. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/getting-started/quickstart.md +0 -0
  54. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/cli-tools.md +0 -0
  55. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/elasticsearch-integration.md +0 -0
  56. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/jwt-authentication.md +0 -0
  57. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/lambda-utilities.md +0 -0
  58. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/llm-integration.md +0 -0
  59. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/log-processing.md +0 -0
  60. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/slack-integration.md +0 -0
  61. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/snowflake-integration.md +0 -0
  62. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/docs/guides/structured-logging-contract.md +0 -0
  63. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/__init__.py +0 -0
  64. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/base_client.py +0 -0
  65. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/cli.py +0 -0
  66. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/cloudwatch_metrics.py +0 -0
  67. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/error_handler.py +0 -0
  68. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/es_query_builder.py +0 -0
  69. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/jwt_auth.py +0 -0
  70. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/lambda_helpers.py +0 -0
  71. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/llm.py +0 -0
  72. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/log_processors.py +0 -0
  73. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/logging/__init__.py +0 -0
  74. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/logging/fields.py +0 -0
  75. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/logging/redaction.py +0 -0
  76. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_formatter.py +0 -0
  77. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_setup/__init__.py +0 -0
  78. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
  79. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
  80. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
  81. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/snowflake_client.py +0 -0
  82. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/nui_shared_utils/utils.py +0 -0
  83. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/pytest.ini +0 -0
  84. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/redirect/README.md +0 -0
  85. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/redirect/setup.py +0 -0
  86. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/requirements-test.txt +0 -0
  87. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/scripts/bench_imports.py +0 -0
  88. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/setup.cfg +0 -0
  89. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/__init__.py +0 -0
  90. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_base_client.py +0 -0
  91. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_cloudwatch_metrics.py +0 -0
  92. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_config.py +0 -0
  93. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_error_handler.py +0 -0
  94. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_es_client_compat.py +0 -0
  95. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_es_query_builder.py +0 -0
  96. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_jwt_auth.py +0 -0
  97. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_lambda_helpers.py +0 -0
  98. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_lazy_imports.py +0 -0
  99. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_llm.py +0 -0
  100. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_log_processors.py +0 -0
  101. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_logging_binding.py +0 -0
  102. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_logging_fields.py +0 -0
  103. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_redaction.py +0 -0
  104. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_slack_formatter.py +0 -0
  105. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_snowflake_client.py +0 -0
  106. {nui_python_shared_utils-2.4 → nui_python_shared_utils-2.4.2}/tests/test_utils.py +0 -0
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  strategy:
17
17
  matrix:
18
- python-version: ['3.9', '3.10', '3.11', '3.12']
18
+ python-version: ['3.10', '3.11', '3.12']
19
19
 
20
20
  steps:
21
21
  - uses: actions/checkout@v5
@@ -12,7 +12,7 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  strategy:
14
14
  matrix:
15
- python-version: ['3.9', '3.10', '3.11', '3.12']
15
+ python-version: ['3.10', '3.11', '3.12']
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v5
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- python-version: ['3.9', '3.10', '3.11']
14
+ python-version: ['3.10', '3.11']
15
15
 
16
16
  steps:
17
17
  - uses: actions/checkout@v5
@@ -0,0 +1,66 @@
1
+ # === managed by sync-markdownlint.sh — DO NOT EDIT ===
2
+ # Source of truth: shared tooling config/markdownlint-base.yaml
3
+ # Change rules in the source, then run: sync-markdownlint.sh --update
4
+ # stamp: e9f1a3abeab6
5
+ # === end managed header (sync-markdownlint.sh) ===
6
+ # Shared markdownlint ruleset (FLAT format).
7
+ # See: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
8
+ #
9
+ # SOURCE OF TRUTH. Repos adopt this by vendoring a stamped copy as
10
+ # `.markdownlint-base.yaml` (via sync-markdownlint.sh) and pointing their own
11
+ # `.markdownlint-cli2.yaml` at it with `config: { extends: .markdownlint-base.yaml }`,
12
+ # then adding repo-specific `globs`/`ignores` (and any rare repo-only rule tweaks).
13
+ #
14
+ # This file is FLAT (rules at top level, no `config:` wrapper) because that is
15
+ # the format markdownlint's `extends` requires. Keep it rules-only: file
16
+ # selection (globs) and exclusions (ignores) are per-repo.
17
+
18
+ # Enable all rules by default; disables/customisations follow.
19
+ default: true
20
+
21
+ # MD013: Line length - off. Code blocks, tables, and URLs routinely exceed any
22
+ # sane width; wrapping prose to a column is not a standard we enforce.
23
+ MD013: false
24
+
25
+ # MD033: Inline HTML - allowed. Needed for details/summary, anchors, and the
26
+ # HTML comment markers some docs use (companions metadata, stay markers).
27
+ MD033: false
28
+
29
+ # MD034: Bare URLs - off. Bare URLs in reference docs are intentional.
30
+ MD034: false
31
+
32
+ # MD041: First line in file should be a top-level heading - off. Many docs open
33
+ # with YAML frontmatter or a metadata block, not an H1.
34
+ MD041: false
35
+
36
+ # MD024: Multiple headings with the same content - allowed when not siblings
37
+ # (e.g. a repeated "Example" under different sections).
38
+ MD024:
39
+ siblings_only: true
40
+
41
+ # MD046: Code block style - require fenced (not indented) blocks.
42
+ MD046:
43
+ style: "fenced"
44
+
45
+ # MD026: Trailing punctuation in headings - off. "Note:" / "Warning!" are fine.
46
+ MD026: false
47
+
48
+ # MD029: Ordered list item prefix - off. Docs mix "1. 1. 1." and "1. 2. 3." and
49
+ # both render correctly; not worth churning.
50
+ MD029: false
51
+
52
+ # MD036: Emphasis used instead of a heading - off. Bold-as-label
53
+ # ("**Root cause:**", date stamps) is an accepted convention here.
54
+ MD036: false
55
+
56
+ # MD040: Fenced code language - off. Not every code block has (or needs) a
57
+ # language tag; forcing one produces noise on output samples and snippets.
58
+ MD040: false
59
+
60
+ # MD051: Link fragments should be valid - off. Cross-doc anchor validation is
61
+ # unreliable across the workspace's mixed heading styles and generated pages.
62
+ MD051: false
63
+
64
+ # MD060: Table column style - off. Minor pipe-alignment preference; the corpus
65
+ # has always mixed styles.
66
+ MD060: false
@@ -0,0 +1,17 @@
1
+ # Markdownlint configuration for nui-python-shared-utils
2
+ # Shared rules are vendored by sync-markdownlint.sh.
3
+
4
+ config:
5
+ extends: .markdownlint-base.yaml
6
+
7
+ # File patterns to check
8
+ globs:
9
+ - "**/*.md"
10
+
11
+ # Exclusions
12
+ ignores:
13
+ - "node_modules/**"
14
+ - "vendor/**"
15
+ - ".git/**"
16
+ - "venv/**"
17
+ - ".venv/**"
@@ -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:
@@ -8,6 +8,47 @@ that contained them.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ### Changed
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
+
36
+ - **BREAKING**: require Python 3.10 or newer in both the main package and the legacy
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.
41
+
42
+ ### Fixed
43
+
44
+ - `PostgreSQLClient` given a secret with no `port` or `database` fills PostgreSQL defaults
45
+ (`5432`, `postgres`) instead of the MySQL ones (`3306`, `app`).
46
+ - Use canonical IANA names `America/New_York` and `America/Los_Angeles` for US timezone
47
+ constants and option labels, so importing timezone utilities and Slack formatting works
48
+ on systems without legacy timezone links.
49
+
50
+ ## [2.4] - 2026-10-20
51
+
11
52
  ### Added
12
53
 
13
54
  - Add an opt-in `reraise_exceptions` parameter to `powertools_handler`. When enabled,
@@ -230,7 +271,8 @@ that contained them.
230
271
  - Basic project structure
231
272
  - Python package configuration
232
273
 
233
- [Unreleased]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.3...HEAD
274
+ [Unreleased]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.4...HEAD
275
+ [2.4]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.3...2.4
234
276
  [2.3]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.2...2.3
235
277
  [2.2]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.1...2.2
236
278
  [2.1]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.0...2.1
@@ -6,7 +6,7 @@ We welcome contributions to the NUI Python Shared Utilities project! This docume
6
6
 
7
7
  ### Prerequisites
8
8
 
9
- - Python 3.9 or higher
9
+ - Python 3.10 or higher
10
10
  - Git
11
11
  - Virtual environment tool (venv, virtualenv, etc.)
12
12
 
@@ -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
@@ -36,7 +36,6 @@ Classifier: Intended Audience :: Developers
36
36
  Classifier: License :: OSI Approved :: MIT License
37
37
  Classifier: Operating System :: OS Independent
38
38
  Classifier: Programming Language :: Python :: 3
39
- Classifier: Programming Language :: Python :: 3.9
40
39
  Classifier: Programming Language :: Python :: 3.10
41
40
  Classifier: Programming Language :: Python :: 3.11
42
41
  Classifier: Programming Language :: Python :: 3.12
@@ -45,7 +44,7 @@ Classifier: Topic :: System :: Monitoring
45
44
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
46
45
  Classifier: Environment :: Console
47
46
  Classifier: Framework :: AWS CDK
48
- Requires-Python: >=3.9
47
+ Requires-Python: >=3.10
49
48
  Description-Content-Type: text/markdown
50
49
  License-File: LICENSE
51
50
  Requires-Dist: boto3>=1.20.0
@@ -102,7 +101,7 @@ Dynamic: requires-python
102
101
  # NUI Python Shared Utilities
103
102
 
104
103
  [![PyPI version](https://badge.fury.io/py/nui-python-shared-utils.svg)](https://badge.fury.io/py/nui-python-shared-utils)
105
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
104
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
106
105
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
107
106
 
108
107
  Production-ready shared Python utilities for AWS Lambda functions, CLI tools, and agents. Provides standardized integrations for Slack, Elasticsearch, database, and monitoring. Built and battle-tested on the NUI platform with sensible defaults configurable for any environment.
@@ -222,7 +221,7 @@ from nui_shared_utils import get_secret, get_slack_credentials
222
221
  api_keys = get_secret("my-service/api-keys")
223
222
 
224
223
  # Specialized getters with normalized field names
225
- slack_creds = get_slack_credentials() # Uses configured secret name
224
+ slack_creds = get_slack_credentials() # SLACK_BOT_TOKEN if set, else the configured secret
226
225
  ```
227
226
 
228
227
  **[→ See full secrets management guide](docs/getting-started/configuration.md#aws-secrets-manager)**
@@ -1,7 +1,7 @@
1
1
  # NUI Python Shared Utilities
2
2
 
3
3
  [![PyPI version](https://badge.fury.io/py/nui-python-shared-utils.svg)](https://badge.fury.io/py/nui-python-shared-utils)
4
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  Production-ready shared Python utilities for AWS Lambda functions, CLI tools, and agents. Provides standardized integrations for Slack, Elasticsearch, database, and monitoring. Built and battle-tested on the NUI platform with sensible defaults configurable for any environment.
@@ -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() # Uses configured secret name
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; nothing else is
34
- consulted. The caller owns these values and the package does not merge or second-guess them.
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 helper mirrors
46
- the tail of this ladder for callers that bypass the client classes:
47
- `get_elasticsearch_credentials` checks env vars, then the store.
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
 
@@ -11,7 +11,7 @@ that wanted only the logging contract or the metrics helper still shipped it.
11
11
 
12
12
  The cost is not the code. `pytz` bundles its own copy of the Olson/IANA database (0.86 MB of a
13
13
  1.0 MB installed package, measured on `pytz==2025.2`), and that copy is redundant on
14
- every interpreter this package supports: `requires-python = ">=3.9"`, and `zoneinfo` has been in
14
+ every interpreter this package supports: `requires-python = ">=3.10"`, and `zoneinfo` has been in
15
15
  the standard library since 3.9, reading the system database that Linux, macOS, and the AWS Lambda
16
16
  runtimes all ship.
17
17
 
@@ -82,8 +82,9 @@ a database to everyone:
82
82
 
83
83
  ## Load-bearing premises (supersede triggers)
84
84
 
85
- - **The interpreter floor stays at 3.9+.** `zoneinfo` is stdlib from 3.9. A drop below that
86
- would need `backports.zoneinfo`, at which point the calculus changes.
85
+ - **All supported interpreters include stdlib `zoneinfo`.** The package requires Python 3.10
86
+ or higher. Supporting an interpreter older than 3.9 would need `backports.zoneinfo`, at
87
+ which point the calculus changes.
87
88
  - **Target runtimes ship a system IANA database.** True of Linux, macOS, and the AWS Lambda
88
89
  runtimes today. If the primary deployment target became one that does not, `tzdata` belongs in
89
90
  the base dependencies unconditionally and this ADR's marker split is obsolete.
@@ -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` - Can override ES_HOST if provided
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
- **All fields are required** for database connections.
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")
@@ -4,7 +4,7 @@ This guide covers installation, dependency management, and environment setup for
4
4
 
5
5
  ## Requirements
6
6
 
7
- - Python 3.9 or higher
7
+ - Python 3.10 or higher
8
8
  - AWS credentials configured for Secrets Manager access
9
9
  - Optional: AWS CLI for credential management
10
10
 
@@ -139,7 +139,7 @@ pip install nui-python-shared-utils[all]
139
139
 
140
140
  ```bash
141
141
  # Create environment
142
- conda create -n nui-utils python=3.9
142
+ conda create -n nui-utils python=3.10
143
143
 
144
144
  # Activate environment
145
145
  conda activate nui-utils
@@ -209,7 +209,7 @@ aws lambda publish-layer-version \
209
209
  --layer-name nui-python-shared-utils \
210
210
  --description "NUI Lambda Shared Utilities" \
211
211
  --zip-file fileb://nui-utils-layer.zip \
212
- --compatible-runtimes python3.9 python3.10 python3.11
212
+ --compatible-runtimes python3.10 python3.11
213
213
  ```
214
214
 
215
215
  ### Using in Lambda Function
@@ -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., "nui-tender-analyser")
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., "NUI/TenderAnalyser")
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
@@ -103,6 +103,7 @@ client.get_connection_info() -> Dict
103
103
  ```
104
104
 
105
105
  **Connection Pool Entry Structure:**
106
+
106
107
  ```python
107
108
  {
108
109
  "connection": pymysql.Connection,
@@ -111,6 +112,7 @@ client.get_connection_info() -> Dict
111
112
  ```
112
113
 
113
114
  **Connection Info Response:**
115
+
114
116
  ```python
115
117
  {
116
118
  "host": str,
@@ -169,6 +171,7 @@ client.get_indices_info(pattern="*") -> List[Dict]
169
171
  ```
170
172
 
171
173
  **Service Stats Response:**
174
+
172
175
  ```python
173
176
  {
174
177
  "service": str,
@@ -183,6 +186,7 @@ client.get_indices_info(pattern="*") -> List[Dict]
183
186
  ```
184
187
 
185
188
  **Cluster Info Response:**
189
+
186
190
  ```python
187
191
  {
188
192
  "version": str,
@@ -222,6 +226,7 @@ client.get_bot_info() -> Dict
222
226
  ```
223
227
 
224
228
  **Lambda Context Structure:**
229
+
225
230
  ```python
226
231
  {
227
232
  "function_name": str,
@@ -240,6 +245,7 @@ client.get_bot_info() -> Dict
240
245
  ```
241
246
 
242
247
  **Bot Info Response:**
248
+
243
249
  ```python
244
250
  {
245
251
  "bot_id": str,
@@ -274,6 +280,7 @@ result = matcher.categorize_error(exception)
274
280
  ```
275
281
 
276
282
  **Categorized Error Response:**
283
+
277
284
  ```python
278
285
  {
279
286
  "error": str,
@@ -286,6 +293,7 @@ result = matcher.categorize_error(exception)
286
293
  ```
287
294
 
288
295
  **Built-in Error Categories:**
296
+
289
297
  - `data_format` - JSON parsing errors
290
298
  - `authentication` - Auth failures
291
299
  - `database` - DB timeouts
@@ -310,6 +318,7 @@ summary = aggregator.get_summary()
310
318
  ```
311
319
 
312
320
  **Error Summary Response:**
321
+
313
322
  ```python
314
323
  {
315
324
  "total_errors": int,
@@ -360,6 +369,7 @@ publisher.flush() -> bool
360
369
  ```
361
370
 
362
371
  **Metric Data Structure (internal buffer):**
372
+
363
373
  ```python
364
374
  {
365
375
  "MetricName": str,
@@ -429,6 +439,7 @@ with TimedMetric(publisher, "DatabaseQuery", unit="Milliseconds", dimensions=Non
429
439
  ### Credential Structures
430
440
 
431
441
  **Database Credentials:**
442
+
432
443
  ```python
433
444
  {
434
445
  "host": str,
@@ -440,6 +451,7 @@ with TimedMetric(publisher, "DatabaseQuery", unit="Milliseconds", dimensions=Non
440
451
  ```
441
452
 
442
453
  **Elasticsearch Credentials:**
454
+
443
455
  ```python
444
456
  {
445
457
  "host": str, # With port (e.g., "elastic:9200")
@@ -449,6 +461,7 @@ with TimedMetric(publisher, "DatabaseQuery", unit="Milliseconds", dimensions=Non
449
461
  ```
450
462
 
451
463
  **Slack Credentials:**
464
+
452
465
  ```python
453
466
  {
454
467
  "bot_token": str,
@@ -492,6 +505,7 @@ stats = get_pool_stats()
492
505
  ```
493
506
 
494
507
  **Pool Stats Response:**
508
+
495
509
  ```python
496
510
  {
497
511
  "total_pools": int,
@@ -514,6 +528,7 @@ response = handle_lambda_error(error, context)
514
528
  ```
515
529
 
516
530
  **Lambda Error Response:**
531
+
517
532
  ```python
518
533
  {
519
534
  "statusCode": 500,
@@ -1,5 +1,5 @@
1
1
  [mypy]
2
- python_version = 3.9
2
+ python_version = 3.10
3
3
  warn_return_any = False
4
4
  warn_unused_configs = True
5
5
  ignore_missing_imports = True
@@ -1,4 +1,5 @@
1
1
  .editorconfig
2
+ .markdownlint-base.yaml
2
3
  .markdownlint-cli2.yaml
3
4
  AGENTS.md
4
5
  CHANGELOG.md
@@ -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: