Skip to content

Commit 9701102

Browse files
committed
create one-time script to add expiration dates to events
1 parent 4fb48c6 commit 9701102

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

onetime/events-expiration.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import json
2+
import boto3
3+
import time
4+
import logging
5+
from datetime import datetime, timezone
6+
from decimal import Decimal
7+
from botocore.exceptions import ClientError
8+
9+
# --- Configuration ---
10+
TABLE_NAME = "infra-core-api-events"
11+
EVENTS_EXPIRY_AFTER_LAST_OCCURRENCE_DAYS = 365 * 4
12+
13+
# --- Logging Setup ---
14+
logging.basicConfig(
15+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
16+
)
17+
18+
19+
def parse_date_string(date_str: str) -> datetime | None:
20+
"""
21+
Parses an ISO 8601 date string into a timezone-aware datetime object.
22+
Returns None if the string is invalid or empty.
23+
"""
24+
if not date_str:
25+
return None
26+
try:
27+
if date_str.endswith("Z"):
28+
return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
29+
dt_obj = datetime.fromisoformat(date_str)
30+
if dt_obj.tzinfo is None:
31+
return dt_obj.replace(tzinfo=timezone.utc)
32+
return dt_obj
33+
except ValueError:
34+
logging.warning(f"Could not parse invalid date string: {date_str}")
35+
return None
36+
37+
38+
def determine_expires_at(event: dict) -> int | None:
39+
"""
40+
Calculates the expiration timestamp based on the provided logic.
41+
The event dict should contain keys like 'repeats', 'repeatEnds', and 'end'.
42+
"""
43+
if event.get("repeats") and not event.get("repeatEnds"):
44+
return None
45+
46+
now_ts = int(time.time())
47+
expiry_offset_seconds = 86400 * EVENTS_EXPIRY_AFTER_LAST_OCCURRENCE_DAYS
48+
now_expiry = now_ts + expiry_offset_seconds
49+
50+
end_attr_val = event.get("repeatEnds") if event.get("repeats") else event.get("end")
51+
52+
if not end_attr_val:
53+
return now_expiry
54+
55+
ends_dt = parse_date_string(end_attr_val)
56+
if not ends_dt:
57+
return now_expiry
58+
end_date_expiry = round(ends_dt.timestamp()) + expiry_offset_seconds
59+
60+
return end_date_expiry
61+
62+
63+
def process_table():
64+
"""
65+
Scans the table and updates each item.
66+
"""
67+
try:
68+
dynamodb = boto3.resource("dynamodb")
69+
table = dynamodb.Table(TABLE_NAME)
70+
71+
# A paginator is used to handle scanning tables of any size
72+
paginator = dynamodb.meta.client.get_paginator("scan")
73+
page_iterator = paginator.paginate(TableName=TABLE_NAME)
74+
75+
item_count = 0
76+
updated_count = 0
77+
logging.info(f"Starting to process table: {TABLE_NAME}")
78+
79+
for page in page_iterator:
80+
for item in page.get("Items", []):
81+
item_count += 1
82+
pk_id = item.get("id", {})
83+
if not pk_id:
84+
logging.warning(f"Skipping item with missing 'id': {item}")
85+
continue
86+
87+
# Prepare a simple dict for the logic function
88+
event_data = {
89+
"repeats": item.get("repeats", {}),
90+
"repeatEnds": item.get("repeatEnds", {}),
91+
"end": item.get("end", {}),
92+
}
93+
94+
expires_at_ts = determine_expires_at(event_data)
95+
96+
# Prepare the update expression and values
97+
new_updated_at = (
98+
datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
99+
)
100+
update_expression = "SET updatedAt = :ua"
101+
expression_attribute_values = {":ua": new_updated_at}
102+
103+
if expires_at_ts is not None:
104+
update_expression += ", expiresAt = :ea"
105+
# DynamoDB requires numbers to be passed as Decimal objects
106+
expression_attribute_values[":ea"] = Decimal(expires_at_ts)
107+
108+
# Update the item in DynamoDB
109+
try:
110+
table.update_item(
111+
Key={"id": pk_id},
112+
UpdateExpression=update_expression,
113+
ExpressionAttributeValues=expression_attribute_values,
114+
)
115+
updated_count += 1
116+
if updated_count % 100 == 0:
117+
logging.info(
118+
f"Processed {item_count} items, updated {updated_count} so far..."
119+
)
120+
except ClientError as e:
121+
logging.error(f"Failed to update item {pk_id}: {e}")
122+
123+
logging.info("--- Script Finished ---")
124+
logging.info(f"Total items scanned: {item_count}")
125+
logging.info(f"Total items updated: {updated_count}")
126+
127+
except ClientError as e:
128+
logging.critical(f"A critical AWS error occurred: {e}")
129+
except Exception as e:
130+
logging.critical(f"An unexpected error occurred: {e}")
131+
132+
133+
if __name__ == "__main__":
134+
process_table()

terraform/modules/dynamo/main.tf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
resource "null_resource" "onetime_events_expiration" {
2+
provisioner "local-exec" {
3+
command = <<-EOT
4+
set -e
5+
python events-expiration.py
6+
EOT
7+
interpreter = ["bash", "-c"]
8+
working_dir = "${path.module}/../../../onetime/"
9+
}
10+
}
11+
112
resource "aws_dynamodb_table" "app_audit_log" {
213
billing_mode = "PAY_PER_REQUEST"
314
name = "${var.ProjectId}-audit-log"

0 commit comments

Comments
 (0)