added generic rule generation
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
|
||||
from abc import ABCMeta
|
||||
from typing import Any, _SpecialForm
|
||||
|
||||
def check_input(variable:Any, expected_type:type, alt_types:list[type]=[],
|
||||
@ -70,14 +71,18 @@ 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)->None:
|
||||
strict:bool=True, min_length:int=1)->None:
|
||||
check_input(variable, dict)
|
||||
check_input(key_type, type, alt_types=[_SpecialForm])
|
||||
check_input(value_type, type, alt_types=[_SpecialForm])
|
||||
check_input(key_type, type, alt_types=[_SpecialForm, ABCMeta])
|
||||
check_input(value_type, type, alt_types=[_SpecialForm, ABCMeta])
|
||||
check_input(required_keys, list)
|
||||
check_input(optional_keys, list)
|
||||
check_input(strict, bool)
|
||||
|
||||
if len(variable) < min_length:
|
||||
raise ValueError(f"Dictionary '{variable}' is below minimum length of "
|
||||
f"{min_length}")
|
||||
|
||||
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)}' "
|
||||
@ -96,3 +101,12 @@ def valid_dict(variable:dict[Any, Any], key_type:type, value_type:type,
|
||||
if k not in required_keys and k not in optional_keys:
|
||||
raise ValueError(f"Unexpected key '{k}' should not be present "
|
||||
f"in dict '{variable}'")
|
||||
|
||||
def valid_list(variable:list[Any], entry_type:type,
|
||||
alt_types:list[type]=[], min_length:int=1)->None:
|
||||
check_input(variable, list)
|
||||
if len(variable) < min_length:
|
||||
raise ValueError(f"List '{variable}' is too short. Should be at least "
|
||||
f"of length {min_length}")
|
||||
for entry in variable:
|
||||
check_input(entry, entry_type, alt_types=alt_types)
|
||||
|
78
core/functionality.py
Normal file
78
core/functionality.py
Normal file
@ -0,0 +1,78 @@
|
||||
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
from typing import Union
|
||||
from random import SystemRandom
|
||||
|
||||
from core.meow import BasePattern, BaseRecipe, BaseRule
|
||||
from core.correctness.validation import check_input, valid_dict, valid_list
|
||||
from core.correctness.vars import CHAR_LOWERCASE, CHAR_UPPERCASE
|
||||
from patterns import *
|
||||
from recipes import *
|
||||
from rules import *
|
||||
|
||||
def check_pattern_dict(patterns, min_length=1):
|
||||
valid_dict(patterns, str, BasePattern, strict=False, min_length=min_length)
|
||||
for k, v in patterns.items():
|
||||
if k != v.name:
|
||||
raise KeyError(f"Key '{k}' indexes unexpected Pattern '{v.name}' "
|
||||
"Pattern dictionaries must be keyed with the name of the "
|
||||
"Pattern.")
|
||||
|
||||
def check_recipe_dict(recipes, min_length=1):
|
||||
valid_dict(recipes, str, BaseRecipe, strict=False, min_length=min_length)
|
||||
for k, v in recipes.items():
|
||||
if k != v.name:
|
||||
raise KeyError(f"Key '{k}' indexes unexpected Recipe '{v.name}' "
|
||||
"Recipe dictionaries must be keyed with the name of the "
|
||||
"Recipe.")
|
||||
|
||||
def generate_id(prefix:str="", length:int=16, existing_ids:list[str]=[],
|
||||
charset:str=CHAR_UPPERCASE+CHAR_LOWERCASE, attempts:int=24):
|
||||
random_length = max(length - len(prefix), 0)
|
||||
for _ in range(attempts):
|
||||
id = prefix + ''.join(SystemRandom().choice(charset)
|
||||
for _ in range(random_length))
|
||||
if id not in existing_ids:
|
||||
return id
|
||||
raise ValueError(f"Could not generate ID unique from '{existing_ids}' "
|
||||
f"using values '{charset}' and length of '{length}'.")
|
||||
|
||||
def create_rules(patterns:Union[dict[str,BasePattern],list[BasePattern]],
|
||||
recipes:Union[dict[str,BaseRecipe],list[BaseRecipe]],
|
||||
new_rules:list[BaseRule]=[])->dict[str,BaseRule]:
|
||||
check_input(patterns, dict, alt_types=[list])
|
||||
check_input(recipes, dict, alt_types=[list])
|
||||
valid_list(new_rules, BaseRule, min_length=0)
|
||||
|
||||
if isinstance(patterns, list):
|
||||
valid_list(patterns, BasePattern, min_length=0)
|
||||
patterns = {pattern.name:pattern for pattern in patterns}
|
||||
else:
|
||||
check_pattern_dict(patterns, min_length=0)
|
||||
|
||||
if isinstance(recipes, list):
|
||||
valid_list(recipes, BaseRecipe, min_length=0)
|
||||
recipes = {recipe.name:recipe for recipe in recipes}
|
||||
else:
|
||||
check_recipe_dict(recipes, min_length=0)
|
||||
|
||||
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))]}
|
||||
|
||||
for pattern in patterns.values():
|
||||
if pattern.recipe in recipes:
|
||||
key = (type(pattern).__name__,
|
||||
type(recipes[pattern.recipe]).__name__)
|
||||
if (key) in all_rules:
|
||||
rule = all_rules[key](
|
||||
generate_id(prefix="Rule_"),
|
||||
pattern,
|
||||
recipes[pattern.recipe]
|
||||
)
|
||||
rules[rule.name] = rule
|
||||
return rules
|
66
core/meow.py
66
core/meow.py
@ -2,6 +2,8 @@
|
||||
import core.correctness.vars
|
||||
import core.correctness.validation
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from typing import Any
|
||||
|
||||
class BaseRecipe:
|
||||
@ -20,21 +22,6 @@ class BaseRecipe:
|
||||
self._is_valid_requirements(requirements)
|
||||
self.requirements = requirements
|
||||
|
||||
def __init_subclass__(cls, **kwargs) -> None:
|
||||
if cls._is_valid_recipe == BaseRecipe._is_valid_recipe:
|
||||
raise NotImplementedError(
|
||||
f"Recipe '{cls.__name__}' has not implemented "
|
||||
"'_is_valid_recipe(self, recipe)' function.")
|
||||
if cls._is_valid_parameters == BaseRecipe._is_valid_parameters:
|
||||
raise NotImplementedError(
|
||||
f"Recipe '{cls.__name__}' has not implemented "
|
||||
"'_is_valid_parameters(self, parameters)' function.")
|
||||
if cls._is_valid_requirements == BaseRecipe._is_valid_requirements:
|
||||
raise NotImplementedError(
|
||||
f"Recipe '{cls.__name__}' has not implemented "
|
||||
"'_is_valid_requirements(self, requirements)' function.")
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is BaseRecipe:
|
||||
raise TypeError("BaseRecipe may not be instantiated directly")
|
||||
@ -44,17 +31,20 @@ class BaseRecipe:
|
||||
core.correctness.validation.valid_string(
|
||||
name, core.correctness.vars.VALID_RECIPE_NAME_CHARS)
|
||||
|
||||
@abstractmethod
|
||||
def _is_valid_recipe(self, recipe:Any)->None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _is_valid_parameters(self, parameters:Any)->None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _is_valid_requirements(self, requirements:Any)->None:
|
||||
pass
|
||||
|
||||
|
||||
class BasePattern:
|
||||
class BasePattern(ABC):
|
||||
name:str
|
||||
recipe:str
|
||||
parameters:dict[str, Any]
|
||||
@ -70,21 +60,6 @@ class BasePattern:
|
||||
self._is_valid_output(outputs)
|
||||
self.outputs = outputs
|
||||
|
||||
def __init_subclass__(cls, **kwargs) -> None:
|
||||
if cls._is_valid_recipe == BasePattern._is_valid_recipe:
|
||||
raise NotImplementedError(
|
||||
f"Pattern '{cls.__name__}' has not implemented "
|
||||
"'_is_valid_recipe(self, recipe)' function.")
|
||||
if cls._is_valid_parameters == BasePattern._is_valid_parameters:
|
||||
raise NotImplementedError(
|
||||
f"Pattern '{cls.__name__}' has not implemented "
|
||||
"'_is_valid_parameters(self, parameters)' function.")
|
||||
if cls._is_valid_output == BasePattern._is_valid_output:
|
||||
raise NotImplementedError(
|
||||
f"Pattern '{cls.__name__}' has not implemented "
|
||||
"'_is_valid_output(self, outputs)' function.")
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is BasePattern:
|
||||
raise TypeError("BasePattern may not be instantiated directly")
|
||||
@ -94,20 +69,25 @@ class BasePattern:
|
||||
core.correctness.validation.valid_string(
|
||||
name, core.correctness.vars.VALID_PATTERN_NAME_CHARS)
|
||||
|
||||
@abstractmethod
|
||||
def _is_valid_recipe(self, recipe:Any)->None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _is_valid_parameters(self, parameters:Any)->None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _is_valid_output(self, outputs:Any)->None:
|
||||
pass
|
||||
|
||||
|
||||
class BaseRule:
|
||||
class BaseRule(ABC):
|
||||
name:str
|
||||
pattern:BasePattern
|
||||
recipe:BaseRecipe
|
||||
pattern_type:str=""
|
||||
recipe_type:str=""
|
||||
def __init__(self, name:str, pattern:BasePattern, recipe:BaseRecipe):
|
||||
self._is_valid_name(name)
|
||||
self.name = name
|
||||
@ -115,29 +95,29 @@ class BaseRule:
|
||||
self.pattern = pattern
|
||||
self._is_valid_recipe(recipe)
|
||||
self.recipe = recipe
|
||||
self.__check_types_set()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is BaseRule:
|
||||
raise TypeError("BaseRule may not be instantiated directly")
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init_subclass__(cls, **kwargs) -> None:
|
||||
if cls._is_valid_pattern == BaseRule._is_valid_pattern:
|
||||
raise NotImplementedError(
|
||||
f"Rule '{cls.__name__}' has not implemented "
|
||||
"'_is_valid_pattern(self, pattern)' function.")
|
||||
if cls._is_valid_recipe == BaseRule._is_valid_recipe:
|
||||
raise NotImplementedError(
|
||||
f"Pattern '{cls.__name__}' has not implemented "
|
||||
"'_is_valid_recipe(self, recipe)' function.")
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
def _is_valid_name(self, name:str)->None:
|
||||
core.correctness.validation.valid_string(
|
||||
name, core.correctness.vars.VALID_RULE_NAME_CHARS)
|
||||
|
||||
@abstractmethod
|
||||
def _is_valid_pattern(self, pattern:Any)->None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _is_valid_recipe(self, recipe:Any)->None:
|
||||
pass
|
||||
|
||||
def __check_types_set(self)->None:
|
||||
if self.pattern_type == "":
|
||||
raise AttributeError(f"Rule Class '{self.__class__.__name__}' "
|
||||
"does not set a pattern_type.")
|
||||
if self.recipe_type == "":
|
||||
raise AttributeError(f"Rule Class '{self.__class__.__name__}' "
|
||||
"does not set a recipe_type.")
|
||||
|
2
patterns/__init__.py
Normal file
2
patterns/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
from patterns.file_event_pattern import FileEventPattern
|
@ -34,11 +34,11 @@ class FileEventPattern(BasePattern):
|
||||
valid_string(triggering_file, VALID_VARIABLE_NAME_CHARS)
|
||||
|
||||
def _is_valid_parameters(self, parameters:dict[str,Any])->None:
|
||||
valid_dict(parameters, str, Any, strict=False)
|
||||
valid_dict(parameters, str, Any, strict=False, min_length=0)
|
||||
for k in parameters.keys():
|
||||
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
||||
|
||||
def _is_valid_output(self, outputs:dict[str,str])->None:
|
||||
valid_dict(outputs, str, str, strict=False)
|
||||
valid_dict(outputs, str, str, strict=False, min_length=0)
|
||||
for k in outputs.keys():
|
||||
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
||||
|
2
recipes/__init__.py
Normal file
2
recipes/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
from recipes.jupyter_notebook_recipe import JupyterNotebookRecipe
|
@ -36,11 +36,11 @@ class JupyterNotebookRecipe(BaseRecipe):
|
||||
nbformat.validate(recipe)
|
||||
|
||||
def _is_valid_parameters(self, parameters:dict[str,Any])->None:
|
||||
valid_dict(parameters, str, Any, strict=False)
|
||||
valid_dict(parameters, str, Any, strict=False, min_length=0)
|
||||
for k in parameters.keys():
|
||||
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
||||
|
||||
def _is_valid_requirements(self, requirements:dict[str,Any])->None:
|
||||
valid_dict(requirements, str, Any, strict=False)
|
||||
valid_dict(requirements, str, Any, strict=False, min_length=0)
|
||||
for k in requirements.keys():
|
||||
valid_string(k, VALID_VARIABLE_NAME_CHARS)
|
||||
|
2
rules/__init__.py
Normal file
2
rules/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
from rules.file_event_jupyter_notebook_rule import FileEventJupyterNotebookRule
|
@ -5,6 +5,8 @@ from patterns.file_event_pattern import FileEventPattern
|
||||
from recipes.jupyter_notebook_recipe import JupyterNotebookRecipe
|
||||
|
||||
class FileEventJupyterNotebookRule(BaseRule):
|
||||
pattern_type = "FileEventPattern"
|
||||
recipe_type = "JupyterNotebookRecipe"
|
||||
def __init__(self, name: str, pattern:FileEventPattern,
|
||||
recipe:JupyterNotebookRecipe):
|
||||
super().__init__(name, pattern, recipe)
|
||||
@ -18,3 +20,6 @@ class FileEventJupyterNotebookRule(BaseRule):
|
||||
|
||||
def _is_valid_recipe(self, recipe:JupyterNotebookRecipe) -> None:
|
||||
check_input(recipe, JupyterNotebookRecipe)
|
||||
|
||||
def _set_pattern_type(self)->None:
|
||||
self.pattern_type
|
||||
|
95
tests/test_functionality.py
Normal file
95
tests/test_functionality.py
Normal file
@ -0,0 +1,95 @@
|
||||
|
||||
import unittest
|
||||
|
||||
from core.correctness.vars import CHAR_LOWERCASE, CHAR_UPPERCASE
|
||||
from core.functionality import create_rules, generate_id
|
||||
from core.meow import BaseRule
|
||||
from patterns.file_event_pattern import FileEventPattern
|
||||
from recipes.jupyter_notebook_recipe import JupyterNotebookRecipe
|
||||
|
||||
BAREBONES_NOTEBOOK = {
|
||||
"cells": [],
|
||||
"metadata": {},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
||||
valid_pattern_one = FileEventPattern(
|
||||
"pattern_one", "path_one", "recipe_one", "file_one")
|
||||
valid_pattern_two = FileEventPattern(
|
||||
"pattern_two", "path_two", "recipe_two", "file_two")
|
||||
|
||||
valid_recipe_one = JupyterNotebookRecipe(
|
||||
"recipe_one", BAREBONES_NOTEBOOK)
|
||||
valid_recipe_two = JupyterNotebookRecipe(
|
||||
"recipe_two", BAREBONES_NOTEBOOK)
|
||||
|
||||
class CorrectnessTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
return super().setUp()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
return super().tearDown()
|
||||
|
||||
def testCreateRulesMinimum(self)->None:
|
||||
create_rules({}, {})
|
||||
|
||||
def testGenerateIDWorking(self)->None:
|
||||
id = generate_id()
|
||||
self.assertEqual(len(id), 16)
|
||||
for i in range(len(id)):
|
||||
self.assertIn(id[i], CHAR_UPPERCASE+CHAR_LOWERCASE)
|
||||
|
||||
# In extrememly rare cases this may fail due to randomness in algorithm
|
||||
new_id = generate_id(existing_ids=[id])
|
||||
self.assertNotEqual(id, new_id)
|
||||
|
||||
another_id = generate_id(length=32)
|
||||
self.assertEqual(len(another_id), 32)
|
||||
|
||||
again_id = generate_id(charset="a")
|
||||
for i in range(len(again_id)):
|
||||
self.assertIn(again_id[i], "a")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
generate_id(length=2, charset="a", existing_ids=["aa"])
|
||||
|
||||
prefix_id = generate_id(length=4, prefix="Test")
|
||||
self.assertEqual(prefix_id, "Test")
|
||||
|
||||
prefix_id = generate_id(prefix="Test")
|
||||
self.assertEqual(len(prefix_id), 16)
|
||||
self.assertTrue(prefix_id.startswith("Test"))
|
||||
|
||||
def testCreateRulesPatternsAndRecipesDicts(self)->None:
|
||||
patterns = {
|
||||
valid_pattern_one.name: valid_pattern_one,
|
||||
valid_pattern_two.name: valid_pattern_two
|
||||
}
|
||||
recipes = {
|
||||
valid_recipe_one.name: valid_recipe_one,
|
||||
valid_recipe_two.name: valid_recipe_two
|
||||
}
|
||||
rules = create_rules(patterns, recipes)
|
||||
self.assertIsInstance(rules, dict)
|
||||
self.assertEqual(len(rules), 2)
|
||||
for k, rule in rules.items():
|
||||
self.assertIsInstance(k, str)
|
||||
self.assertIsInstance(rule, BaseRule)
|
||||
self.assertEqual(k, rule.name)
|
||||
|
||||
def testCreateRulesMisindexedPatterns(self)->None:
|
||||
patterns = {
|
||||
valid_pattern_two.name: valid_pattern_one,
|
||||
valid_pattern_one.name: valid_pattern_two
|
||||
}
|
||||
with self.assertRaises(KeyError):
|
||||
create_rules(patterns, {})
|
||||
|
||||
def testCreateRulesMisindexedRecipes(self)->None:
|
||||
recipes = {
|
||||
valid_recipe_two.name: valid_recipe_one,
|
||||
valid_recipe_one.name: valid_recipe_two
|
||||
}
|
||||
with self.assertRaises(KeyError):
|
||||
create_rules({}, recipes)
|
@ -76,3 +76,4 @@ class CorrectnessTests(unittest.TestCase):
|
||||
fejnr = FileEventJupyterNotebookRule("name", fep, jnr)
|
||||
|
||||
self.assertEqual(fejnr.recipe, jnr)
|
||||
|
||||
|
@ -3,7 +3,8 @@ import unittest
|
||||
|
||||
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_list
|
||||
from core.correctness.vars import VALID_NAME_CHARS
|
||||
|
||||
|
||||
@ -85,3 +86,23 @@ class CorrectnessTests(unittest.TestCase):
|
||||
def testValidDictOverlyStrict(self)->None:
|
||||
with self.assertRaises(ValueError):
|
||||
valid_dict({"a": 0, "b": 1}, str, int)
|
||||
|
||||
def testValidListMinimum(self)->None:
|
||||
valid_list([1, 2, 3], int)
|
||||
valid_list(["1", "2", "3"], str)
|
||||
valid_list([1], int)
|
||||
|
||||
def testValidListAltTypes(self)->None:
|
||||
valid_list([1, "2", 3], int, alt_types=[str])
|
||||
|
||||
def testValidListMismatchedNotList(self)->None:
|
||||
with self.assertRaises(TypeError):
|
||||
valid_list((1, 2, 3), int)
|
||||
|
||||
def testValidListMismatchedType(self)->None:
|
||||
with self.assertRaises(TypeError):
|
||||
valid_list([1, 2, 3], str)
|
||||
|
||||
def testValidListMinLength(self)->None:
|
||||
with self.assertRaises(ValueError):
|
||||
valid_list([1, 2, 3], str, min_length=10)
|
||||
|
Reference in New Issue
Block a user