sqlite3 works for storage now. will retry every 24 hours if not acknowledged

This commit is contained in:
Tony Blyler 2021-11-08 23:51:32 -05:00
parent bba899677f
commit f153a7543b
4 changed files with 228 additions and 20 deletions

View file

@ -25,6 +25,7 @@ __STEPS = [
CREATE TABLE notification (
id INTEGER PRIMARY KEY NOT NULL CHECK (typeof(id) = 'integer'),
id_transaction INTEGER NOT NULL CHECK (typeof(id_transaction) = 'integer'),
user TEXT NOT NULL CHECK (typeof(user) = 'text'),
pushover_receipt TEXT NOT NULL CHECK (typeof(pushover_receipt) = 'text'),
acknowledged BOOLEAN NOT NULL CHECK (typeof(acknowledged) = 'integer'),
expired BOOLEAN NOT NULL CHECK (typeof(expired) = 'integer'),

View file

@ -2,14 +2,16 @@ from dataclasses import dataclass
from datetime import datetime
import logging
import sqlite3
from typing import Iterable, Tuple
from db import conn
from db.transaction import Transaction
@dataclass(frozen=True)
class Notification:
id: int
id_transaction: int
user: str
pushover_receipt: str
acknowledged: bool
expired: bool
@ -28,6 +30,7 @@ class NotificationManager:
"""
INSERT INTO notification (
id_transaction,
user,
pushover_receipt,
acknowledged,
expired,
@ -39,11 +42,13 @@ class NotificationManager:
?,
?,
?,
?,
?
) RETURNING id;
""",
(
notification.id_transaction,
notification.user,
notification.pushover_receipt,
notification.acknowledged,
notification.expired,
@ -57,9 +62,130 @@ class NotificationManager:
return Notification(
id=result[0],
id_transaction=notification.id_transaction,
user=notification.user,
pushover_receipt=notification.pushover_receipt,
acknowledged=notification.acknowledged,
expired=notification.expired,
ts_update=now,
ts_insert=now,
)
def expire_notification(self, notification_id: int) -> Notification:
row = self.__db_conn.execute(
"""
UPDATE notification
SET expired = TRUE, ts_update = strftime('%s')
WHERE id = ?
RETURNING
id_transaction,
user,
pushover_receipt,
acknowledged,
ts_update,
ts_insert
""",
(notification_id,),
).fetchone()
self.__db_conn.commit()
return Notification(
id=notification_id,
id_transaction=row[0],
user=row[1],
pushover_receipt=row[2],
acknowledged=row[3],
expired=True,
ts_insert=datetime.fromtimestamp(row[4]),
ts_update=datetime.fromtimestamp(row[5]),
)
def acknowledge_notification(self, notification_id: int) -> Notification:
row = self.__db_conn.execute(
"""
UPDATE notification
SET acknowledged = TRUE, ts_update = strftime('%s')
WHERE id = ?
RETURNING
id_transaction,
user,
pushover_receipt,
acknowledged,
ts_update,
ts_insert
""",
(notification_id,),
).fetchone()
self.__db_conn.commit()
return Notification(
id=notification_id,
id_transaction=row[0],
user=row[1],
pushover_receipt=row[2],
acknowledged=row[3],
expired=True,
ts_insert=datetime.fromtimestamp(row[4]),
ts_update=datetime.fromtimestamp(row[5]),
)
def list_unacknowledged_transactions_with_notifications(self) -> Iterable[Tuple[Transaction, Notification]]:
result = self.__db_conn.execute(
"""
SELECT
t.id,
t.email_message_id,
t.amount,
t.card_ending_in,
t.merchant,
t.date,
t.time,
t.acknowledged,
t.ts_update,
t.ts_insert,
n.id,
n.id_transaction,
n.user,
n.pushover_receipt,
n.acknowledged,
n.expired,
n.ts_update,
n.ts_insert
FROM "transaction" AS t
JOIN notification AS n
ON t.id = n.id_transaction
WHERE
t.acknowledged = FALSE AND
n.expired = FALSE AND
n.acknowledged = FALSE
GROUP BY t.id
HAVING n.id = MAX(n.id)
""",
)
for row in result:
yield (
Transaction(
id=row[0],
email_message_id=row[1],
amount=row[2],
card_ending_in=row[3],
merchant=row[4],
date=row[5],
time=row[6],
acknowledged=row[7],
ts_update=datetime.fromtimestamp(row[8]),
ts_insert=datetime.fromtimestamp(row[9]),
),
Notification(
id=row[10],
id_transaction=row[11],
user=row[12],
pushover_receipt=row[13],
acknowledged=row[14],
expired=row[15],
ts_update=datetime.fromtimestamp(row[16]),
ts_insert=datetime.fromtimestamp(row[17]),
),
)

View file

@ -1,7 +1,7 @@
from datetime import datetime
import logging
import sqlite3
from typing import Iterable, Optional
from typing import Optional
from dataclasses import dataclass
@ -56,10 +56,45 @@ class TransactionManager:
date=result[5],
time=result[6],
acknowledged=result[7],
ts_update=result[8],
ts_insert=result[9],
ts_update=datetime.fromtimestamp(result[8]),
ts_insert=datetime.fromtimestamp(result[9]),
)
def acknowledge_transaction(self, transaction_id: int) -> Transaction:
row = self.__db_conn.execute(
"""
UPDATE "transaction"
SET acknowledged = TRUE, ts_update = strftime('%s')
WHERE id = ?
RETURNING
id,
email_message_id,
amount,
card_ending_in,
merchant,
date,
time,
acknowledged,
ts_update,
ts_insert
""",
(transaction_id,),
).fetchone()
return Transaction(
id=row[0],
email_message_id=row[1],
amount=row[2],
card_ending_in=row[3],
merchant=row[4],
date=row[5],
time=row[6],
acknowledged=row[7],
ts_update=datetime.fromtimestamp(row[8]),
ts_insert=datetime.fromtimestamp(row[9]),
)
def insert_transaction(self, transaction: Transaction) -> Transaction:
now = datetime.now()
result = self.__db_conn.execute(

78
main.py
View file

@ -15,6 +15,9 @@ from db import migrate_db, create_conn
from db.notification import Notification, NotificationManager
from db.transaction import Transaction, TransactionManager
__MAX_PUSHOVER_EXPIRATION = 10800 # 3 hours, max allowed by Pushover's API
__logger = None
@ -131,7 +134,7 @@ def grouper(data: Iterable, group_size: int) -> Iterable[List]:
yield group
def send_pushover(token: str, user: str, transaction: Transaction) -> dict[str, Any]:
def send_pushover(notification_manager: NotificationManager, token: str, user: str, transaction: Transaction) -> dict[str, Any]:
conn = http.client.HTTPSConnection("api.pushover.net:443")
try:
conn.request(
@ -142,8 +145,8 @@ def send_pushover(token: str, user: str, transaction: Transaction) -> dict[str,
"token": token,
"user": user,
"priority": "2",
"retry": "3600", # 3 hours, max allowed by Pushover's API
"expire": "10800", # 3 hours, max allowed by Pushover's API
"retry": "3600",
"expire": f"{__MAX_PUSHOVER_EXPIRATION}",
"message": f'{transaction.amount} at "{transaction.merchant}" with card {transaction.card_ending_in} on {transaction.date} at {transaction.time}',
}
),
@ -154,7 +157,42 @@ def send_pushover(token: str, user: str, transaction: Transaction) -> dict[str,
if response.getcode() != 200:
raise Exception(f"failed to send notifcation via Pushover: {str(body, 'utf-8')}")
return json.loads(body)
response_payload = json.loads(body)
notification = notification_manager.insert_notification(Notification(
id=0,
ts_insert=datetime.now(),
ts_update=datetime.now(),
acknowledged=False,
expired=False,
id_transaction=transaction.id,
pushover_receipt=response_payload["receipt"],
user=pushover_user,
))
log.debug(f"recording notification id {notification.id}")
return response_payload
finally:
conn.close()
def get_pushover_receipt_status(receipt: str, token: str) -> dict[str, Any]:
conn = http.client.HTTPSConnection("api.pushover.net:443")
try:
conn.request(
"GET",
f"/1/receipts/{receipt}.json",
urllib.parse.urlencode(
{
"token": token,
}
),
{"Content-type": "application/x-www-form-urlencoded"},
)
response = conn.getresponse()
return json.loads(response.read())
finally:
conn.close()
@ -252,17 +290,25 @@ if __name__ == "__main__":
log.debug(f"recording message id {transaction.email_message_id} to message id list")
transaction = transaction_manager.insert_transaction(transaction)
log.debug(f"sending pushover notification for message id {transaction.email_message_id}")
notification_manager.insert_notification(
Notification(
id=0,
id_transaction=transaction.id,
pushover_receipt="foobar",
acknowledged=False,
expired=False,
ts_insert=0,
ts_update=0,
)
)
# send_pushover(pushover_token, pushover_user, transaction)
response = send_pushover(notification_manager, pushover_token, pushover_user, transaction)
log.info(f"recorded {count} transactions")
log.info("checking transactions without an acknowledgement")
for transaction, notification in notification_manager.list_unacknowledged_transactions_with_notifications():
response = get_pushover_receipt_status(notification.pushover_receipt, pushover_token)
# if status is falsey, it has been >= 1 week
if not response["status"] or datetime.now() >= notification.ts_update + timedelta(days=1):
log.info(f"notification id {notification.id} for transaction id {transaction.id} is expired, retrying")
send_pushover(notification_manager, pushover_token, pushover_user, transaction)
notification_manager.expire_notification(notification.id)
continue
if response["acknowledged"]:
log.info(f"notification id {notification.id} for transaction id {transaction.id} is acknowledged")
notification_manager.acknowledge_notification(notification.id)
transaction_manager.acknowledge_transaction(transaction.id)
continue
log.debug(f"notification id {notification.id} is not expired nor acknowledged, continuing")