nui-python-shared-utils 1.3.2__tar.gz → 1.3.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 (100) hide show
  1. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/PKG-INFO +1 -1
  2. nui_python_shared_utils-1.3.3/nui_lambda_shared_utils/__init__.py +44 -0
  3. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_python_shared_utils.egg-info/SOURCES.txt +2 -0
  4. nui_python_shared_utils-1.3.3/nui_shared_utils/__init__.py +263 -0
  5. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/cloudwatch_metrics.py +13 -3
  6. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/powertools_helpers.py +37 -15
  7. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/secrets_helper.py +3 -2
  8. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/utils.py +74 -88
  9. nui_python_shared_utils-1.3.3/scripts/bench_imports.py +107 -0
  10. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_cloudwatch_metrics.py +13 -13
  11. nui_python_shared_utils-1.3.3/tests/test_lazy_imports.py +160 -0
  12. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_utils.py +79 -70
  13. nui_python_shared_utils-1.3.2/nui_lambda_shared_utils/__init__.py +0 -25
  14. nui_python_shared_utils-1.3.2/nui_shared_utils/__init__.py +0 -252
  15. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/.editorconfig +0 -0
  16. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/.github/workflows/ci.yml +0 -0
  17. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/.github/workflows/publish.yml +0 -0
  18. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/.github/workflows/test.yml +0 -0
  19. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/.markdownlint-cli2.yaml +0 -0
  20. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/CLAUDE.md +0 -0
  21. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/CONTRIBUTING.md +0 -0
  22. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/LICENSE +0 -0
  23. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/MANIFEST.in +0 -0
  24. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/README.md +0 -0
  25. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/README.md +0 -0
  26. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/development/testing.md +0 -0
  27. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/getting-started/configuration.md +0 -0
  28. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/getting-started/installation.md +0 -0
  29. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/getting-started/quickstart.md +0 -0
  30. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/guides/cli-tools.md +0 -0
  31. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/guides/elasticsearch-integration.md +0 -0
  32. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/guides/jwt-authentication.md +0 -0
  33. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/guides/lambda-utilities.md +0 -0
  34. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/guides/log-processing.md +0 -0
  35. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/guides/powertools-integration.md +0 -0
  36. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/guides/shared-types.md +0 -0
  37. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/docs/guides/slack-integration.md +0 -0
  38. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/mypy.ini +0 -0
  39. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/base_client.py +0 -0
  40. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/cli.py +0 -0
  41. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/cloudwatch_metrics.py +0 -0
  42. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/config.py +0 -0
  43. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/db_client.py +0 -0
  44. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/error_handler.py +0 -0
  45. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/es_client.py +0 -0
  46. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/es_query_builder.py +0 -0
  47. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/jwt_auth.py +0 -0
  48. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/lambda_helpers.py +0 -0
  49. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/log_processors.py +0 -0
  50. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/powertools_helpers.py +0 -0
  51. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/secrets_helper.py +0 -0
  52. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/slack_client.py +0 -0
  53. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/slack_formatter.py +0 -0
  54. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/slack_setup/__init__.py +0 -0
  55. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/slack_setup/channel_creator.py +0 -0
  56. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/slack_setup/channel_definitions.py +0 -0
  57. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/slack_setup/setup_helpers.py +0 -0
  58. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/timezone.py +0 -0
  59. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_lambda_shared_utils/utils.py +0 -0
  60. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/base_client.py +0 -0
  61. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/cli.py +0 -0
  62. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/config.py +0 -0
  63. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/db_client.py +0 -0
  64. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/error_handler.py +0 -0
  65. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/es_client.py +0 -0
  66. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/es_query_builder.py +0 -0
  67. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/jwt_auth.py +0 -0
  68. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/lambda_helpers.py +0 -0
  69. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/log_processors.py +0 -0
  70. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/slack_client.py +0 -0
  71. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/slack_formatter.py +0 -0
  72. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/slack_setup/__init__.py +0 -0
  73. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/slack_setup/channel_creator.py +0 -0
  74. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/slack_setup/channel_definitions.py +0 -0
  75. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/slack_setup/setup_helpers.py +0 -0
  76. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/nui_shared_utils/timezone.py +0 -0
  77. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/pyproject.toml +0 -0
  78. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/pytest.ini +0 -0
  79. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/redirect/README.md +0 -0
  80. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/redirect/pyproject.toml +0 -0
  81. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/redirect/setup.py +0 -0
  82. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/requirements-test.txt +0 -0
  83. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/setup.cfg +0 -0
  84. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/setup.py +0 -0
  85. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/__init__.py +0 -0
  86. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_aws_utils.py +0 -0
  87. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_base_client.py +0 -0
  88. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_config.py +0 -0
  89. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_db_client.py +0 -0
  90. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_error_handler.py +0 -0
  91. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_es_client.py +0 -0
  92. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_es_query_builder.py +0 -0
  93. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_jwt_auth.py +0 -0
  94. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_lambda_helpers.py +0 -0
  95. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_log_processors.py +0 -0
  96. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_powertools_helpers.py +0 -0
  97. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_secrets_helper.py +0 -0
  98. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_slack_client.py +0 -0
  99. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_slack_formatter.py +0 -0
  100. {nui_python_shared_utils-1.3.2 → nui_python_shared_utils-1.3.3}/tests/test_timezone.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nui-python-shared-utils
