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,337 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ MIT License. See Project Root for the license information.
4
+
5
+ ChatChannel model for internal team messaging (Slack-like functionality).
6
+ """
7
+
8
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
9
+ from boto3_assist.utilities.string_utility import StringUtility
10
+ import datetime as dt
11
+ from typing import List, Optional, Dict, Any
12
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
13
+
14
+
15
+ class ChatChannel(BaseModel):
16
+ """
17
+ ChatChannel model for Slack-like team messaging.
18
+
19
+ Optimized for high-volume, real-time messaging with symmetric membership.
20
+ Messages are stored separately to avoid document size limits.
21
+
22
+ Features:
23
+ - Public channels (visible to all team members)
24
+ - Private channels (invite-only)
25
+ - Direct messages (1-on-1)
26
+ - Member management
27
+ - Channel settings (announcements-only, auto-join, etc.)
28
+ - Archive/unarchive
29
+ - Activity tracking
30
+
31
+ Note: Messages are stored in separate ChatMessage documents.
32
+ """
33
+
34
+ def __init__(self):
35
+ super().__init__()
36
+ self._name: str | None = None
37
+ self._description: str | None = None
38
+ self._channel_type: str = "public" # public, private, direct
39
+
40
+ # Membership tracking (members stored as adjacent records)
41
+ self._member_count: int = 0 # Cached count for display
42
+ self._created_by: str | None = None
43
+
44
+ # Activity tracking (no embedded messages)
45
+ self._last_message_id: str | None = None
46
+ self._last_message_at: float | None = None
47
+ self._message_count: int = 0
48
+
49
+ # Channel settings
50
+ self._is_archived: bool = False
51
+ self._is_default: bool = False # Auto-join for new users
52
+ self._is_announcement: bool = False # Only admins can post
53
+
54
+ # Metadata
55
+ self._topic: str | None = None # Channel topic/purpose
56
+ self._icon: str | None = None # Emoji or image URL
57
+
58
+ # Sharding configuration (optional, for high-traffic channels)
59
+ self._sharding_config: Dict[str, Any] | None = None
60
+
61
+ self._setup_indexes()
62
+
63
+ def _setup_indexes(self):
64
+ """Setup DynamoDB indexes for efficient querying."""
65
+
66
+ # Primary index: channels by ID
67
+ primary: DynamoDBIndex = DynamoDBIndex()
68
+ primary.name = "primary"
69
+ primary.partition_key.attribute_name = "pk"
70
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("channel", self.id))
71
+ primary.sort_key.attribute_name = "sk"
72
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("channel", self.id))
73
+ self.indexes.add_primary(primary)
74
+
75
+ # GSI1: Query by tenant and type (list all public/private channels)
76
+ gsi: DynamoDBIndex = DynamoDBIndex()
77
+ gsi.name = "gsi1"
78
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
79
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
80
+ ("tenant", self.tenant_id),
81
+ ("type", self.channel_type)
82
+ )
83
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
84
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
85
+ ("ts", self.last_message_at or self.created_utc_ts)
86
+ )
87
+ self.indexes.add_secondary(gsi)
88
+
89
+ # GSI2: Query all channels by tenant (for admin views)
90
+ gsi: DynamoDBIndex = DynamoDBIndex()
91
+ gsi.name = "gsi2"
92
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
93
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
94
+ ("tenant", self.tenant_id)
95
+ )
96
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
97
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
98
+ ("model", "channel"),
99
+ ("name", self.name)
100
+ )
101
+ self.indexes.add_secondary(gsi)
102
+
103
+ # GSI3: Query default channels (auto-join for new users)
104
+ gsi: DynamoDBIndex = DynamoDBIndex()
105
+ gsi.name = "gsi3"
106
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
107
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
108
+ ("tenant", self.tenant_id),
109
+ ("default", "1" if self.is_default else "0")
110
+ )
111
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
112
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
113
+ ("ts", self.created_utc_ts)
114
+ )
115
+ self.indexes.add_secondary(gsi)
116
+
117
+ # GSI4: Query archived channels
118
+ gsi: DynamoDBIndex = DynamoDBIndex()
119
+ gsi.name = "gsi4"
120
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
121
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
122
+ ("tenant", self.tenant_id),
123
+ ("archived", "1" if self.is_archived else "0")
124
+ )
125
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
126
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
127
+ ("ts", self.last_message_at or self.created_utc_ts)
128
+ )
129
+ self.indexes.add_secondary(gsi)
130
+
131
+ # Name
132
+ @property
133
+ def name(self) -> str | None:
134
+ return self._name
135
+
136
+ @name.setter
137
+ def name(self, value: str | None):
138
+ self._name = value
139
+
140
+ # Description
141
+ @property
142
+ def description(self) -> str | None:
143
+ return self._description
144
+
145
+ @description.setter
146
+ def description(self, value: str | None):
147
+ self._description = value
148
+
149
+ # Channel Type
150
+ @property
151
+ def channel_type(self) -> str:
152
+ return self._channel_type
153
+
154
+ @channel_type.setter
155
+ def channel_type(self, value: str | None):
156
+ valid_types = ["public", "private", "direct"]
157
+ if value in valid_types:
158
+ self._channel_type = value
159
+ else:
160
+ self._channel_type = "public" # default
161
+
162
+ # Member Count (cached for display)
163
+ @property
164
+ def member_count(self) -> int:
165
+ return self._member_count
166
+
167
+ @member_count.setter
168
+ def member_count(self, value: int | None):
169
+ self._member_count = value if isinstance(value, int) else 0
170
+
171
+ # Created By
172
+ @property
173
+ def created_by(self) -> str | None:
174
+ return self._created_by
175
+
176
+ @created_by.setter
177
+ def created_by(self, value: str | None):
178
+ self._created_by = value
179
+
180
+ # Last Message ID
181
+ @property
182
+ def last_message_id(self) -> str | None:
183
+ return self._last_message_id
184
+
185
+ @last_message_id.setter
186
+ def last_message_id(self, value: str | None):
187
+ self._last_message_id = value
188
+
189
+ # Last Message At
190
+ @property
191
+ def last_message_at(self) -> float | None:
192
+ return self._last_message_at
193
+
194
+ @last_message_at.setter
195
+ def last_message_at(self, value: float | None):
196
+ self._last_message_at = value
197
+
198
+ # Message Count
199
+ @property
200
+ def message_count(self) -> int:
201
+ return self._message_count
202
+
203
+ @message_count.setter
204
+ def message_count(self, value: int | None):
205
+ self._message_count = value if value is not None else 0
206
+
207
+ # Is Archived
208
+ @property
209
+ def is_archived(self) -> bool:
210
+ return self._is_archived
211
+
212
+ @is_archived.setter
213
+ def is_archived(self, value: bool | None):
214
+ self._is_archived = value if value is not None else False
215
+
216
+ # Is Default
217
+ @property
218
+ def is_default(self) -> bool:
219
+ return self._is_default
220
+
221
+ @is_default.setter
222
+ def is_default(self, value: bool | None):
223
+ self._is_default = value if value is not None else False
224
+
225
+ # Is Announcement
226
+ @property
227
+ def is_announcement(self) -> bool:
228
+ return self._is_announcement
229
+
230
+ @is_announcement.setter
231
+ def is_announcement(self, value: bool | None):
232
+ self._is_announcement = value if value is not None else False
233
+
234
+ # Topic
235
+ @property
236
+ def topic(self) -> str | None:
237
+ return self._topic
238
+
239
+ @topic.setter
240
+ def topic(self, value: str | None):
241
+ self._topic = value
242
+
243
+ # Icon
244
+ @property
245
+ def icon(self) -> str | None:
246
+ return self._icon
247
+
248
+ @icon.setter
249
+ def icon(self, value: str | None):
250
+ self._icon = value
251
+
252
+ # Sharding Config
253
+ @property
254
+ def sharding_config(self) -> Dict[str, Any] | None:
255
+ """
256
+ Sharding configuration for high-traffic channels.
257
+
258
+ Example:
259
+ {
260
+ "enabled": True,
261
+ "bucket_span": "day", # "day" or "hour"
262
+ "shard_count": 4, # 1, 2, 4, or 8
263
+ "enabled_at": 1729123200.0
264
+ }
265
+
266
+ Returns:
267
+ Sharding config dict or None if not sharded
268
+ """
269
+ return self._sharding_config
270
+
271
+ @sharding_config.setter
272
+ def sharding_config(self, value: Dict[str, Any] | None):
273
+ if value is None:
274
+ self._sharding_config = None
275
+ elif isinstance(value, dict):
276
+ self._sharding_config = value
277
+ else:
278
+ self._sharding_config = None
279
+
280
+ # Helper Methods
281
+
282
+ def increment_member_count(self):
283
+ """Increment the cached member count."""
284
+ self._member_count += 1
285
+
286
+ def decrement_member_count(self):
287
+ """Decrement the cached member count."""
288
+ if self._member_count > 0:
289
+ self._member_count -= 1
290
+
291
+ def increment_message_count(self):
292
+ """Increment the message count for this channel."""
293
+ self._message_count += 1
294
+
295
+ def update_last_message(self, message_id: str, timestamp: float):
296
+ """
297
+ Update the last message tracking.
298
+
299
+ Args:
300
+ message_id: ID of the last message
301
+ timestamp: Timestamp of the message
302
+ """
303
+ self._last_message_id = message_id
304
+ self._last_message_at = timestamp
305
+ self.increment_message_count()
306
+
307
+ def is_sharded(self) -> bool:
308
+ """
309
+ Check if this channel uses message sharding.
310
+
311
+ Returns:
312
+ True if sharding is enabled, False otherwise
313
+ """
314
+ return (self._sharding_config is not None
315
+ and self._sharding_config.get("enabled", False))
316
+
317
+ def get_bucket_span(self) -> str | None:
318
+ """
319
+ Get the time bucket span for sharded messages.
320
+
321
+ Returns:
322
+ "day" or "hour" if sharded, None otherwise
323
+ """
324
+ if not self.is_sharded():
325
+ return None
326
+ return self._sharding_config.get("bucket_span", "day")
327
+
328
+ def get_shard_count(self) -> int:
329
+ """
330
+ Get the number of shards per bucket.
331
+
332
+ Returns:
333
+ Shard count (1-8) if sharded, 1 otherwise
334
+ """
335
+ if not self.is_sharded():
336
+ return 1
337
+ return self._sharding_config.get("shard_count", 1)
@@ -0,0 +1,180 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ MIT License. See Project Root for the license information.
4
+
5
+ ChatChannelMember model for scalable channel membership using single-table design.
6
+ """
7
+
8
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
9
+ import datetime as dt
10
+ from typing import Optional
11
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
12
+
13
+
14
+ class ChatChannelMember(BaseModel):
15
+ """
16
+ ChatChannelMember model for scalable membership tracking.
17
+
18
+ Single-table design pattern:
19
+ - PK: channel#<channel_id>
20
+ - SK: member#<user_id>
21
+ - GSI1PK: user#<user_id> (for "what channels am I in?")
22
+ - GSI1SK: channel#<channel_id>#<joined_at>
23
+
24
+ Benefits:
25
+ - Unlimited members per channel
26
+ - Fast membership checks (single GetItem)
27
+ - Member metadata (role, permissions, preferences)
28
+ - Efficient pagination
29
+ - Concurrent updates without contention
30
+ """
31
+
32
+ def __init__(self):
33
+ super().__init__()
34
+ # Note: Uses normal UUID id, but pk/sk create adjacent record pattern
35
+ self._channel_id: str | None = None
36
+ self._user_id: str | None = None
37
+
38
+ # Member metadata
39
+ self._role: str = "member" # member, admin, owner
40
+ self._joined_at: float | None = None
41
+ self._added_by_id: str | None = None
42
+
43
+ # Notification preferences
44
+ self._notification_level: str = "all" # all, mentions, none
45
+ self._muted: bool = False
46
+
47
+ # Activity tracking
48
+ self._last_read_message_id: str | None = None
49
+ self._last_read_at: float | None = None
50
+
51
+ # Setup DynamoDB indexes (adjacent record pattern)
52
+ self._setup_indexes()
53
+
54
+ def _setup_indexes(self):
55
+ """Setup DynamoDB indexes for member queries."""
56
+
57
+ # Primary index: Adjacent record pattern - members grouped by channel
58
+ # Query: pk="channel#<id>" AND sk BEGINS_WITH "member#" to get all members
59
+ primary: DynamoDBIndex = DynamoDBIndex()
60
+ primary.name = "primary"
61
+ primary.partition_key.attribute_name = "pk"
62
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("channel", self.channel_id))
63
+ primary.sort_key.attribute_name = "sk"
64
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("member", self.user_id))
65
+ self.indexes.add_primary(primary)
66
+
67
+ # GSI1: User's channels - query channels by user
68
+ gsi: DynamoDBIndex = DynamoDBIndex()
69
+ gsi.name = "gsi1"
70
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
71
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.user_id))
72
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
73
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("channel", self.channel_id), ("ts", self.joined_at))
74
+ self.indexes.add_secondary(gsi)
75
+
76
+ # GSI2: Members by role - query members by role within channel
77
+ gsi: DynamoDBIndex = DynamoDBIndex()
78
+ gsi.name = "gsi2"
79
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
80
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("channel", self.channel_id), ("role", self.role))
81
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
82
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("member", self.user_id))
83
+ self.indexes.add_secondary(gsi)
84
+
85
+ @property
86
+ def model_version(self) -> str:
87
+ return "1.0"
88
+
89
+ def prep_for_save(self):
90
+ """
91
+ Prepare member record for save.
92
+
93
+ Note: ChatChannelMember uses adjacent record pattern where:
94
+ - Normal UUID id (generated by parent)
95
+ - pk = "channel#<channel_id>" (groups by channel)
96
+ - sk = "member#<user_id>" (enables begins_with queries)
97
+ """
98
+ # Validate required fields for adjacent record pattern
99
+ if not self.channel_id:
100
+ raise ValueError("channel_id is required for ChatChannelMember")
101
+ if not self.user_id:
102
+ raise ValueError("user_id is required for ChatChannelMember")
103
+
104
+ # Call parent to generate normal UUID id and set timestamps
105
+ super().prep_for_save()
106
+
107
+ # Properties
108
+ @property
109
+ def channel_id(self) -> str:
110
+ return self._channel_id
111
+
112
+ @channel_id.setter
113
+ def channel_id(self, value: str | None):
114
+ self._channel_id = value
115
+
116
+ @property
117
+ def user_id(self) -> str:
118
+ return self._user_id
119
+
120
+ @user_id.setter
121
+ def user_id(self, value: str | None):
122
+ self._user_id = value
123
+
124
+ @property
125
+ def role(self) -> str:
126
+ return self._role
127
+
128
+ @role.setter
129
+ def role(self, value: str | None):
130
+ valid_roles = ["member", "admin", "owner"]
131
+ self._role = value if value in valid_roles else "member"
132
+
133
+ @property
134
+ def joined_at(self) -> float:
135
+ return self._joined_at
136
+
137
+ @joined_at.setter
138
+ def joined_at(self, value: float | None):
139
+ self._joined_at = value
140
+
141
+ @property
142
+ def added_by_id(self) -> str:
143
+ return self._added_by_id
144
+
145
+ @added_by_id.setter
146
+ def added_by_id(self, value: str | None):
147
+ self._added_by_id = value
148
+
149
+ @property
150
+ def notification_level(self) -> str:
151
+ return self._notification_level
152
+
153
+ @notification_level.setter
154
+ def notification_level(self, value: str | None):
155
+ valid_levels = ["all", "mentions", "none"]
156
+ self._notification_level = value if value in valid_levels else "all"
157
+
158
+ @property
159
+ def muted(self) -> bool:
160
+ return self._muted
161
+
162
+ @muted.setter
163
+ def muted(self, value: bool | None):
164
+ self._muted = value if isinstance(value, bool) else False
165
+
166
+ @property
167
+ def last_read_message_id(self) -> str:
168
+ return self._last_read_message_id
169
+
170
+ @last_read_message_id.setter
171
+ def last_read_message_id(self, value: str | None):
172
+ self._last_read_message_id = value
173
+
174
+ @property
175
+ def last_read_at(self) -> float:
176
+ return self._last_read_at
177
+
178
+ @last_read_at.setter
179
+ def last_read_at(self, value: float | None):
180
+ self._last_read_at = value