From 8e8a85e822ce3e94dfd5d7f0aeb12739e7a8b9e0 Mon Sep 17 00:00:00 2001 From: Johnny Gear Date: Thu, 13 Nov 2025 22:03:20 -0600 Subject: [PATCH] Scheduling --- Pipfile | 2 ++ Pipfile.lock | 27 +++++++++++++++- db.py | 2 +- generate.py | 6 ++-- main.py | 90 +++++---------------------------------------------- mastodon.py | 3 +- orders.py | 77 +++++++++++++++++++++++++++++++++++++++++++ scheduling.py | 45 ++++++++++++++++++++++++++ settings.py | 17 ++++++++++ util.py | 4 +++ 10 files changed, 185 insertions(+), 88 deletions(-) create mode 100644 orders.py create mode 100644 scheduling.py create mode 100644 settings.py create mode 100644 util.py diff --git a/Pipfile b/Pipfile index f0a5864..aa8a23b 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,8 @@ name = "pypi" [packages] pyyaml = "*" aiohttp = "*" +scheduler = "*" +pytz = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index eddc418..f555222 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "184f1a8e3109bf0a0a96236437c7bfe1bddfc114455b6cdf36bedaa491aed03f" + "sha256": "97e23fc67642a95bffea8782f93d340d26a92a440feae5e4b7d10dedbec5cccb" }, "pipfile-spec": 6, "requires": { @@ -591,6 +591,14 @@ "markers": "python_version >= '3.9'", "version": "==0.4.1" }, + "pytz": { + "hashes": [ + "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", + "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" + ], + "index": "pypi", + "version": "==2025.2" + }, "pyyaml": { "hashes": [ "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", @@ -671,6 +679,23 @@ "markers": "python_version >= '3.8'", "version": "==6.0.3" }, + "scheduler": { + "hashes": [ + "sha256:4575a12cd269e4e4896409836fd911560cb63fb7634360ee62aa2fa4ec495ffd", + "sha256:8f52ea6390757e4f42a8becbcb474e7744a3082ea5e1cdb0c972ea8b2c5d1891" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==0.8.8" + }, + "typeguard": { + "hashes": [ + "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", + "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e" + ], + "markers": "python_version >= '3.9'", + "version": "==4.4.4" + }, "typing-extensions": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", diff --git a/db.py b/db.py index a5dcb4f..dcf2aad 100644 --- a/db.py +++ b/db.py @@ -2,7 +2,7 @@ import os import sqlite3 import datetime -SQLITE_DB = os.environ.get('SQLITE_DB', 'db.sqlite3') +from settings import SQLITE_DB TABLE_REPEATS = ''' CREATE TABLE IF NOT EXISTS repeats ( diff --git a/generate.py b/generate.py index 98b6d44..850b371 100644 --- a/generate.py +++ b/generate.py @@ -2,7 +2,9 @@ import yaml import json import logging import random + from db import Database +from settings import ORDERS_YML logger = logging.getLogger(__name__) @@ -33,8 +35,8 @@ def pick_order(orders): return (result, repeat) -def read_config(filename="orders.yml"): - with open(filename) as stream: +def read_config(): + with open(ORDERS_YML) as stream: try: orders = yaml.safe_load(stream) except yaml.YAMLError as exc: diff --git a/main.py b/main.py index 7f00880..43824b8 100644 --- a/main.py +++ b/main.py @@ -3,90 +3,12 @@ import sys import logging import argparse import asyncio -import aiohttp -import datetime -from generate import generate_order, generate_punishment -from mastodon import Mastodon -from db import Database +from scheduling import OrderScheduler +from orders import order_issue, order_check logger = logging.getLogger(__name__) -ORDER_TIMEOUT = datetime.timedelta( - hours=os.getenv('ORDER_TIMEOUT', default=3) -) -MASTODON_USERNAME = os.getenv('MASTODON_USERNAME') - -def make_session(): - return aiohttp.ClientSession() - -async def immediate(): - orders_info = generate_order() - - if 'orders' not in orders_info: - logger.info("No orders for today") - return - - post = "Here are today's orders for @%s - \n\n" % MASTODON_USERNAME - if "count" in orders_info and orders_info['count'] > 1: - post += f"These are the same orders from the last {orders_info['count']} days\n\n" - post += "\n".join(orders_info['orders']) - - async with make_session() as session: - m = Mastodon(session) - status = await m.statusPost(post) - db = Database() - - db.order_status_put( - status['id'], - status['created_at'], - post - ) - -async def check(): - async with make_session() as session: - m = Mastodon(session) - db = Database() - - outstanding_orders = db.order_status_outstanding() - for outstanding_order in outstanding_orders: - context = await m.statusContext(outstanding_order['mastodon_id']) - - confirmed_at = None - for d in context['descendants']: - if ( - d['in_reply_to_id'] == outstanding_order['mastodon_id'] and - d['account']['username'] == MASTODON_USERNAME and - len(d['media_attachments']) > 0 - ): - confirmed_at = d['created_at'] - db.order_status_confirm(outstanding_order['id'], confirmed_at) - logger.info('Confirmed order %s' % (outstanding_order['created_at'])) - break - - if confirmed_at is None: - logger.info('Order %s remains unconfirmed' % (outstanding_order['created_at'])) - - oo_created_at = datetime.datetime.fromisoformat(outstanding_order['created_at']) - if(oo_created_at + ORDER_TIMEOUT < datetime.datetime.now(datetime.UTC)): - logger.info('Time to issue a punishment for %s' % outstanding_order['created_at']) - - punishment = generate_punishment() - - post = "@%s has failed to post proof of compliance. Punishment is as follows -\n\n" % MASTODON_USERNAME - post += "\n".join(punishment) - - punishment_status = await m.statusPost( - post, - in_reply_to_id=outstanding_order['mastodon_id'] - ) - db.punishment_status_put( - outstanding_order['id'], - punishment_status['id'], - punishment_status['created_at'], - post - ) - if __name__=='__main__': logging.basicConfig( format="%(asctime)s %(module)s [%(levelname)-4.4s] %(message)s", @@ -106,9 +28,13 @@ if __name__=='__main__': if args.command == 'immediate': loop = asyncio.new_event_loop() - loop.run_until_complete(immediate()) + loop.run_until_complete(order_issue()) loop.close() elif args.command == 'check': loop = asyncio.new_event_loop() - loop.run_until_complete(check()) + loop.run_until_complete(order_check()) loop.close() + else: + loop = asyncio.new_event_loop() + s = OrderScheduler(loop) + loop.run_forever() diff --git a/mastodon.py b/mastodon.py index 5f66025..6558253 100644 --- a/mastodon.py +++ b/mastodon.py @@ -1,8 +1,7 @@ import os import logging -MASTODON_INSTANCE = os.environ.get("MASTODON_INSTANCE") -MASTODON_ACCESS_TOKEN = os.environ.get('MASTODON_ACCESS_TOKEN') +from settings import MASTODON_INSTANCE, MASTODON_ACCESS_TOKEN API_STATUSES = '/api/v1/statuses' API_STATUS_CONTEXT = '/api/v1/statuses/%(id)s/context' diff --git a/orders.py b/orders.py new file mode 100644 index 0000000..581dd0a --- /dev/null +++ b/orders.py @@ -0,0 +1,77 @@ +import logging +import datetime + +from util import make_session +from generate import generate_order, generate_punishment +from db import Database +from mastodon import Mastodon +from settings import MASTODON_USERNAME, ORDER_TIMEOUT + +logger = logging.getLogger(__name__) + +async def order_issue(): + orders_info = generate_order() + + if 'orders' not in orders_info: + logger.info("No orders for today") + return + + post = "Here are today's orders for @%s - \n\n" % MASTODON_USERNAME + if "count" in orders_info and orders_info['count'] > 1: + post += f"These are the same orders from the last {orders_info['count']} days\n\n" + post += "\n".join(orders_info['orders']) + + async with make_session() as session: + m = Mastodon(session) + status = await m.statusPost(post) + db = Database() + + db.order_status_put( + status['id'], + status['created_at'], + post + ) + +async def order_check(): + async with make_session() as session: + m = Mastodon(session) + db = Database() + + outstanding_orders = db.order_status_outstanding() + for outstanding_order in outstanding_orders: + context = await m.statusContext(outstanding_order['mastodon_id']) + + confirmed_at = None + for d in context['descendants']: + if ( + d['in_reply_to_id'] == outstanding_order['mastodon_id'] and + d['account']['username'] == MASTODON_USERNAME and + len(d['media_attachments']) > 0 + ): + confirmed_at = d['created_at'] + db.order_status_confirm(outstanding_order['id'], confirmed_at) + logger.info('Confirmed order %s' % (outstanding_order['created_at'])) + break + + if confirmed_at is None: + logger.info('Order %s remains unconfirmed' % (outstanding_order['created_at'])) + + oo_created_at = datetime.datetime.fromisoformat(outstanding_order['created_at']) + if(oo_created_at + ORDER_TIMEOUT < datetime.datetime.now(datetime.UTC)): + logger.info('Time to issue a punishment for %s' % outstanding_order['created_at']) + + punishment = generate_punishment() + + post = "@%s has failed to post proof of compliance. Punishment is as follows -\n\n" % MASTODON_USERNAME + post += "\n".join(punishment) + + punishment_status = await m.statusPost( + post, + in_reply_to_id=outstanding_order['mastodon_id'] + ) + db.punishment_status_put( + outstanding_order['id'], + punishment_status['id'], + punishment_status['created_at'], + post + ) diff --git a/scheduling.py b/scheduling.py new file mode 100644 index 0000000..ddb2e97 --- /dev/null +++ b/scheduling.py @@ -0,0 +1,45 @@ +import logging +import datetime +import pytz + +from scheduler.asyncio import Scheduler + +from settings import TIMEZONE, ORDER_TIME, ORDER_TIMEOUT +from orders import order_issue, order_check + +logger = logging.getLogger(__name__) + +SATURDAY = 5 +SUNDAY = 6 + +class OrderScheduler(): + def __init__(self, loop): + self.tz = pytz.timezone(TIMEZONE) + + self.scheduler = Scheduler(loop=loop, tzinfo=self.tz) + + order_time_arr = list(map(int, ORDER_TIME.split(':'))) + order_time = datetime.time(hour=order_time_arr[0], minute=order_time_arr[1], tzinfo=self.tz) + self.scheduler.daily(order_time, self.scheduled_order) + + logger.info(self.scheduler) + + async def scheduled_order(self): + day_of_week = datetime.datetime.now(tz=self.tz).weekday() + if (day_of_week in [SATURDAY, SUNDAY]): + return + + await order_issue() + + # Schedule check + self.scheduler.once( + datetime.datetime.now(tz=self.tz) + + ORDER_TIMEOUT + + datetime.timedelta(seconds=10), + self.scheduled_check + ) + + logger.info(self.scheduler) + + async def scheduled_check(self): + await order_check() diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..e4dcd5f --- /dev/null +++ b/settings.py @@ -0,0 +1,17 @@ +import os +import datetime + +ORDER_TIME = os.environ.get('ORDER_TIME', '9:00') +ORDER_TIMEOUT = datetime.timedelta( + hours=os.environ.get('ORDER_TIMEOUT', 3) +) +MASTODON_USERNAME = os.environ.get('MASTODON_USERNAME') + +MASTODON_INSTANCE = os.environ.get("MASTODON_INSTANCE") +MASTODON_ACCESS_TOKEN = os.environ.get('MASTODON_ACCESS_TOKEN') + +SQLITE_DB = os.environ.get('SQLITE_DB', 'db.sqlite3') + +ORDERS_YML = os.environ.get('ORDERS_YML', 'orders.yml') + +TIMEZONE = os.environ.get('TIMEZONE', 'America/Chicago') diff --git a/util.py b/util.py new file mode 100644 index 0000000..a92f5b2 --- /dev/null +++ b/util.py @@ -0,0 +1,4 @@ +import aiohttp + +def make_session(): + return aiohttp.ClientSession()