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,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
|