Due to the flexibiilty of Django , I will often use it in places that are not a normal web app. A combination of the Django ORM and admin commands makes a Django project a useful place to collect useful scripts. In the past I would often add things to Cron, but due to the tasks framework I have started to add some jobs there. To make it even easier to call tasks, I hooked up Alfred to my Django project.
Zatsumu 雑務
Every project needs a name, and since this is a private project, any name is fine. I called it Zatsumu which means: miscellaneous duties; (trivial) routine tasks; small jobs; odd jobs
For the most part, it is a normal Django project, even though I never run the web component. One minor interesting snippet, is one I did to be able to reuse the repo across a few machines.
# from settings.py
import socket
from importlib.util import find_spec
HOST_FQDN = socket.gethostname()
HOST_SHORT = HOST_FQDN.split(".")[0]
for app in [
f"zatsumu.custom.{HOST_SHORT}",
f"zatsumu.platform.{platform.system().lower()}",
]:
if find_spec(app):
INSTALLED_APPS.insert(0, app)
This allows me to have host specific modules and platform specific modules that live in the same project, but are enabled based on where they are deployed.
For the tasks backend, I am using one of the builtin backends
# Tasks
# https://docs.djangoproject.com/en/6.0/ref/settings#tasks
TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
"QUEUES": ["default", HOST_FQDN],
}
}
I do have two extra projects added:
- django-crontask is used for scheduling background tasks
- django-inspect-tasks is used to manually call a specific task
INSTALLED_APPS = [
"crontask",
"django_inspect_tasks",
# Default Django, could probably remove most of these
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
Calling Django from Alfred
In Alfred, we will configure the following workflow.
Script Filter -> Run Script -> Post Notification
Script Filterwill simply call our django-admin command.uv run zatsumu alfredRun Scriptwill call the task name we passeduv run zatsumu tasks $1Post Notificationis so we can see the result.
Returning Django results to Alfred
We can reference Alfred’s script-filter for the format we need to use. We can return the json payload using our own django-admin script.
import json
import sys
from collections.abc import Generator
from crontask import scheduler
from django.core.management.base import BaseCommand
from django_inspect_tasks.util import all_tasks
# I often prefer to normalize items when rendering the json at the end
def normalize_json(obj):
match obj:
case Generator():
return list(obj)
case _:
return str(obj)
class Command(BaseCommand):
def handle(self, **kwargs):
def items():
# First we need to lookup all the tasks our app can see
tasks = {t.module_path: t for t in all_tasks()}
# Then we lookup the schedule so we can add it as a subtitle
scheduled = {}
for job in scheduler.get_jobs():
scheduled[job.func.__self__.module_path] = job.trigger
# Loop through the tasks and return a script-filter payload
# https://www.alfredapp.com/help/workflows/inputs/script-filter/json/
for task in tasks:
item = {
"title": str(task),
"arg": str(task),
}
if task in scheduled:
item["subtitle"] = str(scheduled[task])
yield item
# Output our items for Alfred to consume
json.dump(
{
"items": items(),
"cache": {"seconds": 3600, "loosereload": True},
},
fp=sys.stdout,
default=normalize_json,
sort_keys=True,
indent=2,
)
With this, now I have an alfred command, that can list all of the tasks in my Django project. Selecting a task will then execute it in the background, and pop up a notification when it is done.