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,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FileShare model for file sharing and permissions.
|
|
3
|
+
|
|
4
|
+
Geek Cafe, LLC
|
|
5
|
+
MIT License. See Project Root for the license information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
9
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
10
|
+
import datetime as dt
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileShare(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
File sharing record with permissions.
|
|
18
|
+
|
|
19
|
+
Manages access control for files. Users can share files with other users
|
|
20
|
+
with specific permission levels and optional expiration.
|
|
21
|
+
|
|
22
|
+
Access Patterns (DynamoDB Keys):
|
|
23
|
+
- pk: FILE#{tenant_id}#{file_id}
|
|
24
|
+
- sk: SHARE#{share_id}
|
|
25
|
+
- gsi1_pk: SHARE#{tenant_id}#{shared_with_user_id}
|
|
26
|
+
- gsi1_sk: FILE#{file_id}
|
|
27
|
+
- gsi2_pk: FILE#{tenant_id}#{file_id}
|
|
28
|
+
- gsi2_sk: SHARE#{created_utc_ts}
|
|
29
|
+
|
|
30
|
+
Permission Levels:
|
|
31
|
+
- "view": Can view metadata only
|
|
32
|
+
- "download": Can view and download
|
|
33
|
+
- "edit": Can view, download, and modify metadata
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
super().__init__()
|
|
38
|
+
|
|
39
|
+
# Identity
|
|
40
|
+
self._share_id: str | None = None # Unique share ID
|
|
41
|
+
self._file_id: str | None = None # Shared file
|
|
42
|
+
|
|
43
|
+
# Sharing Information
|
|
44
|
+
self._shared_by: str | None = None # User who shared the file
|
|
45
|
+
self._shared_with_user_id: str | None = None # Recipient user ID
|
|
46
|
+
self._shared_with_email: str | None = None # Recipient email (for external shares)
|
|
47
|
+
|
|
48
|
+
# Permissions
|
|
49
|
+
self._permission_level: str = "view" # "view", "download", "edit"
|
|
50
|
+
self._can_reshare: bool = False # Can recipient share with others?
|
|
51
|
+
|
|
52
|
+
# Access Control
|
|
53
|
+
self._access_token: str | None = None # Token for external/unauthenticated access
|
|
54
|
+
self._expires_at_ts: float | None = None # Expiration timestamp
|
|
55
|
+
|
|
56
|
+
# Usage Tracking
|
|
57
|
+
self._access_count: int = 0 # Number of times accessed
|
|
58
|
+
self._last_accessed_at_ts: float | None = None # Last access timestamp
|
|
59
|
+
|
|
60
|
+
# State
|
|
61
|
+
self._status: str = "active" # "active", "revoked", "expired"
|
|
62
|
+
self._revoked_at_ts: float | None = None # When revoked
|
|
63
|
+
|
|
64
|
+
# Timestamps (inherited from BaseModel)
|
|
65
|
+
# created_utc_ts
|
|
66
|
+
|
|
67
|
+
# Properties - Identity
|
|
68
|
+
@property
|
|
69
|
+
def share_id(self) -> str | None:
|
|
70
|
+
"""Unique share ID."""
|
|
71
|
+
return self._share_id or self.id
|
|
72
|
+
|
|
73
|
+
@share_id.setter
|
|
74
|
+
def share_id(self, value: str | None):
|
|
75
|
+
self._share_id = value
|
|
76
|
+
if value:
|
|
77
|
+
self.id = value
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def file_id(self) -> str | None:
|
|
81
|
+
"""Shared file ID."""
|
|
82
|
+
return self._file_id
|
|
83
|
+
|
|
84
|
+
@file_id.setter
|
|
85
|
+
def file_id(self, value: str | None):
|
|
86
|
+
self._file_id = value
|
|
87
|
+
|
|
88
|
+
# Properties - Sharing Information
|
|
89
|
+
@property
|
|
90
|
+
def shared_by(self) -> str | None:
|
|
91
|
+
"""User who shared the file."""
|
|
92
|
+
return self._shared_by
|
|
93
|
+
|
|
94
|
+
@shared_by.setter
|
|
95
|
+
def shared_by(self, value: str | None):
|
|
96
|
+
self._shared_by = value
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def shared_with_user_id(self) -> str | None:
|
|
100
|
+
"""Recipient user ID."""
|
|
101
|
+
return self._shared_with_user_id
|
|
102
|
+
|
|
103
|
+
@shared_with_user_id.setter
|
|
104
|
+
def shared_with_user_id(self, value: str | None):
|
|
105
|
+
self._shared_with_user_id = value
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def shared_with_email(self) -> str | None:
|
|
109
|
+
"""Recipient email (for external shares)."""
|
|
110
|
+
return self._shared_with_email
|
|
111
|
+
|
|
112
|
+
@shared_with_email.setter
|
|
113
|
+
def shared_with_email(self, value: str | None):
|
|
114
|
+
self._shared_with_email = value
|
|
115
|
+
|
|
116
|
+
# Properties - Permissions
|
|
117
|
+
@property
|
|
118
|
+
def permission_level(self) -> str:
|
|
119
|
+
"""Permission level: 'view', 'download', 'edit'."""
|
|
120
|
+
return self._permission_level
|
|
121
|
+
|
|
122
|
+
@permission_level.setter
|
|
123
|
+
def permission_level(self, value: str):
|
|
124
|
+
if value not in ["view", "download", "edit"]:
|
|
125
|
+
raise ValueError(f"Invalid permission level: {value}. Must be 'view', 'download', or 'edit'")
|
|
126
|
+
self._permission_level = value
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def can_reshare(self) -> bool:
|
|
130
|
+
"""Can recipient share with others?"""
|
|
131
|
+
return self._can_reshare
|
|
132
|
+
|
|
133
|
+
@can_reshare.setter
|
|
134
|
+
def can_reshare(self, value: bool):
|
|
135
|
+
self._can_reshare = bool(value)
|
|
136
|
+
|
|
137
|
+
# Properties - Access Control
|
|
138
|
+
@property
|
|
139
|
+
def access_token(self) -> str | None:
|
|
140
|
+
"""Access token for public links."""
|
|
141
|
+
return self._access_token
|
|
142
|
+
|
|
143
|
+
@access_token.setter
|
|
144
|
+
def access_token(self, value: str | None):
|
|
145
|
+
self._access_token = value
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def expires_at_ts(self) -> float | None:
|
|
149
|
+
"""Expiration timestamp."""
|
|
150
|
+
return self._expires_at_ts
|
|
151
|
+
|
|
152
|
+
@expires_at_ts.setter
|
|
153
|
+
def expires_at_ts(self, value: float | None):
|
|
154
|
+
self._expires_at_ts = value
|
|
155
|
+
|
|
156
|
+
# Properties - Usage Tracking
|
|
157
|
+
@property
|
|
158
|
+
def access_count(self) -> int:
|
|
159
|
+
"""Number of times accessed."""
|
|
160
|
+
return self._access_count
|
|
161
|
+
|
|
162
|
+
@access_count.setter
|
|
163
|
+
def access_count(self, value: int):
|
|
164
|
+
self._access_count = value if value is not None else 0
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def last_accessed_at_ts(self) -> float | None:
|
|
168
|
+
"""Last access timestamp."""
|
|
169
|
+
return self._last_accessed_at_ts
|
|
170
|
+
|
|
171
|
+
@last_accessed_at_ts.setter
|
|
172
|
+
def last_accessed_at_ts(self, value: float | None):
|
|
173
|
+
self._last_accessed_at_ts = value
|
|
174
|
+
|
|
175
|
+
# Properties - State
|
|
176
|
+
@property
|
|
177
|
+
def status(self) -> str:
|
|
178
|
+
"""Share status: 'active', 'revoked', 'expired'."""
|
|
179
|
+
return self._status
|
|
180
|
+
|
|
181
|
+
@status.setter
|
|
182
|
+
def status(self, value: str):
|
|
183
|
+
if value not in ["active", "revoked", "expired"]:
|
|
184
|
+
raise ValueError(f"Invalid status: {value}. Must be 'active', 'revoked', or 'expired'")
|
|
185
|
+
self._status = value
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def revoked_at_ts(self) -> float | None:
|
|
189
|
+
"""When share was revoked."""
|
|
190
|
+
return self._revoked_at_ts
|
|
191
|
+
|
|
192
|
+
@revoked_at_ts.setter
|
|
193
|
+
def revoked_at_ts(self, value: float | None):
|
|
194
|
+
self._revoked_at_ts = value
|
|
195
|
+
|
|
196
|
+
# Helper Methods
|
|
197
|
+
def is_active(self) -> bool:
|
|
198
|
+
"""Check if share is active."""
|
|
199
|
+
return self._status == "active"
|
|
200
|
+
|
|
201
|
+
def is_revoked(self) -> bool:
|
|
202
|
+
"""Check if share is revoked."""
|
|
203
|
+
return self._status == "revoked"
|
|
204
|
+
|
|
205
|
+
def is_expired(self) -> bool:
|
|
206
|
+
"""Check if share is expired (by status or timestamp)."""
|
|
207
|
+
if self._status == "expired":
|
|
208
|
+
return True
|
|
209
|
+
if self._expires_at_ts:
|
|
210
|
+
return dt.datetime.now(dt.UTC).timestamp() > self._expires_at_ts
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def is_public_link(self) -> bool:
|
|
214
|
+
"""Check if this is a public link (has access token)."""
|
|
215
|
+
return self._access_token is not None and self._access_token != ""
|
|
216
|
+
|
|
217
|
+
def is_internal_share(self) -> bool:
|
|
218
|
+
"""Check if shared with another user (not public)."""
|
|
219
|
+
return self._shared_with_user_id is not None and self._shared_with_user_id != ""
|
|
220
|
+
|
|
221
|
+
def can_view(self) -> bool:
|
|
222
|
+
"""Check if share allows viewing."""
|
|
223
|
+
return self._permission_level in ["view", "download", "edit"]
|
|
224
|
+
|
|
225
|
+
def can_download(self) -> bool:
|
|
226
|
+
"""Check if share allows downloading."""
|
|
227
|
+
return self._permission_level in ["download", "edit"]
|
|
228
|
+
|
|
229
|
+
def can_edit(self) -> bool:
|
|
230
|
+
"""Check if share allows editing."""
|
|
231
|
+
return self._permission_level == "edit"
|
|
232
|
+
|
|
233
|
+
def increment_access_count(self):
|
|
234
|
+
"""Increment access count and update last accessed time."""
|
|
235
|
+
self._access_count += 1
|
|
236
|
+
self._last_accessed_at_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
237
|
+
|
|
238
|
+
def revoke(self):
|
|
239
|
+
"""Revoke the share."""
|
|
240
|
+
self._status = "revoked"
|
|
241
|
+
self._revoked_at_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
242
|
+
|
|
243
|
+
def mark_as_expired(self):
|
|
244
|
+
"""Mark share as expired."""
|
|
245
|
+
self._status = "expired"
|
|
246
|
+
|
|
247
|
+
def has_permission(self, required_level: str) -> bool:
|
|
248
|
+
"""
|
|
249
|
+
Check if share has required permission level.
|
|
250
|
+
|
|
251
|
+
Permission hierarchy: edit > download > view
|
|
252
|
+
"""
|
|
253
|
+
hierarchy = {"view": 1, "download": 2, "edit": 3}
|
|
254
|
+
current = hierarchy.get(self._permission_level, 0)
|
|
255
|
+
required = hierarchy.get(required_level, 0)
|
|
256
|
+
return current >= required
|
|
257
|
+
|
|
258
|
+
def get_expires_at_datetime(self) -> dt.datetime | None:
|
|
259
|
+
"""Get expiration as datetime object."""
|
|
260
|
+
if self._expires_at_ts:
|
|
261
|
+
return dt.datetime.fromtimestamp(self._expires_at_ts, tz=dt.UTC)
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def get_revoked_at_datetime(self) -> dt.datetime | None:
|
|
265
|
+
"""Get revocation time as datetime object."""
|
|
266
|
+
if self._revoked_at_ts:
|
|
267
|
+
return dt.datetime.fromtimestamp(self._revoked_at_ts, tz=dt.UTC)
|
|
268
|
+
return None
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FileVersion model for file versioning system.
|
|
3
|
+
|
|
4
|
+
Geek Cafe, LLC
|
|
5
|
+
MIT License. See Project Root for the license information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
9
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
10
|
+
import datetime as dt
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileVersion(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
File version metadata.
|
|
18
|
+
|
|
19
|
+
Tracks explicit versions when using "explicit" versioning strategy.
|
|
20
|
+
For "s3_native" strategy, tracks S3 version IDs for reference.
|
|
21
|
+
|
|
22
|
+
Access Patterns (DynamoDB Keys):
|
|
23
|
+
- pk: FILE#{tenant_id}#{file_id}
|
|
24
|
+
- sk: VERSION#{version_number}
|
|
25
|
+
- gsi1_pk: FILE#{tenant_id}#{file_id}
|
|
26
|
+
- gsi1_sk: VERSION#{version_number}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__()
|
|
31
|
+
|
|
32
|
+
# Identity
|
|
33
|
+
self._file_id: str | None = None # Parent file ID
|
|
34
|
+
self._version_id: str | None = None # Unique version identifier
|
|
35
|
+
self._version_number: int = 1 # Sequential version number (1, 2, 3...)
|
|
36
|
+
|
|
37
|
+
# S3 Location
|
|
38
|
+
self._s3_key: str | None = None # S3 key for this version (explicit versioning)
|
|
39
|
+
self._s3_version_id: str | None = None # S3 version ID (s3_native versioning)
|
|
40
|
+
self._s3_bucket: str | None = None # S3 bucket (for convenience)
|
|
41
|
+
|
|
42
|
+
# Version Information
|
|
43
|
+
self._file_size: int = 0 # Size of this version in bytes
|
|
44
|
+
self._checksum: str | None = None # MD5/SHA256 checksum
|
|
45
|
+
self._mime_type: str | None = None # MIME type
|
|
46
|
+
|
|
47
|
+
# Change Information
|
|
48
|
+
self._created_by: str | None = None # User who created this version
|
|
49
|
+
self._change_description: str | None = None # Optional description of changes
|
|
50
|
+
|
|
51
|
+
# State
|
|
52
|
+
self._is_current: bool = False # Is this the current version?
|
|
53
|
+
self._status: str = "active" # "active", "archived"
|
|
54
|
+
|
|
55
|
+
# Timestamps (inherited from BaseModel)
|
|
56
|
+
# created_utc_ts - when this version was created
|
|
57
|
+
|
|
58
|
+
# Properties - Identity
|
|
59
|
+
@property
|
|
60
|
+
def file_id(self) -> str | None:
|
|
61
|
+
"""Parent file ID."""
|
|
62
|
+
return self._file_id
|
|
63
|
+
|
|
64
|
+
@file_id.setter
|
|
65
|
+
def file_id(self, value: str | None):
|
|
66
|
+
self._file_id = value
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def version_id(self) -> str | None:
|
|
70
|
+
"""Unique version identifier."""
|
|
71
|
+
return self._version_id or self.id
|
|
72
|
+
|
|
73
|
+
@version_id.setter
|
|
74
|
+
def version_id(self, value: str | None):
|
|
75
|
+
self._version_id = value
|
|
76
|
+
if value:
|
|
77
|
+
self.id = value
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def version_number(self) -> int:
|
|
81
|
+
"""Sequential version number."""
|
|
82
|
+
return self._version_number
|
|
83
|
+
|
|
84
|
+
@version_number.setter
|
|
85
|
+
def version_number(self, value: int):
|
|
86
|
+
self._version_number = value if value is not None else 1
|
|
87
|
+
|
|
88
|
+
# Properties - S3 Location
|
|
89
|
+
@property
|
|
90
|
+
def s3_key(self) -> str | None:
|
|
91
|
+
"""S3 key for this version."""
|
|
92
|
+
return self._s3_key
|
|
93
|
+
|
|
94
|
+
@s3_key.setter
|
|
95
|
+
def s3_key(self, value: str | None):
|
|
96
|
+
self._s3_key = value
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def s3_version_id(self) -> str | None:
|
|
100
|
+
"""S3 version ID (for s3_native strategy)."""
|
|
101
|
+
return self._s3_version_id
|
|
102
|
+
|
|
103
|
+
@s3_version_id.setter
|
|
104
|
+
def s3_version_id(self, value: str | None):
|
|
105
|
+
self._s3_version_id = value
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def s3_bucket(self) -> str | None:
|
|
109
|
+
"""S3 bucket name."""
|
|
110
|
+
return self._s3_bucket
|
|
111
|
+
|
|
112
|
+
@s3_bucket.setter
|
|
113
|
+
def s3_bucket(self, value: str | None):
|
|
114
|
+
self._s3_bucket = value
|
|
115
|
+
|
|
116
|
+
# Properties - Version Information
|
|
117
|
+
@property
|
|
118
|
+
def file_size(self) -> int:
|
|
119
|
+
"""Size of this version in bytes."""
|
|
120
|
+
return self._file_size
|
|
121
|
+
|
|
122
|
+
@file_size.setter
|
|
123
|
+
def file_size(self, value: int):
|
|
124
|
+
self._file_size = value if value is not None else 0
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def checksum(self) -> str | None:
|
|
128
|
+
"""File checksum."""
|
|
129
|
+
return self._checksum
|
|
130
|
+
|
|
131
|
+
@checksum.setter
|
|
132
|
+
def checksum(self, value: str | None):
|
|
133
|
+
self._checksum = value
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def mime_type(self) -> str | None:
|
|
137
|
+
"""MIME type."""
|
|
138
|
+
return self._mime_type
|
|
139
|
+
|
|
140
|
+
@mime_type.setter
|
|
141
|
+
def mime_type(self, value: str | None):
|
|
142
|
+
self._mime_type = value
|
|
143
|
+
|
|
144
|
+
# Properties - Change Information
|
|
145
|
+
@property
|
|
146
|
+
def created_by(self) -> str | None:
|
|
147
|
+
"""User who created this version."""
|
|
148
|
+
return self._created_by
|
|
149
|
+
|
|
150
|
+
@created_by.setter
|
|
151
|
+
def created_by(self, value: str | None):
|
|
152
|
+
self._created_by = value
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def change_description(self) -> str | None:
|
|
156
|
+
"""Description of changes in this version."""
|
|
157
|
+
return self._change_description
|
|
158
|
+
|
|
159
|
+
@change_description.setter
|
|
160
|
+
def change_description(self, value: str | None):
|
|
161
|
+
self._change_description = value
|
|
162
|
+
|
|
163
|
+
# Properties - State
|
|
164
|
+
@property
|
|
165
|
+
def is_current(self) -> bool:
|
|
166
|
+
"""Is this the current version?"""
|
|
167
|
+
return self._is_current
|
|
168
|
+
|
|
169
|
+
@is_current.setter
|
|
170
|
+
def is_current(self, value: bool):
|
|
171
|
+
self._is_current = bool(value)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def status(self) -> str:
|
|
175
|
+
"""Version status: 'active' or 'archived'."""
|
|
176
|
+
return self._status
|
|
177
|
+
|
|
178
|
+
@status.setter
|
|
179
|
+
def status(self, value: str):
|
|
180
|
+
if value not in ["active", "archived"]:
|
|
181
|
+
raise ValueError(f"Invalid status: {value}. Must be 'active' or 'archived'")
|
|
182
|
+
self._status = value
|
|
183
|
+
|
|
184
|
+
# Helper Methods
|
|
185
|
+
def is_active(self) -> bool:
|
|
186
|
+
"""Check if version is active."""
|
|
187
|
+
return self._status == "active"
|
|
188
|
+
|
|
189
|
+
def is_archived(self) -> bool:
|
|
190
|
+
"""Check if version is archived."""
|
|
191
|
+
return self._status == "archived"
|
|
192
|
+
|
|
193
|
+
def get_file_size_mb(self) -> float:
|
|
194
|
+
"""Get file size in megabytes."""
|
|
195
|
+
return self._file_size / (1024 * 1024) if self._file_size else 0.0
|
|
196
|
+
|
|
197
|
+
def get_file_size_kb(self) -> float:
|
|
198
|
+
"""Get file size in kilobytes."""
|
|
199
|
+
return self._file_size / 1024 if self._file_size else 0.0
|
|
200
|
+
|
|
201
|
+
def get_s3_uri(self) -> str | None:
|
|
202
|
+
"""Get full S3 URI (s3://bucket/key)."""
|
|
203
|
+
if self._s3_bucket and self._s3_key:
|
|
204
|
+
uri = f"s3://{self._s3_bucket}/{self._s3_key}"
|
|
205
|
+
if self._s3_version_id:
|
|
206
|
+
uri += f"?versionId={self._s3_version_id}"
|
|
207
|
+
return uri
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def mark_as_current(self):
|
|
211
|
+
"""Mark this version as the current version."""
|
|
212
|
+
self._is_current = True
|
|
213
|
+
|
|
214
|
+
def unmark_as_current(self):
|
|
215
|
+
"""Unmark this version as current."""
|
|
216
|
+
self._is_current = False
|
|
File without changes
|