nui-python-shared-utils 2.2__tar.gz → 2.4__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 (102) hide show
  1. nui_python_shared_utils-2.4/CHANGELOG.md +251 -0
  2. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/PKG-INFO +1 -1
  3. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/powertools-integration.md +61 -0
  4. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_python_shared_utils.egg-info/SOURCES.txt +1 -0
  5. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/powertools_helpers.py +38 -2
  6. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_powertools_helpers.py +222 -1
  7. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/.editorconfig +0 -0
  8. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/.github/workflows/ci.yml +0 -0
  9. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/.github/workflows/publish.yml +0 -0
  10. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/.github/workflows/test.yml +0 -0
  11. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/.markdownlint-cli2.yaml +0 -0
  12. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/AGENTS.md +0 -0
  13. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/CLAUDE.md +0 -0
  14. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/CONTRIBUTING.md +0 -0
  15. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/LICENSE +0 -0
  16. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/MANIFEST.in +0 -0
  17. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/README.md +0 -0
  18. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/README.md +0 -0
  19. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/ADR-001-structured-logging-field-vocabulary.md +0 -0
  20. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/ADR-002-keep-shared-utilities-generic.md +0 -0
  21. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/ADR-003-lazy-load-integrations.md +0 -0
  22. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/ADR-004-credential-resolution-precedence.md +0 -0
  23. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/ADR-005-explicit-secrets-manager-region.md +0 -0
  24. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/ADR-006-snowflake-sql-api-client.md +0 -0
  25. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/ADR-007-elasticsearch-client-version-range.md +0 -0
  26. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/ADR-008-stdlib-zoneinfo-over-pytz.md +0 -0
  27. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/decisions/README.md +0 -0
  28. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/development/testing.md +0 -0
  29. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/getting-started/configuration.md +0 -0
  30. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/getting-started/installation.md +0 -0
  31. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/getting-started/quickstart.md +0 -0
  32. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/cli-tools.md +0 -0
  33. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/elasticsearch-integration.md +0 -0
  34. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/jwt-authentication.md +0 -0
  35. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/lambda-utilities.md +0 -0
  36. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/llm-integration.md +0 -0
  37. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/log-processing.md +0 -0
  38. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/shared-types.md +0 -0
  39. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/slack-integration.md +0 -0
  40. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/snowflake-integration.md +0 -0
  41. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/docs/guides/structured-logging-contract.md +0 -0
  42. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/mypy.ini +0 -0
  43. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/__init__.py +0 -0
  44. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/base_client.py +0 -0
  45. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/cli.py +0 -0
  46. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/cloudwatch_metrics.py +0 -0
  47. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/config.py +0 -0
  48. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/db_client.py +0 -0
  49. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/error_handler.py +0 -0
  50. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/es_client.py +0 -0
  51. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/es_query_builder.py +0 -0
  52. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/jwt_auth.py +0 -0
  53. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/lambda_helpers.py +0 -0
  54. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/llm.py +0 -0
  55. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/log_processors.py +0 -0
  56. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/logging/__init__.py +0 -0
  57. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/logging/binding.py +0 -0
  58. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/logging/fields.py +0 -0
  59. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/logging/redaction.py +0 -0
  60. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/secrets_helper.py +0 -0
  61. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/slack_client.py +0 -0
  62. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/slack_formatter.py +0 -0
  63. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/slack_setup/__init__.py +0 -0
  64. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
  65. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
  66. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
  67. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/snowflake_client.py +0 -0
  68. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/timezone.py +0 -0
  69. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/nui_shared_utils/utils.py +0 -0
  70. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/pyproject.toml +0 -0
  71. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/pytest.ini +0 -0
  72. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/redirect/README.md +0 -0
  73. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/redirect/pyproject.toml +0 -0
  74. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/redirect/setup.py +0 -0
  75. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/requirements-test.txt +0 -0
  76. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/scripts/bench_imports.py +0 -0
  77. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/setup.cfg +0 -0
  78. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/setup.py +0 -0
  79. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/__init__.py +0 -0
  80. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_aws_utils.py +0 -0
  81. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_base_client.py +0 -0
  82. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_cloudwatch_metrics.py +0 -0
  83. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_config.py +0 -0
  84. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_db_client.py +0 -0
  85. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_error_handler.py +0 -0
  86. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_es_client.py +0 -0
  87. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_es_client_compat.py +0 -0
  88. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_es_query_builder.py +0 -0
  89. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_jwt_auth.py +0 -0
  90. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_lambda_helpers.py +0 -0
  91. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_lazy_imports.py +0 -0
  92. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_llm.py +0 -0
  93. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_log_processors.py +0 -0
  94. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_logging_binding.py +0 -0
  95. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_logging_fields.py +0 -0
  96. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_redaction.py +0 -0
  97. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_secrets_helper.py +0 -0
  98. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_slack_client.py +0 -0
  99. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_slack_formatter.py +0 -0
  100. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_snowflake_client.py +0 -0
  101. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_timezone.py +0 -0
  102. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.4}/tests/test_utils.py +0 -0
