geek-cafe-saas-sdk 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (194) hide show
  1. geek_cafe_saas_sdk/__init__.py +9 -0
  2. geek_cafe_saas_sdk/core/__init__.py +11 -0
  3. geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
  4. geek_cafe_saas_sdk/core/error_codes.py +132 -0
  5. geek_cafe_saas_sdk/core/service_errors.py +19 -0
  6. geek_cafe_saas_sdk/core/service_result.py +121 -0
  7. geek_cafe_saas_sdk/decorators/__init__.py +64 -0
  8. geek_cafe_saas_sdk/decorators/auth.py +373 -0
  9. geek_cafe_saas_sdk/decorators/core.py +358 -0
  10. geek_cafe_saas_sdk/domains/__init__.py +0 -0
  11. geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
  12. geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
  13. geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
  14. geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
  15. geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
  16. geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
  17. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
  18. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
  19. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
  20. geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
  21. geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
  22. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
  23. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
  24. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
  25. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
  26. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
  27. geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
  28. geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
  29. geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
  30. geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
  31. geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
  32. geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
  33. geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
  34. geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
  35. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
  36. geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
  37. geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
  38. geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
  39. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
  40. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
  41. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
  42. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
  43. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
  44. geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
  45. geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
  46. geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
  47. geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
  48. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
  49. geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
  50. geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
  51. geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
  52. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
  53. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
  54. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
  55. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
  56. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
  57. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
  58. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
  59. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
  60. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
  61. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
  62. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
  63. geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
  64. geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
  65. geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
  66. geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
  67. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
  68. geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
  69. geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
  70. geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
  71. geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
  72. geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
  73. geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
  74. geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
  75. geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
  76. geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
  77. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
  78. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
  79. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
  80. geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
  81. geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
  82. geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
  83. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
  84. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
  85. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
  86. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
  87. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
  88. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
  89. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
  90. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
  91. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
  92. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
  93. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
  94. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
  95. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
  96. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
  97. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
  98. geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
  99. geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
  100. geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
  101. geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
  102. geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
  103. geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
  104. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
  105. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
  106. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
  107. geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
  114. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
  115. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
  116. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
  117. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
  118. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
  119. geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
  120. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
  121. geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
  122. geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
  123. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
  124. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
  125. geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
  126. geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
  127. geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
  128. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
  129. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
  130. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
  131. geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
  132. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
  133. geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
  134. geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
  135. geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
  136. geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
  137. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
  138. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
  139. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
  140. geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
  141. geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
  142. geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
  143. geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
  144. geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
  145. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
  146. geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
  147. geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
  148. geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
  149. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
  150. geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
  151. geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
  152. geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
  153. geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
  154. geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
  155. geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
  156. geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
  157. geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
  158. geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
  159. geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
  160. geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
  161. geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
  162. geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
  163. geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
  164. geek_cafe_saas_sdk/middleware/__init__.py +36 -0
  165. geek_cafe_saas_sdk/middleware/auth.py +85 -0
  166. geek_cafe_saas_sdk/middleware/authorization.py +523 -0
  167. geek_cafe_saas_sdk/middleware/cors.py +63 -0
  168. geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
  169. geek_cafe_saas_sdk/middleware/validation.py +80 -0
  170. geek_cafe_saas_sdk/models/__init__.py +20 -0
  171. geek_cafe_saas_sdk/models/base_model.py +233 -0
  172. geek_cafe_saas_sdk/services/__init__.py +18 -0
  173. geek_cafe_saas_sdk/services/database_service.py +441 -0
  174. geek_cafe_saas_sdk/utilities/__init__.py +88 -0
  175. geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
  176. geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
  177. geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
  178. geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
  179. geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
  180. geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
  181. geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
  182. geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
  183. geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
  184. geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
  185. geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
  186. geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
  187. geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
  188. geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
  189. geek_cafe_saas_sdk/utilities/response.py +209 -0
  190. geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
  191. geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
  192. geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
  193. geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
  194. geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
@@ -0,0 +1,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