diff --git a/db/models.py b/db/models.py index 80eccc8..310e9fd 100644 --- a/db/models.py +++ b/db/models.py @@ -1,5 +1,8 @@ +import pytz +import datetime from peewee import * from settings import SQLITE_DB, MASTODON_INSTANCE +from util import sqlite_time database = SqliteDatabase(SQLITE_DB) @@ -75,6 +78,8 @@ class OrdersPool(BaseModel): null=True ) + updated_at = DateTimeField() + def to_dict(self): return { 'id': self.id, @@ -101,6 +106,14 @@ class OrdersPool(BaseModel): } 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: table_name = 'orders_pool' @@ -127,9 +140,9 @@ class OrderAddOn(BaseModel): # Order State # class OrderStatus(BaseModel): - confirmed_at = DateTimeField(null=True) # TIMESTAMP - created_at = DateTimeField() # TIMESTAMP - due_at = DateTimeField(null=True) # TIMESTAMP + confirmed_at = DateTimeField(null=True) + created_at = DateTimeField() + due_at = DateTimeField(null=True) mastodon_id = TextField() text = TextField() diff --git a/db/queries.py b/db/queries.py index 0a14a30..1249e98 100644 --- a/db/queries.py +++ b/db/queries.py @@ -67,6 +67,9 @@ def orders_pool(user_id, set_id): def orders_pool_by_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(): return OrdersPool.select().where(OrdersPool.scheduled == True) @@ -113,7 +116,7 @@ def skip_day_put(user, date): return SkipDay.create(user=user, date=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() def skip_days_upcoming(user): @@ -123,7 +126,7 @@ def skip_days_upcoming(user): .limit(10)) 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 def order_status_put(orders_pool, user, mastodon_id, created_at, due_at, text, punishment_for=None): diff --git a/migrations/019_add_orders_pool_updated_at.py b/migrations/019_add_orders_pool_updated_at.py new file mode 100644 index 0000000..ab752b6 --- /dev/null +++ b/migrations/019_add_orders_pool_updated_at.py @@ -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') diff --git a/migrations/020_update_orders_pool_updated_at.py b/migrations/020_update_orders_pool_updated_at.py new file mode 100644 index 0000000..3b62520 --- /dev/null +++ b/migrations/020_update_orders_pool_updated_at.py @@ -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.""" + diff --git a/migrations/021_not_null_orders_pool_updated_at.py b/migrations/021_not_null_orders_pool_updated_at.py new file mode 100644 index 0000000..4d919d7 --- /dev/null +++ b/migrations/021_not_null_orders_pool_updated_at.py @@ -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.""" + diff --git a/scheduling.py b/scheduling.py index 1657817..091be08 100644 --- a/scheduling.py +++ b/scheduling.py @@ -4,10 +4,10 @@ import pytz from scheduler.asyncio import Scheduler -from settings import TIMEZONE +from settings import TIMEZONE, SCHEDULE_SYNC_INTERVAL 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 util import order_time +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, sqlite_time logger = logging.getLogger(__name__) @@ -24,24 +24,35 @@ class OrderScheduler(): self.scheduled_pools = {} for orders_pool in orders_pool_scheduled(): - self.scheduled_pools[orders_pool.id] = self.scheduler.daily( - order_time(orders_pool.time), - self.scheduled_order, - args=(orders_pool.id,) - ) + self.schedule_pool(orders_pool) - self.outstanding_orders = {} 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, self.scheduled_check, 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) + 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): orders_pool = orders_pool_by_id(orders_pool_id) @@ -68,7 +79,7 @@ class OrderScheduler(): if order_status is not None: # Schedule check - self.outstanding_orders[order_status.id] = self.scheduler.once( + self.scheduler.once( order_status.due_at + GRACE_PERIOD, self.scheduled_check, args=(order_status.id,) @@ -76,3 +87,18 @@ class OrderScheduler(): async def scheduled_check(self, 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) diff --git a/settings.py b/settings.py index 23ad228..3aeec4a 100644 --- a/settings.py +++ b/settings.py @@ -28,6 +28,7 @@ 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') +SCHEDULE_SYNC_INTERVAL = int(os.environ.get('SCHEDULE_SYNC_INTERVAL', 60 * 15)) # 15 minutes FLASK_URL = os.environ.get("FLASK_URL") FLASK_SECRET_KEY = os.environ.get("FLASK_SECRET_KEY") diff --git a/util.py b/util.py index 5e64366..269fdbf 100644 --- a/util.py +++ b/util.py @@ -10,6 +10,9 @@ def make_session(): def 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): order_time_arr = list(map(int, str.split(':'))) return datetime.time(