nui-python-shared-utils 1.3.2__py3-none-any.whl → 1.3.3__py3-none-any.whl

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.
@@ -4,15 +4,21 @@ Backwards-compatibility shim for nui-lambda-shared-utils.
4
4
  This package has been renamed to nui-python-shared-utils.
5
5
  The import name has changed from nui_lambda_shared_utils to nui_shared_utils.
6
6
 
7
- This shim re-exports everything from nui_shared_utils so existing consumers
7
+ This shim forwards attribute access to nui_shared_utils so existing consumers
8
8
  continue to work without changes. New code should use:
9
9
 
10
10
  from nui_shared_utils import ...
11
11
 
12
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.
13
18
  """
14
19
 
15
20
  import warnings
21
+ from typing import Any, List
16
22
 
17
23
  warnings.warn(
18
24
  "nui_lambda_shared_utils is deprecated. Use nui_shared_utils instead. "
@@ -21,5 +27,18 @@ warnings.warn(
21
27
  stacklevel=2,
22
28
  )
23
29
 
24
- from nui_shared_utils import * # noqa: F401,F403
25
- from nui_shared_utils import __all__ # noqa: F401
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__))
@@ -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
@@ -1,4 +1,4 @@
1
- nui_lambda_shared_utils/__init__.py,sha256=5hyn9xyUmLXov5Ej5i2CHFQgSNNoOkwCHuQjdKySG8E,741
1
+ nui_lambda_shared_utils/__init__.py,sha256=FaaYBqgNxSQ0x7jlf5fw2pQoMh8Pbmw7uZiVfavI_IY,1320
2
2
  nui_lambda_shared_utils/base_client.py,sha256=c7t2_IfISQ39Y1AY6MOj-SlX2tkpzNpp47RRMfu5xqY,135
3
3
  nui_lambda_shared_utils/cli.py,sha256=3TtfQy0x3P0QKiNDiz0j3kze7Hs26CMeQE8X_948OEQ,127
4
4
  nui_lambda_shared_utils/cloudwatch_metrics.py,sha256=Qwj52AIVrxg7HEEMjmgvDptCJ5Csr_w3NJBI1v34sFw,142
@@ -20,11 +20,11 @@ nui_lambda_shared_utils/slack_setup/__init__.py,sha256=q78NzSznsSd3ri5OUohWxQt7r
20
20
  nui_lambda_shared_utils/slack_setup/channel_creator.py,sha256=xaeqzyEAqVqYj2EhN69UvcyZQ6LdFKYODPPCwxHInLA,172
21
21
  nui_lambda_shared_utils/slack_setup/channel_definitions.py,sha256=3TkdLzWvEMBMDy5jqOPMUGRgKQ3z9TN58D43V7O7LpE,180
22
22
  nui_lambda_shared_utils/slack_setup/setup_helpers.py,sha256=ex_RiUfiZqH9qGLQwmedezMbfp35uI-VQPevducfbBk,168
23
- nui_python_shared_utils-1.3.2.dist-info/licenses/LICENSE,sha256=vGe2mC5yLUb8toYlY3T36ZwCB5zQUW5hlCtEMiqokhM,1071
24
- nui_shared_utils/__init__.py,sha256=0yYLzo2zggr43W2zdybp7PBc5oPsU-kwh1M1r7CuFv4,6617
23
+ nui_python_shared_utils-1.3.3.dist-info/licenses/LICENSE,sha256=vGe2mC5yLUb8toYlY3T36ZwCB5zQUW5hlCtEMiqokhM,1071
24
+ nui_shared_utils/__init__.py,sha256=Myt_55yTIZO71iyz2wkM9wySd7xhOCCky4EBOs-aI0A,10964
25
25
  nui_shared_utils/base_client.py,sha256=I1lKQGhrKyvujV2zps0TrtEdPDqMCwRQaIh6ceGONXQ,11016
26
26
  nui_shared_utils/cli.py,sha256=JJpSoQWKvAz4b8cO30yFNi5vY9jmqrCHzbFpvrVTkbU,8747
27
- nui_shared_utils/cloudwatch_metrics.py,sha256=Rdyfy-3zAQqZkPL2krzjyB3FXYgrh7Hx56nVeT6eDIw,11836
27
+ nui_shared_utils/cloudwatch_metrics.py,sha256=p0J-s63UJDGK14TRGt0k8yoj7aWSSM55UKdgcuaT1dU,12098
28
28
  nui_shared_utils/config.py,sha256=ZbkUKDxnHMJiHNv-TlWKjotmf2mH1Vz3GaIXgGfWg7E,4524
29
29
  nui_shared_utils/db_client.py,sha256=HyVKJ1Su0dVPV1QI2MU5UtT8myDRCjKgV60xRL4SEdE,21726
30
30
  nui_shared_utils/error_handler.py,sha256=pJ6b_mI0Ait8QRbs9UmLzYUN-Ft0l_dnpudVO_6LRSE,12171
@@ -33,18 +33,18 @@ nui_shared_utils/es_query_builder.py,sha256=UuheQf8b2UXaOiJlYCKYIfKfVERDZ3ag1P0O
33
33
  nui_shared_utils/jwt_auth.py,sha256=2Ag1zZKxd2R8QBz3aQA4h8OtKHQvUVKrjUp6lXpJxJU,9143
34
34
  nui_shared_utils/lambda_helpers.py,sha256=psHVotpmOfnmyQCoOt4MSIEj7VwWcy6gZM3vYSZwjOk,3041
35
35
  nui_shared_utils/log_processors.py,sha256=x5gz1LEkbmoMCZ3ZB6q-mbn26dSzwh1trz4wSkBGxqk,5538
36
- nui_shared_utils/powertools_helpers.py,sha256=pVu9MVBC6PEirHBbMC-9nKreP9jk8wd0fQt6OWekzdk,10126
37
- nui_shared_utils/secrets_helper.py,sha256=KoEKplGjfxc8HnhRXvxCRAqLKOOBN9oxKABjTlZvy4I,6806
36
+ nui_shared_utils/powertools_helpers.py,sha256=qc-lYLfY-QX20gALfPOJcihrtJhiCU-fJW8rsPT7LJE,11037
37
+ nui_shared_utils/secrets_helper.py,sha256=jtQZSbghNTyWYylGxFiHWeL28Q1JUpLTrnGIICU4R3k,6815
38
38
  nui_shared_utils/slack_client.py,sha256=_qR7Q1GU7gvYhUxiaBxEohhoEYznqr8kz9enGfeDtn8,24759
39
39
  nui_shared_utils/slack_formatter.py,sha256=95g6XfAJst7RVhd0M0ahiF3gWWW5j96WYkCg5tLS6Zg,10894
40
40
  nui_shared_utils/timezone.py,sha256=TvtSZV7w3Vvz3NbMTcJSNbUf0ZxPwpFS-xfgye1h-4Q,3583
41
- nui_shared_utils/utils.py,sha256=oQQBmPA-0dM-XFRLBEBk_hh9FtRZyoY9_egmviyGemg,8146
41
+ nui_shared_utils/utils.py,sha256=gOpw80tZGC24QsyesR8EySRODY-TFXG8cXUvggPBsRI,8184
42
42
  nui_shared_utils/slack_setup/__init__.py,sha256=OElyS3xk4F_YKH5uUUTDpN0ah1dOO3e52muIPjAMPW8,320
43
43
  nui_shared_utils/slack_setup/channel_creator.py,sha256=0gyCBIS0EC96SVn1Z2dD4Fpzk0xNdHSWyiCxoUQIUe8,10667
44
44
  nui_shared_utils/slack_setup/channel_definitions.py,sha256=atfz5ZhpqefOeLh1gShbWd-TLzgjPmhv95bfuTdmZog,5676
45
45
  nui_shared_utils/slack_setup/setup_helpers.py,sha256=pzzXMs12GI9sdZttWeYzgLPgC0xqPz7ZLHM1GUPNNXc,7219
46
- nui_python_shared_utils-1.3.2.dist-info/METADATA,sha256=-N4PxQPpnnqdwBjiZ-uCWfVdAgeYARr7tRfcXsoUuto,19387
47
- nui_python_shared_utils-1.3.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
48
- nui_python_shared_utils-1.3.2.dist-info/entry_points.txt,sha256=TrJ3Z4kz3oXfy6InXn20jHu7rnTwo-4EA0KRFX3fkL0,66
49
- nui_python_shared_utils-1.3.2.dist-info/top_level.txt,sha256=sNceq5okmEB54L-gm4a0OgGhnPlU62zYmlUoXKn-Fa8,41
50
- nui_python_shared_utils-1.3.2.dist-info/RECORD,,
46
+ nui_python_shared_utils-1.3.3.dist-info/METADATA,sha256=y_FgR8-pq8PWqEayZQfYAtOqzijcsdG2UOQDVxd4-7E,19387
47
+ nui_python_shared_utils-1.3.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
48
+ nui_python_shared_utils-1.3.3.dist-info/entry_points.txt,sha256=TrJ3Z4kz3oXfy6InXn20jHu7rnTwo-4EA0KRFX3fkL0,66
49
+ nui_python_shared_utils-1.3.3.dist-info/top_level.txt,sha256=sNceq5okmEB54L-gm4a0OgGhnPlU62zYmlUoXKn-Fa8,41
50
+ nui_python_shared_utils-1.3.3.dist-info/RECORD,,
@@ -1,252 +1,263 @@
1
1
  """
