Skip to content

Commit 7d190f1

Browse files
afriedman412claude
andcommitted
Add test mode for manual cloud function runs
- Default to test mode when run without PRODUCTION env var - Test mode sends email with most recent expenditure from DB - Includes recipient list, transaction details, and JSON attachment - Scheduled runs set PRODUCTION=1 to run full flow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e8e6d52 commit 7d190f1

File tree

5 files changed

+98
-20
lines changed

5 files changed

+98
-20
lines changed

.github/workflows/deploy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ jobs:
3232
run: |
3333
gcloud run jobs update freeway2026 \
3434
--image $IMAGE \
35-
--region us-central1
35+
--region us-central1 \
36+
--set-env-vars PRODUCTION=1

app/db.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from sqlmodel import SQLModel, create_engine, text
1+
from sqlmodel import SQLModel, Session, create_engine, text, select
2+
import pandas as pd
23
from app.schemas import Committee, Contribution, Expenditure
34
from app.config import POSTGRES_URL
45

@@ -23,3 +24,22 @@ def reset_tables(database_url: str, echo: bool = False) -> None:
2324
print("CREATING IN DB:", db, "SCHEMA:", schema)
2425
SQLModel.metadata.drop_all(engine)
2526
SQLModel.metadata.create_all(engine)
27+
28+
29+
def get_latest_expenditure(engine) -> pd.DataFrame:
30+
"""Get the most recent expenditure from the database, with committee name."""
31+
with Session(engine) as session:
32+
stmt = select(Expenditure).order_by(
33+
Expenditure.expenditure_date.desc()
34+
).limit(1)
35+
result = session.exec(stmt).first()
36+
if result:
37+
data = result.model_dump()
38+
# Join committee name
39+
if result.committee_id:
40+
committee = session.get(Committee, result.committee_id)
41+
data["committee_name"] = committee.name if committee else None
42+
else:
43+
data["committee_name"] = None
44+
return pd.DataFrame([data])
45+
return pd.DataFrame()

app/helpers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ def df_to_csv_bytes(df) -> bytes:
2828
return buf.getvalue().encode("utf-8")
2929

3030

31+
def df_to_json_bytes(df) -> bytes:
32+
return df.to_json(orient="records", indent=2).encode("utf-8")
33+
34+
3135
def format_results(results: dict) -> str:
3236
return "\n".join(
3337
f"{k:25} {results[k]}"

app/mail.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import smtplib
22
from email.message import EmailMessage
33
import os
4-
from app.helpers import df_to_csv_bytes, normalize_recipients
4+
from app.helpers import df_to_csv_bytes, df_to_json_bytes, normalize_recipients
55

66

77
def send_email(
@@ -11,6 +11,7 @@ def send_email(
1111
to: list[str],
1212
sender: str,
1313
df=None,
14+
attachment_format: str = "csv",
1415
):
1516
msg = EmailMessage()
1617
msg["From"] = sender
@@ -19,14 +20,22 @@ def send_email(
1920
msg.set_content(body)
2021

2122
if df is not None and not df.empty:
22-
csv_bytes = df_to_csv_bytes(df)
23-
24-
msg.add_attachment(
25-
csv_bytes,
26-
maintype="text",
27-
subtype="csv",
28-
filename="deduped_results.csv",
29-
)
23+
if attachment_format == "json":
24+
data_bytes = df_to_json_bytes(df)
25+
msg.add_attachment(
26+
data_bytes,
27+
maintype="application",
28+
subtype="json",
29+
filename="results.json",
30+
)
31+
else:
32+
csv_bytes = df_to_csv_bytes(df)
33+
msg.add_attachment(
34+
csv_bytes,
35+
maintype="text",
36+
subtype="csv",
37+
filename="deduped_results.csv",
38+
)
3039

3140
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
3241
smtp.login(

app/main.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from app.ingestion import ingest_jsonl
33
from app.mail import send_email
44
from app.logger import logger
5-
from app.db import get_engine, create_tables
5+
from app.db import get_engine, create_tables, get_latest_expenditure
66
from app.helpers import get_now, format_results, get_today
77
from app.config import CYCLE, TARGET_EMAILS
88
import os
@@ -68,13 +68,57 @@ def run(variant, key, cycle):
6868
logger.info("No new data! ")
6969

7070

71+
def run_test_mode():
72+
"""Send a test email with the most recent expenditure from the database."""
73+
logger.info("Running in TEST MODE...")
74+
engine = get_engine()
75+
76+
latest_df = get_latest_expenditure(engine)
77+
78+
if latest_df.empty:
79+
logger.info("No expenditures found in database!")
80+
return
81+
82+
ts = get_now()
83+
row = latest_df.iloc[0]
84+
recipients_list = "\n".join(f" - {email}" for email in TARGET_EMAILS)
85+
86+
body = f"""[TEST MODE] Most recent expenditure as of {ts}
87+
88+
Recipients:
89+
{recipients_list}
90+
91+
Latest Record:
92+
--------------
93+
Date: {row['expenditure_date']}
94+
Amount: ${row['expenditure_amount']:,.2f}
95+
Committee: {row['committee_name']} ({row['committee_id']})
96+
Payee: {row['payee_name']}
97+
Description: {row['expenditure_description']}
98+
"""
99+
100+
send_email(
101+
subject=f"[sludgewire] TEST - Last Update Check, {ts}",
102+
body=body,
103+
to=TARGET_EMAILS,
104+
sender="steadynappin@gmail.com",
105+
df=latest_df,
106+
attachment_format="json"
107+
)
108+
logger.info("Test email sent!")
109+
110+
71111
if __name__ == "__main__":
72-
today = get_today()
73112
create_tables()
74-
logger.info("Running...")
75-
run(
76-
variant="expenditure",
77-
key=today,
78-
cycle=CYCLE
79-
)
80-
logger.info("Run complete.")
113+
114+
if os.environ.get("PRODUCTION"):
115+
today = get_today()
116+
logger.info("Running production mode...")
117+
run(
118+
variant="expenditure",
119+
key=today,
120+
cycle=CYCLE
121+
)
122+
logger.info("Run complete.")
123+
else:
124+
run_test_mode()

0 commit comments

Comments
 (0)