Coverage for src / ai_lls_lib / core / cache.py: 100%
46 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-06 23:45 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-06 23:45 +0000
1"""
2DynamoDB cache implementation for phone verifications
3"""
5from datetime import UTC, datetime, timedelta
6from typing import Any
8import boto3
9from aws_lambda_powertools import Logger
11from .models import LineType, PhoneVerification, VerificationSource
13logger = Logger()
16class DynamoDBCache:
17 """Cache for phone verification results using DynamoDB with TTL"""
19 def __init__(self, table_name: str, ttl_days: int = 90):
20 self.table_name = table_name
21 self.ttl_days = ttl_days
22 self.dynamodb = boto3.resource("dynamodb")
23 self.table = self.dynamodb.Table(table_name)
25 def get(self, phone_number: str) -> PhoneVerification | None:
26 """Get cached verification result"""
27 try:
28 response = self.table.get_item(Key={"phone_number": phone_number})
30 if "Item" not in response:
31 logger.info(f"Cache miss for {phone_number[:6]}***")
32 return None
34 item: dict[str, Any] = response["Item"]
35 logger.info(f"Cache hit for {phone_number[:6]}***")
37 return PhoneVerification(
38 phone_number=str(item["phone_number"]),
39 line_type=LineType(str(item["line_type"])),
40 dnc=bool(item["dnc"]),
41 cached=True,
42 verified_at=datetime.fromisoformat(str(item["verified_at"])),
43 source=VerificationSource.CACHE,
44 )
46 except Exception as e:
47 logger.error(f"Cache get error: {str(e)}")
48 return None
50 def set(self, phone_number: str, verification: PhoneVerification) -> None:
51 """Store verification result in cache"""
52 try:
53 ttl = int((datetime.now(UTC) + timedelta(days=self.ttl_days)).timestamp())
55 self.table.put_item(
56 Item={
57 "phone_number": phone_number,
58 "line_type": verification.line_type.value,
59 "dnc": verification.dnc,
60 "verified_at": verification.verified_at.isoformat(),
61 "source": verification.source.value,
62 "ttl": ttl,
63 }
64 )
66 logger.info(f"Cached result for {phone_number[:6]}***")
68 except Exception as e:
69 logger.error(f"Cache set error: {str(e)}")
70 # Don't fail the request if cache write fails
72 def batch_get(self, phone_numbers: list[str]) -> dict[str, PhoneVerification | None]:
73 """Get multiple cached results"""
74 results: dict[str, PhoneVerification | None] = {}
76 # DynamoDB batch get (max 100 items per request)
77 for i in range(0, len(phone_numbers), 100):
78 batch = phone_numbers[i : i + 100]
80 try:
81 response = self.dynamodb.batch_get_item(
82 RequestItems={
83 self.table_name: {"Keys": [{"phone_number": phone} for phone in batch]}
84 }
85 )
87 for item in response.get("Responses", {}).get(self.table_name, []):
88 phone = str(item["phone_number"])
89 results[phone] = PhoneVerification(
90 phone_number=phone,
91 line_type=LineType(str(item["line_type"])),
92 dnc=bool(item["dnc"]),
93 cached=True,
94 verified_at=datetime.fromisoformat(str(item["verified_at"])),
95 source=VerificationSource.CACHE,
96 )
98 except Exception as e:
99 logger.error(f"Batch cache get error: {str(e)}")
101 # Fill in None for misses
102 for phone in phone_numbers:
103 if phone not in results:
104 results[phone] = None
106 return results