2
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.
3
7
  """
4
8
 
5
- # Configuration system
6
- from .config import (
7
- Config,
8
- get_config,
9
- set_config,
10
- configure,
11
- get_es_host,
12
- get_es_credentials_secret,
13
- get_db_credentials_secret,
14
- get_slack_credentials_secret,
15
- )
9
+ import importlib
10
+ from typing import TYPE_CHECKING, Any, List
16
11
 
17
- # Core utilities
18
- from .secrets_helper import (
19
- get_secret,
20
- get_database_credentials,
21
- get_elasticsearch_credentials,
22
- get_slack_credentials,
23
- get_api_key,
24
- clear_cache,
25
- )
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
+ }
26
115
 
27
- # Common utilities
28
- from .utils import (
29
- resolve_config_value,
30
- create_aws_client,
31
- handle_client_errors,
32
- merge_dimensions,
33
- validate_required_param,
34
- )
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
+ }
35
129
 
36
- # Base client architecture
37
- from .base_client import BaseClient, ServiceHealthMixin, RetryableOperationMixin
38
130
 
39
- # Client implementations - only fail if actually used
40
- try:
41
- from .slack_client import SlackClient
42
- except ImportError:
43
- SlackClient = None # type: ignore
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
44
143
 
45
- try:
46
- from .es_client import ElasticsearchClient
47
- except ImportError:
48
- ElasticsearchClient = None # type: ignore
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}")
49
158
 
50
- try:
51
- from .db_client import DatabaseClient, PostgreSQLClient, get_pool_stats
52
- except ImportError:
53
- DatabaseClient = None # type: ignore
54
- PostgreSQLClient = None # type: ignore
55
- get_pool_stats = None # type: ignore
56
159
 
57
- from .timezone import nz_time, format_nz_time
160
+ def __dir__() -> List[str]:
161
+ return sorted(set(globals()) | set(_LAZY_EXPORTS) | {"slack_setup"})
58
162
 
59
- # Slack formatting utilities (no external dependencies)
60
- from .slack_formatter import (
61
- SlackBlockBuilder,
62
- format_currency,
63
- format_percentage,
64
- format_number,
65
- format_nz_time as format_nz_time_slack,
66
- format_date_range,
67
- format_daily_header,
68
- format_weekly_header,
69
- format_error_alert,
70
- SEVERITY_EMOJI,
71
- STATUS_EMOJI,
72
- )
73
163
 
74
- # ES query builder - optional import
75
- try:
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
76
240
  from .es_query_builder import (
77
241
  ESQueryBuilder,
78
242
  build_error_rate_query,
79
- build_top_errors_query,
243
+ build_pattern_detection_query,
80
244
  build_response_time_query,
81
245
  build_service_volume_query,
82
- build_user_activity_query,
83
- build_pattern_detection_query,
84
246
  build_tender_participant_query,
247
+ build_top_errors_query,
248
+ build_user_activity_query,
85
249
  )
86
- except ImportError:
87
- ESQueryBuilder = None # type: ignore
88
- build_error_rate_query = None # type: ignore
89
- build_top_errors_query = None # type: ignore
90
- build_response_time_query = None # type: ignore
91
- build_service_volume_query = None # type: ignore
92
- build_user_activity_query = None # type: ignore
93
- build_pattern_detection_query = None # type: ignore
94
- build_tender_participant_query = None # type: ignore
95
- from .error_handler import (
96
- RetryableError,
97
- NonRetryableError,
98
- ErrorPatternMatcher,
99
- ErrorAggregator,
100
- with_retry,
101
- retry_on_network_error,
102
- retry_on_db_error,
103
- retry_on_es_error,
104
- handle_lambda_error,
105
- categorize_retryable_error,
106
- )
107
- from .cloudwatch_metrics import (
108
- MetricsPublisher,
109
- MetricAggregator,
110
- StandardMetrics,
111
- TimedMetric,
112
- track_lambda_performance,
113
- create_service_dimensions,
114
- publish_health_metric,
115
- )
116
-
117
- # AWS Powertools integration - optional import
118
- try:
250
+ from .db_client import DatabaseClient, PostgreSQLClient, get_pool_stats
119
251
  from .powertools_helpers import get_powertools_logger, powertools_handler
120
- except ImportError:
121
- get_powertools_logger = None # type: ignore
122
- powertools_handler = None # type: ignore
123
-
124
- # Log processing utilities (no external dependencies)
125
- from .log_processors import (
126
- CloudWatchLogEvent,
127
- CloudWatchLogsData,
128
- derive_index_name,
129
- extract_cloudwatch_logs_from_kinesis,
130
- )
131
-
132
- # Lambda context helpers (no external dependencies)
133
- from .lambda_helpers import get_lambda_environment_info
134
-
135
- # JWT authentication - optional import
136
- try:
137
252
  from .jwt_auth import (
138
- validate_jwt,
139
- require_auth,
253
+ AuthenticationError,
254
+ JWTValidationError,
140
255
  check_auth,
141
256
  get_jwt_public_key,
142
- JWTValidationError,
143
- AuthenticationError,
257
+ require_auth,
258
+ validate_jwt,
144
259
  )
145
- except ImportError:
146
- validate_jwt = None # type: ignore
147
- require_auth = None # type: ignore
148
- check_auth = None # type: ignore
149
- get_jwt_public_key = None # type: ignore
150
- JWTValidationError = None # type: ignore
151
- AuthenticationError = None # type: ignore
152
-
153
- # Slack setup utilities (for CLI usage) - optional import
154
- try:
155
260
  from . import slack_setup
156
- except ImportError:
157
- slack_setup = None # type: ignore
158
261
 
159
- __all__ = [
160
- # Configuration system
161
- "Config",
162
- "get_config",
163
- "set_config",
164
- "configure",
165
- "get_es_host",
166
- "get_es_credentials_secret",
167
- "get_db_credentials_secret",
168
- "get_slack_credentials_secret",
169
- # Core utilities
170
- "get_secret",
171
- "get_database_credentials",
172
- "get_elasticsearch_credentials",
173
- "get_slack_credentials",
174
- "get_api_key",
175
- "clear_cache",
176
- # Common utilities
177
- "resolve_config_value",
178
- "create_aws_client",
179
- "handle_client_errors",
180
- "merge_dimensions",
181
- "validate_required_param",
182
- # Base client architecture
183
- "BaseClient",
184
- "ServiceHealthMixin",
185
- "RetryableOperationMixin",
186
- # Client implementations
187
- "SlackClient",
188
- "ElasticsearchClient",
189
- "DatabaseClient",
190
- "PostgreSQLClient",
191
- "get_pool_stats", # Legacy compatibility (None)
192
- "nz_time",
193
- "format_nz_time",
194
- "slack_setup",
195
- # Slack formatting
196
- "SlackBlockBuilder",
197
- "format_currency",
198
- "format_percentage",
199
- "format_number",
200
- "format_nz_time_slack",
201
- "format_date_range",
202
- "format_daily_header",
203
- "format_weekly_header",
204
- "format_error_alert",
205
- "SEVERITY_EMOJI",
206
- "STATUS_EMOJI",
207
- # ES query building
208
- "ESQueryBuilder",
209
- "build_error_rate_query",
210
- "build_top_errors_query",
211
- "build_response_time_query",
212
- "build_service_volume_query",
213
- "build_user_activity_query",
214
- "build_pattern_detection_query",
215
- "build_tender_participant_query",
216
- # Error handling
217
- "RetryableError",
218
- "NonRetryableError",
219
- "ErrorPatternMatcher",
220
- "ErrorAggregator",
221
- "with_retry",
222
- "retry_on_network_error",
223
- "retry_on_db_error",
224
- "retry_on_es_error",
225
- "handle_lambda_error",
226
- "categorize_retryable_error",
227
- # CloudWatch metrics
228
- "MetricsPublisher",
229
- "MetricAggregator",
230
- "StandardMetrics",
231
- "TimedMetric",
232
- "track_lambda_performance",
233
- "create_service_dimensions",
234
- "publish_health_metric",
235
- # AWS Powertools integration
236
- "get_powertools_logger",
237
- "powertools_handler",
238
- # Log processing
239
- "extract_cloudwatch_logs_from_kinesis",
240
- "derive_index_name",
241
- "CloudWatchLogEvent",
242
- "CloudWatchLogsData",
243
- # Lambda context helpers
244
- "get_lambda_environment_info",
245
- # JWT authentication
246
- "validate_jwt",
247
- "require_auth",
248
- "check_auth",
249
- "get_jwt_public_key",
250
- "JWTValidationError",
251
- "AuthenticationError",
252
- ]
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")
nui_shared_utils/utils.py CHANGED
@@ -7,53 +7,49 @@ import time
7
7
  import logging
8
8
  import functools
9
9
  from typing import Union, List, Optional, Any, Dict
10
- import boto3
11
- from botocore.exceptions import ClientError, NoCredentialsError
12
10
 
13
11
  from .config import get_config
14
12
 
15
13
  log = logging.getLogger(__name__)
16
14
 
17
- # AWS region resolution constants
15
+ # AWS region fallback. Used only when no explicit region, env var, config,
16
+ # or boto3 session region is available. Override at deploy time via the
17
+ # ``AWS_REGION_FALLBACK`` environment variable.
18
18
  DEFAULT_AWS_REGION = "ap-southeast-2"
19
19
 
20
20
 
21
- def resolve_config_value(
22
- param_value: Optional[Any],
23
- env_var_names: Union[str, List[str]],
24
- config_default: Any
25
- ) -> Any:
21
+ def resolve_config_value(param_value: Optional[Any], env_var_names: Union[str, List[str]], config_default: Any) -> Any:
26
22
  """
