This commit is contained in:
Joakim Hellsén 2026-04-27 20:43:26 +02:00
commit a7a5b5c8ea
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
43 changed files with 5531 additions and 9 deletions

View file

@ -0,0 +1,3 @@
from config.celery import app as celery_app
__all__ = ("celery_app",)

11
config/celery.py Normal file
View file

@ -0,0 +1,11 @@
from __future__ import annotations
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
app = Celery("tussilago")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

View file

@ -39,9 +39,7 @@ def check_required_dev_commands(*_: object, **__: object) -> list[CheckMessage]:
if not (settings.DEBUG or getattr(settings, "TESTING", False)):
return []
missing_commands: list[str] = [
command for command in REQUIRED_DEV_COMMANDS if shutil.which(command) is None
]
missing_commands: list[str] = [command for command in REQUIRED_DEV_COMMANDS if shutil.which(command) is None]
if not missing_commands:
return []

112
config/dev_autoreload.py Normal file
View file

@ -0,0 +1,112 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
import django_watchfiles
from django.utils import autoreload
if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Sequence
from watchfiles import Change
IGNORED_PROJECT_ROOT_NAMES: frozenset[str] = frozenset({
".git",
".venv",
"__pycache__",
"firecracker",
})
def _resolve_path(path: Path) -> Path:
try:
return path.resolve()
except OSError:
return path.absolute()
def _is_relative_to(path: Path, parent: Path) -> bool:
try:
path.relative_to(parent)
except ValueError:
return False
return True
def build_project_watch_roots(
project_root: Path,
*,
ignored_root_names: Sequence[str] = tuple(IGNORED_PROJECT_ROOT_NAMES),
) -> tuple[Path, ...]:
"""Return top-level project directories worth watching during development."""
ignored_names = set(ignored_root_names)
resolved_project_root = _resolve_path(project_root)
return tuple(
sorted(
_resolve_path(child)
for child in resolved_project_root.iterdir()
if child.is_dir() and not child.name.startswith(".") and child.name not in ignored_names
),
)
class TussilagoWatchfilesReloader(django_watchfiles.WatchfilesReloader):
"""Use child directories instead of repo root for dev autoreload watches."""
def __init__(
self,
*,
project_root: Path,
ignored_paths: Sequence[Path] | None = None,
) -> None:
"""Store project-specific watch settings before watcher startup."""
self.project_root = _resolve_path(project_root)
self.ignored_paths = tuple(
_resolve_path(path) for path in (ignored_paths or (self.project_root / "firecracker",))
)
self.project_watch_roots = build_project_watch_roots(self.project_root)
super().__init__()
def file_filter(self, change: Change, filename: str) -> bool:
"""Drop file events from ignored paths such as the firecracker tree.
Returns:
True when the file event should still be watched.
"""
resolved_path = _resolve_path(Path(filename))
if any(_is_relative_to(resolved_path, ignored_path) for ignored_path in self.ignored_paths):
return False
return super().file_filter(change, filename)
def watched_roots(self, watched_files: Iterable[Path]) -> frozenset[Path]:
"""Replace repo-root watches with top-level child directories.
Returns:
Watch roots with the project root expanded into child directories.
"""
roots = {_resolve_path(root) for root in super().watched_roots(watched_files)}
filtered_roots = {
root
for root in roots
if not any(_is_relative_to(root, ignored_path) for ignored_path in self.ignored_paths)
}
if self.project_root in filtered_roots:
filtered_roots.remove(self.project_root)
filtered_roots.update(self.project_watch_roots)
return frozenset(filtered_roots)
def install_watchfiles_reloader_patch(*, project_root: Path) -> None:
"""Install project-specific watchfiles reloader for dev and test runtimes."""
resolved_project_root = _resolve_path(project_root)
class ProjectWatchfilesReloader(TussilagoWatchfilesReloader):
def __init__(self) -> None:
super().__init__(project_root=resolved_project_root)
def replaced_get_reloader() -> autoreload.BaseReloader:
return ProjectWatchfilesReloader()
autoreload.get_reloader = replaced_get_reloader

View file

@ -70,6 +70,7 @@ if DEBUG:
INSTALLED_APPS: list[str] = [
"config.apps.TussilagoConfig",
"control_plane.apps.ControlPlaneConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
@ -122,6 +123,24 @@ DATABASES: dict[str, dict[str, str | Path | dict[str, str | int]]] = {
},
}
DISABLE_SERVER_SIDE_CURSORS: bool = True
CELERY_BROKER_URL: str = os.getenv(
"TUSSILAGO_CELERY_BROKER_URL",
"memory://" if DEBUG or TESTING else "",
)
CELERY_RESULT_BACKEND: str = os.getenv(
"TUSSILAGO_CELERY_RESULT_BACKEND",
"cache+memory://" if DEBUG or TESTING else "",
)
CELERY_ACCEPT_CONTENT: list[str] = ["json"]
CELERY_TASK_SERIALIZER: str = "json"
CELERY_RESULT_SERIALIZER: str = "json"
CELERY_TIMEZONE: str = TIME_ZONE
CELERY_TASK_ALWAYS_EAGER: bool = TESTING
CELERY_TASK_EAGER_PROPAGATES: bool = TESTING
CELERY_TASK_DEFAULT_QUEUE: str = "control-plane"
AUTH_PASSWORD_VALIDATORS: list[dict[str, str]] = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
@ -186,5 +205,9 @@ if DEBUG or TESTING:
"django_browser_reload",
))
from config.dev_autoreload import install_watchfiles_reloader_patch
install_watchfiles_reloader_patch(project_root=BASE_DIR)
# Customize the config to support htmx boosting.
DEBUG_TOOLBAR_CONFIG: dict[str, str] = {"ROOT_TAG_EXTRA_ATTRS": "hx-preserve"}

View file

@ -11,6 +11,7 @@ if TYPE_CHECKING:
from django.urls.resolvers import URLResolver
urlpatterns: list[URLPattern | URLResolver] = [
path(route="", view=include(arg="control_plane.urls")),
path(route="admin/", view=admin.site.urls),
]