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,258 @@
1
+ """
2
+ Directory model for virtual directory structure.
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 Directory(BaseModel):
16
+ """
17
+ Virtual directory in the file system.
18
+
19
+ Represents a logical directory structure. Files reference directories
20
+ via directory_id, but are physically stored in S3 with their own keys.
21
+ Moving a file updates its directory_id, not its S3 location.
22
+
23
+ Access Patterns (DynamoDB Keys):
24
+ - pk: DIRECTORY#{tenant_id}#{directory_id}
25
+ - sk: METADATA
26
+ - gsi1_pk: TENANT#{tenant_id}
27
+ - gsi1_sk: PATH#{full_path}
28
+ - gsi2_pk: DIRECTORY#{tenant_id}#{parent_id}
29
+ - gsi2_sk: NAME#{directory_name}
30
+ """
31
+
32
+ def __init__(self):
33
+ super().__init__()
34
+
35
+ # Identity
36
+ self._directory_id: str | None = None # Unique directory ID
37
+
38
+ # Ownership
39
+ self._owner_id: str | None = None # User who created directory
40
+
41
+ # Directory Information
42
+ self._directory_name: str | None = None # Display name (e.g., "Projects")
43
+ self._full_path: str | None = None # Complete path (/root/projects/2024)
44
+
45
+ # Hierarchy
46
+ self._parent_id: str | None = None # Parent directory (null = root)
47
+ self._depth: int = 0 # Depth in tree (0 = root level)
48
+
49
+ # Contents Tracking
50
+ self._file_count: int = 0 # Number of files in this directory
51
+ self._subdirectory_count: int = 0 # Number of subdirectories
52
+ self._total_size: int = 0 # Total size of all files (bytes)
53
+
54
+ # Metadata
55
+ self._description: str | None = None
56
+ self._color: str | None = None # UI color code (e.g., "#FF5733")
57
+ self._icon: str | None = None # UI icon identifier
58
+
59
+ # State
60
+ self._status: str = "active" # "active", "archived", "deleted"
61
+
62
+ # Timestamps (inherited from BaseModel)
63
+ # created_utc_ts, updated_utc_ts
64
+
65
+ # Properties - Identity
66
+ @property
67
+ def directory_id(self) -> str | None:
68
+ """Unique directory ID."""
69
+ return self._directory_id or self.id
70
+
71
+ @directory_id.setter
72
+ def directory_id(self, value: str | None):
73
+ self._directory_id = value
74
+ if value:
75
+ self.id = value
76
+
77
+ # Properties - Ownership
78
+ @property
79
+ def owner_id(self) -> str | None:
80
+ """User who created directory."""
81
+ return self._owner_id
82
+
83
+ @owner_id.setter
84
+ def owner_id(self, value: str | None):
85
+ self._owner_id = value
86
+
87
+ # Properties - Directory Information
88
+ @property
89
+ def directory_name(self) -> str | None:
90
+ """Directory display name."""
91
+ return self._directory_name
92
+
93
+ @directory_name.setter
94
+ def directory_name(self, value: str | None):
95
+ self._directory_name = value
96
+
97
+ @property
98
+ def full_path(self) -> str | None:
99
+ """Complete path from root."""
100
+ return self._full_path
101
+
102
+ @full_path.setter
103
+ def full_path(self, value: str | None):
104
+ self._full_path = value
105
+
106
+ # Properties - Hierarchy
107
+ @property
108
+ def parent_id(self) -> str | None:
109
+ """Parent directory ID (null = root)."""
110
+ return self._parent_id
111
+
112
+ @parent_id.setter
113
+ def parent_id(self, value: str | None):
114
+ self._parent_id = value
115
+
116
+ @property
117
+ def depth(self) -> int:
118
+ """Depth in directory tree."""
119
+ return self._depth
120
+
121
+ @depth.setter
122
+ def depth(self, value: int):
123
+ self._depth = value if value is not None else 0
124
+
125
+ # Properties - Contents Tracking
126
+ @property
127
+ def file_count(self) -> int:
128
+ """Number of files in this directory."""
129
+ return self._file_count
130
+
131
+ @file_count.setter
132
+ def file_count(self, value: int):
133
+ self._file_count = value if value is not None else 0
134
+
135
+ @property
136
+ def subdirectory_count(self) -> int:
137
+ """Number of subdirectories."""
138
+ return self._subdirectory_count
139
+
140
+ @subdirectory_count.setter
141
+ def subdirectory_count(self, value: int):
142
+ self._subdirectory_count = value if value is not None else 0
143
+
144
+ @property
145
+ def total_size(self) -> int:
146
+ """Total size of all files in bytes."""
147
+ return self._total_size
148
+
149
+ @total_size.setter
150
+ def total_size(self, value: int):
151
+ self._total_size = value if value is not None else 0
152
+
153
+ # Properties - Metadata
154
+ @property
155
+ def description(self) -> str | None:
156
+ """Directory description."""
157
+ return self._description
158
+
159
+ @description.setter
160
+ def description(self, value: str | None):
161
+ self._description = value
162
+
163
+ @property
164
+ def color(self) -> str | None:
165
+ """UI color code."""
166
+ return self._color
167
+
168
+ @color.setter
169
+ def color(self, value: str | None):
170
+ self._color = value
171
+
172
+ @property
173
+ def icon(self) -> str | None:
174
+ """UI icon identifier."""
175
+ return self._icon
176
+
177
+ @icon.setter
178
+ def icon(self, value: str | None):
179
+ self._icon = value
180
+
181
+ # Properties - State
182
+ @property
183
+ def status(self) -> str:
184
+ """Directory status: 'active', 'archived', 'deleted'."""
185
+ return self._status
186
+
187
+ @status.setter
188
+ def status(self, value: str):
189
+ if value not in ["active", "archived", "deleted"]:
190
+ raise ValueError(f"Invalid status: {value}. Must be 'active', 'archived', or 'deleted'")
191
+ self._status = value
192
+
193
+ # Helper Methods
194
+ def is_active(self) -> bool:
195
+ """Check if directory is active."""
196
+ return self._status == "active"
197
+
198
+ def is_archived(self) -> bool:
199
+ """Check if directory is archived."""
200
+ return self._status == "archived"
201
+
202
+ def is_root(self) -> bool:
203
+ """Check if this is a root directory."""
204
+ return self._parent_id is None or self._parent_id == ""
205
+
206
+ def is_empty(self) -> bool:
207
+ """Check if directory has no files or subdirectories."""
208
+ return self._file_count == 0 and self._subdirectory_count == 0
209
+
210
+ def has_files(self) -> bool:
211
+ """Check if directory contains files."""
212
+ return self._file_count > 0
213
+
214
+ def has_subdirectories(self) -> bool:
215
+ """Check if directory contains subdirectories."""
216
+ return self._subdirectory_count > 0
217
+
218
+ def get_total_size_mb(self) -> float:
219
+ """Get total size in megabytes."""
220
+ return self._total_size / (1024 * 1024) if self._total_size else 0.0
221
+
222
+ def get_total_size_gb(self) -> float:
223
+ """Get total size in gigabytes."""
224
+ return self._total_size / (1024 * 1024 * 1024) if self._total_size else 0.0
225
+
226
+ def increment_file_count(self, count: int = 1):
227
+ """Increment the file count."""
228
+ self._file_count += count
229
+
230
+ def decrement_file_count(self, count: int = 1):
231
+ """Decrement the file count."""
232
+ self._file_count = max(0, self._file_count - count)
233
+
234
+ def increment_subdirectory_count(self, count: int = 1):
235
+ """Increment the subdirectory count."""
236
+ self._subdirectory_count += count
237
+
238
+ def decrement_subdirectory_count(self, count: int = 1):
239
+ """Decrement the subdirectory count."""
240
+ self._subdirectory_count = max(0, self._subdirectory_count - count)
241
+
242
+ def add_to_total_size(self, size: int):
243
+ """Add to total size."""
244
+ self._total_size += size
245
+
246
+ def subtract_from_total_size(self, size: int):
247
+ """Subtract from total size."""
248
+ self._total_size = max(0, self._total_size - size)
249
+
250
+ def get_path_parts(self) -> list:
251
+ """Get path as list of parts (e.g., '/a/b/c' -> ['a', 'b', 'c'])."""
252
+ if not self._full_path:
253
+ return []
254
+ return [p for p in self._full_path.split('/') if p]
255
+
256
+ def get_path_depth(self) -> int:
257
+ """Calculate depth from path."""
258
+ return len(self.get_path_parts())
@@ -0,0 +1,312 @@
1
+ """
2
+ File model for file storage 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 List, Optional, Dict, Any
12
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
13
+
14
+
15
+ class File(BaseModel):
16
+ """
17
+ File metadata and references.
18
+
19
+ Represents a file in the system with metadata, virtual path, and S3 location.
20
+ Does not contain file data (stored in S3) - only metadata and references.
21
+
22
+ Access Patterns (DynamoDB Keys):
23
+ - pk: FILE#{tenant_id}#{file_id}
24
+ - sk: METADATA
25
+ - gsi1_pk: TENANT#{tenant_id}
26
+ - gsi1_sk: DIRECTORY#{directory_id}#{file_name}
27
+ - gsi2_pk: TENANT#{tenant_id}#USER#{owner_id}
28
+ - gsi2_sk: FILE#{created_utc_ts}
29
+
30
+ Versioning Strategies:
31
+ - "s3_native": Same S3 key, S3 manages versions
32
+ - "explicit": Unique S3 key per version, we manage versions
33
+ """
34
+
35
+ def __init__(self):
36
+ super().__init__()
37
+
38
+ # File Identity
39
+ self._file_id: str | None = None # Unique file ID (same as self.id)
40
+
41
+ # Ownership (inherited tenant_id from BaseModel)
42
+ self._owner_id: str | None = None # User who owns the file
43
+
44
+ # File Information
45
+ self._file_name: str | None = None # Display name (e.g., "report.pdf")
46
+ self._file_extension: str | None = None # Extension (e.g., ".pdf")
47
+ self._mime_type: str | None = None # MIME type (e.g., "application/pdf")
48
+ self._file_size: int = 0 # Size in bytes
49
+ self._checksum: str | None = None # MD5/SHA256 checksum
50
+
51
+ # Virtual Location (logical structure in DynamoDB)
52
+ self._directory_id: str | None = None # Parent directory ID (null = root)
53
+ self._virtual_path: str | None = None # Full virtual path (/docs/reports/Q1.pdf)
54
+
55
+ # S3 Physical Location
56
+ self._s3_bucket: str | None = None # S3 bucket name
57
+ self._s3_key: str | None = None # S3 object key (physical location)
58
+ self._s3_version_id: str | None = None # S3 version ID (for s3_native strategy)
59
+
60
+ # Versioning Strategy
61
+ self._versioning_strategy: str = "explicit" # "s3_native" or "explicit"
62
+ self._current_version_id: str | None = None # Current version identifier
63
+ self._version_count: int = 0 # Total number of versions
64
+
65
+ # Metadata
66
+ self._description: str | None = None # Optional description
67
+ self._tags: List[str] = [] # Searchable tags
68
+
69
+ # State
70
+ self._status: str = "active" # "active", "archived", "deleted"
71
+ self._is_shared: bool = False # Has active shares
72
+
73
+ # Timestamps (inherited from BaseModel)
74
+ # created_utc_ts, updated_utc_ts, deleted_utc_ts
75
+
76
+ # Properties - File Identity
77
+ @property
78
+ def file_id(self) -> str | None:
79
+ """Unique file ID."""
80
+ return self._file_id or self.id
81
+
82
+ @file_id.setter
83
+ def file_id(self, value: str | None):
84
+ self._file_id = value
85
+ if value:
86
+ self.id = value
87
+
88
+ # Properties - Ownership
89
+ @property
90
+ def owner_id(self) -> str | None:
91
+ """User who owns the file."""
92
+ return self._owner_id
93
+
94
+ @owner_id.setter
95
+ def owner_id(self, value: str | None):
96
+ self._owner_id = value
97
+
98
+ # Properties - File Information
99
+ @property
100
+ def file_name(self) -> str | None:
101
+ """Display file name."""
102
+ return self._file_name
103
+
104
+ @file_name.setter
105
+ def file_name(self, value: str | None):
106
+ self._file_name = value
107
+
108
+ @property
109
+ def file_extension(self) -> str | None:
110
+ """File extension (e.g., '.pdf')."""
111
+ return self._file_extension
112
+
113
+ @file_extension.setter
114
+ def file_extension(self, value: str | None):
115
+ self._file_extension = value
116
+
117
+ @property
118
+ def mime_type(self) -> str | None:
119
+ """MIME type (e.g., 'application/pdf')."""
120
+ return self._mime_type
121
+
122
+ @mime_type.setter
123
+ def mime_type(self, value: str | None):
124
+ self._mime_type = value
125
+
126
+ @property
127
+ def file_size(self) -> int:
128
+ """File size in bytes."""
129
+ return self._file_size
130
+
131
+ @file_size.setter
132
+ def file_size(self, value: int):
133
+ self._file_size = value if value is not None else 0
134
+
135
+ @property
136
+ def checksum(self) -> str | None:
137
+ """File checksum (MD5/SHA256)."""
138
+ return self._checksum
139
+
140
+ @checksum.setter
141
+ def checksum(self, value: str | None):
142
+ self._checksum = value
143
+
144
+ # Properties - Virtual Location
145
+ @property
146
+ def directory_id(self) -> str | None:
147
+ """Parent directory ID (null = root)."""
148
+ return self._directory_id
149
+
150
+ @directory_id.setter
151
+ def directory_id(self, value: str | None):
152
+ self._directory_id = value
153
+
154
+ @property
155
+ def virtual_path(self) -> str | None:
156
+ """Full virtual path (e.g., /docs/reports/Q1.pdf)."""
157
+ return self._virtual_path
158
+
159
+ @virtual_path.setter
160
+ def virtual_path(self, value: str | None):
161
+ self._virtual_path = value
162
+
163
+ # Properties - S3 Physical Location
164
+ @property
165
+ def s3_bucket(self) -> str | None:
166
+ """S3 bucket name."""
167
+ return self._s3_bucket
168
+
169
+ @s3_bucket.setter
170
+ def s3_bucket(self, value: str | None):
171
+ self._s3_bucket = value
172
+
173
+ @property
174
+ def s3_key(self) -> str | None:
175
+ """S3 object key (physical location)."""
176
+ return self._s3_key
177
+
178
+ @s3_key.setter
179
+ def s3_key(self, value: str | None):
180
+ self._s3_key = value
181
+
182
+ @property
183
+ def s3_version_id(self) -> str | None:
184
+ """S3 version ID (for s3_native versioning)."""
185
+ return self._s3_version_id
186
+
187
+ @s3_version_id.setter
188
+ def s3_version_id(self, value: str | None):
189
+ self._s3_version_id = value
190
+
191
+ # Properties - Versioning
192
+ @property
193
+ def versioning_strategy(self) -> str:
194
+ """Versioning strategy: 's3_native' or 'explicit'."""
195
+ return self._versioning_strategy
196
+
197
+ @versioning_strategy.setter
198
+ def versioning_strategy(self, value: str):
199
+ if value not in ["s3_native", "explicit"]:
200
+ raise ValueError(f"Invalid versioning strategy: {value}. Must be 's3_native' or 'explicit'")
201
+ self._versioning_strategy = value
202
+
203
+ @property
204
+ def current_version_id(self) -> str | None:
205
+ """Current version identifier."""
206
+ return self._current_version_id
207
+
208
+ @current_version_id.setter
209
+ def current_version_id(self, value: str | None):
210
+ self._current_version_id = value
211
+
212
+ @property
213
+ def version_count(self) -> int:
214
+ """Total number of versions."""
215
+ return self._version_count
216
+
217
+ @version_count.setter
218
+ def version_count(self, value: int):
219
+ self._version_count = value if value is not None else 0
220
+
221
+ # Properties - Metadata
222
+ @property
223
+ def description(self) -> str | None:
224
+ """File description."""
225
+ return self._description
226
+
227
+ @description.setter
228
+ def description(self, value: str | None):
229
+ self._description = value
230
+
231
+ @property
232
+ def tags(self) -> List[str]:
233
+ """Searchable tags."""
234
+ return self._tags
235
+
236
+ @tags.setter
237
+ def tags(self, value: List[str] | None):
238
+ self._tags = value if isinstance(value, list) else []
239
+
240
+ # Properties - State
241
+ @property
242
+ def status(self) -> str:
243
+ """File status: 'active', 'archived', 'deleted'."""
244
+ return self._status
245
+
246
+ @status.setter
247
+ def status(self, value: str):
248
+ if value not in ["active", "archived", "deleted"]:
249
+ raise ValueError(f"Invalid status: {value}. Must be 'active', 'archived', or 'deleted'")
250
+ self._status = value
251
+
252
+ @property
253
+ def is_shared(self) -> bool:
254
+ """Has active shares."""
255
+ return self._is_shared
256
+
257
+ @is_shared.setter
258
+ def is_shared(self, value: bool):
259
+ self._is_shared = bool(value)
260
+
261
+ # Helper Methods
262
+ def is_active(self) -> bool:
263
+ """Check if file is active."""
264
+ return self._status == "active"
265
+
266
+ def is_archived(self) -> bool:
267
+ """Check if file is archived."""
268
+ return self._status == "archived"
269
+
270
+ def is_in_root(self) -> bool:
271
+ """Check if file is in root directory."""
272
+ return self._directory_id is None or self._directory_id == ""
273
+
274
+ def uses_s3_native_versioning(self) -> bool:
275
+ """Check if using S3 native versioning."""
276
+ return self._versioning_strategy == "s3_native"
277
+
278
+ def uses_explicit_versioning(self) -> bool:
279
+ """Check if using explicit versioning."""
280
+ return self._versioning_strategy == "explicit"
281
+
282
+ def get_file_size_mb(self) -> float:
283
+ """Get file size in megabytes."""
284
+ return self._file_size / (1024 * 1024) if self._file_size else 0.0
285
+
286
+ def get_file_size_kb(self) -> float:
287
+ """Get file size in kilobytes."""
288
+ return self._file_size / 1024 if self._file_size else 0.0
289
+
290
+ def add_tag(self, tag: str):
291
+ """Add a tag to the file."""
292
+ if tag and tag not in self._tags:
293
+ self._tags.append(tag)
294
+
295
+ def remove_tag(self, tag: str):
296
+ """Remove a tag from the file."""
297
+ if tag in self._tags:
298
+ self._tags.remove(tag)
299
+
300
+ def has_tag(self, tag: str) -> bool:
301
+ """Check if file has a specific tag."""
302
+ return tag in self._tags
303
+
304
+ def increment_version_count(self):
305
+ """Increment the version count."""
306
+ self._version_count += 1
307
+
308
+ def get_s3_uri(self) -> str | None:
309
+ """Get full S3 URI (s3://bucket/key)."""
310
+ if self._s3_bucket and self._s3_key:
311
+ return f"s3://{self._s3_bucket}/{self._s3_key}"
312
+ return None