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,501 @@
|
|
|
1
|
+
"""
|
|
2
|
+
S3 file operations service using boto3-assist.
|
|
3
|
+
|
|
4
|
+
Geek Cafe, LLC
|
|
5
|
+
MIT License. See Project Root for the license information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
from boto3_assist.s3.s3_object import S3Object
|
|
10
|
+
from boto3_assist.s3.s3_bucket import S3Bucket
|
|
11
|
+
from boto3_assist.s3.s3_connection import S3Connection
|
|
12
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
13
|
+
from geek_cafe_saas_sdk.core.error_codes import ErrorCode
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class S3FileService:
|
|
18
|
+
"""
|
|
19
|
+
S3 file operations using boto3-assist.
|
|
20
|
+
|
|
21
|
+
Handles all S3 operations including upload, download, delete,
|
|
22
|
+
presigned URLs, and versioning. Supports dependency injection
|
|
23
|
+
for testing with Moto.
|
|
24
|
+
|
|
25
|
+
Configuration (Environment Variables):
|
|
26
|
+
- FILE_UPLOAD_MAX_SIZE: Maximum file size in bytes (default: 104857600 = 100MB)
|
|
27
|
+
- FILE_PRESIGNED_URL_EXPIRY: Presigned URL expiration in seconds (default: 300)
|
|
28
|
+
- S3_FILE_BUCKET: Default S3 bucket name
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
s3_object: Optional[S3Object] = None,
|
|
34
|
+
s3_bucket: Optional[S3Bucket] = None,
|
|
35
|
+
default_bucket: Optional[str] = None
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize S3 service.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
s3_object: S3Object instance (inject for testing with Moto)
|
|
42
|
+
s3_bucket: S3Bucket instance (inject for testing)
|
|
43
|
+
default_bucket: Default bucket name (from env or parameter)
|
|
44
|
+
"""
|
|
45
|
+
if s3_object is None:
|
|
46
|
+
# Create connection and S3Object if not provided
|
|
47
|
+
self.connection = S3Connection()
|
|
48
|
+
self.s3_object = S3Object(connection=self.connection)
|
|
49
|
+
self.s3_bucket = s3_bucket or S3Bucket(connection=self.connection)
|
|
50
|
+
else:
|
|
51
|
+
self.s3_object = s3_object
|
|
52
|
+
# If bucket not provided, need to create connection
|
|
53
|
+
if s3_bucket is None:
|
|
54
|
+
self.connection = S3Connection()
|
|
55
|
+
self.s3_bucket = S3Bucket(connection=self.connection)
|
|
56
|
+
else:
|
|
57
|
+
self.s3_bucket = s3_bucket
|
|
58
|
+
# Try to get connection from s3_object
|
|
59
|
+
self.connection = getattr(s3_object, 'connection', S3Connection())
|
|
60
|
+
|
|
61
|
+
self.default_bucket = default_bucket or os.getenv("S3_FILE_BUCKET")
|
|
62
|
+
|
|
63
|
+
# Configuration
|
|
64
|
+
self.max_file_size = int(os.getenv("FILE_UPLOAD_MAX_SIZE", "104857600")) # 100MB
|
|
65
|
+
self.presigned_url_expiry = int(os.getenv("FILE_PRESIGNED_URL_EXPIRY", "300")) # 5 minutes
|
|
66
|
+
|
|
67
|
+
def upload_file(
|
|
68
|
+
self,
|
|
69
|
+
file_data: bytes,
|
|
70
|
+
key: str,
|
|
71
|
+
bucket: Optional[str] = None,
|
|
72
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
73
|
+
content_type: Optional[str] = None
|
|
74
|
+
) -> ServiceResult:
|
|
75
|
+
"""
|
|
76
|
+
Upload file to S3.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
file_data: File content as bytes
|
|
80
|
+
key: S3 object key
|
|
81
|
+
bucket: Bucket name (uses default if not provided)
|
|
82
|
+
metadata: Custom metadata
|
|
83
|
+
content_type: MIME type
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
ServiceResult with upload details
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
bucket_name = bucket or self.default_bucket
|
|
90
|
+
|
|
91
|
+
if not bucket_name:
|
|
92
|
+
return ServiceResult.error_result(
|
|
93
|
+
message="S3 bucket name is required",
|
|
94
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Validate file size
|
|
98
|
+
if len(file_data) > self.max_file_size:
|
|
99
|
+
return ServiceResult.error_result(
|
|
100
|
+
message=f"File size exceeds maximum allowed size of {self.max_file_size} bytes",
|
|
101
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Upload to S3
|
|
105
|
+
self.s3_object.put(
|
|
106
|
+
bucket=bucket_name,
|
|
107
|
+
key=key,
|
|
108
|
+
data=file_data
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Note: boto3-assist S3Object.put doesn't support metadata/content_type directly
|
|
112
|
+
# For production, you may need to use boto3 client directly or extend S3Object
|
|
113
|
+
|
|
114
|
+
return ServiceResult.success_result({
|
|
115
|
+
"bucket": bucket_name,
|
|
116
|
+
"key": key,
|
|
117
|
+
"size": len(file_data),
|
|
118
|
+
"s3_uri": f"s3://{bucket_name}/{key}"
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
return ServiceResult.error_result(
|
|
123
|
+
message=f"Failed to upload file to S3: {str(e)}",
|
|
124
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def download_file(
|
|
128
|
+
self,
|
|
129
|
+
key: str,
|
|
130
|
+
bucket: Optional[str] = None
|
|
131
|
+
) -> ServiceResult:
|
|
132
|
+
"""
|
|
133
|
+
Download file from S3.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
key: S3 object key
|
|
137
|
+
bucket: Bucket name
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
ServiceResult with file data
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
bucket_name = bucket or self.default_bucket
|
|
144
|
+
|
|
145
|
+
if not bucket_name:
|
|
146
|
+
return ServiceResult.error_result(
|
|
147
|
+
message="S3 bucket name is required",
|
|
148
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
response = self.s3_object.get_object(
|
|
152
|
+
bucket_name=bucket_name,
|
|
153
|
+
key=key
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
file_data = response['Body'].read()
|
|
157
|
+
|
|
158
|
+
return ServiceResult.success_result({
|
|
159
|
+
"data": file_data,
|
|
160
|
+
"content_type": response.get('ContentType'),
|
|
161
|
+
"size": response.get('ContentLength'),
|
|
162
|
+
"metadata": response.get('Metadata', {}),
|
|
163
|
+
"last_modified": response.get('LastModified'),
|
|
164
|
+
"etag": response.get('ETag')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
# Check if it's a NoSuchKey error
|
|
169
|
+
if 'NoSuchKey' in str(e) or 'Not Found' in str(e) or '404' in str(e):
|
|
170
|
+
return ServiceResult.error_result(
|
|
171
|
+
message=f"File not found: {key}",
|
|
172
|
+
error_code=ErrorCode.NOT_FOUND
|
|
173
|
+
)
|
|
174
|
+
return ServiceResult.error_result(
|
|
175
|
+
message=f"Failed to download file from S3: {str(e)}",
|
|
176
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def delete_file(
|
|
180
|
+
self,
|
|
181
|
+
key: str,
|
|
182
|
+
bucket: Optional[str] = None
|
|
183
|
+
) -> ServiceResult:
|
|
184
|
+
"""
|
|
185
|
+
Delete file from S3.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
key: S3 object key
|
|
189
|
+
bucket: Bucket name
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
ServiceResult with deletion details
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
bucket_name = bucket or self.default_bucket
|
|
196
|
+
|
|
197
|
+
if not bucket_name:
|
|
198
|
+
return ServiceResult.error_result(
|
|
199
|
+
message="S3 bucket name is required",
|
|
200
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
self.s3_object.delete(
|
|
204
|
+
bucket_name=bucket_name,
|
|
205
|
+
key=key
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return ServiceResult.success_result({
|
|
209
|
+
"deleted": key,
|
|
210
|
+
"bucket": bucket_name
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
return ServiceResult.error_result(
|
|
215
|
+
message=f"Failed to delete file from S3: {str(e)}",
|
|
216
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def generate_presigned_upload_url(
|
|
220
|
+
self,
|
|
221
|
+
key: str,
|
|
222
|
+
file_name: str,
|
|
223
|
+
bucket: Optional[str] = None,
|
|
224
|
+
expires_in: Optional[int] = None,
|
|
225
|
+
content_type: Optional[str] = None,
|
|
226
|
+
metadata: Optional[Dict[str, str]] = None
|
|
227
|
+
) -> ServiceResult:
|
|
228
|
+
"""
|
|
229
|
+
Generate presigned URL for file upload.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
key: S3 object key
|
|
233
|
+
file_name: Original file name
|
|
234
|
+
bucket: Bucket name
|
|
235
|
+
expires_in: Expiration time in seconds
|
|
236
|
+
content_type: MIME type
|
|
237
|
+
metadata: Custom metadata
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
ServiceResult with presigned URL
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
bucket_name = bucket or self.default_bucket
|
|
244
|
+
expiry = expires_in or self.presigned_url_expiry
|
|
245
|
+
|
|
246
|
+
if not bucket_name:
|
|
247
|
+
return ServiceResult.error_result(
|
|
248
|
+
message="S3 bucket name is required",
|
|
249
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
url_data = self.s3_object.generate_presigned_url(
|
|
253
|
+
bucket_name=bucket_name,
|
|
254
|
+
key_path=key,
|
|
255
|
+
file_name=file_name,
|
|
256
|
+
meta_data=metadata,
|
|
257
|
+
expiration=expiry,
|
|
258
|
+
method_type='POST'
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return ServiceResult.success_result({
|
|
262
|
+
"url": url_data.get('url'),
|
|
263
|
+
"fields": url_data.get('fields', {}),
|
|
264
|
+
"expires_in": expiry,
|
|
265
|
+
"key": key
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
return ServiceResult.error_result(
|
|
270
|
+
message=f"Failed to generate presigned upload URL: {str(e)}",
|
|
271
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def generate_presigned_download_url(
|
|
275
|
+
self,
|
|
276
|
+
key: str,
|
|
277
|
+
bucket: Optional[str] = None,
|
|
278
|
+
expires_in: Optional[int] = None,
|
|
279
|
+
file_name: Optional[str] = None
|
|
280
|
+
) -> ServiceResult:
|
|
281
|
+
"""
|
|
282
|
+
Generate presigned URL for file download.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
key: S3 object key
|
|
286
|
+
bucket: Bucket name
|
|
287
|
+
expires_in: Expiration time in seconds
|
|
288
|
+
file_name: Optional filename for download
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
ServiceResult with presigned URL
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
bucket_name = bucket or self.default_bucket
|
|
295
|
+
expiry = expires_in or self.presigned_url_expiry
|
|
296
|
+
|
|
297
|
+
if not bucket_name:
|
|
298
|
+
return ServiceResult.error_result(
|
|
299
|
+
message="S3 bucket name is required",
|
|
300
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Use file_name or extract from key
|
|
304
|
+
download_name = file_name or key.split('/')[-1]
|
|
305
|
+
|
|
306
|
+
url_data = self.s3_object.generate_presigned_url(
|
|
307
|
+
bucket_name=bucket_name,
|
|
308
|
+
key_path=key,
|
|
309
|
+
file_name=download_name,
|
|
310
|
+
expiration=expiry,
|
|
311
|
+
method_type='GET'
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return ServiceResult.success_result({
|
|
315
|
+
"url": url_data.get('url'),
|
|
316
|
+
"expires_in": expiry,
|
|
317
|
+
"key": key
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
return ServiceResult.error_result(
|
|
322
|
+
message=f"Failed to generate presigned download URL: {str(e)}",
|
|
323
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def list_object_versions(
|
|
327
|
+
self,
|
|
328
|
+
key: str,
|
|
329
|
+
bucket: Optional[str] = None
|
|
330
|
+
) -> ServiceResult:
|
|
331
|
+
"""
|
|
332
|
+
List all versions of an object (for S3 native versioning).
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
key: S3 object key
|
|
336
|
+
bucket: Bucket name
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
ServiceResult with versions list
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
bucket_name = bucket or self.default_bucket
|
|
343
|
+
|
|
344
|
+
if not bucket_name:
|
|
345
|
+
return ServiceResult.error_result(
|
|
346
|
+
message="S3 bucket name is required",
|
|
347
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
versions = self.s3_object.list_versions(
|
|
351
|
+
bucket=bucket_name,
|
|
352
|
+
prefix=key
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return ServiceResult.success_result({
|
|
356
|
+
"versions": versions,
|
|
357
|
+
"key": key,
|
|
358
|
+
"bucket": bucket_name
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
except Exception as e:
|
|
362
|
+
return ServiceResult.error_result(
|
|
363
|
+
message=f"Failed to list object versions: {str(e)}",
|
|
364
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def copy_object(
|
|
368
|
+
self,
|
|
369
|
+
source_key: str,
|
|
370
|
+
dest_key: str,
|
|
371
|
+
source_bucket: Optional[str] = None,
|
|
372
|
+
dest_bucket: Optional[str] = None
|
|
373
|
+
) -> ServiceResult:
|
|
374
|
+
"""
|
|
375
|
+
Copy object within S3.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
source_key: Source S3 key
|
|
379
|
+
dest_key: Destination S3 key
|
|
380
|
+
source_bucket: Source bucket (default bucket if not provided)
|
|
381
|
+
dest_bucket: Destination bucket (default bucket if not provided)
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
ServiceResult with copy details
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
src_bucket = source_bucket or self.default_bucket
|
|
388
|
+
dst_bucket = dest_bucket or self.default_bucket
|
|
389
|
+
|
|
390
|
+
if not src_bucket or not dst_bucket:
|
|
391
|
+
return ServiceResult.error_result(
|
|
392
|
+
message="S3 bucket names are required",
|
|
393
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
self.s3_object.copy(
|
|
397
|
+
source_bucket=src_bucket,
|
|
398
|
+
source_key=source_key,
|
|
399
|
+
destination_bucket=dst_bucket,
|
|
400
|
+
destination_key=dest_key
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return ServiceResult.success_result({
|
|
404
|
+
"source": f"s3://{src_bucket}/{source_key}",
|
|
405
|
+
"destination": f"s3://{dst_bucket}/{dest_key}"
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
except Exception as e:
|
|
409
|
+
return ServiceResult.error_result(
|
|
410
|
+
message=f"Failed to copy object: {str(e)}",
|
|
411
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def enable_bucket_versioning(
|
|
415
|
+
self,
|
|
416
|
+
bucket: Optional[str] = None
|
|
417
|
+
) -> ServiceResult:
|
|
418
|
+
"""
|
|
419
|
+
Enable versioning on S3 bucket.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
bucket: Bucket name
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
ServiceResult with status
|
|
426
|
+
"""
|
|
427
|
+
try:
|
|
428
|
+
bucket_name = bucket or self.default_bucket
|
|
429
|
+
|
|
430
|
+
if not bucket_name:
|
|
431
|
+
return ServiceResult.error_result(
|
|
432
|
+
message="S3 bucket name is required",
|
|
433
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
self.s3_bucket.enable_versioning(
|
|
437
|
+
bucket_name=bucket_name
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
return ServiceResult.success_result({
|
|
441
|
+
"versioning_enabled": True,
|
|
442
|
+
"bucket": bucket_name
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
return ServiceResult.error_result(
|
|
447
|
+
message=f"Failed to enable bucket versioning: {str(e)}",
|
|
448
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def get_object_metadata(
|
|
452
|
+
self,
|
|
453
|
+
key: str,
|
|
454
|
+
bucket: Optional[str] = None
|
|
455
|
+
) -> ServiceResult:
|
|
456
|
+
"""
|
|
457
|
+
Get S3 object metadata without downloading file.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
key: S3 object key
|
|
461
|
+
bucket: Bucket name
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
ServiceResult with metadata
|
|
465
|
+
"""
|
|
466
|
+
try:
|
|
467
|
+
bucket_name = bucket or self.default_bucket
|
|
468
|
+
|
|
469
|
+
if not bucket_name:
|
|
470
|
+
return ServiceResult.error_result(
|
|
471
|
+
message="S3 bucket name is required",
|
|
472
|
+
error_code=ErrorCode.VALIDATION_ERROR
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Use head_object to get metadata without downloading
|
|
476
|
+
response = self.connection.client.head_object(
|
|
477
|
+
Bucket=bucket_name,
|
|
478
|
+
Key=key
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
return ServiceResult.success_result({
|
|
482
|
+
"content_type": response.get('ContentType'),
|
|
483
|
+
"size": response.get('ContentLength'),
|
|
484
|
+
"metadata": response.get('Metadata', {}),
|
|
485
|
+
"last_modified": response.get('LastModified'),
|
|
486
|
+
"etag": response.get('ETag'),
|
|
487
|
+
"version_id": response.get('VersionId'),
|
|
488
|
+
"storage_class": response.get('StorageClass')
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
except Exception as e:
|
|
492
|
+
# Check if it's a NoSuchKey error
|
|
493
|
+
if '404' in str(e) or 'NoSuchKey' in str(e) or 'Not Found' in str(e):
|
|
494
|
+
return ServiceResult.error_result(
|
|
495
|
+
message=f"File not found: {key}",
|
|
496
|
+
error_code=ErrorCode.NOT_FOUND
|
|
497
|
+
)
|
|
498
|
+
return ServiceResult.error_result(
|
|
499
|
+
message=f"Failed to get object metadata: {str(e)}",
|
|
500
|
+
error_code=ErrorCode.EXTERNAL_SERVICE_ERROR
|
|
501
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for creating chat channels.
|
|
3
|
+
|
|
4
|
+
Requires authentication (secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.messaging.services import ChatChannelService
|
|
10
|
+
|
|
11
|
+
# Factory creates handler (defaults to secure auth)
|
|
12
|
+
handler_wrapper = create_handler(
|
|
13
|
+
service_class=ChatChannelService,
|
|
14
|
+
require_body=True,
|
|
15
|
+
convert_case=True
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Create a new chat channel.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
event: Lambda event from API Gateway
|
|
25
|
+
context: Lambda context
|
|
26
|
+
injected_service: Optional ChatChannelService for testing
|
|
27
|
+
|
|
28
|
+
Expected body (camelCase from frontend):
|
|
29
|
+
{
|
|
30
|
+
"name": "general",
|
|
31
|
+
"description": "General discussion",
|
|
32
|
+
"channelType": "public" | "private" | "direct",
|
|
33
|
+
"ownerId": "user_456", # Optional: For admins creating channels for others
|
|
34
|
+
"members": ["user_123", "user_456"],
|
|
35
|
+
"topic": "Channel topic",
|
|
36
|
+
"isDefault": false,
|
|
37
|
+
"isAnnouncement": false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Note:
|
|
41
|
+
- ownerId: Who the channel belongs to (defaults to authenticated user)
|
|
42
|
+
- createdBy: Always set to authenticated user (audit trail)
|
|
43
|
+
|
|
44
|
+
Returns 201 with created chat channel
|
|
45
|
+
"""
|
|
46
|
+
return handler_wrapper.execute(event, context, create_chat_channel, injected_service)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_chat_channel(
|
|
50
|
+
event: Dict[str, Any],
|
|
51
|
+
service: ChatChannelService,
|
|
52
|
+
user_context: Dict[str, str]
|
|
53
|
+
) -> Any:
|
|
54
|
+
"""
|
|
55
|
+
Business logic for creating chat channels.
|
|
56
|
+
|
|
57
|
+
Owner is automatically added as a member.
|
|
58
|
+
Supports admin scenario (Rule #1):
|
|
59
|
+
- ownerId in payload: who the channel belongs to
|
|
60
|
+
- createdById: authenticated admin (for audit trail - Rule #2)
|
|
61
|
+
|
|
62
|
+
Owner validation (Rule #3):
|
|
63
|
+
- Missing ownerId: defaults to authenticated user (self-service)
|
|
64
|
+
- Present ownerId: uses specified owner (admin-on-behalf)
|
|
65
|
+
- Empty ownerId: ERROR (fail fast)
|
|
66
|
+
"""
|
|
67
|
+
payload = event["parsed_body"]
|
|
68
|
+
|
|
69
|
+
authenticated_user_id = user_context.get("user_id")
|
|
70
|
+
tenant_id = user_context.get("tenant_id")
|
|
71
|
+
|
|
72
|
+
# Validate and resolve owner (Rule #3)
|
|
73
|
+
# Will raise ValidationError if explicitly empty
|
|
74
|
+
owner_user_id = service._validate_owner_field(payload, authenticated_user_id, "owner_id")
|
|
75
|
+
|
|
76
|
+
# Set audit trail to authenticated user (Rule #2)
|
|
77
|
+
payload["created_by_id"] = authenticated_user_id
|
|
78
|
+
|
|
79
|
+
# Create the chat channel with owner
|
|
80
|
+
result = service.create(
|
|
81
|
+
tenant_id=tenant_id,
|
|
82
|
+
user_id=owner_user_id, # Resource owner
|
|
83
|
+
payload=payload
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return result
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for deleting (soft delete) chat channels.
|
|
3
|
+
|
|
4
|
+
Requires authentication and channel creator permissions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.messaging.services import ChatChannelService
|
|
10
|
+
|
|
11
|
+
# Factory creates handler (defaults to secure auth)
|
|
12
|
+
handler_wrapper = create_handler(
|
|
13
|
+
service_class=ChatChannelService,
|
|
14
|
+
require_body=False
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Delete (soft delete) a chat channel.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
event: Lambda event from API Gateway
|
|
24
|
+
context: Lambda context
|
|
25
|
+
injected_service: Optional ChatChannelService for testing
|
|
26
|
+
|
|
27
|
+
Path parameters:
|
|
28
|
+
id: Chat channel ID
|
|
29
|
+
|
|
30
|
+
Returns 200 with success boolean
|
|
31
|
+
"""
|
|
32
|
+
return handler_wrapper.execute(event, context, delete_chat_channel, injected_service)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def delete_chat_channel(
|
|
36
|
+
event: Dict[str, Any],
|
|
37
|
+
service: ChatChannelService,
|
|
38
|
+
user_context: Dict[str, str]
|
|
39
|
+
) -> Any:
|
|
40
|
+
"""
|
|
41
|
+
Business logic for deleting a chat channel.
|
|
42
|
+
|
|
43
|
+
Performs soft delete (sets deleted timestamp).
|
|
44
|
+
Only channel creator can delete.
|
|
45
|
+
"""
|
|
46
|
+
# Extract path parameter
|
|
47
|
+
path_params = event.get("pathParameters") or {}
|
|
48
|
+
channel_id = path_params.get("id")
|
|
49
|
+
|
|
50
|
+
if not channel_id:
|
|
51
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
52
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
53
|
+
return ServiceResult.exception_result(
|
|
54
|
+
ValidationError("Channel ID is required in path")
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
user_id = user_context.get("user_id")
|
|
58
|
+
tenant_id = user_context.get("tenant_id")
|
|
59
|
+
|
|
60
|
+
# Delete the chat channel
|
|
61
|
+
return service.delete(
|
|
62
|
+
resource_id=channel_id,
|
|
63
|
+
tenant_id=tenant_id,
|
|
64
|
+
user_id=user_id
|
|
65
|
+
)
|