27
23
  Resolve configuration value with priority: param > env vars > config default.
28
-
24
+
29
25
  Args:
30
26
  param_value: Explicitly provided parameter value
31
27
  env_var_names: Environment variable name(s) to check (string or list)
32
28
  config_default: Default value from configuration
33
-
29
+
34
30
  Returns:
35
31
  Resolved configuration value
36
-
32
+
37
33
  Example:
38
34
  host = resolve_config_value(
39
- host_param,
40
- ["ES_HOST", "ELASTICSEARCH_HOST"],
35
+ host_param,
36
+ ["ES_HOST", "ELASTICSEARCH_HOST"],
41
37
  "localhost:9200"
42
38
  )
43
39
  """
44
40
  # Parameter takes highest precedence
45
41
  if param_value is not None:
46
42
  return param_value
47
-
43
+
48
44
  # Check environment variables
49
45
  if isinstance(env_var_names, str):
50
46
  env_var_names = [env_var_names]
51
-
47
+
52
48
  for env_var in env_var_names:
53
49
  value = os.environ.get(env_var)
54
50
  if value is not None:
55
51
  return value
56
-
52
+
57
53
  # Fall back to config default
58
54
  return config_default
59
55
 
@@ -61,70 +57,72 @@ def resolve_config_value(
61
57
  def resolve_aws_region(explicit_region: Optional[str] = None) -> str:
62
58
  """
