geek-cafe-saas-sdk 0.7.4__py3-none-any.whl → 0.8.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.
Potentially problematic release.
This version of geek-cafe-saas-sdk might be problematic. Click here for more details.
- geek_cafe_saas_sdk/__init__.py +1 -1
- geek_cafe_saas_sdk/core/anonymous_context.py +321 -0
- geek_cafe_saas_sdk/core/request_context.py +184 -0
- geek_cafe_saas_sdk/decorators/__init__.py +1 -1
- geek_cafe_saas_sdk/decorators/auth.py +6 -6
- geek_cafe_saas_sdk/decorators/core.py +44 -44
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +1 -3
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +1 -3
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +15 -3
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +1 -4
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +0 -2
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +1 -3
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +3 -3
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +1 -2
- geek_cafe_saas_sdk/domains/events/services/event_service.py +6 -4
- geek_cafe_saas_sdk/domains/files/handlers/README.md +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/models/file.py +16 -6
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +34 -9
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +38 -3
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +33 -36
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +3 -2
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +35 -2
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +20 -3
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +1 -3
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +1 -2
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +1 -2
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +2 -2
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +0 -2
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +3 -3
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +3 -3
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +1 -5
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +1 -5
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +10 -3
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +40 -6
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +157 -12
- geek_cafe_saas_sdk/middleware/authorization.py +1 -1
- geek_cafe_saas_sdk/middleware/cors.py +8 -8
- geek_cafe_saas_sdk/services/database_service.py +76 -5
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/METADATA +16 -16
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/RECORD +131 -129
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/WHEEL +0 -0
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,7 +9,7 @@ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
|
9
9
|
|
|
10
10
|
tenant_service_pool = ServicePool(TenantService)
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def lambda_handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
13
|
"""
|
|
14
14
|
Lambda handler for retrieving a tenant by ID.
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
|
9
9
|
|
|
10
10
|
tenant_service_pool = ServicePool(TenantService)
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def lambda_handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
13
|
"""
|
|
14
14
|
Lambda handler for retrieving the authenticated user's tenant.
|
|
15
15
|
|
|
@@ -10,7 +10,7 @@ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
|
10
10
|
|
|
11
11
|
tenant_service_pool = ServicePool(TenantService)
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def lambda_handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
14
14
|
"""
|
|
15
15
|
Lambda handler for tenant signup (creates tenant + primary admin user).
|
|
16
16
|
|
|
@@ -10,7 +10,7 @@ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
|
10
10
|
|
|
11
11
|
tenant_service_pool = ServicePool(TenantService)
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def lambda_handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
14
14
|
"""
|
|
15
15
|
Lambda handler for updating a tenant.
|
|
16
16
|
|
|
@@ -38,11 +38,11 @@ class SubscriptionService(DatabaseService[Subscription]):
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None,
|
|
41
|
-
tenant_service: TenantService = None):
|
|
42
|
-
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
41
|
+
tenant_service: TenantService = None, request_context: Optional[Dict[str, str]] = None):
|
|
42
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name, request_context=request_context)
|
|
43
43
|
# Tenant service for updating tenant plan_tier
|
|
44
44
|
self.tenant_service = tenant_service or TenantService(
|
|
45
|
-
dynamodb=dynamodb, table_name=table_name
|
|
45
|
+
dynamodb=dynamodb, table_name=table_name, request_context=request_context
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
def create(self, tenant_id: str, user_id: str, payload: Dict[str, Any]) -> ServiceResult[Subscription]:
|
|
@@ -29,11 +29,11 @@ class TenantService(DatabaseService[Tenant]):
|
|
|
29
29
|
|
|
30
30
|
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None,
|
|
31
31
|
user_service: UserService = None, cognito_utility: CognitoUtility = None,
|
|
32
|
-
user_pool_id: str = None, enable_cognito: bool = True):
|
|
33
|
-
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
32
|
+
user_pool_id: str = None, enable_cognito: bool = True, request_context: Optional[Dict[str, str]] = None):
|
|
33
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name, request_context=request_context)
|
|
34
34
|
# User service for creating primary users
|
|
35
35
|
self.user_service = user_service or UserService(
|
|
36
|
-
dynamodb=dynamodb, table_name=table_name
|
|
36
|
+
dynamodb=dynamodb, table_name=table_name, request_context=request_context
|
|
37
37
|
)
|
|
38
38
|
# Cognito integration (optional for testing)
|
|
39
39
|
self.cognito_utility = cognito_utility
|
|
@@ -9,7 +9,7 @@ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
|
9
9
|
|
|
10
10
|
vote_service_pool = ServicePool(VoteService)
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def lambda_handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
13
|
"""
|
|
14
14
|
Lambda handler for deleting a vote by its ID.
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
|
9
9
|
|
|
10
10
|
vote_service_pool = ServicePool(VoteService)
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def lambda_handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
13
|
"""
|
|
14
14
|
Lambda handler for retrieving a single vote by its ID.
|
|
15
15
|
|
|
@@ -9,7 +9,7 @@ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
|
9
9
|
|
|
10
10
|
vote_service_pool = ServicePool(VoteService)
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def lambda_handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
13
|
"""
|
|
14
14
|
Lambda handler for listing votes with optional filters.
|
|
15
15
|
|
|
@@ -10,7 +10,7 @@ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
|
10
10
|
|
|
11
11
|
vote_service_pool = ServicePool(VoteService)
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def lambda_handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
14
14
|
"""
|
|
15
15
|
Lambda handler for updating an existing vote.
|
|
16
16
|
|
|
@@ -11,9 +11,6 @@ from geek_cafe_saas_sdk.domains.voting.models import Vote
|
|
|
11
11
|
class VoteService(DatabaseService[Vote]):
|
|
12
12
|
"""Service for Vote database operations."""
|
|
13
13
|
|
|
14
|
-
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
15
|
-
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
16
|
-
|
|
17
14
|
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[Vote]:
|
|
18
15
|
"""Create or update (upsert) a vote for a target by a user."""
|
|
19
16
|
try:
|
|
@@ -211,10 +208,9 @@ class VoteService(DatabaseService[Vote]):
|
|
|
211
208
|
setattr(vote, field, value)
|
|
212
209
|
|
|
213
210
|
# Update metadata
|
|
214
|
-
vote.updated_by_id = user_id
|
|
215
211
|
vote.prep_for_save() # Updates timestamp
|
|
216
212
|
|
|
217
|
-
# Save updated vote
|
|
213
|
+
# Save updated vote (_save_model automatically populates updated_by_id from request_context)
|
|
218
214
|
return self._save_model(vote)
|
|
219
215
|
|
|
220
216
|
except Exception as e:
|
|
@@ -11,9 +11,6 @@ from geek_cafe_saas_sdk.domains.voting.models import VoteSummary
|
|
|
11
11
|
class VoteSummaryService(DatabaseService[VoteSummary]):
|
|
12
12
|
"""Service for VoteSummary database operations."""
|
|
13
13
|
|
|
14
|
-
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
15
|
-
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
16
|
-
|
|
17
14
|
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[VoteSummary]:
|
|
18
15
|
"""Create or update (upsert) a vote summary for a target."""
|
|
19
16
|
try:
|
|
@@ -155,10 +152,9 @@ class VoteSummaryService(DatabaseService[VoteSummary]):
|
|
|
155
152
|
setattr(summary, field, value)
|
|
156
153
|
|
|
157
154
|
# Update metadata
|
|
158
|
-
summary.updated_by_id = user_id
|
|
159
155
|
summary.prep_for_save() # Updates timestamp
|
|
160
156
|
|
|
161
|
-
# Save updated summary
|
|
157
|
+
# Save updated summary (_save_model automatically populates updated_by_id from request_context)
|
|
162
158
|
return self._save_model(summary)
|
|
163
159
|
|
|
164
160
|
except Exception as e:
|
|
@@ -4,6 +4,7 @@ from typing import Dict, Any, Optional, List
|
|
|
4
4
|
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
5
5
|
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
6
6
|
from geek_cafe_saas_sdk.core.error_codes import ErrorCode
|
|
7
|
+
from geek_cafe_saas_sdk.core.request_context import RequestContext
|
|
7
8
|
from .vote_service import VoteService
|
|
8
9
|
from .vote_summary_service import VoteSummaryService
|
|
9
10
|
from geek_cafe_saas_sdk.domains.voting.models import Vote, VoteSummary
|
|
@@ -17,9 +18,15 @@ logger = Logger()
|
|
|
17
18
|
class VoteTallyService:
|
|
18
19
|
"""Service for tallying votes and updating vote summaries."""
|
|
19
20
|
|
|
20
|
-
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None, request_context: RequestContext):
|
|
22
|
+
"""
|
|
23
|
+
Initialize tally service with child services.
|
|
24
|
+
|
|
25
|
+
NOTE: This service keeps custom __init__ because it creates child services.
|
|
26
|
+
Simple services inherit DatabaseService.__init__ directly.
|
|
27
|
+
"""
|
|
28
|
+
self.vote_service = VoteService(dynamodb=dynamodb, table_name=table_name, request_context=request_context)
|
|
29
|
+
self.vote_summary_service = VoteSummaryService(dynamodb=dynamodb, table_name=table_name, request_context=request_context)
|
|
23
30
|
self.page_size = 100 # Configurable page size for pagination
|
|
24
31
|
|
|
25
32
|
# Pagination monitoring configuration from environment variables
|
|
@@ -6,6 +6,7 @@ request/response handling, error management, and service injection.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import time
|
|
9
10
|
from typing import Dict, Any, Callable, Optional, Type, TypeVar
|
|
10
11
|
from aws_lambda_powertools import Logger
|
|
11
12
|
|
|
@@ -60,19 +61,33 @@ class BaseLambdaHandler:
|
|
|
60
61
|
# Initialize service pool if a class is provided
|
|
61
62
|
self._service_pool = ServicePool(service_class, **self.service_kwargs) if service_class else None
|
|
62
63
|
|
|
63
|
-
def _get_service(self, injected_service: Optional[T]) -> Optional[T]:
|
|
64
|
+
def _get_service(self, injected_service: Optional[T], request_context: Optional[Any] = None) -> Optional[T]:
|
|
64
65
|
"""
|
|
65
|
-
Get service instance (injected or from pool).
|
|
66
|
+
Get service instance (injected or from pool) with FRESH request_context.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
injected_service: Injected service for testing
|
|
70
|
+
request_context: Fresh RequestContext for this invocation
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Service instance with refreshed security context
|
|
66
74
|
"""
|
|
67
75
|
if injected_service:
|
|
76
|
+
# Testing: Refresh context on injected service too
|
|
77
|
+
if request_context is not None and hasattr(injected_service, '_request_context'):
|
|
78
|
+
injected_service._request_context = request_context
|
|
68
79
|
return injected_service
|
|
69
80
|
|
|
70
81
|
if self._service_pool:
|
|
71
|
-
|
|
82
|
+
# Production: Get from pool with fresh context
|
|
83
|
+
return self._service_pool.get(request_context)
|
|
72
84
|
|
|
73
85
|
# Fallback for direct instantiation if pooling is not used (rare)
|
|
74
86
|
if self.service_class:
|
|
75
|
-
|
|
87
|
+
kwargs = {**self.service_kwargs}
|
|
88
|
+
if request_context is not None:
|
|
89
|
+
kwargs['request_context'] = request_context
|
|
90
|
+
return self.service_class(**kwargs)
|
|
76
91
|
|
|
77
92
|
return None
|
|
78
93
|
|
|
@@ -86,6 +101,8 @@ class BaseLambdaHandler:
|
|
|
86
101
|
"""
|
|
87
102
|
Execute the Lambda handler with the given business logic.
|
|
88
103
|
|
|
104
|
+
SECURITY: Creates fresh RequestContext at start, clears it at end.
|
|
105
|
+
|
|
89
106
|
Args:
|
|
90
107
|
event: Lambda event dictionary
|
|
91
108
|
context: Lambda context object
|
|
@@ -95,6 +112,8 @@ class BaseLambdaHandler:
|
|
|
95
112
|
Returns:
|
|
96
113
|
Lambda response dictionary
|
|
97
114
|
"""
|
|
115
|
+
service = None # Track service for cleanup
|
|
116
|
+
start_time = time.time() # Track execution time for telemetry
|
|
98
117
|
try:
|
|
99
118
|
# Log event payload if enabled (sanitized for security)
|
|
100
119
|
if EnvironmentVariables.should_log_lambda_events():
|
|
@@ -152,8 +171,12 @@ class BaseLambdaHandler:
|
|
|
152
171
|
# Extract user context from authorizer claims
|
|
153
172
|
user_context = extract_user_context(event)
|
|
154
173
|
|
|
155
|
-
#
|
|
156
|
-
|
|
174
|
+
# Create FRESH RequestContext for this invocation
|
|
175
|
+
from geek_cafe_saas_sdk.core.request_context import RequestContext
|
|
176
|
+
request_context = RequestContext(user_context)
|
|
177
|
+
|
|
178
|
+
# Get service instance with FRESH request_context
|
|
179
|
+
service = self._get_service(injected_service, request_context)
|
|
157
180
|
|
|
158
181
|
# Execute business logic
|
|
159
182
|
result = business_logic(event, service, user_context)
|
|
@@ -190,3 +213,14 @@ class BaseLambdaHandler:
|
|
|
190
213
|
500
|
|
191
214
|
)
|
|
192
215
|
raise
|
|
216
|
+
finally:
|
|
217
|
+
# Track execution time for telemetry
|
|
218
|
+
execution_time_ms = (time.time() - start_time) * 1000
|
|
219
|
+
if self._service_pool and hasattr(self._service_pool, 'track_execution_time'):
|
|
220
|
+
self._service_pool.track_execution_time(execution_time_ms)
|
|
221
|
+
|
|
222
|
+
# SECURITY: Clear request_context after invocation completes
|
|
223
|
+
# This ensures no lingering user credentials in warm Lambda containers
|
|
224
|
+
if service and hasattr(service, '_request_context'):
|
|
225
|
+
service._request_context = None
|
|
226
|
+
logger.debug("Cleared request_context after invocation")
|
|
@@ -3,53 +3,198 @@ Service pooling manager for Lambda warm starts.
|
|
|
3
3
|
|
|
4
4
|
Manages service initialization and caching to improve Lambda performance
|
|
5
5
|
by reusing connections across invocations.
|
|
6
|
+
|
|
7
|
+
Includes optional telemetry for tracking cold/warm starts, execution times,
|
|
8
|
+
and Lambda container uptime.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from typing import Dict, Type, TypeVar, Generic, Optional, Any
|
|
14
|
+
from aws_lambda_powertools import Logger
|
|
15
|
+
|
|
16
|
+
logger = Logger()
|
|
9
17
|
|
|
10
18
|
T = TypeVar('T')
|
|
11
19
|
|
|
12
20
|
|
|
13
21
|
class ServicePool(Generic[T]):
|
|
14
22
|
"""
|
|
15
|
-
Manages service instances for Lambda warm starts.
|
|
23
|
+
Manages service instances for Lambda warm starts with PER-INVOCATION security context refresh.
|
|
16
24
|
|
|
17
25
|
Lambda containers reuse the global scope between invocations, allowing
|
|
18
26
|
us to cache service instances (and their DB connections) to reduce
|
|
19
27
|
cold start latency by 80-90%.
|
|
20
28
|
|
|
29
|
+
SECURITY: request_context is refreshed on EVERY invocation to prevent
|
|
30
|
+
cross-user data leaks in warm Lambda containers.
|
|
31
|
+
|
|
21
32
|
Example:
|
|
22
33
|
# Module level
|
|
23
|
-
vote_service_pool = ServicePool(VoteService)
|
|
34
|
+
vote_service_pool = ServicePool(VoteService, dynamodb=db, table_name=TABLE)
|
|
24
35
|
|
|
25
|
-
# In handler
|
|
26
|
-
|
|
36
|
+
# In handler (per invocation)
|
|
37
|
+
request_context = RequestContext(user_context) # Fresh context
|
|
38
|
+
service = vote_service_pool.get(request_context) # Context refreshed!
|
|
27
39
|
"""
|
|
28
40
|
|
|
29
|
-
def __init__(self, service_class: Type[T]):
|
|
41
|
+
def __init__(self, service_class: Type[T], **service_kwargs):
|
|
30
42
|
"""
|
|
31
|
-
Initialize the service pool.
|
|
43
|
+
Initialize the service pool with optional telemetry.
|
|
32
44
|
|
|
33
45
|
Args:
|
|
34
46
|
service_class: The service class to instantiate
|
|
47
|
+
**service_kwargs: Keyword arguments for service (dynamodb, table_name, etc.)
|
|
48
|
+
NOTE: request_context should NOT be in kwargs - it's per-invocation
|
|
49
|
+
|
|
50
|
+
Environment Variables:
|
|
51
|
+
ENABLE_SERVICE_POOL_TELEMETRY: Set to 'true' to enable metrics logging
|
|
35
52
|
"""
|
|
36
53
|
self.service_class = service_class
|
|
54
|
+
self.service_kwargs = service_kwargs
|
|
37
55
|
self._instance: Optional[T] = None
|
|
56
|
+
|
|
57
|
+
# Telemetry tracking (optional)
|
|
58
|
+
self._telemetry_enabled = os.getenv('ENABLE_SERVICE_POOL_TELEMETRY', 'false').lower() in ('true', '1', 'yes')
|
|
59
|
+
self._container_start_time = time.time() # When this Lambda container started
|
|
60
|
+
self._cold_start_count = 0 # Number of cold starts
|
|
61
|
+
self._warm_start_count = 0 # Number of warm starts
|
|
62
|
+
self._total_invocations = 0 # Total invocations
|
|
63
|
+
self._last_invocation_time: Optional[float] = None
|
|
64
|
+
self._execution_times: list = [] # Track execution durations (limited to last 10)
|
|
38
65
|
|
|
39
|
-
def get(self) -> T:
|
|
66
|
+
def get(self, request_context: Optional[Any] = None) -> T:
|
|
40
67
|
"""
|
|
41
|
-
Get or create the service instance.
|
|
68
|
+
Get or create the service instance with FRESH request_context.
|
|
42
69
|
|
|
70
|
+
SECURITY CRITICAL: On warm starts, the request_context is REFRESHED
|
|
71
|
+
to prevent User A's credentials from being used for User B's request.
|
|
72
|
+
|
|
73
|
+
TELEMETRY: Tracks cold/warm starts and logs metrics if enabled.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
request_context: Fresh RequestContext for this invocation
|
|
77
|
+
|
|
43
78
|
Returns:
|
|
44
|
-
Service instance (cached
|
|
79
|
+
Service instance (DB connection cached, security context refreshed)
|
|
45
80
|
"""
|
|
46
|
-
|
|
47
|
-
|
|
81
|
+
is_cold_start = self._instance is None
|
|
82
|
+
|
|
83
|
+
if is_cold_start:
|
|
84
|
+
# Cold start - create new service instance
|
|
85
|
+
if self._telemetry_enabled:
|
|
86
|
+
self._cold_start_count += 1
|
|
87
|
+
logger.info(
|
|
88
|
+
"ServicePool: Cold start",
|
|
89
|
+
extra={
|
|
90
|
+
"service": self.service_class.__name__,
|
|
91
|
+
"cold_starts": self._cold_start_count,
|
|
92
|
+
"warm_starts": self._warm_start_count
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
kwargs = {**self.service_kwargs}
|
|
97
|
+
if request_context is not None:
|
|
98
|
+
kwargs['request_context'] = request_context
|
|
99
|
+
self._instance = self.service_class(**kwargs)
|
|
100
|
+
else:
|
|
101
|
+
# Warm start - REFRESH request_context for security
|
|
102
|
+
if self._telemetry_enabled:
|
|
103
|
+
self._warm_start_count += 1
|
|
104
|
+
|
|
105
|
+
if request_context is not None and hasattr(self._instance, '_request_context'):
|
|
106
|
+
self._instance._request_context = request_context
|
|
107
|
+
|
|
108
|
+
self._total_invocations += 1
|
|
109
|
+
self._last_invocation_time = time.time()
|
|
110
|
+
|
|
111
|
+
# Log telemetry periodically (every 10 invocations)
|
|
112
|
+
if self._telemetry_enabled and self._total_invocations % 10 == 0:
|
|
113
|
+
self._log_telemetry()
|
|
114
|
+
|
|
48
115
|
return self._instance
|
|
49
116
|
|
|
117
|
+
def track_execution_time(self, duration_ms: float):
|
|
118
|
+
"""
|
|
119
|
+
Track execution time for this invocation.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
duration_ms: Execution duration in milliseconds
|
|
123
|
+
"""
|
|
124
|
+
if not self._telemetry_enabled:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Keep only last 10 execution times to avoid memory bloat
|
|
128
|
+
self._execution_times.append(duration_ms)
|
|
129
|
+
if len(self._execution_times) > 10:
|
|
130
|
+
self._execution_times.pop(0)
|
|
131
|
+
|
|
132
|
+
def _log_telemetry(self):
|
|
133
|
+
"""Log telemetry metrics."""
|
|
134
|
+
container_uptime_seconds = time.time() - self._container_start_time
|
|
135
|
+
|
|
136
|
+
metrics = {
|
|
137
|
+
"service": self.service_class.__name__,
|
|
138
|
+
"total_invocations": self._total_invocations,
|
|
139
|
+
"cold_starts": self._cold_start_count,
|
|
140
|
+
"warm_starts": self._warm_start_count,
|
|
141
|
+
"container_uptime_seconds": round(container_uptime_seconds, 2),
|
|
142
|
+
"container_uptime_minutes": round(container_uptime_seconds / 60, 2),
|
|
143
|
+
"warm_start_ratio": round(
|
|
144
|
+
self._warm_start_count / self._total_invocations * 100, 2
|
|
145
|
+
) if self._total_invocations > 0 else 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Add execution time stats if available
|
|
149
|
+
if self._execution_times:
|
|
150
|
+
avg_exec_time = sum(self._execution_times) / len(self._execution_times)
|
|
151
|
+
metrics.update({
|
|
152
|
+
"avg_execution_ms": round(avg_exec_time, 2),
|
|
153
|
+
"min_execution_ms": round(min(self._execution_times), 2),
|
|
154
|
+
"max_execution_ms": round(max(self._execution_times), 2),
|
|
155
|
+
"recent_executions_tracked": len(self._execution_times)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
logger.info(
|
|
159
|
+
"ServicePool Telemetry",
|
|
160
|
+
extra=metrics
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
164
|
+
"""
|
|
165
|
+
Get current telemetry metrics.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dictionary of metrics
|
|
169
|
+
"""
|
|
170
|
+
container_uptime = time.time() - self._container_start_time
|
|
171
|
+
|
|
172
|
+
metrics = {
|
|
173
|
+
"service_class": self.service_class.__name__,
|
|
174
|
+
"total_invocations": self._total_invocations,
|
|
175
|
+
"cold_starts": self._cold_start_count,
|
|
176
|
+
"warm_starts": self._warm_start_count,
|
|
177
|
+
"container_uptime_seconds": round(container_uptime, 2),
|
|
178
|
+
"warm_start_ratio_percent": round(
|
|
179
|
+
self._warm_start_count / self._total_invocations * 100, 2
|
|
180
|
+
) if self._total_invocations > 0 else 0,
|
|
181
|
+
"telemetry_enabled": self._telemetry_enabled
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if self._execution_times:
|
|
185
|
+
avg_exec = sum(self._execution_times) / len(self._execution_times)
|
|
186
|
+
metrics.update({
|
|
187
|
+
"avg_execution_ms": round(avg_exec, 2),
|
|
188
|
+
"min_execution_ms": round(min(self._execution_times), 2),
|
|
189
|
+
"max_execution_ms": round(max(self._execution_times), 2)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return metrics
|
|
193
|
+
|
|
50
194
|
def reset(self):
|
|
51
195
|
"""Reset the pool (useful for testing)."""
|
|
52
196
|
self._instance = None
|
|
197
|
+
# Don't reset telemetry - it tracks the entire container lifecycle
|
|
53
198
|
|
|
54
199
|
|
|
55
200
|
class MultiServicePool:
|
|
@@ -427,7 +427,7 @@ def require_authorization(
|
|
|
427
427
|
|
|
428
428
|
Usage:
|
|
429
429
|
@require_authorization(operation=Operation.READ, resource_type="message")
|
|
430
|
-
def
|
|
430
|
+
def lambda_handler(event, context):
|
|
431
431
|
# Authorization already checked
|
|
432
432
|
# Access auth info via event['authorization_context']
|
|
433
433
|
pass
|
|
@@ -5,13 +5,13 @@ import functools
|
|
|
5
5
|
from typing import Dict, Any, Callable
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def add_cors_headers(
|
|
8
|
+
def add_cors_headers(lambda_handler: Callable) -> Callable:
|
|
9
9
|
"""
|
|
10
10
|
Decorator that adds CORS headers to Lambda response.
|
|
11
11
|
"""
|
|
12
|
-
@functools.wraps(
|
|
12
|
+
@functools.wraps(lambda_handler)
|
|
13
13
|
def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
|
|
14
|
-
response =
|
|
14
|
+
response = lambda_handler(event, context, *args, **kwargs)
|
|
15
15
|
|
|
16
16
|
# Ensure headers exist
|
|
17
17
|
if 'headers' not in response:
|
|
@@ -31,11 +31,11 @@ def add_cors_headers(handler: Callable) -> Callable:
|
|
|
31
31
|
return wrapper
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def handle_preflight(
|
|
34
|
+
def handle_preflight(lambda_handler: Callable) -> Callable:
|
|
35
35
|
"""
|
|
36
36
|
Decorator that handles OPTIONS preflight requests.
|
|
37
37
|
"""
|
|
38
|
-
@functools.wraps(
|
|
38
|
+
@functools.wraps(lambda_handler)
|
|
39
39
|
def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
|
|
40
40
|
# Handle OPTIONS request
|
|
41
41
|
if event.get('httpMethod') == 'OPTIONS':
|
|
@@ -50,14 +50,14 @@ def handle_preflight(handler: Callable) -> Callable:
|
|
|
50
50
|
'body': ''
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
return
|
|
53
|
+
return lambda_handler(event, context, *args, **kwargs)
|
|
54
54
|
|
|
55
55
|
return wrapper
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
# Convenience decorator that combines both CORS functionalities
|
|
59
|
-
def handle_cors(
|
|
59
|
+
def handle_cors(lambda_handler: Callable) -> Callable:
|
|
60
60
|
"""
|
|
61
61
|
Decorator that handles both preflight requests and adds CORS headers.
|
|
62
62
|
"""
|
|
63
|
-
return add_cors_headers(handle_preflight(
|
|
63
|
+
return add_cors_headers(handle_preflight(lambda_handler))
|