added watchdog file monitoring

This commit is contained in:
PatchOfScotland
2022-12-13 14:59:43 +01:00
parent 4041b86343
commit 380f7066e1
13 changed files with 550 additions and 65 deletions

View File

@ -0,0 +1,3 @@
from core.correctness.validation import *
from core.correctness.vars import *

View File

@ -1,7 +1,9 @@
from abc import ABCMeta from os.path import sep
from typing import Any, _SpecialForm from typing import Any, _SpecialForm
from core.correctness.vars import VALID_PATH_CHARS
def check_input(variable:Any, expected_type:type, alt_types:list[type]=[], def check_input(variable:Any, expected_type:type, alt_types:list[type]=[],
or_none:bool=False)->None: or_none:bool=False)->None:
""" """
@ -73,8 +75,8 @@ 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:
check_input(variable, dict) check_input(variable, dict)
check_input(key_type, type, alt_types=[_SpecialForm, ABCMeta]) check_input(key_type, type, alt_types=[_SpecialForm])
check_input(value_type, type, alt_types=[_SpecialForm, ABCMeta]) check_input(value_type, type, alt_types=[_SpecialForm])
check_input(required_keys, list) check_input(required_keys, list)
check_input(optional_keys, list) check_input(optional_keys, list)
check_input(strict, bool) check_input(strict, bool)
@ -110,3 +112,12 @@ def valid_list(variable:list[Any], entry_type:type,
f"of length {min_length}") f"of length {min_length}")
for entry in variable: for entry in variable:
check_input(entry, entry_type, alt_types=alt_types) check_input(entry, entry_type, alt_types=alt_types)
def valid_path(variable:str, allow_base=False, extension:str="", min_length=1):
valid_string(variable, VALID_PATH_CHARS, min_length=min_length)
if not allow_base and variable.startswith(sep):
raise ValueError(f"Cannot accept path '{variable}'. Must be relative.")
if min_length > 0 and extension and not variable.endswith(extension):
raise ValueError(f"Path '{variable}' does not have required "
f"extension '{extension}'.")

View File

@ -1,6 +1,8 @@
import os import os
from inspect import signature
CHAR_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz' CHAR_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'
CHAR_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' CHAR_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
CHAR_NUMERIC = '0123456789' CHAR_NUMERIC = '0123456789'
@ -15,4 +17,48 @@ VALID_VARIABLE_NAME_CHARS = CHAR_UPPERCASE + CHAR_LOWERCASE + CHAR_NUMERIC + "_"
VALID_JUPYTER_NOTEBOOK_FILENAME_CHARS = VALID_NAME_CHARS + "." + os.path.sep VALID_JUPYTER_NOTEBOOK_FILENAME_CHARS = VALID_NAME_CHARS + "." + os.path.sep
VALID_JUPYTER_NOTEBOOK_EXTENSIONS = [".ipynb"] VALID_JUPYTER_NOTEBOOK_EXTENSIONS = [".ipynb"]
VALID_TRIGGERING_PATH_CHARS = VALID_NAME_CHARS + "." + os.path.sep VALID_PATH_CHARS = VALID_NAME_CHARS + "." + os.path.sep
VALID_TRIGGERING_PATH_CHARS = VALID_NAME_CHARS + ".*" + os.path.sep
BAREBONES_NOTEBOOK = {
"cells": [],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}
FILE_CREATE_EVENT = "file_created"
FILE_MODIFY_EVENT = "file_modified"
FILE_MOVED_EVENT = "file_moved"
FILE_CLOSED_EVENT = "file_closed"
FILE_DELETED_EVENT = "file_deleted"
FILE_EVENTS = [
FILE_CREATE_EVENT,
FILE_MODIFY_EVENT,
FILE_MOVED_EVENT,
FILE_CLOSED_EVENT,
FILE_DELETED_EVENT
]
DIR_CREATE_EVENT = "dir_created"
DIR_MODIFY_EVENT = "dir_modified"
DIR_MOVED_EVENT = "dir_moved"
DIR_DELETED_EVENT = "dir_deleted"
DIR_EVENTS = [
DIR_CREATE_EVENT,
DIR_MODIFY_EVENT,
DIR_MOVED_EVENT,
DIR_DELETED_EVENT
]
PIPE_READ = 0
PIPE_WRITE = 1
def get_drt_imp_msg(base_class):
return f"{base_class.__name__} may not be instantiated directly. " \
f"Implement a child class."
def get_not_imp_msg(parent_class, class_function):
return f"Children of the '{parent_class.__name__}' class must implement " \
f"the '{class_function.__name__}({signature(class_function)})' " \
"function"