63
59
  Resolve AWS region with priority: param > env > config > session > default.
64
-
60
+
65
61
  Args:
66
62
  explicit_region: Explicitly provided region
67
-
63
+
68
64
  Returns:
69
65
  AWS region string
70
66
  """
71
67
  # Explicit parameter wins
72
68
  if explicit_region:
73
69
  return explicit_region
74
-
70
+
75
71
  # Check environment variables
76
- env_region = resolve_config_value(
77
- None,
78
- ["AWS_REGION", "AWS_DEFAULT_REGION"],
79
- None
80
- )
72
+ env_region = resolve_config_value(None, ["AWS_REGION", "AWS_DEFAULT_REGION"], None)
81
73
  if env_region:
82
74
  return env_region
83
-
75
+
84
76
  # Check config
85
77
  config = get_config()
86
- if hasattr(config, 'aws_region') and config.aws_region:
78
+ if hasattr(config, "aws_region") and config.aws_region:
87
79
  return config.aws_region
88
-
80
+
89
81
  # Check boto3 session default
90
82
  try:
83
+ import boto3
84
+ from botocore.exceptions import NoCredentialsError
85
+
91
86
  session = boto3.session.Session()
92
87
  if session.region_name:
93
88
  return session.region_name
94
- except Exception as e:
95
- log.debug(f"Failed to get session region: {e}")
96
-
97
- # Final fallback
98
- return DEFAULT_AWS_REGION
89
+ except ImportError as e:
90
+ log.warning(f"boto3 not available for session-based region resolution: {e}")
91
+ except NoCredentialsError as e:
92
+ log.warning(f"No AWS credentials configured for session-based region resolution: {e}")
93
+
94
+ # Final fallback. Operators can override the package default via
95
+ # AWS_REGION_FALLBACK without forking or monkey-patching the constant.
96
+ return os.environ.get("AWS_REGION_FALLBACK") or DEFAULT_AWS_REGION
99
97
 
100
98
 
101
99
  def create_aws_client(service_name: str, region: Optional[str] = None):
102
100
  """
