added comments throughout
This commit is contained in:
@ -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 inspect import signature
|
||||
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, \
|
||||
JOB_RECIPE, JOB_RULE, JOB_STATUS, JOB_CREATE_TIME
|
||||
|
||||
# Required keys in event dict
|
||||
EVENT_KEYS = {
|
||||
EVENT_TYPE: str,
|
||||
EVENT_PATH: str
|
||||
}
|
||||
|
||||
# Required keys in job dict
|
||||
JOB_KEYS = {
|
||||
JOB_TYPE: str,
|
||||
JOB_EVENT: dict,
|
||||
@ -26,28 +34,16 @@ JOB_KEYS = {
|
||||
|
||||
def check_type(variable:Any, expected_type:type, alt_types:list[type]=[],
|
||||
or_none:bool=False)->None:
|
||||
"""
|
||||
Checks if a given variable is of the expected type. Raises TypeError or
|
||||
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.
|
||||
"""
|
||||
"""Checks if a given variable is of the expected type. Raises TypeError or
|
||||
ValueError as appropriate if any issues are encountered."""
|
||||
|
||||
# Get a list of all allowed types
|
||||
type_list = [expected_type]
|
||||
if get_origin(expected_type) is Union:
|
||||
type_list = list(get_args(expected_type))
|
||||
type_list = type_list + alt_types
|
||||
|
||||
# Only accept None if explicitly allowed
|
||||
if variable is None:
|
||||
if or_none == False:
|
||||
raise TypeError(
|
||||
@ -56,55 +52,53 @@ def check_type(variable:Any, expected_type:type, alt_types:list[type]=[],
|
||||
else:
|
||||
return
|
||||
|
||||
# If any type is allowed, then we can stop checking
|
||||
if expected_type == Any:
|
||||
return
|
||||
|
||||
# Check that variable type is within the accepted type list
|
||||
if not isinstance(variable, tuple(type_list)):
|
||||
print("egh")
|
||||
|
||||
raise TypeError(
|
||||
'Expected type(s) are %s, got %s'
|
||||
% (get_args(expected_type), type(variable))
|
||||
)
|
||||
|
||||
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__):
|
||||
raise AttributeError(
|
||||
f"Parent class {parent_class} does not implement base function "
|
||||
f"{child_func.__name__} for children to override.")
|
||||
parent_func = getattr(parent_class, child_func.__name__)
|
||||
|
||||
# Check child implements function with correct name
|
||||
if (child_func == parent_func):
|
||||
msg = get_not_imp_msg(parent_class, parent_func)
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
# Check that child implements function with correct signature
|
||||
child_sig = signature(child_func).parameters
|
||||
parent_sig = signature(parent_func).parameters
|
||||
|
||||
if child_sig.keys() != parent_sig.keys():
|
||||
msg = get_not_imp_msg(parent_class, parent_func)
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
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
|
||||
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.
|
||||
"""
|
||||
encountered."""
|
||||
check_type(variable, str)
|
||||
check_type(valid_chars, str)
|
||||
|
||||
# Check string is long enough
|
||||
if len(variable) < min_length:
|
||||
raise ValueError (
|
||||
f"String '{variable}' is too short. Minimum length is {min_length}"
|
||||
)
|
||||
|
||||
# Check each char is acceptable
|
||||
for char in variable:
|
||||
if char not in valid_chars:
|
||||
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,
|
||||
required_keys:list[Any]=[], optional_keys:list[Any]=[],
|
||||
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(key_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(strict, bool)
|
||||
|
||||
# Check dict meets minimum length
|
||||
if len(variable) < min_length:
|
||||
raise ValueError(f"Dictionary '{variable}' is below minimum length of "
|
||||
f"{min_length}")
|
||||
|
||||
# Check key and value types
|
||||
for k, v in variable.items():
|
||||
if key_type != Any and not isinstance(k, key_type):
|
||||
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)}' "
|
||||
f"rather than expected '{value_type}' in dict '{variable}'")
|
||||
|
||||
# Check all required keys present
|
||||
for rk in required_keys:
|
||||
if rk not in variable.keys():
|
||||
raise KeyError(f"Missing required key '{rk}' from dict "
|
||||
f"'{variable}'")
|
||||
|
||||
# If strict checking, enforce that only required and optional keys are
|
||||
# present
|
||||
if strict:
|
||||
for k in variable.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,
|
||||
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 length meets minimum
|
||||
if len(variable) < min_length:
|
||||
raise ValueError(f"List '{variable}' is too short. Should be at least "
|
||||
f"of length {min_length}")
|
||||
|
||||
# Check type of each value
|
||||
for entry in variable:
|
||||
check_type(entry, entry_type, alt_types=alt_types)
|
||||
|
||||
def valid_path(variable:str, allow_base:bool=False, extension:str="",
|
||||
min_length:int=1):
|
||||
"""Check that a given string expresses a valid path."""
|
||||
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):
|
||||
raise ValueError(f"Cannot accept path '{variable}'. Must be relative.")
|
||||
|
||||
# Check path contains a valid extension
|
||||
if extension and not variable.endswith(extension):
|
||||
raise ValueError(f"Path '{variable}' does not have required "
|
||||
f"extension '{extension}'.")
|
||||
|
||||
def valid_existing_file_path(variable:str, allow_base:bool=False,
|
||||
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)
|
||||
# Check the path exists
|
||||
if not exists(variable):
|
||||
raise FileNotFoundError(
|
||||
f"Requested file path '{variable}' does not exist.")
|
||||
# Check it is a file
|
||||
if not isfile(variable):
|
||||
raise ValueError(
|
||||
f"Requested file '{variable}' is not a file.")
|
||||
|
||||
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="")
|
||||
# Check the path exists
|
||||
if not exists(variable):
|
||||
raise FileNotFoundError(
|
||||
f"Requested dir path '{variable}' does not exist.")
|
||||
# Check it is a directory
|
||||
if not isdir(variable):
|
||||
raise ValueError(
|
||||
f"Requested dir '{variable}' is not a directory.")
|
||||
|
||||
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="")
|
||||
# Check the path does not exist
|
||||
if exists(variable):
|
||||
raise ValueError(f"Requested path '{variable}' already exists.")
|
||||
# Check that any intermediate directories exist
|
||||
if dirname(variable) and not exists(dirname(variable)):
|
||||
raise ValueError(
|
||||
f"Route to requested path '{variable}' does not exist.")
|
||||
|
||||
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)
|
||||
if print is None:
|
||||
return None, 0
|
||||
@ -204,15 +232,22 @@ def setup_debugging(print:Any=None, logging:int=0)->Tuple[Any,int]:
|
||||
|
||||
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 we have all the required keys, and they are all of the expected
|
||||
# type
|
||||
for key, value_type in keys.items():
|
||||
if not key in meow_dict.keys():
|
||||
raise KeyError(f"{msg} require key '{key}'")
|
||||
check_type(meow_dict[key], value_type)
|
||||
|
||||
def valid_event(event:dict[str,Any])->None:
|
||||
"""Check that a given dict expresses a meow event."""
|
||||
valid_meow_dict(event, "Event", EVENT_KEYS)
|
||||
|
||||
def valid_job(job:dict[str,Any])->None:
|
||||
"""Check that a given dict expresses a meow job."""
|
||||
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
|
||||
|
||||
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 sys
|
||||
|
||||
@ -14,12 +22,19 @@ from core.functionality import generate_id
|
||||
|
||||
|
||||
class BaseRecipe:
|
||||
# A unique identifier for the recipe
|
||||
name:str
|
||||
# Actual code to run
|
||||
recipe:Any
|
||||
# Possible parameters that could be overridden by a Pattern
|
||||
parameters:dict[str, Any]
|
||||
# Additional configuration options
|
||||
requirements:dict[str, Any]
|
||||
def __init__(self, name:str, recipe:Any, parameters: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_parameters, BaseRecipe)
|
||||
check_implementation(type(self)._is_valid_requirements, BaseRecipe)
|
||||
@ -33,31 +48,49 @@ class BaseRecipe:
|
||||
self.requirements = requirements
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""A check that this base class is not instantiated itself, only
|
||||
inherited from"""
|
||||
if cls is BaseRecipe:
|
||||
msg = get_drt_imp_msg(BaseRecipe)
|
||||
raise TypeError(msg)
|
||||
return object.__new__(cls)
|
||||
|
||||
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)
|
||||
|
||||
def _is_valid_recipe(self, recipe:Any)->None:
|
||||
"""Validation check for 'recipe' variable from main constructor. Must
|
||||
be implemented by any child class."""
|
||||
pass
|
||||
|
||||
def _is_valid_parameters(self, parameters:Any)->None:
|
||||
"""Validation check for 'parameters' variable from main constructor.
|
||||
Must be implemented by any child class."""
|
||||
pass
|
||||
|
||||
def _is_valid_requirements(self, requirements:Any)->None:
|
||||
"""Validation check for 'requirements' variable from main constructor.
|
||||
Must be implemented by any child class."""
|
||||
pass
|
||||
|
||||
|
||||
class BasePattern:
|
||||
# A unique identifier for the pattern
|
||||
name:str
|
||||
# An identifier of a recipe
|
||||
recipe:str
|
||||
# Parameters to be overridden in the recipe
|
||||
parameters:dict[str,Any]
|
||||
# Parameters showing the potential outputs of a recipe
|
||||
outputs:dict[str,Any]
|
||||
def __init__(self, name:str, recipe:str, parameters: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_parameters, BasePattern)
|
||||
check_implementation(type(self)._is_valid_output, BasePattern)
|
||||
@ -71,31 +104,50 @@ class BasePattern:
|
||||
self.outputs = outputs
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""A check that this base class is not instantiated itself, only
|
||||
inherited from"""
|
||||
if cls is BasePattern:
|
||||
msg = get_drt_imp_msg(BasePattern)
|
||||
raise TypeError(msg)
|
||||
return object.__new__(cls)
|
||||
|
||||
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)
|
||||
|
||||
def _is_valid_recipe(self, recipe:Any)->None:
|
||||
"""Validation check for 'recipe' variable from main constructor. Must
|
||||
be implemented by any child class."""
|
||||
pass
|
||||
|
||||
def _is_valid_parameters(self, parameters:Any)->None:
|
||||
"""Validation check for 'parameters' variable from main constructor.
|
||||
Must be implemented by any child class."""
|
||||
pass
|
||||
|
||||
def _is_valid_output(self, outputs:Any)->None:
|
||||
"""Validation check for 'outputs' variable from main constructor. Must
|
||||
be implemented by any child class."""
|
||||
pass
|
||||
|
||||
|
||||
class BaseRule:
|
||||
# A unique identifier for the rule
|
||||
name:str
|
||||
# A pattern to be used in rule triggering
|
||||
pattern:BasePattern
|
||||
# A recipe to be used in rule execution
|
||||
recipe:BaseRecipe
|
||||
# The string name of the pattern class that can be used to create this rule
|
||||
pattern_type:str=""
|
||||
# The string name of the recipe class that can be used to create this rule
|
||||
recipe_type:str=""
|
||||
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_recipe, BaseRule)
|
||||
self._is_valid_name(name)
|
||||
@ -107,21 +159,32 @@ class BaseRule:
|
||||
self.__check_types_set()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""A check that this base class is not instantiated itself, only
|
||||
inherited from"""
|
||||
if cls is BaseRule:
|
||||
msg = get_drt_imp_msg(BaseRule)
|
||||
raise TypeError(msg)
|
||||
return object.__new__(cls)
|
||||
|
||||
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)
|
||||
|
||||
def _is_valid_pattern(self, pattern:Any)->None:
|
||||
"""Validation check for 'pattern' variable from main constructor. Must
|
||||
be implemented by any child class."""
|
||||
pass
|
||||
|
||||
def _is_valid_recipe(self, recipe:Any)->None:
|
||||
"""Validation check for 'recipe' variable from main constructor. Must
|
||||
be implemented by any child class."""
|
||||
pass
|
||||
|
||||
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 == "":
|
||||
raise AttributeError(f"Rule Class '{self.__class__.__name__}' "
|
||||
"does not set a pattern_type.")
|
||||
@ -131,11 +194,21 @@ class BaseRule:
|
||||
|
||||
|
||||
class BaseMonitor:
|
||||
# A collection of patterns
|
||||
_patterns: dict[str, BasePattern]
|
||||
# A collection of recipes
|
||||
_recipes: dict[str, BaseRecipe]
|
||||
# A collection of rules derived from _patterns and _recipes
|
||||
_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
|
||||
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).stop, 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).get_recipes, 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._recipes = deepcopy(recipes)
|
||||
self._rules = create_rules(patterns, recipes)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""A check that this base class is not instantiated itself, only
|
||||
inherited from"""
|
||||
if cls is BaseMonitor:
|
||||
msg = get_drt_imp_msg(BaseMonitor)
|
||||
raise TypeError(msg)
|
||||
return object.__new__(cls)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def start(self)->None:
|
||||
"""Function to start the monitor as an ongoing process/thread. Must be
|
||||
implemented by any child process"""
|
||||
pass
|
||||
|
||||
def stop(self)->None:
|
||||
"""Function to stop the monitor as an ongoing process/thread. Must be
|
||||
implemented by any child process"""
|
||||
pass
|
||||
|
||||
def add_pattern(self, pattern:BasePattern)->None:
|
||||
"""Function to add a pattern to the current definitions. Must be
|
||||
implemented by any child process."""
|
||||
pass
|
||||
|
||||
def update_pattern(self, pattern:BasePattern)->None:
|
||||
"""Function to update a pattern in the current definitions. Must be
|
||||
implemented by any child process."""
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def add_recipe(self, recipe:BaseRecipe)->None:
|
||||
"""Function to add a recipe to the current definitions. Must be
|
||||
implemented by any child process."""
|
||||
pass
|
||||
|
||||
def update_recipe(self, recipe:BaseRecipe)->None:
|
||||
"""Function to update a recipe in the current definitions. Must be
|
||||
implemented by any child process."""
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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).valid_event_types, BaseHandler)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""A check that this base class is not instantiated itself, only
|
||||
inherited from"""
|
||||
if cls is BaseHandler:
|
||||
msg = get_drt_imp_msg(BaseHandler)
|
||||
raise TypeError(msg)
|
||||
return object.__new__(cls)
|
||||
|
||||
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
|
||||
|
||||
def handle(self, event:dict[str,Any])->None:
|
||||
"""Function to handle a given event. Must be implemented by any child
|
||||
process."""
|
||||
pass
|
||||
|
||||
|
||||
class BaseConductor:
|
||||
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).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]:
|
||||
"""Function to provide a list of the types of jobs this conductor can
|
||||
process. Must be implemented by any child process."""
|
||||
pass
|
||||
|
||||
def execute(self, job:dict[str,Any])->None:
|
||||
"""Function to execute a given job. Must be implemented by any child
|
||||
process."""
|
||||
pass
|
||||
|
||||
|
||||
def create_rules(patterns:Union[dict[str,BasePattern],list[BasePattern]],
|
||||
recipes:Union[dict[str,BaseRecipe],list[BaseRecipe]],
|
||||
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(recipes, dict, alt_types=[list])
|
||||
valid_list(new_rules, BaseRule, min_length=0)
|
||||
|
||||
# Convert a pattern list to a dictionary
|
||||
if isinstance(patterns, list):
|
||||
valid_list(patterns, BasePattern, min_length=0)
|
||||
patterns = {pattern.name:pattern for pattern in patterns}
|
||||
else:
|
||||
# Validate the pattern dictionary
|
||||
valid_dict(patterns, str, BasePattern, strict=False, min_length=0)
|
||||
for k, v in patterns.items():
|
||||
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.")
|
||||
|
||||
# Convert a recipe list into a dictionary
|
||||
if isinstance(recipes, list):
|
||||
valid_list(recipes, BaseRecipe, min_length=0)
|
||||
recipes = {recipe.name:recipe for recipe in recipes}
|
||||
else:
|
||||
# Validate the recipe dictionary
|
||||
valid_dict(recipes, str, BaseRecipe, strict=False, min_length=0)
|
||||
for k, v in recipes.items():
|
||||
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.")
|
||||
|
||||
# Try to create a rule for each rule in turn
|
||||
generated_rules = {}
|
||||
for pattern in patterns.values():
|
||||
if pattern.recipe in recipes:
|
||||
rule = create_rule(pattern, recipes[pattern.recipe])
|
||||
generated_rules[rule.name] = rule
|
||||
try:
|
||||
rule = create_rule(pattern, recipes[pattern.recipe])
|
||||
generated_rules[rule.name] = rule
|
||||
except TypeError:
|
||||
pass
|
||||
return generated_rules
|
||||
|
||||
def create_rule(pattern:BasePattern, recipe:BaseRecipe,
|
||||
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(recipe, BaseRecipe)
|
||||
valid_list(new_rules, BaseRule, min_length=0)
|
||||
|
||||
# Imported here to avoid circular imports at top of file
|
||||
import rules
|
||||
# Get a dictionary of all inbuilt rules
|
||||
all_rules ={(r.pattern_type, r.recipe_type):r for r in [r[1] \
|
||||
for r in inspect.getmembers(sys.modules["rules"], inspect.isclass) \
|
||||
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__)
|
||||
if (key) in all_rules:
|
||||
return all_rules[key](
|
||||
@ -289,5 +440,6 @@ def create_rule(pattern:BasePattern, recipe:BaseRecipe,
|
||||
pattern,
|
||||
recipe
|
||||
)
|
||||
# Raise error if not valid rule type can be found
|
||||
raise TypeError(f"No valid rule for Pattern '{pattern}' and Recipe "
|
||||
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 threading
|
||||
|
||||
@ -16,20 +23,31 @@ from core.meow import BaseHandler, BaseMonitor, BaseConductor
|
||||
|
||||
|
||||
class MeowRunner:
|
||||
# A collection of all monitors in the runner
|
||||
monitors:list[BaseMonitor]
|
||||
# A collection of all handlers in the runner
|
||||
handlers:dict[str:BaseHandler]
|
||||
# A collection of all conductors in the runner
|
||||
conductors:dict[str:BaseConductor]
|
||||
# A collection of all channels from each monitor
|
||||
from_monitors: list[VALID_CHANNELS]
|
||||
# A collection of all channels from each handler
|
||||
from_handlers: list[VALID_CHANNELS]
|
||||
def __init__(self, monitors:Union[BaseMonitor,list[BaseMonitor]],
|
||||
handlers:Union[BaseHandler,list[BaseHandler]],
|
||||
conductors:Union[BaseConductor,list[BaseConductor]],
|
||||
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)
|
||||
# If conductors isn't a list, make it one
|
||||
if not type(conductors) == list:
|
||||
conductors = [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:
|
||||
conductor_jobs = conductor.valid_job_types()
|
||||
if not conductor_jobs:
|
||||
@ -45,10 +63,13 @@ class MeowRunner:
|
||||
self.conductors[job] = [conductor]
|
||||
|
||||
self._is_valid_handlers(handlers)
|
||||
# If handlers isn't a list, make it one
|
||||
if not type(handlers) == list:
|
||||
handlers = [handlers]
|
||||
self.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:
|
||||
handler_events = handler.valid_event_types()
|
||||
if not handler_events:
|
||||
@ -62,80 +83,121 @@ class MeowRunner:
|
||||
self.handlers[event].append(handler)
|
||||
else:
|
||||
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 = handler_to_runner_writer
|
||||
self.from_handlers.append(handler_to_runner_reader)
|
||||
|
||||
self._is_valid_monitors(monitors)
|
||||
# If monitors isn't a list, make it one
|
||||
if not type(monitors) == list:
|
||||
monitors = [monitors]
|
||||
self.monitors = monitors
|
||||
self.from_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 = monitor_to_runner_writer
|
||||
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._mon_han_worker = None
|
||||
|
||||
# Create channel to send stop messages to handler/conductor thread
|
||||
self._stop_han_con_pipe = Pipe()
|
||||
self._han_con_worker = None
|
||||
|
||||
# Setup debugging
|
||||
self._print_target, self.debug_level = setup_debugging(print, logging)
|
||||
|
||||
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]]
|
||||
while True:
|
||||
ready = wait(all_inputs)
|
||||
|
||||
# If we get a message from the stop channel, then finish
|
||||
if self._stop_mon_han_pipe[0] in ready:
|
||||
return
|
||||
else:
|
||||
for from_monitor in self.from_monitors:
|
||||
if from_monitor in ready:
|
||||
# Read event from the monitor channel
|
||||
message = from_monitor.recv()
|
||||
event = message
|
||||
# Abort if we don't have a relevent handler.
|
||||
if not self.handlers[event[EVENT_TYPE]]:
|
||||
print_debug(self._print_target, self.debug_level,
|
||||
"Could not process event as no relevent "
|
||||
f"handler for '{event[EVENT_TYPE]}'",
|
||||
DEBUG_INFO)
|
||||
return
|
||||
# If we've only one handler, use that
|
||||
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:
|
||||
self.handlers[event[EVENT_TYPE]][
|
||||
handler = 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:
|
||||
"""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]]
|
||||
while True:
|
||||
ready = wait(all_inputs)
|
||||
|
||||
# If we get a message from the stop channel, then finish
|
||||
if self._stop_han_con_pipe[0] in ready:
|
||||
return
|
||||
else:
|
||||
for from_handler in self.from_handlers:
|
||||
if from_handler in ready:
|
||||
# Read event from the handler channel
|
||||
message = from_handler.recv()
|
||||
job = message
|
||||
# Abort if we don't have a relevent conductor.
|
||||
if not self.conductors[job[JOB_TYPE]]:
|
||||
print_debug(self._print_target, self.debug_level,
|
||||
"Could not process job as no relevent "
|
||||
f"conductor for '{job[JOB_TYPE]}'", DEBUG_INFO)
|
||||
return
|
||||
# If we've only one conductor, use that
|
||||
if len(self.conductors[job[JOB_TYPE]]) == 1:
|
||||
conductor = self.conductors[job[JOB_TYPE]][0]
|
||||
self.execute_job(conductor, job)
|
||||
# If multiple conductors then randomly pick one
|
||||
else:
|
||||
conductor = self.conductors[job[JOB_TYPE]][
|
||||
randrange(len(self.conductors[job[JOB_TYPE]]))
|
||||
]
|
||||
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:
|
||||
"""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,
|
||||
f"Starting execution for job: '{job[JOB_ID]}'", DEBUG_INFO)
|
||||
try:
|
||||
@ -148,20 +210,28 @@ class MeowRunner:
|
||||
f"'{job[JOB_ID]}'. {e}", DEBUG_INFO)
|
||||
|
||||
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:
|
||||
monitor.start()
|
||||
startable = []
|
||||
# Start all handlers, if they need it
|
||||
for handler_list in self.handlers.values():
|
||||
for handler in handler_list:
|
||||
if hasattr(handler, "start") and handler not in startable:
|
||||
startable.append()
|
||||
# Start all conductors, if they need it
|
||||
for conductor_list in self.conductors.values():
|
||||
for conductor in conductor_list:
|
||||
if hasattr(conductor, "start") and conductor not in startable:
|
||||
startable.append()
|
||||
for starting in startable:
|
||||
starting.start()
|
||||
|
||||
|
||||
# If we've not started the monitor/handler interaction thread yet, then
|
||||
# do so
|
||||
if self._mon_han_worker is None:
|
||||
self._mon_han_worker = threading.Thread(
|
||||
target=self.run_monitor_handler_interaction,
|
||||
@ -177,6 +247,8 @@ class MeowRunner:
|
||||
msg, DEBUG_WARNING)
|
||||
raise RuntimeWarning(msg)
|
||||
|
||||
# If we've not started the handler/conductor interaction thread yet,
|
||||
# then do so
|
||||
if self._han_con_worker is None:
|
||||
self._han_con_worker = threading.Thread(
|
||||
target=self.run_handler_conductor_interaction,
|
||||
@ -193,14 +265,20 @@ class MeowRunner:
|
||||
raise RuntimeWarning(msg)
|
||||
|
||||
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:
|
||||
monitor.stop()
|
||||
|
||||
stopable = []
|
||||
# Stop all handlers, if they need it
|
||||
for handler_list in self.handlers.values():
|
||||
for handler in handler_list:
|
||||
if hasattr(handler, "stop") and handler not in stopable:
|
||||
stopable.append()
|
||||
# Stop all conductors, if they need it
|
||||
for conductor_list in self.conductors.values():
|
||||
for conductor in conductor_list:
|
||||
if hasattr(conductor, "stop") and conductor not in stopable:
|
||||
@ -208,6 +286,7 @@ class MeowRunner:
|
||||
for stopping in stopable:
|
||||
stopping.stop()
|
||||
|
||||
# If we've started the monitor/handler interaction thread, then stop it
|
||||
if self._mon_han_worker is None:
|
||||
msg = "Cannot stop event handling thread that is not started."
|
||||
print_debug(self._print_target, self.debug_level,
|
||||
@ -219,6 +298,8 @@ class MeowRunner:
|
||||
print_debug(self._print_target, self.debug_level,
|
||||
"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:
|
||||
msg = "Cannot stop job conducting thread that is not started."
|
||||
print_debug(self._print_target, self.debug_level,
|
||||
@ -232,18 +313,21 @@ class MeowRunner:
|
||||
|
||||
def _is_valid_monitors(self,
|
||||
monitors:Union[BaseMonitor,list[BaseMonitor]])->None:
|
||||
"""Validation check for 'monitors' variable from main constructor."""
|
||||
check_type(monitors, BaseMonitor, alt_types=[list])
|
||||
if type(monitors) == list:
|
||||
valid_list(monitors, BaseMonitor, min_length=1)
|
||||
|
||||
def _is_valid_handlers(self,
|
||||
handlers:Union[BaseHandler,list[BaseHandler]])->None:
|
||||
"""Validation check for 'handlers' variable from main constructor."""
|
||||
check_type(handlers, BaseHandler, alt_types=[list])
|
||||
if type(handlers) == list:
|
||||
valid_list(handlers, BaseHandler, min_length=1)
|
||||
|
||||
def _is_valid_conductors(self,
|
||||
conductors:Union[BaseConductor,list[BaseConductor]])->None:
|
||||
"""Validation check for 'conductors' variable from main constructor."""
|
||||
check_type(conductors, BaseConductor, alt_types=[list])
|
||||
if type(conductors) == list:
|
||||
valid_list(conductors, BaseConductor, min_length=1)
|
||||
|
Reference in New Issue
Block a user