geek-cafe-saas-sdk 0.7.5__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.

Files changed (131) hide show
  1. geek_cafe_saas_sdk/__init__.py +1 -1
  2. geek_cafe_saas_sdk/core/anonymous_context.py +321 -0
  3. geek_cafe_saas_sdk/core/request_context.py +184 -0
  4. geek_cafe_saas_sdk/decorators/__init__.py +1 -1
  5. geek_cafe_saas_sdk/decorators/auth.py +6 -6
  6. geek_cafe_saas_sdk/decorators/core.py +44 -44
  7. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +1 -3
  8. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +1 -3
  9. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +15 -3
  10. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +1 -1
  11. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +1 -1
  12. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +1 -1
  13. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +1 -1
  14. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +1 -1
  15. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +1 -4
  16. geek_cafe_saas_sdk/domains/auth/services/user_service.py +0 -2
  17. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +1 -1
  18. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +1 -1
  19. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +1 -1
  20. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +1 -1
  21. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +1 -1
  22. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +1 -3
  23. geek_cafe_saas_sdk/domains/communities/services/community_service.py +3 -3
  24. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +1 -1
  25. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +1 -1
  26. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +1 -1
  27. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +1 -1
  28. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +1 -1
  29. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +1 -1
  30. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +1 -1
  31. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +1 -1
  32. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +1 -1
  33. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +1 -1
  34. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +1 -1
  35. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +1 -2
  36. geek_cafe_saas_sdk/domains/events/services/event_service.py +6 -4
  37. geek_cafe_saas_sdk/domains/files/handlers/README.md +1 -1
  38. geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +1 -1
  39. geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +1 -1
  40. geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +1 -1
  41. geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +1 -1
  42. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +1 -1
  43. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +1 -1
  44. geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +1 -1
  45. geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +1 -1
  46. geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +1 -1
  47. geek_cafe_saas_sdk/domains/files/models/file.py +16 -6
  48. geek_cafe_saas_sdk/domains/files/services/directory_service.py +34 -9
  49. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +38 -3
  50. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +33 -36
  51. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +1 -1
  52. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +1 -1
  53. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +1 -1
  54. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +1 -1
  55. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +1 -1
  56. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +1 -1
  57. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +1 -1
  58. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +1 -1
  59. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +1 -1
  60. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +1 -1
  61. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +1 -1
  62. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +1 -1
  63. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +1 -1
  64. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +1 -1
  65. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +1 -1
  66. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +35 -2
  67. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +20 -3
  68. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +1 -3
  69. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +1 -1
  70. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +1 -1
  71. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +1 -1
  72. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +1 -1
  73. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +1 -1
  74. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +1 -1
  75. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +1 -1
  76. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +1 -1
  77. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +1 -2
  78. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +1 -1
  79. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +1 -1
  80. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +1 -1
  81. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +1 -1
  82. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +1 -1
  83. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +1 -1
  84. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +1 -1
  85. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +1 -1
  86. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +1 -1
  87. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +1 -1
  88. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +1 -2
  89. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +2 -2
  90. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +1 -1
  91. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +1 -1
  92. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +1 -1
  93. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +1 -1
  94. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +1 -1
  95. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +1 -1
  96. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +1 -1
  97. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +1 -1
  98. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +1 -1
  99. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +1 -1
  100. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +1 -1
  101. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +1 -1
  102. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +1 -1
  103. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +0 -2
  104. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +1 -1
  105. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +1 -1
  106. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +1 -1
  107. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +1 -1
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +1 -1
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +1 -1
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +1 -1
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +1 -1
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +1 -1
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +1 -1
  114. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +3 -3
  115. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +3 -3
  116. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +1 -1
  117. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +1 -1
  118. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +1 -1
  119. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +1 -1
  120. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +1 -5
  121. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +1 -5
  122. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +10 -3
  123. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +40 -6
  124. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +157 -12
  125. geek_cafe_saas_sdk/middleware/authorization.py +1 -1
  126. geek_cafe_saas_sdk/middleware/cors.py +8 -8
  127. geek_cafe_saas_sdk/services/database_service.py +76 -5
  128. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/METADATA +16 -16
  129. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/RECORD +131 -129
  130. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/WHEEL +0 -0
  131. {geek_cafe_saas_sdk-0.7.5.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 handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
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 handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
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 handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
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 handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
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 handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
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 handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
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 handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
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 handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
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
- self.vote_service = VoteService(dynamodb=dynamodb, table_name=table_name)
22
- self.vote_summary_service = VoteSummaryService(dynamodb=dynamodb, table_name=table_name)
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
- return self._service_pool.get()
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
- return self.service_class(**self.service_kwargs)
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
- # Get service instance
156
- service = self._get_service(injected_service)
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
- from typing import Dict, Type, TypeVar, Generic, Optional
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
- service = vote_service_pool.get()
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 on warm starts)
79
+ Service instance (DB connection cached, security context refreshed)
45
80
  """
46
- if self._instance is None:
47
- self._instance = self.service_class()
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 handler(event, context):
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(handler: Callable) -> Callable:
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(handler)
12
+ @functools.wraps(lambda_handler)
13
13
  def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
14
- response = handler(event, context, *args, **kwargs)
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(handler: Callable) -> Callable:
34
+ def handle_preflight(lambda_handler: Callable) -> Callable:
35
35
  """
36
36
  Decorator that handles OPTIONS preflight requests.
37
37
  """
38
- @functools.wraps(handler)
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 handler(event, context, *args, **kwargs)
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(handler: Callable) -> Callable:
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(handler))
63
+ return add_cors_headers(handle_preflight(lambda_handler))