@@ -0,0 +1,251 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and breaking changes are called out explicitly below, including historical minor releases
7
+ that contained them.
8
+
9
+ ## [Unreleased]
10
+
11
+ ### Added
12
+
13
+ - Add an opt-in `reraise_exceptions` parameter to `powertools_handler`. When enabled,
14
+ handler failures are logged and alerted before the original exception is re-raised so
15
+ stream and asynchronous event sources can apply their configured failure handling.
16
+ The default remains the existing 500 response, while SQS events continue to re-raise
17
+ unconditionally.
18
+
19
+ ## [2.3] - 2026-10-12
20
+
21
+ ### Fixed
22
+
23
+ - `powertools_handler` re-raises exceptions for SQS event-source mappings after logging and
24
+ alerting, so failed batches are redelivered and can reach a configured DLQ instead of
25
+ being acknowledged by the wrapper's 500 response.
26
+
27
+ ## [2.2] - 2026-09-14
28
+
29
+ ### Added
30
+
31
+ - Add response-returning Slack write methods (`post_message`, `post_thread_reply`,
32
+ `post_update`, and `post_file`) for callers that need message timestamps, channel IDs,
33
+ or the raw upload response. The existing bool-returning `send_*` methods retain their
34
+ error-swallowing behavior.
35
+ - Add `SentMessage` as the result type for posted messages and thread replies.
36
+
37
+ ### Changed
38
+
39
+ - `send_file` and `post_file` accept bytes and can upload into a thread with `thread_ts`.
40
+
41
+ ## [2.1] - 2026-08-03
42
+
43
+ ### Changed
44
+
45
+ - **BREAKING**: replace `pytz` with the standard-library `zoneinfo` API and remove `pytz`
46
+ from the base dependencies. Code importing timezone constants must replace `.localize()`
47
+ with `tzinfo=...`, passing `fold=1` for repeated wall times where the old `is_dst=False`
48
+ default mattered, and replace `.zone` with `.key`. Declare `pytz` directly if application
49
+ code still imports it. The `[timezone]` extra supplies `tzdata` for minimal images without
50
+ a system IANA database.
51
+ - Allow Elasticsearch client majors 7.17, 8.x, and 9.x through the `[elasticsearch]` and
52
+ `all` extras. See
53
+ [ADR-007](docs/decisions/ADR-007-elasticsearch-client-version-range.md).
54
+
55
+ ### Fixed
56
+
57
+ - Send Elasticsearch search `size` inside a copied request body, avoiding caller-dict
58
+ mutation and duplicate-value errors on client majors 8.x and 9.x.
59
+ - Return a plain `list` from `ElasticsearchClient.get_indices_info()` on every supported
60
+ Elasticsearch client major.
61
+ - Make no-argument `slack_formatter.format_nz_time()` independent of the host timezone.
62
+
63
+ ### Added
64
+
65
+ - Add an Elasticsearch compatibility test matrix for client majors 7.17, 8.x, and 9.x.
66
+ - Add ADRs covering Elasticsearch version support and the move to `zoneinfo`.
67
+
68
+ ## [2.0] - 2026-07-13
69
+
70
+ ### Removed
71
+
72
+ - **BREAKING**: the `nui_lambda_shared_utils` import shim. Import from `nui_shared_utils` instead (`from nui_shared_utils import ...`). The shim forwarded to `nui_shared_utils` with a `DeprecationWarning` since the 1.3.1 import-root rename and was always slated for removal in 2.0.0.
73
+ - The old distribution name still installs: `pip install nui-lambda-shared-utils` continues to resolve via the redirect package (which depends on `nui-python-shared-utils`). Only the old import root is gone.
74
+
75
+ ### Added
76
+
77
+ - Add ADRs for the package's generic scope, lazy integrations, credential precedence,
78
+ explicit Secrets Manager region resolution, and Snowflake SQL API choice.
79
+
80
+ ## [1.6.0] - 2026-07-13
81
+
82
+ ### Added
83
+
84
+ - Add a backend-neutral structured logging field registry, request-scoped Powertools and
85
+ stdlib binders, binding helpers, field validation, and recursive payload redaction.
86
+ - Add the structured logging contract guide and ADR-001.
87
+
88
+ ## [1.5.0] - 2026-07-02
89
+
90
+ ### Added
91
+
92
+ - Add optional `unfurl_links` and `unfurl_media` controls to `SlackClient.send_message`.
93
+ Unset values preserve Slack's existing defaults.
94
+
95
+ ## [1.4.1] - 2026-06-29
96
+
97
+ ### Fixed
98
+
99
+ - Replace the Snowflake extra's direct Git dependency with the published
100
+ `snowflake-sql-api>=0.1.1,<0.2.0` range so PyPI accepts the package metadata.
101
+
102
+ ## [1.4.0] - 2026-06-29
103
+
104
+ ### Added
105
+
106
+ - Add the pure-Python Snowflake SQL API client behind the optional `[snowflake]` extra,
107
+ including sync and async factories, key-pair credential resolution, query logging, and
108
+ offline tests.
109
+ - Add a generic Anthropic client helper behind the optional `[llm]` extra, with API-key and
110
+ Bedrock authentication, forced tool-use calls, and text calls.
111
+ - Add Snowflake and LLM integration guides.
112
+
113
+ ### Changed
114
+
115
+ - **BREAKING**: resolve the Secrets Manager region from the boto3 session, `AWS_REGION`, or
116
+ `AWS_DEFAULT_REGION`, and fail explicitly instead of silently defaulting to
117
+ `ap-southeast-2`.
118
+
119
+ ## [1.3.3] - 2026-05-06
120
+
121
+ ### Performance
122
+
123
+ - Cold-start init for Lambda consumers reduced by ~400ms.
124
+ - `boto3` and `botocore` are no longer imported at module load. They are deferred to first use inside `secrets_helper.get_secret`, `utils.create_aws_client`/`resolve_aws_region`, and `cloudwatch_metrics.MetricsPublisher` (now lazy-constructs its boto3 client on first metric publish).
125
+ - The package `__init__` uses PEP 562 `__getattr__` so submodules (`slack_client`, `es_client`, `db_client`, `jwt_auth`, `powertools_helpers`, etc.) only load when their attributes are first accessed. `import nui_shared_utils` is ~10ms, down from ~700ms+.
126
+ - Public API is unchanged. Optional integrations still resolve to `None` when their backing dependency is not installed (matching the previous behaviour).
127
+
128
+ ### Added
129
+
130
+ - `scripts/bench_imports.py` for measuring `python -X importtime` cumulative cost across common entry points.
131
+ - `tests/test_lazy_imports.py` regression tests (boto3 must not appear in `sys.modules` after a bare package import or after importing `jwt_auth`/`powertools_helpers`).
132
+
133
+ ## [1.3.2] - 2026-04-10
134
+
135
+ ### Fixed
136
+
137
+ - `get_elasticsearch_credentials()` now checks env vars (`ES_PASSWORD`/`ES_PASS`) before Secrets Manager, aligning with `BaseServiceClient` credential precedence
138
+ - `ESClient._resolve_credentials_from_env()` accepts both `ES_PASSWORD`/`ES_USERNAME` and `ES_PASS`/`ES_USER` conventions
139
+
140
+ ## [1.3.1] - 2026-04-05
141
+
142
+ ### Changed
143
+
144
+ - Renamed internal import from `nui_lambda_shared_utils` to `nui_shared_utils`
145
+
146
+ ## [1.3.0] - 2026-04-05
147
+
148
+ ### Changed
149
+
150
+ - Renamed PyPI package from `nui-lambda-shared-utils` to `nui-python-shared-utils`
151
+ - Added explicit license field for twine 6.x compatibility
152
+ - Updated CI publish workflow for new package name and tag pattern
153
+
154
+ ## [1.0.0 - 1.2.3] - 2025-11 to 2026-03
155
+
156
+ ### Added
157
+
158
+ - **Log Processors Module** - `log_processors` for Kinesis/CloudWatch log extraction
159
+ - **AWS Powertools Integration** - `powertools_helpers` module with `get_powertools_logger()` and `powertools_handler()`
160
+ - **JWT Authentication** - RS256 token validation for API Gateway Lambdas
161
+ - **Slack Block Kit improvements** - Enhanced block builder patterns
162
+ - **`slack-channel-setup` CLI tool** for Slack workspace automation
163
+ - `service_name` parameter to `SlackClient` for custom display names
164
+ - `[powertools]` optional dependency group
165
+ - Streaming bulk operations for Elasticsearch
166
+ - Environment-based credential resolution in `BaseServiceClient`
167
+ - Pass-through account names support
168
+
169
+ ### Changed
170
+
171
+ - Rebranded as generic production-ready package (not NUI-specific)
172
+ - Improved Slack Lambda header formatting (cleaner 2-line layout)
173
+ - Documentation reorganization and expansion
174
+
175
+ ### Breaking Changes
176
+
177
+ - Removed `SERVICE_EMOJI` from public API exports
178
+ - Removed `get_entity_stats()` and `get_record_stats()` from `DatabaseClient`
179
+ - `format_currency()` now requires explicit currency parameter
180
+
181
+ ## [0.0.6] - 2025-11-16
182
+
183
+ ### Added
184
+
185
+ - Configuration YAML support for account info
186
+ - RabbitMQ integration utilities
187
+
188
+ ### Changed
189
+
190
+ - Enhanced configuration system
191
+
192
+ ## [0.0.5] - 2025-09-29
193
+
194
+ ### Added
195
+
196
+ - Comprehensive test improvements
197
+ - Better test coverage across modules
198
+
199
+ ### Fixed
200
+
201
+ - Various test fixes and improvements
202
+
203
+ ## [0.0.4] - 2025-09-05
204
+
205
+ ### Changed
206
+
207
+ - Minor improvements and fixes
208
+
209
+ ## [0.0.3] - 2025-09-05
210
+
211
+ ### Changed
212
+
213
+ - Initial improvements
214
+
215
+ ## [0.0.2] - 2025-09-05
216
+
217
+ ### Added
218
+
219
+ - Initial package structure
220
+ - Core client implementations (Slack, Elasticsearch, Database)
221
+ - Metrics publishing utilities
222
+ - Error handling decorators
223
+ - Timezone utilities for NZ timezone handling
224
+
225
+ ## [0.0.1] - 2025-09-05
226
+
227
+ ### Added
228
+
229
+ - Initial release
230
+ - Basic project structure
231
+ - Python package configuration
232
+
233
+ [Unreleased]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.3...HEAD
234
+ [2.3]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.2...2.3
235
+ [2.2]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.1...2.2
236
+ [2.1]: https://github.com/nuimarkets/nui-python-shared-utils/compare/2.0...2.1
237
+ [2.0]: https://github.com/nuimarkets/nui-python-shared-utils/compare/1.6.0...2.0
238
+ [1.6.0]: https://github.com/nuimarkets/nui-python-shared-utils/compare/1.5.0...1.6.0
239
+ [1.5.0]: https://github.com/nuimarkets/nui-python-shared-utils/compare/1.4.1...1.5.0
240
+ [1.4.1]: https://github.com/nuimarkets/nui-python-shared-utils/compare/1.4.0...1.4.1
241
+ [1.4.0]: https://github.com/nuimarkets/nui-python-shared-utils/compare/1.3.3...1.4.0
242
+ [1.3.3]: https://github.com/nuimarkets/nui-python-shared-utils/compare/1.3.2...1.3.3
243
+ [1.3.2]: https://github.com/nuimarkets/nui-python-shared-utils/compare/1.3.1...1.3.2
244
+ [1.3.1]: https://github.com/nuimarkets/nui-python-shared-utils/compare/1.3.0...1.3.1
245
+ [1.3.0]: https://github.com/nuimarkets/nui-python-shared-utils/compare/0.0.6...1.3.0
246
+ [0.0.6]: https://github.com/nuimarkets/nui-python-shared-utils/compare/0.0.5...0.0.6
247
+ [0.0.5]: https://github.com/nuimarkets/nui-python-shared-utils/compare/0.0.4...0.0.5
248
+ [0.0.4]: https://github.com/nuimarkets/nui-python-shared-utils/compare/0.0.3...0.0.4
249
+ [0.0.3]: https://github.com/nuimarkets/nui-python-shared-utils/compare/0.0.2...0.0.3
250
+ [0.0.2]: https://github.com/nuimarkets/nui-python-shared-utils/compare/0.0.1...0.0.2
251
+ [0.0.1]: https://github.com/nuimarkets/nui-python-shared-utils/releases/tag/0.0.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nui-python-shared-utils
3
- Version: 2.2
3
+ Version: 2.4
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
@@ -168,6 +168,9 @@ def powertools_handler(
168
168
  service_name: str,
169
169
  metrics_namespace: Optional[str] = None,
170
170
  slack_alert_channel: Optional[str] = None,
171
+ slack_account_names: Optional[Dict[str, str]] = None,
172
+ slack_account_names_config: Optional[str] = None,
173
+ reraise_exceptions: bool = False,
171
174
  ):
