nui-python-shared-utils 2.2__tar.gz → 2.3__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 (101) hide show
  1. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/PKG-INFO +1 -1
  2. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/powertools-integration.md +16 -0
  3. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/powertools_helpers.py +29 -2
  4. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_powertools_helpers.py +147 -0
  5. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/.editorconfig +0 -0
  6. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/.github/workflows/ci.yml +0 -0
  7. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/.github/workflows/publish.yml +0 -0
  8. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/.github/workflows/test.yml +0 -0
  9. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/.markdownlint-cli2.yaml +0 -0
  10. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/AGENTS.md +0 -0
  11. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/CLAUDE.md +0 -0
  12. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/CONTRIBUTING.md +0 -0
  13. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/LICENSE +0 -0
  14. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/MANIFEST.in +0 -0
  15. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/README.md +0 -0
  16. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/README.md +0 -0
  17. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/ADR-001-structured-logging-field-vocabulary.md +0 -0
  18. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/ADR-002-keep-shared-utilities-generic.md +0 -0
  19. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/ADR-003-lazy-load-integrations.md +0 -0
  20. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/ADR-004-credential-resolution-precedence.md +0 -0
  21. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/ADR-005-explicit-secrets-manager-region.md +0 -0
  22. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/ADR-006-snowflake-sql-api-client.md +0 -0
  23. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/ADR-007-elasticsearch-client-version-range.md +0 -0
  24. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/ADR-008-stdlib-zoneinfo-over-pytz.md +0 -0
  25. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/decisions/README.md +0 -0
  26. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/development/testing.md +0 -0
  27. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/getting-started/configuration.md +0 -0
  28. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/getting-started/installation.md +0 -0
  29. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/getting-started/quickstart.md +0 -0
  30. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/cli-tools.md +0 -0
  31. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/elasticsearch-integration.md +0 -0
  32. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/jwt-authentication.md +0 -0
  33. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/lambda-utilities.md +0 -0
  34. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/llm-integration.md +0 -0
  35. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/log-processing.md +0 -0
  36. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/shared-types.md +0 -0
  37. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/slack-integration.md +0 -0
  38. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/snowflake-integration.md +0 -0
  39. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/docs/guides/structured-logging-contract.md +0 -0
  40. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/mypy.ini +0 -0
  41. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_python_shared_utils.egg-info/SOURCES.txt +0 -0
  42. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/__init__.py +0 -0
  43. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/base_client.py +0 -0
  44. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/cli.py +0 -0
  45. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/cloudwatch_metrics.py +0 -0
  46. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/config.py +0 -0
  47. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/db_client.py +0 -0
  48. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/error_handler.py +0 -0
  49. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/es_client.py +0 -0
  50. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/es_query_builder.py +0 -0
  51. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/jwt_auth.py +0 -0
  52. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/lambda_helpers.py +0 -0
  53. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/llm.py +0 -0
  54. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/log_processors.py +0 -0
  55. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/logging/__init__.py +0 -0
  56. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/logging/binding.py +0 -0
  57. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/logging/fields.py +0 -0
  58. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/logging/redaction.py +0 -0
  59. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/secrets_helper.py +0 -0
  60. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/slack_client.py +0 -0
  61. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/slack_formatter.py +0 -0
  62. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/slack_setup/__init__.py +0 -0
  63. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
  64. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
  65. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
  66. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/snowflake_client.py +0 -0
  67. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/timezone.py +0 -0
  68. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/nui_shared_utils/utils.py +0 -0
  69. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/pyproject.toml +0 -0
  70. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/pytest.ini +0 -0
  71. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/redirect/README.md +0 -0
  72. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/redirect/pyproject.toml +0 -0
  73. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/redirect/setup.py +0 -0
  74. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/requirements-test.txt +0 -0
  75. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/scripts/bench_imports.py +0 -0
  76. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/setup.cfg +0 -0
  77. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/setup.py +0 -0
  78. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/__init__.py +0 -0
  79. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_aws_utils.py +0 -0
  80. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_base_client.py +0 -0
  81. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_cloudwatch_metrics.py +0 -0
  82. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_config.py +0 -0
  83. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_db_client.py +0 -0
  84. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_error_handler.py +0 -0
  85. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_es_client.py +0 -0
  86. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_es_client_compat.py +0 -0
  87. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_es_query_builder.py +0 -0
  88. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_jwt_auth.py +0 -0
  89. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_lambda_helpers.py +0 -0
  90. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_lazy_imports.py +0 -0
  91. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_llm.py +0 -0
  92. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_log_processors.py +0 -0
  93. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_logging_binding.py +0 -0
  94. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_logging_fields.py +0 -0
  95. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_redaction.py +0 -0
  96. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_secrets_helper.py +0 -0
  97. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_slack_client.py +0 -0
  98. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_slack_formatter.py +0 -0
  99. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_snowflake_client.py +0 -0
  100. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_timezone.py +0 -0
  101. {nui_python_shared_utils-2.2 → nui_python_shared_utils-2.3}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nui-python-shared-utils