3
- Version: 1.3.2
3
+ Version: 1.3.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
@@ -0,0 +1,44 @@
1
+ """
2
+ Backwards-compatibility shim for nui-lambda-shared-utils.
3
+
4
+ This package has been renamed to nui-python-shared-utils.
5
+ The import name has changed from nui_lambda_shared_utils to nui_shared_utils.
6
+
7
+ This shim forwards attribute access to nui_shared_utils so existing consumers
8
+ continue to work without changes. New code should use:
9
+
10
+ from nui_shared_utils import ...
11
+
12
+ This shim will be removed in the next major version (2.0.0).
13
+
14
+ Forwarding is lazy (PEP 562 ``__getattr__``) to preserve the cold-start
15
+ optimisation in the underlying package: ``from nui_lambda_shared_utils.jwt_auth
16
+ import check_auth`` only imports ``jwt_auth`` and its dependencies, not the
17
+ full slack/es/db client surface.
18
+ """
19
+
20
+ import warnings
21
+ from typing import Any, List
22
+
23
+ warnings.warn(
24
+ "nui_lambda_shared_utils is deprecated. Use nui_shared_utils instead. "
25
+ "This shim will be removed in version 2.0.0.",
26
+ DeprecationWarning,
27
+ stacklevel=2,
28
+ )
29
+
30
+ import nui_shared_utils as _target
31
+
32
+ __all__ = list(_target.__all__)
33
+
34
+
35
+ def __getattr__(name: str) -> Any:
36
+ # Delegate to the new package's lazy resolver. Cache on this module so
37
+ # subsequent accesses avoid the round-trip.
38
+ value = getattr(_target, name)
39
+ globals()[name] = value
40
+ return value
41
+
42
+
43
+ def __dir__() -> List[str]:
44
+ return sorted(set(globals()) | set(__all__))
@@ -73,6 +73,7 @@ nui_shared_utils/slack_setup/setup_helpers.py
73
73
  redirect/README.md
74
74
  redirect/pyproject.toml
75
75
  redirect/setup.py
76
+ scripts/bench_imports.py
76
77
  tests/__init__.py
77
78
  tests/test_aws_utils.py
78
79
  tests/test_base_client.py
@@ -84,6 +85,7 @@ tests/test_es_client.py
84
85
  tests/test_es_query_builder.py
85
86
  tests/test_jwt_auth.py
86
87
  tests/test_lambda_helpers.py
88
+ tests/test_lazy_imports.py
87
89
  tests/test_log_processors.py
88
90
  tests/test_powertools_helpers.py
89
91
  tests/test_secrets_helper.py
