geek-cafe-saas-sdk 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (194) hide show
  1. geek_cafe_saas_sdk/__init__.py +9 -0
  2. geek_cafe_saas_sdk/core/__init__.py +11 -0
  3. geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
  4. geek_cafe_saas_sdk/core/error_codes.py +132 -0
  5. geek_cafe_saas_sdk/core/service_errors.py +19 -0
  6. geek_cafe_saas_sdk/core/service_result.py +121 -0
  7. geek_cafe_saas_sdk/decorators/__init__.py +64 -0
  8. geek_cafe_saas_sdk/decorators/auth.py +373 -0
  9. geek_cafe_saas_sdk/decorators/core.py +358 -0
  10. geek_cafe_saas_sdk/domains/__init__.py +0 -0
  11. geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
  12. geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
  13. geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
  14. geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
  15. geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
  16. geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
  17. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
  18. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
  19. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
  20. geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
  21. geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
  22. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
  23. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
  24. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
  25. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
  26. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
  27. geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
  28. geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
  29. geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
  30. geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
  31. geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
  32. geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
  33. geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
  34. geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
  35. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
  36. geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
  37. geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
  38. geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
  39. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
  40. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
  41. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
  42. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
  43. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
  44. geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
  45. geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
  46. geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
  47. geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
  48. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
  49. geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
  50. geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
  51. geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
  52. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
  53. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
  54. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
  55. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
  56. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
  57. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
  58. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
  59. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
  60. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
  61. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
  62. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
  63. geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
  64. geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
  65. geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
  66. geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
  67. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
  68. geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
  69. geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
  70. geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
  71. geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
  72. geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
  73. geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
  74. geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
  75. geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
  76. geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
  77. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
  78. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
  79. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
  80. geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
  81. geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
  82. geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
  83. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
  84. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
  85. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
  86. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
  87. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
  88. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
  89. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
  90. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
  91. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
  92. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
  93. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
  94. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
  95. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
  96. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
  97. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
  98. geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
  99. geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
  100. geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
  101. geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
  102. geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
  103. geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
  104. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
  105. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
  106. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
  107. geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
  114. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
  115. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
  116. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
  117. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
  118. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
  119. geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
  120. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
  121. geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
  122. geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
  123. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
  124. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
  125. geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
  126. geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
  127. geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
  128. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
  129. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
  130. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
  131. geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
  132. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
  133. geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
  134. geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
  135. geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
  136. geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
  137. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
  138. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
  139. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
  140. geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
  141. geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
  142. geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
  143. geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
  144. geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
  145. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
  146. geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
  147. geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
  148. geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
  149. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
  150. geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
  151. geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
  152. geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
  153. geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
  154. geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
  155. geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
  156. geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
  157. geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
  158. geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
  159. geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
  160. geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
  161. geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
  162. geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
  163. geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
  164. geek_cafe_saas_sdk/middleware/__init__.py +36 -0
  165. geek_cafe_saas_sdk/middleware/auth.py +85 -0
  166. geek_cafe_saas_sdk/middleware/authorization.py +523 -0
  167. geek_cafe_saas_sdk/middleware/cors.py +63 -0
  168. geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
  169. geek_cafe_saas_sdk/middleware/validation.py +80 -0
  170. geek_cafe_saas_sdk/models/__init__.py +20 -0
  171. geek_cafe_saas_sdk/models/base_model.py +233 -0
  172. geek_cafe_saas_sdk/services/__init__.py +18 -0
  173. geek_cafe_saas_sdk/services/database_service.py +441 -0
  174. geek_cafe_saas_sdk/utilities/__init__.py +88 -0
  175. geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
  176. geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
  177. geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
  178. geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
  179. geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
  180. geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
  181. geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
  182. geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
  183. geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
  184. geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
  185. geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
  186. geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
  187. geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
  188. geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
  189. geek_cafe_saas_sdk/utilities/response.py +209 -0
  190. geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
  191. geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
  192. geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
  193. geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
  194. geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
@@ -0,0 +1,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
@@ -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
+ )