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,776 @@
1
+ """Lambda Event Utilities
2
+
3
+ This module provides utility functions for working with AWS Lambda event payloads.
4
+ """
5
+
6
+ from typing import List, Dict, Any, Optional, Union
7
+ import re
8
+ import json
9
+ from aws_lambda_powertools import Logger
10
+ from geek_cafe_saas_sdk.utilities.custom_exceptions import Error
11
+ from geek_cafe_saas_sdk.utilities.http_status_code import HttpStatusCodes
12
+ from geek_cafe_saas_sdk.utilities.environment_variables import (
13
+ EnvironmentVariables,
14
+ )
15
+ from geek_cafe_saas_sdk.utilities.jwt_utility import JwtUtility
16
+
17
+ from boto3_assist.utilities.serialization_utility import JsonConversions
18
+
19
+
20
+ logger = Logger(__name__)
21
+
22
+
23
+ TABLE_NAME_NOT_AVAILABLE = "FAKE_TABLE_ENVIRONMENT_VAR_IS_NOT_SET"
24
+
25
+
26
+ class LambdaEventUtility:
27
+ """Utility class for extracting and processing data from AWS Lambda event payloads.
28
+
29
+ This class provides static methods to extract common data elements from Lambda events,
30
+ including user information, path parameters, query parameters, and more.
31
+ """
32
+
33
+ @staticmethod
34
+ def get_dynamodb_table_name() -> str | None:
35
+ """Get the application DynamoDB table name from environment variables.
36
+
37
+ Returns:
38
+ str | None: The DynamoDB table name or TABLE_NAME_NOT_AVAILABLE if not set.
39
+ Returns None if the value is not a string.
40
+ """
41
+ value = EnvironmentVariables.get_dynamodb_table_name()
42
+ if value is None:
43
+ raise Error("The DynamoDB table name is not set")
44
+ if not isinstance(value, str):
45
+ raise Error("The DynamoDB table name is not a string")
46
+ return value
47
+
48
+
49
+
50
+ @staticmethod
51
+ def get_file_name_from_event(event: Dict[str, Any]) -> Optional[str]:
52
+ """Extract the file name from the event payload.
53
+
54
+ Args:
55
+ event: The Lambda event dictionary
56
+
57
+ Returns:
58
+ The file name as a string, or None if not found or not a string
59
+ """
60
+ value = LambdaEventUtility.get_value_from_event(event, "file_name")
61
+ if not isinstance(value, str):
62
+ return None
63
+ return value
64
+
65
+ @staticmethod
66
+ def get_file_type_from_event(event: Dict[str, Any]) -> Optional[str]:
67
+ """Extract the file type from the event payload.
68
+
69
+ Args:
70
+ event: The Lambda event dictionary
71
+
72
+ Returns:
73
+ The file type as a string, or None if not found or not a string
74
+ """
75
+ value = LambdaEventUtility.get_value_from_event(event, "file_type")
76
+ if not isinstance(value, str):
77
+ return None
78
+ return value
79
+
80
+ @staticmethod
81
+ def get_method_type_from_event(event: Dict[str, Any]) -> Optional[str]:
82
+ """Extract the HTTP method type from the event payload.
83
+
84
+ Args:
85
+ event: The Lambda event dictionary
86
+
87
+ Returns:
88
+ The HTTP method (GET, POST, PUT, DELETE, etc.) as a string, or None if not found or not a string
89
+ """
90
+ value = LambdaEventUtility.get_value_from_event(event, "method_type")
91
+ if not isinstance(value, str):
92
+ return None
93
+ return value
94
+
95
+ @staticmethod
96
+ def get_token_use(event: Dict[str, Any]) -> Optional[str]:
97
+ """Extract the token use information from the event payload.
98
+
99
+ Retrieves the token_use claim from the authorizer context, which indicates
100
+ whether the token is an 'id', 'access', or other type of token.
101
+
102
+ Args:
103
+ event: The Lambda event dictionary
104
+
105
+ Returns:
106
+ The token use value as a string, or None if not found or not a string
107
+ """
108
+ path = "requestContext/authorizer/claims/token_use"
109
+ value = LambdaEventUtility.get_value_from_event(event, path)
110
+
111
+ if not isinstance(value, str):
112
+ return None
113
+ return value
114
+
115
+ @staticmethod
116
+ def get_target_user_id(event: Dict[str, Any]) -> Optional[str]:
117
+ """Extract the target user ID from the event path parameters.
118
+
119
+ Target users are the users we are performing an action on, such as CRUD operations.
120
+ The user ID is typically found in the route path parameters.
121
+
122
+ Args:
123
+ event: The Lambda event dictionary
124
+
125
+ Returns:
126
+ The target user ID as a string, or None if not found or not a string
127
+ """
128
+ value = LambdaEventUtility.get_value_from_path_parameters(event, "user-id")
129
+ if not isinstance(value, str):
130
+ return None
131
+ return value
132
+
133
+ @staticmethod
134
+ def get_target_tenant_id(event: Dict[str, Any]) -> Optional[str]:
135
+ """Extract the target tenant ID from the event path parameters.
136
+
137
+ Target tenants are the tenants we are performing an action on, such as CRUD operations.
138
+ The tenant ID is typically found in the route path parameters.
139
+
140
+ Args:
141
+ event: The Lambda event dictionary
142
+
143
+ Returns:
144
+ The target tenant ID as a string, or None if not found or not a string
145
+ """
146
+ value = LambdaEventUtility.get_value_from_path_parameters(event, "tenant-id")
147
+ if not isinstance(value, str):
148
+ return None
149
+ return value
150
+
151
+ @staticmethod
152
+ def get_authenticated_user_id(event: Dict[str, Any]) -> Optional[str]:
153
+ """Extract the authenticated user ID from the event claims.
154
+
155
+ Retrieves the user ID from the custom:user_id claim in the authorizer context.
156
+
157
+ Args:
158
+ event: The Lambda event dictionary
159
+
160
+ Returns:
161
+ The authenticated user ID as a string, or None if not found or not a string
162
+ """
163
+ return LambdaEventUtility.get_claims_data(
164
+ event, "custom:user_id"
165
+ )
166
+
167
+ @staticmethod
168
+ def get_authenticated_user_email(event: Dict[str, Any]) -> Optional[str]:
169
+ """Extract the authenticated user email from the event claims.
170
+
171
+ Retrieves the user email from the email claim in the authorizer context.
172
+
173
+ Args:
174
+ event: The Lambda event dictionary
175
+
176
+ Returns:
177
+ The authenticated user email as a string, or None if not found or not a string
178
+ """
179
+ return LambdaEventUtility.get_claims_data(event, "email")
180
+
181
+ @staticmethod
182
+ def get_authenticated_user_tenant_id(event: Dict[str, Any]) -> Optional[str]:
183
+ """Extract the authenticated user's tenant ID from the event claims.
184
+
185
+ Retrieves the tenant ID from the custom:tenant_id claim in the authorizer context.
186
+
187
+ Args:
188
+ event: The Lambda event dictionary
189
+
190
+ Returns:
191
+ The authenticated user's tenant ID as a string, or None if not found or not a string
192
+ """
193
+ return LambdaEventUtility.get_claims_data(
194
+ event, "custom:tenant_id"
195
+ )
196
+
197
+ @staticmethod
198
+ def get_authenticated_user_roles(event: Dict[str, Any]) -> List[str]:
199
+ """Extract the authenticated user's roles from the event claims.
200
+
201
+ Retrieves the user roles from the custom:user_roles claim in the authorizer context.
202
+ The roles are returned as a comma-separated string and converted to a list.
203
+
204
+ Args:
205
+ event: The Lambda event dictionary
206
+
207
+ Returns:
208
+ A list of role strings, or an empty list if no roles are found
209
+ """
210
+ roles = LambdaEventUtility.get_claims_data(
211
+ event, "custom:user_roles"
212
+ )
213
+ if roles:
214
+ return str(roles).split(",")
215
+
216
+ return []
217
+
218
+ @staticmethod
219
+ def get_authenticated_user_context(event: Dict[str, Any]) -> tuple[Optional[str], Optional[str], List[str]]:
220
+ """Extract the authenticated user's context (tenant_id, user_id, roles) from the event.
221
+
222
+ Convenience method that retrieves all three common authentication values
223
+ from the authorizer context in a single call.
224
+
225
+ Args:
226
+ event: The Lambda event dictionary
227
+
228
+ Returns:
229
+ A tuple containing (tenant_id, user_id, roles)
230
+ - tenant_id: The user's tenant ID (or None)
231
+ - user_id: The user's ID (or None)
232
+ - roles: A list of role strings (or empty list)
233
+ """
234
+ tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
235
+ user_id = LambdaEventUtility.get_authenticated_user_id(event)
236
+ roles = LambdaEventUtility.get_authenticated_user_roles(event)
237
+
238
+ return tenant_id, user_id, roles
239
+
240
+ @staticmethod
241
+ def get_claims(event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
242
+ """Extract the complete claims dictionary from the event.
243
+
244
+ Retrieves all claims from the authorizer context in the request context.
245
+
246
+ Args:
247
+ event: The Lambda event dictionary
248
+
249
+ Returns:
250
+ The claims dictionary, or None if not found or invalid format
251
+
252
+ Raises:
253
+ ValueError: If the event payload is empty
254
+ """
255
+ if not event or len(event) == 0:
256
+ raise ValueError("The event payload is empty")
257
+
258
+ path = "requestContext/authorizer/claims"
259
+ value = LambdaEventUtility.get_value_from_event(event, path)
260
+ if isinstance(value, str):
261
+ value = None
262
+ return value
263
+
264
+ @staticmethod
265
+ def get_claims_data(event: Dict[str, Any], key: str) -> Optional[str]:
266
+ """Extract a specific claim value from the event claims.
267
+
268
+ First attempts to find the claim in the authorizer context, then falls back to
269
+ extracting it from the JWT token if available.
270
+
271
+ Args:
272
+ event: The Lambda event dictionary
273
+ key: The claim key to extract
274
+
275
+ Returns:
276
+ The claim value as a string, or None if not found
277
+
278
+ Raises:
279
+ ValueError: If the event payload is empty
280
+ Error: If the claim cannot be found or if there's an error processing the JWT token
281
+ """
282
+ try:
283
+ if not event or len(event) == 0:
284
+ raise ValueError("The event payload is empty")
285
+
286
+ path = f"requestContext/authorizer/claims/{key}"
287
+ value = LambdaEventUtility.get_value_from_event(event, path)
288
+
289
+ if not value:
290
+ value = LambdaEventUtility.get_value_from_token(event, key)
291
+
292
+ if value:
293
+ logger.debug(
294
+ {
295
+ "message": f"Found key in JWT with {key}",
296
+ f"{key}": value,
297
+ }
298
+ )
299
+ if isinstance(value, str):
300
+ return value
301
+ else:
302
+ return str(value)
303
+ else:
304
+ raise Error(
305
+ message=f"Failed to locate {key} info in JWT Token",
306
+ status_code=HttpStatusCodes.HTTP_404_NOT_FOUND.value
307
+ )
308
+ except Exception as e:
309
+ raise Error(
310
+ message=f"Failed to locate {key} info in JWT Token",
311
+ status_code=HttpStatusCodes.HTTP_401_UNAUTHENTICATED.value,
312
+ details=str(e)
313
+ ) from e
314
+
315
+ @staticmethod
316
+ def get_value_from_token(event: Dict[str, Any], key: str) -> Optional[str]:
317
+ """Extract a specific claim value directly from the JWT token in the Authorization header.
318
+
319
+ This is used as a fallback when the claim is not available in the authorizer context.
320
+
321
+ Args:
322
+ event: The Lambda event dictionary
323
+ key: The claim key to extract from the token
324
+
325
+ Returns:
326
+ The claim value as a string, or None if not found or if the token is invalid
327
+
328
+ Raises:
329
+ ValueError: If the event payload is empty
330
+ """
331
+ try:
332
+ if not event or len(event) == 0:
333
+ raise ValueError("The event payload is empty")
334
+
335
+ path = "headers/Authorization"
336
+ jwt_token = LambdaEventUtility.get_value_from_event(event, path)
337
+ value: Optional[str] = None
338
+ if jwt_token and isinstance(jwt_token, str):
339
+ # Use the generic JWT utility to parse the token
340
+ value = JwtUtility.get_claim_from_token(jwt_token, key)
341
+ logger.debug(f"Extracted claim '{key}' from JWT token: {value is not None}")
342
+
343
+ if value:
344
+ return value
345
+
346
+ else:
347
+ raise Error(
348
+ {
349
+ "status_code": HttpStatusCodes.HTTP_404_NOT_FOUND.value,
350
+ "message": f"Failed to locate {key} info it JWT Token",
351
+ }
352
+ )
353
+ except Exception as e:
354
+ raise Error(
355
+ {
356
+ "status_code": HttpStatusCodes.HTTP_401_UNAUTHENTICATED.value,
357
+ "message": f"Failed to locate {key} info it JWT Token",
358
+ "exception": str(e),
359
+ }
360
+ ) from e
361
+
362
+ @staticmethod
363
+ def get_user_email_from_event(event) -> str | None:
364
+ """Get the user email from the event for claims"""
365
+ if not event:
366
+ return None
367
+
368
+ token_use = LambdaEventUtility.get_token_use(event)
369
+ path: str | None = None
370
+ if token_use == "id":
371
+ path = "requestContext/authorizer/claims/email"
372
+
373
+ elif token_use == "access":
374
+ path = "requestContext/authorizer/claims/client_id"
375
+
376
+ value: str | None = None
377
+ if path:
378
+ value = str(LambdaEventUtility.get_value_from_event(event, path))
379
+ else:
380
+ value = LambdaEventUtility.get_claims_data(event, "email")
381
+
382
+ if not isinstance(value, str):
383
+ return None
384
+ return value
385
+
386
+ @staticmethod
387
+ def get_message_id_from_event(event: dict, index: int = 0) -> str | None:
388
+ """Gets the message id from an event"""
389
+ records = event.get("Records")
390
+ if records:
391
+ item: dict = records[index]
392
+ value = item.get("messageId")
393
+ if not isinstance(value, str):
394
+ return None
395
+ return value
396
+
397
+ return None
398
+
399
+
400
+
401
+
402
+ @staticmethod
403
+ def get_http_method_from_event(event: Dict[str, Any]) -> Optional[str]:
404
+ """Extract the HTTP method from the event.
405
+
406
+ Returns the HTTP method (e.g., GET, POST, PUT, DELETE) from the event.
407
+
408
+ Args:
409
+ event: The Lambda event dictionary
410
+
411
+ Returns:
412
+ The HTTP method as a string, or None if not found or invalid format
413
+ """
414
+ value = LambdaEventUtility.get_value_from_event_ex(event, "httpMethod")
415
+ if not isinstance(value, str):
416
+ return None
417
+ return value
418
+
419
+ @staticmethod
420
+ def get_resource_path_from_event(event: Dict[str, Any]) -> Optional[str]:
421
+ """Extract the resource path from the event.
422
+
423
+ Returns the actual resource path (route) from the event.
424
+
425
+ Args:
426
+ event: The Lambda event dictionary
427
+
428
+ Returns:
429
+ The resource path as a string, or None if not found or invalid format
430
+ """
431
+ value = LambdaEventUtility.get_value_from_event_ex(event, "path")
432
+ if not isinstance(value, str):
433
+ return None
434
+ return value
435
+
436
+ @staticmethod
437
+ def get_resource_pattern_from_event(event: Dict[str, Any]) -> Optional[str]:
438
+ """Extract the resource pattern from the event.
439
+
440
+ Returns the resource path with placeholders instead of actual values.
441
+ For example, instead of returning:
442
+ "/tenants/d46814e8-d061-7036-3c81-e37152773912/subscriptions/8925f80c344f0096a5fe6bfb159cdd370ad888c7d914bb6568797701003f155b"
443
+
444
+ it returns:
445
+ "/tenants/{tenant-id}/subscriptions/{subscription-id}"
446
+
447
+ Args:
448
+ event: The Lambda event dictionary
449
+
450
+ Returns:
451
+ The resource pattern as a string, or None if not found or invalid format
452
+ """
453
+ value = LambdaEventUtility.get_value_from_event_ex(event, "resourcePath")
454
+ if not isinstance(value, str):
455
+ return None
456
+ return value
457
+
458
+ @staticmethod
459
+ def get_value_from_event_ex(event: Dict[str, Any], key: str) -> Union[str, Dict[str, Any], None]:
460
+ """Get a value from the event, checking both direct key and requestContext path.
461
+
462
+ This is an extended version of get_value_from_event that also checks in the requestContext
463
+ if the value is not found in the main event object.
464
+
465
+ Args:
466
+ event: The Lambda event dictionary
467
+ key: The key to look for in the event
468
+
469
+ Returns:
470
+ The value if found, or None if not found
471
+ """
472
+ value = LambdaEventUtility.get_value_from_event(event, key)
473
+ if not value:
474
+ key = f"requestContext/{key}"
475
+ value = LambdaEventUtility.get_value_from_event(event, key)
476
+ return value
477
+
478
+
479
+
480
+
481
+
482
+
483
+
484
+ @staticmethod
485
+ def get_value_from_event(
486
+ event: Dict[str, Any],
487
+ key: str | List[str],
488
+ ) -> str | Dict[str, Any] | None:
489
+ """
490
+ get the value from the event payload
491
+ """
492
+ logger.debug({"source": "get_value_from_event", "event": event, "key": key})
493
+
494
+ if not event:
495
+ return None
496
+
497
+ if "event" in event:
498
+ event = event["event"]
499
+
500
+ if not key:
501
+ logger.warning(
502
+ {
503
+ "source": "get_value_from_event",
504
+ "warning": "missing key for lookup",
505
+ "event": event,
506
+ "key": key,
507
+ }
508
+ )
509
+ return None
510
+
511
+ if "/" in key:
512
+ key = str(key).split("/")
513
+
514
+ elif "." in key:
515
+ key = str(key).split(".")
516
+
517
+ if isinstance(key, str):
518
+ if key in event:
519
+ return event[key]
520
+ else:
521
+ logger.debug(f'key "{key}" is not in event, checking body.')
522
+
523
+ return LambdaEventUtility.get_value_from_event_body(event, key)
524
+ elif isinstance(key, list):
525
+ # loop through
526
+ value: str | Dict[str, Any] | None = event
527
+ for k in key:
528
+ if isinstance(value, dict):
529
+ value = LambdaEventUtility.get_value_from_event(value, k)
530
+ if not value:
531
+ break
532
+
533
+ return value
534
+
535
+ return None
536
+
537
+
538
+
539
+ @staticmethod
540
+ def get_value_from_header(event, key, default=None) -> str | dict | None:
541
+ value = LambdaEventUtility.search_payload_for_container_element(
542
+ event, "headers", key, default
543
+ )
544
+
545
+ return value
546
+
547
+ @staticmethod
548
+ def get_value_from_path_parameters(event, key, default=None) -> str | dict | None:
549
+ value = LambdaEventUtility.search_payload_for_container_element(
550
+ event, "pathParameters", key, default
551
+ )
552
+ if value is None:
553
+ path = LambdaEventUtility.get_resource_path_from_event(event=event)
554
+ pattern = LambdaEventUtility.get_resource_pattern_from_event(event=event)
555
+ if path and pattern:
556
+ value = LambdaEventUtility.extract_value_from_path(
557
+ path=path, pattern=pattern, variable_name=key
558
+ )
559
+ return value
560
+
561
+ @staticmethod
562
+ def extract_value_from_path(
563
+ path: str, pattern: str, variable_name: str
564
+ ) -> str | None:
565
+ # Convert the pattern into a regex pattern to match the variable placeholders
566
+ regex_pattern = re.sub(
567
+ r"\{([^}]+)\}",
568
+ lambda m: f"(?P<{m.group(1).replace('-', '_')}>[^/]+)",
569
+ pattern,
570
+ )
571
+
572
+ # Replace hyphens with underscores in the variable name for matching
573
+ variable_name = variable_name.replace("-", "_")
574
+
575
+ # Use regex to match the path against the pattern
576
+ match = re.match(regex_pattern, path)
577
+
578
+ if match:
579
+ # Extract the value corresponding to the variable_name
580
+ try:
581
+ return match.group(variable_name)
582
+ except: # noqa: E722, pylint: disable=W0702
583
+ pass
584
+ # else:
585
+ # raise ValueError(f"No match found for the variable '{variable_name}' in the provided path and pattern.")
586
+ return None
587
+
588
+ @staticmethod
589
+ def get_value_from_query_string_parameters(
590
+ event, key, default=None
591
+ ) -> str | dict | None:
592
+ value = LambdaEventUtility.search_payload_for_container_element(
593
+ event, "queryStringParameters", key, default
594
+ )
595
+
596
+ return value
597
+
598
+ @staticmethod
599
+ def get_value_from_multi_value_query_string_parameters(
600
+ event, key, default=None
601
+ ) -> str | dict | None:
602
+ value = LambdaEventUtility.search_payload_for_container_element(
603
+ event, "multiValueQueryStringParameters", key, default
604
+ )
605
+
606
+ return value
607
+
608
+ @staticmethod
609
+ def search_payload_for_container_element(
610
+ event, container, key, default=None
611
+ ) -> str | dict | None:
612
+ logger.debug(
613
+ {
614
+ "action": "search_payload_for_container_element",
615
+ "event": event,
616
+ "container": container,
617
+ "key": key,
618
+ "metric_filter": "search_payload_for_container_element",
619
+ }
620
+ )
621
+
622
+ events = []
623
+ events.append(event)
624
+
625
+ if isinstance(event, dict):
626
+ if "message" in event:
627
+ event = event["message"]
628
+ events.append(event)
629
+ if "Records" in event:
630
+ event = event["Records"][0]
631
+ events.append(event)
632
+ if "body" in event:
633
+ body = event["body"]
634
+ if isinstance(body, str):
635
+ body = JsonConversions.string_to_json_obj(body)
636
+ if isinstance(body, dict):
637
+ events.append(body)
638
+ if "requestContext" in body:
639
+ events.append(body["requestContext"])
640
+
641
+ value = None
642
+ for e in events:
643
+ if container in e:
644
+ if container:
645
+ item = e[container]
646
+ if item is not None and key in item:
647
+ value = item[key]
648
+
649
+ logger.debug(
650
+ {
651
+ "action": "search_payload_for_container_element",
652
+ "event": event,
653
+ "container": container,
654
+ "key": key,
655
+ "value": value,
656
+ "default": default,
657
+ "metric_filter": "search_payload_for_container_element",
658
+ }
659
+ )
660
+
661
+ if value is None:
662
+ value = default
663
+
664
+ return value
665
+
666
+ @staticmethod
667
+ def get_value_from_event_body(event: dict, key: str) -> str | dict | None:
668
+ """Gets the value from the body section of the event"""
669
+ body = LambdaEventUtility.get_body_from_event(event)
670
+ if body is not None and key in body:
671
+ return body[key]
672
+
673
+ logger.debug(
674
+ {
675
+ "info": "Could not find key in event",
676
+ "key": key,
677
+ "found": "False",
678
+ "event": event,
679
+ }
680
+ )
681
+ return None
682
+
683
+ @staticmethod
684
+ def get_body_from_event(event, raise_on_error=True) -> dict | None:
685
+ """
686
+ Get the payload from the event. If more than one record is found,
687
+ the first record is returned.
688
+ """
689
+ tmp = event
690
+ if "Records" in tmp:
691
+ tmp = tmp["Records"][0]
692
+
693
+ if "body" in tmp:
694
+ tmp = tmp["body"]
695
+
696
+ if isinstance(tmp, str):
697
+ try:
698
+ tmp = JsonConversions.string_to_json_obj(tmp, raise_on_error)
699
+ except Exception as e: # noqa: E722, pylint: disable=W0702
700
+ raise ValueError("Invalid json body in the payload.") from e
701
+ return tmp
702
+
703
+ @staticmethod
704
+ def update_event_info(event: dict, key: str, value: str | dict) -> dict:
705
+ """
706
+ update the event with the key and value
707
+ """
708
+ original = event
709
+ status = "processing"
710
+ outcome = "success"
711
+ logger.debug({"update_event_info": {"status": f"{status}", "event": event}})
712
+ body: dict | None = event
713
+ if "body" in event:
714
+ body = LambdaEventUtility.get_body_from_event(event)
715
+
716
+ if body is not None and isinstance(body, dict):
717
+ body[key] = value
718
+ else:
719
+ outcome = "failed / not found"
720
+
721
+ logger.debug(
722
+ {
723
+ "update_event_info": {
724
+ "status": f"{status}",
725
+ "outcome": f"{outcome}",
726
+ "event": body,
727
+ "original": original,
728
+ }
729
+ }
730
+ )
731
+
732
+ return body or original
733
+
734
+ @staticmethod
735
+ def to_snake_case_for_backend(event: Dict[str, Any] | None) -> Dict[str, Any]:
736
+ """
737
+ Convert UI payloads from camelCase to snake_case for backend processing.
738
+
739
+ Args:
740
+ event: The event payload from the UI in camelCase format
741
+
742
+ Returns:
743
+ The payload converted to snake_case format
744
+
745
+ Raises:
746
+ ValueError: If the event is None or not a dictionary
747
+ """
748
+ if event is None:
749
+ raise ValueError("Event payload cannot be None")
750
+ if not isinstance(event, dict):
751
+ raise ValueError(f"Event payload must be a dictionary, got {type(event)}")
752
+ if not event:
753
+ return {}
754
+
755
+ return JsonConversions.json_camel_to_snake(event)
756
+
757
+ @staticmethod
758
+ def to_camel_case_for_ui(payload: Union[List[Dict[str, Any]], Dict[str, Any], None]) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
759
+ """
760
+ Convert backend data from snake_case to camelCase for UI consumption.
761
+
762
+ Args:
763
+ payload: The backend data in snake_case format (dict or list of dicts)
764
+
765
+ Returns:
766
+ The payload converted to camelCase format, maintaining the same structure
767
+
768
+ Raises:
769
+ ValueError: If the payload is None
770
+ """
771
+ if payload is None:
772
+ raise ValueError("Payload cannot be None")
773
+ if not payload:
774
+ return payload # Return empty dict/list as-is
775
+
776
+ return JsonConversions.json_snake_to_camel(payload)