@@ -0,0 +1,263 @@
1
+ """
2
+ Enterprise-grade utilities for AWS Lambda functions with Slack, Elasticsearch, and monitoring integrations.
3
+
4
+ Public API is resolved lazily via PEP 562 ``__getattr__`` to keep package import
5
+ cheap. Submodules and their dependencies (boto3, slack-sdk, elasticsearch, etc.)
6
+ are only imported when an attribute is first accessed.
7
+ """
8
+
9
+ import importlib
10
+ from typing import TYPE_CHECKING, Any, List
11
+
12
+ # Map public name -> (submodule, attr_name).
13
+ # Optional integrations: keep their entries here; on ImportError during lazy
14
+ # resolution we return None to preserve historical behaviour where consumers
15
+ # could check ``if nui.SlackClient is not None``.
16
+ _LAZY_EXPORTS = {
17
+ # Configuration system
18
+ "Config": ("config", "Config"),
19
+ "get_config": ("config", "get_config"),
20
+ "set_config": ("config", "set_config"),
21
+ "configure": ("config", "configure"),
22
+ "get_es_host": ("config", "get_es_host"),
23
+ "get_es_credentials_secret": ("config", "get_es_credentials_secret"),
24
+ "get_db_credentials_secret": ("config", "get_db_credentials_secret"),
25
+ "get_slack_credentials_secret": ("config", "get_slack_credentials_secret"),
26
+ # Secrets
27
+ "get_secret": ("secrets_helper", "get_secret"),
28
+ "get_database_credentials": ("secrets_helper", "get_database_credentials"),
29
+ "get_elasticsearch_credentials": ("secrets_helper", "get_elasticsearch_credentials"),
30
+ "get_slack_credentials": ("secrets_helper", "get_slack_credentials"),
31
+ "get_api_key": ("secrets_helper", "get_api_key"),
32
+ "clear_cache": ("secrets_helper", "clear_cache"),
33
+ # Common utilities
34
+ "resolve_config_value": ("utils", "resolve_config_value"),
35
+ "resolve_aws_region": ("utils", "resolve_aws_region"),
36
+ "create_aws_client": ("utils", "create_aws_client"),
37
+ "handle_client_errors": ("utils", "handle_client_errors"),
38
+ "merge_dimensions": ("utils", "merge_dimensions"),
39
+ "validate_required_param": ("utils", "validate_required_param"),
40
+ "safe_close_connection": ("utils", "safe_close_connection"),
41
+ "format_log_context": ("utils", "format_log_context"),
42
+ "DEFAULT_AWS_REGION": ("utils", "DEFAULT_AWS_REGION"),
43
+ # Base client architecture
44
+ "BaseClient": ("base_client", "BaseClient"),
45
+ "ServiceHealthMixin": ("base_client", "ServiceHealthMixin"),
46
+ "RetryableOperationMixin": ("base_client", "RetryableOperationMixin"),
47
+ # Timezone helpers
48
+ "nz_time": ("timezone", "nz_time"),
49
+ "format_nz_time": ("timezone", "format_nz_time"),
50
+ # Slack formatting (no external deps)
51
+ "SlackBlockBuilder": ("slack_formatter", "SlackBlockBuilder"),
52
+ "format_currency": ("slack_formatter", "format_currency"),
53
+ "format_percentage": ("slack_formatter", "format_percentage"),
54
+ "format_number": ("slack_formatter", "format_number"),
55
+ "format_nz_time_slack": ("slack_formatter", "format_nz_time"),
56
+ "format_date_range": ("slack_formatter", "format_date_range"),
57
+ "format_daily_header": ("slack_formatter", "format_daily_header"),
58
+ "format_weekly_header": ("slack_formatter", "format_weekly_header"),
59
+ "format_error_alert": ("slack_formatter", "format_error_alert"),
60
+ "SEVERITY_EMOJI": ("slack_formatter", "SEVERITY_EMOJI"),
61
+ "STATUS_EMOJI": ("slack_formatter", "STATUS_EMOJI"),
62
+ # Error handling
63
+ "RetryableError": ("error_handler", "RetryableError"),
64
+ "NonRetryableError": ("error_handler", "NonRetryableError"),
65
+ "ErrorPatternMatcher": ("error_handler", "ErrorPatternMatcher"),
66
+ "ErrorAggregator": ("error_handler", "ErrorAggregator"),
67
+ "with_retry": ("error_handler", "with_retry"),
68
+ "retry_on_network_error": ("error_handler", "retry_on_network_error"),
69
+ "retry_on_db_error": ("error_handler", "retry_on_db_error"),
70
+ "retry_on_es_error": ("error_handler", "retry_on_es_error"),
71
+ "handle_lambda_error": ("error_handler", "handle_lambda_error"),
72
+ "categorize_retryable_error": ("error_handler", "categorize_retryable_error"),
73
+ # CloudWatch metrics
74
+ "MetricsPublisher": ("cloudwatch_metrics", "MetricsPublisher"),
75
+ "MetricAggregator": ("cloudwatch_metrics", "MetricAggregator"),
76
+ "StandardMetrics": ("cloudwatch_metrics", "StandardMetrics"),
77
+ "TimedMetric": ("cloudwatch_metrics", "TimedMetric"),
78
+ "track_lambda_performance": ("cloudwatch_metrics", "track_lambda_performance"),
79
+ "create_service_dimensions": ("cloudwatch_metrics", "create_service_dimensions"),
80
+ "publish_health_metric": ("cloudwatch_metrics", "publish_health_metric"),
81
+ # Log processing (no external deps)
82
+ "extract_cloudwatch_logs_from_kinesis": ("log_processors", "extract_cloudwatch_logs_from_kinesis"),
83
+ "derive_index_name": ("log_processors", "derive_index_name"),
84
+ "CloudWatchLogEvent": ("log_processors", "CloudWatchLogEvent"),
85
+ "CloudWatchLogsData": ("log_processors", "CloudWatchLogsData"),
86
+ # Lambda context helpers
87
+ "get_lambda_environment_info": ("lambda_helpers", "get_lambda_environment_info"),
88
+ # Optional: Slack client (slack-sdk)
89
+ "SlackClient": ("slack_client", "SlackClient"),
90
+ # Optional: Elasticsearch client + query builder
91
+ "ElasticsearchClient": ("es_client", "ElasticsearchClient"),
92
+ "ESQueryBuilder": ("es_query_builder", "ESQueryBuilder"),
93
+ "build_error_rate_query": ("es_query_builder", "build_error_rate_query"),
94
+ "build_top_errors_query": ("es_query_builder", "build_top_errors_query"),
95
+ "build_response_time_query": ("es_query_builder", "build_response_time_query"),
96
+ "build_service_volume_query": ("es_query_builder", "build_service_volume_query"),
97
+ "build_user_activity_query": ("es_query_builder", "build_user_activity_query"),
98
+ "build_pattern_detection_query": ("es_query_builder", "build_pattern_detection_query"),
99
+ "build_tender_participant_query": ("es_query_builder", "build_tender_participant_query"),
100
+ # Optional: Database client (pymysql / psycopg2)
101
+ "DatabaseClient": ("db_client", "DatabaseClient"),
102
+ "PostgreSQLClient": ("db_client", "PostgreSQLClient"),
103
+ "get_pool_stats": ("db_client", "get_pool_stats"),
104
+ # Optional: AWS Powertools
105
+ "get_powertools_logger": ("powertools_helpers", "get_powertools_logger"),
106
+ "powertools_handler": ("powertools_helpers", "powertools_handler"),
107
+ # Optional: JWT validation (rsa)
108
+ "validate_jwt": ("jwt_auth", "validate_jwt"),
109
+ "require_auth": ("jwt_auth", "require_auth"),
110
+ "check_auth": ("jwt_auth", "check_auth"),
111
+ "get_jwt_public_key": ("jwt_auth", "get_jwt_public_key"),
112
+ "JWTValidationError": ("jwt_auth", "JWTValidationError"),
113
+ "AuthenticationError": ("jwt_auth", "AuthenticationError"),
114
+ }
115
+
116
+ # Submodules that are optional integrations; ImportError during lazy load
117
+ # resolves to None instead of propagating, matching pre-1.4 behaviour.
118
+ # Includes ``slack_setup`` which is also handled by a special-case branch in
119
+ # ``__getattr__`` (it is exposed as a submodule object, not an attribute).
120
+ _OPTIONAL_SUBMODULES = {
121
+ "slack_client",
122
+ "es_client",
123
+ "es_query_builder",
124
+ "db_client",
125
+ "powertools_helpers",
126
+ "jwt_auth",
127
+ "slack_setup",
128
+ }
129
+
130
+
131
+ def __getattr__(name: str) -> Any:
132
+ # ``slack_setup`` is exposed as a submodule attribute (``nui.slack_setup``).
133
+ if name == "slack_setup":
134
+ try:
135
+ mod = importlib.import_module(".slack_setup", __name__)
136
+ except ImportError:
137
+ if name in _OPTIONAL_SUBMODULES:
138
+ mod = None
139
+ else:
140
+ raise
141
+ globals()["slack_setup"] = mod
142
+ return mod
143
+
144
+ if name in _LAZY_EXPORTS:
145
+ submod_name, attr = _LAZY_EXPORTS[name]
146
+ try:
147
+ submod = importlib.import_module(f".{submod_name}", __name__)
148
+ value = getattr(submod, attr)
149
+ except ImportError:
150
+ if submod_name in _OPTIONAL_SUBMODULES:
151
+ value = None
152
+ else:
153
+ raise
154
+ globals()[name] = value
155
+ return value
156
+
157
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
158
+
159
+
160
+ def __dir__() -> List[str]:
161
+ return sorted(set(globals()) | set(_LAZY_EXPORTS) | {"slack_setup"})
162
+
163
+
164
+ if TYPE_CHECKING:
165
+ # Imported for type checkers / IDE completion only; not executed at runtime.
166
+ from .config import (
167
+ Config,
168
+ configure,
169
+ get_config,
170
+ get_db_credentials_secret,
171
+ get_es_credentials_secret,
172
+ get_es_host,
173
+ get_slack_credentials_secret,
174
+ set_config,
175
+ )
176
+ from .secrets_helper import (
177
+ clear_cache,
178
+ get_api_key,
179
+ get_database_credentials,
180
+ get_elasticsearch_credentials,
181
+ get_secret,
182
+ get_slack_credentials,
183
+ )
184
+ from .utils import (
185
+ DEFAULT_AWS_REGION,
186
+ create_aws_client,
187
+ format_log_context,
188
+ handle_client_errors,
189
+ merge_dimensions,
190
+ resolve_aws_region,
191
+ resolve_config_value,
192
+ safe_close_connection,
193
+ validate_required_param,
194
+ )
195
+ from .base_client import BaseClient, RetryableOperationMixin, ServiceHealthMixin
196
+ from .timezone import format_nz_time, nz_time
197
+ from .slack_formatter import (
198
+ SEVERITY_EMOJI,
199
+ STATUS_EMOJI,
200
+ SlackBlockBuilder,
201
+ format_currency,
202
+ format_daily_header,
203
+ format_date_range,
204
+ format_error_alert,
205
+ format_number,
206
+ format_nz_time as format_nz_time_slack,
207
+ format_percentage,
208
+ format_weekly_header,
209
+ )
210
+ from .error_handler import (
211
+ ErrorAggregator,
212
+ ErrorPatternMatcher,
213
+ NonRetryableError,
214
+ RetryableError,
215
+ categorize_retryable_error,
216
+ handle_lambda_error,
217
+ retry_on_db_error,
218
+ retry_on_es_error,
219
+ retry_on_network_error,
220
+ with_retry,
221
+ )
222
+ from .cloudwatch_metrics import (
223
+ MetricAggregator,
224
+ MetricsPublisher,
225
+ StandardMetrics,
226
+ TimedMetric,
227
+ create_service_dimensions,
228
+ publish_health_metric,
229
+ track_lambda_performance,
230
+ )
231
+ from .log_processors import (
232
+ CloudWatchLogEvent,
233
+ CloudWatchLogsData,
234
+ derive_index_name,
235
+ extract_cloudwatch_logs_from_kinesis,
236
+ )
237
+ from .lambda_helpers import get_lambda_environment_info
238
+ from .slack_client import SlackClient
239
+ from .es_client import ElasticsearchClient
240
+ from .es_query_builder import (
241
+ ESQueryBuilder,
242
+ build_error_rate_query,
243
+ build_pattern_detection_query,
244
+ build_response_time_query,
245
+ build_service_volume_query,
246
+ build_tender_participant_query,
247
+ build_top_errors_query,
248
+ build_user_activity_query,
249
+ )
250
+ from .db_client import DatabaseClient, PostgreSQLClient, get_pool_stats
251
+ from .powertools_helpers import get_powertools_logger, powertools_handler
252
+ from .jwt_auth import (
253
+ AuthenticationError,
254
+ JWTValidationError,
255
+ check_auth,
256
+ get_jwt_public_key,
257
+ require_auth,
258
+ validate_jwt,
259
+ )
260
+ from . import slack_setup
261
+
262
+
263
+ __all__ = list(sorted(set(_LAZY_EXPORTS) | {"slack_setup"}))
@@ -9,8 +9,6 @@ import logging
9
9
  from typing import Dict, List, Optional, Union
