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,128 @@
1
+ """
2
+ Lambda handler for creating votes.
3
+
4
+ REFACTORED VERSION using Factory Pattern.
5
+ Reduces code and centralizes auth strategy configuration.
6
+
7
+ The handler type is determined by environment variables:
8
+ - AUTH_TYPE=secure (default) - API Gateway authorizer
9
+ - AUTH_TYPE=api_key - x-api-key header validation
10
+ - AUTH_TYPE=public - No authentication
11
+ """
12
+
13
+ from typing import Dict, Any
14
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
15
+ from geek_cafe_saas_sdk.domains.voting.services import VoteService
16
+ from geek_cafe_saas_sdk.utilities.response import error_response
17
+
18
+ # ⚡ Initialize handler at module level for Lambda warm starts
19
+ # Factory automatically selects handler based on AUTH_TYPE env var:
20
+ # - secure (default): API Gateway authorizer (Cognito/Lambda)
21
+ # - api_key: Validates x-api-key header
22
+ # - public: No authentication required
23
+ #
24
+ # This automatically handles:
25
+ # - Authentication (based on AUTH_TYPE)
26
+ # - Request body parsing
27
+ # - Case conversion (camelCase → snake_case)
28
+ # - Service pooling (connection reuse)
29
+ # - User context extraction
30
+ # - CORS headers (apply_cors=True by default)
31
+ # - Error handling (apply_error_handling=True by default)
32
+ handler = create_handler(
33
+ service_class=VoteService,
34
+ require_body=True,
35
+ convert_case=True
36
+ )
37
+
38
+ def lambda_handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
39
+ """
40
+ Create or update a vote.
41
+
42
+ Args:
43
+ event: API Gateway event
44
+ context: Lambda context
45
+ injected_service: Optional VoteService for testing (Moto)
46
+
47
+ Expected request body:
48
+ {
49
+ "userId": "user_id",
50
+ "targetId": "health-meter-ui-choice",
51
+ "choiceId": "gauge" | "traffic_light",
52
+ "voteType": "single_choice",
53
+ "availableChoices": ["gauge", "traffic_light"],
54
+ "content": {
55
+ "description": "User preference for health meter display type",
56
+ "metadata": {...}
57
+ }
58
+ }
59
+ """
60
+ return handler.execute(event, context, create_vote, injected_service)
61
+
62
+
63
+ def create_vote(
64
+ event: Dict[str, Any],
65
+ service: VoteService,
66
+ user_context: Dict[str, str]
67
+ ) -> Any:
68
+ """
69
+ Business logic for creating a vote.
70
+
71
+ All boilerplate has been handled by the wrapper:
72
+ ✅ API key validation
73
+ ✅ Body parsing and case conversion
74
+ ✅ Service initialization
75
+ ✅ User context extraction
76
+
77
+ Focus purely on business logic here.
78
+
79
+ Args:
80
+ event: Enhanced event with parsed_body containing snake_case data
81
+ service: VoteService instance (pooled for warm starts)
82
+ user_context: Extracted user info (user_id, tenant_id, etc.)
83
+
84
+ Returns:
85
+ ServiceResult that will be formatted into Lambda response
86
+ """
87
+ # Get parsed and converted body (camelCase → snake_case already done)
88
+ payload = event["parsed_body"]
89
+
90
+ # Validate required fields
91
+ target_id = payload.get("target_id")
92
+ if not target_id:
93
+ return error_response("target_id is required", 400)
94
+
95
+ choice_id = payload.get("choice_id")
96
+ if not choice_id:
97
+ return error_response("choice_id is required", 400)
98
+
99
+ vote_type = payload.get("vote_type", "single_choice")
100
+ available_choices = payload.get("available_choices", [])
101
+ content = payload.get("content", {})
102
+
103
+ # Validate choice_id is in available_choices if provided
104
+ if available_choices and choice_id not in available_choices:
105
+ return error_response(
106
+ f"choice_id '{choice_id}' must be one of: {available_choices}", 400
107
+ )
108
+
109
+ # Get user info from context
110
+ tenant_id = user_context.get("tenant_id", "anonymous")
111
+ # we will get the logged in user if there is one, or a sudo user id sent by the client
112
+ user_id = user_context.get("user_id") or payload.get("user_id", "anonymous")
113
+
114
+ # Create the vote based on type
115
+ if vote_type == "single_choice":
116
+ result = service.create_single_choice_vote(
117
+ tenant_id=tenant_id,
118
+ user_id=user_id,
119
+ target_id=target_id,
120
+ choice_id=choice_id,
121
+ available_choices=available_choices if available_choices else None,
122
+ content=content if content else None,
123
+ )
124
+ else:
125
+ return error_response(f"Unsupported vote_type: {vote_type}", 400)
126
+
127
+ # Return ServiceResult - handler will automatically format to Lambda response
128
+ return result
@@ -0,0 +1,41 @@
1
+ # src/geek_cafe_saas_sdk/lambda_handlers/votes/delete/app.py
2
+
3
+ from typing import Dict, Any
4
+
5
+ from geek_cafe_saas_sdk.services import VoteService
6
+ from geek_cafe_saas_sdk.lambda_handlers import ServicePool
7
+ from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response, success_response
8
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
9
+
10
+ vote_service_pool = ServicePool(VoteService)
11
+
12
+ def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
13
+ """
14
+ Lambda handler for deleting a vote by its ID.
15
+
16
+ Args:
17
+ event: API Gateway event
18
+ context: Lambda context
19
+ injected_service: Optional VoteService for testing
20
+ """
21
+ try:
22
+ vote_service = injected_service if injected_service else vote_service_pool.get()
23
+ user_id = LambdaEventUtility.get_authenticated_user_id(event)
24
+ tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
25
+ resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
26
+
27
+ if not resource_id:
28
+ return error_response("Vote ID is required in the path.", "VALIDATION_ERROR", 400)
29
+
30
+ result = vote_service.delete(
31
+ resource_id=resource_id,
32
+ tenant_id=tenant_id,
33
+ user_id=user_id
34
+ )
35
+
36
+ if result.success:
37
+ return success_response(message="Vote deleted successfully", status_code=204)
38
+ return service_result_to_response(result)
39
+
40
+ except Exception as e:
41
+ return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
@@ -0,0 +1,39 @@
1
+ # src/geek_cafe_saas_sdk/lambda_handlers/votes/get/app.py
2
+
3
+ from typing import Dict, Any
4
+
5
+ from geek_cafe_saas_sdk.services import VoteService
6
+ from geek_cafe_saas_sdk.lambda_handlers import ServicePool
7
+ from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
8
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
9
+
10
+ vote_service_pool = ServicePool(VoteService)
11
+
12
+ def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
13
+ """
14
+ Lambda handler for retrieving a single vote by its ID.
15
+
16
+ Args:
17
+ event: API Gateway event
18
+ context: Lambda context
19
+ injected_service: Optional VoteService for testing
20
+ """
21
+ try:
22
+ vote_service = injected_service if injected_service else vote_service_pool.get()
23
+ user_id = LambdaEventUtility.get_authenticated_user_id(event)
24
+ tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
25
+ resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
26
+
27
+ if not resource_id:
28
+ return error_response("Vote ID is required in the path.", "VALIDATION_ERROR", 400)
29
+
30
+ result = vote_service.get_by_id(
31
+ resource_id=resource_id,
32
+ tenant_id=tenant_id,
33
+ user_id=user_id
34
+ )
35
+
36
+ return service_result_to_response(result)
37
+
38
+ except Exception as e:
39
+ return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
@@ -0,0 +1,38 @@
1
+ # src/geek_cafe_saas_sdk/lambda_handlers/votes/list/app.py
2
+
3
+ from typing import Dict, Any
4
+
5
+ from geek_cafe_saas_sdk.services import VoteService
6
+ from geek_cafe_saas_sdk.lambda_handlers import ServicePool
7
+ from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
8
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
9
+
10
+ vote_service_pool = ServicePool(VoteService)
11
+
12
+ def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
13
+ """
14
+ Lambda handler for listing votes with optional filters.
15
+
16
+ Args:
17
+ event: API Gateway event
18
+ context: Lambda context
19
+ injected_service: Optional VoteService for testing
20
+ """
21
+ try:
22
+ vote_service = injected_service if injected_service else vote_service_pool.get()
23
+ user_id = LambdaEventUtility.get_authenticated_user_id(event)
24
+ tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
25
+ query_params = event.get('queryStringParameters', {}) or {}
26
+
27
+ # Check for target_id in query params
28
+ target_id = query_params.get('target_id')
29
+ if target_id:
30
+ result = vote_service.list_by_target(target_id=target_id)
31
+ else:
32
+ # Default to listing by user
33
+ result = vote_service.list_by_user(user_id=user_id)
34
+
35
+ return service_result_to_response(result)
36
+
37
+ except Exception as e:
38
+ return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
@@ -0,0 +1,3 @@
1
+ # Summarize votes
2
+
3
+ This lambda handler will summarize votes for a given target id. You should run this on a schedule (e.g. every hour) to keep the summaries up to date. Run via event bridge.
@@ -0,0 +1,44 @@
1
+ # src/geek_cafe_saas_sdk/lambda_handlers/votes/update/app.py
2
+
3
+ import json
4
+ from typing import Dict, Any
5
+
6
+ from geek_cafe_saas_sdk.services import VoteService
7
+ from geek_cafe_saas_sdk.lambda_handlers import ServicePool
8
+ from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
9
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
10
+
11
+ vote_service_pool = ServicePool(VoteService)
12
+
13
+ def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
14
+ """
15
+ Lambda handler for updating an existing vote.
16
+
17
+ Args:
18
+ event: API Gateway event
19
+ context: Lambda context
20
+ injected_service: Optional VoteService for testing
21
+ """
22
+ try:
23
+ vote_service = injected_service if injected_service else vote_service_pool.get()
24
+ body = LambdaEventUtility.get_body_from_event(event)
25
+ user_id = LambdaEventUtility.get_authenticated_user_id(event)
26
+ tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
27
+ resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
28
+
29
+ if not resource_id:
30
+ return error_response("Vote ID is required in the path.", "VALIDATION_ERROR", 400)
31
+
32
+ result = vote_service.update(
33
+ resource_id=resource_id,
34
+ tenant_id=tenant_id,
35
+ user_id=user_id,
36
+ updates=body
37
+ )
38
+
39
+ return service_result_to_response(result)
40
+
41
+ except json.JSONDecodeError:
42
+ return error_response("Invalid JSON in request body.", "VALIDATION_ERROR", 400)
43
+ except Exception as e:
44
+ return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
@@ -0,0 +1,9 @@
1
+ # Voting Domain Models
2
+
3
+ from .vote import Vote
4
+ from .vote_summary import VoteSummary
5
+
6
+ __all__ = [
7
+ "Vote",
8
+ "VoteSummary",
9
+ ]
@@ -0,0 +1,231 @@
1
+
2
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
3
+ from boto3_assist.utilities.string_utility import StringUtility
4
+ import datetime as dt
5
+ from typing import Dict, Any
6
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
7
+
8
+ class Vote(BaseModel):
9
+ def __init__(self):
10
+ super().__init__()
11
+ self._content: Dict[str, Any] = {}
12
+ self._target_id: str | None = None
13
+
14
+ # Enhanced voting system fields
15
+ self._vote_type: str = "single_choice" # single_choice, multi_select, ranking, rating
16
+ self._choices: Dict[str, Any] = {} # Flexible choice storage
17
+ self._max_selections: int | None = None # For multi_select limits
18
+
19
+ # Legacy fields (for backward compatibility)
20
+ self._up_vote: int = 0
21
+ self._down_vote: int = 0
22
+
23
+ self._setup_indexes()
24
+
25
+ def _setup_indexes(self):
26
+ primary: DynamoDBIndex = DynamoDBIndex()
27
+ primary.name = "primary"
28
+ primary.partition_key.attribute_name = "pk"
29
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(
30
+ ("vote", self.id)
31
+ )
32
+
33
+ primary.sort_key.attribute_name = "sk"
34
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("vote", self.id))
35
+ self.indexes.add_primary(primary)
36
+
37
+ ## GSI: 1
38
+ # GSI: all votes
39
+ gsi: DynamoDBIndex = DynamoDBIndex()
40
+
41
+ gsi.name = "gsi1"
42
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
43
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("vote", "all"))
44
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
45
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
46
+ ("ts", self.created_utc_ts)
47
+ )
48
+ self.indexes.add_secondary(gsi)
49
+
50
+ ## GSI: 2
51
+ # GSI: all votes by user, sorted by created ts
52
+ gsi: DynamoDBIndex = DynamoDBIndex()
53
+
54
+ gsi.name = "gsi2"
55
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
56
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.user_id))
57
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
58
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
59
+ ("model", "vote"),("ts", self.created_utc_ts)
60
+ )
61
+ self.indexes.add_secondary(gsi)
62
+
63
+ ## GSI: 3
64
+ # GSI: all votes by tenant, sorted by created ts
65
+ gsi: DynamoDBIndex = DynamoDBIndex()
66
+
67
+ gsi.name = "gsi3"
68
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
69
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
70
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
71
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
72
+ ("model", "vote"),("ts", self.created_utc_ts)
73
+ )
74
+ self.indexes.add_secondary(gsi)
75
+
76
+ ## GSI: 4
77
+ # GSI: enforce uniqueness helper - all votes by user+target (one per target per user)
78
+ gsi: DynamoDBIndex = DynamoDBIndex()
79
+
80
+ gsi.name = "gsi4"
81
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
82
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.user_id))
83
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
84
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
85
+ ("target", self.target_id)
86
+ )
87
+ self.indexes.add_secondary(gsi)
88
+
89
+ ## GSI: 5
90
+ # GSI: all votes for a target
91
+ gsi: DynamoDBIndex = DynamoDBIndex()
92
+
93
+ gsi.name = "gsi5"
94
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
95
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("target", self.target_id))
96
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
97
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
98
+ ("ts", self.created_utc_ts)
99
+ )
100
+ self.indexes.add_secondary(gsi)
101
+
102
+ @property
103
+ def content(self) -> Dict[str, Any]:
104
+ return self._content
105
+
106
+ @content.setter
107
+ def content(self, value: Dict[str, Any]):
108
+ self._content = value
109
+
110
+ @property
111
+ def up_vote(self) -> int:
112
+ return self._up_vote
113
+
114
+ @up_vote.setter
115
+ def up_vote(self, value: int):
116
+ self._up_vote = value
117
+
118
+ @property
119
+ def down_vote(self) -> int:
120
+ return self._down_vote
121
+
122
+ @down_vote.setter
123
+ def down_vote(self, value: int):
124
+ self._down_vote = value
125
+
126
+ @property
127
+ def target_id(self) -> str | None:
128
+ return self._target_id
129
+
130
+ @target_id.setter
131
+ def target_id(self, value: str | None):
132
+ self._target_id = value
133
+
134
+ @property
135
+ def vote_type(self) -> str:
136
+ return self._vote_type
137
+
138
+ @vote_type.setter
139
+ def vote_type(self, value: str):
140
+ self._vote_type = value
141
+
142
+ @property
143
+ def choices(self) -> Dict[str, Any]:
144
+ return self._choices
145
+
146
+ @choices.setter
147
+ def choices(self, value: Dict[str, Any]):
148
+ self._choices = value
149
+
150
+ @property
151
+ def max_selections(self) -> int | None:
152
+ return self._max_selections
153
+
154
+ @max_selections.setter
155
+ def max_selections(self, value: int | None):
156
+ self._max_selections = value
157
+
158
+ # Helper methods for different voting patterns
159
+ def set_single_choice(self, choice_id: str, available_choices: list[str] = None):
160
+ """Set a single choice vote (A/B test, single selection)."""
161
+ self.vote_type = "single_choice"
162
+ self.choices = {choice_id: {"selected": True, "value": 1}}
163
+
164
+ # Set legacy fields for backward compatibility
165
+ if available_choices and len(available_choices) >= 2:
166
+ self.up_vote = 1 if choice_id == available_choices[0] else 0
167
+ self.down_vote = 1 if choice_id == available_choices[1] else 0
168
+ else:
169
+ self.up_vote = 1
170
+ self.down_vote = 0
171
+
172
+ def set_multi_select(self, selected_choices: list[str], available_choices: list[str] = None, max_selections: int = None):
173
+ """Set multiple choice selections."""
174
+ self.vote_type = "multi_select"
175
+ self.max_selections = max_selections
176
+
177
+ # Build choices dict
178
+ if available_choices:
179
+ self.choices = {
180
+ choice: {"selected": choice in selected_choices, "value": 1 if choice in selected_choices else 0}
181
+ for choice in available_choices
182
+ }
183
+ else:
184
+ self.choices = {choice: {"selected": True, "value": 1} for choice in selected_choices}
185
+
186
+ # Set legacy fields
187
+ self.up_vote = len(selected_choices)
188
+ self.down_vote = 0
189
+
190
+ def set_ranking(self, ranked_choices: list[str]):
191
+ """Set ranked choices (1st, 2nd, 3rd preference)."""
192
+ self.vote_type = "ranking"
193
+ self.choices = {
194
+ choice: {"rank": idx + 1, "value": len(ranked_choices) - idx}
195
+ for idx, choice in enumerate(ranked_choices)
196
+ }
197
+
198
+ # Set legacy fields
199
+ self.up_vote = len(ranked_choices)
200
+ self.down_vote = 0
201
+
202
+ def set_rating(self, ratings: Dict[str, float]):
203
+ """Set rating votes (1-5 stars per option)."""
204
+ self.vote_type = "rating"
205
+ self.choices = {
206
+ choice: {"rating": rating, "value": rating}
207
+ for choice, rating in ratings.items()
208
+ }
209
+
210
+ # Set legacy fields (average rating)
211
+ avg_rating = sum(ratings.values()) / len(ratings) if ratings else 0
212
+ self.up_vote = int(avg_rating)
213
+ self.down_vote = 0
214
+
215
+ def get_selected_choices(self) -> list[str]:
216
+ """Get list of selected choices for any vote type."""
217
+ if self.vote_type == "single_choice":
218
+ return [choice for choice, data in self.choices.items() if data.get("selected")]
219
+ elif self.vote_type == "multi_select":
220
+ return [choice for choice, data in self.choices.items() if data.get("selected")]
221
+ elif self.vote_type == "ranking":
222
+ # Return choices sorted by rank
223
+ ranked = sorted(self.choices.items(), key=lambda x: x[1].get("rank", 999))
224
+ return [choice for choice, _ in ranked]
225
+ elif self.vote_type == "rating":
226
+ return list(self.choices.keys())
227
+ return []
228
+
229
+ def get_choice_value(self, choice_id: str) -> Any:
230
+ """Get the value for a specific choice."""
231
+ return self.choices.get(choice_id, {}).get("value", 0)