From 934de1f77217ddc744f847008da9b348b8b361a6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 13 Jan 2022 10:24:47 +1100 Subject: [PATCH] Adds the ability for 'scheduled tasks' to be member functions of plugins --- .../plugin/builtin/integration/mixins.py | 55 ++++++++++++++++--- InvenTree/plugin/registry.py | 16 ++++++ .../samples/integration/scheduled_task.py | 27 ++++++++- docker/dev-config.env | 2 +- 4 files changed, 89 insertions(+), 11 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index a2087ee879..0af712641d 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -75,10 +75,18 @@ class ScheduleMixin: 'schedule': "I", # Schedule type (see django_q.Schedule) 'minutes': 30, # Number of minutes (only if schedule type = Minutes) 'repeats': 5, # Number of repeats (leave blank for 'forever') - } + }, + 'member_func': { + 'func': 'my_class_func', # Note, without the 'dot' notation, it will call a class member function + 'schedule': "H", # Once per hour + }, } Note: 'schedule' parameter must be one of ['I', 'H', 'D', 'W', 'M', 'Q', 'Y'] + + Note: The 'func' argument can take two different forms: + - Dotted notation e.g. 'module.submodule.func' - calls a global function with the defined path + - Member notation e.g. 'my_func' (no dots!) - calls a member function of the calling class """ ALLOWABLE_SCHEDULE_TYPES = ['I', 'H', 'D', 'W', 'M', 'Q', 'Y'] @@ -94,11 +102,14 @@ class ScheduleMixin: def __init__(self): super().__init__() - self.add_mixin('schedule', 'has_scheduled_tasks', __class__) - self.scheduled_tasks = getattr(self, 'SCHEDULED_TASKS', {}) - + self.scheduled_tasks = self.get_scheduled_tasks() self.validate_scheduled_tasks() + self.add_mixin('schedule', 'has_scheduled_tasks', __class__) + + def get_scheduled_tasks(self): + return getattr(self, 'SCHEDULED_TASKS', {}) + @property def has_scheduled_tasks(self): """ @@ -158,18 +169,46 @@ class ScheduleMixin: task_name = self.get_task_name(key) - # If a matching scheduled task does not exist, create it! - if not Schedule.objects.filter(name=task_name).exists(): + if Schedule.objects.filter(name=task_name).exists(): + # Scheduled task already exists - continue! + continue - logger.info(f"Adding scheduled task '{task_name}'") + logger.info(f"Adding scheduled task '{task_name}'") + + func_name = task['func'].strip() + + if '.' in func_name: + """ + Dotted notation indicates that we wish to run a globally defined function, + from a specified Python module. + """ Schedule.objects.create( name=task_name, - func=task['func'], + func=func_name, schedule_type=task['schedule'], minutes=task.get('minutes', None), repeats=task.get('repeats', -1), ) + + else: + """ + Non-dotted notation indicates that we wish to call a 'member function' of the calling plugin. + + This is managed by the plugin registry itself. + """ + + slug = self.plugin_slug() + + Schedule.objects.create( + name=task_name, + func='plugin.registry.registry.call_plugin_function', + args=f'{slug}, {func_name}', + schedule_type=task['schedule'], + minutes=task.get('minutes', None), + repeats=task.get('repeats', -1), + ) + except (ProgrammingError, OperationalError): # Database might not yet be ready logger.warning("register_tasks failed, database not ready") diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index e31f3c6529..b08f643412 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -59,6 +59,22 @@ class PluginsRegistry: # mixins self.mixins_settings = {} + def call_plugin_function(self, slug, func, *args, **kwargs): + """ + Call a member function (named by 'func') of the plugin named by 'slug'. + + As this is intended to be run by the background worker, + we do not perform any try/except here. + + Instead, any error messages are returned to the worker. + """ + + plugin = self.plugins[slug] + + plugin_func = getattr(plugin, func) + + plugin_func(*args, **kwargs) + # region public functions # region loading / unloading def load_plugins(self): diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index 825dab134f..c8b1c4c5d0 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -3,7 +3,7 @@ Sample plugin which supports task scheduling """ from plugin import IntegrationPluginBase -from plugin.mixins import ScheduleMixin +from plugin.mixins import ScheduleMixin, SettingsMixin # Define some simple tasks to perform @@ -15,7 +15,7 @@ def print_world(): print("World") -class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): +class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase): """ A sample plugin which provides support for scheduled tasks """ @@ -25,6 +25,11 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): PLUGIN_TITLE = "Scheduled Tasks" SCHEDULED_TASKS = { + 'member': { + 'func': 'member_func', + 'schedule': 'I', + 'minutes': 30, + }, 'hello': { 'func': 'plugin.samples.integration.scheduled_task.print_hello', 'schedule': 'I', @@ -35,3 +40,21 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): 'schedule': 'H', }, } + + SETTINGS = { + 'T_OR_F': { + 'name': 'True or False', + 'description': 'Print true or false when running the periodic task', + 'validator': bool, + 'default': False, + }, + } + + def member_func(self, *args, **kwargs): + """ + A simple member function to demonstrate functionality + """ + + t_or_f = self.get_setting('T_OR_F') + + print(f"Called member_func - value is {t_or_f}") diff --git a/docker/dev-config.env b/docker/dev-config.env index b7ee4d8526..63a0afe4fb 100644 --- a/docker/dev-config.env +++ b/docker/dev-config.env @@ -14,4 +14,4 @@ INVENTREE_DB_USER=pguser INVENTREE_DB_PASSWORD=pgpassword # Enable plugins? -INVENTREE_PLUGINS_ENABLED=False +INVENTREE_PLUGINS_ENABLED=True