geek-cafe-saas-sdk 0.6.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 (194) hide show
  1. geek_cafe_saas_sdk/__init__.py +9 -0
  2. geek_cafe_saas_sdk/core/__init__.py +11 -0
  3. geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
  4. geek_cafe_saas_sdk/core/error_codes.py +132 -0
  5. geek_cafe_saas_sdk/core/service_errors.py +19 -0
  6. geek_cafe_saas_sdk/core/service_result.py +121 -0
  7. geek_cafe_saas_sdk/decorators/__init__.py +64 -0
  8. geek_cafe_saas_sdk/decorators/auth.py +373 -0
  9. geek_cafe_saas_sdk/decorators/core.py +358 -0
  10. geek_cafe_saas_sdk/domains/__init__.py +0 -0
  11. geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
  12. geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
  13. geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
  14. geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
  15. geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
  16. geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
  17. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
  18. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
  19. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
  20. geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
  21. geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
  22. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
  23. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
  24. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
  25. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
  26. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
  27. geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
  28. geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
  29. geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
  30. geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
  31. geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
  32. geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
  33. geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
  34. geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
  35. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
  36. geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
  37. geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
  38. geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
  39. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
  40. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
  41. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
  42. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
  43. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
  44. geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
  45. geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
  46. geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
  47. geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
  48. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
  49. geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
  50. geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
  51. geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
  52. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
  53. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
  54. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
  55. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
  56. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
  57. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
  58. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
  59. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
  60. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
  61. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
  62. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
  63. geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
  64. geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
  65. geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
  66. geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
  67. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
  68. geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
  69. geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
  70. geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
  71. geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
  72. geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
  73. geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
  74. geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
  75. geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
  76. geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
  77. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
  78. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
  79. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
  80. geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
  81. geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
  82. geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
  83. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
  84. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
  85. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
  86. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
  87. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
  88. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
  89. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
  90. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
  91. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
  92. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
  93. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
  94. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
  95. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
  96. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
  97. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
  98. geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
  99. geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
  100. geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
  101. geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
  102. geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
  103. geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
  104. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
  105. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
  106. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
  107. geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
  114. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
  115. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
  116. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
  117. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
  118. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
  119. geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
  120. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
  121. geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
  122. geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
  123. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
  124. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
  125. geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
  126. geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
  127. geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
  128. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
  129. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
  130. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
  131. geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
  132. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
  133. geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
  134. geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
  135. geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
  136. geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
  137. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
  138. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
  139. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
  140. geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
  141. geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
  142. geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
  143. geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
  144. geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
  145. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
  146. geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
  147. geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
  148. geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
  149. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
  150. geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
  151. geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
  152. geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
  153. geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
  154. geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
  155. geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
  156. geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
  157. geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
  158. geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
  159. geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
  160. geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
  161. geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
  162. geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
  163. geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
  164. geek_cafe_saas_sdk/middleware/__init__.py +36 -0
  165. geek_cafe_saas_sdk/middleware/auth.py +85 -0
  166. geek_cafe_saas_sdk/middleware/authorization.py +523 -0
  167. geek_cafe_saas_sdk/middleware/cors.py +63 -0
  168. geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
  169. geek_cafe_saas_sdk/middleware/validation.py +80 -0
  170. geek_cafe_saas_sdk/models/__init__.py +20 -0
  171. geek_cafe_saas_sdk/models/base_model.py +233 -0
  172. geek_cafe_saas_sdk/services/__init__.py +18 -0
  173. geek_cafe_saas_sdk/services/database_service.py +441 -0
  174. geek_cafe_saas_sdk/utilities/__init__.py +88 -0
  175. geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
  176. geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
  177. geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
  178. geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
  179. geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
  180. geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
  181. geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
  182. geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
  183. geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
  184. geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
  185. geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
  186. geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
  187. geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
  188. geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
  189. geek_cafe_saas_sdk/utilities/response.py +209 -0
  190. geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
  191. geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
  192. geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
  193. geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
  194. geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
