aws-python-helper 0.23.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.
Files changed (37) hide show
  1. aws_python_helper/__init__.py +45 -0
  2. aws_python_helper/api/__init__.py +11 -0
  3. aws_python_helper/api/auth_middleware.py +108 -0
  4. aws_python_helper/api/auth_validators.py +143 -0
  5. aws_python_helper/api/base.py +272 -0
  6. aws_python_helper/api/dispatcher.py +213 -0
  7. aws_python_helper/api/exceptions.py +43 -0
  8. aws_python_helper/api/fetcher.py +210 -0
  9. aws_python_helper/api/handler.py +106 -0
  10. aws_python_helper/database/__init__.py +11 -0
  11. aws_python_helper/database/database_proxy.py +50 -0
  12. aws_python_helper/database/external_database_proxy.py +66 -0
  13. aws_python_helper/database/external_mongo_manager.py +212 -0
  14. aws_python_helper/database/mongo_manager.py +214 -0
  15. aws_python_helper/fargate/__init__.py +9 -0
  16. aws_python_helper/fargate/executor.py +226 -0
  17. aws_python_helper/fargate/fetcher.py +108 -0
  18. aws_python_helper/fargate/handler.py +101 -0
  19. aws_python_helper/fargate/task_base.py +165 -0
  20. aws_python_helper/lambda_standalone/__init__.py +8 -0
  21. aws_python_helper/lambda_standalone/base.py +171 -0
  22. aws_python_helper/lambda_standalone/fetcher.py +122 -0
  23. aws_python_helper/lambda_standalone/handler.py +117 -0
  24. aws_python_helper/sns/__init__.py +6 -0
  25. aws_python_helper/sns/publisher.py +245 -0
  26. aws_python_helper/sqs/__init__.py +10 -0
  27. aws_python_helper/sqs/consumer_base.py +416 -0
  28. aws_python_helper/sqs/fetcher.py +111 -0
  29. aws_python_helper/sqs/handler.py +138 -0
  30. aws_python_helper/utils/__init__.py +9 -0
  31. aws_python_helper/utils/json_encoder.py +108 -0
  32. aws_python_helper/utils/response.py +145 -0
  33. aws_python_helper/utils/serializer.py +103 -0
  34. aws_python_helper-0.23.0.dist-info/METADATA +712 -0
  35. aws_python_helper-0.23.0.dist-info/RECORD +37 -0
  36. aws_python_helper-0.23.0.dist-info/WHEEL +5 -0
  37. aws_python_helper-0.23.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,45 @@
