Scheduling
This commit is contained in:
parent
a728a75245
commit
8e8a85e822
10 changed files with 185 additions and 88 deletions
2
Pipfile
2
Pipfile
|
|
@ -6,6 +6,8 @@ name = "pypi"
|
||||||
[packages]
|
[packages]
|
||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
aiohttp = "*"
|
aiohttp = "*"
|
||||||
|
scheduler = "*"
|
||||||
|
pytz = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|
|
||||||
27
Pipfile.lock
generated
27
Pipfile.lock
generated
|
|
@ -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
2
db.py
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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
90
main.py
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
77
orders.py
Normal 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
45
scheduling.py
Normal 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
17
settings.py
Normal 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
4
util.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
def make_session():
|
||||||
|
return aiohttp.ClientSession()
|
||||||
Loading…
Reference in a new issue