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.
@@ -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