103
101
  Create AWS client with consistent region resolution and error handling.
104
-
102
+
105
103
  Args:
106
104
  service_name: AWS service name (e.g., 'secretsmanager', 'cloudwatch')
107
105
  region: Optional explicit region
108
-
106
+
109
107
  Returns:
110
108
  AWS service client
111
-
109
+
112
110
  Raises:
113
111
  NoCredentialsError: When AWS credentials are not configured
114
112
  ClientError: When client creation fails
115
113
  """
116
114
  resolved_region = resolve_aws_region(region)
117
-
115
+
116
+ import boto3
117
+ from botocore.exceptions import ClientError, NoCredentialsError
118
+
118
119
  try:
119
120
  session = boto3.session.Session()
120
- client = session.client(
121
- service_name=service_name,
122
- region_name=resolved_region
123
- )
124
-
121
+ client = session.client(service_name=service_name, region_name=resolved_region)
122
+
125
123
  log.debug(f"Created {service_name} client for region {resolved_region}")
126
124
  return client
127
-
125
+
128
126
  except NoCredentialsError:
129
127
  log.error(f"AWS credentials not configured for {service_name} client")
130
128
  raise
@@ -137,65 +135,59 @@ def create_aws_client(service_name: str, region: Optional[str] = None):
137
135
 
138
136
 
139
137
  def handle_client_errors(
140
- default_return: Any = None,
141
- log_context: Optional[Dict[str, Any]] = None,
142
- reraise: bool = False
138
+ default_return: Any = None, log_context: Optional[Dict[str, Any]] = None, reraise: bool = False
143
139
  ):
144
140
  """