172
175
  """
173
176
  Decorator for Lambda handlers with logging, metrics, and error handling.
@@ -176,9 +179,67 @@ def powertools_handler(
176
179
  service_name: Service identifier for logging and metrics dimensions
177
180
  metrics_namespace: CloudWatch namespace (e.g., "NUI/TenderAnalyser")
178
181
  slack_alert_channel: Slack channel for error alerts (e.g., "#alerts")
182
+ slack_account_names: AWS account ID to display-name mapping for alerts
183
+ slack_account_names_config: YAML file containing account-name defaults
184
+ reraise_exceptions: Re-raise after logging and alerting. Defaults to False.
179
185
  """
180
186
  ```
181
187
 
188
+ ### When the Handler Raises
189
+
190
+ The decorator logs the exception and sends the Slack alert (if configured), then:
191
+
192
+ - **SQS events** (`Records[0].eventSource == "aws:sqs"`, checked before the handler runs):
193
+ re-raises. A returned error dict tells the event source mapping the batch succeeded (with
194
+ `ReportBatchItemFailures`, a response without `batchItemFailures` means every record did),
195
+ so the batch would be deleted. Re-raising leaves it on the queue for redelivery; it moves
196
+ to a DLQ after `maxReceiveCount` only if the queue has a redrive policy. The whole batch
197
+ comes back, records the handler already processed included, so a handler with
198
+ `batchSize` above 1 must be idempotent or catch per record. On a FIFO queue the message
199
+ group waits until the batch succeeds or redrives, and the Slack alert fires on every
200
+ receive. For per-record retries, enable `ReportBatchItemFailures` on the mapping and
201
+ return `{"batchItemFailures": [...]}` from the handler; the decorator passes it through.
202
+ - **Every other event, by default**: returns
203
+ `{"statusCode": 500, "body": "Internal Server Error"}` to preserve existing behavior.
204
+ - **Every other event with `reraise_exceptions=True`**: re-raises after logging the
205
+ exception and sending the configured Slack alert. Use this for Kinesis and DynamoDB
206
+ Streams, where a raised exception prevents the checkpoint from advancing, and for
207
+ asynchronous sources such as SNS, S3, and EventBridge, where Lambda failure handling
208
+ depends on the invocation failing.
209
+
210
+ The option is handler-wide rather than an event-source list because the handler's trigger
211
+ configuration determines whether Lambda failure semantics are appropriate. SQS does not
212
+ require the option and continues to re-raise unconditionally.
213
+
214
+ For synchronous triggers such as API Gateway, enabling this option changes failures from
215
+ the decorator's 500 response into an unhandled Lambda invocation error. Enable it only when
216
+ that caller-visible behavior is intended.
217
+
218
+ ### Re-raise Failures for Stream and Async Sources
219
+
220
+ ```python
221
+ from nui_shared_utils import powertools_handler
222
+
223
+
224
+ @powertools_handler(
225
+ service_name="event-processor",
226
+ slack_alert_channel="#production-alerts",
227
+ reraise_exceptions=True,
228
+ )
229
+ def handler(event, context):
230
+ records = event.get("Records", [])
231
+ for record in records:
232
+ print(record)
233
+ return {"processed": len(records)}
234
+ ```
235
+
236
+ Logging and Slack alerting run before the original exception is re-raised. Configure the
237
+ trigger's retry, bisect, failure destination, or dead-letter behavior separately in AWS.
238
+ Every retry invokes the decorator again, so a configured Slack alert can be sent once per
239
+ attempt. For Kinesis and DynamoDB Streams, maximum retry attempts and maximum record age
240
+ bound retries. Bisect-on-error isolates a failing record by adding invocations, which do not
241
+ consume the retry quota and can produce additional alerts.
242
+
182
243
  ### Basic Usage (Logging Only)
183
244
 
184
245
  ```python
