"""DynamoDB client for storing and retrieving offboarding snapshots.""" import logging from datetime import datetime, timezone from typing import TYPE_CHECKING import boto3 from shared.schemas.dynamo import Snapshot if TYPE_CHECKING: from mypy_boto3_dynamodb import DynamoDBServiceResource from mypy_boto3_dynamodb.service_resource import Table LOGGER = logging.getLogger(__name__) class DynamoClient: """Stores and retrieves Phase 1 offboarding snapshots in DynamoDB. The table uses ``ticket_id`` as the partition key. Each item has a ``ttl`` attribute (Unix epoch) that DynamoDB uses to automatically expire stale records after the configured number of days. """ def __init__(self, table_name: str, region: str = 'us-east-1') -> None: """Initialise with the target table name and AWS region. :param table_name: DynamoDB table name. :param region: AWS region. """ resource: 'DynamoDBServiceResource' = boto3.resource( 'dynamodb', region_name=region ) self._table: 'Table' = resource.Table(table_name) def put_snapshot(self, snapshot: Snapshot, ttl_days: int = 7) -> None: """Write a snapshot item to DynamoDB. Overwrites any existing item with the same ``ticket_id``. :param snapshot: The snapshot to store. :param ttl_days: Number of days until DynamoDB auto-expires the item. """ now = datetime.now(tz=timezone.utc) ttl = int(now.timestamp()) + ttl_days * 86400 item = snapshot.model_dump() item['ttl'] = ttl self._table.put_item(Item=item) LOGGER.info('Stored snapshot for ticket %s (ttl=%d).', snapshot.ticket_id, ttl) def get_snapshot(self, ticket_id: str) -> 'Snapshot | None': """Retrieve a snapshot by ticket ID. :param ticket_id: Jira ticket key (e.g. ``SYS-123``). :return: Snapshot if found, None otherwise. """ response = self._table.get_item(Key={'ticket_id': ticket_id}) item = response.get('Item') if item is None: LOGGER.info('No snapshot found for ticket %s.', ticket_id) return None # Remove DynamoDB-only field before validating item.pop('ttl', None) LOGGER.info('Retrieved snapshot for ticket %s.', ticket_id) return Snapshot.model_validate(item) def delete_snapshot(self, ticket_id: str) -> None: """Delete a snapshot by ticket ID. Idempotent — deleting a non-existent item is a no-op. :param ticket_id: Jira ticket key to delete. """ self._table.delete_item(Key={'ticket_id': ticket_id}) LOGGER.info('Deleted snapshot for ticket %s.', ticket_id)