Sync schedule

This commit is contained in:
Johnny Gear 2026-03-06 18:33:08 -06:00
parent b1e3d24a54
commit ccd523d740
8 changed files with 200 additions and 17 deletions

View file

@ -1,5 +1,8 @@
import pytz
import datetime
from peewee import * from peewee import *
from settings import SQLITE_DB, MASTODON_INSTANCE from settings import SQLITE_DB, MASTODON_INSTANCE
from util import sqlite_time
database = SqliteDatabase(SQLITE_DB) database = SqliteDatabase(SQLITE_DB)
@ -75,6 +78,8 @@ class OrdersPool(BaseModel):
null=True null=True
) )
updated_at = DateTimeField()
def to_dict(self): def to_dict(self):
return { return {
'id': self.id, 'id': self.id,
@ -101,6 +106,14 @@ class OrdersPool(BaseModel):
} for order in self.orders] } for order in self.orders]
} }
def save(self, *args, **kwargs):
self.updated_at = sqlite_time(datetime.datetime.now(datetime.UTC))
return super(OrdersPool, self).save(*args, **kwargs)
def __str__(self):
return f"{self.name}[{self.user.telegram_username}]"
class Meta: class Meta:
table_name = 'orders_pool' table_name = 'orders_pool'
@ -127,9 +140,9 @@ class OrderAddOn(BaseModel):
# Order State # Order State
# #
class OrderStatus(BaseModel): class OrderStatus(BaseModel):
confirmed_at = DateTimeField(null=True) # TIMESTAMP confirmed_at = DateTimeField(null=True)
created_at = DateTimeField() # TIMESTAMP created_at = DateTimeField()
due_at = DateTimeField(null=True) # TIMESTAMP due_at = DateTimeField(null=True)
mastodon_id = TextField() mastodon_id = TextField()
text = TextField() text = TextField()

View file

@ -67,6 +67,9 @@ def orders_pool(user_id, set_id):
def orders_pool_by_id(pool_id): def orders_pool_by_id(pool_id):
return OrdersPool.get(OrdersPool.id == pool_id) return OrdersPool.get(OrdersPool.id == pool_id)
def orders_pool_since(dt):
return OrdersPool.select().where(OrdersPool.updated_at > dt)
def orders_pool_scheduled(): def orders_pool_scheduled():
return OrdersPool.select().where(OrdersPool.scheduled == True) return OrdersPool.select().where(OrdersPool.scheduled == True)
@ -113,7 +116,7 @@ def skip_day_put(user, date):
return SkipDay.create(user=user, date=date) return SkipDay.create(user=user, date=date)
def skip_day_delete(user, date): def skip_day_delete(user, date):
q = SkipDay.delete().where(SkipDay.user == user and SkipDay.date == date) q = SkipDay.delete().where(SkipDay.user == user & SkipDay.date == date)
return q.execute() return q.execute()
def skip_days_upcoming(user): def skip_days_upcoming(user):
@ -123,7 +126,7 @@ def skip_days_upcoming(user):
.limit(10)) .limit(10))
def skip_day_contains(user, date): def skip_day_contains(user, date):
q = SkipDay.select().where(SkipDay.user == user and SkipDay.date == date) q = SkipDay.select().where(SkipDay.user == user & SkipDay.date == date)
return len(q) > 0 return len(q) > 0
def order_status_put(orders_pool, user, mastodon_id, created_at, due_at, text, punishment_for=None): def order_status_put(orders_pool, user, mastodon_id, created_at, due_at, text, punishment_for=None):

View file

@ -0,0 +1,49 @@
"""Peewee migrations -- 019_add_orders_pool_updated_at.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
'orders_pool',
updated_at=pw.DateTimeField(null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('orders_pool', 'updated_at')

View file

@ -0,0 +1,44 @@
"""Peewee migrations -- 020_update_orders_pool_updated_at.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
database.execute_sql("UPDATE orders_pool SET updated_at = CURRENT_TIMESTAMP")
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""

View file

