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,11 @@
|
|
|
1
|
+
# Core module for foundational classes and utilities
|
|
2
|
+
|
|
3
|
+
from .service_errors import ValidationError, AccessDeniedError, NotFoundError
|
|
4
|
+
from .service_result import ServiceResult
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'ValidationError',
|
|
8
|
+
'AccessDeniedError',
|
|
9
|
+
'NotFoundError',
|
|
10
|
+
'ServiceResult',
|
|
11
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Optional, Dict
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AuditMixin:
|
|
7
|
+
"""Mixin for services that need audit logging."""
|
|
8
|
+
|
|
9
|
+
def _log_activity(self, action: str, resource_type: str, resource_id: str,
|
|
10
|
+
tenant_id: str, user_id: str, metadata: Optional[Dict] = None) -> None:
|
|
11
|
+
"""Log activity for audit purposes."""
|
|
12
|
+
logger = logging.getLogger(f"{__name__}.audit")
|
|
13
|
+
|
|
14
|
+
audit_data = {
|
|
15
|
+
'action': action,
|
|
16
|
+
'resource_type': resource_type,
|
|
17
|
+
'resource_id': resource_id,
|
|
18
|
+
'tenant_id': tenant_id,
|
|
19
|
+
'user_id': user_id,
|
|
20
|
+
'timestamp': datetime.now().isoformat(),
|
|
21
|
+
'metadata': metadata or {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
logger.info(
|
|
25
|
+
f"AUDIT: {action} on {resource_type} {resource_id} by user {user_id}",
|
|
26
|
+
extra=audit_data
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Also print for console visibility
|
|
30
|
+
print(f"š AUDIT: {action} on {resource_type} {resource_id} by user {user_id}")
|
|
31
|
+
|
|
32
|
+
# TODO: Implement activity logging to database such as dynamodb or put into a queue
|
|
33
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
Standardized error codes for all services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ErrorCode(str, Enum):
|
|
12
|
+
"""
|
|
13
|
+
Standardized error codes for all services.
|
|
14
|
+
|
|
15
|
+
Error codes are organized by category and map to HTTP status code equivalents.
|
|
16
|
+
Using an enum ensures type safety and consistency across all services.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Input/Validation Errors (4xx equivalent)
|
|
20
|
+
VALIDATION_ERROR = "VALIDATION_ERROR" # Invalid input data
|
|
21
|
+
MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD" # Specific validation failure
|
|
22
|
+
INVALID_FORMAT = "INVALID_FORMAT" # Data format issues
|
|
23
|
+
INVALID_PARAMETER = "INVALID_PARAMETER" # Parameter value issues
|
|
24
|
+
|
|
25
|
+
# Authorization Errors (403 equivalent)
|
|
26
|
+
ACCESS_DENIED = "ACCESS_DENIED" # User lacks permission
|
|
27
|
+
INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS" # Role-based denial
|
|
28
|
+
AUTHENTICATION_REQUIRED = "AUTHENTICATION_REQUIRED" # No auth token provided
|
|
29
|
+
|
|
30
|
+
# Resource Errors (404 equivalent)
|
|
31
|
+
NOT_FOUND = "NOT_FOUND" # Resource doesn't exist
|
|
32
|
+
RESOURCE_DELETED = "RESOURCE_DELETED" # Resource was soft-deleted
|
|
33
|
+
|
|
34
|
+
# Conflict Errors (409 equivalent)
|
|
35
|
+
ALREADY_EXISTS = "ALREADY_EXISTS" # Duplicate resource
|
|
36
|
+
CONCURRENT_MODIFICATION = "CONCURRENT_MODIFICATION" # Optimistic lock failure
|
|
37
|
+
RESOURCE_CONFLICT = "RESOURCE_CONFLICT" # General conflict
|
|
38
|
+
|
|
39
|
+
# Database Errors (500 equivalent)
|
|
40
|
+
DATABASE_ERROR = "DATABASE_ERROR" # Generic DB error
|
|
41
|
+
DATABASE_SAVE_FAILED = "DATABASE_SAVE_FAILED" # Save operation failed
|
|
42
|
+
DATABASE_DELETE_FAILED = "DATABASE_DELETE_FAILED" # Delete operation failed
|
|
43
|
+
DATABASE_QUERY_FAILED = "DATABASE_QUERY_FAILED" # Query operation failed
|
|
44
|
+
DATABASE_CONNECTION_ERROR = "DATABASE_CONNECTION_ERROR" # Connection issues
|
|
45
|
+
|
|
46
|
+
# Service/Business Logic Errors (500 equivalent)
|
|
47
|
+
INTERNAL_ERROR = "INTERNAL_ERROR" # Unexpected error
|
|
48
|
+
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE" # Dependency failure
|
|
49
|
+
OPERATION_FAILED = "OPERATION_FAILED" # Business logic failure
|
|
50
|
+
|
|
51
|
+
# Batch/Bulk Operation Errors
|
|
52
|
+
PARTIAL_FAILURE = "PARTIAL_FAILURE" # Some items in batch failed
|
|
53
|
+
BATCH_OPERATION_FAILED = "BATCH_OPERATION_FAILED" # Entire batch failed
|
|
54
|
+
|
|
55
|
+
# Rate Limiting / Throttling (429 equivalent)
|
|
56
|
+
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED" # Too many requests
|
|
57
|
+
QUOTA_EXCEEDED = "QUOTA_EXCEEDED" # Usage quota exceeded
|
|
58
|
+
|
|
59
|
+
def __str__(self) -> str:
|
|
60
|
+
"""Return the error code value as string."""
|
|
61
|
+
return self.value
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def is_client_error(cls, code: 'ErrorCode') -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Check if error code represents a client error (4xx equivalent).
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
code: Error code to check
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if client error, False otherwise
|
|
73
|
+
"""
|
|
74
|
+
client_errors = {
|
|
75
|
+
cls.VALIDATION_ERROR,
|
|
76
|
+
cls.MISSING_REQUIRED_FIELD,
|
|
77
|
+
cls.INVALID_FORMAT,
|
|
78
|
+
cls.INVALID_PARAMETER,
|
|
79
|
+
cls.ACCESS_DENIED,
|
|
80
|
+
cls.INSUFFICIENT_PERMISSIONS,
|
|
81
|
+
cls.AUTHENTICATION_REQUIRED,
|
|
82
|
+
cls.NOT_FOUND,
|
|
83
|
+
cls.RESOURCE_DELETED,
|
|
84
|
+
cls.ALREADY_EXISTS,
|
|
85
|
+
cls.CONCURRENT_MODIFICATION,
|
|
86
|
+
cls.RESOURCE_CONFLICT,
|
|
87
|
+
cls.RATE_LIMIT_EXCEEDED,
|
|
88
|
+
cls.QUOTA_EXCEEDED,
|
|
89
|
+
}
|
|
90
|
+
return code in client_errors
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def is_server_error(cls, code: 'ErrorCode') -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Check if error code represents a server error (5xx equivalent).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
code: Error code to check
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if server error, False otherwise
|
|
102
|
+
"""
|
|
103
|
+
return not cls.is_client_error(code)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def get_http_status(cls, code: 'ErrorCode') -> int:
|
|
107
|
+
"""
|
|
108
|
+
Get suggested HTTP status code for error code.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
code: Error code
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
HTTP status code (e.g., 400, 404, 500)
|
|
115
|
+
"""
|
|
116
|
+
status_map = {
|
|
117
|
+
cls.VALIDATION_ERROR: 400,
|
|
118
|
+
cls.MISSING_REQUIRED_FIELD: 400,
|
|
119
|
+
cls.INVALID_FORMAT: 400,
|
|
120
|
+
cls.INVALID_PARAMETER: 400,
|
|
121
|
+
cls.AUTHENTICATION_REQUIRED: 401,
|
|
122
|
+
cls.ACCESS_DENIED: 403,
|
|
123
|
+
cls.INSUFFICIENT_PERMISSIONS: 403,
|
|
124
|
+
cls.NOT_FOUND: 404,
|
|
125
|
+
cls.RESOURCE_DELETED: 410, # Gone
|
|
126
|
+
cls.ALREADY_EXISTS: 409,
|
|
127
|
+
cls.CONCURRENT_MODIFICATION: 409,
|
|
128
|
+
cls.RESOURCE_CONFLICT: 409,
|
|
129
|
+
cls.RATE_LIMIT_EXCEEDED: 429,
|
|
130
|
+
cls.QUOTA_EXCEEDED: 429,
|
|
131
|
+
}
|
|
132
|
+
return status_map.get(code, 500) # Default to 500 for server errors
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Service Errors
|
|
2
|
+
from typing import Optional, Union, List
|
|
3
|
+
|
|
4
|
+
class ValidationError(Exception):
|
|
5
|
+
"""Validation error for service operations."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, field: Optional[Union[str, List[str]]] = None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.field = field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AccessDeniedError(Exception):
|
|
13
|
+
"""Access denied error for service operations."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NotFoundError(Exception):
|
|
18
|
+
"""Resource not found error."""
|
|
19
|
+
pass
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Base service classes for collaborative property operations."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Any, Dict, List, Optional, TypeVar, Generic
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from aws_lambda_powertools import Logger
|
|
9
|
+
|
|
10
|
+
logger = Logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ServiceResult(Generic[T]):
|
|
17
|
+
"""Standard service operation result with enhanced error handling."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, success: bool, data: Optional[T] = None,
|
|
20
|
+
message: Optional[str] = None, error_code: Optional[str] = None,
|
|
21
|
+
error_details: Optional[Dict[str, Any]] = None,
|
|
22
|
+
stack_trace: Optional[str] = None):
|
|
23
|
+
self.success = success
|
|
24
|
+
self.data = data
|
|
25
|
+
self.message = message
|
|
26
|
+
self.error_code = error_code
|
|
27
|
+
self.error_details = error_details or {}
|
|
28
|
+
self.stack_trace = stack_trace
|
|
29
|
+
self.timestamp = datetime.now()
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def success_result(cls, data: T) -> 'ServiceResult[T]':
|
|
33
|
+
"""Create a successful result."""
|
|
34
|
+
return cls(success=True, data=data)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def error_result(cls, message: str, error_code: Optional[str] = None,
|
|
38
|
+
error_details: Optional[Dict[str, Any]] = None) -> 'ServiceResult[T]':
|
|
39
|
+
"""Create an error result with basic error information."""
|
|
40
|
+
return cls(success=False, message=message, error_code=error_code, error_details=error_details)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def exception_result(cls, exception: Exception, error_code: Optional[str] = None,
|
|
44
|
+
context: Optional[str] = None) -> 'ServiceResult[T]':
|
|
45
|
+
"""Create an error result from an exception with full stack trace logging."""
|
|
46
|
+
|
|
47
|
+
# Get the full stack trace
|
|
48
|
+
stack_trace = traceback.format_exc()
|
|
49
|
+
|
|
50
|
+
# Create detailed error message
|
|
51
|
+
error_message = f"{type(exception).__name__}: {str(exception)}"
|
|
52
|
+
if context:
|
|
53
|
+
error_message = f"{context} - {error_message}"
|
|
54
|
+
|
|
55
|
+
# Prepare error details
|
|
56
|
+
error_details = {
|
|
57
|
+
'exception_type': type(exception).__name__,
|
|
58
|
+
'exception_message': str(exception),
|
|
59
|
+
'context': context,
|
|
60
|
+
'timestamp': datetime.now().isoformat()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Log the full error with stack trace to CloudWatch
|
|
64
|
+
logger.error(
|
|
65
|
+
f"Service operation failed: {error_message}\n"
|
|
66
|
+
f"Context: {context or 'None'}\n"
|
|
67
|
+
f"Exception Type: {type(exception).__name__}\n"
|
|
68
|
+
f"Exception Message: {str(exception)}\n"
|
|
69
|
+
f"Stack Trace:\n{stack_trace}",
|
|
70
|
+
extra={
|
|
71
|
+
'error_code': error_code,
|
|
72
|
+
'exception_type': type(exception).__name__,
|
|
73
|
+
'context': context,
|
|
74
|
+
'stack_trace': stack_trace
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Also print to console for immediate visibility
|
|
79
|
+
print(f"\nšØ SERVICE ERROR: {error_message}")
|
|
80
|
+
print(f"š Context: {context or 'None'}")
|
|
81
|
+
print(f"š Exception Type: {type(exception).__name__}")
|
|
82
|
+
print(f"š Stack Trace:")
|
|
83
|
+
print(stack_trace)
|
|
84
|
+
print("" + "="*80 + "")
|
|
85
|
+
|
|
86
|
+
return cls(
|
|
87
|
+
success=False,
|
|
88
|
+
message=error_message,
|
|
89
|
+
error_code=error_code or 'INTERNAL_ERROR',
|
|
90
|
+
error_details=error_details,
|
|
91
|
+
stack_trace=stack_trace
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
95
|
+
"""Convert result to dictionary for API responses."""
|
|
96
|
+
result = {
|
|
97
|
+
'success': self.success,
|
|
98
|
+
'timestamp': self.timestamp.isoformat()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if self.success:
|
|
102
|
+
result['data'] = self.data
|
|
103
|
+
else:
|
|
104
|
+
result['error'] = {
|
|
105
|
+
'message': self.message,
|
|
106
|
+
'code': self.error_code,
|
|
107
|
+
'details': self.error_details
|
|
108
|
+
}
|
|
109
|
+
# Only include stack trace in development/debug mode
|
|
110
|
+
# You might want to add a flag to control this
|
|
111
|
+
if self.stack_trace:
|
|
112
|
+
result['error']['stack_trace'] = self.stack_trace
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler decorators for cross-cutting concerns.
|
|
3
|
+
|
|
4
|
+
This module provides composable decorators for Lambda handlers following
|
|
5
|
+
industry best practices (AWS Lambda Powertools, Flask, FastAPI patterns).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from geek_cafe_saas_sdk.decorators import (
|
|
9
|
+
handle_errors,
|
|
10
|
+
add_cors,
|
|
11
|
+
parse_request_body,
|
|
12
|
+
inject_service
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
@handle_errors
|
|
16
|
+
@add_cors
|
|
17
|
+
@parse_request_body(convert_case=True)
|
|
18
|
+
@inject_service(MessageService)
|
|
19
|
+
def handler(event, context, service):
|
|
20
|
+
return service.get_by_id(event['pathParameters']['id'])
|
|
21
|
+
|
|
22
|
+
Design Principles:
|
|
23
|
+
- Explicit > Implicit: See all behaviors in handler signature
|
|
24
|
+
- Composable: Stack decorators as needed
|
|
25
|
+
- Single Responsibility: Each decorator has one job
|
|
26
|
+
- Testable: Test handlers and decorators independently
|
|
27
|
+
- Industry Standard: Follows AWS Lambda Powertools patterns
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from .core import (
|
|
31
|
+
handle_errors,
|
|
32
|
+
add_cors,
|
|
33
|
+
parse_request_body,
|
|
34
|
+
inject_service,
|
|
35
|
+
log_execution,
|
|
36
|
+
validate_path_params,
|
|
37
|
+
extract_user_context_decorator
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from .auth import (
|
|
41
|
+
require_authorization,
|
|
42
|
+
require_admin,
|
|
43
|
+
require_tenant_admin,
|
|
44
|
+
require_platform_admin,
|
|
45
|
+
public
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# Core decorators
|
|
50
|
+
"handle_errors",
|
|
51
|
+
"add_cors",
|
|
52
|
+
"parse_request_body",
|
|
53
|
+
"inject_service",
|
|
54
|
+
"log_execution",
|
|
55
|
+
"validate_path_params",
|
|
56
|
+
"extract_user_context_decorator",
|
|
57
|
+
|
|
58
|
+
# Auth decorators
|
|
59
|
+
"require_authorization",
|
|
60
|
+
"require_admin",
|
|
61
|
+
"require_tenant_admin",
|
|
62
|
+
"require_platform_admin",
|
|
63
|
+
"public",
|
|
64
|
+
]
|