View File

@ -1,11 +1,13 @@
import core.correctness.vars from multiprocessing.connection import Connection
import core.correctness.validation
from abc import ABC, abstractmethod
from typing import Any from typing import Any
from core.correctness.vars import VALID_RECIPE_NAME_CHARS, \
VALID_PATTERN_NAME_CHARS, VALID_RULE_NAME_CHARS, \
get_not_imp_msg, get_drt_imp_msg
from core.correctness.validation import valid_string
class BaseRecipe: class BaseRecipe:
name:str name:str
recipe:Any recipe:Any
@ -13,6 +15,15 @@ class BaseRecipe:
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]={}):
if (type(self)._is_valid_recipe == BaseRecipe._is_valid_recipe):
msg = get_not_imp_msg(BaseRecipe, BaseRecipe._is_valid_recipe)
raise NotImplementedError(msg)
if (type(self)._is_valid_parameters == BaseRecipe._is_valid_parameters):
msg = get_not_imp_msg(BaseRecipe, BaseRecipe._is_valid_parameters)
raise NotImplementedError(msg)
if (type(self)._is_valid_requirements == BaseRecipe._is_valid_requirements):
msg = get_not_imp_msg(BaseRecipe, BaseRecipe._is_valid_requirements)
raise NotImplementedError(msg)
self._is_valid_name(name) self._is_valid_name(name)
self.name = name self.name = name
self._is_valid_recipe(recipe) self._is_valid_recipe(recipe)
@ -24,33 +35,39 @@ class BaseRecipe:
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls is BaseRecipe: if cls is BaseRecipe:
raise TypeError("BaseRecipe may not be instantiated directly") msg = get_drt_imp_msg(BaseRecipe)
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:
core.correctness.validation.valid_string( valid_string(name, VALID_RECIPE_NAME_CHARS)
name, core.correctness.vars.VALID_RECIPE_NAME_CHARS)
@abstractmethod
def _is_valid_recipe(self, recipe:Any)->None: def _is_valid_recipe(self, recipe:Any)->None:
pass pass
@abstractmethod
def _is_valid_parameters(self, parameters:Any)->None: def _is_valid_parameters(self, parameters:Any)->None:
pass pass
@abstractmethod
def _is_valid_requirements(self, requirements:Any)->None: def _is_valid_requirements(self, requirements:Any)->None:
pass pass
class BasePattern(ABC): class BasePattern:
name:str name:str
recipe:str recipe:str
parameters:dict[str, Any] parameters:dict[str, Any]
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]={}):
if (type(self)._is_valid_recipe == BasePattern._is_valid_recipe):
msg = get_not_imp_msg(BasePattern, BasePattern._is_valid_recipe)
raise NotImplementedError(msg)
if (type(self)._is_valid_parameters == BasePattern._is_valid_parameters):
msg = get_not_imp_msg(BasePattern, BasePattern._is_valid_parameters)
raise NotImplementedError(msg)
if (type(self)._is_valid_output == BasePattern._is_valid_output):
msg = get_not_imp_msg(BasePattern, BasePattern._is_valid_output)
raise NotImplementedError(msg)
self._is_valid_name(name) self._is_valid_name(name)
self.name = name self.name = name
self._is_valid_recipe(recipe) self._is_valid_recipe(recipe)
@ -62,33 +79,37 @@ class BasePattern(ABC):
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls is BasePattern: if cls is BasePattern:
raise TypeError("BasePattern may not be instantiated directly") msg = get_drt_imp_msg(BasePattern)
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:
core.correctness.validation.valid_string( valid_string(name, VALID_PATTERN_NAME_CHARS)
name, core.correctness.vars.VALID_PATTERN_NAME_CHARS)
@abstractmethod
def _is_valid_recipe(self, recipe:Any)->None: def _is_valid_recipe(self, recipe:Any)->None:
pass pass
@abstractmethod
def _is_valid_parameters(self, parameters:Any)->None: def _is_valid_parameters(self, parameters:Any)->None:
pass pass
@abstractmethod
def _is_valid_output(self, outputs:Any)->None: def _is_valid_output(self, outputs:Any)->None:
pass pass
class BaseRule(ABC): class BaseRule:
name:str name:str
pattern:BasePattern pattern:BasePattern
recipe:BaseRecipe recipe:BaseRecipe
pattern_type:str="" pattern_type:str=""
recipe_type:str="" recipe_type:str=""
def __init__(self, name:str, pattern:BasePattern, recipe:BaseRecipe): def __init__(self, name:str, pattern:BasePattern, recipe:BaseRecipe):
if (type(self)._is_valid_pattern == BaseRule._is_valid_pattern):
msg = get_not_imp_msg(BaseRule, BaseRule._is_valid_pattern)
raise NotImplementedError(msg)
if (type(self)._is_valid_recipe == BaseRule._is_valid_recipe):
msg = get_not_imp_msg(BaseRule, BaseRule._is_valid_recipe)
raise NotImplementedError(msg)
self._is_valid_name(name) self._is_valid_name(name)
self.name = name self.name = name
self._is_valid_pattern(pattern) self._is_valid_pattern(pattern)
@ -99,18 +120,16 @@ class BaseRule(ABC):
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls is BaseRule: if cls is BaseRule:
raise TypeError("BaseRule may not be instantiated directly") msg = get_drt_imp_msg(BaseRule)
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:
core.correctness.validation.valid_string( valid_string(name, VALID_RULE_NAME_CHARS)
name, core.correctness.vars.VALID_RULE_NAME_CHARS)
@abstractmethod
def _is_valid_pattern(self, pattern:Any)->None: def _is_valid_pattern(self, pattern:Any)->None:
pass pass
@abstractmethod
def _is_valid_recipe(self, recipe:Any)->None: def _is_valid_recipe(self, recipe:Any)->None:
pass pass
@ -121,3 +140,78 @@ class BaseRule(ABC):
if self.recipe_type == "": if self.recipe_type == "":
raise AttributeError(f"Rule Class '{self.__class__.__name__}' " raise AttributeError(f"Rule Class '{self.__class__.__name__}' "
"does not set a recipe_type.") "does not set a recipe_type.")
class BaseMonitor:
rules: dict[str, BaseRule]
report: Connection
listen: Connection
def __init__(self, rules:dict[str, BaseRule], report:Connection,
listen:Connection) -> None:
if (type(self).start == BaseMonitor.start):
msg = get_not_imp_msg(BaseMonitor, BaseMonitor.start)
raise NotImplementedError(msg)
if (type(self).stop == BaseMonitor.stop):
msg = get_not_imp_msg(BaseMonitor, BaseMonitor.stop)
raise NotImplementedError(msg)
if (type(self)._is_valid_report == BaseMonitor._is_valid_report):
msg = get_not_imp_msg(BaseMonitor, BaseMonitor._is_valid_report)
raise NotImplementedError(msg)
self._is_valid_report(report)
self.report = report
if (type(self)._is_valid_listen == BaseMonitor._is_valid_listen):
msg = get_not_imp_msg(BaseMonitor, BaseMonitor._is_valid_listen)
raise NotImplementedError(msg)
self._is_valid_listen(listen)
self.listen = listen
if (type(self)._is_valid_rules == BaseMonitor._is_valid_rules):
msg = get_not_imp_msg(BaseMonitor, BaseMonitor._is_valid_rules)
raise NotImplementedError(msg)
self._is_valid_rules(rules)
self.rules = rules
def __new__(cls, *args, **kwargs):
if cls is BaseMonitor:
msg = get_drt_imp_msg(BaseMonitor)
raise TypeError(msg)
return object.__new__(cls)
def _is_valid_report(self, report:Connection)->None:
pass
def _is_valid_listen(self, listen:Connection)->None:
pass
def _is_valid_rules(self, rules:dict[str, BaseRule])->None:
pass
def start(self)->None:
pass
def stop(self)->None:
pass
class BaseHandler:
inputs:Any
def __init__(self, inputs:Any) -> None:
if (type(self).handle == BaseHandler.handle):
msg = get_not_imp_msg(BaseHandler, BaseHandler.handle)
raise NotImplementedError(msg)
if (type(self)._is_valid_inputs == BaseHandler._is_valid_inputs):
msg = get_not_imp_msg(BaseHandler, BaseHandler._is_valid_inputs)
raise NotImplementedError(msg)
self._is_valid_inputs(inputs)
self.inputs = inputs
def __new__(cls, *args, **kwargs):
if cls is BaseHandler:
msg = get_drt_imp_msg(BaseHandler)
raise TypeError(msg)
return object.__new__(cls)
def _is_valid_inputs(self, inputs:Any)->None:
pass
def handle()->None:
pass