@ -0,0 +1,44 @@
"""Peewee migrations -- 021_not_null_orders_pool_updated_at.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""

View file

@ -4,10 +4,10 @@ import pytz
from scheduler.asyncio import Scheduler from scheduler.asyncio import Scheduler
from settings import TIMEZONE from settings import TIMEZONE, SCHEDULE_SYNC_INTERVAL
from orders import order_issue, order_check from orders import order_issue, order_check
from db.queries import orders_pool_by_id, orders_pool_scheduled, skip_day_contains, order_status_outstanding from db.queries import orders_pool_by_id, orders_pool_scheduled, orders_pool_since, skip_day_contains, order_status_outstanding
from util import order_time from util import order_time, sqlite_time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,24 +24,35 @@ class OrderScheduler():
self.scheduled_pools = {} self.scheduled_pools = {}
for orders_pool in orders_pool_scheduled(): for orders_pool in orders_pool_scheduled():
self.scheduled_pools[orders_pool.id] = self.scheduler.daily( self.schedule_pool(orders_pool)
order_time(orders_pool.time),
self.scheduled_order,
args=(orders_pool.id,)
)
self.outstanding_orders = {}
for order_status in order_status_outstanding(): for order_status in order_status_outstanding():
self.outstanding_orders[order_status.id] = self.scheduler.once( self.scheduler.once(
datetime.datetime.fromisoformat(order_status.due_at) + GRACE_PERIOD, datetime.datetime.fromisoformat(order_status.due_at) + GRACE_PERIOD,
self.scheduled_check, self.scheduled_check,
args=(order_status.id,) args=(order_status.id,)
) )
# TODO: Schedule keeping schedule up to date self.last_update = datetime.datetime.now(datetime.UTC)
self.scheduler.cyclic(
datetime.timedelta(seconds=SCHEDULE_SYNC_INTERVAL),
self.update_schedule
)
logger.info(self.scheduler) logger.info(self.scheduler)
def schedule_pool(self, orders_pool):
if orders_pool.id in self.scheduled_pools:
self.scheduler.delete_job(self.scheduled_pools[orders_pool.id])
del self.scheduled_pools[orders_pool.id]
if orders_pool.scheduled:
self.scheduled_pools[orders_pool.id] = self.scheduler.daily(
order_time(orders_pool.time),
self.scheduled_order,
args=(orders_pool.id,)
)
async def scheduled_order(self, orders_pool_id): async def scheduled_order(self, orders_pool_id):
orders_pool = orders_pool_by_id(orders_pool_id) orders_pool = orders_pool_by_id(orders_pool_id)
@ -68,7 +79,7 @@ class OrderScheduler():
if order_status is not None: if order_status is not None:
# Schedule check # Schedule check
self.outstanding_orders[order_status.id] = self.scheduler.once( self.scheduler.once(
order_status.due_at + GRACE_PERIOD, order_status.due_at + GRACE_PERIOD,
self.scheduled_check, self.scheduled_check,
args=(order_status.id,) args=(order_status.id,)
@ -76,3 +87,18 @@ class OrderScheduler():
async def scheduled_check(self, outstanding_order_id): async def scheduled_check(self, outstanding_order_id):
await order_check(outstanding_order_id) await order_check(outstanding_order_id)
async def update_schedule(self):
last_update_sqlite = sqlite_time(self.last_update)
updated = False
for orders_pool_updated in orders_pool_since(last_update_sqlite):
logger.info(f'Updating schedule for {orders_pool_updated}')
self.schedule_pool(orders_pool_updated)
updated = True
if updated:
logger.info(self.scheduler)
self.last_update = datetime.datetime.now(datetime.UTC)

View file

@ -28,6 +28,7 @@ SQLITE_DB = os.environ.get('SQLITE_DB', 'db.sqlite3')
ORDERS_YML = os.environ.get('ORDERS_YML', 'orders.yml') ORDERS_YML = os.environ.get('ORDERS_YML', 'orders.yml')
TIMEZONE = os.environ.get('TIMEZONE', 'America/Chicago') TIMEZONE = os.environ.get('TIMEZONE', 'America/Chicago')
SCHEDULE_SYNC_INTERVAL = int(os.environ.get('SCHEDULE_SYNC_INTERVAL', 60 * 15)) # 15 minutes
FLASK_URL = os.environ.get("FLASK_URL") FLASK_URL = os.environ.get("FLASK_URL")
FLASK_SECRET_KEY = os.environ.get("FLASK_SECRET_KEY") FLASK_SECRET_KEY = os.environ.get("FLASK_SECRET_KEY")

View file

@ -10,6 +10,9 @@ def make_session():
def timezone(): def timezone():
return pytz.timezone(TIMEZONE) return pytz.timezone(TIMEZONE)
def sqlite_time(dt):
return dt.astimezone(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
def order_time(str): def order_time(str):
order_time_arr = list(map(int, str.split(':'))) order_time_arr = list(map(int, str.split(':')))
return datetime.time( return datetime.time(