10
10
  from datetime import datetime
11
11
  from collections import defaultdict
12
- import boto3
13
- from botocore.exceptions import ClientError
14
12
 
15
13
  log = logging.getLogger(__name__)
16
14
 
@@ -45,9 +43,19 @@ class MetricsPublisher:
45
43
  self.namespace = namespace
46
44
  self.default_dimensions = dimensions or {}
47
45
  self.auto_flush_size = auto_flush_size
48
- self.client = boto3.client("cloudwatch", region_name=region)
46
+ self._region = region
47
+ self._client = None
49
48
  self.metric_buffer: List[Dict] = []
50
49
 
50
+ @property
51
+ def client(self):
52
+ """Lazily construct the boto3 CloudWatch client on first use."""
53
+ if self._client is None:
54
+ import boto3
55
+
56
+ self._client = boto3.client("cloudwatch", region_name=self._region)
57
+ return self._client
58
+
51
59
  def put_metric(
52
60
  self,
53
61
  metric_name: str,
@@ -148,6 +156,8 @@ class MetricsPublisher:
148
156
  if not self.metric_buffer:
149
157
  return True
150
158
 
159
+ from botocore.exceptions import ClientError
160
+
151
161
  try:
152
162
  # CloudWatch allows max 20 metrics per request
153
163
  for i in range(0, len(self.metric_buffer), 20):
@@ -26,15 +26,34 @@ try:
26
26
  except ImportError:
27
27
  COLOREDLOGS_AVAILABLE = False
28
28
 
29
- try:
30
- from .slack_client import SlackClient
29
+ from .lambda_helpers import get_lambda_environment_info
31
30
 
32
- SLACK_CLIENT_AVAILABLE = True
33
- except ImportError:
34
- SLACK_CLIENT_AVAILABLE = False
35
- SlackClient = None # type: ignore
31
+ # SlackClient is loaded lazily on first use to keep this module's import
32
+ # cost low for callers that don't enable Slack alerting (it transitively
33
+ # pulls in slack_sdk, which is the dominant cost).
34
+ SLACK_CLIENT_AVAILABLE = False
35
+ SlackClient = None # type: ignore[assignment]
36
36
 
37
- from .lambda_helpers import get_lambda_environment_info
37
+
38
+ def _ensure_slack_client_loaded() -> None:
39
+ """Lazy-import :class:`SlackClient` and update module-level flags.
40
+
41
+ Idempotent: returns immediately if ``SlackClient`` has already been
42
+ populated (real import or test mock).
43
+ """
44
+ global SLACK_CLIENT_AVAILABLE, SlackClient
45
+ if SlackClient is not None:
46
+ # Already populated (real import or test mock) — keep the availability
47
+ # flag in sync so callers don't see a stale False.
48
+ SLACK_CLIENT_AVAILABLE = True
49
+ return
50
+ try:
51
+ from .slack_client import SlackClient as _SC
52
+
53
+ SlackClient = _SC
54
+ SLACK_CLIENT_AVAILABLE = True
55
+ except ImportError:
56
+ SLACK_CLIENT_AVAILABLE = False
38
57
 
39
58
 
40
59
  __all__ = ["get_powertools_logger", "powertools_handler"]
@@ -99,6 +118,7 @@ def get_powertools_logger(
99
118
  if func is not None:
100
119
  return func
101
120
  return lambda f: f
121
+
102
122
  logger.inject_lambda_context = _mock_inject_lambda_context # type: ignore
103
123
 
104
124
  return logger
@@ -194,14 +214,16 @@ def powertools_handler(
194
214
 
195
215
  # Create Slack client if channel provided
196
216
  slack_client = None
197
- if slack_alert_channel and SLACK_CLIENT_AVAILABLE:
198
- try:
199
- slack_client = SlackClient(
200
- account_names=slack_account_names,
201
- account_names_config=slack_account_names_config,
202
- )
203
- except Exception as e:
204
- logger.warning("Failed to initialize Slack client: %s", e)
217
+ if slack_alert_channel:
218
+ _ensure_slack_client_loaded()
219
+ if SLACK_CLIENT_AVAILABLE and SlackClient is not None:
220
+ try:
221
+ slack_client = SlackClient(
222
+ account_names=slack_account_names,
223
+ account_names_config=slack_account_names_config,
224
+ )
225
+ except Exception as e:
226
+ logger.warning("Failed to initialize Slack client: %s", e)
205
227
 
206
228
  @functools.wraps(func)
207
229
  def wrapper(event: dict, context: Any) -> dict:
@@ -7,8 +7,6 @@ import os
7
7
  import json
8
8
  import logging
9
9
  from typing import Dict, Optional
10
- import boto3
11
- from botocore.exceptions import ClientError
12
10
 
13
11
  from .config import get_config
14
12
 
@@ -35,6 +33,9 @@ def get_secret(secret_name: str) -> Dict:
35
33
  if secret_name in _secrets_cache:
36
34
  return _secrets_cache[secret_name]
37
35
 
36
+ import boto3
37
+ from botocore.exceptions import ClientError
38
+
38
39
  # Create a Secrets Manager client
39
40
  session = boto3.session.Session()
40
41
  client = session.client(service_name="secretsmanager", region_name=session.region_name or "ap-southeast-2")