🧑💻(tasks) run management commands
This allows to run management commands from a celery task.
This commit is contained in:
@@ -8,6 +8,10 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 🧑💻(tasks) run management commands #814
|
||||
|
||||
## [1.14.1] - 2025-03-17
|
||||
|
||||
## [1.14.0] - 2025-03-17
|
||||
|
||||
@@ -1,11 +1,66 @@
|
||||
"""People Core application"""
|
||||
# from django.apps import AppConfig
|
||||
# from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command, get_commands
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.utils.io import TeeStringIO
|
||||
|
||||
from people.celery_app import app as celery_app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# class CoreConfig(AppConfig):
|
||||
# """Configuration class for the People core app."""
|
||||
class CoreConfig(AppConfig):
|
||||
"""Configuration class for the People core app."""
|
||||
|
||||
# name = "core"
|
||||
# app_label = "core"
|
||||
# verbose_name = _("People core application")
|
||||
name = "core"
|
||||
app_label = "core"
|
||||
verbose_name = _("People core application")
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Register management command which are enabled via MANAGEMENT_COMMAND_AS_TASK setting.
|
||||
"""
|
||||
for command_name in settings.MANAGEMENT_COMMAND_AS_TASK:
|
||||
# Check if the command is a valid management command
|
||||
try:
|
||||
app_name = get_commands()[command_name]
|
||||
except KeyError:
|
||||
logging.error(
|
||||
"Command %s is not a valid management command.", command_name
|
||||
)
|
||||
continue
|
||||
|
||||
command_full_name = ".".join([app_name, command_name])
|
||||
|
||||
# Create a closure to capture the current value of command_full_name and command_name
|
||||
def create_task(cmd_name, cmd_full_name):
|
||||
@celery_app.task(name=cmd_full_name)
|
||||
def task_wrapper(*command_args, **command_options):
|
||||
stdout = TeeStringIO(logging.getLogger(cmd_full_name).info)
|
||||
stderr = TeeStringIO(logging.getLogger(cmd_full_name).error)
|
||||
|
||||
call_command(
|
||||
cmd_name,
|
||||
*command_args,
|
||||
no_color=True,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
**command_options,
|
||||
)
|
||||
|
||||
stdout.seek(0)
|
||||
stderr.seek(0)
|
||||
return {
|
||||
"stdout": str(stdout.read()),
|
||||
"stderr": str(stderr.read()),
|
||||
}
|
||||
|
||||
return task_wrapper
|
||||
|
||||
# Create the task with the current values
|
||||
create_task(command_name, command_full_name)
|
||||
|
||||
36
src/backend/core/tests/test_management_command_tasks.py
Normal file
36
src/backend/core/tests/test_management_command_tasks.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Tests the core application loads the management command as tasks."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from people.celery_app import app as celery_app
|
||||
|
||||
|
||||
def test_fill_organization_metadata_as_task(settings):
|
||||
"""Check the fill_organization_metadata command is loaded as a task."""
|
||||
# Verify the command is configured to be loaded as a task
|
||||
assert "fill_organization_metadata" in settings.MANAGEMENT_COMMAND_AS_TASK
|
||||
|
||||
# The task should be registered in the format "app_name.command_name"
|
||||
task_name = "core.fill_organization_metadata"
|
||||
assert task_name in celery_app.tasks
|
||||
|
||||
# Test that the task can be executed properly
|
||||
with patch("core.apps.call_command") as mock_call_command:
|
||||
# Get the registered task
|
||||
task = celery_app.tasks[task_name]
|
||||
|
||||
# Execute the task
|
||||
result = task("arg1", "arg2", kwarg1="value1", kwarg2="value2")
|
||||
|
||||
# Verify call_command was called with the correct command name
|
||||
mock_call_command.assert_called_once()
|
||||
assert mock_call_command.call_args[0][0] == "fill_organization_metadata"
|
||||
assert mock_call_command.call_args[0][1] == "arg1"
|
||||
assert mock_call_command.call_args[0][2] == "arg2"
|
||||
assert mock_call_command.call_args[1]["kwarg1"] == "value1"
|
||||
assert mock_call_command.call_args[1]["kwarg2"] == "value2"
|
||||
|
||||
# Verify the task returns a dictionary with stdout and stderr
|
||||
assert isinstance(result, dict)
|
||||
assert "stdout" in result
|
||||
assert "stderr" in result
|
||||
49
src/backend/core/utils/io.py
Normal file
49
src/backend/core/utils/io.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Utility module providing I/O related classes and functions."""
|
||||
|
||||
from io import StringIO
|
||||
|
||||
|
||||
class TeeStringIO:
|
||||
"""String IO implementation that captures output while preserving original logger output."""
|
||||
|
||||
def __init__(self, logger_output):
|
||||
"""Initialize a TeeStringIO instance.
|
||||
|
||||
Args:
|
||||
logger_output: A callable that will receive captured output.
|
||||
"""
|
||||
self.logger_output = logger_output
|
||||
self.buffer = StringIO()
|
||||
|
||||
def write(self, value):
|
||||
"""Write a string to both the logger and internal buffer.
|
||||
|
||||
Args:
|
||||
value: The string to write.
|
||||
"""
|
||||
self.logger_output(value.strip("\n"))
|
||||
self.buffer.write(value)
|
||||
|
||||
def read(self):
|
||||
"""Read the contents of the buffer.
|
||||
|
||||
Returns:
|
||||
The buffer contents as a string.
|
||||
"""
|
||||
return self.buffer.read()
|
||||
|
||||
def seek(self, *args, **kwargs):
|
||||
"""Set the buffer's position.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments passed to the underlying buffer.
|
||||
**kwargs: Keyword arguments passed to the underlying buffer.
|
||||
|
||||
Returns:
|
||||
The new position in the buffer.
|
||||
"""
|
||||
return self.buffer.seek(*args, **kwargs)
|
||||
|
||||
def flush(self):
|
||||
"""Flush the internal buffer."""
|
||||
self.buffer.flush()
|
||||
@@ -583,6 +583,14 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
MANAGEMENT_COMMAND_AS_TASK = [
|
||||
"fill_organization_metadata",
|
||||
] + values.ListValue(
|
||||
default=[],
|
||||
environ_name="MANAGEMENT_COMMAND_AS_TASK",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
|
||||
Reference in New Issue
Block a user