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,252 @@
1
+ """
2
+ Enterprise-grade utilities for AWS Lambda functions with Slack, Elasticsearch, and monitoring integrations.
3
+ """
4
+
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
+ )
16
+
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
+ )
26
+
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
+ )
35
+
36
+ # Base client architecture
37
+ from .base_client import BaseClient, ServiceHealthMixin, RetryableOperationMixin
38
+
39
+ # Client implementations - only fail if actually used
40
+ try:
41
+ from .slack_client import SlackClient
42
+ except ImportError:
43
+ SlackClient = None # type: ignore
44
+
45
+ try:
46
+ from .es_client import ElasticsearchClient
47
+ except ImportError:
48
+ ElasticsearchClient = None # type: ignore
49
+
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
+
57
+ from .timezone import nz_time, format_nz_time
58
+
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
+
74
+ # ES query builder - optional import
75
+ try:
76
+ from .es_query_builder import (
77
+ ESQueryBuilder,
78
+ build_error_rate_query,
79
+ build_top_errors_query,
80
+ build_response_time_query,
81
+ build_service_volume_query,
82
+ build_user_activity_query,
83
+ build_pattern_detection_query,
84
+ build_tender_participant_query,
85
+ )
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:
119
+ 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
+ from .jwt_auth import (
138
+ validate_jwt,
139
+ require_auth,
140
+ check_auth,
141
+ get_jwt_public_key,
142
+ JWTValidationError,
143
+ AuthenticationError,
144
+ )
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
+ from . import slack_setup
156
+ except ImportError:
157
+ slack_setup = None # type: ignore
158
+
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
+ ]
@@ -0,0 +1,323 @@
1
+ """
2
+ Base client class providing common functionality for AWS service clients.
3
+ """
4
+
5
+ import logging
6
+ from typing import Optional, Dict, Any
7
+ from abc import ABC, abstractmethod
8
+
9
+ from .config import get_config
10
+ from .secrets_helper import get_secret
11
+ from .utils import resolve_config_value, validate_required_param, handle_client_errors
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ class BaseClient(ABC):
17
+ """
18
+ Base class for AWS service clients providing standardized:
19
+ - Credential resolution and management
20
+ - Configuration integration
21
+ - Error handling patterns
22
+ - Logging context
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ secret_name: Optional[str] = None,
28
+ config_key_prefix: Optional[str] = None,
29
+ credentials: Optional[Dict[str, Any]] = None,
30
+ **kwargs
31
+ ):
32
+ """
33
+ Initialize base client with standardized credential and config resolution.
34
+
35
+ Credential resolution precedence:
36
+ 1. Explicit ``credentials`` dict — skip everything else
37
+ 2. Environment variables (per-client patterns) — skip Secrets Manager
38
+ 3. Secrets Manager (existing behavior) — unchanged
39
+
40
+ Args:
41
+ secret_name: Override secret name for credentials
42
+ config_key_prefix: Prefix for config keys (e.g., 'slack', 'es', 'db')
43
+ credentials: Direct credentials dict, bypasses Secrets Manager entirely
44
+ **kwargs: Additional client-specific parameters
45
+ """
46
+ self.config = get_config()
47
+ self.config_key_prefix = config_key_prefix or self._get_default_config_prefix()
48
+
49
+ # Resolve and store credentials
50
+ self.credentials = self._resolve_credentials(secret_name, credentials)
51
+
52
+ # Store additional configuration
53
+ self.client_config = kwargs
54
+
55
+ # Initialize service-specific client
56
+ self._service_client = self._create_service_client()
57
+
58
+ log.info(
59
+ f"Initialized {self.__class__.__name__}",
60
+ extra={
61
+ "client_type": self.__class__.__name__,
62
+ "config_prefix": self.config_key_prefix,
63
+ "has_credentials": bool(self.credentials)
64
+ }
65
+ )
66
+
67
+ @abstractmethod
68
+ def _get_default_config_prefix(self) -> str:
69
+ """Return the default configuration key prefix for this client type."""
70
+ pass
71
+
72
+ @abstractmethod
73
+ def _create_service_client(self) -> Any:
74
+ """Create and return the underlying service client (e.g., WebClient, Elasticsearch)."""
75
+ pass
76
+
77
+ @abstractmethod
78
+ def _get_default_secret_name(self) -> str:
79
+ """Return the default secret name for this client type."""
80
+ pass
81
+
82
+ def _resolve_credentials_from_env(self) -> Optional[Dict[str, Any]]:
83
+ """
84
+ Resolve credentials from environment variables.
85
+
86
+ Subclasses override this to check client-specific env vars
87
+ (e.g. SLACK_BOT_TOKEN, ES_PASSWORD). Return a credentials dict
88
+ if the required env vars are present, or ``None`` to fall through
89
+ to Secrets Manager.
90
+
91
+ Returns:
92
+ Credentials dict or None
93
+ """
94
+ return None
95
+
96
+ def _resolve_credentials(
97
+ self,
98
+ secret_name: Optional[str],
99
+ explicit_credentials: Optional[Dict[str, Any]] = None,
100
+ ) -> Dict[str, Any]:
101
+ """
102
+ Resolve credentials using standardized precedence.
103
+
104
+ Precedence:
105
+ 1. Explicit credentials dict (constructor param)
106
+ 2. Environment variables (per-client ``_resolve_credentials_from_env``)
107
+ 3. AWS Secrets Manager (via ``_fetch_credentials_from_sm``)
108
+
109
+ Args:
110
+ secret_name: Optional override for secret name
111
+ explicit_credentials: Direct credentials dict from constructor
112
+
113
+ Returns:
114
+ Dictionary containing resolved credentials
115
+ """
116
+ # 1. Explicit credentials dict — skip everything
117
+ if explicit_credentials is not None:
118
+ log.debug("Using explicitly provided credentials")
119
+ return explicit_credentials
120
+
121
+ # 2. Environment variables — skip Secrets Manager
122
+ env_credentials = self._resolve_credentials_from_env()
123
+ if env_credentials is not None:
124
+ log.debug("Using credentials from environment variables")
125
+ return env_credentials
126
+
127
+ # 3. Secrets Manager
128
+ return self._fetch_credentials_from_sm(secret_name)
129
+
130
+ def _resolve_secret_name(self, secret_name: Optional[str]) -> str:
131
+ """
132
+ Resolve the Secrets Manager secret name using standard precedence:
133
+ explicit param > env var > config default.
134
+
135
+ Args:
136
+ secret_name: Optional explicit override
137
+
138
+ Returns:
139
+ Resolved secret name string
140
+ """
141
+ resolved = resolve_config_value(
142
+ secret_name,
143
+ [
144
+ f"{self.config_key_prefix.upper()}_CREDENTIALS_SECRET",
145
+ f"{self.config_key_prefix.upper()}CREDENTIALS_SECRET" # Alternative format
146
+ ],
147
+ getattr(self.config, f"{self.config_key_prefix}_credentials_secret", self._get_default_secret_name())
148
+ )
149
+ validate_required_param(resolved, "secret_name")
150
+ return resolved
151
+
152
+ def _fetch_credentials_from_sm(self, secret_name: Optional[str]) -> Dict[str, Any]:
153
+ """
154
+ Fetch credentials from AWS Secrets Manager.
155
+
156
+ Subclasses override this to customize the SM fetch (e.g. DB clients
157
+ use ``get_database_credentials`` for field normalization).
158
+
159
+ Args:
160
+ secret_name: Optional override for secret name
161
+
162
+ Returns:
163
+ Dictionary containing resolved credentials
164
+ """
165
+ resolved_secret_name = self._resolve_secret_name(secret_name)
166
+
167
+ try:
168
+ credentials = get_secret(resolved_secret_name)
169
+ log.debug(f"Retrieved credentials from secret: {resolved_secret_name}")
170
+ return credentials
171
+ except Exception as e:
172
+ log.error(
173
+ f"Failed to retrieve credentials from secret: {resolved_secret_name}",
174
+ extra={"secret_name": resolved_secret_name, "error": str(e)}
175
+ )
176
+ raise
177
+
178
+ def _get_config_value(self, key: str, env_vars: Optional[list] = None, default: Any = None) -> Any:
179
+ """
180
+ Get configuration value with standardized precedence.
181
+
182
+ Args:
183
+ key: Configuration key (without prefix)
184
+ env_vars: Environment variable names to check
185
+ default: Default value
186
+
187
+ Returns:
188
+ Resolved configuration value
189
+ """
190
+ # Get from client config (constructor kwargs) first
191
+ if key in self.client_config:
192
+ return self.client_config[key]
193
+
194
+ # Build full config key
195
+ config_key = f"{self.config_key_prefix}_{key}"
196
+ config_value = getattr(self.config, config_key, default)
197
+
198
+ # Use utility for full resolution
199
+ return resolve_config_value(
200
+ None, # No explicit parameter (already checked client_config)
201
+ env_vars or [],
202
+ config_value
203
+ )
204
+
205
+ @handle_client_errors(reraise=True)
206
+ def _execute_with_error_handling(self, operation_name: str, operation_func, **log_context):
207
+ """
208
+ Execute an operation with standardized error handling and logging.
209
+
210
+ Args:
211
+ operation_name: Name of the operation for logging
212
+ operation_func: Function to execute
213
+ **log_context: Additional logging context
214
+
215
+ Returns:
216
+ Result of operation_func
217
+ """
218
+ context = {
219
+ "client_type": self.__class__.__name__,
220
+ "operation": operation_name,
221
+ **log_context
222
+ }
223
+
224
+ log.debug(f"Executing {operation_name}", extra=context)
225
+
226
+ try:
227
+ result = operation_func()
228
+ log.debug(f"Successfully completed {operation_name}", extra=context)
229
+ return result
230
+ except Exception as e:
231
+ context["error_type"] = type(e).__name__
232
+ context["error_message"] = str(e)
233
+ log.error(f"Failed to execute {operation_name}: {e}", extra=context)
234
+ raise
235
+
236
+ def get_client_info(self) -> Dict[str, Any]:
237
+ """
238
+ Get information about this client instance.
239
+
240
+ Returns:
241
+ Dictionary with client information
242
+ """
243
+ return {
244
+ "client_type": self.__class__.__name__,
245
+ "config_prefix": self.config_key_prefix,
246
+ "has_credentials": bool(self.credentials),
247
+ "client_config": {k: v for k, v in self.client_config.items() if not k.startswith('_')},
248
+ }
249
+
250
+
251
+ class ServiceHealthMixin:
252
+ """
253
+ Mixin providing common health check functionality for service clients.
254
+ """
255
+
256
+ def health_check(self) -> Dict[str, Any]:
257
+ """
258
+ Perform a basic health check for the service.
259
+
260
+ Returns:
261
+ Dictionary with health status information
262
+ """
263
+ try:
264
+ self._perform_health_check()
265
+ return {
266
+ "status": "healthy",
267
+ "client_type": self.__class__.__name__,
268
+ "timestamp": log.time.time() if hasattr(log, 'time') else None
269
+ }
270
+ except Exception as e:
271
+ return {
272
+ "status": "unhealthy",
273
+ "client_type": self.__class__.__name__,
274
+ "error": str(e),
275
+ "error_type": type(e).__name__,
276
+ "timestamp": log.time.time() if hasattr(log, 'time') else None
277
+ }
278
+
279
+ @abstractmethod
280
+ def _perform_health_check(self):
281
+ """Perform service-specific health check. Should raise exception if unhealthy."""
282
+ pass
283
+
284
+
285
+ class RetryableOperationMixin:
286
+ """
287
+ Mixin providing retry functionality for operations.
288
+ """
289
+
290
+ def execute_with_retry(
291
+ self,
292
+ operation_func,
293
+ operation_name: str,
294
+ max_attempts: int = 3,
295
+ **retry_kwargs
296
+ ):
297
+ """
298
+ Execute operation with retry logic.
299
+
300
+ Args:
301
+ operation_func: Function to execute
302
+ operation_name: Name for logging
303
+ max_attempts: Maximum retry attempts
304
+ **retry_kwargs: Additional retry configuration
305
+
306
+ Returns:
307
+ Result of operation_func
308
+ """
309
+ from .error_handler import with_retry
310
+
311
+ # Apply retry decorator dynamically
312
+ retried_operation = with_retry(
313
+ max_attempts=max_attempts,
314
+ **retry_kwargs
315
+ )(operation_func)
316
+
317
+ executor = getattr(self, "_execute_with_error_handling", None)
318
+ if executor is None:
319
+ raise AttributeError(
320
+ f"{self.__class__.__name__} must implement _execute_with_error_handling "
321
+ "to use RetryableOperationMixin"
322
+ )
323
+ return executor(operation_name, retried_operation)