145
141
  Decorator for standardized client error handling.
146
-
142
+
147
143
  Args:
148
144
  default_return: Value to return on error (if not reraising)
149
145
  log_context: Additional context for error logging
150
146
  reraise: Whether to re-raise exceptions after logging
151
-
147
+
152
148
  Example:
153
149
  @handle_client_errors(default_return=[])
154
150
  def search_documents(self, query):
155
151
  # Implementation that might fail
156
152
  return results
157
153
  """
154
+
158
155
  def decorator(func):
159
156
  @functools.wraps(func)
160
- def wrapper(*args, **kwargs):
157
+ def wrapper(*args, **kwargs) -> Any:
161
158
  try:
162
159
  return func(*args, **kwargs)
163
160
  except Exception as e:
164
161
  # Build log context
165
- context = {
166
- "function": func.__name__,
167
- "error_type": type(e).__name__,
168
- "error_message": str(e)
169
- }
162
+ context = {"function": func.__name__, "error_type": type(e).__name__, "error_message": str(e)}
170
163
  if log_context:
171
164
  context.update(log_context)
172
-
173
- log.error(
174
- f"{func.__name__} failed: {e}",
175
- exc_info=True,
176
- extra=context
177
- )
178
-
165
+
166
+ log.error(f"{func.__name__} failed: {e}", exc_info=True, extra=context)
167
+
179
168
  if reraise:
180
169
  raise
181
-
170
+
182
171
  return default_return
183
-
172
+
184
173
  return wrapper
174
+
185
175
  return decorator
186
176
 
187
177
 
188
- def merge_dimensions(base_dimensions: Dict[str, str], additional_dimensions: Optional[Dict[str, str]] = None) -> List[Dict[str, str]]:
178
+ def merge_dimensions(
179
+ base_dimensions: Dict[str, str], additional_dimensions: Optional[Dict[str, str]] = None
180
+ ) -> List[Dict[str, str]]:
189
181
  """
