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.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- 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
|
|
File without changes
|
|
@@ -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
|
+
}
|