View File

@ -1,2 +1,2 @@
from patterns.file_event_pattern import FileEventPattern from patterns.file_event_pattern import FileEventPattern, WatchdogMonitor

View File

@ -1,32 +1,62 @@
import threading
import os
from fnmatch import translate
from multiprocessing.connection import Connection
from re import match
from time import time, sleep
from typing import Any from typing import Any
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler, FileCreatedEvent, \
FileModifiedEvent, FileMovedEvent, FileClosedEvent, FileDeletedEvent, \
DirCreatedEvent, DirDeletedEvent, DirModifiedEvent, DirMovedEvent
from core.correctness.validation import check_input, valid_string, valid_dict from core.correctness.validation import check_input, valid_string, \
valid_dict, valid_list, valid_path
from core.correctness.vars import VALID_RECIPE_NAME_CHARS, \ from core.correctness.vars import VALID_RECIPE_NAME_CHARS, \
VALID_VARIABLE_NAME_CHARS VALID_VARIABLE_NAME_CHARS, FILE_EVENTS, FILE_CREATE_EVENT, \
from core.meow import BasePattern FILE_MODIFY_EVENT, FILE_MOVED_EVENT, FILE_CLOSED_EVENT, \
FILE_DELETED_EVENT, DIR_CREATE_EVENT, DIR_DELETED_EVENT, \
DIR_MODIFY_EVENT, DIR_MOVED_EVENT
from core.meow import BasePattern, BaseMonitor, BaseRule
_EVENT_TRANSLATIONS = {
FileCreatedEvent: FILE_CREATE_EVENT,
FileModifiedEvent: FILE_MODIFY_EVENT,
FileMovedEvent: FILE_MOVED_EVENT,
FileClosedEvent: FILE_CLOSED_EVENT,
FileDeletedEvent: FILE_DELETED_EVENT,
DirCreatedEvent: DIR_CREATE_EVENT,
DirDeletedEvent: DIR_DELETED_EVENT,
DirModifiedEvent: DIR_MODIFY_EVENT,
DirMovedEvent: DIR_MOVED_EVENT
}
class FileEventPattern(BasePattern): class FileEventPattern(BasePattern):
triggering_path:str triggering_path:str
triggering_file:str triggering_file:str
event_mask:list[str]
def __init__(self, name:str, triggering_path:str, recipe:str, def __init__(self, name:str, triggering_path:str, recipe:str,
triggering_file:str, parameters:dict[str,Any]={}, triggering_file:str, event_mask:list[str]=FILE_EVENTS,
outputs:dict[str,Any]={}): parameters:dict[str,Any]={}, outputs:dict[str,Any]={}):
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
self._is_valid_triggering_file(triggering_file) self._is_valid_triggering_file(triggering_file)
self.triggering_file = triggering_file self.triggering_file = triggering_file
self._is_valid_event_mask(event_mask)
self.event_mask = event_mask
def _is_valid_recipe(self, recipe:str)->None: def _is_valid_recipe(self, recipe:str)->None:
valid_string(recipe, VALID_RECIPE_NAME_CHARS) 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:
check_input(triggering_path, str) valid_path(triggering_path)
if len(triggering_path) < 1: if len(triggering_path) < 1:
raise ValueError ( raise ValueError (
f"trigginering path '{triggering_path}' is too short. " f"triggiering path '{triggering_path}' is too short. "
"Minimum length is 1" "Minimum length is 1"
) )
@ -42,3 +72,152 @@ class FileEventPattern(BasePattern):
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:
valid_list(event_mask, str, min_length=1)
for mask in event_mask:
if mask not in FILE_EVENTS:
raise ValueError(f"Invalid event mask '{mask}'. Valid are: "
f"{FILE_EVENTS}")
class WatchdogMonitor(BaseMonitor):
event_handler:PatternMatchingEventHandler
monitor:Observer
base_dir:str
_rules_lock:threading.Lock
def __init__(self, base_dir:str, rules:dict[str, BaseRule],
report:Connection, listen:Connection, autostart=False,
settletime:int=1)->None:
super().__init__(rules, report, listen)
self._is_valid_base_dir(base_dir)
self.base_dir = base_dir
check_input(settletime, int)
self._rules_lock = threading.Lock()
self.event_handler = MEOWEventHandler(self, settletime=settletime)
self.monitor = Observer()
self.monitor.schedule(
self.event_handler,
self.base_dir,
recursive=True
)
if autostart:
self.start()
def start(self)->None:
self.monitor.start()
def stop(self)->None:
self.monitor.stop()
def match(self, event)->None:
src_path = event.src_path
event_type = "dir_"+ event.event_type if event.is_directory \
else "file_" + event.event_type
handle_path = src_path.replace(self.base_dir, '', 1)
while handle_path.startswith(os.path.sep):
handle_path = handle_path[1:]
self._rules_lock.acquire()
try:
for rule in self.rules.values():
if event_type not in rule.pattern.event_mask:
continue
target_path = rule.pattern.triggering_path
recursive_regexp = translate(target_path)
direct_regexp = recursive_regexp.replace('.*', '[^/]*')
recursive_hit = match(recursive_regexp, handle_path)
direct_hit = match(direct_regexp, handle_path)
if direct_hit or recursive_hit:
self.report.send((event, rule))
except Exception as e:
self._rules_lock.release()
raise Exception(e)
self._rules_lock.release()
def _is_valid_base_dir(self, base_dir:str)->None:
valid_path(base_dir)
def _is_valid_report(self, report:Connection)->None:
check_input(report, Connection)
def _is_valid_listen(self, listen:Connection)->None:
check_input(listen, Connection)
def _is_valid_rules(self, rules:dict[str, BaseRule])->None:
valid_dict(rules, str, BaseRule, min_length=0, strict=False)
class MEOWEventHandler(PatternMatchingEventHandler):
monitor:WatchdogMonitor
_settletime:int
_recent_jobs:dict[str, Any]
_recent_jobs_lock:threading.Lock
def __init__(self, monitor:WatchdogMonitor, settletime:int=1):
super().__init__()
self.monitor = monitor
self._settletime = settletime
self._recent_jobs = {}
self._recent_jobs_lock = threading.Lock()
def threaded_handler(self, event):
self._recent_jobs_lock.acquire()
try:
if event.src_path in self._recent_jobs:
recent_timestamp = self._recent_jobs[event.src_path]
difference = event.time_stamp - recent_timestamp
if difference <= self._settletime:
self._recent_jobs[event.src_path] = \
max(recent_timestamp, event.time_stamp)
self._recent_jobs_lock.release()
return
else:
self._recent_jobs[event.src_path] = event.time_stamp
else:
self._recent_jobs[event.src_path] = event.time_stamp
except Exception as ex:
self._recent_jobs_lock.release()
raise Exception(ex)
self._recent_jobs_lock.release()
self.monitor.match(event)
def handle_event(self, event):
event.time_stamp = time()
waiting_for_threaded_resources = True
while waiting_for_threaded_resources:
try:
worker = threading.Thread(
target=self.threaded_handler,
args=[event])
worker.daemon = True
worker.start()
waiting_for_threaded_resources = False
except threading.ThreadError:
sleep(1)
def on_created(self, event):
self.handle_event(event)
def on_modified(self, event):
self.handle_event(event)
def on_moved(self, event):
self.handle_event(event)
def on_deleted(self, event):
self.handle_event(event)
def on_closed(self, event):
self.handle_event(event)