@@ -1,6 +1,7 @@
1
1
  .editorconfig
2
2
  .markdownlint-cli2.yaml
3
3
  AGENTS.md
4
+ CHANGELOG.md
4
5
  CLAUDE.md
5
6
  CONTRIBUTING.md
6
7
  LICENSE
@@ -145,12 +145,24 @@ def get_powertools_logger(
145
145
  return powertools_logger
146
146
 
147
147
 
148
+ def _is_sqs_event(event: Any) -> bool:
149
+ """True when ``event`` is a batch delivered by an SQS event source mapping."""
150
+ if not isinstance(event, dict):
151
+ return False
152
+ records = event.get("Records")
153
+ if not isinstance(records, list) or not records:
154
+ return False
155
+ first = records[0]
156
+ return isinstance(first, dict) and first.get("eventSource") == "aws:sqs"
157
+
158
+
148
159
  def powertools_handler(
149
160
  service_name: str,
150
161
  metrics_namespace: Optional[str] = None,
151
162
  slack_alert_channel: Optional[str] = None,
152
163
  slack_account_names: Optional[Dict[str, str]] = None,
153
164
  slack_account_names_config: Optional[str] = None,
165
+ reraise_exceptions: bool = False,
154
166
  ):
155
167
  """
156
168
  Decorator for Lambda handlers with logging, metrics, and error handling.
@@ -164,6 +176,7 @@ def powertools_handler(
164
176
  - Optional metrics.log_metrics integration (if metrics_namespace provided)
165
177
  - Structured exception logging with traceback
166
178
  - Optional Slack alerts on failures (if slack_alert_channel provided)
179
+ - Optional exception re-raising for event sources that use Lambda failure semantics
167
180
  - Graceful degradation if Slack client unavailable
168
181
  - Proper Lambda error response formatting
169
182
 
@@ -179,6 +192,11 @@ def powertools_handler(
179
192
  Values loaded from this file are used as defaults, but any keys
180
193
  provided in ``slack_account_names`` take precedence and override
181
194
  the YAML-loaded values.
195
+ reraise_exceptions: Re-raise handler exceptions after logging and Slack
196
+ alerting. Enable this for event sources such as Kinesis, DynamoDB
197
+ Streams, SNS, S3, and EventBridge that rely on a raised exception to
198
+ record a failed invocation and apply their retry or failure handling.
199
+ Defaults to False to preserve the existing 500 response behavior.
182
200
 
183
201
  Returns:
184
202
  Decorator function for Lambda handlers
@@ -199,8 +217,18 @@ def powertools_handler(
199
217
  ... return {"statusCode": 200}
200
218
 
201
219
  Note:
202
- The decorated handler must return a dict with statusCode and optional body.
203
- On exception, returns: {"statusCode": 500, "body": "Internal Server Error"}
220
+ The handler's return value passes through unchanged: a ``statusCode`` dict
221
+ for API Gateway, ``{"batchItemFailures": [...]}`` for an SQS mapping with
222
+ ``ReportBatchItemFailures``.
223
+
224
+ On exception, returns ``{"statusCode": 500, "body": "Internal Server Error"}``
225
+ by default. Set ``reraise_exceptions=True`` to re-raise after logging and
226
+ alerting when the event source relies on Lambda failure semantics. SQS events
227
+ are always re-raised, regardless of this option: a returned value tells the
228
+ SQS mapping the batch succeeded (with ``ReportBatchItemFailures``, a response
229
+ without ``batchItemFailures`` means every record did), so the 500 dict would
230
+ delete the batch. Re-raising leaves it on the queue for redelivery, and in the
231
+ DLQ after ``maxReceiveCount`` when the queue has a redrive policy.
204
232
  """
205
233
 
206
234
  def decorator(func: Callable) -> Callable:
@@ -231,6 +259,9 @@ def powertools_handler(
231
259
  if slack_client:
232
260
  slack_client.set_handler_context(context)
233
261
 
262
+ # Classify before the handler runs: it may consume or mutate event["Records"].
263
+ is_sqs = _is_sqs_event(event)
264
+
234
265
  try:
235
266
  # Apply logger context injection
236
267
  # Note: inject_lambda_context is added dynamically to logging.Logger (line 95)
@@ -274,6 +305,11 @@ def powertools_handler(
274
305
  except Exception as slack_error:
275
306
  logger.warning("Failed to send Slack alert: %s", slack_error)
276
307
 
308
+ # Re-raise for SQS so the batch stays on the queue, or when the caller
309
+ # opts into Lambda failure semantics; see the Note above.
310
+ if is_sqs or reraise_exceptions:
311
+ raise
312
+
277
313
  # Return proper Lambda error response
278
314
  return {
279
315
  "statusCode": 500,
@@ -7,7 +7,6 @@ from unittest.mock import MagicMock, patch
7
7
 
8
8
  import pytest
9
9
 
10
-
11
10
  # Test markers
12
11
  pytestmark = pytest.mark.unit
13
12
 
@@ -334,6 +333,228 @@ class TestPowertoolsHandler:
334
333
  assert result["body"] == "Internal Server Error"
335
334
 
336
335
 
336
+ def _sqs_event(*message_ids):
337
+ return {"Records": [{"messageId": mid, "eventSource": "aws:sqs", "body": "{}"} for mid in message_ids]}
338
+
339
+
340
+ def _kinesis_event():
341
+ return {
342
+ "Records": [
343
+ {
344
+ "eventID": "shardId-000000000000:1",
345
+ "eventSource": "aws:kinesis",
346
+ "kinesis": {"sequenceNumber": "1", "data": "e30="},
347
+ }
348
+ ]
349
+ }
350
+
351
+
352
+ class TestPowertoolsHandlerSqs:
353
+ """An SQS batch must stay on the queue when the handler raises.
354
+
355
+ Any value returned to an SQS event source mapping counts as success, so
356
+ returning the 500 dict deletes the batch: no redelivery, no DLQ.
357
+ """
358
+
359
+ def test_sqs_exception_is_reraised(self, monkeypatch):
360
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
361
+
362
+ from nui_shared_utils.powertools_helpers import powertools_handler
363
+
364
+ error = ConnectionError("Error reading SSH protocol banner")
365
+
366
+ @powertools_handler(service_name="test-service")
367
+ def handler(event, context):
368
+ raise error
369
+
370
+ with pytest.raises(ConnectionError) as excinfo:
371
+ handler(_sqs_event("m-1", "m-2"), MagicMock())
372
+
373
+ assert excinfo.value is error
374
+
375
+ def test_sqs_exception_alerts_slack_before_reraising(self, monkeypatch):
376
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
377
+
378
+ mock_slack_client = MagicMock()
379
+
380
+ with patch("nui_shared_utils.powertools_helpers.SLACK_CLIENT_AVAILABLE", True):
381
+ with patch("nui_shared_utils.powertools_helpers.SlackClient", return_value=mock_slack_client):
382
+ from nui_shared_utils.powertools_helpers import powertools_handler
383
+
384
+ @powertools_handler(service_name="test-service", slack_alert_channel="#errors")
385
+ def handler(event, context):
386
+ raise ValueError("Test error")
387
+
388
+ with pytest.raises(ValueError):
389
+ handler(_sqs_event("m-1"), MagicMock())
390
+
391
+ mock_slack_client.send_message.assert_called_once()
392
+
393
+ def test_sqs_reraises_original_exception_when_slack_fails(self, monkeypatch):
394
+ """A failed alert must not replace the handler's exception or swallow it."""
395
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
396
+
397
+ mock_slack_client = MagicMock()
398
+ mock_slack_client.send_message.side_effect = RuntimeError("Slack API error")
399
+
400
+ with patch("nui_shared_utils.powertools_helpers.SLACK_CLIENT_AVAILABLE", True):
401
+ with patch("nui_shared_utils.powertools_helpers.SlackClient", return_value=mock_slack_client):
402
+ from nui_shared_utils.powertools_helpers import powertools_handler
403
+
404
+ @powertools_handler(service_name="test-service", slack_alert_channel="#errors")
405
+ def handler(event, context):
406
+ raise ValueError("Test error")
407
+
408
+ with pytest.raises(ValueError, match="Test error"):
409
+ handler(_sqs_event("m-1"), MagicMock())
410
+
411
+ def test_sqs_detected_before_the_handler_consumes_records(self, monkeypatch):
412
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
413
+
414
+ from nui_shared_utils.powertools_helpers import powertools_handler
415
+
416
+ @powertools_handler(service_name="test-service")
417
+ def handler(event, context):
418
+ event.pop("Records")
419
+ raise ValueError("Test error")
420
+
421
+ with pytest.raises(ValueError):
422
+ handler(_sqs_event("m-1"), MagicMock())
423
+
424
+ def test_sqs_reraises_after_metrics_flush(self, monkeypatch):
425
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
426
+
427
+ flushed = []
428
+
429
+ def log_metrics(func):
430
+ def inner(event, context):
431
+ try:
432
+ return func(event, context)
433
+ finally:
434
+ flushed.append(True)
435
+
436
+ return inner
437
+
438
+ mock_metrics = MagicMock()
439
+ mock_metrics.log_metrics = log_metrics
440
+
441
+ with patch("nui_shared_utils.powertools_helpers.POWERTOOLS_AVAILABLE", True):
442
+ with patch("nui_shared_utils.powertools_helpers.Metrics", return_value=mock_metrics):
443
+ from nui_shared_utils.powertools_helpers import powertools_handler
444
+
445
+ @powertools_handler(service_name="test-service", metrics_namespace="Test/Metrics")
446
+ def handler(event, context):
447
+ raise ValueError("Test error")
448
+
449
+ with pytest.raises(ValueError):
450
+ handler(_sqs_event("m-1"), MagicMock())
451
+
452
+ assert flushed == [True]
453
+
454
+ def test_sqs_success_returns_handler_result(self, monkeypatch):
455
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
456
+
457
+ from nui_shared_utils.powertools_helpers import powertools_handler
458
+
459
+ @powertools_handler(service_name="test-service")
460
+ def handler(event, context):
461
+ return {"batchItemFailures": [{"itemIdentifier": "m-2"}]}
462
+
463
+ result = handler(_sqs_event("m-1", "m-2"), MagicMock())
464
+
465
+ assert result == {"batchItemFailures": [{"itemIdentifier": "m-2"}]}
466
+
467
+ @pytest.mark.parametrize(
468
+ "event",
469
+ [
470
+ {"Records": [{"EventSource": "aws:sns", "Sns": {}}]},
471
+ {"Records": [{"eventSource": "aws:kinesis", "kinesis": {}}]},
472
+ {"Records": [{"eventSource": "aws:dynamodb", "dynamodb": {}}]},
473
+ {"Records": [{"eventSource": "aws:s3", "s3": {}}]},
474
+ {"Records": []},
475
+ {"Records": "not-a-list"},
476
+ {"source": "aws.events", "detail-type": "Scheduled Event"},
477
+ ["not", "a", "dict"],
478
+ ],
479
+ ids=["sns", "kinesis", "dynamodb", "s3", "empty-records", "records-not-list", "scheduled", "non-dict"],
480
+ )
481
+ def test_non_sqs_events_keep_returning_500(self, monkeypatch, event):
482
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
483
+
484
+ from nui_shared_utils.powertools_helpers import powertools_handler
485
+
486
+ @powertools_handler(service_name="test-service")
487
+ def handler(event, context):
488
+ raise ValueError("Test error")
489
+
490
+ result = handler(event, MagicMock())
491
+
492
+ assert result == {"statusCode": 500, "body": "Internal Server Error"}
493
+
494
+
495
+ class TestPowertoolsHandlerReraise:
496
+ def test_kinesis_exception_is_reraised_when_enabled(self, monkeypatch):
497
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
498
+
499
+ from nui_shared_utils.powertools_helpers import powertools_handler
500
+
501
+ error = RuntimeError("Kinesis record failed")
502
+
503
+ @powertools_handler(service_name="test-service", reraise_exceptions=True)
504
+ def handler(event, context):
505
+ raise error
506
+
507
+ with pytest.raises(RuntimeError) as excinfo:
508
+ handler(_kinesis_event(), MagicMock())
509
+
510
+ assert excinfo.value is error
511
+
512
+ def test_kinesis_exception_keeps_500_by_default(self, monkeypatch):
513
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
514
+
515
+ from nui_shared_utils.powertools_helpers import powertools_handler
516
+
517
+ @powertools_handler(service_name="test-service", reraise_exceptions=False)
518
+ def handler(event, context):
519
+ raise RuntimeError("Kinesis record failed")
520
+
521
+ result = handler(_kinesis_event(), MagicMock())
522
+
523
+ assert result == {"statusCode": 500, "body": "Internal Server Error"}
524
+
525
+ def test_eventbridge_exception_alerts_before_reraising(self, monkeypatch):
526
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
527
+
528
+ from nui_shared_utils.powertools_helpers import powertools_handler
529
+
530
+ calls = []
531
+ mock_logger = MagicMock()
532
+ mock_logger.inject_lambda_context.side_effect = lambda func: func
533
+ mock_logger.exception.side_effect = lambda *args, **kwargs: calls.append("log")
534
+ mock_slack_client = MagicMock()
535
+ mock_slack_client.send_message.side_effect = lambda **kwargs: calls.append("slack")
536
+
537
+ with patch("nui_shared_utils.powertools_helpers.get_powertools_logger", return_value=mock_logger):
538
+ with patch("nui_shared_utils.powertools_helpers.SLACK_CLIENT_AVAILABLE", True):
539
+ with patch("nui_shared_utils.powertools_helpers.SlackClient", return_value=mock_slack_client):
540
+
541
+ @powertools_handler(
542
+ service_name="test-service",
543
+ slack_alert_channel="#errors",
544
+ reraise_exceptions=True,
545
+ )
546
+ def handler(event, context):
547
+ raise ValueError("EventBridge event failed")
548
+
549
+ with pytest.raises(ValueError, match="EventBridge event failed"):
550
+ handler(
551
+ {"source": "example.orders", "detail-type": "Order Created", "detail": {}},
552
+ MagicMock(),
553
+ )
554
+
555
+ assert calls == ["log", "slack"]
556
+
557
+
337
558
  class TestOptionalImports:
338
559
  """Tests for graceful degradation of optional dependencies"""
339
560