Scheduling

This commit is contained in:
Johnny Gear 2025-11-13 22:03:20 -06:00
parent a728a75245
commit 8e8a85e822
10 changed files with 185 additions and 88 deletions

View file

@ -6,6 +6,8 @@ name = "pypi"
[packages] [packages]
pyyaml = "*" pyyaml = "*"
aiohttp = "*" aiohttp = "*"
scheduler = "*"
pytz = "*"
[dev-packages] [dev-packages]

27
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "184f1a8e3109bf0a0a96236437c7bfe1bddfc114455b6cdf36bedaa491aed03f" "sha256": "97e23fc67642a95bffea8782f93d340d26a92a440feae5e4b7d10dedbec5cccb"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -591,6 +591,14 @@
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==0.4.1" "version": "==0.4.1"
}, },
"pytz": {
"hashes": [
"sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3",
"sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"
],
"index": "pypi",
"version": "==2025.2"
},
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c",
@ -671,6 +679,23 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==6.0.3" "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": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",

2
db.py
View file

@ -2,7 +2,7 @@ import os
import sqlite3 import sqlite3
import datetime import datetime
SQLITE_DB = os.environ.get('SQLITE_DB', 'db.sqlite3') from settings import SQLITE_DB
TABLE_REPEATS = ''' TABLE_REPEATS = '''
CREATE TABLE IF NOT EXISTS repeats ( CREATE TABLE IF NOT EXISTS repeats (

View file

@ -2,7 +2,9 @@ import yaml
import json import json
import logging import logging
import random import random
from db import Database from db import Database
from settings import ORDERS_YML
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,8 +35,8 @@ def pick_order(orders):
return (result, repeat) return (result, repeat)
def read_config(filename="orders.yml"): def read_config():
with open(filename) as stream: with open(ORDERS_YML) as stream:
try: try:
orders = yaml.safe_load(stream) orders = yaml.safe_load(stream)
except yaml.YAMLError as exc: except yaml.YAMLError as exc:

90
main.py
View file

@ -3,90 +3,12 @@ import sys
import logging import logging
import argparse import argparse
import asyncio import asyncio
import aiohttp
import datetime
from generate import generate_order, generate_punishment from scheduling import OrderScheduler
from mastodon import Mastodon from orders import order_issue, order_check
from db import Database
logger = logging.getLogger(__name__) 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__': if __name__=='__main__':
logging.basicConfig( logging.basicConfig(
format="%(asctime)s %(module)s [%(levelname)-4.4s] %(message)s", format="%(asctime)s %(module)s [%(levelname)-4.4s] %(message)s",
@ -106,9 +28,13 @@ if __name__=='__main__':
if args.command == 'immediate': if args.command == 'immediate':
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
loop.run_until_complete(immediate()) loop.run_until_complete(order_issue())
loop.close() loop.close()
elif args.command == 'check': elif args.command == 'check':
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
loop.run_until_complete(check()) loop.run_until_complete(order_check())
loop.close() loop.close()
else:
loop = asyncio.new_event_loop()
s = OrderScheduler(loop)
loop.run_forever()

View file

@ -1,8 +1,7 @@
import os import os
import logging import logging
MASTODON_INSTANCE = os.environ.get("MASTODON_INSTANCE") from settings import MASTODON_INSTANCE, MASTODON_ACCESS_TOKEN
MASTODON_ACCESS_TOKEN = os.environ.get('MASTODON_ACCESS_TOKEN')
API_STATUSES = '/api/v1/statuses' API_STATUSES = '/api/v1/statuses'
API_STATUS_CONTEXT = '/api/v1/statuses/%(id)s/context' API_STATUS_CONTEXT = '/api/v1/statuses/%(id)s/context'

77
orders.py Normal file
View file

@ -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
)

45
scheduling.py Normal file
View file

@ -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()

17
settings.py Normal file
View file

@ -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')

4
util.py Normal file
View file

@ -0,0 +1,4 @@
import aiohttp
def make_session():
return aiohttp.ClientSession()