190
182
  Merge CloudWatch metric dimensions and format for API.
191
-
183
+
192
184
  Args:
193
185
  base_dimensions: Base dimensions dictionary
194
186
  additional_dimensions: Additional dimensions to merge
195
-
187
+
196
188
  Returns:
197
189
  List of dimension dictionaries formatted for CloudWatch API
198
-
190
+
199
191
  Example:
200
192
  dimensions = merge_dimensions(
201
193
  {"Service": "auth", "Environment": "prod"},
@@ -206,40 +198,37 @@ def merge_dimensions(base_dimensions: Dict[str, str], additional_dimensions: Opt
206
198
  all_dimensions = {**base_dimensions}
207
199
  if additional_dimensions:
208
200
  all_dimensions.update(additional_dimensions)
209
-
210
- return [
211
- {"Name": str(key), "Value": str(value)}
212
- for key, value in all_dimensions.items()
213
- ]
201
+
202
+ return [{"Name": str(key), "Value": str(value)} for key, value in all_dimensions.items()]
214
203
 
215
204
 
216
205
  def validate_required_param(param_value: Any, param_name: str) -> Any:
217
206
  """
218
207
  Validate that a required parameter is provided.
219
-
208
+
220
209
  Args:
221
210
  param_value: Parameter value to validate
222
211
  param_name: Parameter name for error messages
223
-
212
+
224
213
  Returns:
225
214
  The parameter value if valid
226
-
215
+
227
216
  Raises:
228
217
  ValueError: If parameter is None or empty string
229
218
  """
230
219
  if param_value is None:
231
220
  raise ValueError(f"{param_name} is required")
232
-
221
+
233
222
  if isinstance(param_value, str) and not param_value.strip():
234
223
  raise ValueError(f"{param_name} cannot be empty")
235
-
224
+
236
225
  return param_value
237
226
 
238
227
 
239
228
  def safe_close_connection(connection) -> None:
240
229
  """
241
230
  Safely close a database connection with proper error handling.
242
-
231
+
243
232
  Args:
244
233
  connection: Database connection to close
245
234
  """
@@ -251,29 +240,26 @@ def safe_close_connection(connection) -> None:
251
240
  return
252
241
  if hasattr(connection, "open") and not connection.open:
253
242
  return
254
-
243
+
255
244
  # Generic close
256
245
  connection.close()
257
246
  log.debug("Database connection closed successfully")
258
-
247
+
259
248
  except Exception as e:
260
249
  log.debug(f"Error closing connection (non-fatal): {e}")
261
250
 
262
251
 
263
- def format_log_context(
264
- operation: str,
265
- **context_data
266
- ) -> Dict[str, Any]:
252
+ def format_log_context(operation: str, **context_data) -> Dict[str, Any]:
267
253
  """
268
254
  Format consistent logging context for operations.
269
-
255
+
270
256
  Args:
271
257
  operation: Operation name
272
258
  **context_data: Additional context key-value pairs
273
-
259
+
274
260
  Returns:
275
261
  Formatted context dictionary
276
-
262
+
277
263
  Example:
278
264
  context = format_log_context(
279
265
  "database_query",
@@ -287,5 +273,5 @@ def format_log_context(
287
273
  "timestamp": time.time(),
288
274
  }
289
275
  context.update(context_data)
290
-
291
- return context
276
+
277
+ return context