geek-cafe-saas-sdk 0.6.0__py3-none-any.whl → 0.7.1__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 +2 -2
- geek_cafe_saas_sdk/domains/files/handlers/README.md +446 -0
- geek_cafe_saas_sdk/domains/files/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +121 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +80 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +62 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +72 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +99 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +104 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +99 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +68 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +76 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
- geek_cafe_saas_sdk/domains/files/models/file.py +158 -16
- geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +21 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
- geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py +487 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +67 -103
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
- geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
- geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
- geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
- geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
- geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
- geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
- geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
- geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
- geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
- geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
- geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
- geek_cafe_saas_sdk/services/database_service.py +10 -6
- geek_cafe_saas_sdk/utilities/cognito_utility.py +16 -26
- geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +11 -11
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +94 -23
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -19,12 +19,16 @@ class File(BaseModel):
|
|
|
19
19
|
Represents a file in the system with metadata, virtual path, and S3 location.
|
|
20
20
|
Does not contain file data (stored in S3) - only metadata and references.
|
|
21
21
|
|
|
22
|
+
Multi-Tenancy:
|
|
23
|
+
- tenant_id: Organization/company (can have multiple users)
|
|
24
|
+
- owner_id: Specific user within the tenant who owns this file
|
|
25
|
+
|
|
22
26
|
Access Patterns (DynamoDB Keys):
|
|
23
27
|
- pk: FILE#{tenant_id}#{file_id}
|
|
24
|
-
- sk:
|
|
25
|
-
- gsi1_pk:
|
|
26
|
-
- gsi1_sk:
|
|
27
|
-
- gsi2_pk:
|
|
28
|
+
- sk: metadata
|
|
29
|
+
- gsi1_pk: tenant#{tenant_id}
|
|
30
|
+
- gsi1_sk: directory#{directory_id}#{file_name}
|
|
31
|
+
- gsi2_pk: tenant#{tenant_id}#USER#{owner_id}
|
|
28
32
|
- gsi2_sk: FILE#{created_utc_ts}
|
|
29
33
|
|
|
30
34
|
Versioning Strategies:
|
|
@@ -35,11 +39,9 @@ class File(BaseModel):
|
|
|
35
39
|
def __init__(self):
|
|
36
40
|
super().__init__()
|
|
37
41
|
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# Ownership (inherited tenant_id from BaseModel)
|
|
42
|
-
self._owner_id: str | None = None # User who owns the file
|
|
42
|
+
# Identity (inherited from BaseModel: id, tenant_id)
|
|
43
|
+
# Note: tenant_id = organization/company, owner_id = specific user within tenant
|
|
44
|
+
self._owner_id: str | None = None # User ID who owns this file
|
|
43
45
|
|
|
44
46
|
# File Information
|
|
45
47
|
self._file_name: str | None = None # Display name (e.g., "report.pdf")
|
|
@@ -70,25 +72,72 @@ class File(BaseModel):
|
|
|
70
72
|
self._status: str = "active" # "active", "archived", "deleted"
|
|
71
73
|
self._is_shared: bool = False # Has active shares
|
|
72
74
|
|
|
75
|
+
# Lineage Tracking (for data processing pipelines)
|
|
76
|
+
self._file_role: str = "standalone" # "standalone", "original", "main", "derived"
|
|
77
|
+
self._original_file_id: str | None = None # Root file in lineage chain
|
|
78
|
+
self._parent_file_id: str | None = None # Immediate parent file
|
|
79
|
+
|
|
80
|
+
# Transformation Tracking
|
|
81
|
+
self._transformation_type: str | None = None # "convert", "clean", "process"
|
|
82
|
+
self._transformation_operation: str | None = None # "xls_to_csv", "data_cleaning_v2"
|
|
83
|
+
self._transformation_metadata: Dict[str, Any] | None = None # Additional operation details
|
|
84
|
+
|
|
85
|
+
# Relationship Counts
|
|
86
|
+
self._derived_file_count: int = 0 # Number of files derived from this one
|
|
87
|
+
|
|
73
88
|
# Timestamps (inherited from BaseModel)
|
|
74
89
|
# created_utc_ts, updated_utc_ts, deleted_utc_ts
|
|
90
|
+
|
|
91
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
92
|
+
self._setup_indexes()
|
|
75
93
|
|
|
76
|
-
|
|
94
|
+
def _setup_indexes(self):
|
|
95
|
+
"""Setup DynamoDB indexes for file queries."""
|
|
96
|
+
|
|
97
|
+
# Primary index: File by ID
|
|
98
|
+
primary = DynamoDBIndex()
|
|
99
|
+
primary.name = "primary"
|
|
100
|
+
primary.partition_key.attribute_name = "pk"
|
|
101
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("file", self.id))
|
|
102
|
+
primary.sort_key.attribute_name = "sk"
|
|
103
|
+
primary.sort_key.value = lambda: "metadata"
|
|
104
|
+
self.indexes.add_primary(primary)
|
|
105
|
+
|
|
106
|
+
# GSI1: Files by directory
|
|
107
|
+
gsi = DynamoDBIndex()
|
|
108
|
+
gsi.name = "gsi1"
|
|
109
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
110
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
111
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
112
|
+
if self.directory_id:
|
|
113
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("directory", self.directory_id), ("file", self.file_name))
|
|
114
|
+
else:
|
|
115
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("root", "file"), ("name", self.file_name))
|
|
116
|
+
self.indexes.add_secondary(gsi)
|
|
117
|
+
|
|
118
|
+
# GSI2: Files by owner
|
|
119
|
+
gsi = DynamoDBIndex()
|
|
120
|
+
gsi.name = "gsi2"
|
|
121
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
122
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id), ("user", self.owner_id))
|
|
123
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
124
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("file", self.created_utc_ts))
|
|
125
|
+
self.indexes.add_secondary(gsi)
|
|
126
|
+
|
|
127
|
+
# Properties - File Identity (alias for BaseModel.id)
|
|
77
128
|
@property
|
|
78
129
|
def file_id(self) -> str | None:
|
|
79
|
-
"""Unique file ID."""
|
|
80
|
-
return self.
|
|
130
|
+
"""Unique file ID (alias for id)."""
|
|
131
|
+
return self.id
|
|
81
132
|
|
|
82
133
|
@file_id.setter
|
|
83
134
|
def file_id(self, value: str | None):
|
|
84
|
-
self.
|
|
85
|
-
if value:
|
|
86
|
-
self.id = value
|
|
135
|
+
self.id = value
|
|
87
136
|
|
|
88
137
|
# Properties - Ownership
|
|
89
138
|
@property
|
|
90
139
|
def owner_id(self) -> str | None:
|
|
91
|
-
"""User who owns the file."""
|
|
140
|
+
"""User ID who owns the file (not tenant_id - that's the organization)."""
|
|
92
141
|
return self._owner_id
|
|
93
142
|
|
|
94
143
|
@owner_id.setter
|
|
@@ -258,6 +307,74 @@ class File(BaseModel):
|
|
|
258
307
|
def is_shared(self, value: bool):
|
|
259
308
|
self._is_shared = bool(value)
|
|
260
309
|
|
|
310
|
+
# Properties - Lineage Tracking
|
|
311
|
+
@property
|
|
312
|
+
def file_role(self) -> str:
|
|
313
|
+
"""File role in lineage chain: 'standalone', 'original', 'main', 'derived'."""
|
|
314
|
+
return self._file_role
|
|
315
|
+
|
|
316
|
+
@file_role.setter
|
|
317
|
+
def file_role(self, value: str | None):
|
|
318
|
+
valid_roles = ["standalone", "original", "main", "derived"]
|
|
319
|
+
if value in valid_roles:
|
|
320
|
+
self._file_role = value
|
|
321
|
+
else:
|
|
322
|
+
self._file_role = "standalone"
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def original_file_id(self) -> str | None:
|
|
326
|
+
"""Root file in lineage chain."""
|
|
327
|
+
return self._original_file_id
|
|
328
|
+
|
|
329
|
+
@original_file_id.setter
|
|
330
|
+
def original_file_id(self, value: str | None):
|
|
331
|
+
self._original_file_id = value
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def parent_file_id(self) -> str | None:
|
|
335
|
+
"""Immediate parent file."""
|
|
336
|
+
return self._parent_file_id
|
|
337
|
+
|
|
338
|
+
@parent_file_id.setter
|
|
339
|
+
def parent_file_id(self, value: str | None):
|
|
340
|
+
self._parent_file_id = value
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def transformation_type(self) -> str | None:
|
|
344
|
+
"""Type of transformation applied: 'convert', 'clean', 'process'."""
|
|
345
|
+
return self._transformation_type
|
|
346
|
+
|
|
347
|
+
@transformation_type.setter
|
|
348
|
+
def transformation_type(self, value: str | None):
|
|
349
|
+
self._transformation_type = value
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def transformation_operation(self) -> str | None:
|
|
353
|
+
"""Specific operation performed (e.g., 'xls_to_csv', 'data_cleaning_v2')."""
|
|
354
|
+
return self._transformation_operation
|
|
355
|
+
|
|
356
|
+
@transformation_operation.setter
|
|
357
|
+
def transformation_operation(self, value: str | None):
|
|
358
|
+
self._transformation_operation = value
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def transformation_metadata(self) -> Dict[str, Any] | None:
|
|
362
|
+
"""Additional transformation details."""
|
|
363
|
+
return self._transformation_metadata
|
|
364
|
+
|
|
365
|
+
@transformation_metadata.setter
|
|
366
|
+
def transformation_metadata(self, value: Dict[str, Any] | None):
|
|
367
|
+
self._transformation_metadata = value if isinstance(value, dict) else None
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def derived_file_count(self) -> int:
|
|
371
|
+
"""Number of files derived from this one."""
|
|
372
|
+
return self._derived_file_count
|
|
373
|
+
|
|
374
|
+
@derived_file_count.setter
|
|
375
|
+
def derived_file_count(self, value: int | None):
|
|
376
|
+
self._derived_file_count = value if isinstance(value, int) else 0
|
|
377
|
+
|
|
261
378
|
# Helper Methods
|
|
262
379
|
def is_active(self) -> bool:
|
|
263
380
|
"""Check if file is active."""
|
|
@@ -310,3 +427,28 @@ class File(BaseModel):
|
|
|
310
427
|
if self._s3_bucket and self._s3_key:
|
|
311
428
|
return f"s3://{self._s3_bucket}/{self._s3_key}"
|
|
312
429
|
return None
|
|
430
|
+
|
|
431
|
+
# Lineage Helper Methods
|
|
432
|
+
def has_lineage(self) -> bool:
|
|
433
|
+
"""Check if file participates in lineage tracking."""
|
|
434
|
+
return self._file_role != "standalone"
|
|
435
|
+
|
|
436
|
+
def is_original(self) -> bool:
|
|
437
|
+
"""Check if this is an original file."""
|
|
438
|
+
return self._file_role == "original"
|
|
439
|
+
|
|
440
|
+
def is_main(self) -> bool:
|
|
441
|
+
"""Check if this is a main file."""
|
|
442
|
+
return self._file_role == "main"
|
|
443
|
+
|
|
444
|
+
def is_derived(self) -> bool:
|
|
445
|
+
"""Check if this is a derived file."""
|
|
446
|
+
return self._file_role == "derived"
|
|
447
|
+
|
|
448
|
+
def is_standalone(self) -> bool:
|
|
449
|
+
"""Check if this is a standalone file (no lineage)."""
|
|
450
|
+
return self._file_role == "standalone"
|
|
451
|
+
|
|
452
|
+
def increment_derived_count(self):
|
|
453
|
+
"""Increment the derived file count."""
|
|
454
|
+
self._derived_file_count += 1
|
|
@@ -63,6 +63,39 @@ class FileShare(BaseModel):
|
|
|
63
63
|
|
|
64
64
|
# Timestamps (inherited from BaseModel)
|
|
65
65
|
# created_utc_ts
|
|
66
|
+
|
|
67
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
68
|
+
self._setup_indexes()
|
|
69
|
+
|
|
70
|
+
def _setup_indexes(self):
|
|
71
|
+
"""Setup DynamoDB indexes for file share queries."""
|
|
72
|
+
|
|
73
|
+
# Primary index: Share by ID
|
|
74
|
+
primary = DynamoDBIndex()
|
|
75
|
+
primary.name = "primary"
|
|
76
|
+
primary.partition_key.attribute_name = "pk"
|
|
77
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("share", self.id))
|
|
78
|
+
primary.sort_key.attribute_name = "sk"
|
|
79
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("share", self.id))
|
|
80
|
+
self.indexes.add_primary(primary)
|
|
81
|
+
|
|
82
|
+
# GSI1: Shares by file
|
|
83
|
+
gsi = DynamoDBIndex()
|
|
84
|
+
gsi.name = "gsi1"
|
|
85
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
86
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("file", self.file_id))
|
|
87
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
88
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("created", self.created_utc_ts))
|
|
89
|
+
self.indexes.add_secondary(gsi)
|
|
90
|
+
|
|
91
|
+
# GSI2: Shares by shared_with_user
|
|
92
|
+
gsi = DynamoDBIndex()
|
|
93
|
+
gsi.name = "gsi2"
|
|
94
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
95
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("shared_with", self.shared_with_user_id))
|
|
96
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
97
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("created", self.created_utc_ts))
|
|
98
|
+
self.indexes.add_secondary(gsi)
|
|
66
99
|
|
|
67
100
|
# Properties - Identity
|
|
68
101
|
@property
|
|
@@ -54,6 +54,30 @@ class FileVersion(BaseModel):
|
|
|
54
54
|
|
|
55
55
|
# Timestamps (inherited from BaseModel)
|
|
56
56
|
# created_utc_ts - when this version was created
|
|
57
|
+
|
|
58
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
59
|
+
self._setup_indexes()
|
|
60
|
+
|
|
61
|
+
def _setup_indexes(self):
|
|
62
|
+
"""Setup DynamoDB indexes for file version queries."""
|
|
63
|
+
|
|
64
|
+
# Primary index: Version by ID
|
|
65
|
+
primary = DynamoDBIndex()
|
|
66
|
+
primary.name = "primary"
|
|
67
|
+
primary.partition_key.attribute_name = "pk"
|
|
68
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("file_version", self.id))
|
|
69
|
+
primary.sort_key.attribute_name = "sk"
|
|
70
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("version", self.id))
|
|
71
|
+
self.indexes.add_primary(primary)
|
|
72
|
+
|
|
73
|
+
# GSI1: Versions by file (ordered by version number)
|
|
74
|
+
gsi = DynamoDBIndex()
|
|
75
|
+
gsi.name = "gsi1"
|
|
76
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
77
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("file", self.file_id))
|
|
78
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
79
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("version", self.version_number))
|
|
80
|
+
self.indexes.add_secondary(gsi)
|
|
57
81
|
|
|
58
82
|
# Properties - Identity
|
|
59
83
|
@property
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""File services.
|
|
2
|
+
|
|
3
|
+
Geek Cafe, LLC
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .file_system_service import FileSystemService
|
|
8
|
+
from .directory_service import DirectoryService
|
|
9
|
+
from .file_version_service import FileVersionService
|
|
10
|
+
from .file_share_service import FileShareService
|
|
11
|
+
from .s3_file_service import S3FileService
|
|
12
|
+
from .file_lineage_service import FileLineageService
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FileSystemService",
|
|
16
|
+
"DirectoryService",
|
|
17
|
+
"FileVersionService",
|
|
18
|
+
"FileShareService",
|
|
19
|
+
"S3FileService",
|
|
20
|
+
"FileLineageService",
|
|
21
|
+
]
|
|
@@ -113,28 +113,11 @@ class DirectoryService(DatabaseService[Directory]):
|
|
|
113
113
|
directory.total_size = 0
|
|
114
114
|
|
|
115
115
|
# Save to DynamoDB
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
item = directory.to_dictionary()
|
|
120
|
-
item["pk"] = pk
|
|
121
|
-
item["sk"] = sk
|
|
122
|
-
|
|
123
|
-
# GSI1: Directories by parent
|
|
124
|
-
item["gsi1_pk"] = f"TENANT#{tenant_id}"
|
|
125
|
-
if parent_id:
|
|
126
|
-
item["gsi1_sk"] = f"PARENT#{parent_id}#{directory_name}"
|
|
127
|
-
else:
|
|
128
|
-
item["gsi1_sk"] = f"PARENT#ROOT#{directory_name}"
|
|
116
|
+
directory.prep_for_save()
|
|
117
|
+
save_result = self._save_model(directory)
|
|
129
118
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
item["gsi2_sk"] = f"DIR#{directory.created_utc_ts}"
|
|
133
|
-
|
|
134
|
-
self.dynamodb.save(
|
|
135
|
-
item=item,
|
|
136
|
-
table_name=self.table_name
|
|
137
|
-
)
|
|
119
|
+
if not save_result.success:
|
|
120
|
+
return save_result
|
|
138
121
|
|
|
139
122
|
# Update parent's subdirectory count
|
|
140
123
|
if parent_id:
|
|
@@ -172,22 +155,13 @@ class DirectoryService(DatabaseService[Directory]):
|
|
|
172
155
|
ServiceResult with Directory model
|
|
173
156
|
"""
|
|
174
157
|
try:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
result = self.dynamodb.get(
|
|
179
|
-
table_name=self.table_name,
|
|
180
|
-
key={"pk": pk, "sk": sk}
|
|
181
|
-
)
|
|
158
|
+
# Use helper method with tenant check
|
|
159
|
+
directory = self._get_model_by_id_with_tenant_check(resource_id, Directory, tenant_id)
|
|
182
160
|
|
|
183
161
|
# Check if directory exists
|
|
184
|
-
if not
|
|
162
|
+
if not directory:
|
|
185
163
|
raise NotFoundError(f"Directory not found: {resource_id}")
|
|
186
164
|
|
|
187
|
-
# Convert to Directory model
|
|
188
|
-
directory = Directory()
|
|
189
|
-
directory.map(result['Item'])
|
|
190
|
-
|
|
191
165
|
# Access control: Check if user is owner
|
|
192
166
|
if directory.owner_id != user_id:
|
|
193
167
|
# TODO: Check shared access
|
|
@@ -292,27 +266,8 @@ class DirectoryService(DatabaseService[Directory]):
|
|
|
292
266
|
directory.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
293
267
|
|
|
294
268
|
# Save to DynamoDB
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
item = directory.to_dictionary()
|
|
299
|
-
item["pk"] = pk
|
|
300
|
-
item["sk"] = sk
|
|
301
|
-
|
|
302
|
-
# Update GSI keys if name changed
|
|
303
|
-
if "directory_name" in updates:
|
|
304
|
-
item["gsi1_pk"] = f"TENANT#{tenant_id}"
|
|
305
|
-
if directory.parent_id:
|
|
306
|
-
item["gsi1_sk"] = f"PARENT#{directory.parent_id}#{directory.directory_name}"
|
|
307
|
-
else:
|
|
308
|
-
item["gsi1_sk"] = f"PARENT#ROOT#{directory.directory_name}"
|
|
309
|
-
|
|
310
|
-
self.dynamodb.save(
|
|
311
|
-
item=item,
|
|
312
|
-
table_name=self.table_name
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
return ServiceResult.success_result(directory)
|
|
269
|
+
directory.prep_for_save()
|
|
270
|
+
return self._save_model(directory)
|
|
316
271
|
|
|
317
272
|
except (ValidationError, AccessDeniedError) as e:
|
|
318
273
|
return ServiceResult.error_result(
|
|
@@ -369,17 +324,11 @@ class DirectoryService(DatabaseService[Directory]):
|
|
|
369
324
|
directory.status = "deleted"
|
|
370
325
|
directory.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
371
326
|
|
|
372
|
-
|
|
373
|
-
|
|
327
|
+
directory.prep_for_save()
|
|
328
|
+
save_result = self._save_model(directory)
|
|
374
329
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
item["sk"] = sk
|
|
378
|
-
|
|
379
|
-
self.dynamodb.save(
|
|
380
|
-
item=item,
|
|
381
|
-
table_name=self.table_name
|
|
382
|
-
)
|
|
330
|
+
if not save_result.success:
|
|
331
|
+
return save_result
|
|
383
332
|
|
|
384
333
|
# Update parent's subdirectory count
|
|
385
334
|
if directory.parent_id:
|
|
@@ -419,30 +368,26 @@ class DirectoryService(DatabaseService[Directory]):
|
|
|
419
368
|
ServiceResult with list of Directory models
|
|
420
369
|
"""
|
|
421
370
|
try:
|
|
422
|
-
|
|
371
|
+
# Use GSI2 to query subdirectories by parent
|
|
372
|
+
temp_directory = Directory()
|
|
373
|
+
temp_directory.tenant_id = tenant_id
|
|
374
|
+
temp_directory.parent_id = parent_id # None for root directories
|
|
423
375
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
# Query GSI1
|
|
430
|
-
results = self.dynamodb.query(
|
|
431
|
-
key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').begins_with(gsi1_sk_prefix),
|
|
432
|
-
table_name=self.table_name,
|
|
433
|
-
index_name="gsi1",
|
|
434
|
-
limit=limit
|
|
435
|
-
)
|
|
376
|
+
# Query using helper method
|
|
377
|
+
query_result = self._query_by_index(temp_directory, "gsi2", limit=limit, ascending=True)
|
|
378
|
+
|
|
379
|
+
if not query_result.success:
|
|
380
|
+
return query_result
|
|
436
381
|
|
|
382
|
+
# Filter results
|
|
437
383
|
directories = []
|
|
438
|
-
for
|
|
439
|
-
|
|
440
|
-
directory.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if directory.owner_id == user_id:
|
|
384
|
+
for directory in query_result.data:
|
|
385
|
+
# Debug: print what we're seeing
|
|
386
|
+
# print(f"Found dir: {directory.directory_name}, parent_id={directory.parent_id}, expected={parent_id}")
|
|
387
|
+
# Filter out deleted directories and apply access control
|
|
388
|
+
if directory.status != "deleted" and directory.owner_id == user_id:
|
|
389
|
+
# Only include directories whose parent_id matches what we're looking for
|
|
390
|
+
if directory.parent_id == parent_id:
|
|
446
391
|
directories.append(directory)
|
|
447
392
|
|
|
448
393
|
return ServiceResult.success_result(directories)
|
|
@@ -570,24 +515,11 @@ class DirectoryService(DatabaseService[Directory]):
|
|
|
570
515
|
directory.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
571
516
|
|
|
572
517
|
# Save to DynamoDB
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
item = directory.to_dictionary()
|
|
577
|
-
item["pk"] = pk
|
|
578
|
-
item["sk"] = sk
|
|
579
|
-
|
|
580
|
-
# Update GSI1 for new parent
|
|
581
|
-
item["gsi1_pk"] = f"TENANT#{tenant_id}"
|
|
582
|
-
if new_parent_id:
|
|
583
|
-
item["gsi1_sk"] = f"PARENT#{new_parent_id}#{directory.directory_name}"
|
|
584
|
-
else:
|
|
585
|
-
item["gsi1_sk"] = f"PARENT#ROOT#{directory.directory_name}"
|
|
518
|
+
directory.prep_for_save()
|
|
519
|
+
save_result = self._save_model(directory)
|
|
586
520
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
table_name=self.table_name
|
|
590
|
-
)
|
|
521
|
+
if not save_result.success:
|
|
522
|
+
return save_result
|
|
591
523
|
|
|
592
524
|
# Update parent counts
|
|
593
525
|
if old_parent_id:
|
|
@@ -619,21 +551,23 @@ class DirectoryService(DatabaseService[Directory]):
|
|
|
619
551
|
) -> bool:
|
|
620
552
|
"""Check if directory name already exists in parent."""
|
|
621
553
|
try:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
554
|
+
# Use GSI2 to query subdirectories by parent
|
|
555
|
+
temp_directory = Directory()
|
|
556
|
+
temp_directory.tenant_id = tenant_id
|
|
557
|
+
temp_directory.parent_id = parent_id
|
|
558
|
+
temp_directory.directory_name = directory_name
|
|
627
559
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
limit=1
|
|
633
|
-
)
|
|
560
|
+
query_result = self._query_by_index(temp_directory, "gsi2", limit=1)
|
|
561
|
+
|
|
562
|
+
if not query_result.success:
|
|
563
|
+
return False
|
|
634
564
|
|
|
635
|
-
|
|
636
|
-
|
|
565
|
+
# Check if any non-deleted directories with this name exist
|
|
566
|
+
for directory in query_result.data:
|
|
567
|
+
if directory.directory_name == directory_name and directory.status != "deleted":
|
|
568
|
+
return True
|
|
569
|
+
|
|
570
|
+
return False
|
|
637
571
|
|
|
638
572
|
except Exception:
|
|
639
573
|
return False
|
|
@@ -646,29 +580,14 @@ class DirectoryService(DatabaseService[Directory]):
|
|
|
646
580
|
) -> None:
|
|
647
581
|
"""Increment or decrement subdirectory count."""
|
|
648
582
|
try:
|
|
649
|
-
|
|
650
|
-
|
|
583
|
+
# Get current directory using helper
|
|
584
|
+
directory = self._get_model_by_id_with_tenant_check(directory_id, Directory, tenant_id)
|
|
651
585
|
|
|
652
|
-
|
|
653
|
-
result = self.dynamodb.get(
|
|
654
|
-
table_name=self.table_name,
|
|
655
|
-
key={"pk": pk, "sk": sk}
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
if result and 'Item' in result:
|
|
659
|
-
directory = Directory()
|
|
660
|
-
directory.map(result['Item'])
|
|
661
|
-
|
|
586
|
+
if directory:
|
|
662
587
|
directory.subdirectory_count = max(0, directory.subdirectory_count + delta)
|
|
663
588
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
item["sk"] = sk
|
|
667
|
-
|
|
668
|
-
self.dynamodb.save(
|
|
669
|
-
item=item,
|
|
670
|
-
table_name=self.table_name
|
|
671
|
-
)
|
|
589
|
+
directory.prep_for_save()
|
|
590
|
+
self._save_model(directory)
|
|
672
591
|
except Exception:
|
|
673
592
|
# Silent fail - this is a best-effort update
|
|
674
593
|
pass
|