added generic rule generation

This commit is contained in:
PatchOfScotland
2022-12-12 11:01:26 +01:00
parent ccaca5e60e
commit 4041b86343
12 changed files with 251 additions and 51 deletions

View File

@ -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
View 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

View File

@ -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
View File

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

View File

@ -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
View File

@ -0,0 +1,2 @@
from recipes.jupyter_notebook_recipe import JupyterNotebookRecipe

View File

@ -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
View File

@ -0,0 +1,2 @@
from rules.file_event_jupyter_notebook_rule import FileEventJupyterNotebookRule

View File

@ -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

View 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)

View File

@ -76,3 +76,4 @@ class CorrectnessTests(unittest.TestCase):
fejnr = FileEventJupyterNotebookRule("name", fep, jnr)
self.assertEqual(fejnr.recipe, jnr)

View File

@ -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)