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.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- 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
|
+
|