added comments throughout
This commit is contained in:
@ -1,4 +1,10 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
This file contains definitions for the LocalPythonConductor, in order to
|
||||||
|
execute Python jobs on the local resource.
|
||||||
|
|
||||||
|
Author(s): David Marchant
|
||||||
|
"""
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from core.correctness.vars import PYTHON_TYPE, PYTHON_FUNC
|
from core.correctness.vars import PYTHON_TYPE, PYTHON_FUNC
|
||||||
@ -18,8 +24,6 @@ class LocalPythonConductor(BaseConductor):
|
|||||||
valid_job(job)
|
valid_job(job)
|
||||||
|
|
||||||
job_function = job[PYTHON_FUNC]
|
job_function = job[PYTHON_FUNC]
|
||||||
job_arguments = job
|
job_function(job)
|
||||||
|
|
||||||
job_function(job_arguments)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
This file contains various validation functions to be used throughout the
|
||||||
|
package.
|
||||||
|
|
||||||
|
Author(s): David Marchant
|
||||||
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from os.path import sep, exists, isfile, isdir, dirname
|
from os.path import sep, exists, isfile, isdir, dirname
|
||||||
@ -8,11 +14,13 @@ from core.correctness.vars import VALID_PATH_CHARS, get_not_imp_msg, \
|
|||||||
EVENT_TYPE, EVENT_PATH, JOB_EVENT, JOB_TYPE, JOB_ID, JOB_PATTERN, \
|
EVENT_TYPE, EVENT_PATH, JOB_EVENT, JOB_TYPE, JOB_ID, JOB_PATTERN, \
|
||||||
JOB_RECIPE, JOB_RULE, JOB_STATUS, JOB_CREATE_TIME
|
JOB_RECIPE, JOB_RULE, JOB_STATUS, JOB_CREATE_TIME
|
||||||
|
|
||||||
|
# Required keys in event dict
|
||||||
EVENT_KEYS = {
|
EVENT_KEYS = {
|
||||||
EVENT_TYPE: str,
|
EVENT_TYPE: str,
|
||||||
EVENT_PATH: str
|
EVENT_PATH: str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Required keys in job dict
|
||||||
JOB_KEYS = {
|
JOB_KEYS = {
|
||||||
JOB_TYPE: str,
|
JOB_TYPE: str,
|
||||||
JOB_EVENT: dict,
|
JOB_EVENT: dict,
|
||||||
@ -26,28 +34,16 @@ JOB_KEYS = {
|
|||||||
|
|
||||||
def check_type(variable:Any, expected_type:type, alt_types:list[type]=[],
|
def check_type(variable:Any, expected_type:type, alt_types:list[type]=[],
|
||||||
or_none:bool=False)->None:
|
or_none:bool=False)->None:
|
||||||
"""
|
"""Checks if a given variable is of the expected type. Raises TypeError or
|
||||||
Checks if a given variable is of the expected type. Raises TypeError or
|
ValueError as appropriate if any issues are encountered."""
|
||||||
ValueError as appropriate if any issues are encountered.
|
|
||||||
|
|
||||||
:param variable: (any) variable to check type of
|
|
||||||
|
|
||||||
:param expected_type: (type) expected type of the provided variable
|
|
||||||
|
|
||||||
:param alt_types: (optional)(list) additional types that are also
|
|
||||||
acceptable
|
|
||||||
|
|
||||||
:param or_none: (optional) boolean of if the variable can be unset.
|
|
||||||
Default value is False.
|
|
||||||
|
|
||||||
:return: No return.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
# Get a list of all allowed types
|
||||||
type_list = [expected_type]
|
type_list = [expected_type]
|
||||||
if get_origin(expected_type) is Union:
|
if get_origin(expected_type) is Union:
|
||||||
type_list = list(get_args(expected_type))
|
type_list = list(get_args(expected_type))
|
||||||
type_list = type_list + alt_types
|
type_list = type_list + alt_types
|
||||||
|
|
||||||
|
# Only accept None if explicitly allowed
|
||||||
if variable is None:
|
if variable is None:
|
||||||
if or_none == False:
|
if or_none == False:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@ -56,55 +52,53 @@ def check_type(variable:Any, expected_type:type, alt_types:list[type]=[],
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# If any type is allowed, then we can stop checking
|
||||||
if expected_type == Any:
|
if expected_type == Any:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check that variable type is within the accepted type list
|
||||||
if not isinstance(variable, tuple(type_list)):
|
if not isinstance(variable, tuple(type_list)):
|
||||||
print("egh")
|
|
||||||
|
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'Expected type(s) are %s, got %s'
|
'Expected type(s) are %s, got %s'
|
||||||
% (get_args(expected_type), type(variable))
|
% (get_args(expected_type), type(variable))
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_implementation(child_func, parent_class):
|
def check_implementation(child_func, parent_class):
|
||||||
|
"""Checks if the given function has been overridden from the one inherited
|
||||||
|
from the parent class. Raises a NotImplementedError if this is the case."""
|
||||||
|
# Check parent first implements func to measure against
|
||||||
if not hasattr(parent_class, child_func.__name__):
|
if not hasattr(parent_class, child_func.__name__):
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
f"Parent class {parent_class} does not implement base function "
|
f"Parent class {parent_class} does not implement base function "
|
||||||
f"{child_func.__name__} for children to override.")
|
f"{child_func.__name__} for children to override.")
|
||||||
parent_func = getattr(parent_class, child_func.__name__)
|
parent_func = getattr(parent_class, child_func.__name__)
|
||||||
|
|
||||||
|
# Check child implements function with correct name
|
||||||
if (child_func == parent_func):
|
if (child_func == parent_func):
|
||||||
msg = get_not_imp_msg(parent_class, parent_func)
|
msg = get_not_imp_msg(parent_class, parent_func)
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
|
# Check that child implements function with correct signature
|
||||||
child_sig = signature(child_func).parameters
|
child_sig = signature(child_func).parameters
|
||||||
parent_sig = signature(parent_func).parameters
|
parent_sig = signature(parent_func).parameters
|
||||||
|
|
||||||
if child_sig.keys() != parent_sig.keys():
|
if child_sig.keys() != parent_sig.keys():
|
||||||
msg = get_not_imp_msg(parent_class, parent_func)
|
msg = get_not_imp_msg(parent_class, parent_func)
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
def valid_string(variable:str, valid_chars:str, min_length:int=1)->None:
|
def valid_string(variable:str, valid_chars:str, min_length:int=1)->None:
|
||||||
"""
|
"""Checks that all characters in a given string are present in a provided
|
||||||
Checks that all characters in a given string are present in a provided
|
|
||||||
list of characters. Will raise an ValueError if unexpected character is
|
list of characters. Will raise an ValueError if unexpected character is
|
||||||
encountered.
|
encountered."""
|
||||||
|
|
||||||
:param variable: (str) variable to check.
|
|
||||||
|
|
||||||
:param valid_chars: (str) collection of valid characters.
|
|
||||||
|
|
||||||
:param min_length: (int) minimum length of variable.
|
|
||||||
|
|
||||||
:return: No return.
|
|
||||||
"""
|
|
||||||
check_type(variable, str)
|
check_type(variable, str)
|
||||||
check_type(valid_chars, str)
|
check_type(valid_chars, str)
|
||||||
|
|
||||||
|
# Check string is long enough
|
||||||
if len(variable) < min_length:
|
if len(variable) < min_length:
|
||||||
raise ValueError (
|
raise ValueError (
|
||||||
f"String '{variable}' is too short. Minimum length is {min_length}"
|
f"String '{variable}' is too short. Minimum length is {min_length}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check each char is acceptable
|
||||||
for char in variable:
|
for char in variable:
|
||||||
if char not in valid_chars:
|
if char not in valid_chars:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -115,6 +109,10 @@ def valid_string(variable:str, valid_chars:str, min_length:int=1)->None:
|
|||||||
def valid_dict(variable:dict[Any, Any], key_type:type, value_type:type,
|
def valid_dict(variable:dict[Any, Any], key_type:type, value_type:type,
|
||||||
required_keys:list[Any]=[], optional_keys:list[Any]=[],
|
required_keys:list[Any]=[], optional_keys:list[Any]=[],
|
||||||
strict:bool=True, min_length:int=1)->None:
|
strict:bool=True, min_length:int=1)->None:
|
||||||
|
"""Checks that a given dictionary is valid. Key and Value types are
|
||||||
|
enforced, as are required and optional keys. Will raise ValueError,
|
||||||
|
TypeError or KeyError depending on the problem encountered."""
|
||||||
|
# Validate inputs
|
||||||
check_type(variable, dict)
|
check_type(variable, dict)
|
||||||
check_type(key_type, type, alt_types=[_SpecialForm])
|
check_type(key_type, type, alt_types=[_SpecialForm])
|
||||||
check_type(value_type, type, alt_types=[_SpecialForm])
|
check_type(value_type, type, alt_types=[_SpecialForm])
|
||||||
@ -122,10 +120,12 @@ def valid_dict(variable:dict[Any, Any], key_type:type, value_type:type,
|
|||||||
check_type(optional_keys, list)
|
check_type(optional_keys, list)
|
||||||
check_type(strict, bool)
|
check_type(strict, bool)
|
||||||
|
|
||||||
|
# Check dict meets minimum length
|
||||||
if len(variable) < min_length:
|
if len(variable) < min_length:
|
||||||
raise ValueError(f"Dictionary '{variable}' is below minimum length of "
|
raise ValueError(f"Dictionary '{variable}' is below minimum length of "
|
||||||
f"{min_length}")
|
f"{min_length}")
|
||||||
|
|
||||||
|
# Check key and value types
|
||||||
for k, v in variable.items():
|
for k, v in variable.items():
|
||||||
if key_type != Any and not isinstance(k, key_type):
|
if key_type != Any and not isinstance(k, key_type):
|
||||||
raise TypeError(f"Key {k} had unexpected type '{type(k)}' "
|
raise TypeError(f"Key {k} had unexpected type '{type(k)}' "
|
||||||
@ -134,11 +134,14 @@ def valid_dict(variable:dict[Any, Any], key_type:type, value_type:type,
|
|||||||
raise TypeError(f"Value {v} had unexpected type '{type(v)}' "
|
raise TypeError(f"Value {v} had unexpected type '{type(v)}' "
|
||||||
f"rather than expected '{value_type}' in dict '{variable}'")
|
f"rather than expected '{value_type}' in dict '{variable}'")
|
||||||
|
|
||||||
|
# Check all required keys present
|
||||||
for rk in required_keys:
|
for rk in required_keys:
|
||||||
if rk not in variable.keys():
|
if rk not in variable.keys():
|
||||||
raise KeyError(f"Missing required key '{rk}' from dict "
|
raise KeyError(f"Missing required key '{rk}' from dict "
|
||||||
f"'{variable}'")
|
f"'{variable}'")
|
||||||
|
|
||||||
|
# If strict checking, enforce that only required and optional keys are
|
||||||
|
# present
|
||||||
if strict:
|
if strict:
|
||||||
for k in variable.keys():
|
for k in variable.keys():
|
||||||
if k not in required_keys and k not in optional_keys:
|
if k not in required_keys and k not in optional_keys:
|
||||||
@ -147,50 +150,75 @@ def valid_dict(variable:dict[Any, Any], key_type:type, value_type:type,
|
|||||||
|
|
||||||
def valid_list(variable:list[Any], entry_type:type,
|
def valid_list(variable:list[Any], entry_type:type,
|
||||||
alt_types:list[type]=[], min_length:int=1)->None:
|
alt_types:list[type]=[], min_length:int=1)->None:
|
||||||
|
"""Checks that a given list is valid. Value types are checked and a
|
||||||
|
ValueError or TypeError is raised if a problem is encountered."""
|
||||||
check_type(variable, list)
|
check_type(variable, list)
|
||||||
|
|
||||||
|
# Check length meets minimum
|
||||||
if len(variable) < min_length:
|
if len(variable) < min_length:
|
||||||
raise ValueError(f"List '{variable}' is too short. Should be at least "
|
raise ValueError(f"List '{variable}' is too short. Should be at least "
|
||||||
f"of length {min_length}")
|
f"of length {min_length}")
|
||||||
|
|
||||||
|
# Check type of each value
|
||||||
for entry in variable:
|
for entry in variable:
|
||||||
check_type(entry, entry_type, alt_types=alt_types)
|
check_type(entry, entry_type, alt_types=alt_types)
|
||||||
|
|
||||||
def valid_path(variable:str, allow_base:bool=False, extension:str="",
|
def valid_path(variable:str, allow_base:bool=False, extension:str="",
|
||||||
min_length:int=1):
|
min_length:int=1):
|
||||||
|
"""Check that a given string expresses a valid path."""
|
||||||
valid_string(variable, VALID_PATH_CHARS, min_length=min_length)
|
valid_string(variable, VALID_PATH_CHARS, min_length=min_length)
|
||||||
|
|
||||||
|
# Check we aren't given a root path
|
||||||
if not allow_base and variable.startswith(sep):
|
if not allow_base and variable.startswith(sep):
|
||||||
raise ValueError(f"Cannot accept path '{variable}'. Must be relative.")
|
raise ValueError(f"Cannot accept path '{variable}'. Must be relative.")
|
||||||
|
|
||||||
|
# Check path contains a valid extension
|
||||||
if extension and not variable.endswith(extension):
|
if extension and not variable.endswith(extension):
|
||||||
raise ValueError(f"Path '{variable}' does not have required "
|
raise ValueError(f"Path '{variable}' does not have required "
|
||||||
f"extension '{extension}'.")
|
f"extension '{extension}'.")
|
||||||
|
|
||||||
def valid_existing_file_path(variable:str, allow_base:bool=False,
|
def valid_existing_file_path(variable:str, allow_base:bool=False,
|
||||||
extension:str=""):
|
extension:str=""):
|
||||||
|
"""Check the given string is a path to an existing file."""
|
||||||
|
# Check that the string is a path
|
||||||
valid_path(variable, allow_base=allow_base, extension=extension)
|
valid_path(variable, allow_base=allow_base, extension=extension)
|
||||||
|
# Check the path exists
|
||||||
if not exists(variable):
|
if not exists(variable):
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Requested file path '{variable}' does not exist.")
|
f"Requested file path '{variable}' does not exist.")
|
||||||
|
# Check it is a file
|
||||||
if not isfile(variable):
|
if not isfile(variable):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Requested file '{variable}' is not a file.")
|
f"Requested file '{variable}' is not a file.")
|
||||||
|
|
||||||
def valid_existing_dir_path(variable:str, allow_base:bool=False):
|
def valid_existing_dir_path(variable:str, allow_base:bool=False):
|
||||||
|
"""Check the given string is a path to an existing directory."""
|
||||||
|
# Check that the string is a path
|
||||||
valid_path(variable, allow_base=allow_base, extension="")
|
valid_path(variable, allow_base=allow_base, extension="")
|
||||||
|
# Check the path exists
|
||||||
if not exists(variable):
|
if not exists(variable):
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Requested dir path '{variable}' does not exist.")
|
f"Requested dir path '{variable}' does not exist.")
|
||||||
|
# Check it is a directory
|
||||||
if not isdir(variable):
|
if not isdir(variable):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Requested dir '{variable}' is not a directory.")
|
f"Requested dir '{variable}' is not a directory.")
|
||||||
|
|
||||||
def valid_non_existing_path(variable:str, allow_base:bool=False):
|
def valid_non_existing_path(variable:str, allow_base:bool=False):
|
||||||
|
"""Check the given string is a path to something that does not exist."""
|
||||||
|
# Check that the string is a path
|
||||||
valid_path(variable, allow_base=allow_base, extension="")
|
valid_path(variable, allow_base=allow_base, extension="")
|
||||||
|
# Check the path does not exist
|
||||||
if exists(variable):
|
if exists(variable):
|
||||||
raise ValueError(f"Requested path '{variable}' already exists.")
|
raise ValueError(f"Requested path '{variable}' already exists.")
|
||||||
|
# Check that any intermediate directories exist
|
||||||
if dirname(variable) and not exists(dirname(variable)):
|
if dirname(variable) and not exists(dirname(variable)):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Route to requested path '{variable}' does not exist.")
|
f"Route to requested path '{variable}' does not exist.")
|
||||||
|
|
||||||
def setup_debugging(print:Any=None, logging:int=0)->Tuple[Any,int]:
|
def setup_debugging(print:Any=None, logging:int=0)->Tuple[Any,int]:
|
||||||
|
"""Create a place for debug messages to be sent. Always returns a place,
|
||||||
|
along with a logging level."""
|
||||||
check_type(logging, int)
|
check_type(logging, int)
|
||||||
if print is None:
|
if print is None:
|
||||||
return None, 0
|
return None, 0
|
||||||
@ -204,15 +232,22 @@ def setup_debugging(print:Any=None, logging:int=0)->Tuple[Any,int]:
|
|||||||
|
|
||||||
return print, logging
|
return print, logging
|
||||||
|
|
||||||
def valid_meow_dict(meow_dict:dict[str,Any], msg:str, keys:dict[str,type])->None:
|
def valid_meow_dict(meow_dict:dict[str,Any], msg:str,
|
||||||
|
keys:dict[str,type])->None:
|
||||||
|
"""Check given dictionary expresses a meow construct. This won't do much
|
||||||
|
directly, but is called by more specific validation functions."""
|
||||||
check_type(meow_dict, dict)
|
check_type(meow_dict, dict)
|
||||||
|
# Check we have all the required keys, and they are all of the expected
|
||||||
|
# type
|
||||||
for key, value_type in keys.items():
|
for key, value_type in keys.items():
|
||||||
if not key in meow_dict.keys():
|
if not key in meow_dict.keys():
|
||||||
raise KeyError(f"{msg} require key '{key}'")
|
raise KeyError(f"{msg} require key '{key}'")
|
||||||
check_type(meow_dict[key], value_type)
|
check_type(meow_dict[key], value_type)
|
||||||
|
|
||||||
def valid_event(event:dict[str,Any])->None:
|
def valid_event(event:dict[str,Any])->None:
|
||||||
|
"""Check that a given dict expresses a meow event."""
|
||||||
valid_meow_dict(event, "Event", EVENT_KEYS)
|
valid_meow_dict(event, "Event", EVENT_KEYS)
|
||||||
|
|
||||||
def valid_job(job:dict[str,Any])->None:
|
def valid_job(job:dict[str,Any])->None:
|
||||||
|
"""Check that a given dict expresses a meow job."""
|
||||||
valid_meow_dict(job, "Job", JOB_KEYS)
|
valid_meow_dict(job, "Job", JOB_KEYS)
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
This file contains a variety of constants used throughout the package.
|
||||||
|
Constants specific to only one file should be stored there, and only shared
|
||||||
|
here.
|
||||||
|
|
||||||
|
Author(s): David Marchant
|
||||||
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from multiprocessing import Queue
|
from multiprocessing import Queue
|
||||||
|
164
core/meow.py
164
core/meow.py
@ -1,4 +1,12 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
This file contains the core MEOW defintions, used throughout this package.
|
||||||
|
It is intended that these base definitions are what should be inherited from in
|
||||||
|
order to create an extendable framework for event-based scheduling and
|
||||||
|
processing.
|
||||||
|
|
||||||
|
Author(s): David Marchant
|
||||||
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -14,12 +22,19 @@ from core.functionality import generate_id
|
|||||||
|
|
||||||
|
|
||||||
class BaseRecipe:
|
class BaseRecipe:
|
||||||
|
# A unique identifier for the recipe
|
||||||
name:str
|
name:str
|
||||||
|
# Actual code to run
|
||||||
recipe:Any
|
recipe:Any
|
||||||
|
# Possible parameters that could be overridden by a Pattern
|
||||||
parameters:dict[str, Any]
|
parameters:dict[str, Any]
|
||||||
|
# Additional configuration options
|
||||||
requirements:dict[str, Any]
|
requirements:dict[str, Any]
|
||||||
def __init__(self, name:str, recipe:Any, parameters:dict[str,Any]={},
|
def __init__(self, name:str, recipe:Any, parameters:dict[str,Any]={},
|
||||||
requirements:dict[str,Any]={}):
|
requirements:dict[str,Any]={}):
|
||||||
|
"""BaseRecipe Constructor. This will check that any class inheriting
|
||||||
|
from it implements its validation functions. It will then call these on
|
||||||
|
the input parameters."""
|
||||||
check_implementation(type(self)._is_valid_recipe, BaseRecipe)
|
check_implementation(type(self)._is_valid_recipe, BaseRecipe)
|
||||||
check_implementation(type(self)._is_valid_parameters, BaseRecipe)
|
check_implementation(type(self)._is_valid_parameters, BaseRecipe)
|
||||||
check_implementation(type(self)._is_valid_requirements, BaseRecipe)
|
check_implementation(type(self)._is_valid_requirements, BaseRecipe)
|
||||||
@ -33,31 +48,49 @@ class BaseRecipe:
|
|||||||
self.requirements = requirements
|
self.requirements = requirements
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""A check that this base class is not instantiated itself, only
|
||||||
|
inherited from"""
|
||||||
if cls is BaseRecipe:
|
if cls is BaseRecipe:
|
||||||
msg = get_drt_imp_msg(BaseRecipe)
|
msg = get_drt_imp_msg(BaseRecipe)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
return object.__new__(cls)
|
return object.__new__(cls)
|
||||||
|
|
||||||
def _is_valid_name(self, name:str)->None:
|
def _is_valid_name(self, name:str)->None:
|
||||||
|
"""Validation check for 'name' variable from main constructor. Is
|
||||||
|
automatically called during initialisation. This does not need to be
|
||||||
|
overridden by child classes."""
|
||||||
valid_string(name, VALID_RECIPE_NAME_CHARS)
|
valid_string(name, VALID_RECIPE_NAME_CHARS)
|
||||||
|
|
||||||
def _is_valid_recipe(self, recipe:Any)->None:
|
def _is_valid_recipe(self, recipe:Any)->None:
|
||||||
|
"""Validation check for 'recipe' variable from main constructor. Must
|
||||||
|
be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_valid_parameters(self, parameters:Any)->None:
|
def _is_valid_parameters(self, parameters:Any)->None:
|
||||||
|
"""Validation check for 'parameters' variable from main constructor.
|
||||||
|
Must be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_valid_requirements(self, requirements:Any)->None:
|
def _is_valid_requirements(self, requirements:Any)->None:
|
||||||
|
"""Validation check for 'requirements' variable from main constructor.
|
||||||
|
Must be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BasePattern:
|
class BasePattern:
|
||||||
|
# A unique identifier for the pattern
|
||||||
name:str
|
name:str
|
||||||
|
# An identifier of a recipe
|
||||||
recipe:str
|
recipe:str
|
||||||
|
# Parameters to be overridden in the recipe
|
||||||
parameters:dict[str,Any]
|
parameters:dict[str,Any]
|
||||||
|
# Parameters showing the potential outputs of a recipe
|
||||||
outputs:dict[str,Any]
|
outputs:dict[str,Any]
|
||||||
def __init__(self, name:str, recipe:str, parameters:dict[str,Any]={},
|
def __init__(self, name:str, recipe:str, parameters:dict[str,Any]={},
|
||||||
outputs:dict[str,Any]={}):
|
outputs:dict[str,Any]={}):
|
||||||
|
"""BasePattern Constructor. This will check that any class inheriting
|
||||||
|
from it implements its validation functions. It will then call these on
|
||||||
|
the input parameters."""
|
||||||
check_implementation(type(self)._is_valid_recipe, BasePattern)
|
check_implementation(type(self)._is_valid_recipe, BasePattern)
|
||||||
check_implementation(type(self)._is_valid_parameters, BasePattern)
|
check_implementation(type(self)._is_valid_parameters, BasePattern)
|
||||||
check_implementation(type(self)._is_valid_output, BasePattern)
|
check_implementation(type(self)._is_valid_output, BasePattern)
|
||||||
@ -71,31 +104,50 @@ class BasePattern:
|
|||||||
self.outputs = outputs
|
self.outputs = outputs
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""A check that this base class is not instantiated itself, only
|
||||||
|
inherited from"""
|
||||||
if cls is BasePattern:
|
if cls is BasePattern:
|
||||||
msg = get_drt_imp_msg(BasePattern)
|
msg = get_drt_imp_msg(BasePattern)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
return object.__new__(cls)
|
return object.__new__(cls)
|
||||||
|
|
||||||
def _is_valid_name(self, name:str)->None:
|
def _is_valid_name(self, name:str)->None:
|
||||||
|
"""Validation check for 'name' variable from main constructor. Is
|
||||||
|
automatically called during initialisation. This does not need to be
|
||||||
|
overridden by child classes."""
|
||||||
valid_string(name, VALID_PATTERN_NAME_CHARS)
|
valid_string(name, VALID_PATTERN_NAME_CHARS)
|
||||||
|
|
||||||
def _is_valid_recipe(self, recipe:Any)->None:
|
def _is_valid_recipe(self, recipe:Any)->None:
|
||||||
|
"""Validation check for 'recipe' variable from main constructor. Must
|
||||||
|
be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_valid_parameters(self, parameters:Any)->None:
|
def _is_valid_parameters(self, parameters:Any)->None:
|
||||||
|
"""Validation check for 'parameters' variable from main constructor.
|
||||||
|
Must be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_valid_output(self, outputs:Any)->None:
|
def _is_valid_output(self, outputs:Any)->None:
|
||||||
|
"""Validation check for 'outputs' variable from main constructor. Must
|
||||||
|
be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseRule:
|
class BaseRule:
|
||||||
|
# A unique identifier for the rule
|
||||||
name:str
|
name:str
|
||||||
|
# A pattern to be used in rule triggering
|
||||||
pattern:BasePattern
|
pattern:BasePattern
|
||||||
|
# A recipe to be used in rule execution
|
||||||
recipe:BaseRecipe
|
recipe:BaseRecipe
|
||||||
|
# The string name of the pattern class that can be used to create this rule
|
||||||
pattern_type:str=""
|
pattern_type:str=""
|
||||||
|
# The string name of the recipe class that can be used to create this rule
|
||||||
recipe_type:str=""
|
recipe_type:str=""
|
||||||
def __init__(self, name:str, pattern:BasePattern, recipe:BaseRecipe):
|
def __init__(self, name:str, pattern:BasePattern, recipe:BaseRecipe):
|
||||||
|
"""BaseRule Constructor. This will check that any class inheriting
|
||||||
|
from it implements its validation functions. It will then call these on
|
||||||
|
the input parameters."""
|
||||||
check_implementation(type(self)._is_valid_pattern, BaseRule)
|
check_implementation(type(self)._is_valid_pattern, BaseRule)
|
||||||
check_implementation(type(self)._is_valid_recipe, BaseRule)
|
check_implementation(type(self)._is_valid_recipe, BaseRule)
|
||||||
self._is_valid_name(name)
|
self._is_valid_name(name)
|
||||||
@ -107,21 +159,32 @@ class BaseRule:
|
|||||||
self.__check_types_set()
|
self.__check_types_set()
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""A check that this base class is not instantiated itself, only
|
||||||
|
inherited from"""
|
||||||
if cls is BaseRule:
|
if cls is BaseRule:
|
||||||
msg = get_drt_imp_msg(BaseRule)
|
msg = get_drt_imp_msg(BaseRule)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
return object.__new__(cls)
|
return object.__new__(cls)
|
||||||
|
|
||||||
def _is_valid_name(self, name:str)->None:
|
def _is_valid_name(self, name:str)->None:
|
||||||
|
"""Validation check for 'name' variable from main constructor. Is
|
||||||
|
automatically called during initialisation. This does not need to be
|
||||||
|
overridden by child classes."""
|
||||||
valid_string(name, VALID_RULE_NAME_CHARS)
|
valid_string(name, VALID_RULE_NAME_CHARS)
|
||||||
|
|
||||||
def _is_valid_pattern(self, pattern:Any)->None:
|
def _is_valid_pattern(self, pattern:Any)->None:
|
||||||
|
"""Validation check for 'pattern' variable from main constructor. Must
|
||||||
|
be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_valid_recipe(self, recipe:Any)->None:
|
def _is_valid_recipe(self, recipe:Any)->None:
|
||||||
|
"""Validation check for 'recipe' variable from main constructor. Must
|
||||||
|
be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __check_types_set(self)->None:
|
def __check_types_set(self)->None:
|
||||||
|
"""Validation check that the self.pattern_type and self.recipe_type
|
||||||
|
attributes have been set in a child class."""
|
||||||
if self.pattern_type == "":
|
if self.pattern_type == "":
|
||||||
raise AttributeError(f"Rule Class '{self.__class__.__name__}' "
|
raise AttributeError(f"Rule Class '{self.__class__.__name__}' "
|
||||||
"does not set a pattern_type.")
|
"does not set a pattern_type.")
|
||||||
@ -131,11 +194,21 @@ class BaseRule:
|
|||||||
|
|
||||||
|
|
||||||
class BaseMonitor:
|
class BaseMonitor:
|
||||||
|
# A collection of patterns
|
||||||
_patterns: dict[str, BasePattern]
|
_patterns: dict[str, BasePattern]
|
||||||
|
# A collection of recipes
|
||||||
_recipes: dict[str, BaseRecipe]
|
_recipes: dict[str, BaseRecipe]
|
||||||
|
# A collection of rules derived from _patterns and _recipes
|
||||||
_rules: dict[str, BaseRule]
|
_rules: dict[str, BaseRule]
|
||||||
|
# A channel for sending messages to the runner. Note that this is not
|
||||||
|
# initialised within the constructor, but within the runner when passed the
|
||||||
|
# monitor is passed to it.
|
||||||
to_runner: VALID_CHANNELS
|
to_runner: VALID_CHANNELS
|
||||||
def __init__(self, patterns:dict[str,BasePattern], recipes:dict[str,BaseRecipe])->None:
|
def __init__(self, patterns:dict[str,BasePattern],
|
||||||
|
recipes:dict[str,BaseRecipe])->None:
|
||||||
|
"""BaseMonitor Constructor. This will check that any class inheriting
|
||||||
|
from it implements its validation functions. It will then call these on
|
||||||
|
the input parameters."""
|
||||||
check_implementation(type(self).start, BaseMonitor)
|
check_implementation(type(self).start, BaseMonitor)
|
||||||
check_implementation(type(self).stop, BaseMonitor)
|
check_implementation(type(self).stop, BaseMonitor)
|
||||||
check_implementation(type(self)._is_valid_patterns, BaseMonitor)
|
check_implementation(type(self)._is_valid_patterns, BaseMonitor)
|
||||||
@ -151,98 +224,161 @@ class BaseMonitor:
|
|||||||
check_implementation(type(self).remove_recipe, BaseMonitor)
|
check_implementation(type(self).remove_recipe, BaseMonitor)
|
||||||
check_implementation(type(self).get_recipes, BaseMonitor)
|
check_implementation(type(self).get_recipes, BaseMonitor)
|
||||||
check_implementation(type(self).get_rules, BaseMonitor)
|
check_implementation(type(self).get_rules, BaseMonitor)
|
||||||
|
# Ensure that patterns and recipes cannot be trivially modified from
|
||||||
|
# outside the monitor, as this will cause internal consistency issues
|
||||||
self._patterns = deepcopy(patterns)
|
self._patterns = deepcopy(patterns)
|
||||||
self._recipes = deepcopy(recipes)
|
self._recipes = deepcopy(recipes)
|
||||||
self._rules = create_rules(patterns, recipes)
|
self._rules = create_rules(patterns, recipes)
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""A check that this base class is not instantiated itself, only
|
||||||
|
inherited from"""
|
||||||
if cls is BaseMonitor:
|
if cls is BaseMonitor:
|
||||||
msg = get_drt_imp_msg(BaseMonitor)
|
msg = get_drt_imp_msg(BaseMonitor)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
return object.__new__(cls)
|
return object.__new__(cls)
|
||||||
|
|
||||||
def _is_valid_patterns(self, patterns:dict[str,BasePattern])->None:
|
def _is_valid_patterns(self, patterns:dict[str,BasePattern])->None:
|
||||||
|
"""Validation check for 'patterns' variable from main constructor. Must
|
||||||
|
be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _is_valid_recipes(self, recipes:dict[str,BaseRecipe])->None:
|
def _is_valid_recipes(self, recipes:dict[str,BaseRecipe])->None:
|
||||||
|
"""Validation check for 'recipes' variable from main constructor. Must
|
||||||
|
be implemented by any child class."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start(self)->None:
|
def start(self)->None:
|
||||||
|
"""Function to start the monitor as an ongoing process/thread. Must be
|
||||||
|
implemented by any child process"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stop(self)->None:
|
def stop(self)->None:
|
||||||
|
"""Function to stop the monitor as an ongoing process/thread. Must be
|
||||||
|
implemented by any child process"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_pattern(self, pattern:BasePattern)->None:
|
def add_pattern(self, pattern:BasePattern)->None:
|
||||||
|
"""Function to add a pattern to the current definitions. Must be
|
||||||
|
implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_pattern(self, pattern:BasePattern)->None:
|
def update_pattern(self, pattern:BasePattern)->None:
|
||||||
|
"""Function to update a pattern in the current definitions. Must be
|
||||||
|
implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove_pattern(self, pattern:Union[str,BasePattern])->None:
|
def remove_pattern(self, pattern:Union[str,BasePattern])->None:
|
||||||
|
"""Function to remove a pattern from the current definitions. Must be
|
||||||
|
implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_patterns(self)->None:
|
def get_patterns(self)->dict[str,BasePattern]:
|
||||||
|
"""Function to get a dictionary of all current pattern definitions.
|
||||||
|
Must be implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_recipe(self, recipe:BaseRecipe)->None:
|
def add_recipe(self, recipe:BaseRecipe)->None:
|
||||||
|
"""Function to add a recipe to the current definitions. Must be
|
||||||
|
implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_recipe(self, recipe:BaseRecipe)->None:
|
def update_recipe(self, recipe:BaseRecipe)->None:
|
||||||
|
"""Function to update a recipe in the current definitions. Must be
|
||||||
|
implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove_recipe(self, recipe:Union[str,BaseRecipe])->None:
|
def remove_recipe(self, recipe:Union[str,BaseRecipe])->None:
|
||||||
|
"""Function to remove a recipe from the current definitions. Must be
|
||||||
|
implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_recipes(self)->None:
|
def get_recipes(self)->dict[str,BaseRecipe]:
|
||||||
|
"""Function to get a dictionary of all current recipe definitions.
|
||||||
|
Must be implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_rules(self)->None:
|
def get_rules(self)->dict[str,BaseRule]:
|
||||||
|
"""Function to get a dictionary of all current rule definitions.
|
||||||
|
Must be implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler:
|
class BaseHandler:
|
||||||
|
# A channel for sending messages to the runner. Note that this is not
|
||||||
|
# initialised within the constructor, but within the runner when passed the
|
||||||
|
# handler is passed to it.
|
||||||
to_runner: VALID_CHANNELS
|
to_runner: VALID_CHANNELS
|
||||||
def __init__(self)->None:
|
def __init__(self)->None:
|
||||||
|
"""BaseHandler Constructor. This will check that any class inheriting
|
||||||
|
from it implements its validation functions."""
|
||||||
check_implementation(type(self).handle, BaseHandler)
|
check_implementation(type(self).handle, BaseHandler)
|
||||||
check_implementation(type(self).valid_event_types, BaseHandler)
|
check_implementation(type(self).valid_event_types, BaseHandler)
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""A check that this base class is not instantiated itself, only
|
||||||
|
inherited from"""
|
||||||
if cls is BaseHandler:
|
if cls is BaseHandler:
|
||||||
msg = get_drt_imp_msg(BaseHandler)
|
msg = get_drt_imp_msg(BaseHandler)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
return object.__new__(cls)
|
return object.__new__(cls)
|
||||||
|
|
||||||
def valid_event_types(self)->list[str]:
|
def valid_event_types(self)->list[str]:
|
||||||
|
"""Function to provide a list of the types of events this handler can
|
||||||
|
process. Must be implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def handle(self, event:dict[str,Any])->None:
|
def handle(self, event:dict[str,Any])->None:
|
||||||
|
"""Function to handle a given event. Must be implemented by any child
|
||||||
|
process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseConductor:
|
class BaseConductor:
|
||||||
def __init__(self)->None:
|
def __init__(self)->None:
|
||||||
|
"""BaseConductor Constructor. This will check that any class inheriting
|
||||||
|
from it implements its validation functions."""
|
||||||
check_implementation(type(self).execute, BaseConductor)
|
check_implementation(type(self).execute, BaseConductor)
|
||||||
check_implementation(type(self).valid_job_types, BaseConductor)
|
check_implementation(type(self).valid_job_types, BaseConductor)
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""A check that this base class is not instantiated itself, only
|
||||||
|
inherited from"""
|
||||||
|
if cls is BaseConductor:
|
||||||
|
msg = get_drt_imp_msg(BaseConductor)
|
||||||
|
raise TypeError(msg)
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
def valid_job_types(self)->list[str]:
|
def valid_job_types(self)->list[str]:
|
||||||
|
"""Function to provide a list of the types of jobs this conductor can
|
||||||
|
process. Must be implemented by any child process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def execute(self, job:dict[str,Any])->None:
|
def execute(self, job:dict[str,Any])->None:
|
||||||
|
"""Function to execute a given job. Must be implemented by any child
|
||||||
|
process."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def create_rules(patterns:Union[dict[str,BasePattern],list[BasePattern]],
|
def create_rules(patterns:Union[dict[str,BasePattern],list[BasePattern]],
|
||||||
recipes:Union[dict[str,BaseRecipe],list[BaseRecipe]],
|
recipes:Union[dict[str,BaseRecipe],list[BaseRecipe]],
|
||||||
new_rules:list[BaseRule]=[])->dict[str,BaseRule]:
|
new_rules:list[BaseRule]=[])->dict[str,BaseRule]:
|
||||||
|
"""Function to create any valid rules from a given collection of patterns
|
||||||
|
and recipes. All inbuilt rule types are considered, with additional
|
||||||
|
definitions provided through the 'new_rules' variable. Note that any
|
||||||
|
provided pattern and recipe dictionaries must be keyed with the
|
||||||
|
corresponding pattern and recipe names."""
|
||||||
|
# Validation of inputs
|
||||||
check_type(patterns, dict, alt_types=[list])
|
check_type(patterns, dict, alt_types=[list])
|
||||||
check_type(recipes, dict, alt_types=[list])
|
check_type(recipes, dict, alt_types=[list])
|
||||||
valid_list(new_rules, BaseRule, min_length=0)
|
valid_list(new_rules, BaseRule, min_length=0)
|
||||||
|
|
||||||
|
# Convert a pattern list to a dictionary
|
||||||
if isinstance(patterns, list):
|
if isinstance(patterns, list):
|
||||||
valid_list(patterns, BasePattern, min_length=0)
|
valid_list(patterns, BasePattern, min_length=0)
|
||||||
patterns = {pattern.name:pattern for pattern in patterns}
|
patterns = {pattern.name:pattern for pattern in patterns}
|
||||||
else:
|
else:
|
||||||
|
# Validate the pattern dictionary
|
||||||
valid_dict(patterns, str, BasePattern, strict=False, min_length=0)
|
valid_dict(patterns, str, BasePattern, strict=False, min_length=0)
|
||||||
for k, v in patterns.items():
|
for k, v in patterns.items():
|
||||||
if k != v.name:
|
if k != v.name:
|
||||||
@ -251,10 +387,12 @@ def create_rules(patterns:Union[dict[str,BasePattern],list[BasePattern]],
|
|||||||
"Pattern dictionaries must be keyed with the name of the "
|
"Pattern dictionaries must be keyed with the name of the "
|
||||||
"Pattern.")
|
"Pattern.")
|
||||||
|
|
||||||
|
# Convert a recipe list into a dictionary
|
||||||
if isinstance(recipes, list):
|
if isinstance(recipes, list):
|
||||||
valid_list(recipes, BaseRecipe, min_length=0)
|
valid_list(recipes, BaseRecipe, min_length=0)
|
||||||
recipes = {recipe.name:recipe for recipe in recipes}
|
recipes = {recipe.name:recipe for recipe in recipes}
|
||||||
else:
|
else:
|
||||||
|
# Validate the recipe dictionary
|
||||||
valid_dict(recipes, str, BaseRecipe, strict=False, min_length=0)
|
valid_dict(recipes, str, BaseRecipe, strict=False, min_length=0)
|
||||||
for k, v in recipes.items():
|
for k, v in recipes.items():
|
||||||
if k != v.name:
|
if k != v.name:
|
||||||
@ -263,25 +401,38 @@ def create_rules(patterns:Union[dict[str,BasePattern],list[BasePattern]],
|
|||||||
"Recipe dictionaries must be keyed with the name of the "
|
"Recipe dictionaries must be keyed with the name of the "
|
||||||
"Recipe.")
|
"Recipe.")
|
||||||
|
|
||||||
|
# Try to create a rule for each rule in turn
|
||||||
generated_rules = {}
|
generated_rules = {}
|
||||||
for pattern in patterns.values():
|
for pattern in patterns.values():
|
||||||
if pattern.recipe in recipes:
|
if pattern.recipe in recipes:
|
||||||
rule = create_rule(pattern, recipes[pattern.recipe])
|
try:
|
||||||
generated_rules[rule.name] = rule
|
rule = create_rule(pattern, recipes[pattern.recipe])
|
||||||
|
generated_rules[rule.name] = rule
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
return generated_rules
|
return generated_rules
|
||||||
|
|
||||||
def create_rule(pattern:BasePattern, recipe:BaseRecipe,
|
def create_rule(pattern:BasePattern, recipe:BaseRecipe,
|
||||||
new_rules:list[BaseRule]=[])->BaseRule:
|
new_rules:list[BaseRule]=[])->BaseRule:
|
||||||
|
"""Function to create a valid rule from a given pattern and recipe. All
|
||||||
|
inbuilt rule types are considered, with additional definitions provided
|
||||||
|
through the 'new_rules' variable."""
|
||||||
check_type(pattern, BasePattern)
|
check_type(pattern, BasePattern)
|
||||||
check_type(recipe, BaseRecipe)
|
check_type(recipe, BaseRecipe)
|
||||||
valid_list(new_rules, BaseRule, min_length=0)
|
valid_list(new_rules, BaseRule, min_length=0)
|
||||||
|
|
||||||
# Imported here to avoid circular imports at top of file
|
# Imported here to avoid circular imports at top of file
|
||||||
import rules
|
import rules
|
||||||
|
# Get a dictionary of all inbuilt rules
|
||||||
all_rules ={(r.pattern_type, r.recipe_type):r for r in [r[1] \
|
all_rules ={(r.pattern_type, r.recipe_type):r for r in [r[1] \
|
||||||
for r in inspect.getmembers(sys.modules["rules"], inspect.isclass) \
|
for r in inspect.getmembers(sys.modules["rules"], inspect.isclass) \
|
||||||
if (issubclass(r[1], BaseRule))]}
|
if (issubclass(r[1], BaseRule))]}
|
||||||
|
|
||||||
|
# Add in new rules
|
||||||
|
for rule in new_rules:
|
||||||
|
all_rules[(rule.pattern_type, rule.recipe_type)] = rule
|
||||||
|
|
||||||
|
# Find appropriate rule type from pattern and recipe types
|
||||||
key = (type(pattern).__name__, type(recipe).__name__)
|
key = (type(pattern).__name__, type(recipe).__name__)
|
||||||
if (key) in all_rules:
|
if (key) in all_rules:
|
||||||
return all_rules[key](
|
return all_rules[key](
|
||||||
@ -289,5 +440,6 @@ def create_rule(pattern:BasePattern, recipe:BaseRecipe,
|
|||||||
pattern,
|
pattern,
|
||||||
recipe
|
recipe
|
||||||
)
|
)
|
||||||
|
# Raise error if not valid rule type can be found
|
||||||
raise TypeError(f"No valid rule for Pattern '{pattern}' and Recipe "
|
raise TypeError(f"No valid rule for Pattern '{pattern}' and Recipe "
|
||||||
f"'{recipe}' could be found.")
|
f"'{recipe}' could be found.")
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
This file contains the defintion for the MeowRunner, the main construct used
|
||||||
|
for actually orchestration MEOW analysis. It is intended as a modular system,
|
||||||
|
with monitors, handlers, and conductors being swappable at initialisation.
|
||||||
|
|
||||||
|
Author(s): David Marchant
|
||||||
|
"""
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@ -16,20 +23,31 @@ from core.meow import BaseHandler, BaseMonitor, BaseConductor
|
|||||||
|
|
||||||
|
|
||||||
class MeowRunner:
|
class MeowRunner:
|
||||||
|
# A collection of all monitors in the runner
|
||||||
monitors:list[BaseMonitor]
|
monitors:list[BaseMonitor]
|
||||||
|
# A collection of all handlers in the runner
|
||||||
handlers:dict[str:BaseHandler]
|
handlers:dict[str:BaseHandler]
|
||||||
|
# A collection of all conductors in the runner
|
||||||
conductors:dict[str:BaseConductor]
|
conductors:dict[str:BaseConductor]
|
||||||
|
# A collection of all channels from each monitor
|
||||||
from_monitors: list[VALID_CHANNELS]
|
from_monitors: list[VALID_CHANNELS]
|
||||||
|
# A collection of all channels from each handler
|
||||||
from_handlers: list[VALID_CHANNELS]
|
from_handlers: list[VALID_CHANNELS]
|
||||||
def __init__(self, monitors:Union[BaseMonitor,list[BaseMonitor]],
|
def __init__(self, monitors:Union[BaseMonitor,list[BaseMonitor]],
|
||||||
handlers:Union[BaseHandler,list[BaseHandler]],
|
handlers:Union[BaseHandler,list[BaseHandler]],
|
||||||
conductors:Union[BaseConductor,list[BaseConductor]],
|
conductors:Union[BaseConductor,list[BaseConductor]],
|
||||||
print:Any=sys.stdout, logging:int=0)->None:
|
print:Any=sys.stdout, logging:int=0)->None:
|
||||||
|
"""MeowRunner constructor. This connects all provided monitors,
|
||||||
|
handlers and conductors according to what events and jobs they produce
|
||||||
|
or consume."""
|
||||||
|
|
||||||
self._is_valid_conductors(conductors)
|
self._is_valid_conductors(conductors)
|
||||||
|
# If conductors isn't a list, make it one
|
||||||
if not type(conductors) == list:
|
if not type(conductors) == list:
|
||||||
conductors = [conductors]
|
conductors = [conductors]
|
||||||
self.conductors = {}
|
self.conductors = {}
|
||||||
|
# Create a dictionary of conductors, keyed by job type, and valued by a
|
||||||
|
# list of conductors for that job type
|
||||||
for conductor in conductors:
|
for conductor in conductors:
|
||||||
conductor_jobs = conductor.valid_job_types()
|
conductor_jobs = conductor.valid_job_types()
|
||||||
if not conductor_jobs:
|
if not conductor_jobs:
|
||||||
@ -45,10 +63,13 @@ class MeowRunner:
|
|||||||
self.conductors[job] = [conductor]
|
self.conductors[job] = [conductor]
|
||||||
|
|
||||||
self._is_valid_handlers(handlers)
|
self._is_valid_handlers(handlers)
|
||||||
|
# If handlers isn't a list, make it one
|
||||||
if not type(handlers) == list:
|
if not type(handlers) == list:
|
||||||
handlers = [handlers]
|
handlers = [handlers]
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
self.from_handlers = []
|
self.from_handlers = []
|
||||||
|
# Create a dictionary of handlers, keyed by event type, and valued by a
|
||||||
|
# list of handlers for that event type
|
||||||
for handler in handlers:
|
for handler in handlers:
|
||||||
handler_events = handler.valid_event_types()
|
handler_events = handler.valid_event_types()
|
||||||
if not handler_events:
|
if not handler_events:
|
||||||
@ -62,80 +83,121 @@ class MeowRunner:
|
|||||||
self.handlers[event].append(handler)
|
self.handlers[event].append(handler)
|
||||||
else:
|
else:
|
||||||
self.handlers[event] = [handler]
|
self.handlers[event] = [handler]
|
||||||
|
# Create a channel from the handler back to this runner
|
||||||
handler_to_runner_reader, handler_to_runner_writer = Pipe()
|
handler_to_runner_reader, handler_to_runner_writer = Pipe()
|
||||||
handler.to_runner = handler_to_runner_writer
|
handler.to_runner = handler_to_runner_writer
|
||||||
self.from_handlers.append(handler_to_runner_reader)
|
self.from_handlers.append(handler_to_runner_reader)
|
||||||
|
|
||||||
self._is_valid_monitors(monitors)
|
self._is_valid_monitors(monitors)
|
||||||
|
# If monitors isn't a list, make it one
|
||||||
if not type(monitors) == list:
|
if not type(monitors) == list:
|
||||||
monitors = [monitors]
|
monitors = [monitors]
|
||||||
self.monitors = monitors
|
self.monitors = monitors
|
||||||
self.from_monitors = []
|
self.from_monitors = []
|
||||||
for monitor in self.monitors:
|
for monitor in self.monitors:
|
||||||
|
# Create a channel from the monitor back to this runner
|
||||||
monitor_to_runner_reader, monitor_to_runner_writer = Pipe()
|
monitor_to_runner_reader, monitor_to_runner_writer = Pipe()
|
||||||
monitor.to_runner = monitor_to_runner_writer
|
monitor.to_runner = monitor_to_runner_writer
|
||||||
self.from_monitors.append(monitor_to_runner_reader)
|
self.from_monitors.append(monitor_to_runner_reader)
|
||||||
|
|
||||||
|
# Create channel to send stop messages to monitor/handler thread
|
||||||
self._stop_mon_han_pipe = Pipe()
|
self._stop_mon_han_pipe = Pipe()
|
||||||
self._mon_han_worker = None
|
self._mon_han_worker = None
|
||||||
|
|
||||||
|
# Create channel to send stop messages to handler/conductor thread
|
||||||
self._stop_han_con_pipe = Pipe()
|
self._stop_han_con_pipe = Pipe()
|
||||||
self._han_con_worker = None
|
self._han_con_worker = None
|
||||||
|
|
||||||
|
# Setup debugging
|
||||||
self._print_target, self.debug_level = setup_debugging(print, logging)
|
self._print_target, self.debug_level = setup_debugging(print, logging)
|
||||||
|
|
||||||
def run_monitor_handler_interaction(self)->None:
|
def run_monitor_handler_interaction(self)->None:
|
||||||
|
"""Function to be run in its own thread, to handle any inbound messages
|
||||||
|
from monitors. These will be events, which should be matched to an
|
||||||
|
appropriate handler and handled."""
|
||||||
all_inputs = self.from_monitors + [self._stop_mon_han_pipe[0]]
|
all_inputs = self.from_monitors + [self._stop_mon_han_pipe[0]]
|
||||||
while True:
|
while True:
|
||||||
ready = wait(all_inputs)
|
ready = wait(all_inputs)
|
||||||
|
|
||||||
|
# If we get a message from the stop channel, then finish
|
||||||
if self._stop_mon_han_pipe[0] in ready:
|
if self._stop_mon_han_pipe[0] in ready:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
for from_monitor in self.from_monitors:
|
for from_monitor in self.from_monitors:
|
||||||
if from_monitor in ready:
|
if from_monitor in ready:
|
||||||
|
# Read event from the monitor channel
|
||||||
message = from_monitor.recv()
|
message = from_monitor.recv()
|
||||||
event = message
|
event = message
|
||||||
|
# Abort if we don't have a relevent handler.
|
||||||
if not self.handlers[event[EVENT_TYPE]]:
|
if not self.handlers[event[EVENT_TYPE]]:
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Could not process event as no relevent "
|
"Could not process event as no relevent "
|
||||||
f"handler for '{event[EVENT_TYPE]}'",
|
f"handler for '{event[EVENT_TYPE]}'",
|
||||||
DEBUG_INFO)
|
DEBUG_INFO)
|
||||||
return
|
return
|
||||||
|
# If we've only one handler, use that
|
||||||
if len(self.handlers[event[EVENT_TYPE]]) == 1:
|
if len(self.handlers[event[EVENT_TYPE]]) == 1:
|
||||||
self.handlers[event[EVENT_TYPE]][0].handle(event)
|
handler = self.handlers[event[EVENT_TYPE]][0]
|
||||||
|
self.handle_event(handler, event)
|
||||||
|
# If multiple handlers then randomly pick one
|
||||||
else:
|
else:
|
||||||
self.handlers[event[EVENT_TYPE]][
|
handler = self.handlers[event[EVENT_TYPE]][
|
||||||
randrange(len(self.handlers[event[EVENT_TYPE]]))
|
randrange(len(self.handlers[event[EVENT_TYPE]]))
|
||||||
].handle(event)
|
]
|
||||||
|
self.handle_event(handler, event)
|
||||||
|
|
||||||
def run_handler_conductor_interaction(self)->None:
|
def run_handler_conductor_interaction(self)->None:
|
||||||
|
"""Function to be run in its own thread, to handle any inbound messages
|
||||||
|
from handlers. These will be jobs, which should be matched to an
|
||||||
|
appropriate conductor and executed."""
|
||||||
all_inputs = self.from_handlers + [self._stop_han_con_pipe[0]]
|
all_inputs = self.from_handlers + [self._stop_han_con_pipe[0]]
|
||||||
while True:
|
while True:
|
||||||
ready = wait(all_inputs)
|
ready = wait(all_inputs)
|
||||||
|
|
||||||
|
# If we get a message from the stop channel, then finish
|
||||||
if self._stop_han_con_pipe[0] in ready:
|
if self._stop_han_con_pipe[0] in ready:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
for from_handler in self.from_handlers:
|
for from_handler in self.from_handlers:
|
||||||
if from_handler in ready:
|
if from_handler in ready:
|
||||||
|
# Read event from the handler channel
|
||||||
message = from_handler.recv()
|
message = from_handler.recv()
|
||||||
job = message
|
job = message
|
||||||
|
# Abort if we don't have a relevent conductor.
|
||||||
if not self.conductors[job[JOB_TYPE]]:
|
if not self.conductors[job[JOB_TYPE]]:
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Could not process job as no relevent "
|
"Could not process job as no relevent "
|
||||||
f"conductor for '{job[JOB_TYPE]}'", DEBUG_INFO)
|
f"conductor for '{job[JOB_TYPE]}'", DEBUG_INFO)
|
||||||
return
|
return
|
||||||
|
# If we've only one conductor, use that
|
||||||
if len(self.conductors[job[JOB_TYPE]]) == 1:
|
if len(self.conductors[job[JOB_TYPE]]) == 1:
|
||||||
conductor = self.conductors[job[JOB_TYPE]][0]
|
conductor = self.conductors[job[JOB_TYPE]][0]
|
||||||
self.execute_job(conductor, job)
|
self.execute_job(conductor, job)
|
||||||
|
# If multiple conductors then randomly pick one
|
||||||
else:
|
else:
|
||||||
conductor = self.conductors[job[JOB_TYPE]][
|
conductor = self.conductors[job[JOB_TYPE]][
|
||||||
randrange(len(self.conductors[job[JOB_TYPE]]))
|
randrange(len(self.conductors[job[JOB_TYPE]]))
|
||||||
]
|
]
|
||||||
self.execute_job(conductor, job)
|
self.execute_job(conductor, job)
|
||||||
|
|
||||||
|
def handle_event(self, handler:BaseHandler, event:dict[str:Any])->None:
|
||||||
|
"""Function for a given handler to handle a given event, without
|
||||||
|
crashing the runner in the event of a problem."""
|
||||||
|
print_debug(self._print_target, self.debug_level,
|
||||||
|
f"Starting handling for event: '{event[EVENT_TYPE]}'", DEBUG_INFO)
|
||||||
|
try:
|
||||||
|
handler.handle(event)
|
||||||
|
print_debug(self._print_target, self.debug_level,
|
||||||
|
f"Completed handling for event: '{event[EVENT_TYPE]}'",
|
||||||
|
DEBUG_INFO)
|
||||||
|
except Exception as e:
|
||||||
|
print_debug(self._print_target, self.debug_level,
|
||||||
|
"Something went wrong during handling for event "
|
||||||
|
f"'{event[EVENT_TYPE]}'. {e}", DEBUG_INFO)
|
||||||
|
|
||||||
def execute_job(self, conductor:BaseConductor, job:dict[str:Any])->None:
|
def execute_job(self, conductor:BaseConductor, job:dict[str:Any])->None:
|
||||||
|
"""Function for a given conductor to execute a given job, without
|
||||||
|
crashing the runner in the event of a problem."""
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
f"Starting execution for job: '{job[JOB_ID]}'", DEBUG_INFO)
|
f"Starting execution for job: '{job[JOB_ID]}'", DEBUG_INFO)
|
||||||
try:
|
try:
|
||||||
@ -148,20 +210,28 @@ class MeowRunner:
|
|||||||
f"'{job[JOB_ID]}'. {e}", DEBUG_INFO)
|
f"'{job[JOB_ID]}'. {e}", DEBUG_INFO)
|
||||||
|
|
||||||
def start(self)->None:
|
def start(self)->None:
|
||||||
|
"""Function to start the runner by starting all of the constituent
|
||||||
|
monitors, handlers and conductors, along with managing interaction
|
||||||
|
threads."""
|
||||||
|
# Start all monitors
|
||||||
for monitor in self.monitors:
|
for monitor in self.monitors:
|
||||||
monitor.start()
|
monitor.start()
|
||||||
startable = []
|
startable = []
|
||||||
|
# Start all handlers, if they need it
|
||||||
for handler_list in self.handlers.values():
|
for handler_list in self.handlers.values():
|
||||||
for handler in handler_list:
|
for handler in handler_list:
|
||||||
if hasattr(handler, "start") and handler not in startable:
|
if hasattr(handler, "start") and handler not in startable:
|
||||||
startable.append()
|
startable.append()
|
||||||
|
# Start all conductors, if they need it
|
||||||
for conductor_list in self.conductors.values():
|
for conductor_list in self.conductors.values():
|
||||||
for conductor in conductor_list:
|
for conductor in conductor_list:
|
||||||
if hasattr(conductor, "start") and conductor not in startable:
|
if hasattr(conductor, "start") and conductor not in startable:
|
||||||
startable.append()
|
startable.append()
|
||||||
for starting in startable:
|
for starting in startable:
|
||||||
starting.start()
|
starting.start()
|
||||||
|
|
||||||
|
# If we've not started the monitor/handler interaction thread yet, then
|
||||||
|
# do so
|
||||||
if self._mon_han_worker is None:
|
if self._mon_han_worker is None:
|
||||||
self._mon_han_worker = threading.Thread(
|
self._mon_han_worker = threading.Thread(
|
||||||
target=self.run_monitor_handler_interaction,
|
target=self.run_monitor_handler_interaction,
|
||||||
@ -177,6 +247,8 @@ class MeowRunner:
|
|||||||
msg, DEBUG_WARNING)
|
msg, DEBUG_WARNING)
|
||||||
raise RuntimeWarning(msg)
|
raise RuntimeWarning(msg)
|
||||||
|
|
||||||
|
# If we've not started the handler/conductor interaction thread yet,
|
||||||
|
# then do so
|
||||||
if self._han_con_worker is None:
|
if self._han_con_worker is None:
|
||||||
self._han_con_worker = threading.Thread(
|
self._han_con_worker = threading.Thread(
|
||||||
target=self.run_handler_conductor_interaction,
|
target=self.run_handler_conductor_interaction,
|
||||||
@ -193,14 +265,20 @@ class MeowRunner:
|
|||||||
raise RuntimeWarning(msg)
|
raise RuntimeWarning(msg)
|
||||||
|
|
||||||
def stop(self)->None:
|
def stop(self)->None:
|
||||||
|
"""Function to stop the runner by stopping all of the constituent
|
||||||
|
monitors, handlers and conductors, along with managing interaction
|
||||||
|
threads."""
|
||||||
|
# Stop all the monitors
|
||||||
for monitor in self.monitors:
|
for monitor in self.monitors:
|
||||||
monitor.stop()
|
monitor.stop()
|
||||||
|
|
||||||
stopable = []
|
stopable = []
|
||||||
|
# Stop all handlers, if they need it
|
||||||
for handler_list in self.handlers.values():
|
for handler_list in self.handlers.values():
|
||||||
for handler in handler_list:
|
for handler in handler_list:
|
||||||
if hasattr(handler, "stop") and handler not in stopable:
|
if hasattr(handler, "stop") and handler not in stopable:
|
||||||
stopable.append()
|
stopable.append()
|
||||||
|
# Stop all conductors, if they need it
|
||||||
for conductor_list in self.conductors.values():
|
for conductor_list in self.conductors.values():
|
||||||
for conductor in conductor_list:
|
for conductor in conductor_list:
|
||||||
if hasattr(conductor, "stop") and conductor not in stopable:
|
if hasattr(conductor, "stop") and conductor not in stopable:
|
||||||
@ -208,6 +286,7 @@ class MeowRunner:
|
|||||||
for stopping in stopable:
|
for stopping in stopable:
|
||||||
stopping.stop()
|
stopping.stop()
|
||||||
|
|
||||||
|
# If we've started the monitor/handler interaction thread, then stop it
|
||||||
if self._mon_han_worker is None:
|
if self._mon_han_worker is None:
|
||||||
msg = "Cannot stop event handling thread that is not started."
|
msg = "Cannot stop event handling thread that is not started."
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
@ -219,6 +298,8 @@ class MeowRunner:
|
|||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Event handler thread stopped", DEBUG_INFO)
|
"Event handler thread stopped", DEBUG_INFO)
|
||||||
|
|
||||||
|
# If we've started the handler/conductor interaction thread, then stop
|
||||||
|
# it
|
||||||
if self._han_con_worker is None:
|
if self._han_con_worker is None:
|
||||||
msg = "Cannot stop job conducting thread that is not started."
|
msg = "Cannot stop job conducting thread that is not started."
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
@ -232,18 +313,21 @@ class MeowRunner:
|
|||||||
|
|
||||||
def _is_valid_monitors(self,
|
def _is_valid_monitors(self,
|
||||||
monitors:Union[BaseMonitor,list[BaseMonitor]])->None:
|
monitors:Union[BaseMonitor,list[BaseMonitor]])->None:
|
||||||
|
"""Validation check for 'monitors' variable from main constructor."""
|
||||||
check_type(monitors, BaseMonitor, alt_types=[list])
|
check_type(monitors, BaseMonitor, alt_types=[list])
|
||||||
if type(monitors) == list:
|
if type(monitors) == list:
|
||||||
valid_list(monitors, BaseMonitor, min_length=1)
|
valid_list(monitors, BaseMonitor, min_length=1)
|
||||||
|
|
||||||
def _is_valid_handlers(self,
|
def _is_valid_handlers(self,
|
||||||
handlers:Union[BaseHandler,list[BaseHandler]])->None:
|
handlers:Union[BaseHandler,list[BaseHandler]])->None:
|
||||||
|
"""Validation check for 'handlers' variable from main constructor."""
|
||||||
check_type(handlers, BaseHandler, alt_types=[list])
|
check_type(handlers, BaseHandler, alt_types=[list])
|
||||||
if type(handlers) == list:
|
if type(handlers) == list:
|
||||||
valid_list(handlers, BaseHandler, min_length=1)
|
valid_list(handlers, BaseHandler, min_length=1)
|
||||||
|
|
||||||
def _is_valid_conductors(self,
|
def _is_valid_conductors(self,
|
||||||
conductors:Union[BaseConductor,list[BaseConductor]])->None:
|
conductors:Union[BaseConductor,list[BaseConductor]])->None:
|
||||||
|
"""Validation check for 'conductors' variable from main constructor."""
|
||||||
check_type(conductors, BaseConductor, alt_types=[list])
|
check_type(conductors, BaseConductor, alt_types=[list])
|
||||||
if type(conductors) == list:
|
if type(conductors) == list:
|
||||||
valid_list(conductors, BaseConductor, min_length=1)
|
valid_list(conductors, BaseConductor, min_length=1)
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
This file contains definitions for a MEOW pattern based off of file events,
|
||||||
|
along with an appropriate monitor for said events.
|
||||||
|
|
||||||
|
Author(s): David Marchant
|
||||||
|
"""
|
||||||
import glob
|
import glob
|
||||||
import threading
|
import threading
|
||||||
import sys
|
import sys
|
||||||
@ -23,6 +29,7 @@ from core.functionality import print_debug, create_event, get_file_hash
|
|||||||
from core.meow import BasePattern, BaseMonitor, BaseRule, BaseRecipe, \
|
from core.meow import BasePattern, BaseMonitor, BaseRule, BaseRecipe, \
|
||||||
create_rule
|
create_rule
|
||||||
|
|
||||||
|
# Events that are monitored by default
|
||||||
_DEFAULT_MASK = [
|
_DEFAULT_MASK = [
|
||||||
FILE_CREATE_EVENT,
|
FILE_CREATE_EVENT,
|
||||||
FILE_MODIFY_EVENT,
|
FILE_MODIFY_EVENT,
|
||||||
@ -30,20 +37,28 @@ _DEFAULT_MASK = [
|
|||||||
FILE_RETROACTIVE_EVENT
|
FILE_RETROACTIVE_EVENT
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Parameter sweep keys
|
||||||
SWEEP_START = "start"
|
SWEEP_START = "start"
|
||||||
SWEEP_STOP = "stop"
|
SWEEP_STOP = "stop"
|
||||||
SWEEP_JUMP = "jump"
|
SWEEP_JUMP = "jump"
|
||||||
|
|
||||||
class FileEventPattern(BasePattern):
|
class FileEventPattern(BasePattern):
|
||||||
|
# The path at which events will trigger this pattern
|
||||||
triggering_path:str
|
triggering_path:str
|
||||||
|
# The variable name given to the triggering file within recipe code
|
||||||
triggering_file:str
|
triggering_file:str
|
||||||
|
# Which types of event the pattern responds to
|
||||||
event_mask:list[str]
|
event_mask:list[str]
|
||||||
|
# TODO move me to BasePattern defintion
|
||||||
|
# A collection of variables to be swept over for job scheduling
|
||||||
sweep:dict[str,Any]
|
sweep:dict[str,Any]
|
||||||
|
|
||||||
def __init__(self, name:str, triggering_path:str, recipe:str,
|
def __init__(self, name:str, triggering_path:str, recipe:str,
|
||||||
triggering_file:str, event_mask:list[str]=_DEFAULT_MASK,
|
triggering_file:str, event_mask:list[str]=_DEFAULT_MASK,
|
||||||
parameters:dict[str,Any]={}, outputs:dict[str,Any]={},
|
parameters:dict[str,Any]={}, outputs:dict[str,Any]={},
|
||||||
sweep:dict[str,Any]={}):
|
sweep:dict[str,Any]={}):
|
||||||
|
"""FileEventPattern Constructor. This is used to match against file
|
||||||
|
system events, as caught by the python watchdog module."""
|
||||||
super().__init__(name, recipe, parameters, outputs)
|
super().__init__(name, recipe, parameters, outputs)
|
||||||
self._is_valid_triggering_path(triggering_path)
|
self._is_valid_triggering_path(triggering_path)
|
||||||
self.triggering_path = triggering_path
|
self.triggering_path = triggering_path
|
||||||
@ -54,10 +69,9 @@ class FileEventPattern(BasePattern):
|
|||||||
self._is_valid_sweep(sweep)
|
self._is_valid_sweep(sweep)
|
||||||
self.sweep = sweep
|
self.sweep = sweep
|
||||||
|
|
||||||
def _is_valid_recipe(self, recipe:str)->None:
|
|
||||||
valid_string(recipe, VALID_RECIPE_NAME_CHARS)
|
|
||||||
|
|
||||||
def _is_valid_triggering_path(self, triggering_path:str)->None:
|
def _is_valid_triggering_path(self, triggering_path:str)->None:
|
||||||
|
"""Validation check for 'triggering_path' variable from main
|
||||||
|
constructor."""
|
||||||
valid_path(triggering_path)
|
valid_path(triggering_path)
|
||||||
if len(triggering_path) < 1:
|
if len(triggering_path) < 1:
|
||||||
raise ValueError (
|
raise ValueError (
|
||||||
@ -66,19 +80,31 @@ class FileEventPattern(BasePattern):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _is_valid_triggering_file(self, triggering_file:str)->None:
|
def _is_valid_triggering_file(self, triggering_file:str)->None:
|
||||||
|
"""Validation check for 'triggering_file' variable from main
|
||||||
|
constructor."""
|
||||||
valid_string(triggering_file, VALID_VARIABLE_NAME_CHARS)
|
valid_string(triggering_file, VALID_VARIABLE_NAME_CHARS)
|
||||||
|
|
||||||
|
def _is_valid_recipe(self, recipe:str)->None:
|
||||||
|
"""Validation check for 'recipe' variable from main constructor.
|
||||||
|
Called within parent BasePattern constructor."""
|
||||||
|
valid_string(recipe, VALID_RECIPE_NAME_CHARS)
|
||||||
|
|
||||||
def _is_valid_parameters(self, parameters:dict[str,Any])->None:
|
def _is_valid_parameters(self, parameters:dict[str,Any])->None:
|
||||||
|
"""Validation check for 'parameters' variable from main constructor.
|
||||||
|
Called within parent BasePattern constructor."""
|
||||||
valid_dict(parameters, str, Any, strict=False, min_length=0)
|
valid_dict(parameters, str, Any, strict=False, min_length=0)
|
||||||
for k in parameters.keys():
|
for k in parameters.keys():
|
||||||
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
||||||
|
|
||||||
def _is_valid_output(self, outputs:dict[str,str])->None:
|
def _is_valid_output(self, outputs:dict[str,str])->None:
|
||||||
|
"""Validation check for 'output' variable from main constructor.
|
||||||
|
Called within parent BasePattern constructor."""
|
||||||
valid_dict(outputs, str, str, strict=False, min_length=0)
|
valid_dict(outputs, str, str, strict=False, min_length=0)
|
||||||
for k in outputs.keys():
|
for k in outputs.keys():
|
||||||
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
||||||
|
|
||||||
def _is_valid_event_mask(self, event_mask)->None:
|
def _is_valid_event_mask(self, event_mask)->None:
|
||||||
|
"""Validation check for 'event_mask' variable from main constructor."""
|
||||||
valid_list(event_mask, str, min_length=1)
|
valid_list(event_mask, str, min_length=1)
|
||||||
for mask in event_mask:
|
for mask in event_mask:
|
||||||
if mask not in FILE_EVENTS:
|
if mask not in FILE_EVENTS:
|
||||||
@ -86,6 +112,7 @@ class FileEventPattern(BasePattern):
|
|||||||
f"{FILE_EVENTS}")
|
f"{FILE_EVENTS}")
|
||||||
|
|
||||||
def _is_valid_sweep(self, sweep)->None:
|
def _is_valid_sweep(self, sweep)->None:
|
||||||
|
"""Validation check for 'sweep' variable from main constructor."""
|
||||||
check_type(sweep, dict)
|
check_type(sweep, dict)
|
||||||
if not sweep:
|
if not sweep:
|
||||||
return
|
return
|
||||||
@ -109,30 +136,42 @@ class FileEventPattern(BasePattern):
|
|||||||
elif v[SWEEP_JUMP] > 0:
|
elif v[SWEEP_JUMP] > 0:
|
||||||
if not v[SWEEP_STOP] > v[SWEEP_START]:
|
if not v[SWEEP_STOP] > v[SWEEP_START]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Cannot create sweep with a positive '{SWEEP_JUMP}' "
|
f"Cannot create sweep with a positive '{SWEEP_JUMP}' "
|
||||||
"value where the end point is smaller than the start."
|
"value where the end point is smaller than the start."
|
||||||
)
|
)
|
||||||
elif v[SWEEP_JUMP] < 0:
|
elif v[SWEEP_JUMP] < 0:
|
||||||
if not v[SWEEP_STOP] < v[SWEEP_START]:
|
if not v[SWEEP_STOP] < v[SWEEP_START]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Cannot create sweep with a negative '{SWEEP_JUMP}' "
|
f"Cannot create sweep with a negative '{SWEEP_JUMP}' "
|
||||||
"value where the end point is smaller than the start."
|
"value where the end point is smaller than the start."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WatchdogMonitor(BaseMonitor):
|
class WatchdogMonitor(BaseMonitor):
|
||||||
|
# A handler object, to catch events
|
||||||
event_handler:PatternMatchingEventHandler
|
event_handler:PatternMatchingEventHandler
|
||||||
|
# The watchdog observer object
|
||||||
monitor:Observer
|
monitor:Observer
|
||||||
|
# The base monitored directory
|
||||||
base_dir:str
|
base_dir:str
|
||||||
|
# Config option, above which debug messages are ignored
|
||||||
debug_level:int
|
debug_level:int
|
||||||
|
# Where print messages are sent
|
||||||
_print_target:Any
|
_print_target:Any
|
||||||
|
#A lock to solve race conditions on '_patterns'
|
||||||
_patterns_lock:threading.Lock
|
_patterns_lock:threading.Lock
|
||||||
|
#A lock to solve race conditions on '_recipes'
|
||||||
_recipes_lock:threading.Lock
|
_recipes_lock:threading.Lock
|
||||||
|
#A lock to solve race conditions on '_rules'
|
||||||
_rules_lock:threading.Lock
|
_rules_lock:threading.Lock
|
||||||
|
|
||||||
def __init__(self, base_dir:str, patterns:dict[str,FileEventPattern],
|
def __init__(self, base_dir:str, patterns:dict[str,FileEventPattern],
|
||||||
recipes:dict[str,BaseRecipe], autostart=False, settletime:int=1,
|
recipes:dict[str,BaseRecipe], autostart=False, settletime:int=1,
|
||||||
print:Any=sys.stdout, logging:int=0)->None:
|
print:Any=sys.stdout, logging:int=0)->None:
|
||||||
|
"""WatchdogEventHandler Constructor. This uses the watchdog module to
|
||||||
|
monitor a directory and all its sub-directories. Watchdog will provide
|
||||||
|
the monitor with an caught events, with the monitor comparing them
|
||||||
|
against its rules, and informing the runner of match."""
|
||||||
super().__init__(patterns, recipes)
|
super().__init__(patterns, recipes)
|
||||||
self._is_valid_base_dir(base_dir)
|
self._is_valid_base_dir(base_dir)
|
||||||
self.base_dir = base_dir
|
self.base_dir = base_dir
|
||||||
@ -155,22 +194,28 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def start(self)->None:
|
def start(self)->None:
|
||||||
|
"""Function to start the monitor."""
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Starting WatchdogMonitor", DEBUG_INFO)
|
"Starting WatchdogMonitor", DEBUG_INFO)
|
||||||
self._apply_retroactive_rules()
|
self._apply_retroactive_rules()
|
||||||
self.monitor.start()
|
self.monitor.start()
|
||||||
|
|
||||||
def stop(self)->None:
|
def stop(self)->None:
|
||||||
|
"""Function to stop the monitor."""
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Stopping WatchdogMonitor", DEBUG_INFO)
|
"Stopping WatchdogMonitor", DEBUG_INFO)
|
||||||
self.monitor.stop()
|
self.monitor.stop()
|
||||||
|
|
||||||
def match(self, event)->None:
|
def match(self, event)->None:
|
||||||
|
"""Function to determine if a given event matches the current rules."""
|
||||||
src_path = event.src_path
|
src_path = event.src_path
|
||||||
event_type = "dir_"+ event.event_type if event.is_directory \
|
event_type = "dir_"+ event.event_type if event.is_directory \
|
||||||
else "file_" + event.event_type
|
else "file_" + event.event_type
|
||||||
|
|
||||||
|
# Remove the base dir from the path as trigger paths are given relative
|
||||||
|
# to that
|
||||||
handle_path = src_path.replace(self.base_dir, '', 1)
|
handle_path = src_path.replace(self.base_dir, '', 1)
|
||||||
|
# Also remove leading slashes, so we don't go off of the root directory
|
||||||
while handle_path.startswith(os.path.sep):
|
while handle_path.startswith(os.path.sep):
|
||||||
handle_path = handle_path[1:]
|
handle_path = handle_path[1:]
|
||||||
|
|
||||||
@ -178,15 +223,18 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
try:
|
try:
|
||||||
for rule in self._rules.values():
|
for rule in self._rules.values():
|
||||||
|
|
||||||
|
# Skip events not within the event mask
|
||||||
if event_type not in rule.pattern.event_mask:
|
if event_type not in rule.pattern.event_mask:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Use regex to match event paths against rule paths
|
||||||
target_path = rule.pattern.triggering_path
|
target_path = rule.pattern.triggering_path
|
||||||
recursive_regexp = translate(target_path)
|
recursive_regexp = translate(target_path)
|
||||||
direct_regexp = recursive_regexp.replace('.*', '[^/]*')
|
direct_regexp = recursive_regexp.replace('.*', '[^/]*')
|
||||||
recursive_hit = match(recursive_regexp, handle_path)
|
recursive_hit = match(recursive_regexp, handle_path)
|
||||||
direct_hit = match(direct_regexp, handle_path)
|
direct_hit = match(direct_regexp, handle_path)
|
||||||
|
|
||||||
|
# If matched, thte create a watchdog event
|
||||||
if direct_hit or recursive_hit:
|
if direct_hit or recursive_hit:
|
||||||
meow_event = create_event(
|
meow_event = create_event(
|
||||||
WATCHDOG_TYPE,
|
WATCHDOG_TYPE,
|
||||||
@ -203,6 +251,7 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
f"Event at {src_path} of type {event_type} hit rule "
|
f"Event at {src_path} of type {event_type} hit rule "
|
||||||
f"{rule.name}", DEBUG_INFO)
|
f"{rule.name}", DEBUG_INFO)
|
||||||
|
# Send the event to the runner
|
||||||
self.to_runner.send(meow_event)
|
self.to_runner.send(meow_event)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -212,6 +261,9 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._rules_lock.release()
|
self._rules_lock.release()
|
||||||
|
|
||||||
def add_pattern(self, pattern:FileEventPattern)->None:
|
def add_pattern(self, pattern:FileEventPattern)->None:
|
||||||
|
"""Function to add a pattern to the current definitions. Any rules
|
||||||
|
that can be possibly created from that pattern will be automatically
|
||||||
|
created."""
|
||||||
check_type(pattern, FileEventPattern)
|
check_type(pattern, FileEventPattern)
|
||||||
self._patterns_lock.acquire()
|
self._patterns_lock.acquire()
|
||||||
try:
|
try:
|
||||||
@ -227,12 +279,16 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._identify_new_rules(new_pattern=pattern)
|
self._identify_new_rules(new_pattern=pattern)
|
||||||
|
|
||||||
def update_pattern(self, pattern:FileEventPattern)->None:
|
def update_pattern(self, pattern:FileEventPattern)->None:
|
||||||
|
"""Function to update a pattern in the current definitions. Any rules
|
||||||
|
created from that pattern will be automatically updated."""
|
||||||
check_type(pattern, FileEventPattern)
|
check_type(pattern, FileEventPattern)
|
||||||
self.remove_pattern(pattern.name)
|
self.remove_pattern(pattern.name)
|
||||||
print(f"adding pattern w/ recipe {pattern.recipe}")
|
print(f"adding pattern w/ recipe {pattern.recipe}")
|
||||||
self.add_pattern(pattern)
|
self.add_pattern(pattern)
|
||||||
|
|
||||||
def remove_pattern(self, pattern: Union[str,FileEventPattern])->None:
|
def remove_pattern(self, pattern: Union[str,FileEventPattern])->None:
|
||||||
|
"""Function to remove a pattern from the current definitions. Any rules
|
||||||
|
that will be no longer valid will be automatically removed."""
|
||||||
check_type(pattern, str, alt_types=[FileEventPattern])
|
check_type(pattern, str, alt_types=[FileEventPattern])
|
||||||
lookup_key = pattern
|
lookup_key = pattern
|
||||||
if isinstance(lookup_key, FileEventPattern):
|
if isinstance(lookup_key, FileEventPattern):
|
||||||
@ -253,7 +309,10 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
else:
|
else:
|
||||||
self._identify_lost_rules(lost_pattern=pattern)
|
self._identify_lost_rules(lost_pattern=pattern)
|
||||||
|
|
||||||
def get_patterns(self)->None:
|
def get_patterns(self)->dict[str,FileEventPattern]:
|
||||||
|
"""Function to get a dict of the currently defined patterns of the
|
||||||
|
monitor. Note that the result is deep-copied, and so can be manipulated
|
||||||
|
without directly manipulating the internals of the monitor."""
|
||||||
to_return = {}
|
to_return = {}
|
||||||
self._patterns_lock.acquire()
|
self._patterns_lock.acquire()
|
||||||
try:
|
try:
|
||||||
@ -265,6 +324,9 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
return to_return
|
return to_return
|
||||||
|
|
||||||
def add_recipe(self, recipe: BaseRecipe)->None:
|
def add_recipe(self, recipe: BaseRecipe)->None:
|
||||||
|
"""Function to add a recipe to the current definitions. Any rules
|
||||||
|
that can be possibly created from that recipe will be automatically
|
||||||
|
created."""
|
||||||
check_type(recipe, BaseRecipe)
|
check_type(recipe, BaseRecipe)
|
||||||
self._recipes_lock.acquire()
|
self._recipes_lock.acquire()
|
||||||
try:
|
try:
|
||||||
@ -280,17 +342,22 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._identify_new_rules(new_recipe=recipe)
|
self._identify_new_rules(new_recipe=recipe)
|
||||||
|
|
||||||
def update_recipe(self, recipe: BaseRecipe)->None:
|
def update_recipe(self, recipe: BaseRecipe)->None:
|
||||||
|
"""Function to update a recipe in the current definitions. Any rules
|
||||||
|
created from that recipe will be automatically updated."""
|
||||||
check_type(recipe, BaseRecipe)
|
check_type(recipe, BaseRecipe)
|
||||||
self.remove_recipe(recipe.name)
|
self.remove_recipe(recipe.name)
|
||||||
self.add_recipe(recipe)
|
self.add_recipe(recipe)
|
||||||
|
|
||||||
def remove_recipe(self, recipe:Union[str,BaseRecipe])->None:
|
def remove_recipe(self, recipe:Union[str,BaseRecipe])->None:
|
||||||
|
"""Function to remove a recipe from the current definitions. Any rules
|
||||||
|
that will be no longer valid will be automatically removed."""
|
||||||
check_type(recipe, str, alt_types=[BaseRecipe])
|
check_type(recipe, str, alt_types=[BaseRecipe])
|
||||||
lookup_key = recipe
|
lookup_key = recipe
|
||||||
if isinstance(lookup_key, BaseRecipe):
|
if isinstance(lookup_key, BaseRecipe):
|
||||||
lookup_key = recipe.name
|
lookup_key = recipe.name
|
||||||
self._recipes_lock.acquire()
|
self._recipes_lock.acquire()
|
||||||
try:
|
try:
|
||||||
|
# Check that recipe has not already been deleted
|
||||||
if lookup_key not in self._recipes:
|
if lookup_key not in self._recipes:
|
||||||
raise KeyError(f"Cannot remote Recipe '{lookup_key}' as it "
|
raise KeyError(f"Cannot remote Recipe '{lookup_key}' as it "
|
||||||
"does not already exist")
|
"does not already exist")
|
||||||
@ -305,7 +372,10 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
else:
|
else:
|
||||||
self._identify_lost_rules(lost_recipe=recipe)
|
self._identify_lost_rules(lost_recipe=recipe)
|
||||||
|
|
||||||
def get_recipes(self)->None:
|
def get_recipes(self)->dict[str,BaseRecipe]:
|
||||||
|
"""Function to get a dict of the currently defined recipes of the
|
||||||
|
monitor. Note that the result is deep-copied, and so can be manipulated
|
||||||
|
without directly manipulating the internals of the monitor."""
|
||||||
to_return = {}
|
to_return = {}
|
||||||
self._recipes_lock.acquire()
|
self._recipes_lock.acquire()
|
||||||
try:
|
try:
|
||||||
@ -316,7 +386,10 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._recipes_lock.release()
|
self._recipes_lock.release()
|
||||||
return to_return
|
return to_return
|
||||||
|
|
||||||
def get_rules(self)->None:
|
def get_rules(self)->dict[str,BaseRule]:
|
||||||
|
"""Function to get a dict of the currently defined rules of the
|
||||||
|
monitor. Note that the result is deep-copied, and so can be manipulated
|
||||||
|
without directly manipulating the internals of the monitor."""
|
||||||
to_return = {}
|
to_return = {}
|
||||||
self._rules_lock.acquire()
|
self._rules_lock.acquire()
|
||||||
try:
|
try:
|
||||||
@ -329,15 +402,20 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
|
|
||||||
def _identify_new_rules(self, new_pattern:FileEventPattern=None,
|
def _identify_new_rules(self, new_pattern:FileEventPattern=None,
|
||||||
new_recipe:BaseRecipe=None)->None:
|
new_recipe:BaseRecipe=None)->None:
|
||||||
|
"""Function to determine if a new rule can be created given a new
|
||||||
|
pattern or recipe, in light of other existing patterns or recipes in
|
||||||
|
the monitor."""
|
||||||
|
|
||||||
if new_pattern:
|
if new_pattern:
|
||||||
self._patterns_lock.acquire()
|
self._patterns_lock.acquire()
|
||||||
self._recipes_lock.acquire()
|
self._recipes_lock.acquire()
|
||||||
try:
|
try:
|
||||||
|
# Check in case pattern has been deleted since function called
|
||||||
if new_pattern.name not in self._patterns:
|
if new_pattern.name not in self._patterns:
|
||||||
self._patterns_lock.release()
|
self._patterns_lock.release()
|
||||||
self._recipes_lock.release()
|
self._recipes_lock.release()
|
||||||
return
|
return
|
||||||
|
# If pattern specifies recipe that already exists, make a rule
|
||||||
if new_pattern.recipe in self._recipes:
|
if new_pattern.recipe in self._recipes:
|
||||||
self._create_new_rule(
|
self._create_new_rule(
|
||||||
new_pattern,
|
new_pattern,
|
||||||
@ -354,10 +432,12 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._patterns_lock.acquire()
|
self._patterns_lock.acquire()
|
||||||
self._recipes_lock.acquire()
|
self._recipes_lock.acquire()
|
||||||
try:
|
try:
|
||||||
|
# Check in case recipe has been deleted since function called
|
||||||
if new_recipe.name not in self._recipes:
|
if new_recipe.name not in self._recipes:
|
||||||
self._patterns_lock.release()
|
self._patterns_lock.release()
|
||||||
self._recipes_lock.release()
|
self._recipes_lock.release()
|
||||||
return
|
return
|
||||||
|
# If recipe is specified by existing pattern, make a rule
|
||||||
for pattern in self._patterns.values():
|
for pattern in self._patterns.values():
|
||||||
if pattern.recipe == new_recipe.name:
|
if pattern.recipe == new_recipe.name:
|
||||||
self._create_new_rule(
|
self._create_new_rule(
|
||||||
@ -373,14 +453,18 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
|
|
||||||
def _identify_lost_rules(self, lost_pattern:str=None,
|
def _identify_lost_rules(self, lost_pattern:str=None,
|
||||||
lost_recipe:str=None)->None:
|
lost_recipe:str=None)->None:
|
||||||
|
"""Function to remove rules that should be deleted in response to a
|
||||||
|
pattern or recipe having been deleted."""
|
||||||
to_delete = []
|
to_delete = []
|
||||||
self._rules_lock.acquire()
|
self._rules_lock.acquire()
|
||||||
try:
|
try:
|
||||||
|
# Identify any offending rules
|
||||||
for name, rule in self._rules.items():
|
for name, rule in self._rules.items():
|
||||||
if lost_pattern and rule.pattern.name == lost_pattern:
|
if lost_pattern and rule.pattern.name == lost_pattern:
|
||||||
to_delete.append(name)
|
to_delete.append(name)
|
||||||
if lost_recipe and rule.recipe.name == lost_recipe:
|
if lost_recipe and rule.recipe.name == lost_recipe:
|
||||||
to_delete.append(name)
|
to_delete.append(name)
|
||||||
|
# Now delete them
|
||||||
for delete in to_delete:
|
for delete in to_delete:
|
||||||
if delete in self._rules.keys():
|
if delete in self._rules.keys():
|
||||||
self._rules.pop(delete)
|
self._rules.pop(delete)
|
||||||
@ -389,7 +473,12 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
raise e
|
raise e
|
||||||
self._rules_lock.release()
|
self._rules_lock.release()
|
||||||
|
|
||||||
def _create_new_rule(self, pattern:FileEventPattern, recipe:BaseRecipe)->None:
|
def _create_new_rule(self, pattern:FileEventPattern,
|
||||||
|
recipe:BaseRecipe)->None:
|
||||||
|
"""Function to create a new rule from a given pattern and recipe. This
|
||||||
|
will only be called to create rules at runtime, as rules are
|
||||||
|
automatically created at initialisation using the same 'create_rule'
|
||||||
|
function called here."""
|
||||||
rule = create_rule(pattern, recipe)
|
rule = create_rule(pattern, recipe)
|
||||||
self._rules_lock.acquire()
|
self._rules_lock.acquire()
|
||||||
try:
|
try:
|
||||||
@ -405,31 +494,45 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
self._apply_retroactive_rule(rule)
|
self._apply_retroactive_rule(rule)
|
||||||
|
|
||||||
def _is_valid_base_dir(self, base_dir:str)->None:
|
def _is_valid_base_dir(self, base_dir:str)->None:
|
||||||
|
"""Validation check for 'base_dir' variable from main constructor. Is
|
||||||
|
automatically called during initialisation."""
|
||||||
valid_existing_dir_path(base_dir)
|
valid_existing_dir_path(base_dir)
|
||||||
|
|
||||||
def _is_valid_patterns(self, patterns:dict[str,FileEventPattern])->None:
|
def _is_valid_patterns(self, patterns:dict[str,FileEventPattern])->None:
|
||||||
|
"""Validation check for 'patterns' variable from main constructor. Is
|
||||||
|
automatically called during initialisation."""
|
||||||
valid_dict(patterns, str, FileEventPattern, min_length=0, strict=False)
|
valid_dict(patterns, str, FileEventPattern, min_length=0, strict=False)
|
||||||
|
|
||||||
def _is_valid_recipes(self, recipes:dict[str,BaseRecipe])->None:
|
def _is_valid_recipes(self, recipes:dict[str,BaseRecipe])->None:
|
||||||
|
"""Validation check for 'recipes' variable from main constructor. Is
|
||||||
|
automatically called during initialisation."""
|
||||||
valid_dict(recipes, str, BaseRecipe, min_length=0, strict=False)
|
valid_dict(recipes, str, BaseRecipe, min_length=0, strict=False)
|
||||||
|
|
||||||
def _apply_retroactive_rules(self)->None:
|
def _apply_retroactive_rules(self)->None:
|
||||||
|
"""Function to determine if any rules should be applied to the existing
|
||||||
|
file structure, were the file structure created/modified now."""
|
||||||
for rule in self._rules.values():
|
for rule in self._rules.values():
|
||||||
self._apply_retroactive_rule(rule)
|
self._apply_retroactive_rule(rule)
|
||||||
|
|
||||||
def _apply_retroactive_rule(self, rule:BaseRule)->None:
|
def _apply_retroactive_rule(self, rule:BaseRule)->None:
|
||||||
|
"""Function to determine if a rule should be applied to the existing
|
||||||
|
file structure, were the file structure created/modified now."""
|
||||||
self._rules_lock.acquire()
|
self._rules_lock.acquire()
|
||||||
try:
|
try:
|
||||||
|
# Check incase rule deleted since this function first called
|
||||||
if rule.name not in self._rules:
|
if rule.name not in self._rules:
|
||||||
self._rules_lock.release()
|
self._rules_lock.release()
|
||||||
return
|
return
|
||||||
|
|
||||||
if FILE_RETROACTIVE_EVENT in rule.pattern.event_mask:
|
if FILE_RETROACTIVE_EVENT in rule.pattern.event_mask:
|
||||||
|
# Determine what paths are potentially triggerable and gather
|
||||||
|
# files at those paths
|
||||||
testing_path = os.path.join(
|
testing_path = os.path.join(
|
||||||
self.base_dir, rule.pattern.triggering_path)
|
self.base_dir, rule.pattern.triggering_path)
|
||||||
|
|
||||||
globbed = glob.glob(testing_path)
|
globbed = glob.glob(testing_path)
|
||||||
|
|
||||||
|
# For each file create a fake event.
|
||||||
for globble in globbed:
|
for globble in globbed:
|
||||||
|
|
||||||
meow_event = create_event(
|
meow_event = create_event(
|
||||||
@ -440,6 +543,7 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
f"Retroactive event for file at at {globble} hit rule "
|
f"Retroactive event for file at at {globble} hit rule "
|
||||||
f"{rule.name}", DEBUG_INFO)
|
f"{rule.name}", DEBUG_INFO)
|
||||||
|
# Send it to the runner
|
||||||
self.to_runner.send(meow_event)
|
self.to_runner.send(meow_event)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -449,11 +553,19 @@ class WatchdogMonitor(BaseMonitor):
|
|||||||
|
|
||||||
|
|
||||||
class WatchdogEventHandler(PatternMatchingEventHandler):
|
class WatchdogEventHandler(PatternMatchingEventHandler):
|
||||||
|
# The monitor class running this handler
|
||||||
monitor:WatchdogMonitor
|
monitor:WatchdogMonitor
|
||||||
|
# A time to wait per event path, during which extra events are discared
|
||||||
_settletime:int
|
_settletime:int
|
||||||
|
# TODO clean this struct occasionally
|
||||||
|
# A dict of recent job timestamps
|
||||||
_recent_jobs:dict[str, Any]
|
_recent_jobs:dict[str, Any]
|
||||||
|
# A lock to solve race conditions on '_recent_jobs'
|
||||||
_recent_jobs_lock:threading.Lock
|
_recent_jobs_lock:threading.Lock
|
||||||
def __init__(self, monitor:WatchdogMonitor, settletime:int=1):
|
def __init__(self, monitor:WatchdogMonitor, settletime:int=1):
|
||||||
|
"""WatchdogEventHandler Constructor. This inherits from watchdog
|
||||||
|
PatternMatchingEventHandler, and is used to catch events, then filter
|
||||||
|
out excessive events at the same location."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.monitor = monitor
|
self.monitor = monitor
|
||||||
self._settletime = settletime
|
self._settletime = settletime
|
||||||
@ -461,12 +573,19 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
self._recent_jobs_lock = threading.Lock()
|
self._recent_jobs_lock = threading.Lock()
|
||||||
|
|
||||||
def threaded_handler(self, event):
|
def threaded_handler(self, event):
|
||||||
|
"""Function to determine if the given event shall be sent on to the
|
||||||
|
monitor. After each event we wait for '_settletime', to catch
|
||||||
|
subsequent events at the same location, so as to not swamp the system
|
||||||
|
with repeated events."""
|
||||||
self._recent_jobs_lock.acquire()
|
self._recent_jobs_lock.acquire()
|
||||||
try:
|
try:
|
||||||
if event.src_path in self._recent_jobs:
|
if event.src_path in self._recent_jobs:
|
||||||
recent_timestamp = self._recent_jobs[event.src_path]
|
recent_timestamp = self._recent_jobs[event.src_path]
|
||||||
difference = event.time_stamp - recent_timestamp
|
difference = event.time_stamp - recent_timestamp
|
||||||
|
|
||||||
|
# Discard the event if we already have a recent event at this
|
||||||
|
# same path. Update the most recent time, so we can hopefully
|
||||||
|
# wait till events have stopped happening
|
||||||
if difference <= self._settletime:
|
if difference <= self._settletime:
|
||||||
self._recent_jobs[event.src_path] = \
|
self._recent_jobs[event.src_path] = \
|
||||||
max(recent_timestamp, event.time_stamp)
|
max(recent_timestamp, event.time_stamp)
|
||||||
@ -481,9 +600,14 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
raise Exception(ex)
|
raise Exception(ex)
|
||||||
self._recent_jobs_lock.release()
|
self._recent_jobs_lock.release()
|
||||||
|
|
||||||
|
# If we did not have a recent event, then send it on to the monitor
|
||||||
self.monitor.match(event)
|
self.monitor.match(event)
|
||||||
|
|
||||||
def handle_event(self, event):
|
def handle_event(self, event):
|
||||||
|
"""Handler function, called by all specific event functions. Will
|
||||||
|
attach a timestamp to the event immediately, and attempt to start a
|
||||||
|
threaded_handler so that the monitor can resume monitoring as soon as
|
||||||
|
possible."""
|
||||||
event.time_stamp = time()
|
event.time_stamp = time()
|
||||||
|
|
||||||
waiting_for_threaded_resources = True
|
waiting_for_threaded_resources = True
|
||||||
@ -499,16 +623,21 @@ class WatchdogEventHandler(PatternMatchingEventHandler):
|
|||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
|
"""Function called when a file created event occurs."""
|
||||||
self.handle_event(event)
|
self.handle_event(event)
|
||||||
|
|
||||||
def on_modified(self, event):
|
def on_modified(self, event):
|
||||||
|
"""Function called when a file modified event occurs."""
|
||||||
self.handle_event(event)
|
self.handle_event(event)
|
||||||
|
|
||||||
def on_moved(self, event):
|
def on_moved(self, event):
|
||||||
|
"""Function called when a file moved event occurs."""
|
||||||
self.handle_event(event)
|
self.handle_event(event)
|
||||||
|
|
||||||
def on_deleted(self, event):
|
def on_deleted(self, event):
|
||||||
|
"""Function called when a file deleted event occurs."""
|
||||||
self.handle_event(event)
|
self.handle_event(event)
|
||||||
|
|
||||||
def on_closed(self, event):
|
def on_closed(self, event):
|
||||||
|
"""Function called when a file closed event occurs."""
|
||||||
self.handle_event(event)
|
self.handle_event(event)
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
This file contains definitions for a MEOW recipe based off of jupyter notebooks,
|
||||||
|
along with an appropriate handler for said events.
|
||||||
|
|
||||||
|
Author(s): David Marchant
|
||||||
|
"""
|
||||||
import nbformat
|
import nbformat
|
||||||
import sys
|
import sys
|
||||||
import threading
|
|
||||||
|
|
||||||
from multiprocessing import Pipe
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from core.correctness.validation import check_type, valid_string, \
|
from core.correctness.validation import check_type, valid_string, \
|
||||||
valid_dict, valid_path, valid_list, valid_existing_dir_path, \
|
valid_dict, valid_path, valid_existing_dir_path, setup_debugging
|
||||||
setup_debugging
|
from core.correctness.vars import VALID_VARIABLE_NAME_CHARS, PYTHON_FUNC, \
|
||||||
from core.correctness.vars import VALID_VARIABLE_NAME_CHARS, VALID_CHANNELS, \
|
DEBUG_INFO, WATCHDOG_TYPE, JOB_HASH, PYTHON_EXECUTION_BASE, \
|
||||||
PYTHON_FUNC, DEBUG_INFO, WATCHDOG_TYPE, JOB_HASH, PYTHON_EXECUTION_BASE, \
|
|
||||||
WATCHDOG_RULE, EVENT_PATH, PYTHON_TYPE, WATCHDOG_HASH, JOB_PARAMETERS, \
|
WATCHDOG_RULE, EVENT_PATH, PYTHON_TYPE, WATCHDOG_HASH, JOB_PARAMETERS, \
|
||||||
PYTHON_OUTPUT_DIR
|
PYTHON_OUTPUT_DIR
|
||||||
from core.functionality import print_debug, create_job, replace_keywords
|
from core.functionality import print_debug, create_job, replace_keywords
|
||||||
@ -19,57 +22,75 @@ from patterns.file_event_pattern import SWEEP_START, SWEEP_STOP, SWEEP_JUMP
|
|||||||
|
|
||||||
|
|
||||||
class JupyterNotebookRecipe(BaseRecipe):
|
class JupyterNotebookRecipe(BaseRecipe):
|
||||||
|
# A path to the jupyter notebook used to create this recipe
|
||||||
source:str
|
source:str
|
||||||
def __init__(self, name:str, recipe:Any, parameters:dict[str,Any]={},
|
def __init__(self, name:str, recipe:Any, parameters:dict[str,Any]={},
|
||||||
requirements:dict[str,Any]={}, source:str=""):
|
requirements:dict[str,Any]={}, source:str=""):
|
||||||
|
"""JupyterNotebookRecipe Constructor. This is used to execute analysis
|
||||||
|
code using the papermill module."""
|
||||||
super().__init__(name, recipe, parameters, requirements)
|
super().__init__(name, recipe, parameters, requirements)
|
||||||
self._is_valid_source(source)
|
self._is_valid_source(source)
|
||||||
self.source = source
|
self.source = source
|
||||||
|
|
||||||
def _is_valid_source(self, source:str)->None:
|
def _is_valid_source(self, source:str)->None:
|
||||||
|
"""Validation check for 'source' variable from main constructor."""
|
||||||
if source:
|
if source:
|
||||||
valid_path(source, extension=".ipynb", min_length=0)
|
valid_path(source, extension=".ipynb", min_length=0)
|
||||||
|
|
||||||
def _is_valid_recipe(self, recipe:dict[str,Any])->None:
|
def _is_valid_recipe(self, recipe:dict[str,Any])->None:
|
||||||
|
"""Validation check for 'recipe' variable from main constructor.
|
||||||
|
Called within parent BaseRecipe constructor."""
|
||||||
check_type(recipe, dict)
|
check_type(recipe, dict)
|
||||||
nbformat.validate(recipe)
|
nbformat.validate(recipe)
|
||||||
|
|
||||||
def _is_valid_parameters(self, parameters:dict[str,Any])->None:
|
def _is_valid_parameters(self, parameters:dict[str,Any])->None:
|
||||||
|
"""Validation check for 'parameters' variable from main constructor.
|
||||||
|
Called within parent BaseRecipe constructor."""
|
||||||
valid_dict(parameters, str, Any, strict=False, min_length=0)
|
valid_dict(parameters, str, Any, strict=False, min_length=0)
|
||||||
for k in parameters.keys():
|
for k in parameters.keys():
|
||||||
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
||||||
|
|
||||||
def _is_valid_requirements(self, requirements:dict[str,Any])->None:
|
def _is_valid_requirements(self, requirements:dict[str,Any])->None:
|
||||||
|
"""Validation check for 'requirements' variable from main constructor.
|
||||||
|
Called within parent BaseRecipe constructor."""
|
||||||
valid_dict(requirements, str, Any, strict=False, min_length=0)
|
valid_dict(requirements, str, Any, strict=False, min_length=0)
|
||||||
for k in requirements.keys():
|
for k in requirements.keys():
|
||||||
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
||||||
|
|
||||||
class PapermillHandler(BaseHandler):
|
class PapermillHandler(BaseHandler):
|
||||||
|
# TODO move me to conductor
|
||||||
|
# Execution directory
|
||||||
handler_base:str
|
handler_base:str
|
||||||
|
# TODO possibly move me also to conductor?
|
||||||
|
# Final location for job output to be placed
|
||||||
output_dir:str
|
output_dir:str
|
||||||
|
# Config option, above which debug messages are ignored
|
||||||
debug_level:int
|
debug_level:int
|
||||||
_worker:threading.Thread
|
# Where print messages are sent
|
||||||
_stop_pipe:Pipe
|
|
||||||
_print_target:Any
|
_print_target:Any
|
||||||
def __init__(self, handler_base:str, output_dir:str, print:Any=sys.stdout,
|
def __init__(self, handler_base:str, output_dir:str, print:Any=sys.stdout,
|
||||||
logging:int=0)->None:
|
logging:int=0)->None:
|
||||||
|
"""PapermillHandler Constructor. This creats jobs to be executed using
|
||||||
|
the papermill module. This does not run as a continuous thread to
|
||||||
|
handle execution, but is invoked according to a factory pattern using
|
||||||
|
the handle function."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._is_valid_handler_base(handler_base)
|
self._is_valid_handler_base(handler_base)
|
||||||
self.handler_base = handler_base
|
self.handler_base = handler_base
|
||||||
self._is_valid_output_dir(output_dir)
|
self._is_valid_output_dir(output_dir)
|
||||||
self.output_dir = output_dir
|
self.output_dir = output_dir
|
||||||
self._print_target, self.debug_level = setup_debugging(print, logging)
|
self._print_target, self.debug_level = setup_debugging(print, logging)
|
||||||
self._worker = None
|
|
||||||
self._stop_pipe = Pipe()
|
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
"Created new PapermillHandler instance", DEBUG_INFO)
|
"Created new PapermillHandler instance", DEBUG_INFO)
|
||||||
|
|
||||||
def handle(self, event:dict[str,Any])->None:
|
def handle(self, event:dict[str,Any])->None:
|
||||||
|
"""Function called to handle a given event."""
|
||||||
print_debug(self._print_target, self.debug_level,
|
print_debug(self._print_target, self.debug_level,
|
||||||
f"Handling event {event[EVENT_PATH]}", DEBUG_INFO)
|
f"Handling event {event[EVENT_PATH]}", DEBUG_INFO)
|
||||||
|
|
||||||
rule = event[WATCHDOG_RULE]
|
rule = event[WATCHDOG_RULE]
|
||||||
|
|
||||||
|
# Assemble job parameters dict from pattern variables
|
||||||
yaml_dict = {}
|
yaml_dict = {}
|
||||||
for var, val in rule.pattern.parameters.items():
|
for var, val in rule.pattern.parameters.items():
|
||||||
yaml_dict[var] = val
|
yaml_dict[var] = val
|
||||||
@ -77,9 +98,11 @@ class PapermillHandler(BaseHandler):
|
|||||||
yaml_dict[var] = val
|
yaml_dict[var] = val
|
||||||
yaml_dict[rule.pattern.triggering_file] = event[EVENT_PATH]
|
yaml_dict[rule.pattern.triggering_file] = event[EVENT_PATH]
|
||||||
|
|
||||||
|
# If no parameter sweeps, then one job will suffice
|
||||||
if not rule.pattern.sweep:
|
if not rule.pattern.sweep:
|
||||||
self.setup_job(event, yaml_dict)
|
self.setup_job(event, yaml_dict)
|
||||||
else:
|
else:
|
||||||
|
# If parameter sweeps, then many jobs created
|
||||||
for var, val in rule.pattern.sweep.items():
|
for var, val in rule.pattern.sweep.items():
|
||||||
values = []
|
values = []
|
||||||
par_val = rule.pattern.sweep[SWEEP_START]
|
par_val = rule.pattern.sweep[SWEEP_START]
|
||||||
@ -92,18 +115,25 @@ class PapermillHandler(BaseHandler):
|
|||||||
self.setup_job(event, yaml_dict)
|
self.setup_job(event, yaml_dict)
|
||||||
|
|
||||||
def valid_event_types(self)->list[str]:
|
def valid_event_types(self)->list[str]:
|
||||||
|
"""Function to provide a list of the types of events this handler can
|
||||||
|
process."""
|
||||||
return [WATCHDOG_TYPE]
|
return [WATCHDOG_TYPE]
|
||||||
|
|
||||||
def _is_valid_inputs(self, inputs:list[VALID_CHANNELS])->None:
|
|
||||||
valid_list(inputs, VALID_CHANNELS)
|
|
||||||
|
|
||||||
def _is_valid_handler_base(self, handler_base)->None:
|
def _is_valid_handler_base(self, handler_base)->None:
|
||||||
|
"""Validation check for 'handler_base' variable from main
|
||||||
|
constructor."""
|
||||||
valid_existing_dir_path(handler_base)
|
valid_existing_dir_path(handler_base)
|
||||||
|
|
||||||
def _is_valid_output_dir(self, output_dir)->None:
|
def _is_valid_output_dir(self, output_dir)->None:
|
||||||
|
"""Validation check for 'output_dir' variable from main
|
||||||
|
constructor."""
|
||||||
valid_existing_dir_path(output_dir, allow_base=True)
|
valid_existing_dir_path(output_dir, allow_base=True)
|
||||||
|
|
||||||
def setup_job(self, event:dict[str,Any], yaml_dict:dict[str,Any])->None:
|
def setup_job(self, event:dict[str,Any], yaml_dict:dict[str,Any])->None:
|
||||||
|
"""Function to set up new job dict and send it to the runner to be
|
||||||
|
executed."""
|
||||||
|
# TODO edit me to write job files to a local store in handler, then
|
||||||
|
# read those files within conductor
|
||||||
meow_job = create_job(PYTHON_TYPE, event, {
|
meow_job = create_job(PYTHON_TYPE, event, {
|
||||||
JOB_PARAMETERS:yaml_dict,
|
JOB_PARAMETERS:yaml_dict,
|
||||||
JOB_HASH: event[WATCHDOG_HASH],
|
JOB_HASH: event[WATCHDOG_HASH],
|
||||||
@ -115,7 +145,9 @@ class PapermillHandler(BaseHandler):
|
|||||||
f"{PYTHON_TYPE}.", DEBUG_INFO)
|
f"{PYTHON_TYPE}.", DEBUG_INFO)
|
||||||
self.to_runner.send(meow_job)
|
self.to_runner.send(meow_job)
|
||||||
|
|
||||||
|
# Papermill job execution code, to be run within the conductor
|
||||||
def job_func(job):
|
def job_func(job):
|
||||||
|
# Requires own imports as will be run in its own execution environment
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import papermill
|
import papermill
|
||||||
@ -131,6 +163,7 @@ def job_func(job):
|
|||||||
|
|
||||||
event = job[JOB_EVENT]
|
event = job[JOB_EVENT]
|
||||||
|
|
||||||
|
# replace MEOW keyworks within variables dict
|
||||||
yaml_dict = replace_keywords(
|
yaml_dict = replace_keywords(
|
||||||
job[JOB_PARAMETERS],
|
job[JOB_PARAMETERS],
|
||||||
job[JOB_ID],
|
job[JOB_ID],
|
||||||
@ -138,15 +171,19 @@ def job_func(job):
|
|||||||
event[WATCHDOG_BASE]
|
event[WATCHDOG_BASE]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create a base job directory
|
||||||
job_dir = os.path.join(job[PYTHON_EXECUTION_BASE], job[JOB_ID])
|
job_dir = os.path.join(job[PYTHON_EXECUTION_BASE], job[JOB_ID])
|
||||||
make_dir(job_dir)
|
make_dir(job_dir)
|
||||||
|
|
||||||
|
# write a status file to the job directory
|
||||||
meta_file = os.path.join(job_dir, META_FILE)
|
meta_file = os.path.join(job_dir, META_FILE)
|
||||||
write_yaml(job, meta_file)
|
write_yaml(job, meta_file)
|
||||||
|
|
||||||
|
# write an executable notebook to the job directory
|
||||||
base_file = os.path.join(job_dir, BASE_FILE)
|
base_file = os.path.join(job_dir, BASE_FILE)
|
||||||
write_notebook(event[WATCHDOG_RULE].recipe.recipe, base_file)
|
write_notebook(event[WATCHDOG_RULE].recipe.recipe, base_file)
|
||||||
|
|
||||||
|
# write a parameter file to the job directory
|
||||||
param_file = os.path.join(job_dir, PARAMS_FILE)
|
param_file = os.path.join(job_dir, PARAMS_FILE)
|
||||||
write_yaml(yaml_dict, param_file)
|
write_yaml(yaml_dict, param_file)
|
||||||
|
|
||||||
@ -156,10 +193,17 @@ def job_func(job):
|
|||||||
job[JOB_STATUS] = STATUS_RUNNING
|
job[JOB_STATUS] = STATUS_RUNNING
|
||||||
job[JOB_START_TIME] = datetime.now()
|
job[JOB_START_TIME] = datetime.now()
|
||||||
|
|
||||||
|
# update the status file with running status
|
||||||
write_yaml(job, meta_file)
|
write_yaml(job, meta_file)
|
||||||
|
|
||||||
|
# Check the hash of the triggering file, if present. This addresses
|
||||||
|
# potential race condition as file could have been modified since
|
||||||
|
# triggering event
|
||||||
if JOB_HASH in job:
|
if JOB_HASH in job:
|
||||||
|
# get current hash
|
||||||
triggerfile_hash = get_file_hash(job[JOB_EVENT][EVENT_PATH], SHA256)
|
triggerfile_hash = get_file_hash(job[JOB_EVENT][EVENT_PATH], SHA256)
|
||||||
|
# If hash doesn't match, then abort the job. If its been modified, then
|
||||||
|
# another job will have been scheduled anyway.
|
||||||
if not triggerfile_hash \
|
if not triggerfile_hash \
|
||||||
or triggerfile_hash != job[JOB_HASH]:
|
or triggerfile_hash != job[JOB_HASH]:
|
||||||
job[JOB_STATUS] = STATUS_SKIPPED
|
job[JOB_STATUS] = STATUS_SKIPPED
|
||||||
@ -172,6 +216,7 @@ def job_func(job):
|
|||||||
write_yaml(job, meta_file)
|
write_yaml(job, meta_file)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Create a parameterised version of the executable notebook
|
||||||
try:
|
try:
|
||||||
job_notebook = parameterize_jupyter_notebook(
|
job_notebook = parameterize_jupyter_notebook(
|
||||||
event[WATCHDOG_RULE].recipe.recipe, yaml_dict
|
event[WATCHDOG_RULE].recipe.recipe, yaml_dict
|
||||||
@ -185,6 +230,7 @@ def job_func(job):
|
|||||||
write_yaml(job, meta_file)
|
write_yaml(job, meta_file)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Execute the parameterised notebook
|
||||||
try:
|
try:
|
||||||
papermill.execute_notebook(job_file, result_file, {})
|
papermill.execute_notebook(job_file, result_file, {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -195,10 +241,12 @@ def job_func(job):
|
|||||||
write_yaml(job, meta_file)
|
write_yaml(job, meta_file)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Update the status file with the finalised status
|
||||||
job[JOB_STATUS] = STATUS_DONE
|
job[JOB_STATUS] = STATUS_DONE
|
||||||
job[JOB_END_TIME] = datetime.now()
|
job[JOB_END_TIME] = datetime.now()
|
||||||
write_yaml(job, meta_file)
|
write_yaml(job, meta_file)
|
||||||
|
|
||||||
|
# Move the contents of the execution directory to the final output
|
||||||
|
# directory.
|
||||||
job_output_dir = os.path.join(job[PYTHON_OUTPUT_DIR], job[JOB_ID])
|
job_output_dir = os.path.join(job[PYTHON_OUTPUT_DIR], job[JOB_ID])
|
||||||
|
|
||||||
shutil.move(job_dir, job_output_dir)
|
shutil.move(job_dir, job_output_dir)
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
This file contains definitions for a MEOW rule connecting the FileEventPattern
|
||||||
|
and JupyterNotebookRecipe.
|
||||||
|
|
||||||
|
Author(s): David Marchant
|
||||||
|
"""
|
||||||
from core.correctness.validation import check_type
|
from core.correctness.validation import check_type
|
||||||
from core.meow import BaseRule
|
from core.meow import BaseRule
|
||||||
from patterns.file_event_pattern import FileEventPattern
|
from patterns.file_event_pattern import FileEventPattern
|
||||||
from recipes.jupyter_notebook_recipe import JupyterNotebookRecipe
|
from recipes.jupyter_notebook_recipe import JupyterNotebookRecipe
|
||||||
|
|
||||||
|
# TODO potentailly remove this and just invoke BaseRule directly, as does not
|
||||||
|
# add any functionality other than some validation.
|
||||||
class FileEventJupyterNotebookRule(BaseRule):
|
class FileEventJupyterNotebookRule(BaseRule):
|
||||||
pattern_type = "FileEventPattern"
|
pattern_type = "FileEventPattern"
|
||||||
recipe_type = "JupyterNotebookRecipe"
|
recipe_type = "JupyterNotebookRecipe"
|
||||||
@ -16,7 +24,11 @@ class FileEventJupyterNotebookRule(BaseRule):
|
|||||||
f"uses {pattern.recipe}")
|
f"uses {pattern.recipe}")
|
||||||
|
|
||||||
def _is_valid_pattern(self, pattern:FileEventPattern)->None:
|
def _is_valid_pattern(self, pattern:FileEventPattern)->None:
|
||||||
|
"""Validation check for 'pattern' variable from main constructor. Is
|
||||||
|
automatically called during initialisation."""
|
||||||
check_type(pattern, FileEventPattern)
|
check_type(pattern, FileEventPattern)
|
||||||
|
|
||||||
def _is_valid_recipe(self, recipe:JupyterNotebookRecipe)->None:
|
def _is_valid_recipe(self, recipe:JupyterNotebookRecipe)->None:
|
||||||
|
"""Validation check for 'recipe' variable from main constructor. Is
|
||||||
|
automatically called during initialisation."""
|
||||||
check_type(recipe, JupyterNotebookRecipe)
|
check_type(recipe, JupyterNotebookRecipe)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
|
# A script to run all tests. This will automatically move to the tests
|
||||||
|
# directory and call all other files as pytest scripts
|
||||||
|
|
||||||
# Need to more to local dir to run tests
|
# Need to more to local dir to run tests
|
||||||
starting_working_dir=$(pwd)
|
starting_working_dir=$(pwd)
|
||||||
|
@ -200,7 +200,7 @@ class MeowTests(unittest.TestCase):
|
|||||||
FullTestHandler()
|
FullTestHandler()
|
||||||
|
|
||||||
def testBaseConductor(self)->None:
|
def testBaseConductor(self)->None:
|
||||||
with self.assertRaises(NotImplementedError):
|
with self.assertRaises(TypeError):
|
||||||
BaseConductor()
|
BaseConductor()
|
||||||
|
|
||||||
class TestConductor(BaseConductor):
|
class TestConductor(BaseConductor):
|
||||||
|
Reference in New Issue
Block a user