nui-python-shared-utils 1.3.0__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.
- nui_lambda_shared_utils/__init__.py +252 -0
- nui_lambda_shared_utils/base_client.py +323 -0
- nui_lambda_shared_utils/cli.py +225 -0
- nui_lambda_shared_utils/cloudwatch_metrics.py +367 -0
- nui_lambda_shared_utils/config.py +136 -0
- nui_lambda_shared_utils/db_client.py +623 -0
- nui_lambda_shared_utils/error_handler.py +372 -0
- nui_lambda_shared_utils/es_client.py +460 -0
- nui_lambda_shared_utils/es_query_builder.py +315 -0
- nui_lambda_shared_utils/jwt_auth.py +277 -0
- nui_lambda_shared_utils/lambda_helpers.py +84 -0
- nui_lambda_shared_utils/log_processors.py +172 -0
- nui_lambda_shared_utils/powertools_helpers.py +263 -0
- nui_lambda_shared_utils/secrets_helper.py +187 -0
- nui_lambda_shared_utils/slack_client.py +675 -0
- nui_lambda_shared_utils/slack_formatter.py +307 -0
- nui_lambda_shared_utils/slack_setup/__init__.py +14 -0
- nui_lambda_shared_utils/slack_setup/channel_creator.py +295 -0
- nui_lambda_shared_utils/slack_setup/channel_definitions.py +187 -0
- nui_lambda_shared_utils/slack_setup/setup_helpers.py +211 -0
- nui_lambda_shared_utils/timezone.py +117 -0
- nui_lambda_shared_utils/utils.py +291 -0
- nui_python_shared_utils-1.3.0.dist-info/METADATA +470 -0
- nui_python_shared_utils-1.3.0.dist-info/RECORD +28 -0
- nui_python_shared_utils-1.3.0.dist-info/WHEEL +5 -0
- nui_python_shared_utils-1.3.0.dist-info/entry_points.txt +2 -0
- nui_python_shared_utils-1.3.0.dist-info/licenses/LICENSE +21 -0
- nui_python_shared_utils-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for DRY code patterns across the lambda shared utils.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import logging
|
|
8
|
+
import functools
|
|
9
|
+
from typing import Union, List, Optional, Any, Dict
|
|
10
|
+
import boto3
|
|
11
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
12
|
+
|
|
13
|
+
from .config import get_config
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# AWS region resolution constants
|
|
18
|
+
DEFAULT_AWS_REGION = "ap-southeast-2"
|
|
19
|
+
|
|
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:
|
|
26
|
+
"""
|
|
27
|
+
Resolve configuration value with priority: param > env vars > config default.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
param_value: Explicitly provided parameter value
|
|
31
|
+
env_var_names: Environment variable name(s) to check (string or list)
|
|
32
|
+
config_default: Default value from configuration
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Resolved configuration value
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
host = resolve_config_value(
|
|
39
|
+
host_param,
|
|
40
|
+
["ES_HOST", "ELASTICSEARCH_HOST"],
|
|
41
|
+
"localhost:9200"
|
|
42
|
+
)
|
|
43
|
+
"""
|
|
44
|
+
# Parameter takes highest precedence
|
|
45
|
+
if param_value is not None:
|
|
46
|
+
return param_value
|
|
47
|
+
|
|
48
|
+
# Check environment variables
|
|
49
|
+
if isinstance(env_var_names, str):
|
|
50
|
+
env_var_names = [env_var_names]
|
|
51
|
+
|
|
52
|
+
for env_var in env_var_names:
|
|
53
|
+
value = os.environ.get(env_var)
|
|
54
|
+
if value is not None:
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
# Fall back to config default
|
|
58
|
+
return config_default
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_aws_region(explicit_region: Optional[str] = None) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Resolve AWS region with priority: param > env > config > session > default.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
explicit_region: Explicitly provided region
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
AWS region string
|
|
70
|
+
"""
|
|
71
|
+
# Explicit parameter wins
|
|
72
|
+
if explicit_region:
|
|
73
|
+
return explicit_region
|
|
74
|
+
|
|
75
|
+
# Check environment variables
|
|
76
|
+
env_region = resolve_config_value(
|
|
77
|
+
None,
|
|
78
|
+
["AWS_REGION", "AWS_DEFAULT_REGION"],
|
|
79
|
+
None
|
|
80
|
+
)
|
|
81
|
+
if env_region:
|
|
82
|
+
return env_region
|
|
83
|
+
|
|
84
|
+
# Check config
|
|
85
|
+
config = get_config()
|
|
86
|
+
if hasattr(config, 'aws_region') and config.aws_region:
|
|
87
|
+
return config.aws_region
|
|
88
|
+
|
|
89
|
+
# Check boto3 session default
|
|
90
|
+
try:
|
|
91
|
+
session = boto3.session.Session()
|
|
92
|
+
if session.region_name:
|
|
93
|
+
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
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_aws_client(service_name: str, region: Optional[str] = None):
|
|
102
|
+
"""
|
|
103
|
+
Create AWS client with consistent region resolution and error handling.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
service_name: AWS service name (e.g., 'secretsmanager', 'cloudwatch')
|
|
107
|
+
region: Optional explicit region
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
AWS service client
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
NoCredentialsError: When AWS credentials are not configured
|
|
114
|
+
ClientError: When client creation fails
|
|
115
|
+
"""
|
|
116
|
+
resolved_region = resolve_aws_region(region)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
session = boto3.session.Session()
|
|
120
|
+
client = session.client(
|
|
121
|
+
service_name=service_name,
|
|
122
|
+
region_name=resolved_region
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
log.debug(f"Created {service_name} client for region {resolved_region}")
|
|
126
|
+
return client
|
|
127
|
+
|
|
128
|
+
except NoCredentialsError:
|
|
129
|
+
log.error(f"AWS credentials not configured for {service_name} client")
|
|
130
|
+
raise
|
|
131
|
+
except ClientError as e:
|
|
132
|
+
log.error(f"Failed to create {service_name} client: {e}")
|
|
133
|
+
raise
|
|
134
|
+
except Exception as e:
|
|
135
|
+
log.error(f"Unexpected error creating {service_name} client: {e}")
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def handle_client_errors(
|
|
140
|
+
default_return: Any = None,
|
|
141
|
+
log_context: Optional[Dict[str, Any]] = None,
|
|
142
|
+
reraise: bool = False
|
|
143
|
+
):
|
|
144
|
+
"""
|
|
145
|
+
Decorator for standardized client error handling.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
default_return: Value to return on error (if not reraising)
|
|
149
|
+
log_context: Additional context for error logging
|
|
150
|
+
reraise: Whether to re-raise exceptions after logging
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
@handle_client_errors(default_return=[])
|
|
154
|
+
def search_documents(self, query):
|
|
155
|
+
# Implementation that might fail
|
|
156
|
+
return results
|
|
157
|
+
"""
|
|
158
|
+
def decorator(func):
|
|
159
|
+
@functools.wraps(func)
|
|
160
|
+
def wrapper(*args, **kwargs):
|
|
161
|
+
try:
|
|
162
|
+
return func(*args, **kwargs)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
# Build log context
|
|
165
|
+
context = {
|
|
166
|
+
"function": func.__name__,
|
|
167
|
+
"error_type": type(e).__name__,
|
|
168
|
+
"error_message": str(e)
|
|
169
|
+
}
|
|
170
|
+
if log_context:
|
|
171
|
+
context.update(log_context)
|
|
172
|
+
|
|
173
|
+
log.error(
|
|
174
|
+
f"{func.__name__} failed: {e}",
|
|
175
|
+
exc_info=True,
|
|
176
|
+
extra=context
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if reraise:
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
return default_return
|
|
183
|
+
|
|
184
|
+
return wrapper
|
|
185
|
+
return decorator
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def merge_dimensions(base_dimensions: Dict[str, str], additional_dimensions: Optional[Dict[str, str]] = None) -> List[Dict[str, str]]:
|
|
189
|
+
"""
|
|
190
|
+
Merge CloudWatch metric dimensions and format for API.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
base_dimensions: Base dimensions dictionary
|
|
194
|
+
additional_dimensions: Additional dimensions to merge
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of dimension dictionaries formatted for CloudWatch API
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
dimensions = merge_dimensions(
|
|
201
|
+
{"Service": "auth", "Environment": "prod"},
|
|
202
|
+
{"Version": "1.2.3"}
|
|
203
|
+
)
|
|
204
|
+
# Returns: [{"Name": "Service", "Value": "auth"}, ...]
|
|
205
|
+
"""
|
|
206
|
+
all_dimensions = {**base_dimensions}
|
|
207
|
+
if additional_dimensions:
|
|
208
|
+
all_dimensions.update(additional_dimensions)
|
|
209
|
+
|
|
210
|
+
return [
|
|
211
|
+
{"Name": str(key), "Value": str(value)}
|
|
212
|
+
for key, value in all_dimensions.items()
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def validate_required_param(param_value: Any, param_name: str) -> Any:
|
|
217
|
+
"""
|
|
218
|
+
Validate that a required parameter is provided.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
param_value: Parameter value to validate
|
|
222
|
+
param_name: Parameter name for error messages
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The parameter value if valid
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
ValueError: If parameter is None or empty string
|
|
229
|
+
"""
|
|
230
|
+
if param_value is None:
|
|
231
|
+
raise ValueError(f"{param_name} is required")
|
|
232
|
+
|
|
233
|
+
if isinstance(param_value, str) and not param_value.strip():
|
|
234
|
+
raise ValueError(f"{param_name} cannot be empty")
|
|
235
|
+
|
|
236
|
+
return param_value
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def safe_close_connection(connection) -> None:
|
|
240
|
+
"""
|
|
241
|
+
Safely close a database connection with proper error handling.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
connection: Database connection to close
|
|
245
|
+
"""
|
|
246
|
+
if connection and hasattr(connection, "close"):
|
|
247
|
+
# Check if connection is already closed (database-specific checks)
|
|
248
|
+
try:
|
|
249
|
+
# PyMySQL specific checks
|
|
250
|
+
if hasattr(connection, "_closed") and connection._closed:
|
|
251
|
+
return
|
|
252
|
+
if hasattr(connection, "open") and not connection.open:
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Generic close
|
|
256
|
+
connection.close()
|
|
257
|
+
log.debug("Database connection closed successfully")
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
log.debug(f"Error closing connection (non-fatal): {e}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def format_log_context(
|
|
264
|
+
operation: str,
|
|
265
|
+
**context_data
|
|
266
|
+
) -> Dict[str, Any]:
|
|
267
|
+
"""
|
|
268
|
+
Format consistent logging context for operations.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
operation: Operation name
|
|
272
|
+
**context_data: Additional context key-value pairs
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Formatted context dictionary
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
context = format_log_context(
|
|
279
|
+
"database_query",
|
|
280
|
+
table="users",
|
|
281
|
+
query_type="SELECT",
|
|
282
|
+
duration_ms=150
|
|
283
|
+
)
|
|
284
|
+
"""
|
|
285
|
+
context = {
|
|
286
|
+
"operation": operation,
|
|
287
|
+
"timestamp": time.time(),
|
|
288
|
+
}
|
|
289
|
+
context.update(context_data)
|
|
290
|
+
|
|
291
|
+
return context
|