View File

@ -3,7 +3,8 @@ import nbformat
from typing import Any from typing import Any
from core.correctness.validation import check_input, valid_string, valid_dict from core.correctness.validation import check_input, valid_string, \
valid_dict, valid_path
from core.correctness.vars import VALID_JUPYTER_NOTEBOOK_FILENAME_CHARS, \ from core.correctness.vars import VALID_JUPYTER_NOTEBOOK_FILENAME_CHARS, \
VALID_JUPYTER_NOTEBOOK_EXTENSIONS, VALID_VARIABLE_NAME_CHARS VALID_JUPYTER_NOTEBOOK_EXTENSIONS, VALID_VARIABLE_NAME_CHARS
from core.meow import BaseRecipe from core.meow import BaseRecipe
@ -17,8 +18,7 @@ class JupyterNotebookRecipe(BaseRecipe):
self.source = source self.source = source
def _is_valid_source(self, source:str)->None: def _is_valid_source(self, source:str)->None:
valid_string( valid_path(source, extension=".ipynb", min_length=0)
source, VALID_JUPYTER_NOTEBOOK_FILENAME_CHARS, min_length=0)
if not source: if not source:
return return

View File

@ -20,6 +20,3 @@ class FileEventJupyterNotebookRule(BaseRule):
def _is_valid_recipe(self, recipe:JupyterNotebookRecipe) -> None: def _is_valid_recipe(self, recipe:JupyterNotebookRecipe) -> None:
check_input(recipe, JupyterNotebookRecipe) check_input(recipe, JupyterNotebookRecipe)
def _set_pattern_type(self)->None:
self.pattern_type

