scheduler WiP
This commit is contained in:
parent
246ef996c1
commit
442118fdc7
5 changed files with 144 additions and 1 deletions
102
facturio/management/commands/run_scheduler.py
Normal file
102
facturio/management/commands/run_scheduler.py
Normal 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()
|
|
@ -54,6 +54,7 @@ THIRD_PARTY_APPS = [
|
|||
'crispy_forms',
|
||||
'crispy_bootstrap5',
|
||||
'post_office',
|
||||
'django_apscheduler',
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
39
uv.lock
|
@ -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"
|
||||
|
|
Reference in a new issue