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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for getting file metadata.
|
|
3
|
+
|
|
4
|
+
Requires authentication (secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.files.services import FileSystemService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Factory creates handler (defaults to secure auth)
|
|
13
|
+
handler_wrapper = create_handler(
|
|
14
|
+
service_class=FileSystemService,
|
|
15
|
+
require_body=False,
|
|
16
|
+
convert_case=True
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Get file metadata by ID.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
event: Lambda event from API Gateway
|
|
26
|
+
context: Lambda context
|
|
27
|
+
injected_service: Optional FileSystemService for testing
|
|
28
|
+
|
|
29
|
+
Path parameters:
|
|
30
|
+
fileId: File ID
|
|
31
|
+
|
|
32
|
+
Returns 200 with file metadata
|
|
33
|
+
"""
|
|
34
|
+
return handler_wrapper.execute(event, context, get_file, injected_service)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_file(
|
|
38
|
+
event: Dict[str, Any],
|
|
39
|
+
service: FileSystemService,
|
|
40
|
+
user_context: Dict[str, str]
|
|
41
|
+
) -> Any:
|
|
42
|
+
"""
|
|
43
|
+
Business logic for getting file metadata.
|
|
44
|
+
"""
|
|
45
|
+
tenant_id = user_context.get("tenant_id")
|
|
46
|
+
user_id = user_context.get("user_id")
|
|
47
|
+
|
|
48
|
+
# Get file ID from path parameters
|
|
49
|
+
path_params = event.get("pathParameters", {})
|
|
50
|
+
file_id = path_params.get("fileId") or path_params.get("id")
|
|
51
|
+
|
|
52
|
+
if not file_id:
|
|
53
|
+
raise ValueError("fileId path parameter is required")
|
|
54
|
+
|
|
55
|
+
# Get file metadata
|
|
56
|
+
result = service.get_by_id(
|
|
57
|
+
resource_id=file_id,
|
|
58
|
+
tenant_id=tenant_id,
|
|
59
|
+
user_id=user_id
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return result
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for listing files.
|
|
3
|
+
|
|
4
|
+
Requires authentication (secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.files.services import FileSystemService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Factory creates handler (defaults to secure auth)
|
|
13
|
+
handler_wrapper = create_handler(
|
|
14
|
+
service_class=FileSystemService,
|
|
15
|
+
require_body=False,
|
|
16
|
+
convert_case=True
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
List files.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
event: Lambda event from API Gateway
|
|
26
|
+
context: Lambda context
|
|
27
|
+
injected_service: Optional FileSystemService for testing
|
|
28
|
+
|
|
29
|
+
Query parameters:
|
|
30
|
+
directoryId: Filter by directory (optional)
|
|
31
|
+
ownerId: Filter by owner (optional)
|
|
32
|
+
limit: Max results (optional, default: 100)
|
|
33
|
+
|
|
34
|
+
Returns 200 with list of files
|
|
35
|
+
"""
|
|
36
|
+
return handler_wrapper.execute(event, context, list_files, injected_service)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def list_files(
|
|
40
|
+
event: Dict[str, Any],
|
|
41
|
+
service: FileSystemService,
|
|
42
|
+
user_context: Dict[str, str]
|
|
43
|
+
) -> Any:
|
|
44
|
+
"""
|
|
45
|
+
Business logic for listing files.
|
|
46
|
+
"""
|
|
47
|
+
tenant_id = user_context.get("tenant_id")
|
|
48
|
+
user_id = user_context.get("user_id")
|
|
49
|
+
|
|
50
|
+
# Get query parameters
|
|
51
|
+
query_params = event.get("queryStringParameters", {}) or {}
|
|
52
|
+
directory_id = query_params.get("directoryId")
|
|
53
|
+
owner_id = query_params.get("ownerId") or user_id # Default to current user
|
|
54
|
+
limit = int(query_params.get("limit", "100"))
|
|
55
|
+
|
|
56
|
+
# List files by directory or owner
|
|
57
|
+
if directory_id:
|
|
58
|
+
result = service.list_files_by_directory(
|
|
59
|
+
tenant_id=tenant_id,
|
|
60
|
+
directory_id=directory_id,
|
|
61
|
+
user_id=user_id,
|
|
62
|
+
limit=limit
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
result = service.list_files_by_owner(
|
|
66
|
+
tenant_id=tenant_id,
|
|
67
|
+
owner_id=owner_id,
|
|
68
|
+
user_id=user_id,
|
|
69
|
+
limit=limit
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return result
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for creating a derived file from a main file.
|
|
3
|
+
|
|
4
|
+
Requires authentication (secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.files.services import FileLineageService
|
|
10
|
+
import base64
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Factory creates handler (defaults to secure auth)
|
|
14
|
+
handler_wrapper = create_handler(
|
|
15
|
+
service_class=FileLineageService,
|
|
16
|
+
require_body=True,
|
|
17
|
+
convert_case=True
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Create a derived file from a main file (e.g., data cleaning).
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
event: Lambda event from API Gateway
|
|
27
|
+
context: Lambda context
|
|
28
|
+
injected_service: Optional FileLineageService for testing
|
|
29
|
+
|
|
30
|
+
Expected body (camelCase from frontend):
|
|
31
|
+
{
|
|
32
|
+
"mainFileId": "file-456", # Main file ID
|
|
33
|
+
"fileName": "data_clean_v1.csv",
|
|
34
|
+
"fileData": "base64_encoded_content",
|
|
35
|
+
"transformationOperation": "data_cleaning_v1",
|
|
36
|
+
"transformationMetadata": {
|
|
37
|
+
"cleaning_version": 1,
|
|
38
|
+
"operations": ["remove_nulls", "normalize_units"],
|
|
39
|
+
"rows_processed": 1000
|
|
40
|
+
},
|
|
41
|
+
"directoryId": "dir-789" # Optional
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Returns 201 with created derived file
|
|
45
|
+
"""
|
|
46
|
+
return handler_wrapper.execute(event, context, create_derived_file, injected_service)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_derived_file(
|
|
50
|
+
event: Dict[str, Any],
|
|
51
|
+
service: FileLineageService,
|
|
52
|
+
user_context: Dict[str, str]
|
|
53
|
+
) -> Any:
|
|
54
|
+
"""
|
|
55
|
+
Business logic for creating derived file.
|
|
56
|
+
"""
|
|
57
|
+
payload = event["parsed_body"]
|
|
58
|
+
|
|
59
|
+
tenant_id = user_context.get("tenant_id")
|
|
60
|
+
user_id = user_context.get("user_id")
|
|
61
|
+
|
|
62
|
+
# Extract required fields
|
|
63
|
+
main_file_id = payload.get("main_file_id")
|
|
64
|
+
file_name = payload.get("file_name")
|
|
65
|
+
file_data_b64 = payload.get("file_data")
|
|
66
|
+
transformation_operation = payload.get("transformation_operation")
|
|
67
|
+
|
|
68
|
+
if not main_file_id:
|
|
69
|
+
raise ValueError("main_file_id is required")
|
|
70
|
+
if not file_name:
|
|
71
|
+
raise ValueError("file_name is required")
|
|
72
|
+
if not file_data_b64:
|
|
73
|
+
raise ValueError("file_data is required")
|
|
74
|
+
if not transformation_operation:
|
|
75
|
+
raise ValueError("transformation_operation is required")
|
|
76
|
+
|
|
77
|
+
# Decode base64 file data
|
|
78
|
+
try:
|
|
79
|
+
file_data = base64.b64decode(file_data_b64)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
raise ValueError(f"Invalid base64 file_data: {str(e)}")
|
|
82
|
+
|
|
83
|
+
# Extract optional fields
|
|
84
|
+
transformation_metadata = payload.get("transformation_metadata", {})
|
|
85
|
+
directory_id = payload.get("directory_id")
|
|
86
|
+
|
|
87
|
+
# Create derived file
|
|
88
|
+
result = service.create_derived_file(
|
|
89
|
+
tenant_id=tenant_id,
|
|
90
|
+
user_id=user_id,
|
|
91
|
+
main_file_id=main_file_id,
|
|
92
|
+
file_name=file_name,
|
|
93
|
+
file_data=file_data,
|
|
94
|
+
transformation_operation=transformation_operation,
|
|
95
|
+
transformation_metadata=transformation_metadata,
|
|
96
|
+
directory_id=directory_id
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return result
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for creating a main file from an original file.
|
|
3
|
+
|
|
4
|
+
Requires authentication (secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.files.services import FileLineageService
|
|
10
|
+
import base64
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Factory creates handler (defaults to secure auth)
|
|
14
|
+
handler_wrapper = create_handler(
|
|
15
|
+
service_class=FileLineageService,
|
|
16
|
+
require_body=True,
|
|
17
|
+
convert_case=True
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Create a main file from an original file (e.g., XLS → CSV conversion).
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
event: Lambda event from API Gateway
|
|
27
|
+
context: Lambda context
|
|
28
|
+
injected_service: Optional FileLineageService for testing
|
|
29
|
+
|
|
30
|
+
Expected body (camelCase from frontend):
|
|
31
|
+
{
|
|
32
|
+
"originalFileId": "file-123", # Original file ID
|
|
33
|
+
"fileName": "data.csv",
|
|
34
|
+
"fileData": "base64_encoded_content",
|
|
35
|
+
"mimeType": "text/csv",
|
|
36
|
+
"transformationOperation": "xls_to_csv",
|
|
37
|
+
"transformationMetadata": {
|
|
38
|
+
"source_format": "xls",
|
|
39
|
+
"target_format": "csv",
|
|
40
|
+
"converter_version": "1.0"
|
|
41
|
+
},
|
|
42
|
+
"directoryId": "dir-456" # Optional
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Returns 201 with created main file
|
|
46
|
+
"""
|
|
47
|
+
return handler_wrapper.execute(event, context, create_main_file, injected_service)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_main_file(
|
|
51
|
+
event: Dict[str, Any],
|
|
52
|
+
service: FileLineageService,
|
|
53
|
+
user_context: Dict[str, str]
|
|
54
|
+
) -> Any:
|
|
55
|
+
"""
|
|
56
|
+
Business logic for creating main file.
|
|
57
|
+
"""
|
|
58
|
+
payload = event["parsed_body"]
|
|
59
|
+
|
|
60
|
+
tenant_id = user_context.get("tenant_id")
|
|
61
|
+
user_id = user_context.get("user_id")
|
|
62
|
+
|
|
63
|
+
# Extract required fields
|
|
64
|
+
original_file_id = payload.get("original_file_id")
|
|
65
|
+
file_name = payload.get("file_name")
|
|
66
|
+
file_data_b64 = payload.get("file_data")
|
|
67
|
+
mime_type = payload.get("mime_type")
|
|
68
|
+
transformation_operation = payload.get("transformation_operation")
|
|
69
|
+
|
|
70
|
+
if not original_file_id:
|
|
71
|
+
raise ValueError("original_file_id is required")
|
|
72
|
+
if not file_name:
|
|
73
|
+
raise ValueError("file_name is required")
|
|
74
|
+
if not file_data_b64:
|
|
75
|
+
raise ValueError("file_data is required")
|
|
76
|
+
if not mime_type:
|
|
77
|
+
raise ValueError("mime_type is required")
|
|
78
|
+
if not transformation_operation:
|
|
79
|
+
raise ValueError("transformation_operation is required")
|
|
80
|
+
|
|
81
|
+
# Decode base64 file data
|
|
82
|
+
try:
|
|
83
|
+
file_data = base64.b64decode(file_data_b64)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise ValueError(f"Invalid base64 file_data: {str(e)}")
|
|
86
|
+
|
|
87
|
+
# Extract optional fields
|
|
88
|
+
transformation_metadata = payload.get("transformation_metadata", {})
|
|
89
|
+
directory_id = payload.get("directory_id")
|
|
90
|
+
|
|
91
|
+
# Create main file
|
|
92
|
+
result = service.create_main_file(
|
|
93
|
+
tenant_id=tenant_id,
|
|
94
|
+
user_id=user_id,
|
|
95
|
+
original_file_id=original_file_id,
|
|
96
|
+
file_name=file_name,
|
|
97
|
+
file_data=file_data,
|
|
98
|
+
mime_type=mime_type,
|
|
99
|
+
transformation_operation=transformation_operation,
|
|
100
|
+
transformation_metadata=transformation_metadata,
|
|
101
|
+
directory_id=directory_id
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return result
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for downloading complete lineage bundle with file content.
|
|
3
|
+
|
|
4
|
+
Requires authentication (secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.files.services import FileLineageService
|
|
10
|
+
import base64
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Factory creates handler (defaults to secure auth)
|
|
14
|
+
handler_wrapper = create_handler(
|
|
15
|
+
service_class=FileLineageService,
|
|
16
|
+
require_body=False,
|
|
17
|
+
convert_case=True
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Download complete lineage bundle with file content.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
event: Lambda event from API Gateway
|
|
27
|
+
context: Lambda context
|
|
28
|
+
injected_service: Optional FileLineageService for testing
|
|
29
|
+
|
|
30
|
+
Path parameters:
|
|
31
|
+
fileId: File ID to bundle
|
|
32
|
+
|
|
33
|
+
Returns 200 with bundle including file data (base64 encoded):
|
|
34
|
+
{
|
|
35
|
+
"selected": {
|
|
36
|
+
"file": {file object},
|
|
37
|
+
"data": "base64_encoded_content"
|
|
38
|
+
},
|
|
39
|
+
"main": {
|
|
40
|
+
"file": {file object},
|
|
41
|
+
"data": "base64_encoded_content"
|
|
42
|
+
},
|
|
43
|
+
"original": {
|
|
44
|
+
"file": {file object},
|
|
45
|
+
"data": "base64_encoded_content"
|
|
46
|
+
},
|
|
47
|
+
"metadata": {transformation chain}
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
return handler_wrapper.execute(event, context, download_bundle, injected_service)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def download_bundle(
|
|
54
|
+
event: Dict[str, Any],
|
|
55
|
+
service: FileLineageService,
|
|
56
|
+
user_context: Dict[str, str]
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Business logic for downloading lineage bundle.
|
|
60
|
+
"""
|
|
61
|
+
tenant_id = user_context.get("tenant_id")
|
|
62
|
+
user_id = user_context.get("user_id")
|
|
63
|
+
|
|
64
|
+
# Get file ID from path parameters
|
|
65
|
+
path_params = event.get("pathParameters", {})
|
|
66
|
+
file_id = path_params.get("fileId") or path_params.get("id")
|
|
67
|
+
|
|
68
|
+
if not file_id:
|
|
69
|
+
raise ValueError("fileId path parameter is required")
|
|
70
|
+
|
|
71
|
+
# Download bundle
|
|
72
|
+
result = service.download_lineage_bundle(
|
|
73
|
+
selected_file_id=file_id,
|
|
74
|
+
tenant_id=tenant_id,
|
|
75
|
+
user_id=user_id
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if result.success:
|
|
79
|
+
bundle = result.data
|
|
80
|
+
|
|
81
|
+
# Encode file data as base64 for JSON response
|
|
82
|
+
if bundle.get('selected') and bundle['selected'].get('data'):
|
|
83
|
+
bundle['selected']['data'] = base64.b64encode(
|
|
84
|
+
bundle['selected']['data']
|
|
85
|
+
).decode('utf-8')
|
|
86
|
+
|
|
87
|
+
if bundle.get('main') and bundle['main'].get('data'):
|
|
88
|
+
bundle['main']['data'] = base64.b64encode(
|
|
89
|
+
bundle['main']['data']
|
|
90
|
+
).decode('utf-8')
|
|
91
|
+
|
|
92
|
+
if bundle.get('original') and bundle['original'].get('data'):
|
|
93
|
+
bundle['original']['data'] = base64.b64encode(
|
|
94
|
+
bundle['original']['data']
|
|
95
|
+
).decode('utf-8')
|
|
96
|
+
|
|
97
|
+
return bundle
|
|
98
|
+
|
|
99
|
+
return result
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for getting file lineage.
|
|
3
|
+
|
|
4
|
+
Requires authentication (secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.files.services import FileLineageService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Factory creates handler (defaults to secure auth)
|
|
13
|
+
handler_wrapper = create_handler(
|
|
14
|
+
service_class=FileLineageService,
|
|
15
|
+
require_body=False,
|
|
16
|
+
convert_case=True
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Get complete lineage for a file.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
event: Lambda event from API Gateway
|
|
26
|
+
context: Lambda context
|
|
27
|
+
injected_service: Optional FileLineageService for testing
|
|
28
|
+
|
|
29
|
+
Path parameters:
|
|
30
|
+
fileId: File ID
|
|
31
|
+
|
|
32
|
+
Returns 200 with lineage information:
|
|
33
|
+
{
|
|
34
|
+
"selected": {file object},
|
|
35
|
+
"main": {file object or null},
|
|
36
|
+
"original": {file object or null},
|
|
37
|
+
"allDerived": [{file objects}] # If viewing main file
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
return handler_wrapper.execute(event, context, get_file_lineage, injected_service)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_file_lineage(
|
|
44
|
+
event: Dict[str, Any],
|
|
45
|
+
service: FileLineageService,
|
|
46
|
+
user_context: Dict[str, str]
|
|
47
|
+
) -> Any:
|
|
48
|
+
"""
|
|
49
|
+
Business logic for getting file lineage.
|
|
50
|
+
"""
|
|
51
|
+
tenant_id = user_context.get("tenant_id")
|
|
52
|
+
user_id = user_context.get("user_id")
|
|
53
|
+
|
|
54
|
+
# Get file ID from path parameters
|
|
55
|
+
path_params = event.get("pathParameters", {})
|
|
56
|
+
file_id = path_params.get("fileId") or path_params.get("id")
|
|
57
|
+
|
|
58
|
+
if not file_id:
|
|
59
|
+
raise ValueError("fileId path parameter is required")
|
|
60
|
+
|
|
61
|
+
# Get lineage
|
|
62
|
+
result = service.get_lineage(
|
|
63
|
+
file_id=file_id,
|
|
64
|
+
tenant_id=tenant_id,
|
|
65
|
+
user_id=user_id
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return result
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for preparing lineage bundle.
|
|
3
|
+
|
|
4
|
+
Requires authentication (secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.files.services import FileLineageService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Factory creates handler (defaults to secure auth)
|
|
13
|
+
handler_wrapper = create_handler(
|
|
14
|
+
service_class=FileLineageService,
|
|
15
|
+
require_body=False,
|
|
16
|
+
convert_case=True
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Prepare lineage bundle for a file (metadata only, no file content).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
event: Lambda event from API Gateway
|
|
26
|
+
context: Lambda context
|
|
27
|
+
injected_service: Optional FileLineageService for testing
|
|
28
|
+
|
|
29
|
+
Path parameters:
|
|
30
|
+
fileId: File ID to bundle
|
|
31
|
+
|
|
32
|
+
Returns 200 with bundle information:
|
|
33
|
+
{
|
|
34
|
+
"selectedFile": {file object},
|
|
35
|
+
"mainFile": {file object or null},
|
|
36
|
+
"originalFile": {file object or null},
|
|
37
|
+
"metadata": {
|
|
38
|
+
"selectedFileId": "...",
|
|
39
|
+
"selectedFileName": "...",
|
|
40
|
+
"transformationChain": [
|
|
41
|
+
{"step": 1, "type": "original", "fileId": "...", "fileName": "..."},
|
|
42
|
+
{"step": 2, "type": "convert", "fileId": "...", "fileName": "...", "operation": "..."},
|
|
43
|
+
{"step": 3, "type": "clean", "fileId": "...", "fileName": "...", "operation": "..."}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
"""
|
|
48
|
+
return handler_wrapper.execute(event, context, prepare_bundle, injected_service)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def prepare_bundle(
|
|
52
|
+
event: Dict[str, Any],
|
|
53
|
+
service: FileLineageService,
|
|
54
|
+
user_context: Dict[str, str]
|
|
55
|
+
) -> Any:
|
|
56
|
+
"""
|
|
57
|
+
Business logic for preparing lineage bundle.
|
|
58
|
+
"""
|
|
59
|
+
tenant_id = user_context.get("tenant_id")
|
|
60
|
+
user_id = user_context.get("user_id")
|
|
61
|
+
|
|
62
|
+
# Get file ID from path parameters
|
|
63
|
+
path_params = event.get("pathParameters", {})
|
|
64
|
+
file_id = path_params.get("fileId") or path_params.get("id")
|
|
65
|
+
|
|
66
|
+
if not file_id:
|
|
67
|
+
raise ValueError("fileId path parameter is required")
|
|
68
|
+
|
|
69
|
+
# Prepare bundle
|
|
70
|
+
result = service.prepare_lineage_bundle(
|
|
71
|
+
selected_file_id=file_id,
|
|
72
|
+
tenant_id=tenant_id,
|
|
73
|
+
user_id=user_id
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return result
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""File models.
|
|
2
|
+
|
|
3
|
+
Geek Cafe, LLC
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .file import File
|
|
8
|
+
from .directory import Directory
|
|
9
|
+
from .file_version import FileVersion
|
|
10
|
+
from .file_share import FileShare
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"File",
|
|
14
|
+
"Directory",
|
|
15
|
+
"FileVersion",
|
|
16
|
+
"FileShare",
|
|
17
|
+
]
|
|
@@ -21,12 +21,12 @@ class Directory(BaseModel):
|
|
|
21
21
|
Moving a file updates its directory_id, not its S3 location.
|
|
22
22
|
|
|
23
23
|
Access Patterns (DynamoDB Keys):
|
|
24
|
-
- pk:
|
|
25
|
-
- sk:
|
|
26
|
-
- gsi1_pk:
|
|
27
|
-
- gsi1_sk:
|
|
28
|
-
- gsi2_pk:
|
|
29
|
-
- gsi2_sk:
|
|
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
30
|
"""
|
|
31
31
|
|
|
32
32
|
def __init__(self):
|
|
@@ -61,6 +61,42 @@ class Directory(BaseModel):
|
|
|
61
61
|
|
|
62
62
|
# Timestamps (inherited from BaseModel)
|
|
63
63
|
# created_utc_ts, updated_utc_ts
|
|
64
|
+
|
|
65
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
66
|
+
self._setup_indexes()
|
|
67
|
+
|
|
68
|
+
def _setup_indexes(self):
|
|
69
|
+
"""Setup DynamoDB indexes for directory queries."""
|
|
70
|
+
|
|
71
|
+
# Primary index: Directory by ID
|
|
72
|
+
primary = DynamoDBIndex()
|
|
73
|
+
primary.name = "primary"
|
|
74
|
+
primary.partition_key.attribute_name = "pk"
|
|
75
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("directory", self.id))
|
|
76
|
+
primary.sort_key.attribute_name = "sk"
|
|
77
|
+
primary.sort_key.value = lambda: "metadata"
|
|
78
|
+
self.indexes.add_primary(primary)
|
|
79
|
+
|
|
80
|
+
# GSI1: Directories by path
|
|
81
|
+
gsi = DynamoDBIndex()
|
|
82
|
+
gsi.name = "gsi1"
|
|
83
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
84
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
85
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
86
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("path", self.full_path))
|
|
87
|
+
self.indexes.add_secondary(gsi)
|
|
88
|
+
|
|
89
|
+
# GSI2: Subdirectories by parent
|
|
90
|
+
gsi = DynamoDBIndex()
|
|
91
|
+
gsi.name = "gsi2"
|
|
92
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
93
|
+
if self.parent_id:
|
|
94
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("directory", self.tenant_id), ("parent", self.parent_id))
|
|
95
|
+
else:
|
|
96
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("directory", self.tenant_id), ("root", "root"))
|
|
97
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
98
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("name", self.directory_name))
|
|
99
|
+
self.indexes.add_secondary(gsi)
|
|
64
100
|
|
|
65
101
|
# Properties - Identity
|
|
66
102
|
@property
|