1
+ """
2
+ AWS Python Framework
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ # All classes
8
+ from .api.base import API
9
+ from .sqs.consumer_base import SQSConsumer
10
+ from .database.mongo_manager import MongoManager
11
+ from .database.database_proxy import DatabaseProxy
12
+ from .sns.publisher import SNSPublisher
13
+ from .fargate.task_base import FargateTask
14
+ from .fargate.executor import FargateExecutor
15
+ from .lambda_standalone.base import Lambda
16
+
17
+ # All handlers
18
+ from .fargate.handler import fargate_handler
19
+ from .api.handler import api_handler
20
+ from .sqs.handler import sqs_handler
21
+ from .lambda_standalone.handler import lambda_handler
22
+
23
+ # Utils
24
+ from .utils.json_encoder import MongoJSONEncoder, mongo_json_dumps
25
+ from .utils.serializer import serialize_mongo_types
26
+
27
+
28
+ __all__ = [
29
+ 'API',
30
+ 'SQSConsumer',
31
+ 'MongoManager',
32
+ 'DatabaseProxy',
33
+ 'SNSPublisher',
34
+ 'FargateTask',
35
+ 'FargateExecutor',
36
+ 'Lambda',
37
+ 'fargate_handler',
38
+ 'api_handler',
39
+ 'sqs_handler',
40
+ 'lambda_handler',
41
+ 'MongoJSONEncoder',
42
+ 'mongo_json_dumps',
43
+ 'serialize_mongo_types',
44
+ ]
45
+
@@ -0,0 +1,11 @@
1
+ """
2
+ API Module - Components to handle REST APIs
3
+ """
4
+
5
+ from .base import API
6
+ from .dispatcher import Dispatcher
7
+ from .fetcher import Fetcher
8
+ from .handler import api_handler
9
+
10
+ __all__ = ['API', 'Dispatcher', 'Fetcher', 'api_handler']
11
+
@@ -0,0 +1,108 @@
1
+ """
2
+ Auth Middleware - Handles authentication for API requests
3
+ """
4
+
5
+ from typing import Dict, Any
6
+ import logging
7
+
8
+ from .auth_validators import AuthValidator
9
+ from .exceptions import UnauthorizedError
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AuthMiddleware:
15
+ """
16
+ Middleware to handle authentication for API requests
17
+
18
+ This middleware:
19
+ 1. Extracts token from Authorization header
20
+ 2. Validates token using configured validator
21
+ 3. Injects user information into API instance
22
+ 4. Raises UnauthorizedError if authentication fails
23
+ """
24
+
25
+ def __init__(self, validator: AuthValidator):
26
+ """
27
+ Initialize middleware with an auth validator
28
+
29
+ Args:
30
+ validator: AuthValidator instance to use for token validation
31
+ """
32
+ self.validator = validator
33
+
34
+ def _extract_token(self, headers: Dict[str, str]) -> str:
35
+ """
36
+ Extract token from Authorization header
37
+
38
+ Supports two formats:
39
+ - Authorization: Bearer <token>
40
+ - authorization: Bearer <token> (case insensitive)
41
+
42
+ Args:
43
+ headers: Request headers dictionary
44
+
45
+ Returns:
46
+ The extracted token string
47
+
48
+ Raises:
49
+ UnauthorizedError: If Authorization header is missing or invalid
50
+ """
51
+ # Try to get Authorization header (case-insensitive)
52
+ auth_header = None
53
+ for key, value in headers.items():
54
+ if key.lower() == 'authorization':
55
+ auth_header = value
56
+ break
57
+
58
+ if not auth_header:
59
+ raise UnauthorizedError("Authorization header is required")
60
+
61
+ # Check Bearer format
62
+ if not auth_header.startswith('Bearer '):
63
+ raise UnauthorizedError("Authorization header must use Bearer scheme")
64
+
65
+ # Extract token
66
+ token = auth_header.replace('Bearer ', '').strip()
67
+
68
+ if not token:
69
+ raise UnauthorizedError("Token cannot be empty")
70
+
71
+ return token
72
+
73
+ async def authenticate(self, headers: Dict[str, str], api: Any):
74
+ """
75
+ Authenticate the request and inject user data into API instance
76
+
77
+ This method:
78
+ 1. Extracts token from headers
79
+ 2. Validates token using the configured validator
80
+ 3. Injects authentication data into the API instance
81
+
82
+ Args:
83
+ headers: Request headers dictionary
84
+ api: API instance to inject authentication data into
85
+
86
+ Raises:
87
+ UnauthorizedError: If authentication fails
88
+ """
89
+
90
+ try:
91
+ # 1. Extract token from headers
92
+ token = self._extract_token(headers)
93
+
94
+ # 2. Validate token
95
+ auth_data = await self.validator.validate_token(token)
96
+
97
+ # 3. Inject authentication data into API instance
98
+ api._current_user = auth_data.get('user')
99
+ api._auth_data = auth_data
100
+ api._is_authenticated = True
101
+ except UnauthorizedError:
102
+ # Re-raise UnauthorizedError as is
103
+ raise
104
+
105
+ except Exception as e:
106
+ # Catch any other error and convert to UnauthorizedError
107
+ logger.error(f"Authentication error: {e}", exc_info=True)
108
+ raise UnauthorizedError(f"Authentication failed: {str(e)}")
@@ -0,0 +1,143 @@
1
+ """
2
+ Auth Validators - Validators for different authentication strategies
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, Any, Optional
7
+ import os
8
+ import logging
9
+ from datetime import datetime
10
+ from .exceptions import UnauthorizedError
11
+ from ..database.mongo_manager import MongoManager
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class AuthValidator(ABC):
17
+ """
18
+ Abstract base class for authentication validators
19
+
20
+ Each validator implements a different strategy to validate tokens.
21
+ """
22
+
23
+ @abstractmethod
24
+ async def validate_token(self, token: str) -> Dict[str, Any]:
25
+ """
26
+ Validate a token and return user information
27
+
28
+ Args:
29
+ token: The authentication token to validate
30
+
31
+ Returns:
32
+ Dict with user information: {'user_id': ..., 'user': {...}, ...}
33
+
34
+ Raises:
35
+ UnauthorizedError: If token is invalid
36
+ """
37
+ raise NotImplementedError("Subclasses must implement validate_token()")
38
+
39
+
40
+ class TokenValidator(AuthValidator):
41
+ """
42
+ Validates authentication tokens
43
+
44
+ Simple unified validator that:
45
+ 1. First checks if token matches AUTH_BYPASS_TOKEN (if set)
46
+ 2. If not bypass, searches token in MongoDB 'tokens' collection
47
+ 3. Validates token is not expired and is active
48
+ 4. Loads complete user data from 'users' collection
49
+ 5. Updates last_used_at timestamp for auditing
50
+ """
51
+
52
+ async def validate_token(self, token: str) -> Dict[str, Any]:
53
+ """
54
+ Validate token against MongoDB or bypass token
55
+
56
+ Args:
57
+ token: The authentication token to validate
58
+
59
+ Returns:
60
+ Dict with user_id, user object, and token_data
61
+
62
+ Raises:
63
+ UnauthorizedError: If token is invalid or expired
64
+ """
65
+
66
+ # 1. Check bypass token first (for development/testing)
67
+ bypass_token = os.getenv('AUTH_BYPASS_TOKEN')
68
+ if bypass_token and token == bypass_token:
69
+ logger.info("Bypass token used - skipping DB validation")
70
+ return {
71
+ 'user_id': 'bypass',
72
+ 'user': {
73
+ 'email': 'bypass@system',
74
+ 'role': 'admin',
75
+ 'name': 'Bypass User',
76
+ '_id': 'bypass'
77
+ },
78
+ 'is_bypass': True,
79
+ 'token_data': None
80
+ }
81
+
82
+ # 2. Get database name from environment
83
+ db_name = os.getenv('AUTH_DB_NAME') or 'core'
84
+ if not db_name:
85
+ raise ValueError(
86
+ "AUTH_DB_NAME environment variable not set. "
87
+ "This is required for MongoDB authentication."
88
+ )
89
+
90
+ # 3. Get database reference
91
+ if not MongoManager.is_initialized():
92
+ raise RuntimeError("MongoManager not initialized")
93
+
94
+ db = MongoManager.get_database(db_name)
95
+
96
+ # 4. Search for token in database
97
+ token_doc = await db.tokens.find_one({
98
+ 'token': token,
99
+ 'is_active': True
100
+ })
101
+
102
+ if not token_doc:
103
+ logger.warning(f"Token not found or inactive")
104
+ raise UnauthorizedError("Invalid or revoked token")
105
+
106
+ # 5. Check if token is expired
107
+ if 'expires_at' in token_doc:
108
+ if token_doc['expires_at'] < datetime.utcnow():
109
+ logger.warning(f"Token expired at {token_doc['expires_at']}")
110
+ raise UnauthorizedError("Token has expired")
111
+
112
+ # 6. Load user from database
113
+ user = await db.users.find_one({'_id': token_doc['user_id']})
114
+
115
+ if not user:
116
+ logger.error(f"User {token_doc['user_id']} not found for valid token")
117
+ raise UnauthorizedError("User not found")
118
+
119
+ # 7. Check if user is active
120
+ if 'is_active' in user and not user['is_active']:
121
+ logger.warning(f"User {user['_id']} is inactive")
122
+ raise UnauthorizedError("User account is inactive")
123
+
124
+ # 8. Update last_used_at for auditing
125
+ try:
126
+ await db.tokens.update_one(
127
+ {'_id': token_doc['_id']},
128
+ {'$set': {'last_used_at': datetime.utcnow()}}
129
+ )
130
+ except Exception as e:
131
+ # Non-critical, just log
132
+ logger.warning(f"Failed to update last_used_at: {e}")
133
+
134
+ # 9. Return user data (remove password from response)
135
+ user_data = dict(user)
136
+ user_data.pop('password', None) # Never return password
137
+
138
+ return {
139
+ 'user_id': str(token_doc['user_id']),
140
+ 'user': user_data,
141
+ 'token_data': token_doc,
142
+ 'is_bypass': False
143
+ }
@@ -0,0 +1,272 @@
1
+ """
2
+ API Base Class - Base class for all REST APIs
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ import logging
7
+ from typing import Dict, Any, Optional, List
8
+ from ..database.mongo_manager import MongoManager
9
+ from ..database.database_proxy import DatabaseProxy
10
+ from ..database.external_mongo_manager import ExternalMongoManager
11
+ from ..database.external_database_proxy import ExternalDatabaseProxy
12
+
13
+
14
+ class API(ABC):
15
+ """
16
+ Base class for all REST APIs
17
+
18
+ Base class that provides the basic structure
19
+ to handle requests and responses from API Gateway.
20
+ """
21
+
22
+ def __init__(self):
23
+ # Request properties
24
+ self._endpoint: str = ""
25
+ self._http_method: str = ""
26
+ self._data: Dict[str, Any] = {}
27
+ self._headers: Dict[str, str] = {}
28
+ self._path_parameters: List[str] = []
29
+ self._query_parameters: Dict[str, Any] = {}
30
+
31
+ # Response properties
32
+ self._response_code: Optional[int] = None
33
+ self._response_body: Any = None
34
+ self._response_headers: Dict[str, str] = {}
35
+
36
+ # Database proxy
37
+ self._db = None
38
+ self._external_db = None
39
+
40
+ # Authentication properties
41
+ self._current_user: Optional[Dict[str, Any]] = None
42
+ self._auth_data: Optional[Dict[str, Any]] = None
43
+ self._is_authenticated: bool = False
44
+ self.logger = logging.getLogger(self.__class__.__name__)
45
+
46
+ @property
47
+ def endpoint(self) -> str:
48
+ """Endpoint of the request"""
49
+ return self._endpoint
50
+
51
+ @endpoint.setter
52
+ def endpoint(self, value: str):
53
+ self._endpoint = value
54
+
55
+ @property
56
+ def http_method(self) -> str:
57
+ """HTTP method (get, post, put, delete, etc.)"""
58
+ return self._http_method
59
+
60
+ @http_method.setter
61
+ def http_method(self, value: str):
62
+ self._http_method = value
63
+
64
+ @property
65
+ def data(self) -> Dict[str, Any]:
66
+ """Data of the request (body for POST/PUT, query params for GET)"""
67
+ return self._data
68
+
69
+ @data.setter
70
+ def data(self, value: Dict[str, Any]):
71
+ self._data = value or {}
72
+
73
+ @property
74
+ def headers(self) -> Dict[str, str]:
75
+ """Headers of the request"""
76
+ return self._headers
77
+
78
+ @headers.setter
79
+ def headers(self, value: Dict[str, str]):
80
+ self._headers = value or {}
81
+
82
+ @property
83
+ def path_parameters(self) -> List[str]:
84
+ """Path parameters extracted from the URL"""
85
+ return self._path_parameters
86
+
87
+ @path_parameters.setter
88
+ def path_parameters(self, value: List[str]):
89
+ self._path_parameters = value or []
90
+
91
+ @property
92
+ def query_parameters(self) -> Dict[str, Any]:
93
+ """Query parameters of the URL"""
94
+ return self._query_parameters
95
+
96
+ @query_parameters.setter
97
+ def query_parameters(self, value: Dict[str, Any]):
98
+ self._query_parameters = value or {}
99
+
100
+ @property
101
+ def db(self):
102
+ """
103
+ Access to MongoDB databases (main cluster)
104
+ """
105
+ if self._db is None:
106
+ self._db = DatabaseProxy(MongoManager)
107
+ return self._db
108
+
109
+ @property
110
+ def external_db(self):
111
+ """
112
+ Access to external MongoDB clusters
113
+
114
+ Returns None if EXTERNAL_MONGODB_CONNECTIONS environment variable is not set.
115
+
116
+ Usage:
117
+ if self.external_db:
118
+ result = await self.external_db.ClusterDockets.smart_data.addresses.find_one({...})
119
+ await self.external_db.ClusterDockets.core.users.insert_one({...})
120
+
121
+ Returns:
122
+ ExternalDatabaseProxy instance for accessing external clusters, or None if not configured
123
+ """
124
+ if self._external_db is None:
125
+ # Initialize external connections if not already done
126
+ if not ExternalMongoManager.is_initialized():
127
+ has_connections = ExternalMongoManager.initialize()
128
+ if not has_connections:
129
+ # No external connections available, return None
130
+ return None
131
+ else:
132
+ # Check if there are any connections available
133
+ if len(ExternalMongoManager.get_available_clusters()) == 0:
134
+ return None
135
+
136
+ self._external_db = ExternalDatabaseProxy()
137
+ return self._external_db
138
+
139
+ @property
140
+ def current_user(self) -> Optional[Dict[str, Any]]:
141
+ """
142
+ Current authenticated user or None if not authenticated
143
+
144
+ This property is populated by the authentication middleware
145
+ when REQUIRE_AUTH=true.
146
+
147
+ Returns:
148
+ Dict with user data (email, role, name, etc.) or None
149
+
150
+ Example:
151
+ if self.is_authenticated:
152
+ user_email = self.current_user['email']
153
+ user_role = self.current_user.get('role', 'user')
154
+ """
155
+ return self._current_user
156
+
157
+ @property
158
+ def is_authenticated(self) -> bool:
159
+ """
160
+ True if the request has been authenticated
161
+
162
+ This is set to True by the authentication middleware
163
+ when a valid token is provided.
164
+
165
+ Returns:
166
+ True if authenticated, False otherwise
167
+
168
+ Example:
169
+ if not self.is_authenticated:
170
+ raise ValueError("This operation requires authentication")
171
+ """
172
+ return self._is_authenticated
173
+
174
+ @property
175
+ def auth_data(self) -> Optional[Dict[str, Any]]:
176
+ """
177
+ Complete authentication data from the middleware
178
+
179
+ This includes user data, token data, and other metadata
180
+ provided by the authentication validator.
181
+
182
+ Returns:
183
+ Dict with authentication data or None
184
+ """
185
+ return self._auth_data
186
+
187
+ def set_code(self, code: int):
188
+ """
189
+ Set the HTTP response code
190
+
191
+ Args:
192
+ code: HTTP code (200, 404, 500, etc.)
193
+
194
+ Returns:
195
+ self to chain calls
196
+ """
197
+ self._response_code = code
198
+ return self
199
+
200
+ def set_body(self, body: Any):
201
+ """
202
+ Set the body of the response
203
+
204
+ Args:
205
+ body: Any serializable object to JSON
206
+
207
+ Returns:
208
+ self to chain calls
209
+ """
210
+ self._response_body = body
211
+ return self
212
+
213
+ def set_header(self, key: str, value: str):
214
+ """
215
+ Add a header to the response
216
+
217
+ Args:
218
+ key: Name of the header
219
+ value: Value of the header
220
+
221
+ Returns:
222
+ self to chain calls
223
+ """
224
+ self._response_headers[key] = value
225
+ return self
226
+
227
+ def set_headers(self, headers: Dict[str, str]):
228
+ """
229
+ Set multiple headers
230
+
231
+ Args:
232
+ headers: Dictionary of headers
233
+
234
+ Returns:
235
+ self to chain calls
236
+ """
237
+ self._response_headers.update(headers)
238
+ return self
239
+
240
+ @property
241
+ def response(self) -> Dict[str, Any]:
242
+ """
243
+ Return the complete response object
244
+
245
+ Returns:
246
+ Dict with code, body and headers
247
+ """
248
+ return {
249
+ 'code': self._response_code,
250
+ 'body': self._response_body,
251
+ 'headers': self._response_headers
252
+ }
253
+
254
+ async def validate(self):
255
+ """
256
+ Hook for data validation
257
+
258
+ Override this method to implement custom validations.
259
+ If the validation fails, raise an exception.
260
+ """
261
+ pass
262
+
263
+ @abstractmethod
264
+ async def process(self):
265
+ """
266
+ Main method that must be implemented by each class
267
+
268
+ This is the method where you implement the logic of your API.
269
+ You must use set_body() and optionally set_code() and set_header().
270
+ """
271
+ raise NotImplementedError("You must implement the process() method")
272
+