View File

@ -1,19 +1,13 @@
import unittest import unittest
from core.correctness.vars import CHAR_LOWERCASE, CHAR_UPPERCASE from core.correctness.vars import CHAR_LOWERCASE, CHAR_UPPERCASE, \
BAREBONES_NOTEBOOK
from core.functionality import create_rules, generate_id from core.functionality import create_rules, generate_id
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
BAREBONES_NOTEBOOK = {
"cells": [],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}
valid_pattern_one = FileEventPattern( valid_pattern_one = FileEventPattern(
"pattern_one", "path_one", "recipe_one", "file_one") "pattern_one", "path_one", "recipe_one", "file_one")
valid_pattern_two = FileEventPattern( valid_pattern_two = FileEventPattern(

View File

@ -1,7 +1,11 @@
import unittest import unittest
from core.meow import BasePattern, BaseRecipe, BaseRule from typing import Any
from core.correctness.vars import BAREBONES_NOTEBOOK
from core.meow import BasePattern, BaseRecipe, BaseRule, BaseMonitor, \
BaseHandler
class MeowTests(unittest.TestCase): class MeowTests(unittest.TestCase):
@ -13,12 +17,97 @@ class MeowTests(unittest.TestCase):
def testBaseRecipe(self)->None: def testBaseRecipe(self)->None:
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
BaseRecipe("", "") BaseRecipe("name", "")
class NewRecipe(BaseRecipe):
pass
with self.assertRaises(NotImplementedError):
NewRecipe("name", "")
class FullRecipe(BaseRecipe):
def _is_valid_recipe(self, recipe:Any)->None:
pass
def _is_valid_parameters(self, parameters:Any)->None:
pass
def _is_valid_requirements(self, requirements:Any)->None:
pass
FullRecipe("name", "")
def testBasePattern(self)->None: def testBasePattern(self)->None:
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
BasePattern("", "") BasePattern("name", "", "", "")
class NewPattern(BasePattern):
pass
with self.assertRaises(NotImplementedError):
NewPattern("name", "", "", "")
class FullPattern(BasePattern):
def _is_valid_recipe(self, recipe:Any)->None:
pass
def _is_valid_parameters(self, parameters:Any)->None:
pass
def _is_valid_output(self, outputs:Any)->None:
pass
FullPattern("name", "", "", "")
def testBaseRule(self)->None: def testBaseRule(self)->None:
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
BaseRule("", "") BaseRule("name", "", "")
class NewRule(BaseRule):
pass
with self.assertRaises(NotImplementedError):
NewRule("name", "", "")
class FullRule(BaseRule):
pattern_type = "pattern"
recipe_type = "recipe"
def _is_valid_recipe(self, recipe:Any)->None:
pass
def _is_valid_pattern(self, pattern:Any)->None:
pass
FullRule("name", "", "")
def testBaseMonitor(self)->None:
with self.assertRaises(TypeError):
BaseMonitor("", "", "")
class TestMonitor(BaseMonitor):
pass
with self.assertRaises(NotImplementedError):
TestMonitor("", "", "")
class FullTestMonitor(BaseMonitor):
def start(self):
pass
def stop(self):
pass
def _is_valid_report(self, report:Any)->None:
pass
def _is_valid_listen(self, listen:Any)->None:
pass
def _is_valid_rules(self, rules:Any)->None:
pass
FullTestMonitor("", "", "")
def testBaseHandler(self)->None:
with self.assertRaises(TypeError):
BaseHandler("")
class TestHandler(BaseHandler):
pass
with self.assertRaises(NotImplementedError):
TestHandler("")
class FullTestHandler(BaseHandler):
def handle(self):
pass
def _is_valid_inputs(self, inputs:Any)->None:
pass
FullTestHandler("")

View File

@ -1,15 +1,28 @@
import shutil
import os
import unittest import unittest
from patterns.file_event_pattern import FileEventPattern from multiprocessing import Pipe
from core.correctness.vars import FILE_EVENTS, FILE_CREATE_EVENT, PIPE_READ, \
PIPE_WRITE, BAREBONES_NOTEBOOK
from core.functionality import create_rules
from patterns.file_event_pattern import FileEventPattern, WatchdogMonitor
from recipes import JupyterNotebookRecipe
TEST_BASE = "test_base"
class CorrectnessTests(unittest.TestCase): class CorrectnessTests(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
return super().setUp() super().setUp()
if not os.path.exists(TEST_BASE):
os.mkdir(TEST_BASE)
def tearDown(self) -> None: def tearDown(self) -> None:
return super().tearDown() super().tearDown()
if os.path.exists(TEST_BASE):
shutil.rmtree(TEST_BASE)
def testFileEventPatternCreationMinimum(self)->None: def testFileEventPatternCreationMinimum(self)->None:
FileEventPattern("name", "path", "recipe", "file") FileEventPattern("name", "path", "recipe", "file")
@ -79,3 +92,68 @@ class CorrectnessTests(unittest.TestCase):
fep = FileEventPattern( fep = FileEventPattern(
"name", "path", "recipe", "file", outputs=outputs) "name", "path", "recipe", "file", outputs=outputs)
self.assertEqual(fep.outputs, outputs) self.assertEqual(fep.outputs, outputs)
def testFileEventPatternEventMask(self)->None:
fep = FileEventPattern("name", "path", "recipe", "file")
self.assertEqual(fep.event_mask, FILE_EVENTS)
with self.assertRaises(TypeError):
fep = FileEventPattern("name", "path", "recipe", "file",
event_mask=FILE_CREATE_EVENT)
with self.assertRaises(ValueError):
fep = FileEventPattern("name", "path", "recipe", "file",
event_mask=["nope"])
with self.assertRaises(ValueError):
fep = FileEventPattern("name", "path", "recipe", "file",
event_mask=[FILE_CREATE_EVENT, "nope"])
self.assertEqual(fep.event_mask, FILE_EVENTS)
def testWatchdogMonitorMinimum(self)->None:
to_monitor = Pipe()
from_monitor = Pipe()
WatchdogMonitor(TEST_BASE, {}, from_monitor[PIPE_WRITE],
to_monitor[PIPE_READ])
def testWatchdogMonitorEventIdentificaion(self)->None:
to_monitor = Pipe()
from_monitor = Pipe()
pattern_one = FileEventPattern(
"pattern_one", "A", "recipe_one", "file_one")
recipe = JupyterNotebookRecipe(
"recipe_one", BAREBONES_NOTEBOOK)
patterns = {
pattern_one.name: pattern_one,
}
recipes = {
recipe.name: recipe,
}
rules = create_rules(patterns, recipes)
wm = WatchdogMonitor(TEST_BASE, rules, from_monitor[PIPE_WRITE],
to_monitor[PIPE_READ])
wm.start()
open(os.path.join(TEST_BASE, "A"), "w")
if from_monitor[PIPE_READ].poll(3):
message = from_monitor[PIPE_READ].recv()
self.assertIsNotNone(message)
event, rule = message
self.assertIsNotNone(event)
self.assertIsNotNone(rule)
self.assertEqual(event.src_path, os.path.join(TEST_BASE, "A"))
open(os.path.join(TEST_BASE, "B"), "w")
if from_monitor[PIPE_READ].poll(3):
new_message = from_monitor[PIPE_READ].recv()
else:
new_message = None
self.assertIsNone(new_message)
wm.stop()

View File

@ -3,13 +3,7 @@ import jsonschema
import unittest import unittest
from recipes.jupyter_notebook_recipe import JupyterNotebookRecipe from recipes.jupyter_notebook_recipe import JupyterNotebookRecipe
from core.correctness.vars import BAREBONES_NOTEBOOK
BAREBONES_NOTEBOOK = {
"cells": [],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}
class CorrectnessTests(unittest.TestCase): class CorrectnessTests(unittest.TestCase):
def setUp(self)->None: def setUp(self)->None:

View File

@ -1,10 +1,10 @@
import unittest import unittest
from core.correctness.vars import BAREBONES_NOTEBOOK
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
from rules.file_event_jupyter_notebook_rule import FileEventJupyterNotebookRule from rules.file_event_jupyter_notebook_rule import FileEventJupyterNotebookRule
from test_recipes import BAREBONES_NOTEBOOK
class CorrectnessTests(unittest.TestCase): class CorrectnessTests(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None: