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,220 @@
|
|
|
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 Dict, Any
|
|
5
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WebsiteAnalyticsSummary(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Model for storing aggregated website analytics data.
|
|
11
|
+
|
|
12
|
+
This model stores hourly/daily summaries of analytics data for efficient querying.
|
|
13
|
+
Aggregated by an EventBridge scheduled job via the tally service.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self._route: str | None = None # URL route this summary is for
|
|
19
|
+
self._slug: str | None = None # Slug for the page
|
|
20
|
+
self._analytics_type: str = "general" # Type of analytics summarized
|
|
21
|
+
self._period_start_ts: float | None = None # Start of aggregation period
|
|
22
|
+
self._period_end_ts: float | None = None # End of aggregation period
|
|
23
|
+
self._period_type: str = "hourly" # hourly, daily, weekly, monthly
|
|
24
|
+
|
|
25
|
+
# Aggregated metrics
|
|
26
|
+
self._total_events: int = 0 # Total number of events in period
|
|
27
|
+
self._unique_sessions: int = 0 # Number of unique sessions
|
|
28
|
+
self._unique_users: int = 0 # Number of unique users
|
|
29
|
+
|
|
30
|
+
# Aggregated data - flexible storage for computed metrics
|
|
31
|
+
self._metrics: Dict[str, Any] = {}
|
|
32
|
+
|
|
33
|
+
# Additional metadata
|
|
34
|
+
self._content: Dict[str, Any] = {}
|
|
35
|
+
|
|
36
|
+
self._setup_indexes()
|
|
37
|
+
|
|
38
|
+
def _setup_indexes(self):
|
|
39
|
+
# Primary index: summary by ID
|
|
40
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
41
|
+
primary.name = "primary"
|
|
42
|
+
primary.partition_key.attribute_name = "pk"
|
|
43
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
44
|
+
("analytics-summary", self.id)
|
|
45
|
+
)
|
|
46
|
+
primary.sort_key.attribute_name = "sk"
|
|
47
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("analytics-summary", self.id))
|
|
48
|
+
self.indexes.add_primary(primary)
|
|
49
|
+
|
|
50
|
+
## GSI: 1
|
|
51
|
+
# GSI: all analytics summaries sorted by period start
|
|
52
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
53
|
+
gsi.name = "gsi1"
|
|
54
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
55
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("analytics-summary", "all"))
|
|
56
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
57
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
58
|
+
("period", self.period_start_ts)
|
|
59
|
+
)
|
|
60
|
+
self.indexes.add_secondary(gsi)
|
|
61
|
+
|
|
62
|
+
## GSI: 2
|
|
63
|
+
# GSI: summaries by route/slug for page-specific queries
|
|
64
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
65
|
+
gsi.name = "gsi2"
|
|
66
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
67
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
68
|
+
("route", self.route or self.slug)
|
|
69
|
+
)
|
|
70
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
71
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
72
|
+
("period", self.period_start_ts)
|
|
73
|
+
)
|
|
74
|
+
self.indexes.add_secondary(gsi)
|
|
75
|
+
|
|
76
|
+
## GSI: 3
|
|
77
|
+
# GSI: summaries by tenant sorted by period
|
|
78
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
79
|
+
gsi.name = "gsi3"
|
|
80
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
81
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
82
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
83
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
84
|
+
("model", "analytics-summary"), ("period", self.period_start_ts)
|
|
85
|
+
)
|
|
86
|
+
self.indexes.add_secondary(gsi)
|
|
87
|
+
|
|
88
|
+
## GSI: 4
|
|
89
|
+
# GSI: summaries by type and period
|
|
90
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
91
|
+
gsi.name = "gsi4"
|
|
92
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
93
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
94
|
+
("analytics-type", self.analytics_type)
|
|
95
|
+
)
|
|
96
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
97
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
98
|
+
("period", self.period_start_ts)
|
|
99
|
+
)
|
|
100
|
+
self.indexes.add_secondary(gsi)
|
|
101
|
+
|
|
102
|
+
## GSI: 5
|
|
103
|
+
# GSI: summaries by tenant, type, and period for filtered queries
|
|
104
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
105
|
+
gsi.name = "gsi5"
|
|
106
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
107
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
108
|
+
("tenant", self.tenant_id), ("type", self.analytics_type)
|
|
109
|
+
)
|
|
110
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
111
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
112
|
+
("period", self.period_start_ts)
|
|
113
|
+
)
|
|
114
|
+
self.indexes.add_secondary(gsi)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def route(self) -> str | None:
|
|
118
|
+
return self._route
|
|
119
|
+
|
|
120
|
+
@route.setter
|
|
121
|
+
def route(self, value: str | None):
|
|
122
|
+
self._route = value
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def slug(self) -> str | None:
|
|
126
|
+
return self._slug
|
|
127
|
+
|
|
128
|
+
@slug.setter
|
|
129
|
+
def slug(self, value: str | None):
|
|
130
|
+
self._slug = value
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def analytics_type(self) -> str:
|
|
134
|
+
return self._analytics_type
|
|
135
|
+
|
|
136
|
+
@analytics_type.setter
|
|
137
|
+
def analytics_type(self, value: str):
|
|
138
|
+
self._analytics_type = value
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def period_start_ts(self) -> float | None:
|
|
142
|
+
return self._period_start_ts
|
|
143
|
+
|
|
144
|
+
@period_start_ts.setter
|
|
145
|
+
def period_start_ts(self, value: float | None):
|
|
146
|
+
self._period_start_ts = value
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def period_end_ts(self) -> float | None:
|
|
150
|
+
return self._period_end_ts
|
|
151
|
+
|
|
152
|
+
@period_end_ts.setter
|
|
153
|
+
def period_end_ts(self, value: float | None):
|
|
154
|
+
self._period_end_ts = value
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def period_type(self) -> str:
|
|
158
|
+
return self._period_type
|
|
159
|
+
|
|
160
|
+
@period_type.setter
|
|
161
|
+
def period_type(self, value: str):
|
|
162
|
+
self._period_type = value
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def total_events(self) -> int:
|
|
166
|
+
return self._total_events
|
|
167
|
+
|
|
168
|
+
@total_events.setter
|
|
169
|
+
def total_events(self, value: int):
|
|
170
|
+
self._total_events = value
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def unique_sessions(self) -> int:
|
|
174
|
+
return self._unique_sessions
|
|
175
|
+
|
|
176
|
+
@unique_sessions.setter
|
|
177
|
+
def unique_sessions(self, value: int):
|
|
178
|
+
self._unique_sessions = value
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def unique_users(self) -> int:
|
|
182
|
+
return self._unique_users
|
|
183
|
+
|
|
184
|
+
@unique_users.setter
|
|
185
|
+
def unique_users(self, value: int):
|
|
186
|
+
self._unique_users = value
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def metrics(self) -> Dict[str, Any]:
|
|
190
|
+
"""Get metrics (boto3-assist v0.30.0+ auto-converts Decimals to float)."""
|
|
191
|
+
return self._metrics
|
|
192
|
+
|
|
193
|
+
@metrics.setter
|
|
194
|
+
def metrics(self, value: Dict[str, Any]):
|
|
195
|
+
"""Set metrics."""
|
|
196
|
+
self._metrics = value if value is not None else {}
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def content(self) -> Dict[str, Any]:
|
|
200
|
+
return self._content
|
|
201
|
+
|
|
202
|
+
@content.setter
|
|
203
|
+
def content(self, value: Dict[str, Any]):
|
|
204
|
+
self._content = value
|
|
205
|
+
|
|
206
|
+
# Helper methods
|
|
207
|
+
def get_metric(self, key: str, default: Any = None) -> Any:
|
|
208
|
+
"""Get a specific metric value."""
|
|
209
|
+
return self.metrics.get(key, default)
|
|
210
|
+
|
|
211
|
+
def set_metric(self, key: str, value: Any):
|
|
212
|
+
"""Set a specific metric value."""
|
|
213
|
+
self.metrics[key] = value
|
|
214
|
+
|
|
215
|
+
def calculate_average(self, metric_key: str) -> float:
|
|
216
|
+
"""Calculate average for a metric stored as a list."""
|
|
217
|
+
values = self.metrics.get(metric_key, [])
|
|
218
|
+
if not values or not isinstance(values, list):
|
|
219
|
+
return 0.0
|
|
220
|
+
return sum(values) / len(values)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Analytics Domain Services
|
|
2
|
+
|
|
3
|
+
from .website_analytics_service import WebsiteAnalyticsService
|
|
4
|
+
from .website_analytics_summary_service import WebsiteAnalyticsSummaryService
|
|
5
|
+
from .website_analytics_tally_service import WebsiteAnalyticsTallyService
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"WebsiteAnalyticsService",
|
|
9
|
+
"WebsiteAnalyticsSummaryService",
|
|
10
|
+
"WebsiteAnalyticsTallyService",
|
|
11
|
+
]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Website Analytics Service
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, List, Optional
|
|
4
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
5
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
6
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
7
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError
|
|
8
|
+
from geek_cafe_saas_sdk.domains.analytics.models import WebsiteAnalytics
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WebsiteAnalyticsService(DatabaseService[WebsiteAnalytics]):
|
|
12
|
+
"""Service for WebsiteAnalytics database operations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
15
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
16
|
+
|
|
17
|
+
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[WebsiteAnalytics]:
|
|
18
|
+
"""Create a new analytics record."""
|
|
19
|
+
try:
|
|
20
|
+
# Validate required fields
|
|
21
|
+
required_fields = ['analytics_type']
|
|
22
|
+
self._validate_required_fields(kwargs, required_fields)
|
|
23
|
+
|
|
24
|
+
# At least one of route or slug should be provided
|
|
25
|
+
if not kwargs.get('route') and not kwargs.get('slug'):
|
|
26
|
+
raise ValidationError("Either 'route' or 'slug' must be provided", "route")
|
|
27
|
+
|
|
28
|
+
# Create new analytics instance
|
|
29
|
+
analytics = WebsiteAnalytics()
|
|
30
|
+
analytics.tenant_id = tenant_id
|
|
31
|
+
analytics.user_id = user_id
|
|
32
|
+
analytics.created_by_id = user_id
|
|
33
|
+
|
|
34
|
+
# Set analytics fields
|
|
35
|
+
analytics.route = kwargs.get('route')
|
|
36
|
+
analytics.slug = kwargs.get('slug')
|
|
37
|
+
analytics.analytics_type = kwargs.get('analytics_type', 'general')
|
|
38
|
+
analytics.data = kwargs.get('data', {})
|
|
39
|
+
analytics.session_id = kwargs.get('session_id')
|
|
40
|
+
analytics.user_agent = kwargs.get('user_agent')
|
|
41
|
+
analytics.ip_address = kwargs.get('ip_address')
|
|
42
|
+
analytics.referrer = kwargs.get('referrer')
|
|
43
|
+
|
|
44
|
+
# Prepare for save (sets ID and timestamps)
|
|
45
|
+
analytics.prep_for_save()
|
|
46
|
+
|
|
47
|
+
# Save to database
|
|
48
|
+
return self._save_model(analytics)
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return self._handle_service_exception(e, 'create_analytics', tenant_id=tenant_id, user_id=user_id)
|
|
52
|
+
|
|
53
|
+
# Convenience methods for different analytics types
|
|
54
|
+
def create_page_view(self, tenant_id: str, user_id: str, route: str, **kwargs) -> ServiceResult[WebsiteAnalytics]:
|
|
55
|
+
"""Create a page view analytics record."""
|
|
56
|
+
analytics = WebsiteAnalytics()
|
|
57
|
+
analytics.tenant_id = tenant_id
|
|
58
|
+
analytics.user_id = user_id
|
|
59
|
+
analytics.created_by_id = user_id
|
|
60
|
+
analytics.set_page_view(route, **kwargs)
|
|
61
|
+
|
|
62
|
+
# Set optional fields
|
|
63
|
+
analytics.session_id = kwargs.get('session_id')
|
|
64
|
+
analytics.user_agent = kwargs.get('user_agent')
|
|
65
|
+
analytics.ip_address = kwargs.get('ip_address')
|
|
66
|
+
analytics.referrer = kwargs.get('referrer')
|
|
67
|
+
|
|
68
|
+
analytics.prep_for_save()
|
|
69
|
+
return self._save_model(analytics)
|
|
70
|
+
|
|
71
|
+
def create_error_log(self, tenant_id: str, user_id: str, route: str,
|
|
72
|
+
error_message: str, **kwargs) -> ServiceResult[WebsiteAnalytics]:
|
|
73
|
+
"""Create an error analytics record."""
|
|
74
|
+
analytics = WebsiteAnalytics()
|
|
75
|
+
analytics.tenant_id = tenant_id
|
|
76
|
+
analytics.user_id = user_id
|
|
77
|
+
analytics.created_by_id = user_id
|
|
78
|
+
analytics.set_error(route, error_message, **kwargs)
|
|
79
|
+
|
|
80
|
+
# Set optional fields
|
|
81
|
+
analytics.session_id = kwargs.get('session_id')
|
|
82
|
+
analytics.user_agent = kwargs.get('user_agent')
|
|
83
|
+
analytics.ip_address = kwargs.get('ip_address')
|
|
84
|
+
analytics.referrer = kwargs.get('referrer')
|
|
85
|
+
|
|
86
|
+
analytics.prep_for_save()
|
|
87
|
+
return self._save_model(analytics)
|
|
88
|
+
|
|
89
|
+
def create_performance_log(self, tenant_id: str, user_id: str, route: str,
|
|
90
|
+
**kwargs) -> ServiceResult[WebsiteAnalytics]:
|
|
91
|
+
"""Create a performance analytics record."""
|
|
92
|
+
analytics = WebsiteAnalytics()
|
|
93
|
+
analytics.tenant_id = tenant_id
|
|
94
|
+
analytics.user_id = user_id
|
|
95
|
+
analytics.created_by_id = user_id
|
|
96
|
+
analytics.set_performance(route, **kwargs)
|
|
97
|
+
|
|
98
|
+
# Set optional fields
|
|
99
|
+
analytics.session_id = kwargs.get('session_id')
|
|
100
|
+
analytics.user_agent = kwargs.get('user_agent')
|
|
101
|
+
analytics.ip_address = kwargs.get('ip_address')
|
|
102
|
+
analytics.referrer = kwargs.get('referrer')
|
|
103
|
+
|
|
104
|
+
analytics.prep_for_save()
|
|
105
|
+
return self._save_model(analytics)
|
|
106
|
+
|
|
107
|
+
def create_custom_event(self, tenant_id: str, user_id: str, route: str,
|
|
108
|
+
event_name: str, **kwargs) -> ServiceResult[WebsiteAnalytics]:
|
|
109
|
+
"""Create a custom event analytics record."""
|
|
110
|
+
analytics = WebsiteAnalytics()
|
|
111
|
+
analytics.tenant_id = tenant_id
|
|
112
|
+
analytics.user_id = user_id
|
|
113
|
+
analytics.created_by_id = user_id
|
|
114
|
+
analytics.set_custom_event(route, event_name, **kwargs)
|
|
115
|
+
|
|
116
|
+
# Set optional fields
|
|
117
|
+
analytics.session_id = kwargs.get('session_id')
|
|
118
|
+
analytics.user_agent = kwargs.get('user_agent')
|
|
119
|
+
analytics.ip_address = kwargs.get('ip_address')
|
|
120
|
+
analytics.referrer = kwargs.get('referrer')
|
|
121
|
+
|
|
122
|
+
analytics.prep_for_save()
|
|
123
|
+
return self._save_model(analytics)
|
|
124
|
+
|
|
125
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[WebsiteAnalytics]:
|
|
126
|
+
"""Get analytics record by ID with access control."""
|
|
127
|
+
try:
|
|
128
|
+
analytics = self._get_model_by_id(resource_id, WebsiteAnalytics)
|
|
129
|
+
|
|
130
|
+
if not analytics:
|
|
131
|
+
raise NotFoundError(f"WebsiteAnalytics with ID {resource_id} not found")
|
|
132
|
+
|
|
133
|
+
# Validate tenant access
|
|
134
|
+
if hasattr(analytics, 'tenant_id'):
|
|
135
|
+
self._validate_tenant_access(analytics.tenant_id, tenant_id)
|
|
136
|
+
|
|
137
|
+
return ServiceResult.success_result(analytics)
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return self._handle_service_exception(e, 'get_analytics', resource_id=resource_id, tenant_id=tenant_id)
|
|
141
|
+
|
|
142
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
143
|
+
updates: Dict[str, Any]) -> ServiceResult[WebsiteAnalytics]:
|
|
144
|
+
"""Update analytics record with access control."""
|
|
145
|
+
try:
|
|
146
|
+
# Get existing analytics
|
|
147
|
+
analytics = self._get_model_by_id(resource_id, WebsiteAnalytics)
|
|
148
|
+
|
|
149
|
+
if not analytics:
|
|
150
|
+
raise NotFoundError(f"WebsiteAnalytics with ID {resource_id} not found")
|
|
151
|
+
|
|
152
|
+
# Validate tenant access
|
|
153
|
+
if hasattr(analytics, 'tenant_id'):
|
|
154
|
+
self._validate_tenant_access(analytics.tenant_id, tenant_id)
|
|
155
|
+
|
|
156
|
+
# Apply updates
|
|
157
|
+
for field, value in updates.items():
|
|
158
|
+
if hasattr(analytics, field) and field not in ['id', 'created_utc_ts', 'tenant_id']:
|
|
159
|
+
setattr(analytics, field, value)
|
|
160
|
+
|
|
161
|
+
# Update metadata
|
|
162
|
+
analytics.updated_by_id = user_id
|
|
163
|
+
analytics.prep_for_save() # Updates timestamp
|
|
164
|
+
|
|
165
|
+
# Save updated analytics
|
|
166
|
+
return self._save_model(analytics)
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
return self._handle_service_exception(e, 'update_analytics', resource_id=resource_id, tenant_id=tenant_id)
|
|
170
|
+
|
|
171
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
172
|
+
"""Delete analytics record with access control."""
|
|
173
|
+
try:
|
|
174
|
+
analytics = self._get_model_by_id(resource_id, WebsiteAnalytics)
|
|
175
|
+
|
|
176
|
+
if not analytics:
|
|
177
|
+
raise NotFoundError(f"WebsiteAnalytics with ID {resource_id} not found")
|
|
178
|
+
|
|
179
|
+
if hasattr(analytics, 'tenant_id'):
|
|
180
|
+
self._validate_tenant_access(analytics.tenant_id, tenant_id)
|
|
181
|
+
|
|
182
|
+
return self._delete_model(analytics)
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
return self._handle_service_exception(e, 'delete_analytics', resource_id=resource_id, tenant_id=tenant_id)
|
|
186
|
+
|
|
187
|
+
def list_by_route(self, route: str, start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalytics]]:
|
|
188
|
+
"""List analytics records by route/slug."""
|
|
189
|
+
try:
|
|
190
|
+
model = WebsiteAnalytics()
|
|
191
|
+
model.route = route
|
|
192
|
+
return self._query_by_index(model, "gsi2", start_key=start_key, limit=limit)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
return self._handle_service_exception(e, 'list_analytics_by_route', route=route)
|
|
195
|
+
|
|
196
|
+
def list_by_tenant(self, tenant_id: str, start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalytics]]:
|
|
197
|
+
"""List analytics records by tenant."""
|
|
198
|
+
try:
|
|
199
|
+
model = WebsiteAnalytics()
|
|
200
|
+
model.tenant_id = tenant_id
|
|
201
|
+
return self._query_by_index(model, "gsi3", start_key=start_key, limit=limit)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
return self._handle_service_exception(e, 'list_analytics_by_tenant', tenant_id=tenant_id)
|
|
204
|
+
|
|
205
|
+
def list_by_type(self, analytics_type: str, start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalytics]]:
|
|
206
|
+
"""List analytics records by type (general, error, performance, custom)."""
|
|
207
|
+
try:
|
|
208
|
+
model = WebsiteAnalytics()
|
|
209
|
+
model.analytics_type = analytics_type
|
|
210
|
+
return self._query_by_index(model, "gsi4", start_key=start_key, limit=limit)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return self._handle_service_exception(e, 'list_analytics_by_type', analytics_type=analytics_type)
|
|
213
|
+
|
|
214
|
+
def list_by_tenant_and_type(self, tenant_id: str, analytics_type: str,
|
|
215
|
+
start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalytics]]:
|
|
216
|
+
"""List analytics records by tenant and type."""
|
|
217
|
+
try:
|
|
218
|
+
model = WebsiteAnalytics()
|
|
219
|
+
model.tenant_id = tenant_id
|
|
220
|
+
model.analytics_type = analytics_type
|
|
221
|
+
return self._query_by_index(model, "gsi5", start_key=start_key, limit=limit)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
return self._handle_service_exception(e, 'list_analytics_by_tenant_and_type',
|
|
224
|
+
tenant_id=tenant_id, analytics_type=analytics_type)
|
|
225
|
+
|
|
226
|
+
def list_all(self, start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalytics]]:
|
|
227
|
+
"""List all analytics records."""
|
|
228
|
+
try:
|
|
229
|
+
model = WebsiteAnalytics()
|
|
230
|
+
return self._query_by_index(model, "gsi1", start_key=start_key, limit=limit)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
return self._handle_service_exception(e, 'list_all_analytics')
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Website Analytics Summary Service
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, List, Optional
|
|
4
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
5
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
6
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
7
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError
|
|
8
|
+
from geek_cafe_saas_sdk.domains.analytics.models import WebsiteAnalyticsSummary
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WebsiteAnalyticsSummaryService(DatabaseService[WebsiteAnalyticsSummary]):
|
|
12
|
+
"""Service for WebsiteAnalyticsSummary database operations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
15
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
16
|
+
|
|
17
|
+
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[WebsiteAnalyticsSummary]:
|
|
18
|
+
"""Create or update (upsert) an analytics summary."""
|
|
19
|
+
try:
|
|
20
|
+
# Validate required fields
|
|
21
|
+
required_fields = ['analytics_type', 'period_start_ts', 'period_end_ts']
|
|
22
|
+
self._validate_required_fields(kwargs, required_fields)
|
|
23
|
+
|
|
24
|
+
# At least one of route or slug should be provided
|
|
25
|
+
if not kwargs.get('route') and not kwargs.get('slug'):
|
|
26
|
+
raise ValidationError("Either 'route' or 'slug' must be provided", "route")
|
|
27
|
+
|
|
28
|
+
# Check if a summary already exists for this route/period
|
|
29
|
+
existing = self._get_by_route_and_period(
|
|
30
|
+
kwargs.get('route') or kwargs.get('slug'),
|
|
31
|
+
kwargs.get('period_start_ts'),
|
|
32
|
+
kwargs.get('analytics_type')
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if existing:
|
|
36
|
+
# Update the existing summary
|
|
37
|
+
return self._update_existing_summary(existing, tenant_id, user_id, **kwargs)
|
|
38
|
+
|
|
39
|
+
# Create new summary instance
|
|
40
|
+
summary = WebsiteAnalyticsSummary()
|
|
41
|
+
summary.tenant_id = tenant_id
|
|
42
|
+
summary.user_id = user_id
|
|
43
|
+
summary.created_by_id = user_id
|
|
44
|
+
|
|
45
|
+
# Set summary fields
|
|
46
|
+
summary.route = kwargs.get('route')
|
|
47
|
+
summary.slug = kwargs.get('slug')
|
|
48
|
+
summary.analytics_type = kwargs.get('analytics_type', 'general')
|
|
49
|
+
summary.period_start_ts = float(kwargs.get('period_start_ts'))
|
|
50
|
+
summary.period_end_ts = float(kwargs.get('period_end_ts'))
|
|
51
|
+
summary.period_type = kwargs.get('period_type', 'hourly')
|
|
52
|
+
|
|
53
|
+
# Set aggregated metrics
|
|
54
|
+
summary.total_events = int(kwargs.get('total_events', 0) or 0)
|
|
55
|
+
summary.unique_sessions = int(kwargs.get('unique_sessions', 0) or 0)
|
|
56
|
+
summary.unique_users = int(kwargs.get('unique_users', 0) or 0)
|
|
57
|
+
summary.metrics = kwargs.get('metrics', {})
|
|
58
|
+
summary.content = kwargs.get('content', {})
|
|
59
|
+
|
|
60
|
+
# Prepare for save (sets ID and timestamps)
|
|
61
|
+
summary.prep_for_save()
|
|
62
|
+
|
|
63
|
+
# Save to database
|
|
64
|
+
return self._save_model(summary)
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return self._handle_service_exception(e, 'create_analytics_summary', tenant_id=tenant_id, user_id=user_id)
|
|
68
|
+
|
|
69
|
+
def _update_existing_summary(self, existing_summary: WebsiteAnalyticsSummary,
|
|
70
|
+
tenant_id: str, user_id: str, **kwargs) -> ServiceResult[WebsiteAnalyticsSummary]:
|
|
71
|
+
"""Update an existing summary with new data."""
|
|
72
|
+
# Update fields
|
|
73
|
+
existing_summary.period_end_ts = float(kwargs.get('period_end_ts', existing_summary.period_end_ts))
|
|
74
|
+
existing_summary.period_type = kwargs.get('period_type', existing_summary.period_type)
|
|
75
|
+
|
|
76
|
+
# Update aggregated metrics
|
|
77
|
+
existing_summary.total_events = int(kwargs.get('total_events', existing_summary.total_events) or 0)
|
|
78
|
+
existing_summary.unique_sessions = int(kwargs.get('unique_sessions', existing_summary.unique_sessions) or 0)
|
|
79
|
+
existing_summary.unique_users = int(kwargs.get('unique_users', existing_summary.unique_users) or 0)
|
|
80
|
+
existing_summary.metrics = kwargs.get('metrics', existing_summary.metrics or {})
|
|
81
|
+
existing_summary.content = kwargs.get('content', existing_summary.content or {})
|
|
82
|
+
|
|
83
|
+
# Update metadata
|
|
84
|
+
existing_summary.updated_by_id = user_id
|
|
85
|
+
existing_summary.prep_for_save() # Updates timestamp
|
|
86
|
+
|
|
87
|
+
# Save updated summary
|
|
88
|
+
return self._save_model(existing_summary)
|
|
89
|
+
|
|
90
|
+
def _get_by_route_and_period(self, route: str, period_start_ts: float,
|
|
91
|
+
analytics_type: str) -> WebsiteAnalyticsSummary | None:
|
|
92
|
+
"""Helper: get a summary by route and period via GSI2."""
|
|
93
|
+
model = WebsiteAnalyticsSummary()
|
|
94
|
+
model.route = route
|
|
95
|
+
result = self._query_by_index(model, "gsi2")
|
|
96
|
+
|
|
97
|
+
if result.success and result.data:
|
|
98
|
+
# Filter by period_start_ts and analytics_type
|
|
99
|
+
for summary in result.data:
|
|
100
|
+
if (summary.period_start_ts == period_start_ts and
|
|
101
|
+
summary.analytics_type == analytics_type):
|
|
102
|
+
return summary
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[WebsiteAnalyticsSummary]:
|
|
106
|
+
"""Get analytics summary by ID with access control."""
|
|
107
|
+
try:
|
|
108
|
+
summary = self._get_model_by_id(resource_id, WebsiteAnalyticsSummary)
|
|
109
|
+
|
|
110
|
+
if not summary:
|
|
111
|
+
raise NotFoundError(f"WebsiteAnalyticsSummary with ID {resource_id} not found")
|
|
112
|
+
|
|
113
|
+
# Validate tenant access
|
|
114
|
+
if hasattr(summary, 'tenant_id'):
|
|
115
|
+
self._validate_tenant_access(summary.tenant_id, tenant_id)
|
|
116
|
+
|
|
117
|
+
return ServiceResult.success_result(summary)
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return self._handle_service_exception(e, 'get_analytics_summary', resource_id=resource_id, tenant_id=tenant_id)
|
|
121
|
+
|
|
122
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
123
|
+
updates: Dict[str, Any]) -> ServiceResult[WebsiteAnalyticsSummary]:
|
|
124
|
+
"""Update analytics summary with access control."""
|
|
125
|
+
try:
|
|
126
|
+
# Get existing summary
|
|
127
|
+
summary = self._get_model_by_id(resource_id, WebsiteAnalyticsSummary)
|
|
128
|
+
|
|
129
|
+
if not summary:
|
|
130
|
+
raise NotFoundError(f"WebsiteAnalyticsSummary with ID {resource_id} not found")
|
|
131
|
+
|
|
132
|
+
# Validate tenant access
|
|
133
|
+
if hasattr(summary, 'tenant_id'):
|
|
134
|
+
self._validate_tenant_access(summary.tenant_id, tenant_id)
|
|
135
|
+
|
|
136
|
+
# Apply updates
|
|
137
|
+
for field, value in updates.items():
|
|
138
|
+
if hasattr(summary, field) and field not in ['id', 'created_utc_ts', 'tenant_id']:
|
|
139
|
+
setattr(summary, field, value)
|
|
140
|
+
|
|
141
|
+
# Update metadata
|
|
142
|
+
summary.updated_by_id = user_id
|
|
143
|
+
summary.prep_for_save() # Updates timestamp
|
|
144
|
+
|
|
145
|
+
# Save updated summary
|
|
146
|
+
return self._save_model(summary)
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return self._handle_service_exception(e, 'update_analytics_summary', resource_id=resource_id, tenant_id=tenant_id)
|
|
150
|
+
|
|
151
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
152
|
+
"""Delete analytics summary with access control."""
|
|
153
|
+
try:
|
|
154
|
+
summary = self._get_model_by_id(resource_id, WebsiteAnalyticsSummary)
|
|
155
|
+
|
|
156
|
+
if not summary:
|
|
157
|
+
raise NotFoundError(f"WebsiteAnalyticsSummary with ID {resource_id} not found")
|
|
158
|
+
|
|
159
|
+
if hasattr(summary, 'tenant_id'):
|
|
160
|
+
self._validate_tenant_access(summary.tenant_id, tenant_id)
|
|
161
|
+
|
|
162
|
+
return self._delete_model(summary)
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
return self._handle_service_exception(e, 'delete_analytics_summary', resource_id=resource_id, tenant_id=tenant_id)
|
|
166
|
+
|
|
167
|
+
def list_by_route(self, route: str, start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalyticsSummary]]:
|
|
168
|
+
"""List analytics summaries by route/slug."""
|
|
169
|
+
try:
|
|
170
|
+
model = WebsiteAnalyticsSummary()
|
|
171
|
+
model.route = route
|
|
172
|
+
return self._query_by_index(model, "gsi2", start_key=start_key, limit=limit)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
return self._handle_service_exception(e, 'list_summaries_by_route', route=route)
|
|
175
|
+
|
|
176
|
+
def list_by_tenant(self, tenant_id: str, start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalyticsSummary]]:
|
|
177
|
+
"""List analytics summaries by tenant."""
|
|
178
|
+
try:
|
|
179
|
+
model = WebsiteAnalyticsSummary()
|
|
180
|
+
model.tenant_id = tenant_id
|
|
181
|
+
return self._query_by_index(model, "gsi3", start_key=start_key, limit=limit)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
return self._handle_service_exception(e, 'list_summaries_by_tenant', tenant_id=tenant_id)
|
|
184
|
+
|
|
185
|
+
def list_by_type(self, analytics_type: str, start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalyticsSummary]]:
|
|
186
|
+
"""List analytics summaries by type."""
|
|
187
|
+
try:
|
|
188
|
+
model = WebsiteAnalyticsSummary()
|
|
189
|
+
model.analytics_type = analytics_type
|
|
190
|
+
return self._query_by_index(model, "gsi4", start_key=start_key, limit=limit)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return self._handle_service_exception(e, 'list_summaries_by_type', analytics_type=analytics_type)
|
|
193
|
+
|
|
194
|
+
def list_by_tenant_and_type(self, tenant_id: str, analytics_type: str,
|
|
195
|
+
start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalyticsSummary]]:
|
|
196
|
+
"""List analytics summaries by tenant and type."""
|
|
197
|
+
try:
|
|
198
|
+
model = WebsiteAnalyticsSummary()
|
|
199
|
+
model.tenant_id = tenant_id
|
|
200
|
+
model.analytics_type = analytics_type
|
|
201
|
+
return self._query_by_index(model, "gsi5", start_key=start_key, limit=limit)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
return self._handle_service_exception(e, 'list_summaries_by_tenant_and_type',
|
|
204
|
+
tenant_id=tenant_id, analytics_type=analytics_type)
|
|
205
|
+
|
|
206
|
+
def list_all(self, start_key: dict = None, limit: int = None) -> ServiceResult[List[WebsiteAnalyticsSummary]]:
|
|
207
|
+
"""List all analytics summaries."""
|
|
208
|
+
try:
|
|
209
|
+
model = WebsiteAnalyticsSummary()
|
|
210
|
+
return self._query_by_index(model, "gsi1", start_key=start_key, limit=limit)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return self._handle_service_exception(e, 'list_all_summaries')
|