sqlite3 works for storage now. will retry every 24 hours if not acknowledged
This commit is contained in:
parent
bba899677f
commit
f153a7543b
4 changed files with 228 additions and 20 deletions
|
@ -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'),
|
||||
|
|
|
@ -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]),
|
||||
),
|
||||
)
|
|
@ -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
78
main.py
|
@ -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")
|
Loading…
Reference in a new issue