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 ( CREATE TABLE notification (
id INTEGER PRIMARY KEY NOT NULL CHECK (typeof(id) = 'integer'), id INTEGER PRIMARY KEY NOT NULL CHECK (typeof(id) = 'integer'),
id_transaction INTEGER NOT NULL CHECK (typeof(id_transaction) = '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'), pushover_receipt TEXT NOT NULL CHECK (typeof(pushover_receipt) = 'text'),
acknowledged BOOLEAN NOT NULL CHECK (typeof(acknowledged) = 'integer'), acknowledged BOOLEAN NOT NULL CHECK (typeof(acknowledged) = 'integer'),
expired BOOLEAN NOT NULL CHECK (typeof(expired) = 'integer'), expired BOOLEAN NOT NULL CHECK (typeof(expired) = 'integer'),

View file

@ -2,14 +2,16 @@ from dataclasses import dataclass
from datetime import datetime from datetime import datetime
import logging import logging
import sqlite3 import sqlite3
from typing import Iterable, Tuple
from db import conn from db.transaction import Transaction
@dataclass(frozen=True) @dataclass(frozen=True)
class Notification: class Notification:
id: int id: int
id_transaction: int id_transaction: int
user: str
pushover_receipt: str pushover_receipt: str
acknowledged: bool acknowledged: bool
expired: bool expired: bool
@ -28,6 +30,7 @@ class NotificationManager:
""" """
INSERT INTO notification ( INSERT INTO notification (
id_transaction, id_transaction,
user,
pushover_receipt, pushover_receipt,
acknowledged, acknowledged,
expired, expired,
@ -39,11 +42,13 @@ class NotificationManager:
?, ?,
?, ?,
?, ?,
?,
? ?
) RETURNING id; ) RETURNING id;
""", """,
( (
notification.id_transaction, notification.id_transaction,
notification.user,
notification.pushover_receipt, notification.pushover_receipt,
notification.acknowledged, notification.acknowledged,
notification.expired, notification.expired,
@ -57,9 +62,130 @@ class NotificationManager:
return Notification( return Notification(
id=result[0], id=result[0],
id_transaction=notification.id_transaction, id_transaction=notification.id_transaction,
user=notification.user,
pushover_receipt=notification.pushover_receipt, pushover_receipt=notification.pushover_receipt,
acknowledged=notification.acknowledged, acknowledged=notification.acknowledged,
expired=notification.expired, expired=notification.expired,
ts_update=now, ts_update=now,
ts_insert=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 from datetime import datetime
import logging import logging
import sqlite3 import sqlite3
from typing import Iterable, Optional from typing import Optional
from dataclasses import dataclass from dataclasses import dataclass
@ -56,10 +56,45 @@ class TransactionManager:
date=result[5], date=result[5],
time=result[6], time=result[6],
acknowledged=result[7], acknowledged=result[7],
ts_update=result[8], ts_update=datetime.fromtimestamp(result[8]),
ts_insert=result[9], 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: def insert_transaction(self, transaction: Transaction) -> Transaction:
now = datetime.now() now = datetime.now()
result = self.__db_conn.execute( 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.notification import Notification, NotificationManager
from db.transaction import Transaction, TransactionManager from db.transaction import Transaction, TransactionManager
__MAX_PUSHOVER_EXPIRATION = 10800 # 3 hours, max allowed by Pushover's API
__logger = None __logger = None
@ -131,7 +134,7 @@ def grouper(data: Iterable, group_size: int) -> Iterable[List]:
yield group 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") conn = http.client.HTTPSConnection("api.pushover.net:443")
try: try:
conn.request( conn.request(
@ -142,8 +145,8 @@ def send_pushover(token: str, user: str, transaction: Transaction) -> dict[str,
"token": token, "token": token,
"user": user, "user": user,
"priority": "2", "priority": "2",
"retry": "3600", # 3 hours, max allowed by Pushover's API "retry": "3600",
"expire": "10800", # 3 hours, max allowed by Pushover's API "expire": f"{__MAX_PUSHOVER_EXPIRATION}",
"message": f'{transaction.amount} at "{transaction.merchant}" with card {transaction.card_ending_in} on {transaction.date} at {transaction.time}', "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: if response.getcode() != 200:
raise Exception(f"failed to send notifcation via Pushover: {str(body, 'utf-8')}") 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: finally:
conn.close() conn.close()
@ -252,17 +290,25 @@ if __name__ == "__main__":
log.debug(f"recording message id {transaction.email_message_id} to message id list") log.debug(f"recording message id {transaction.email_message_id} to message id list")
transaction = transaction_manager.insert_transaction(transaction) transaction = transaction_manager.insert_transaction(transaction)
log.debug(f"sending pushover notification for message id {transaction.email_message_id}") log.debug(f"sending pushover notification for message id {transaction.email_message_id}")
notification_manager.insert_notification( response = send_pushover(notification_manager, pushover_token, pushover_user, transaction)
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)
log.info(f"recorded {count} transactions") 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")