scheduler WiP

This commit is contained in:
Jakub Kropáček 2025-03-03 21:26:27 +01:00
parent 246ef996c1
commit 442118fdc7
5 changed files with 144 additions and 1 deletions

View file

@ -0,0 +1,102 @@
import logging
import signal
from copy import deepcopy
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
from django.conf import settings
from django.core.management.base import BaseCommand
from django_apscheduler import util
from django_apscheduler.models import DjangoJobExecution
from django_apscheduler.models import DjangoJobStore
from django_dramatiq.models import Task
logger = logging.getLogger(__name__)
DEFAULT_JOB_KWARGS = {
"max_instances": 1,
"replace_existing": True,
}
# For cron definitions use https://crontab.guru/
daily_cron = CronTrigger.from_crontab("0 0 * * *")
weekly_cron = CronTrigger.from_crontab("0 0 * * 0")
monthly_cron = CronTrigger.from_crontab("30 3 1 * *")
daily_export_cron = CronTrigger.from_crontab("0 5 * * *")
PERIODIC_JOBS = [
{
"task": "not_working.yet",
"trigger": daily_cron,
},
]
# The `close_old_connections` decorator ensures that database connections that have become
# unusable or are obsolete are closed before and after your job has run.
# You should use it to wrap any jobs that you schedule that access the Django database in any way.
@util.close_old_connections
def delete_old_job_executions(max_age: int = 604_800) -> None:
"""
This job deletes APScheduler job execution entries older than `max_age` from the database.
It helps to prevent the database from filling up with old historical records that are no
longer useful.
:param max_age: The maximum length of time to retain historical job execution records.
Defaults to 7 days.
"""
DjangoJobExecution.objects.delete_old_job_executions(max_age)
Task.tasks.delete_old_tasks(max_age)
class Command(BaseCommand):
help = "Runs APScheduler." # noqa: A003
scheduler = None
def prepare_scheduler(self):
self.stdout.write(self.style.NOTICE("Preparing scheduler"))
self.scheduler = BlockingScheduler(timezone=settings.TIME_ZONE)
self.scheduler.add_jobstore(DjangoJobStore(), "default")
def add_jobs(self):
self.scheduler.add_job(
delete_old_job_executions,
trigger=CronTrigger(
day_of_week="mon", hour="00", minute="00",
), # Midnight on Monday, before the start of the next work week.
id="delete_old_job_executions",
max_instances=1,
replace_existing=True,
)
self.stdout.write("Added weekly job: 'delete_old_job_executions'.")
for job in PERIODIC_JOBS:
kwargs = DEFAULT_JOB_KWARGS | deepcopy(job)
task = kwargs.pop("task")
if "id" not in kwargs:
kwargs["id"] = task
self.scheduler.add_job(task, **kwargs)
self.stdout.write(f"Added job: {task}")
def handle_shutdown(self, *args, **kwargs): # noqa: ARG001
self.stdout.write(self.style.NOTICE("Stopping scheduler..."))
self.scheduler.shutdown()
self.stdout.write(self.style.NOTICE("Scheduler shut down successfully!"))
def handle(self, *args, **options):
self.prepare_scheduler()
self.add_jobs()
# signal.signal(signal.SIGINT, self.handle_shutdown)
signal.signal(signal.SIGTERM, self.handle_shutdown)
try:
self.stdout.write(self.style.NOTICE("Starting scheduler..."))
self.scheduler.start()
except KeyboardInterrupt:
self.handle_shutdown()

View file

@ -54,6 +54,7 @@ THIRD_PARTY_APPS = [
'crispy_forms',
'crispy_bootstrap5',
'post_office',
'django_apscheduler',
]
INSTALLED_APPS = [

View file

@ -10,7 +10,7 @@ from invoices.models import Invoice
@dramatiq.actor()
def generate_pdf(invoice_id: int):
invoice = Invoice.objects.get(pk=invoice_id)
print_url = settings.INTERNAL_API_URL + reverse('invoices:print_invoice', kwargs=dict(invoice_id=invoice.id))
print_url = f"{settings.INTERNAL_API_URL}{reverse('invoices:print_invoice', kwargs=dict(invoice_id=invoice.id))}"
invoice_file = invoice.files.create()

View file

@ -8,6 +8,7 @@ dependencies = [
"ares-util>=0.3.0",
"crispy-bootstrap5>=2024.10",
"django>=5.1.6",
"django-apscheduler>=0.7.0",
"django-crispy-forms>=2.3",
"django-dramatiq>=0.13.0",
"django-environ>=0.12.0",

39
uv.lock
View file

@ -16,6 +16,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "apscheduler"
version = "3.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzlocal" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004 },
]
[[package]]
name = "ares-util"
version = "0.3.0"
@ -164,6 +176,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/75/6f/d2c216d00975e2604b10940937b0ba6b2c2d9b3cc0cc633e414ae3f14b2e/Django-5.1.6-py3-none-any.whl", hash = "sha256:8d203400bc2952fbfb287c2bbda630297d654920c72a73cc82a9ad7926feaad5", size = 8277066 },
]
[[package]]
name = "django-apscheduler"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "apscheduler" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/6b/873899c2da113187b74f0cccdf4c16660e07bfbbcae72621c4758e0958bf/django_apscheduler-0.7.0.tar.gz", hash = "sha256:30d61a2ba98615922fc2c9782f84bba342ec0c5ed63384d686d71ea90a1a4318", size = 473051 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/19/c3d2dea21a6afdc93689b9f769ff3694cac810e4a09c24ab423dd1613e6c/django_apscheduler-0.7.0-py3-none-any.whl", hash = "sha256:869d489775420245c9455d55e35f663c856a33ebfc996d92938f786ffb8730ce", size = 24690 },
]
[[package]]
name = "django-crispy-forms"
version = "2.3"
@ -236,6 +261,7 @@ dependencies = [
{ name = "ares-util" },
{ name = "crispy-bootstrap5" },
{ name = "django" },
{ name = "django-apscheduler" },
{ name = "django-crispy-forms" },
{ name = "django-dramatiq" },
{ name = "django-environ" },
@ -257,6 +283,7 @@ requires-dist = [
{ name = "ares-util", specifier = ">=0.3.0" },
{ name = "crispy-bootstrap5", specifier = ">=2024.10" },
{ name = "django", specifier = ">=5.1.6" },
{ name = "django-apscheduler", specifier = ">=0.7.0" },
{ name = "django-crispy-forms", specifier = ">=2.3" },
{ name = "django-dramatiq", specifier = ">=0.13.0" },
{ name = "django-environ", specifier = ">=0.12.0" },
@ -579,6 +606,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 },
]
[[package]]
name = "tzlocal"
version = "5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/cc/11360404b20a6340b9b4ed39a3338c4af47bc63f87f6cea94dbcbde07029/tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2", size = 30480 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/9f/1c0b69d3abf4c65acac051ad696b8aea55afbb746dea8017baab53febb5e/tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c", size = 17920 },
]
[[package]]
name = "urllib3"
version = "2.3.0"