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