@@ -0,0 +1,358 @@
1
+ """
2
+ Core Lambda handler decorators for common cross-cutting concerns.
3
+
4
+ These decorators handle:
5
+ - Error handling and standardized error responses
6
+ - CORS headers
7
+ - Request body parsing and case conversion
8
+ - Service injection with pooling
9
+ - Path parameter validation
10
+ - User context extraction
11
+ - Execution logging
12
+ """
13
+
14
+ import json
15
+ import time
16
+ import functools
17
+ from typing import Callable, Any, Dict, Optional, Type, List
18
+ from aws_lambda_powertools import Logger
19
+
20
+ from geek_cafe_saas_sdk.utilities.response import error_response, success_response
21
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
22
+ from geek_cafe_saas_sdk.middleware.auth import extract_user_context
23
+ from geek_cafe_saas_sdk.lambda_handlers._base.service_pool import ServicePool
24
+
25
+ logger = Logger()
26
+
27
+
28
+ def handle_errors(handler: Callable) -> Callable:
29
+ """
30
+ Catch exceptions and return standardized error responses.
31
+
32
+ Converts Python exceptions into API Gateway-compatible error responses
33
+ with appropriate status codes and error messages.
34
+
35
+ Usage:
36
+ @handle_errors
37
+ def handler(event, context):
38
+ # Any exception becomes a 500 response
39
+ raise ValueError("Something went wrong")
40
+
41
+ Returns:
42
+ Decorated handler that catches exceptions
43
+ """
44
+ @functools.wraps(handler)
45
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
46
+ try:
47
+ return handler(event, context, *args, **kwargs)
48
+ except ValueError as e:
49
+ # Validation errors -> 400
50
+ logger.warning(f"Validation error: {e}")
51
+ return error_response(str(e), "VALIDATION_ERROR", 400)
52
+ except PermissionError as e:
53
+ # Permission errors -> 403
54
+ logger.warning(f"Permission error: {e}")
55
+ return error_response(str(e), "PERMISSION_DENIED", 403)
56
+ except KeyError as e:
57
+ # Missing required field -> 400
58
+ logger.warning(f"Missing field: {e}")
59
+ return error_response(f"Missing required field: {str(e)}", "MISSING_FIELD", 400)
60
+ except Exception as e:
61
+ # Unexpected errors -> 500
62
+ logger.exception(f"Unexpected error in handler: {e}")
63
+ return error_response(
64
+ "An unexpected error occurred",
65
+ "INTERNAL_ERROR",
66
+ 500
67
+ )
68
+ return wrapper
69
+
70
+
71
+ def add_cors(
72
+ allow_origin: str = "*",
73
+ allow_methods: str = "GET,POST,PUT,DELETE,OPTIONS",
74
+ allow_headers: str = "Content-Type,Authorization,X-Api-Key"
75
+ ) -> Callable:
76
+ """
77
+ Add CORS headers to response.
78
+
79
+ Args:
80
+ allow_origin: Allowed origins (default: "*")
81
+ allow_methods: Allowed HTTP methods
82
+ allow_headers: Allowed headers
83
+
84
+ Usage:
85
+ @add_cors()
86
+ def handler(event, context):
87
+ return {'statusCode': 200, 'body': '{}'}
88
+
89
+ # Custom CORS
90
+ @add_cors(allow_origin="https://example.com")
91
+ def handler(event, context):
92
+ return {'statusCode': 200, 'body': '{}'}
93
+
94
+ Returns:
95
+ Decorated handler with CORS headers
96
+ """
97
+ def decorator(handler: Callable) -> Callable:
98
+ @functools.wraps(handler)
99
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
100
+ response = handler(event, context, *args, **kwargs)
101
+
102
+ # Ensure headers dict exists
103
+ if 'headers' not in response:
104
+ response['headers'] = {}
105
+
106
+ # Add CORS headers
107
+ response['headers']['Access-Control-Allow-Origin'] = allow_origin
108
+ response['headers']['Access-Control-Allow-Methods'] = allow_methods
109
+ response['headers']['Access-Control-Allow-Headers'] = allow_headers
110
+
111
+ return response
112
+ return wrapper
113
+ return decorator
114
+
115
+
116
+ def parse_request_body(
117
+ required: bool = False,
118
+ convert_case: bool = True
119
+ ) -> Callable:
120
+ """
121
+ Parse request body from JSON and optionally convert case.
122
+
123
+ Parses event['body'] as JSON and adds it to event['parsed_body'].
124
+ Optionally converts camelCase to snake_case for backend processing.
125
+
126
+ Args:
127
+ required: If True, returns 400 error if body is missing
128
+ convert_case: If True, converts camelCase keys to snake_case
129
+
130
+ Usage:
131
+ @parse_request_body(required=True)
132
+ def handler(event, context):
133
+ payload = event['parsed_body']
134
+ return {'statusCode': 200, 'body': json.dumps(payload)}
135
+
136
+ Returns:
137
+ Decorated handler with parsed body in event['parsed_body']
138
+ """
139
+ def decorator(handler: Callable) -> Callable:
140
+ @functools.wraps(handler)
141
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
142
+ # Check if body is required
143
+ if required and not event.get('body'):
144
+ return error_response(
145
+ "Request body is required",
146
+ "MISSING_BODY",
147
+ 400
148
+ )
149
+
150
+ # Parse body if present
151
+ if event.get('body'):
152
+ try:
153
+ body = LambdaEventUtility.get_body_from_event(event, raise_on_error=required)
154
+
155
+ if body and convert_case:
156
+ body = LambdaEventUtility.to_snake_case_for_backend(body)
157
+
158
+ if body:
159
+ event['parsed_body'] = body
160
+
161
+ except (ValueError, json.JSONDecodeError) as e:
162
+ logger.warning(f"Failed to parse request body: {e}")
163
+ return error_response(
164
+ "Invalid JSON in request body",
165
+ "INVALID_JSON",
166
+ 400
167
+ )
168
+
169
+ return handler(event, context, *args, **kwargs)
170
+ return wrapper
171
+ return decorator
172
+
173
+
174
+ def inject_service(
175
+ service_class: Type,
176
+ param_name: str = "service",
177
+ use_pooling: bool = True,
178
+ **service_kwargs
179
+ ) -> Callable:
180
+ """
181
+ Inject service instance into handler.
182
+
183
+ Creates and injects a service instance, optionally using connection pooling.
184
+ The service is passed as a keyword argument to the handler.
185
+
186
+ Args:
187
+ service_class: Service class to instantiate
188
+ param_name: Parameter name to inject (default: "service")
189
+ use_pooling: Use connection pooling (default: True)
190
+ **service_kwargs: Additional arguments for service constructor
191
+
192
+ Usage:
193
+ @inject_service(MessageService)
194
+ def handler(event, context, service):
195
+ return service.get_by_id(message_id)
196
+
197
+ # Custom parameter name
198
+ @inject_service(MessageService, param_name="msg_service")
199
+ def handler(event, context, msg_service):
200
+ return msg_service.get_by_id(message_id)
201
+
202
+ Returns:
203
+ Decorated handler with service injected
204
+ """
205
+ # Initialize service pool if using pooling
206
+ if use_pooling:
207
+ service_pool = ServicePool(service_class, **service_kwargs)
208
+
209
+ def decorator(handler: Callable) -> Callable:
210
+ @functools.wraps(handler)
211
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
212
+ # Get service from pool or create new instance
213
+ if use_pooling:
214
+ service = service_pool.get()
215
+ else:
216
+ service = service_class(**service_kwargs)
217
+
218
+ # Inject service as keyword argument
219
+ kwargs[param_name] = service
220
+
221
+ return handler(event, context, *args, **kwargs)
222
+ return wrapper
223
+ return decorator
224
+
225
+
226
+ def log_execution(
227
+ log_request: bool = True,
228
+ log_response: bool = False,
229
+ log_duration: bool = True
230
+ ) -> Callable:
231
+ """
232
+ Log handler execution details.
233
+
234
+ Logs request/response details and execution duration for monitoring
235
+ and debugging purposes.
236
+
237
+ Args:
238
+ log_request: Log incoming request details
239
+ log_response: Log response details (be careful with sensitive data)
240
+ log_duration: Log execution duration
241
+
242
+ Usage:
243
+ @log_execution(log_response=True)
244
+ def handler(event, context):
245
+ return {'statusCode': 200, 'body': '{}'}
246
+
247
+ Returns:
248
+ Decorated handler with execution logging
249
+ """
250
+ def decorator(handler: Callable) -> Callable:
251
+ @functools.wraps(handler)
252
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
253
+ start_time = time.time()
254
+
255
+ if log_request:
256
+ logger.info(
257
+ "Handler execution started",
258
+ extra={
259
+ 'function_name': context.function_name if context else 'unknown',
260
+ 'http_method': event.get('httpMethod'),
261
+ 'path': event.get('path'),
262
+ 'request_id': context.aws_request_id if context else 'unknown'
263
+ }
264
+ )
265
+
266
+ # Execute handler
267
+ response = handler(event, context, *args, **kwargs)
268
+
269
+ duration = time.time() - start_time
270
+
271
+ if log_duration:
272
+ logger.info(
273
+ "Handler execution completed",
274
+ extra={
275
+ 'duration_ms': round(duration * 1000, 2),
276
+ 'status_code': response.get('statusCode')
277
+ }
278
+ )
279
+
280
+ if log_response:
281
+ logger.debug(
282
+ "Response details",
283
+ extra={
284
+ 'status_code': response.get('statusCode'),
285
+ 'has_body': 'body' in response
286
+ }
287
+ )
288
+
289
+ return response
290
+ return wrapper
291
+ return decorator
292
+
293
+
294
+ def validate_path_params(required_params: List[str]) -> Callable:
295
+ """
296
+ Validate that required path parameters are present.
297
+
298
+ Args:
299
+ required_params: List of required path parameter names
300
+
301
+ Usage:
302
+ @validate_path_params(['tenant_id', 'user_id', 'message_id'])
303
+ def handler(event, context):
304
+ # All required params guaranteed to exist
305
+ message_id = event['pathParameters']['message_id']
306
+ return {'statusCode': 200}
307
+
308
+ Returns:
309
+ Decorated handler with validated path parameters
310
+ """
311
+ def decorator(handler: Callable) -> Callable:
312
+ @functools.wraps(handler)
313
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
314
+ path_params = event.get('pathParameters', {})
315
+
316
+ # Check for missing parameters
317
+ missing_params = [
318
+ param for param in required_params
319
+ if not path_params.get(param)
320
+ ]
321
+
322
+ if missing_params:
323
+ return error_response(
324
+ f"Missing required path parameters: {', '.join(missing_params)}",
325
+ "MISSING_PATH_PARAMS",
326
+ 400
327
+ )
328
+
329
+ return handler(event, context, *args, **kwargs)
330
+ return wrapper
331
+ return decorator
332
+
333
+
334
+ def extract_user_context_decorator(handler: Callable) -> Callable:
335
+ """
336
+ Extract user context from JWT and add to event.
337
+
338
+ Extracts user context from API Gateway authorizer and adds it to
339
+ event['user_context'] for easy access in handler.
340
+
341
+ Usage:
342
+ @extract_user_context_decorator
343
+ def handler(event, context):
344
+ user_id = event['user_context']['user_id']
345
+ tenant_id = event['user_context']['tenant_id']
346
+ return {'statusCode': 200}
347
+
348
+ Returns:
349
+ Decorated handler with user_context in event
350
+ """
351
+ @functools.wraps(handler)
352
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
353
+ # Extract user context from authorizer
354
+ user_context = extract_user_context(event)
355
+ event['user_context'] = user_context
356
+
357
+ return handler(event, context, *args, **kwargs)
358
+ return wrapper
File without changes
File without changes
@@ -0,0 +1,9 @@
1
+ # Analytics Domain Models
2
+
3
+ from .website_analytics import WebsiteAnalytics
4
+ from .website_analytics_summary import WebsiteAnalyticsSummary
5
+
6
+ __all__ = [
7
+ "WebsiteAnalytics",
8
+ "WebsiteAnalyticsSummary",
9
+ ]
@@ -0,0 +1,219 @@
1
+
2
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
3
+ from boto3_assist.utilities.string_utility import StringUtility
4
+ import datetime as dt
5
+ from typing import Dict, Any
6
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
7
+
8
+
9
+ class WebsiteAnalytics(BaseModel):
10
+ """
11
+ Model for storing website analytics data.
12
+
13
+ Supports different analytics types:
14
+ - general: Page views, sessions, user interactions
15
+ - error: Error tracking and debugging info
16
+ - performance: Load times, resource metrics
17
+ - custom: Custom event tracking
18
+ """
19
+
20
+ def __init__(self):
21
+ super().__init__()
22
+ self._route: str | None = None # URL route/path (e.g., "/blog/post-123")
23
+ self._slug: str | None = None # Slug for the page (e.g., "post-123")
24
+ self._analytics_type: str = "general" # general, error, performance, custom
25
+ self._data: Dict[str, Any] = {} # Flexible storage for analytics data
26
+ self._session_id: str | None = None
27
+ self._user_agent: str | None = None
28
+ self._ip_address: str | None = None
29
+ self._referrer: str | None = None
30
+
31
+ self._setup_indexes()
32
+
33
+ def _setup_indexes(self):
34
+ # Primary index: analytics by ID
35
+ primary: DynamoDBIndex = DynamoDBIndex()
36
+ primary.name = "primary"
37
+ primary.partition_key.attribute_name = "pk"
38
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(
39
+ ("analytics", self.id)
40
+ )
41
+ primary.sort_key.attribute_name = "sk"
42
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("analytics", self.id))
43
+ self.indexes.add_primary(primary)
44
+
45
+ ## GSI: 1
46
+ # GSI: all analytics records sorted by timestamp
47
+ gsi: DynamoDBIndex = DynamoDBIndex()
48
+ gsi.name = "gsi1"
49
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
50
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("analytics", "all"))
51
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
52
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
53
+ ("ts", self.created_utc_ts)
54
+ )
55
+ self.indexes.add_secondary(gsi)
56
+
57
+ ## GSI: 2
58
+ # GSI: analytics by route/slug for page-specific queries
59
+ gsi: DynamoDBIndex = DynamoDBIndex()
60
+ gsi.name = "gsi2"
61
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
62
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
63
+ ("route", self.route or self.slug)
64
+ )
65
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
66
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
67
+ ("ts", self.created_utc_ts)
68
+ )
69
+ self.indexes.add_secondary(gsi)
70
+
71
+ ## GSI: 3
72
+ # GSI: analytics by tenant sorted by timestamp
73
+ gsi: DynamoDBIndex = DynamoDBIndex()
74
+ gsi.name = "gsi3"
75
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
76
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
77
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
78
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
79
+ ("model", "analytics"), ("ts", self.created_utc_ts)
80
+ )
81
+ self.indexes.add_secondary(gsi)
82
+
83
+ ## GSI: 4
84
+ # GSI: analytics by type and timestamp (e.g., all errors, all performance metrics)
85
+ gsi: DynamoDBIndex = DynamoDBIndex()
86
+ gsi.name = "gsi4"
87
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
88
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
89
+ ("analytics-type", self.analytics_type)
90
+ )
91
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
92
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
93
+ ("ts", self.created_utc_ts)
94
+ )
95
+ self.indexes.add_secondary(gsi)
96
+
97
+ ## GSI: 5
98
+ # GSI: analytics by tenant and type for filtered queries
99
+ gsi: DynamoDBIndex = DynamoDBIndex()
100
+ gsi.name = "gsi5"
101
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
102
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
103
+ ("tenant", self.tenant_id)
104
+ )
105
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
106
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
107
+ ("type", self.analytics_type), ("ts", self.created_utc_ts)
108
+ )
109
+ self.indexes.add_secondary(gsi)
110
+
111
+ @property
112
+ def route(self) -> str | None:
113
+ return self._route
114
+
115
+ @route.setter
116
+ def route(self, value: str | None):
117
+ self._route = value
118
+
119
+ @property
120
+ def slug(self) -> str | None:
121
+ return self._slug
122
+
123
+ @slug.setter
124
+ def slug(self, value: str | None):
125
+ self._slug = value
126
+
127
+ @property
128
+ def analytics_type(self) -> str:
129
+ return self._analytics_type
130
+
131
+ @analytics_type.setter
132
+ def analytics_type(self, value: str):
133
+ self._analytics_type = value
134
+
135
+ @property
136
+ def data(self) -> Dict[str, Any]:
137
+ return self._data
138
+
139
+ @data.setter
140
+ def data(self, value: Dict[str, Any]):
141
+ self._data = value
142
+
143
+ @property
144
+ def session_id(self) -> str | None:
145
+ return self._session_id
146
+
147
+ @session_id.setter
148
+ def session_id(self, value: str | None):
149
+ self._session_id = value
150
+
151
+ @property
152
+ def user_agent(self) -> str | None:
153
+ return self._user_agent
154
+
155
+ @user_agent.setter
156
+ def user_agent(self, value: str | None):
157
+ self._user_agent = value
158
+
159
+ @property
160
+ def ip_address(self) -> str | None:
161
+ return self._ip_address
162
+
163
+ @ip_address.setter
164
+ def ip_address(self, value: str | None):
165
+ self._ip_address = value
166
+
167
+ @property
168
+ def referrer(self) -> str | None:
169
+ return self._referrer
170
+
171
+ @referrer.setter
172
+ def referrer(self, value: str | None):
173
+ self._referrer = value
174
+
175
+ # Helper methods for different analytics types
176
+ def set_page_view(self, route: str, **kwargs):
177
+ """Set general page view analytics."""
178
+ self.route = route
179
+ self.analytics_type = "general"
180
+ self.data = {
181
+ "event": "page_view",
182
+ "duration_ms": kwargs.get("duration_ms"),
183
+ "scroll_depth": kwargs.get("scroll_depth"),
184
+ **kwargs
185
+ }
186
+
187
+ def set_error(self, route: str, error_message: str, **kwargs):
188
+ """Set error analytics."""
189
+ self.route = route
190
+ self.analytics_type = "error"
191
+ self.data = {
192
+ "event": "error",
193
+ "error_message": error_message,
194
+ "error_type": kwargs.get("error_type"),
195
+ "stack_trace": kwargs.get("stack_trace"),
196
+ **kwargs
197
+ }
198
+
199
+ def set_performance(self, route: str, **kwargs):
200
+ """Set performance analytics."""
201
+ self.route = route
202
+ self.analytics_type = "performance"
203
+ self.data = {
204
+ "event": "performance",
205
+ "load_time_ms": kwargs.get("load_time_ms"),
206
+ "ttfb_ms": kwargs.get("ttfb_ms"), # Time to first byte
207
+ "fcp_ms": kwargs.get("fcp_ms"), # First contentful paint
208
+ "lcp_ms": kwargs.get("lcp_ms"), # Largest contentful paint
209
+ **kwargs
210
+ }
211
+
212
+ def set_custom_event(self, route: str, event_name: str, **kwargs):
213
+ """Set custom event analytics."""
214
+ self.route = route
215
+ self.analytics_type = "custom"
216
+ self.data = {
217
+ "event": event_name,
218
+ **kwargs
219
+ }