|
| 1 | +# Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +import boto3 |
| 5 | +import uuid |
| 6 | +from datetime import datetime |
| 7 | +from botocore.config import Config |
| 8 | +from customLogging.logger import safeLogger |
| 9 | +from common.resourceNames import ResourceKeys, get_table_name |
| 10 | + |
| 11 | +retry_config = Config(retries={'max_attempts': 5, 'mode': 'adaptive'}) |
| 12 | +dynamodb = boto3.resource('dynamodb', config=retry_config) |
| 13 | +logger = safeLogger(service_name="AssetHistory") |
| 14 | + |
| 15 | +# Asset lifecycle history change sources. The *Direct variants mark records |
| 16 | +# originated from S3 bucket-sync ingestion rather than a VAMS API call. |
| 17 | +CHANGE_SOURCE_CREATE = "create" |
| 18 | +CHANGE_SOURCE_CREATE_DIRECT = "createDirect" |
| 19 | +CHANGE_SOURCE_EDIT = "edit" |
| 20 | +CHANGE_SOURCE_ARCHIVE = "archive" |
| 21 | +CHANGE_SOURCE_UNARCHIVE = "unarchive" |
| 22 | +CHANGE_SOURCE_UNARCHIVE_DIRECT = "unarchiveDirect" |
| 23 | +CHANGE_SOURCE_PERMANENT_DELETE = "permanentDelete" |
| 24 | + |
| 25 | +try: |
| 26 | + _asset_history_table_name = get_table_name(ResourceKeys.ASSET_HISTORY_STORAGE_TABLE) |
| 27 | +except Exception: |
| 28 | + _asset_history_table_name = None |
| 29 | + |
| 30 | +asset_history_table = dynamodb.Table(_asset_history_table_name) if _asset_history_table_name else None |
| 31 | + |
| 32 | + |
| 33 | +def build_asset_snapshot(asset_record, archived_reason=None, unarchived_reason=None): |
| 34 | + """Build the open-schema assetSnapshot map from an asset record's fields |
| 35 | + as they stand after the operation being recorded.""" |
| 36 | + snapshot = { |
| 37 | + 'assetName': asset_record.get('assetName', ''), |
| 38 | + 'description': asset_record.get('description', ''), |
| 39 | + 'isDistributable': asset_record.get('isDistributable', False), |
| 40 | + 'tags': asset_record.get('tags', []), |
| 41 | + 'bucketId': asset_record.get('bucketId', ''), |
| 42 | + } |
| 43 | + asset_location = asset_record.get('assetLocation') or {} |
| 44 | + if asset_location.get('Key'): |
| 45 | + snapshot['assetLocationKey'] = asset_location['Key'] |
| 46 | + if archived_reason: |
| 47 | + snapshot['archivedReason'] = archived_reason |
| 48 | + if unarchived_reason: |
| 49 | + snapshot['unarchivedReason'] = unarchived_reason |
| 50 | + return snapshot |
| 51 | + |
| 52 | + |
| 53 | +def write_asset_history_record(database_id, asset_id, change_source, change_user_id, asset_snapshot): |
| 54 | + """Write one asset lifecycle history record. Best-effort: failures are |
| 55 | + logged and never raised into the calling operation. Records persist across |
| 56 | + asset permanent deletes.""" |
| 57 | + try: |
| 58 | + if not asset_history_table: |
| 59 | + logger.warning("Asset history table not configured; skipping history record") |
| 60 | + return |
| 61 | + record_date = datetime.utcnow().isoformat() + "Z" |
| 62 | + item = { |
| 63 | + 'databaseId:assetId': f"{database_id}:{asset_id}", |
| 64 | + 'historyRecordId': f"{record_date}#{uuid.uuid4().hex[:8]}", |
| 65 | + 'databaseId': database_id, |
| 66 | + 'assetId': asset_id, |
| 67 | + 'recordDate': record_date, |
| 68 | + 'changeSource': change_source, |
| 69 | + 'changeUserId': change_user_id or 'SYSTEM_USER', |
| 70 | + 'assetSnapshot': asset_snapshot or {}, |
| 71 | + } |
| 72 | + asset_history_table.put_item(Item=item) |
| 73 | + except Exception as e: |
| 74 | + logger.warning(f"Failed writing asset history record for {asset_id}: {e}") |
0 commit comments