3
- Version: 2.2
3
+ Version: 2.3
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
@@ -179,6 +179,22 @@ def powertools_handler(
179
179
  """
180
180
  ```
181
181
 
182
+ ### When the Handler Raises
183
+
184
+ The decorator logs the exception and sends the Slack alert (if configured), then:
185
+
186
+ - **SQS events** (`Records[0].eventSource == "aws:sqs"`, checked before the handler runs):
187
+ re-raises. A returned error dict tells the event source mapping the batch succeeded (with
188
+ `ReportBatchItemFailures`, a response without `batchItemFailures` means every record did),
189
+ so the batch would be deleted. Re-raising leaves it on the queue for redelivery; it moves
190
+ to a DLQ after `maxReceiveCount` only if the queue has a redrive policy. The whole batch
191
+ comes back, records the handler already processed included, so a handler with
192
+ `batchSize` above 1 must be idempotent or catch per record. On a FIFO queue the message
193
+ group waits until the batch succeeds or redrives, and the Slack alert fires on every
194
+ receive. For per-record retries, enable `ReportBatchItemFailures` on the mapping and
195
+ return `{"batchItemFailures": [...]}` from the handler; the decorator passes it through.
196
+ - **Every other event**: returns `{"statusCode": 500, "body": "Internal Server Error"}`.
197
+
182
198
  ### Basic Usage (Logging Only)
183
199
 
184
200
  ```python
@@ -145,6 +145,17 @@ 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,
@@ -199,8 +210,17 @@ def powertools_handler(
199
210
  ... return {"statusCode": 200}
200
211
 
201
212
  Note:
202
- The decorated handler must return a dict with statusCode and optional body.
203
- On exception, returns: {"statusCode": 500, "body": "Internal Server Error"}
213
+ The handler's return value passes through unchanged: a ``statusCode`` dict
214
+ for API Gateway, ``{"batchItemFailures": [...]}`` for an SQS mapping with
215
+ ``ReportBatchItemFailures``.
216
+
217
+ On exception, returns ``{"statusCode": 500, "body": "Internal Server Error"}``,
218
+ except for SQS events, where the exception is re-raised after logging and
219
+ alerting. A returned value tells the SQS mapping the batch succeeded (with
220
+ ``ReportBatchItemFailures``, a response without ``batchItemFailures`` means
221
+ every record did), so the 500 dict would delete the batch. Re-raising leaves
222
+ it on the queue for redelivery, and in the DLQ after ``maxReceiveCount``
223
+ when the queue has a redrive policy.
204
224
  """
205
225
 
206
226
  def decorator(func: Callable) -> Callable:
@@ -231,6 +251,9 @@ def powertools_handler(
231
251
  if slack_client:
232
252
  slack_client.set_handler_context(context)
233
253
 
254
+ # Classify before the handler runs: it may consume or mutate event["Records"].
255
+ is_sqs = _is_sqs_event(event)
256
+
234
257
  try:
235
258
  # Apply logger context injection
236
259
  # Note: inject_lambda_context is added dynamically to logging.Logger (line 95)
@@ -274,6 +297,10 @@ def powertools_handler(
274
297
  except Exception as slack_error:
275
298
  logger.warning("Failed to send Slack alert: %s", slack_error)
276
299
 
300
+ # Re-raise for SQS so the batch stays on the queue; see the Note above.
301
+ if is_sqs:
302
+ raise
303
+
277
304
  # Return proper Lambda error response
278
305
  return {
279
306
  "statusCode": 500,
@@ -334,6 +334,153 @@ class TestPowertoolsHandler:
334
334
  assert result["body"] == "Internal Server Error"
335
335
 
336
336
 
337
+ def _sqs_event(*message_ids):
338
+ return {"Records": [{"messageId": mid, "eventSource": "aws:sqs", "body": "{}"} for mid in message_ids]}
339
+
340
+
341
+ class TestPowertoolsHandlerSqs:
342
+ """An SQS batch must stay on the queue when the handler raises.
343
+
344
+ Any value returned to an SQS event source mapping counts as success, so
345
+ returning the 500 dict deletes the batch: no redelivery, no DLQ.
346
+ """
347
+
348
+ def test_sqs_exception_is_reraised(self, monkeypatch):
349
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
350
+
351
+ from nui_shared_utils.powertools_helpers import powertools_handler
352
+
353
+ error = ConnectionError("Error reading SSH protocol banner")
354
+
355
+ @powertools_handler(service_name="test-service")
356
+ def handler(event, context):
357
+ raise error
358
+
359
+ with pytest.raises(ConnectionError) as excinfo:
360
+ handler(_sqs_event("m-1", "m-2"), MagicMock())
361
+
362
+ assert excinfo.value is error
363
+
364
+ def test_sqs_exception_alerts_slack_before_reraising(self, monkeypatch):
365
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
366
+
367
+ mock_slack_client = MagicMock()
368
+
369
+ with patch("nui_shared_utils.powertools_helpers.SLACK_CLIENT_AVAILABLE", True):
370
+ with patch("nui_shared_utils.powertools_helpers.SlackClient", return_value=mock_slack_client):
371
+ from nui_shared_utils.powertools_helpers import powertools_handler
372
+
373
+ @powertools_handler(service_name="test-service", slack_alert_channel="#errors")
374
+ def handler(event, context):
375
+ raise ValueError("Test error")
376
+
377
+ with pytest.raises(ValueError):
378
+ handler(_sqs_event("m-1"), MagicMock())
379
+
380
+ mock_slack_client.send_message.assert_called_once()
381
+
382
+ def test_sqs_reraises_original_exception_when_slack_fails(self, monkeypatch):
383
+ """A failed alert must not replace the handler's exception or swallow it."""
384
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
385
+
386
+ mock_slack_client = MagicMock()
387
+ mock_slack_client.send_message.side_effect = RuntimeError("Slack API error")
388
+
389
+ with patch("nui_shared_utils.powertools_helpers.SLACK_CLIENT_AVAILABLE", True):
390
+ with patch("nui_shared_utils.powertools_helpers.SlackClient", return_value=mock_slack_client):
391
+ from nui_shared_utils.powertools_helpers import powertools_handler
392
+
393
+ @powertools_handler(service_name="test-service", slack_alert_channel="#errors")
394
+ def handler(event, context):
395
+ raise ValueError("Test error")
396
+
397
+ with pytest.raises(ValueError, match="Test error"):
398
+ handler(_sqs_event("m-1"), MagicMock())
399
+
400
+ def test_sqs_detected_before_the_handler_consumes_records(self, monkeypatch):
401
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
402
+
403
+ from nui_shared_utils.powertools_helpers import powertools_handler
404
+
405
+ @powertools_handler(service_name="test-service")
406
+ def handler(event, context):
407
+ event.pop("Records")
408
+ raise ValueError("Test error")
409
+
410
+ with pytest.raises(ValueError):
411
+ handler(_sqs_event("m-1"), MagicMock())
412
+
413
+ def test_sqs_reraises_after_metrics_flush(self, monkeypatch):
414
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
415
+
416
+ flushed = []
417
+
418
+ def log_metrics(func):
419
+ def inner(event, context):
420
+ try:
421
+ return func(event, context)
422
+ finally:
423
+ flushed.append(True)
424
+
425
+ return inner
426
+
427
+ mock_metrics = MagicMock()
428
+ mock_metrics.log_metrics = log_metrics
429
+
430
+ with patch("nui_shared_utils.powertools_helpers.POWERTOOLS_AVAILABLE", True):
431
+ with patch("nui_shared_utils.powertools_helpers.Metrics", return_value=mock_metrics):
432
+ from nui_shared_utils.powertools_helpers import powertools_handler
433
+
434
+ @powertools_handler(service_name="test-service", metrics_namespace="Test/Metrics")
435
+ def handler(event, context):
436
+ raise ValueError("Test error")
437
+
438
+ with pytest.raises(ValueError):
439
+ handler(_sqs_event("m-1"), MagicMock())
440
+
441
+ assert flushed == [True]
442
+
443
+ def test_sqs_success_returns_handler_result(self, monkeypatch):
444
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
445
+
446
+ from nui_shared_utils.powertools_helpers import powertools_handler
447
+
448
+ @powertools_handler(service_name="test-service")
449
+ def handler(event, context):
450
+ return {"batchItemFailures": [{"itemIdentifier": "m-2"}]}
451
+
452
+ result = handler(_sqs_event("m-1", "m-2"), MagicMock())
453
+
454
+ assert result == {"batchItemFailures": [{"itemIdentifier": "m-2"}]}
455
+
456
+ @pytest.mark.parametrize(
457
+ "event",
458
+ [
459
+ {"Records": [{"EventSource": "aws:sns", "Sns": {}}]},
460
+ {"Records": [{"eventSource": "aws:kinesis", "kinesis": {}}]},
461
+ {"Records": [{"eventSource": "aws:dynamodb", "dynamodb": {}}]},
462
+ {"Records": [{"eventSource": "aws:s3", "s3": {}}]},
463
+ {"Records": []},
464
+ {"Records": "not-a-list"},
465
+ {"source": "aws.events", "detail-type": "Scheduled Event"},
466
+ ["not", "a", "dict"],
467
+ ],
468
+ ids=["sns", "kinesis", "dynamodb", "s3", "empty-records", "records-not-list", "scheduled", "non-dict"],
469
+ )
470
+ def test_non_sqs_events_keep_returning_500(self, monkeypatch, event):
471
+ monkeypatch.delenv("AWS_LAMBDA_RUNTIME_API", raising=False)
472
+
473
+ from nui_shared_utils.powertools_helpers import powertools_handler
474
+
475
+ @powertools_handler(service_name="test-service")
476
+ def handler(event, context):
477
+ raise ValueError("Test error")
478
+
479
+ result = handler(event, MagicMock())
480
+
481
+ assert result == {"statusCode": 500, "body": "Internal Server Error"}
482
+
483
+
337
484
  class TestOptionalImports:
338
485
  """Tests for graceful degradation of optional dependencies"""
339
486