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,681 @@
1
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
2
+ from boto3_assist.utilities.string_utility import StringUtility
3
+ import datetime as dt
4
+ from typing import List, Optional, Dict, Any
5
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
6
+ import hashlib
7
+
8
+
9
+ class Event(BaseModel):
10
+ """
11
+ Event model for event scheduling system (MeetUp/Calendar style).
12
+
13
+ Uses adjacent records pattern with EventAttendee for unlimited guests and RSVP tracking.
14
+ All datetime fields stored as float UTC timestamps for better indexing and querying.
15
+
16
+ Features:
17
+ - Start/end timestamps with timezone support
18
+ - Location-based discovery (city, state, geo)
19
+ - Multiple hosts via EventAttendee records
20
+ - Capacity management and waitlist
21
+ - Recurring events support
22
+ - Custom registration fields
23
+ """
24
+
25
+ def __init__(self):
26
+ super().__init__()
27
+ # Mark as multi-record (works with EventAttendee adjacent records)
28
+ self.is_multi_record = True
29
+
30
+ # Basic Info
31
+ self._title: str | None = None
32
+ self._description: str | None = None
33
+ self._event_type: str = "meetup" # meetup, conference, workshop, social, networking, etc.
34
+
35
+ # Date/Time (ALL STORED AS FLOAT UTC TIMESTAMPS)
36
+ self._start_utc_ts: float | None = None
37
+ self._end_utc_ts: float | None = None
38
+ self._timezone: str | None = None # IANA timezone for display (e.g., "America/New_York")
39
+ self._is_all_day: bool = False
40
+
41
+ # Location
42
+ self._location_type: str = "physical" # physical, virtual, hybrid
43
+ self._location_name: str | None = None
44
+ self._location_address: str | None = None
45
+ self._location_city: str | None = None
46
+ self._location_state: str | None = None # State/Province/Region
47
+ self._location_country: str | None = None
48
+ self._location_postal_code: str | None = None
49
+ self._location_latitude: float | None = None
50
+ self._location_longitude: float | None = None
51
+ self._virtual_link: str | None = None # For virtual/hybrid events
52
+
53
+ # Ownership (owner_id is primary, use EventAttendee for co-hosts)
54
+ self._owner_id: str | None = None # Primary organizer
55
+
56
+ # Capacity & Registration
57
+ self._max_attendees: int | None = None
58
+ self._registration_deadline_utc_ts: float | None = None
59
+ self._requires_approval: bool = False
60
+ self._allow_waitlist: bool = False
61
+ self._allow_guest_plus_one: bool = False
62
+
63
+ # Visibility & Status
64
+ self._visibility: str = "public" # public, private, members_only
65
+ self._status: str = "draft" # draft, published, cancelled, completed
66
+ self._cancellation_reason: str | None = None
67
+ self._group_id: str | None = None
68
+
69
+ # Recurring Events
70
+ self._is_recurring: bool = False
71
+ self._recurrence_rule: str | None = None # RRULE format
72
+ self._recurrence_end_utc_ts: float | None = None
73
+ self._parent_event_id: str | None = None # For recurring event instances
74
+
75
+ # Metadata
76
+ self._tags: List[str] = []
77
+ self._custom_fields: Dict[str, Any] = {} # Flexible registration fields
78
+
79
+ # Legacy fields (keep for backward compatibility, but deprecated)
80
+ self._date: str | None = None # DEPRECATED: Use start_utc_ts
81
+ self._invited_guests: List[str] = [] # DEPRECATED: Use EventAttendee records
82
+ self._organizer_id: str | None = None # DEPRECATED: Use owner_id
83
+ self._is_draft: bool = False # DEPRECATED: Use status
84
+
85
+ self._setup_indexes()
86
+
87
+ def _setup_indexes(self):
88
+ """Setup DynamoDB indexes for event queries."""
89
+
90
+ # Primary index: events by ID
91
+ primary: DynamoDBIndex = DynamoDBIndex()
92
+ primary.name = "primary"
93
+ primary.partition_key.attribute_name = "pk"
94
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("event", self.id))
95
+ primary.sort_key.attribute_name = "sk"
96
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("event", self.id))
97
+ self.indexes.add_primary(primary)
98
+
99
+ ## GSI1: Events by owner/organizer
100
+ gsi: DynamoDBIndex = DynamoDBIndex()
101
+ gsi.name = "gsi1"
102
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
103
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("owner", self.owner_id))
104
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
105
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("date", self.start_utc_ts))
106
+ self.indexes.add_secondary(gsi)
107
+
108
+ ## GSI2: Events by city (MeetUp-style discovery)
109
+ gsi: DynamoDBIndex = DynamoDBIndex()
110
+ gsi.name = "gsi2"
111
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
112
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
113
+ ("city", self.location_city),
114
+ ("state", self.location_state),
115
+ ("country", self.location_country)
116
+ )
117
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
118
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("date", self.start_utc_ts))
119
+ self.indexes.add_secondary(gsi)
120
+
121
+ ## GSI3: Events by state/region
122
+ gsi: DynamoDBIndex = DynamoDBIndex()
123
+ gsi.name = "gsi3"
124
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
125
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
126
+ ("state", self.location_state),
127
+ ("country", self.location_country)
128
+ )
129
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
130
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("date", self.start_utc_ts))
131
+ self.indexes.add_secondary(gsi)
132
+
133
+ ## GSI4: Events by geo-location (geohash grid for nearby events)
134
+ gsi: DynamoDBIndex = DynamoDBIndex()
135
+ gsi.name = "gsi4"
136
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
137
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("geo", self._get_geohash()))
138
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
139
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("date", self.start_utc_ts))
140
+ self.indexes.add_secondary(gsi)
141
+
142
+ ## GSI5: Events by tenant and date
143
+ gsi: DynamoDBIndex = DynamoDBIndex()
144
+ gsi.name = "gsi5"
145
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
146
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
147
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
148
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("date", self.start_utc_ts))
149
+ self.indexes.add_secondary(gsi)
150
+
151
+ ## GSI6: Events by type
152
+ gsi: DynamoDBIndex = DynamoDBIndex()
153
+ gsi.name = "gsi6"
154
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
155
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
156
+ ("type", self.event_type),
157
+ ("status", self.status)
158
+ )
159
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
160
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("date", self.start_utc_ts))
161
+ self.indexes.add_secondary(gsi)
162
+
163
+ ## GSI7: Events by group
164
+ gsi: DynamoDBIndex = DynamoDBIndex()
165
+ gsi.name = "gsi7"
166
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
167
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("group", self.group_id))
168
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
169
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("date", self.start_utc_ts))
170
+ self.indexes.add_secondary(gsi)
171
+
172
+ ## GSI8: Published public events (discovery feed)
173
+ gsi: DynamoDBIndex = DynamoDBIndex()
174
+ gsi.name = "gsi8"
175
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
176
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
177
+ ("visibility", self.visibility if self.status == "published" else None),
178
+ ("status", self.status)
179
+ )
180
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
181
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("date", self.start_utc_ts))
182
+ self.indexes.add_secondary(gsi)
183
+
184
+ # Helper Methods
185
+ def _get_geohash(self, precision: int = 4) -> str | None:
186
+ """Generate geohash for geo-location indexing (4-char = ~20km grid)."""
187
+ if self._location_latitude is None or self._location_longitude is None:
188
+ return None
189
+
190
+ # Simple geohash implementation (4-char precision for ~20km grid)
191
+ lat, lng = self._location_latitude, self._location_longitude
192
+
193
+ # Normalize to 0-1 range
194
+ lat_norm = (lat + 90) / 180
195
+ lng_norm = (lng + 180) / 360
196
+
197
+ # Create hash string
198
+ hash_str = f"{int(lat_norm * 1000):04d}{int(lng_norm * 1000):04d}"
199
+ return hash_str[:precision]
200
+
201
+ @staticmethod
202
+ def datetime_to_utc_ts(datetime_str: str) -> float:
203
+ """Convert ISO8601 datetime string to UTC timestamp.
204
+
205
+ Args:
206
+ datetime_str: ISO8601 string (e.g., "2025-11-15T18:00:00-08:00")
207
+
208
+ Returns:
209
+ float: UTC timestamp
210
+ """
211
+ dt_obj = dt.datetime.fromisoformat(datetime_str.replace('Z', '+00:00'))
212
+ return dt_obj.timestamp()
213
+
214
+ @staticmethod
215
+ def utc_ts_to_datetime_str(timestamp: float, timezone: str = "UTC") -> str:
216
+ """Convert UTC timestamp to ISO8601 string in specified timezone.
217
+
218
+ Args:
219
+ timestamp: UTC timestamp
220
+ timezone: IANA timezone (e.g., "America/New_York")
221
+
222
+ Returns:
223
+ str: ISO8601 datetime string
224
+ """
225
+ import zoneinfo
226
+ dt_utc = dt.datetime.fromtimestamp(timestamp, tz=dt.UTC)
227
+ if timezone != "UTC":
228
+ try:
229
+ tz = zoneinfo.ZoneInfo(timezone)
230
+ dt_local = dt_utc.astimezone(tz)
231
+ return dt_local.isoformat()
232
+ except:
233
+ pass
234
+ return dt_utc.isoformat()
235
+
236
+ # Properties - Basic Info
237
+ @property
238
+ def title(self) -> str | None:
239
+ """Event title."""
240
+ return self._title
241
+
242
+ @title.setter
243
+ def title(self, value: str | None):
244
+ self._title = value
245
+
246
+ @property
247
+ def description(self) -> str | None:
248
+ """Event description."""
249
+ return self._description
250
+
251
+ @description.setter
252
+ def description(self, value: str | None):
253
+ self._description = value
254
+
255
+ @property
256
+ def event_type(self) -> str:
257
+ """Event type (meetup, conference, workshop, etc.)."""
258
+ return self._event_type
259
+
260
+ @event_type.setter
261
+ def event_type(self, value: str):
262
+ self._event_type = value
263
+
264
+ # Properties - Date/Time (FLOAT UTC TIMESTAMPS)
265
+ @property
266
+ def start_utc_ts(self) -> float | None:
267
+ """Event start time as UTC timestamp."""
268
+ return self._start_utc_ts
269
+
270
+ @start_utc_ts.setter
271
+ def start_utc_ts(self, value: float | None):
272
+ self._start_utc_ts = value
273
+
274
+ @property
275
+ def end_utc_ts(self) -> float | None:
276
+ """Event end time as UTC timestamp."""
277
+ return self._end_utc_ts
278
+
279
+ @end_utc_ts.setter
280
+ def end_utc_ts(self, value: float | None):
281
+ self._end_utc_ts = value
282
+
283
+ @property
284
+ def timezone(self) -> str | None:
285
+ """IANA timezone for display (e.g., 'America/New_York')."""
286
+ return self._timezone
287
+
288
+ @timezone.setter
289
+ def timezone(self, value: str | None):
290
+ self._timezone = value
291
+
292
+ @property
293
+ def is_all_day(self) -> bool:
294
+ """Is this an all-day event."""
295
+ return self._is_all_day
296
+
297
+ @is_all_day.setter
298
+ def is_all_day(self, value: bool):
299
+ self._is_all_day = bool(value)
300
+
301
+ # Properties - Location
302
+ @property
303
+ def location_type(self) -> str:
304
+ """Location type: physical, virtual, hybrid."""
305
+ return self._location_type
306
+
307
+ @location_type.setter
308
+ def location_type(self, value: str):
309
+ if value in ["physical", "virtual", "hybrid"]:
310
+ self._location_type = value
311
+
312
+ @property
313
+ def location_name(self) -> str | None:
314
+ """Venue name."""
315
+ return self._location_name
316
+
317
+ @location_name.setter
318
+ def location_name(self, value: str | None):
319
+ self._location_name = value
320
+
321
+ @property
322
+ def location_address(self) -> str | None:
323
+ """Full address."""
324
+ return self._location_address
325
+
326
+ @location_address.setter
327
+ def location_address(self, value: str | None):
328
+ self._location_address = value
329
+
330
+ @property
331
+ def location_city(self) -> str | None:
332
+ """City."""
333
+ return self._location_city
334
+
335
+ @location_city.setter
336
+ def location_city(self, value: str | None):
337
+ self._location_city = value
338
+
339
+ @property
340
+ def location_state(self) -> str | None:
341
+ """State/Province/Region."""
342
+ return self._location_state
343
+
344
+ @location_state.setter
345
+ def location_state(self, value: str | None):
346
+ self._location_state = value
347
+
348
+ @property
349
+ def location_country(self) -> str | None:
350
+ """Country."""
351
+ return self._location_country
352
+
353
+ @location_country.setter
354
+ def location_country(self, value: str | None):
355
+ self._location_country = value
356
+
357
+ @property
358
+ def location_postal_code(self) -> str | None:
359
+ """Postal/ZIP code."""
360
+ return self._location_postal_code
361
+
362
+ @location_postal_code.setter
363
+ def location_postal_code(self, value: str | None):
364
+ self._location_postal_code = value
365
+
366
+ @property
367
+ def location_latitude(self) -> float | None:
368
+ """Latitude."""
369
+ return self._location_latitude
370
+
371
+ @location_latitude.setter
372
+ def location_latitude(self, value: float | None):
373
+ self._location_latitude = value
374
+
375
+ @property
376
+ def location_longitude(self) -> float | None:
377
+ """Longitude."""
378
+ return self._location_longitude
379
+
380
+ @location_longitude.setter
381
+ def location_longitude(self, value: float | None):
382
+ self._location_longitude = value
383
+
384
+ @property
385
+ def virtual_link(self) -> str | None:
386
+ """Virtual meeting link (Zoom, Teams, etc.)."""
387
+ return self._virtual_link
388
+
389
+ @virtual_link.setter
390
+ def virtual_link(self, value: str | None):
391
+ self._virtual_link = value
392
+
393
+ # Properties - Ownership
394
+ @property
395
+ def owner_id(self) -> str | None:
396
+ """Primary event owner/organizer."""
397
+ return self._owner_id
398
+
399
+ @owner_id.setter
400
+ def owner_id(self, value: str | None):
401
+ self._owner_id = value
402
+
403
+ # Properties - Capacity & Registration
404
+ @property
405
+ def max_attendees(self) -> int | None:
406
+ """Maximum number of attendees."""
407
+ return self._max_attendees
408
+
409
+ @max_attendees.setter
410
+ def max_attendees(self, value: int | None):
411
+ self._max_attendees = value
412
+
413
+ @property
414
+ def registration_deadline_utc_ts(self) -> float | None:
415
+ """Registration deadline as UTC timestamp."""
416
+ return self._registration_deadline_utc_ts
417
+
418
+ @registration_deadline_utc_ts.setter
419
+ def registration_deadline_utc_ts(self, value: float | None):
420
+ self._registration_deadline_utc_ts = value
421
+
422
+ @property
423
+ def requires_approval(self) -> bool:
424
+ """Does RSVP require approval."""
425
+ return self._requires_approval
426
+
427
+ @requires_approval.setter
428
+ def requires_approval(self, value: bool):
429
+ self._requires_approval = bool(value)
430
+
431
+ @property
432
+ def allow_waitlist(self) -> bool:
433
+ """Allow waitlist when full."""
434
+ return self._allow_waitlist
435
+
436
+ @allow_waitlist.setter
437
+ def allow_waitlist(self, value: bool):
438
+ self._allow_waitlist = bool(value)
439
+
440
+ @property
441
+ def allow_guest_plus_one(self) -> bool:
442
+ """Allow guests to bring +1."""
443
+ return self._allow_guest_plus_one
444
+
445
+ @allow_guest_plus_one.setter
446
+ def allow_guest_plus_one(self, value: bool):
447
+ self._allow_guest_plus_one = bool(value)
448
+
449
+ # Properties - Visibility & Status
450
+ @property
451
+ def visibility(self) -> str:
452
+ """Event visibility: public, private, members_only."""
453
+ return self._visibility
454
+
455
+ @visibility.setter
456
+ def visibility(self, value: str):
457
+ if value in ["public", "private", "members_only"]:
458
+ self._visibility = value
459
+
460
+ @property
461
+ def status(self) -> str:
462
+ """Event status: draft, published, cancelled, completed."""
463
+ return self._status
464
+
465
+ @status.setter
466
+ def status(self, value: str):
467
+ if value in ["draft", "published", "cancelled", "completed"]:
468
+ self._status = value
469
+
470
+ @property
471
+ def cancellation_reason(self) -> str | None:
472
+ """Reason for cancellation."""
473
+ return self._cancellation_reason
474
+
475
+ @cancellation_reason.setter
476
+ def cancellation_reason(self, value: str | None):
477
+ self._cancellation_reason = value
478
+
479
+ @property
480
+ def group_id(self) -> str | None:
481
+ """Associated group ID."""
482
+ return self._group_id
483
+
484
+ @group_id.setter
485
+ def group_id(self, value: str | None):
486
+ self._group_id = value
487
+
488
+ # Properties - Recurring Events
489
+ @property
490
+ def is_recurring(self) -> bool:
491
+ """Is this a recurring event."""
492
+ return self._is_recurring
493
+
494
+ @is_recurring.setter
495
+ def is_recurring(self, value: bool):
496
+ self._is_recurring = bool(value)
497
+
498
+ @property
499
+ def recurrence_rule(self) -> str | None:
500
+ """Recurrence rule in RRULE format."""
501
+ return self._recurrence_rule
502
+
503
+ @recurrence_rule.setter
504
+ def recurrence_rule(self, value: str | None):
505
+ self._recurrence_rule = value
506
+
507
+ @property
508
+ def recurrence_end_utc_ts(self) -> float | None:
509
+ """Recurrence end time as UTC timestamp."""
510
+ return self._recurrence_end_utc_ts
511
+
512
+ @recurrence_end_utc_ts.setter
513
+ def recurrence_end_utc_ts(self, value: float | None):
514
+ self._recurrence_end_utc_ts = value
515
+
516
+ @property
517
+ def parent_event_id(self) -> str | None:
518
+ """Parent event ID for recurring instances."""
519
+ return self._parent_event_id
520
+
521
+ @parent_event_id.setter
522
+ def parent_event_id(self, value: str | None):
523
+ self._parent_event_id = value
524
+
525
+ # Properties - Metadata
526
+ @property
527
+ def tags(self) -> List[str]:
528
+ """Event tags."""
529
+ return self._tags
530
+
531
+ @tags.setter
532
+ def tags(self, value: List[str] | None):
533
+ self._tags = value if isinstance(value, list) else []
534
+
535
+ @property
536
+ def custom_fields(self) -> Dict[str, Any]:
537
+ """Custom registration fields."""
538
+ return self._custom_fields
539
+
540
+ @custom_fields.setter
541
+ def custom_fields(self, value: Dict[str, Any] | None):
542
+ self._custom_fields = value if isinstance(value, dict) else {}
543
+
544
+ # Legacy Properties (DEPRECATED)
545
+ @property
546
+ def date(self) -> str | None:
547
+ """DEPRECATED: Use start_utc_ts instead."""
548
+ if self._start_utc_ts:
549
+ return self.utc_ts_to_datetime_str(self._start_utc_ts, self._timezone or "UTC")
550
+ return self._date
551
+
552
+ @date.setter
553
+ def date(self, value: str | None):
554
+ """DEPRECATED: Use start_utc_ts instead."""
555
+ self._date = value
556
+ if value:
557
+ try:
558
+ self._start_utc_ts = self.datetime_to_utc_ts(value)
559
+ except:
560
+ pass
561
+
562
+ @property
563
+ def organizer_id(self) -> str | None:
564
+ """DEPRECATED: Use owner_id instead."""
565
+ return self._owner_id or self._organizer_id
566
+
567
+ @organizer_id.setter
568
+ def organizer_id(self, value: str | None):
569
+ """DEPRECATED: Use owner_id instead."""
570
+ self._organizer_id = value
571
+ if value and not self._owner_id:
572
+ self._owner_id = value
573
+
574
+ @property
575
+ def invited_guests(self) -> List[str]:
576
+ """DEPRECATED: Use EventAttendee records instead."""
577
+ return self._invited_guests
578
+
579
+ @invited_guests.setter
580
+ def invited_guests(self, value: List[str] | None):
581
+ """DEPRECATED: Use EventAttendee records instead."""
582
+ self._invited_guests = value if isinstance(value, list) else []
583
+
584
+ @property
585
+ def is_draft(self) -> bool:
586
+ """DEPRECATED: Use status property instead."""
587
+ return self._status == "draft" or self._is_draft
588
+
589
+ @is_draft.setter
590
+ def is_draft(self, value: bool):
591
+ """DEPRECATED: Use status property instead."""
592
+ self._is_draft = bool(value)
593
+ if value:
594
+ self._status = "draft"
595
+
596
+ # Helper Methods
597
+ def is_upcoming(self) -> bool:
598
+ """Check if the event is in the future."""
599
+ if self._start_utc_ts:
600
+ now_ts = dt.datetime.now(dt.UTC).timestamp()
601
+ return self._start_utc_ts > now_ts
602
+ return False
603
+
604
+ def is_past(self) -> bool:
605
+ """Check if the event is in the past."""
606
+ if self._end_utc_ts:
607
+ now_ts = dt.datetime.now(dt.UTC).timestamp()
608
+ return self._end_utc_ts < now_ts
609
+ return False
610
+
611
+ def is_happening_now(self) -> bool:
612
+ """Check if the event is currently happening."""
613
+ if self._start_utc_ts and self._end_utc_ts:
614
+ now_ts = dt.datetime.now(dt.UTC).timestamp()
615
+ return self._start_utc_ts <= now_ts <= self._end_utc_ts
616
+ return False
617
+
618
+ def duration_hours(self) -> float | None:
619
+ """Get event duration in hours."""
620
+ if self._start_utc_ts and self._end_utc_ts:
621
+ return (self._end_utc_ts - self._start_utc_ts) / 3600
622
+ return None
623
+
624
+ def is_published(self) -> bool:
625
+ """Check if event is published."""
626
+ return self._status == "published"
627
+
628
+ def is_cancelled(self) -> bool:
629
+ """Check if event is cancelled."""
630
+ return self._status == "cancelled"
631
+
632
+ def is_physical_location(self) -> bool:
633
+ """Check if event has physical location."""
634
+ return self._location_type in ["physical", "hybrid"]
635
+
636
+ def is_virtual_event(self) -> bool:
637
+ """Check if event is virtual."""
638
+ return self._location_type in ["virtual", "hybrid"]
639
+
640
+ def has_location_coordinates(self) -> bool:
641
+ """Check if event has lat/lng coordinates."""
642
+ return self._location_latitude is not None and self._location_longitude is not None
643
+
644
+ # Legacy helper methods for invited_guests (DEPRECATED)
645
+ def add_invited_guest(self, user_id: str):
646
+ """DEPRECATED: Add a user to invited guests list. Use EventAttendee records instead."""
647
+ if user_id not in self._invited_guests:
648
+ self._invited_guests.append(user_id)
649
+
650
+ def remove_invited_guest(self, user_id: str):
651
+ """DEPRECATED: Remove a user from invited guests list. Use EventAttendee records instead."""
652
+ if user_id in self._invited_guests:
653
+ self._invited_guests.remove(user_id)
654
+
655
+ def is_user_invited(self, user_id: str) -> bool:
656
+ """DEPRECATED: Check if user is invited. Use EventAttendee records instead."""
657
+ return user_id in self._invited_guests
658
+
659
+ @property
660
+ def event_date_timestamp(self) -> float:
661
+ """Get event date as timestamp. Returns start_utc_ts or converts date string."""
662
+ if self._start_utc_ts:
663
+ return self._start_utc_ts
664
+ if self._date:
665
+ try:
666
+ return self.datetime_to_utc_ts(self._date)
667
+ except:
668
+ return 0.0
669
+ return 0.0
670
+
671
+ @property
672
+ def is_private(self) -> bool:
673
+ """Check if event visibility is private."""
674
+ return self._visibility == "private"
675
+
676
+ @property
677
+ def is_standalone(self) -> bool:
678
+ """Check if event is standalone (not associated with a group)."""
679
+ return self._group_id is None or self._group_id == ""
680
+
681
+