Merge pull request #2543 from SchrodingersGat/plugin-schedule-fix

Adds the ability for 'scheduled tasks' to be member functions of plugins
This commit is contained in:
Oliver 2022-01-13 11:25:48 +11:00 committed by GitHub
commit c55b31d2fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 11 deletions

View File

@ -75,10 +75,18 @@ class ScheduleMixin:
'schedule': "I", # Schedule type (see django_q.Schedule) 'schedule': "I", # Schedule type (see django_q.Schedule)
'minutes': 30, # Number of minutes (only if schedule type = Minutes) 'minutes': 30, # Number of minutes (only if schedule type = Minutes)
'repeats': 5, # Number of repeats (leave blank for 'forever') '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: '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'] ALLOWABLE_SCHEDULE_TYPES = ['I', 'H', 'D', 'W', 'M', 'Q', 'Y']
@ -94,11 +102,14 @@ class ScheduleMixin:
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.add_mixin('schedule', 'has_scheduled_tasks', __class__) self.scheduled_tasks = self.get_scheduled_tasks()
self.scheduled_tasks = getattr(self, 'SCHEDULED_TASKS', {})
self.validate_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 @property
def has_scheduled_tasks(self): def has_scheduled_tasks(self):
""" """
@ -158,18 +169,46 @@ class ScheduleMixin:
task_name = self.get_task_name(key) task_name = self.get_task_name(key)
# If a matching scheduled task does not exist, create it! if Schedule.objects.filter(name=task_name).exists():
if not 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( Schedule.objects.create(
name=task_name, name=task_name,
func=task['func'], func=func_name,
schedule_type=task['schedule'], schedule_type=task['schedule'],
minutes=task.get('minutes', None), minutes=task.get('minutes', None),
repeats=task.get('repeats', -1), 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.call_function',
args=f"'{slug}', '{func_name}'",
schedule_type=task['schedule'],
minutes=task.get('minutes', None),
repeats=task.get('repeats', -1),
)
except (ProgrammingError, OperationalError): except (ProgrammingError, OperationalError):
# Database might not yet be ready # Database might not yet be ready
logger.warning("register_tasks failed, database not ready") logger.warning("register_tasks failed, database not ready")

View File

@ -59,6 +59,22 @@ class PluginsRegistry:
# mixins # mixins
self.mixins_settings = {} 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)
return plugin_func(*args, **kwargs)
# region public functions # region public functions
# region loading / unloading # region loading / unloading
def load_plugins(self): def load_plugins(self):
@ -557,3 +573,8 @@ class PluginsRegistry:
registry = PluginsRegistry() registry = PluginsRegistry()
def call_function(plugin_name, function_name, *args, **kwargs):
""" Global helper function to call a specific member function of a plugin """
return registry.call_plugin_function(plugin_name, function_name, *args, **kwargs)

View File

@ -3,7 +3,7 @@ Sample plugin which supports task scheduling
""" """
from plugin import IntegrationPluginBase from plugin import IntegrationPluginBase
from plugin.mixins import ScheduleMixin from plugin.mixins import ScheduleMixin, SettingsMixin
# Define some simple tasks to perform # Define some simple tasks to perform
@ -15,7 +15,7 @@ def print_world():
print("World") print("World")
class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase):
""" """
A sample plugin which provides support for scheduled tasks A sample plugin which provides support for scheduled tasks
""" """
@ -25,6 +25,11 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase):
PLUGIN_TITLE = "Scheduled Tasks" PLUGIN_TITLE = "Scheduled Tasks"
SCHEDULED_TASKS = { SCHEDULED_TASKS = {
'member': {
'func': 'member_func',
'schedule': 'I',
'minutes': 30,
},
'hello': { 'hello': {
'func': 'plugin.samples.integration.scheduled_task.print_hello', 'func': 'plugin.samples.integration.scheduled_task.print_hello',
'schedule': 'I', 'schedule': 'I',
@ -35,3 +40,21 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase):
'schedule': 'H', '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}")

View File

@ -14,4 +14,4 @@ INVENTREE_DB_USER=pguser
INVENTREE_DB_PASSWORD=pgpassword INVENTREE_DB_PASSWORD=pgpassword
# Enable plugins? # Enable plugins?
INVENTREE_PLUGINS_ENABLED=False INVENTREE_PLUGINS_ENABLED=True