diff --git a/a_simple_example/index.html b/a_simple_example/index.html index 005cb34..3fdd5d1 100644 --- a/a_simple_example/index.html +++ b/a_simple_example/index.html @@ -914,12 +914,12 @@
As we already mentioned: Arta is a simple python rules engine.
But what do we mean by rules engine?
True
or False
(i.e., we say verified or not verified) triggering an action (i.e., any python callable object).Imagine the following use case:
Your are managing a superhero school and you want to use some school rules in your python app.
The rules (intentionally simple) are:
Admission rules
If the applicant has a school authorized power then he is admitted,
Else he is not.
Course selection rules
If he is speaking french and his age is known then he must take the \"french\" course,
Else if his age is unknown (e.g., it's a very old superhero), then he must take the \"senior\" course,
Else if he is not speaking french, then he must take the \"international\" course.
Send favorite meal rules
If he is admitted and has a prefered dish, then we send an email to the school cook with the dish name.
"},{"location":"a_simple_example/#rules","title":"Rules","text":"You can define above rules for Arta in one simple YAML file :
---\nrules:\n default_rule_set:\n admission:\n ADMITTED:\n simple_condition: input.power==\"strength\" or input.power==\"fly\"\n action: set_admission\n action_parameters:\n value: True \n NOT_ADMITTED:\n simple_condition: null\n action: set_admission\n action_parameters:\n value: False\n course:\n FRENCH:\n simple_condition: input.language==\"french\" and input.age!=None\n action: set_course\n action_parameters:\n value: french\n SENIOR:\n simple_condition: input.age==None\n action: set_course\n action_parameters:\n value: senior\n INTERNATIONAL:\n simple_condition: input.language!=\"french\"\n action: set_course\n action_parameters:\n value: international\n favorite_meal:\n EMAIL:\n simple_condition: input.favorite_meal!=None\n action: send_email\n action_parameters:\n mail_to: cook@super-heroes.test\n mail_content: \"Thanks for preparing once a month the following dish:\"\n meal: input.favorite_meal\n\nactions_source_modules:\n - my_folder.actions\n
Simple Conditions
This configuration uses what we called simple conditions, you can find out more here.
"},{"location":"a_simple_example/#actions","title":"Actions","text":"An action is triggered when the conditions are verified (i.e., True
).
Actions are defined by the following keys in the previous YAML file:
action: set_admission # (1)\n action_parameters: # (2)\n value: True \n
The action function's implementation has to be located in the configured module:
actions_source_modules:\n - my_folder.actions\n
And could be for example (intentionally simple) in actions.py
:
def set_admission(value: bool, **kwargs: Any) -> dict[str, bool]:\n \"\"\"Return a dictionary containing the admission result.\"\"\"\n return {\"is_admitted\": value}\n\n\ndef set_course(course_id: str, **kwargs: Any) -> dict[str, str]:\n \"\"\"Return the course id as a dictionary.\"\"\"\n return {\"course_id\": course_id}\n\n\ndef send_email(mail_to: str, mail_content: str, meal: str, **kwargs: Any) -> bool:\n \"\"\"Send an email.\"\"\"\n result: str | None = None\n\n if meal is not None:\n # API call here\n result = \"sent\"\n\n return result\n
**kwargs
**kwargs is mandatory in action functions.
"},{"location":"a_simple_example/#engine","title":"Engine","text":"The rules engine is responsible for evaluating the configured rules against some data (usually named \"input data\").
In our use case, the input data could be a list of applicants:
applicants = [\n {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"english\",\n \"power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n },\n {\n \"id\": 2,\n \"name\": \"Batman\",\n \"civilian_name\": \"Bruce Wayne\",\n \"age\": 33,\n \"city\": \"Gotham City\",\n \"language\": \"english\",\n \"power\": \"strength\",\n \"favorite_meal\": None,\n \"secret_weakness\": \"Feel alone\",\n \"weapons\": [\"Hands\", \"Batarang\"],\n },\n {\n \"id\": 3,\n \"name\": \"Wonder Woman\",\n \"civilian_name\": \"Diana Prince\",\n \"age\": 5000,\n \"city\": \"Island of Themyscira\",\n \"language\": \"french\",\n \"power\": \"strength\",\n \"favorite_meal\": None,\n \"secret_weakness\": \"Lost faith in humanity\",\n \"weapons\": [\"Magic lasso\", \"Bulletproof bracelets\", \"Sword\", \"Shield\"],\n },\n]\n
Now, let's apply the rules on a single applicant:
from arta import RulesEngine\n\neng = RulesEngine(config_path=\"/to/my/config/dir\") # (1)\n\nresult = eng.apply_rules(input_data=applicants[0])\n\nprint(result) # (2)\n# {\n# \"admission\": {\"is_admitted\": True},\n# \"course\": {\"course_id\": \"senior\"},\n# \"favorite_meal\": \"sent\"\n# }\n
In the rules engine result, we have 3 outputs:
\"admission\": {\"is_admitted\": True},
\"course\": {\"course_id\": \"senior\"},
\"favorite_meal\": \"sent\"
Each corresponds to one of these rules.
Here, we can apply the rules to all the data set (3 applicants) with a simple dictionary comprehension:
from arta import RulesEngine\n\nresults = {applicant[\"name\"]: eng.apply_rules(applicant) for applicant in applicants}\n\nprint(results)\n# {\n# \"Superman\": {\n# \"admission\": {\"is_admitted\": True}, \n# \"course\": {\"course_id\": \"senior\"}, \n# \"favorite_meal\": \"sent\"},\n# \"Batman\": {\n# \"admission\": {\"is_admitted\": True},\n# \"course\": {\"course_id\": \"international\"},\n# \"favorite_meal\": None,\n# },\n# \"Wonder Woman\": {\n# \"admission\": {\"is_admitted\": True},\n# \"course\": {\"course_id\": \"french\"},\n# \"favorite_meal\": None,\n# }\n# }\n
It is the end of this Arta's overview. If you want now to go deeper in how to use Arta, click here.
"},{"location":"api_reference/","title":"API Reference","text":""},{"location":"api_reference/#_enginepy","title":"_engine.py","text":"Module implementing the rules engine.
Class: RulesEngine
"},{"location":"api_reference/#arta._engine.RulesEngine","title":"RulesEngine
","text":"The Rules Engine is in charge of executing different groups of rules of a given rule set on user input data.
Attributes:
Name Type Descriptionrules
dict[str, dict[str, list[Rule]]]
A dictionary of rules with k: rule set, v: (k: rule group, v: list of rule instances).
Source code inarta/_engine.py
class RulesEngine:\n \"\"\"The Rules Engine is in charge of executing different groups of rules of a given rule set on user input data.\n\n Attributes:\n rules: A dictionary of rules with k: rule set, v: (k: rule group, v: list of rule instances).\n \"\"\"\n\n # ==== Class constants ====\n\n # Rule related config keys\n CONST_RULE_SETS_CONF_KEY: str = \"rules\"\n CONST_DFLT_RULE_SET_ID: str = \"default_rule_set\"\n CONST_STD_RULE_CONDITION_CONF_KEY: str = \"condition\"\n CONST_ACTION_CONF_KEY: str = \"action\"\n CONST_ACTION_PARAMETERS_CONF_KEY: str = \"action_parameters\"\n\n # Condition related config keys\n CONST_STD_CONDITIONS_CONF_KEY: str = \"conditions\"\n CONST_CONDITION_VALIDATION_FUNCTION_CONF_KEY: str = \"validation_function\"\n CONST_CONDITION_DESCRIPTION_CONF_KEY: str = \"description\"\n CONST_CONDITION_VALIDATION_PARAMETERS_CONF_KEY: str = \"condition_parameters\"\n CONST_USER_CONDITION_STRING: str = \"USER_CONDITION\"\n\n # Built-in factory mapping\n BUILTIN_FACTORY_MAPPING: dict[str, type[BaseCondition]] = {\n \"condition\": StandardCondition,\n \"simple_condition\": SimpleCondition,\n }\n\n def __init__(\n self,\n *,\n rules_dict: dict[str, dict[str, Any]] | None = None,\n config_path: str | None = None,\n ) -> None:\n \"\"\"Initialize the rules.\n\n 2 possibilities: either 'rules_dict', or 'config_path', not both.\n\n Args:\n rules_dict: A dictionary containing the rules' definitions.\n config_path: Path of a directory containing the YAML files.\n\n Raises:\n KeyError: Key not found.\n TypeError: Wrong type.\n \"\"\"\n # Var init.\n factory_mapping_classes: dict[str, type[BaseCondition]] = {}\n std_condition_instances: dict[str, StandardCondition] = {}\n\n if config_path is not None and rules_dict is not None:\n raise ValueError(\"RulesEngine takes only one parameter: 'rules_dict' or 'config_path', not both.\")\n\n # Init. default parsing_error_strategy (probably not needed because already defined elsewhere)\n self._parsing_error_strategy: ParsingErrorStrategy = ParsingErrorStrategy.RAISE\n\n # Initialize directly with a rules dict\n if rules_dict is not None:\n # Data validation\n RulesDict.parse_obj(rules_dict)\n\n # Edge cases data validation\n if not isinstance(rules_dict, dict):\n raise TypeError(f\"'rules_dict' must be dict type, not '{type(rules_dict)}'\")\n elif len(rules_dict) == 0:\n raise KeyError(\"'rules_dict' couldn't be empty.\")\n\n # Attribute definition\n self.rules: dict[str, dict[str, list[Rule]]] = self._adapt_user_rules_dict(rules_dict)\n\n # Initialize with a config_path\n elif config_path is not None:\n # Load config in attribute\n config_dict: dict[str, Any] = load_config(config_path)\n\n # Data validation\n config: Configuration = Configuration(**config_dict)\n\n if config.parsing_error_strategy is not None:\n # Set parsing error handling strategy from config\n self._parsing_error_strategy = ParsingErrorStrategy(config.parsing_error_strategy)\n\n # dict of available action functions (k: function name, v: function object)\n action_modules: list[str] = config.actions_source_modules\n action_functions: dict[str, Callable] = self._get_object_from_source_modules(action_modules)\n\n # dict of available standard condition functions (k: function name, v: function object)\n condition_modules: list[str] = (\n config.conditions_source_modules if config.conditions_source_modules is not None else []\n )\n std_condition_functions: dict[str, Callable] = self._get_object_from_source_modules(condition_modules)\n\n # Dictionary of condition instances (k: condition id, v: instance), built from config data\n if len(std_condition_functions) > 0:\n std_condition_instances = self._build_std_conditions(\n config=config.dict(), condition_functions_dict=std_condition_functions\n )\n\n # User-defined/custom conditions\n if config.condition_factory_mapping is not None and config.custom_classes_source_modules is not None:\n # dict of custom condition classes (k: classe name, v: class object)\n custom_condition_classes: dict[str, type[BaseCondition]] = self._get_object_from_source_modules(\n config.custom_classes_source_modules\n )\n\n # Build a factory mapping dictionary (k: conf key, v:class object)\n factory_mapping_classes.update(\n {\n conf_key: custom_condition_classes[class_name]\n for conf_key, class_name in config.condition_factory_mapping.items()\n }\n )\n\n # Arta built-in conditions\n factory_mapping_classes.update(self.BUILTIN_FACTORY_MAPPING)\n\n # Attribute definition\n self.rules = self._build_rules(\n std_condition_instances=std_condition_instances,\n action_functions=action_functions,\n config=config.dict(),\n factory_mapping_classes=factory_mapping_classes,\n )\n else:\n raise ValueError(\"RulesEngine needs a parameter: 'rule_dict' or 'config_path'.\")\n\n def apply_rules(\n self, input_data: dict[str, Any], *, rule_set: str | None = None, verbose: bool = False, **kwargs: Any\n ) -> dict[str, Any]:\n \"\"\"Apply the rules and return results.\n\n For each rule group of a given rule set, rules are applied sequentially,\n The loop is broken when a rule is applied (an action is triggered).\n Then, the next rule group is evaluated.\n And so on...\n\n This means that the order of the rules in the configuration file\n (e.g., rules.yaml) is meaningful.\n\n Args:\n input_data: Input data to apply rules on.\n rule_set: Apply rules associated with the specified rule set.\n verbose: If True, add extra ids (group_id, rule_id) for result explicability.\n **kwargs: For user extra arguments.\n\n Returns:\n A dictionary containing the rule groups' results (k: group id, v: action result).\n\n Raises:\n TypeError: Wrong type.\n KeyError: Key not found.\n \"\"\"\n # Input_data validation\n if not isinstance(input_data, dict):\n raise TypeError(f\"'input_data' must be dict type, not '{type(input_data)}'\")\n elif len(input_data) == 0:\n raise KeyError(\"'input_data' couldn't be empty.\")\n\n # Var init.\n input_data_copy: dict[str, Any] = copy.deepcopy(input_data)\n\n # Prepare the result key\n input_data_copy[\"output\"] = {}\n\n # If there is no given rule set param. and there is only one rule set in self.rules\n # and its value is 'default_rule_set', look for this one (rule_set='default_rule_set')\n if rule_set is None and len(self.rules) == 1 and self.rules.get(self.CONST_DFLT_RULE_SET_ID) is not None:\n rule_set = self.CONST_DFLT_RULE_SET_ID\n\n # Check if given rule set is in self.rules?\n if rule_set not in self.rules:\n raise KeyError(\n f\"Rule set '{rule_set}' not found in the rules, available rule sets are : {list(self.rules.keys())}.\"\n )\n\n # Var init.\n results_dict: dict[str, Any] = {\"verbosity\": {\"rule_set\": rule_set, \"results\": []}}\n\n # Groups' loop\n for group_id, rules_list in self.rules[rule_set].items():\n # Initialize result of the rule group with None\n results_dict[group_id] = None\n\n # Rules' loop (inside a group)\n for rule in rules_list:\n # Apply rules\n action_result, rule_details = rule.apply(\n input_data_copy, parsing_error_strategy=self._parsing_error_strategy, **kwargs\n )\n\n # Check if the rule has been applied (= action activated)\n if \"action_result\" in rule_details:\n # Save result and details\n results_dict[group_id] = action_result\n results_dict[\"verbosity\"][\"results\"].append(rule_details)\n\n # Update input data with current result with key 'output' (can be used in next rules)\n input_data_copy[\"output\"][group_id] = copy.deepcopy(results_dict[group_id])\n\n # We can only have one result per group => break when \"action_result\" in rule_details\n break\n\n # Handling non-verbose mode\n if not verbose:\n results_dict.pop(\"verbosity\")\n\n return results_dict\n\n @staticmethod\n def _get_object_from_source_modules(module_list: list[str]) -> dict[str, Any]:\n \"\"\"(Protected)\n Collect all functions defined in the list of modules.\n\n Args:\n module_list: List of source module names.\n\n Returns:\n Dictionary with objects found in the modules.\n \"\"\"\n object_dict: dict[str, Any] = {}\n\n for module_name in module_list:\n # Import module\n mod: ModuleType = importlib.import_module(module_name)\n\n # Collect functions\n module_functions: dict[str, Any] = {key: val for key, val in getmembers(mod, isfunction)}\n object_dict.update(module_functions)\n\n # Collect classes\n module_classes: dict[str, Any] = {key: val for key, val in getmembers(mod, isclass)}\n object_dict.update(module_classes)\n\n return object_dict\n\n def _build_rules(\n self,\n std_condition_instances: dict[str, StandardCondition],\n action_functions: dict[str, Callable],\n config: dict[str, Any],\n factory_mapping_classes: dict[str, type[BaseCondition]],\n ) -> dict[str, dict[str, list[Any]]]:\n \"\"\"(Protected)\n Return a dictionary of Rule instances built from the configuration.\n\n Args:\n rule_sets: Sets of rules to be loaded in the Rules Engine (as needed by further uses).\n std_condition_instances: Dictionary of condition instances (k: condition id, v: StandardCondition instance)\n actions_dict: Dictionary of action functions (k: action name, v: Callable)\n config: Dictionary of the imported configuration from yaml files.\n factory_mapping_classes: A mapping dictionary (k: condition conf. key, v: custom class object)\n\n Returns:\n A dictionary of rules.\n \"\"\"\n # Var init.\n rules_dict: dict[str, dict[str, list[Any]]] = {}\n\n # Retrieve rule set ids from config\n rule_set_ids: list[str] = list(config[self.CONST_RULE_SETS_CONF_KEY].keys())\n\n # Going all way down to the rules (rule set > rule group > rule)\n for set_id in rule_set_ids:\n rules_conf: dict[str, Any] = config[self.CONST_RULE_SETS_CONF_KEY][set_id]\n rules_dict[set_id] = {}\n rule_set_dict: dict[str, list[Any]] = rules_dict[set_id]\n\n # Looping throught groups\n for group_id, group_rules in rules_conf.items():\n # Initialize list or rules in the group\n rule_set_dict[group_id] = []\n\n # Looping through rules (inside a group)\n for rule_id, rule_dict in group_rules.items():\n # Get action function\n action_function_name: str = rule_dict[self.CONST_ACTION_CONF_KEY]\n\n if action_function_name not in action_functions:\n raise KeyError(f\"Unknwown action function : {action_function_name}\")\n\n action: Callable = action_functions[action_function_name]\n\n # Look for condition conf. keys inside the rule\n condition_conf_keys: set[str] = set(rule_dict.keys()) - {\n self.CONST_ACTION_CONF_KEY,\n self.CONST_ACTION_PARAMETERS_CONF_KEY,\n }\n\n # Store the cond. expressions with the same order as in the configuration file (very important)\n condition_exprs: dict[str, str | None] = {\n key: value for key, value in rule_dict.items() if key in condition_conf_keys\n }\n\n # Create the corresponding Rule instance\n rule: Rule = Rule(\n set_id=set_id,\n group_id=group_id,\n rule_id=rule_id,\n action=action,\n action_parameters=rule_dict[self.CONST_ACTION_PARAMETERS_CONF_KEY],\n condition_exprs=condition_exprs,\n std_condition_instances=std_condition_instances,\n condition_factory_mapping=factory_mapping_classes,\n )\n rule_set_dict[group_id].append(rule)\n\n return rules_dict\n\n def _build_std_conditions(\n self, config: dict[str, Any], condition_functions_dict: dict[str, Callable]\n ) -> dict[str, StandardCondition]:\n \"\"\"(Protected)\n Return a dictionary of Condition instances built from the configuration file.\n\n Args:\n config: Dictionary of the imported configuration from yaml files.\n condition_functions_dict: A dictionary where k:condition id, v:Callable (validation function).\n\n Returns:\n A dictionary of StandardCondition instances (k: condition id, v: StandardCondition instance).\n \"\"\"\n # Var init.\n conditions_dict: dict[str, StandardCondition] = {}\n\n # Condition configuration (under conditions' key)\n conditions_conf: dict[str, dict[str, Any]] = config[self.CONST_STD_CONDITIONS_CONF_KEY]\n\n # Looping through conditions (inside a group)\n for condition_id, condition_params in conditions_conf.items():\n # Get condition validation function\n validation_function_name: str = condition_params[self.CONST_CONDITION_VALIDATION_FUNCTION_CONF_KEY]\n\n if validation_function_name not in condition_functions_dict:\n raise KeyError(f\"Unknwown validation function : {validation_function_name}\")\n\n # Get Callable from function name\n validation_function: Callable = condition_functions_dict[validation_function_name]\n\n # Create Condition instance\n condition_instance: StandardCondition = StandardCondition(\n condition_id=condition_id,\n description=condition_params[self.CONST_CONDITION_DESCRIPTION_CONF_KEY],\n validation_function=validation_function,\n validation_function_parameters=condition_params[self.CONST_CONDITION_VALIDATION_PARAMETERS_CONF_KEY],\n )\n conditions_dict[condition_id] = condition_instance\n\n return conditions_dict\n\n def _adapt_user_rules_dict(self, rules_dict: dict[str, dict[str, Any]]) -> dict[str, dict[str, list[Any]]]:\n \"\"\"(Protected)\n Return a dictionary of Rule's instances built from user's rules dictionary.\n\n Args:\n rules_dict: User raw rules dictionary.\n\n Returns:\n A rules dictionary made from the user input rules.\n \"\"\"\n # Var init.\n rules_dict_formatted: dict[str, list[Any]] = {}\n\n # Looping throught groups\n for group_id, group_rules in rules_dict.items():\n # Initialize list or rules in the group\n rules_dict_formatted[group_id] = []\n\n # Looping through rules (inside a group)\n for rule_id, rule_dict in group_rules.items():\n # Get action function\n action = rule_dict[\"action\"]\n\n # Trigger if not **kwargs\n if \"kwargs\" not in inspect.signature(action).parameters:\n raise KeyError(f\"The action function {action} must have a '**kwargs' parameter.\")\n\n # Create Rule instance\n rule = Rule(\n set_id=self.CONST_DFLT_RULE_SET_ID,\n group_id=group_id,\n rule_id=rule_id,\n action=action,\n action_parameters=rule_dict.get(self.CONST_ACTION_PARAMETERS_CONF_KEY),\n condition_exprs={self.CONST_STD_RULE_CONDITION_CONF_KEY: self.CONST_USER_CONDITION_STRING}\n if self.CONST_STD_RULE_CONDITION_CONF_KEY in rule_dict\n and rule_dict.get(self.CONST_STD_RULE_CONDITION_CONF_KEY) is not None\n else {self.CONST_STD_RULE_CONDITION_CONF_KEY: None},\n std_condition_instances={\n self.CONST_USER_CONDITION_STRING: StandardCondition(\n condition_id=self.CONST_USER_CONDITION_STRING,\n description=\"Automatic description\",\n validation_function=rule_dict.get(self.CONST_STD_RULE_CONDITION_CONF_KEY),\n validation_function_parameters=rule_dict.get(\n self.CONST_CONDITION_VALIDATION_PARAMETERS_CONF_KEY\n ),\n )\n },\n condition_factory_mapping=self.BUILTIN_FACTORY_MAPPING,\n )\n rules_dict_formatted[group_id].append(rule)\n\n return {self.CONST_DFLT_RULE_SET_ID: rules_dict_formatted}\n\n def __str__(self) -> str:\n \"\"\"Object human string representation (called by str()).\n\n Returns:\n A string representation of the instance.\n \"\"\"\n # Vars init.\n attrs_str: str = \"\"\n\n # Get some instance attributes infos\n class_name: str = self.__class__.__name__\n attrs: list[tuple[str, Any]] = [\n attr\n for attr in inspect.getmembers(self)\n if not (\n attr[0].startswith(\"_\")\n or attr[0].startswith(\"CONST_\")\n or isinstance(attr[1], (FunctionType, MethodType))\n )\n ]\n\n # Build string representation\n for attr, val in attrs:\n attrs_str += f\"{attr}={str(val)}, \"\n\n return f\"{class_name}({attrs_str})\"\n
"},{"location":"api_reference/#arta._engine.RulesEngine.__init__","title":"__init__(*, rules_dict=None, config_path=None)
","text":"Initialize the rules.
2 possibilities: either 'rules_dict', or 'config_path', not both.
Parameters:
Name Type Description Defaultrules_dict
dict[str, dict[str, Any]] | None
A dictionary containing the rules' definitions.
None
config_path
str | None
Path of a directory containing the YAML files.
None
Raises:
Type DescriptionKeyError
Key not found.
TypeError
Wrong type.
Source code inarta/_engine.py
def __init__(\n self,\n *,\n rules_dict: dict[str, dict[str, Any]] | None = None,\n config_path: str | None = None,\n) -> None:\n \"\"\"Initialize the rules.\n\n 2 possibilities: either 'rules_dict', or 'config_path', not both.\n\n Args:\n rules_dict: A dictionary containing the rules' definitions.\n config_path: Path of a directory containing the YAML files.\n\n Raises:\n KeyError: Key not found.\n TypeError: Wrong type.\n \"\"\"\n # Var init.\n factory_mapping_classes: dict[str, type[BaseCondition]] = {}\n std_condition_instances: dict[str, StandardCondition] = {}\n\n if config_path is not None and rules_dict is not None:\n raise ValueError(\"RulesEngine takes only one parameter: 'rules_dict' or 'config_path', not both.\")\n\n # Init. default parsing_error_strategy (probably not needed because already defined elsewhere)\n self._parsing_error_strategy: ParsingErrorStrategy = ParsingErrorStrategy.RAISE\n\n # Initialize directly with a rules dict\n if rules_dict is not None:\n # Data validation\n RulesDict.parse_obj(rules_dict)\n\n # Edge cases data validation\n if not isinstance(rules_dict, dict):\n raise TypeError(f\"'rules_dict' must be dict type, not '{type(rules_dict)}'\")\n elif len(rules_dict) == 0:\n raise KeyError(\"'rules_dict' couldn't be empty.\")\n\n # Attribute definition\n self.rules: dict[str, dict[str, list[Rule]]] = self._adapt_user_rules_dict(rules_dict)\n\n # Initialize with a config_path\n elif config_path is not None:\n # Load config in attribute\n config_dict: dict[str, Any] = load_config(config_path)\n\n # Data validation\n config: Configuration = Configuration(**config_dict)\n\n if config.parsing_error_strategy is not None:\n # Set parsing error handling strategy from config\n self._parsing_error_strategy = ParsingErrorStrategy(config.parsing_error_strategy)\n\n # dict of available action functions (k: function name, v: function object)\n action_modules: list[str] = config.actions_source_modules\n action_functions: dict[str, Callable] = self._get_object_from_source_modules(action_modules)\n\n # dict of available standard condition functions (k: function name, v: function object)\n condition_modules: list[str] = (\n config.conditions_source_modules if config.conditions_source_modules is not None else []\n )\n std_condition_functions: dict[str, Callable] = self._get_object_from_source_modules(condition_modules)\n\n # Dictionary of condition instances (k: condition id, v: instance), built from config data\n if len(std_condition_functions) > 0:\n std_condition_instances = self._build_std_conditions(\n config=config.dict(), condition_functions_dict=std_condition_functions\n )\n\n # User-defined/custom conditions\n if config.condition_factory_mapping is not None and config.custom_classes_source_modules is not None:\n # dict of custom condition classes (k: classe name, v: class object)\n custom_condition_classes: dict[str, type[BaseCondition]] = self._get_object_from_source_modules(\n config.custom_classes_source_modules\n )\n\n # Build a factory mapping dictionary (k: conf key, v:class object)\n factory_mapping_classes.update(\n {\n conf_key: custom_condition_classes[class_name]\n for conf_key, class_name in config.condition_factory_mapping.items()\n }\n )\n\n # Arta built-in conditions\n factory_mapping_classes.update(self.BUILTIN_FACTORY_MAPPING)\n\n # Attribute definition\n self.rules = self._build_rules(\n std_condition_instances=std_condition_instances,\n action_functions=action_functions,\n config=config.dict(),\n factory_mapping_classes=factory_mapping_classes,\n )\n else:\n raise ValueError(\"RulesEngine needs a parameter: 'rule_dict' or 'config_path'.\")\n
"},{"location":"api_reference/#arta._engine.RulesEngine.__str__","title":"__str__()
","text":"Object human string representation (called by str()).
Returns:
Type Descriptionstr
A string representation of the instance.
Source code inarta/_engine.py
def __str__(self) -> str:\n \"\"\"Object human string representation (called by str()).\n\n Returns:\n A string representation of the instance.\n \"\"\"\n # Vars init.\n attrs_str: str = \"\"\n\n # Get some instance attributes infos\n class_name: str = self.__class__.__name__\n attrs: list[tuple[str, Any]] = [\n attr\n for attr in inspect.getmembers(self)\n if not (\n attr[0].startswith(\"_\")\n or attr[0].startswith(\"CONST_\")\n or isinstance(attr[1], (FunctionType, MethodType))\n )\n ]\n\n # Build string representation\n for attr, val in attrs:\n attrs_str += f\"{attr}={str(val)}, \"\n\n return f\"{class_name}({attrs_str})\"\n
"},{"location":"api_reference/#arta._engine.RulesEngine.apply_rules","title":"apply_rules(input_data, *, rule_set=None, verbose=False, **kwargs)
","text":"Apply the rules and return results.
For each rule group of a given rule set, rules are applied sequentially, The loop is broken when a rule is applied (an action is triggered). Then, the next rule group is evaluated. And so on...
This means that the order of the rules in the configuration file (e.g., rules.yaml) is meaningful.
Parameters:
Name Type Description Defaultinput_data
dict[str, Any]
Input data to apply rules on.
requiredrule_set
str | None
Apply rules associated with the specified rule set.
None
verbose
bool
If True, add extra ids (group_id, rule_id) for result explicability.
False
**kwargs
Any
For user extra arguments.
{}
Returns:
Type Descriptiondict[str, Any]
A dictionary containing the rule groups' results (k: group id, v: action result).
Raises:
Type DescriptionTypeError
Wrong type.
KeyError
Key not found.
Source code inarta/_engine.py
def apply_rules(\n self, input_data: dict[str, Any], *, rule_set: str | None = None, verbose: bool = False, **kwargs: Any\n) -> dict[str, Any]:\n \"\"\"Apply the rules and return results.\n\n For each rule group of a given rule set, rules are applied sequentially,\n The loop is broken when a rule is applied (an action is triggered).\n Then, the next rule group is evaluated.\n And so on...\n\n This means that the order of the rules in the configuration file\n (e.g., rules.yaml) is meaningful.\n\n Args:\n input_data: Input data to apply rules on.\n rule_set: Apply rules associated with the specified rule set.\n verbose: If True, add extra ids (group_id, rule_id) for result explicability.\n **kwargs: For user extra arguments.\n\n Returns:\n A dictionary containing the rule groups' results (k: group id, v: action result).\n\n Raises:\n TypeError: Wrong type.\n KeyError: Key not found.\n \"\"\"\n # Input_data validation\n if not isinstance(input_data, dict):\n raise TypeError(f\"'input_data' must be dict type, not '{type(input_data)}'\")\n elif len(input_data) == 0:\n raise KeyError(\"'input_data' couldn't be empty.\")\n\n # Var init.\n input_data_copy: dict[str, Any] = copy.deepcopy(input_data)\n\n # Prepare the result key\n input_data_copy[\"output\"] = {}\n\n # If there is no given rule set param. and there is only one rule set in self.rules\n # and its value is 'default_rule_set', look for this one (rule_set='default_rule_set')\n if rule_set is None and len(self.rules) == 1 and self.rules.get(self.CONST_DFLT_RULE_SET_ID) is not None:\n rule_set = self.CONST_DFLT_RULE_SET_ID\n\n # Check if given rule set is in self.rules?\n if rule_set not in self.rules:\n raise KeyError(\n f\"Rule set '{rule_set}' not found in the rules, available rule sets are : {list(self.rules.keys())}.\"\n )\n\n # Var init.\n results_dict: dict[str, Any] = {\"verbosity\": {\"rule_set\": rule_set, \"results\": []}}\n\n # Groups' loop\n for group_id, rules_list in self.rules[rule_set].items():\n # Initialize result of the rule group with None\n results_dict[group_id] = None\n\n # Rules' loop (inside a group)\n for rule in rules_list:\n # Apply rules\n action_result, rule_details = rule.apply(\n input_data_copy, parsing_error_strategy=self._parsing_error_strategy, **kwargs\n )\n\n # Check if the rule has been applied (= action activated)\n if \"action_result\" in rule_details:\n # Save result and details\n results_dict[group_id] = action_result\n results_dict[\"verbosity\"][\"results\"].append(rule_details)\n\n # Update input data with current result with key 'output' (can be used in next rules)\n input_data_copy[\"output\"][group_id] = copy.deepcopy(results_dict[group_id])\n\n # We can only have one result per group => break when \"action_result\" in rule_details\n break\n\n # Handling non-verbose mode\n if not verbose:\n results_dict.pop(\"verbosity\")\n\n return results_dict\n
"},{"location":"api_reference/#conditionpy","title":"condition.py","text":"Condition implementation.
Classes: BaseCondition, StandardCondition, SimpleCondition
"},{"location":"api_reference/#arta.condition.BaseCondition","title":"BaseCondition
","text":" Bases: ABC
Base class of a Condition object (Strategy Pattern).
Is an abstract class and can't be instantiated.
Attributes:
Name Type Descriptioncondition_id
Id of a condition.
description
Description of a condition.
validation_function
Validation function of a condition.
validation_function_parameters
Arguments of the validation function.
Source code inarta/condition.py
class BaseCondition(ABC):\n \"\"\"Base class of a Condition object (Strategy Pattern).\n\n Is an abstract class and can't be instantiated.\n\n Attributes:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n\n # Class constants\n CONST_CONDITION_DATA_LABEL: str = \"Custom condition data (not needed)\"\n CONDITION_ID_PATTERN: str = UPPERCASE_WORD_PATTERN\n\n def __init__(\n self,\n condition_id: str,\n description: str,\n validation_function: Callable | None = None,\n validation_function_parameters: dict[str, Any] | None = None,\n ) -> None:\n \"\"\"\n Initialize attributes.\n\n Args:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n self._condition_id = condition_id # NOSONAR\n self._description = description # NOSONAR\n self._validation_function = validation_function # NOSONAR\n self._validation_function_parameters = validation_function_parameters # NOSONAR\n\n @classmethod\n def extract_condition_ids_from_expression(cls, condition_expr: str | None = None) -> set[str]:\n \"\"\"Get the condition ids from a string (e.g., UPPERCASE words).\n\n E.g., CONDITION_1 and not CONDITION_2\n\n Warning: implementation is based on the current class constant CONDITION_SPLIT_PATTERN.\n\n Args:\n condition_expr: A boolean expression (string).\n\n Returns:\n A set of extracted condition ids.\n \"\"\"\n cond_ids: set[str] = set()\n\n if condition_expr is not None:\n cond_ids = set(re.findall(cls.CONDITION_ID_PATTERN, condition_expr))\n\n return cond_ids\n\n @abstractmethod\n def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"(Abstract)\n Return True if the condition is verified.\n\n Args:\n input_data: Input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n \"\"\"\n raise NotImplementedError\n
"},{"location":"api_reference/#arta.condition.BaseCondition.__init__","title":"__init__(condition_id, description, validation_function=None, validation_function_parameters=None)
","text":"Initialize attributes.
Parameters:
Name Type Description Defaultcondition_id
str
Id of a condition.
requireddescription
str
Description of a condition.
requiredvalidation_function
Callable | None
Validation function of a condition.
None
validation_function_parameters
dict[str, Any] | None
Arguments of the validation function.
None
Source code in arta/condition.py
def __init__(\n self,\n condition_id: str,\n description: str,\n validation_function: Callable | None = None,\n validation_function_parameters: dict[str, Any] | None = None,\n) -> None:\n \"\"\"\n Initialize attributes.\n\n Args:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n self._condition_id = condition_id # NOSONAR\n self._description = description # NOSONAR\n self._validation_function = validation_function # NOSONAR\n self._validation_function_parameters = validation_function_parameters # NOSONAR\n
"},{"location":"api_reference/#arta.condition.BaseCondition.extract_condition_ids_from_expression","title":"extract_condition_ids_from_expression(condition_expr=None)
classmethod
","text":"Get the condition ids from a string (e.g., UPPERCASE words).
E.g., CONDITION_1 and not CONDITION_2
Warning: implementation is based on the current class constant CONDITION_SPLIT_PATTERN.
Parameters:
Name Type Description Defaultcondition_expr
str | None
A boolean expression (string).
None
Returns:
Type Descriptionset[str]
A set of extracted condition ids.
Source code inarta/condition.py
@classmethod\ndef extract_condition_ids_from_expression(cls, condition_expr: str | None = None) -> set[str]:\n \"\"\"Get the condition ids from a string (e.g., UPPERCASE words).\n\n E.g., CONDITION_1 and not CONDITION_2\n\n Warning: implementation is based on the current class constant CONDITION_SPLIT_PATTERN.\n\n Args:\n condition_expr: A boolean expression (string).\n\n Returns:\n A set of extracted condition ids.\n \"\"\"\n cond_ids: set[str] = set()\n\n if condition_expr is not None:\n cond_ids = set(re.findall(cls.CONDITION_ID_PATTERN, condition_expr))\n\n return cond_ids\n
"},{"location":"api_reference/#arta.condition.BaseCondition.verify","title":"verify(input_data, parsing_error_strategy, **kwargs)
abstractmethod
","text":"(Abstract) Return True if the condition is verified.
Parameters:
Name Type Description Defaultinput_data
dict[str, Any]
Input data to apply rules on.
requiredparsing_error_strategy
ParsingErrorStrategy
Error handling strategy for parameter parsing.
required**kwargs
Any
For user extra arguments.
{}
Returns:
Type Descriptionbool
True if the condition is verified, otherwise False.
Source code inarta/condition.py
@abstractmethod\ndef verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"(Abstract)\n Return True if the condition is verified.\n\n Args:\n input_data: Input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n \"\"\"\n raise NotImplementedError\n
"},{"location":"api_reference/#arta.condition.SimpleCondition","title":"SimpleCondition
","text":" Bases: BaseCondition
Class implementing a built-in simple condition.
Attributes:
Name Type Descriptioncondition_id
Id of a condition.
description
Description of a condition.
validation_function
Validation function of a condition.
validation_function_parameters
Arguments of the validation function.
Source code inarta/condition.py
class SimpleCondition(BaseCondition):\n \"\"\"Class implementing a built-in simple condition.\n\n Attributes:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n\n # Class constants\n CONST_CUSTOM_CONDITION_DATA_LABEL: str = \"Simple condition data (not needed)\"\n CONDITION_ID_PATTERN: str = r\"(?:input\\.|output\\.)(?:[a-z_\\-0-9!=<>\\\"NTF\\.]*)\"\n\n def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"Return True if the condition is verified.\n\n Example of a unitary simple condition to be verified: 'input.age>=100'\n\n Args:\n input_data: Request or input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n\n Raises:\n AttributeError: Check the validation function or its parameters.\n \"\"\"\n bool_var: bool = False\n unitary_expr: str = self._condition_id\n\n data_path_patt: str = r\"(?:input\\.|output\\.)(?:[a-z_\\-\\.]*)\"\n\n # Retrieve only the data path\n path_matches: list[str] = re.findall(data_path_patt, unitary_expr)\n\n if len(path_matches) == 1:\n # Regular case: we have a data_path\n data_path: str = path_matches[0]\n\n # Read data from its path\n data = parse_dynamic_parameter( # noqa\n parameter=data_path, input_data=input_data, parsing_error_strategy=parsing_error_strategy\n )\n\n # Replace with the variable name in the expression\n eval_expr: str = unitary_expr.replace(data_path, \"data\")\n\n # Evaluate the expression\n try:\n bool_var = eval(eval_expr) # noqa\n except TypeError:\n # Ignore evaluation --> False\n pass\n\n elif parsing_error_strategy == ParsingErrorStrategy.RAISE:\n # Raise an error because of no match for a data path\n raise ConditionExecutionError(f\"Error when verifying simple condition: '{unitary_expr}'\")\n\n else:\n # Other case: ignore, default value => return False\n pass\n\n return bool_var\n
"},{"location":"api_reference/#arta.condition.SimpleCondition.verify","title":"verify(input_data, parsing_error_strategy, **kwargs)
","text":"Return True if the condition is verified.
Example of a unitary simple condition to be verified: 'input.age>=100'
Parameters:
Name Type Description Defaultinput_data
dict[str, Any]
Request or input data to apply rules on.
requiredparsing_error_strategy
ParsingErrorStrategy
Error handling strategy for parameter parsing.
required**kwargs
Any
For user extra arguments.
{}
Returns:
Type Descriptionbool
True if the condition is verified, otherwise False.
Raises:
Type DescriptionAttributeError
Check the validation function or its parameters.
Source code inarta/condition.py
def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"Return True if the condition is verified.\n\n Example of a unitary simple condition to be verified: 'input.age>=100'\n\n Args:\n input_data: Request or input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n\n Raises:\n AttributeError: Check the validation function or its parameters.\n \"\"\"\n bool_var: bool = False\n unitary_expr: str = self._condition_id\n\n data_path_patt: str = r\"(?:input\\.|output\\.)(?:[a-z_\\-\\.]*)\"\n\n # Retrieve only the data path\n path_matches: list[str] = re.findall(data_path_patt, unitary_expr)\n\n if len(path_matches) == 1:\n # Regular case: we have a data_path\n data_path: str = path_matches[0]\n\n # Read data from its path\n data = parse_dynamic_parameter( # noqa\n parameter=data_path, input_data=input_data, parsing_error_strategy=parsing_error_strategy\n )\n\n # Replace with the variable name in the expression\n eval_expr: str = unitary_expr.replace(data_path, \"data\")\n\n # Evaluate the expression\n try:\n bool_var = eval(eval_expr) # noqa\n except TypeError:\n # Ignore evaluation --> False\n pass\n\n elif parsing_error_strategy == ParsingErrorStrategy.RAISE:\n # Raise an error because of no match for a data path\n raise ConditionExecutionError(f\"Error when verifying simple condition: '{unitary_expr}'\")\n\n else:\n # Other case: ignore, default value => return False\n pass\n\n return bool_var\n
"},{"location":"api_reference/#arta.condition.StandardCondition","title":"StandardCondition
","text":" Bases: BaseCondition
Class implementing a built-in condition, named standard condition.
Attributes:
Name Type Descriptioncondition_id
Id of a condition.
description
Description of a condition.
validation_function
Validation function of a condition.
validation_function_parameters
Arguments of the validation function.
Source code inarta/condition.py
class StandardCondition(BaseCondition):\n \"\"\"Class implementing a built-in condition, named standard condition.\n\n Attributes:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n\n def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"Return True if the condition is verified.\n\n Example of a unitary standard condition: CONDITION_1\n\n Args:\n input_data: Request or input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n\n Raises:\n AttributeError: Check the validation function or its parameters.\n \"\"\"\n if self._validation_function is None:\n raise AttributeError(\"Validation function should not be None\")\n\n if self._validation_function_parameters is None:\n raise AttributeError(\"Validation function parameters should not be None\")\n\n # Parse dynamic parameters\n parameters: dict[str, Any] = {}\n\n for key, value in self._validation_function_parameters.items():\n parameters[key] = parse_dynamic_parameter(\n parameter=value, input_data=input_data, parsing_error_strategy=parsing_error_strategy\n )\n\n # Run validation_function\n return self._validation_function(**parameters)\n
"},{"location":"api_reference/#arta.condition.StandardCondition.verify","title":"verify(input_data, parsing_error_strategy, **kwargs)
","text":"Return True if the condition is verified.
Example of a unitary standard condition: CONDITION_1
Parameters:
Name Type Description Defaultinput_data
dict[str, Any]
Request or input data to apply rules on.
requiredparsing_error_strategy
ParsingErrorStrategy
Error handling strategy for parameter parsing.
required**kwargs
Any
For user extra arguments.
{}
Returns:
Type Descriptionbool
True if the condition is verified, otherwise False.
Raises:
Type DescriptionAttributeError
Check the validation function or its parameters.
Source code inarta/condition.py
def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"Return True if the condition is verified.\n\n Example of a unitary standard condition: CONDITION_1\n\n Args:\n input_data: Request or input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n\n Raises:\n AttributeError: Check the validation function or its parameters.\n \"\"\"\n if self._validation_function is None:\n raise AttributeError(\"Validation function should not be None\")\n\n if self._validation_function_parameters is None:\n raise AttributeError(\"Validation function parameters should not be None\")\n\n # Parse dynamic parameters\n parameters: dict[str, Any] = {}\n\n for key, value in self._validation_function_parameters.items():\n parameters[key] = parse_dynamic_parameter(\n parameter=value, input_data=input_data, parsing_error_strategy=parsing_error_strategy\n )\n\n # Run validation_function\n return self._validation_function(**parameters)\n
"},{"location":"glossary/","title":"Glossary","text":"Concept Definition action A task which is executed when conditions are verified. action function A callable object called to execute the action. action parameter Parameter of an action function. condition A condition to be verified before executing an action. condition id Identifier of a single condition (must be in CAPITAL LETTER). condition expression A boolean expression combining several conditions (meaning several condition id). condition function A callable object called to be verified therefore it returns a boolean. condition parameter Parameter of a condition/validation function. custom condition A user-defined condition. rule A set of conditions combined to one action. rule group A group of rules (usually sharing a common context). rule id Identifier of a single rule. rule set A set of rule groups (mostly one: default_rule_set
). simple condition A built-in very simple condition. standard condition The regular built-in condition. validation function Same thing as a condition function."},{"location":"home/","title":"Home","text":"An Open Source Rules Engine - Make rule handling simple
"},{"location":"home/#welcome-to-the-documentation","title":"Welcome to the documentation","text":"
New feature
Check out the new and very convenient feature called the simple condition. A new and lightweight way of configuring your rules' conditions.
Arta is automatically tested with:
Releases
Want to see last updates, check the Release notes
Pydantic 2
Arta is now working with Pydantic 2! And of course, Pydantic 1 as well.
"},{"location":"how_to/","title":"How to","text":"Ensure that you have correctly installed Arta before, check the Installation page
"},{"location":"how_to/#simple-condition","title":"Simple condition","text":"Beta feature
Simple condition is still a beta feature, some cases could not work as designed.
Simple conditions are a new and straightforward way of configuring your conditions.
It simplifies a lot your rules by:
conditions.py
module (no validation functions needed).conditions:
configuration key in your YAML files.Note
With the simple conditions you use straight boolean expressions directly in your configuration.
It is easyer to read and maintain
The configuration key here is:
simple_condition:
Example :
---\nrules:\n default_rule_set:\n admission:\n ADM_OK:\n simple_condition: input.power==\"strength\" or input.power==\"fly\"\n action: set_admission\n action_parameters:\n value: OK \n ADM_TO_BE_CHECKED:\n simple_condition: input.age>=150 and input.age!=None\n action: set_admission\n action_parameters:\n value: TO_CHECK \n ADM_KO:\n simple_condition: null\n action: set_admission\n action_parameters:\n value: KO\n\nactions_source_modules:\n - my_folder.actions # (1)\n
conditions_source_modules
here.How to write a simple condition like:
input.power==\"strength\" or input.power==\"fly\"\n
input
(for input data)output
(for previous rule's result)input.powers.main_power
.==, <, >, <=, >=, !=
)str, int, None
).Warning
is
or in
, as an operator (yet).float
as right operand (it's a bug, will be fixed).\"
.Security concern
Python code injection:
Because Arta is using the eval()
built-in function to evaluate simple conditions:
simple_condition:
conf. key).It is the first implemented way of using Arta and probably the most powerful.
The configuration key here is:
condition:
YAML
The built-in file format used by Arta for configuration is YAML.
Enhancement proposal
We are thinking on a design that will allow user-implemented loading of the configuration (e.g., if you prefer using a JSON format). Stay tuned.
"},{"location":"how_to/#yaml-file","title":"YAML file","text":"Simple Conditions
The following YAML example illustrates how to configure usual standard conditions but there is another and simpler way to do it by using a special feature: the simple condition.
Create a YAML file and define your rules like this:
---\nrules:\n default_rule_set: # (1)\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER # (2)\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions: # (3)\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n\nconditions_source_modules: # (4)\n - my_folder.conditions\nactions_source_modules: # (5)\n - my_folder.actions\n
default_rule_set
is by default).Warning
Condition ids must be in capital letters here, it is mandatory (e.g., HAS_SCHOOL_AUTHORIZED_POWER
).
Tip
You can split your configuration in multiple YAML files seamlessly in order to keep things clear. Example:
It's very convenient when you have a lot of different rules and conditions in your app.
"},{"location":"how_to/#condition-expression","title":"Condition expression","text":"In the above YAML, the following condition expression is intentionally very simple:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n
The key condition:
can take one condition id but also a condition expression (i.e., a boolean expression of condition ids) combining several conditions:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: (HAS_SCHOOL_AUTHORIZED_POWER or SPEAKS_FRENCH) and not(IS_EVIL)\n action: set_admission\n action_parameters:\n value: true\n
Warning
In that example, you must define the 3 conditions in the configuration:
Tip
Use the condition expressions to keep things simple. Put your conditions in one expression as you can rather than creating several rules
"},{"location":"how_to/#functions","title":"Functions","text":"We must create 2 modules:
conditions.py
-> implements the needed validation functions.actions.py
-> implements the needed action functions.Note
Module names are arbitrary, you can choose what you want.
And implement our 2 needed validation and action functions (the one defined in the configuration file):
conditions.py:
def has_authorized_super_power(power):\n return power in [\"strength\", \"fly\", \"immortality\"]\n
actions.py:
def set_admission(value, **kwargs): # (1)\n return {\"is_admitted\": value}\n
**kwargs
is mandatory here.Warning
Function name and parameters must be the same as the one configured in the YAML file.
"},{"location":"how_to/#usage","title":"Usage","text":"Once your configuration file and your functions are ready, you can use it very simply:
from arta import RulesEngine\n\ninput_data = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"super_power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n\neng = RulesEngine(config_path=\"path/to/conf/dir\")\n\nresult = eng.apply_rules(input_data)\n\nprint(result)\n
You should get:
{'check_admission': {'is_admitted': True}}
API Documentation
You can get details on the RulesEngine
parameters in the API Reference.
Let's go deeper into the concepts.
"},{"location":"how_to/#rule-set-and-rule-group","title":"Rule set and rule group","text":"A rule set is composed of rule groups which are themselves composed of rules. We can find this tree structure in the following YAML:
---\nrules:\n default_rule_set: # (1)\n check_admission: # (2)\n ADMITTED_RULE: # (3)\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n
Rule definitions are identified by an id (e.g., ADMITTED_RULE
):
ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n
Tip
Rule ids are in capital letters for readability only: it is an advised practice.
Rules are made of 2 different things:
ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n
ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n
"},{"location":"how_to/#condition-and-action","title":"Condition and Action","text":"Conditions and actions are quite similar in terms of implementation but their goals are different.
Both are made of a callable object and some parameters:
Condition keys:
validation_function
: name of a callable python object that returns a bool
, we called this function the validation function (or condition function*).
condition_parameters
: the validation function's arguments.
Action keys:
action
: name of a callable python object that returns what you want (or does what you want such as: requesting an api, sending an email, etc.), we called this function the action function.
action_parameters
: the action function's arguments.
Parameter's special syntax
The action and condition arguments can have a special syntax:
condition_parameters:\n power: input.super_power\n
The string input.super_power
is evaluated by the rules engine and it means \"fetch the key super_power
in the input data\".
Rules can be configured in a YAML file but they can also be defined by a regular dictionary:
Without type hintsWith type hints (>=3.9)from arta import RulesEngine\n\nset_admission = lambda value, **kwargs: {\"is_admitted\": value}\n\nrules = {\n \"check_admission\": {\n \"ADMITTED_RULE\": {\n \"condition\": lambda power: power in [\"strength\", \"fly\", \"immortality\"],\n \"condition_parameters\": {\"power\": \"input.super_power\"}, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": True},\n },\n \"DEFAULT_RULE\": {\n \"condition\": None,\n \"condition_parameters\": None, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": False},\n },\n }\n}\n\ninput_data = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"super_power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n\neng = RulesEngine(rules_dict=rules)\n\nresult = eng.apply_rules(input_data)\n\nprint(result)\n
from typing import Any, Callable\n\nfrom arta import RulesEngine\n\nset_admission: Callable = lambda value, **kwargs: {\"is_admitted\": value}\n\nrules: dict[str, Any] = {\n \"check_admission\": {\n \"ADMITTED_RULE\": {\n \"condition\": lambda power: power in [\"strength\", \"fly\", \"immortality\"],\n \"condition_parameters\": {\"power\": \"input.super_power\"}, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": True},\n },\n \"DEFAULT_RULE\": {\n \"condition\": None,\n \"condition_parameters\": None, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": False},\n },\n }\n}\n\ninput_data: dict[str, Any] = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"super_power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n\neng = RulesEngine(rules_dict=rules)\n\nresult: dict[str, Any] = eng.apply_rules(input_data)\n\nprint(result)\n
You should get:
{'check_admission': {'is_admitted': True}}\n
Success
Superman is admitted to the superhero school!
Well done! By executing this code you have:
set_admission
)rules
)input_data
)RulesEngine
).apply_rules()
)Note
In the code example we used some anonymous/lambda function for simplicity but it could be regular python functions as well.
YAML vs Dictionary
How to choose between dictionary and configuration?
In most cases, you must choose the configuration way of defining your rules.
You will improve your rules' maintainability a lot. In some cases like proof-of-concepts or Jupyter notebook works, you will probably be happy with straightforward dictionaries.
Arta has plenty more features to discover. If you want to learn more, go to the next chapter: Advanced User Guide.
"},{"location":"installation/","title":"Installation","text":""},{"location":"installation/#python","title":"Python","text":"Compatible with:
"},{"location":"installation/#pip","title":"pip","text":"In your python environment:
"},{"location":"installation/#regular-use","title":"Regular use","text":"pip install arta\n
"},{"location":"installation/#development","title":"Development","text":"pip install arta[all]\n
"},{"location":"parameters/","title":"Parameters","text":""},{"location":"parameters/#parsing-prefix-keywords","title":"Parsing prefix keywords","text":"There is 2 allowed parsing prefix keywords:
input
: corresponding to the input_data
.output
: corresponding to the result output data (returned by the apply_rules()
method).Here are examples:
input.name
: maps to input_data[\"name\"]
.output.check_admission.is_admitted
: maps to result[\"check_admission\"][\"is_admitted\"]
.They both can be used in condition and action parameters.
Info
A value without any prefix keyword is a constant.
"},{"location":"parameters/#parsing-error","title":"Parsing error","text":""},{"location":"parameters/#raise-by-default","title":"Raise by default","text":"By default, errors during condition and action parameters parsing are raised.
If we refer to the dictionary example:
rules = {\n \"check_admission\": {\n \"ADMITTED_RULE\": {\n \"condition\": lambda power: power in [\"strength\", \"fly\", \"immortality\"],\n \"condition_parameters\": {\"power\": \"input.super_power\"}, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": True},\n },\n \"DEFAULT_RULE\": {\n \"condition\": None,\n \"condition_parameters\": None, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": False},\n },\n }\n}\n
With modified data like:
input_data = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n
By default we will get a KeyError
exception during the execution of the apply_rules()
method because of power
vs super_power
.
You can change the by default raising behavior of the parameter's parsing.
Two ways are possible:
You just have to add the following key somewhere in your configuration:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules: \n - my_folder.actions\n\nparsing_error_strategy: ignore # (1)\n
parsing_error_strategy
has two possible values: raise
and ignore
.It will affect all the parameters.
"},{"location":"parameters/#parameter-level","title":"Parameter level","text":"Quick Sum Up
input.super_power?
: set the value to None
input.super_power?no_power
: set the value to no_power
input.super_power!
: force raise exception (case when ignore is set by default)You can also handle more precisely that aspect at parameter's level:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power? # (1)\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules: \n - my_folder.actions\n
KeyError
when reading, power
will be set to None
rather than raising the exception.Info
You can enforce raising exceptions at parameter's level with !
.
power: input.super_power!\n
"},{"location":"parameters/#default-value-parameter-level","title":"Default value (parameter level)","text":"Finally, you can set a default value at parameter's level. This value will be used if there is an exception during parsing:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power?no_power # (1)\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules: \n - my_folder.actions\n
power
will be set to \"no_power\"
.Good to know
Parameter's level is overriding configuration level.
"},{"location":"rule_sets/","title":"Rule sets","text":"Rule sets are a convenient way to separate your business rules into different collections.
Doing so increases the rules' maintainability because of a better organization and fully uncoupled rules.
Tip
Rule sets are very usefull when you have a lot of rules.
Info
Most of the time, you won't need to handle different rule sets and will only use the default one: default_rule_set
.
The good news is that different rule sets can be used seamlessly with the same rules engine instance
Let's take the following example:
Based on that example, imagine that you need to add some rules about something totally different than the superhero school. Let's say rules for a dinosaur school.
"},{"location":"rule_sets/#configuration","title":"Configuration","text":"Update your configuration by adding a new rule set: dinosaur_school_set
---\nrules:\n superhero_school_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n dinosaur_school_set: # (1)\n food_habit:\n HERBIVOROUS:\n condition: not(IS_EATING_MEAT)\n action: send_mail_to_cook\n action_parameters:\n meal: \"plant\"\n CARNIVOROUS:\n condition: null\n action: send_mail_to_cook\n action_parameters:\n meal: \"meat\"\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n IS_EATING_MEAT: # (2)\n description: \"Is dinosaur eating meat?\"\n validation_function: is_eating_meat\n condition_parameters:\n power: input.diet.regular_food\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules:\n - my_folder.actions\n
rules
keyGood to know
You can define your rule sets into different YAML files (under the rules
key in each).
Now that your rule sets are defined (and assuming that your condition and action functions are implemented in the right modules), you can easily use them:
from arta import RulesEngine\n\ninput_data_1 = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"super_power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n\ninput_data_2 = {\n \"id\": 1,\n \"name\": \"Diplodocus\",\n \"age\": 152000000,\n \"length\": 31,\n \"area\": \"north_america\",\n \"diet\": {\n \"regular_food\": \"plants\",\n },\n}\n\neng = RulesEngine(config_path=\"path/to/conf/dir\")\n\nsuperhero_result = eng.apply_rules(input_data_1, rule_set=\"superhero_school_set\") # (1)\n\ndinosaur_result = eng.apply_rules(input_data_2, rule_set=\"dinosaur_school_set\")\n
Good to know
Input data can be different or the same among the rule sets. It depends on the use case.
"},{"location":"rule_sets/#object-oriented-model","title":"Object-Oriented Model","text":"classDiagram\n rule_set \"1\" -- \"1..*\" rule_group\n rule_group \"1\" -- \"1..*\" rule\n rule \"1..*\" -- \"0..*\" condition\n rule \"1..*\" -- \"1\" action
"},{"location":"special_conditions/","title":"Special conditions","text":""},{"location":"special_conditions/#custom-condition","title":"Custom condition","text":"Custom conditions are user-defined conditions.
A custom condition will impact the atomic evaluation of each conditions (i.e., condition ids).
Vocabulary
To be more precise, a condition expression is something like:
CONDITION_1 and CONDITION_2\n
In that example, the condition expression is made of 2 conditions whose condition ids are:
With the built-in condition (also named standard condition), condition ids map to validation functions and condition parameters but we can change that with a brand new custom condition.
A custom condition example:
my_condition: NAME_JOHN and AGE_42\n
Remember
condition ids have to be in CAPITAL LETTERS.
Imagine you want it to be interpreted as (pseudo-code):
if input.name == \"john\" and input.age == \"42\":\n # Do something\n ...\n
With the custom conditions it's quite simple to implement.
Why using a custom condition?
The main goal is to simplify handling of recurrent conditions (e.i., \"recurrent\" meaning very similar conditions).
"},{"location":"special_conditions/#class-implementation","title":"Class implementation","text":"First, create a class inheriting from BaseCondtion
and implement the verify()
method as you want/need:
from typing import Any\n\nfrom arta.condition import BaseCondition\nfrom arta.utils import ParsingErrorStrategy\n\n\nclass MyCondition(BaseCondition):\n def verify(\n self,\n input_data: dict[str, Any],\n parsing_error_strategy: ParsingErrorStrategy,\n **kwargs: Any\n ) -> bool:\n\n field, value = tuple(self.condition_id.split(\"_\"))\n\n return input_data[field.lower()] == value.lower()\n
self.condition_id
self.condition_id
will be NAME_JOHN
for the first condition and AGE_42
for the second.
Good to know
The parsing_error_strategy
can be used by the developer to adapt exception handling behavior. Possible values:
ParsingErrorStrategy.RAISE\nParsingErrorStrategy.IGNORE\nParsingErrorStrategy.DEFAULT_VALUE\n
"},{"location":"special_conditions/#configuration","title":"Configuration","text":"Last thing to do is to add your new custom condition in the configuration:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n my_condition: NAME_JOHN and AGE_42 # (1)\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules: \n - my_folder.actions\n\ncustom_classes_source_modules:\n - dir.to.my_module # (2)\ncondition_factory_mapping:\n my_condition: MyCondition # (3)\n
condition
then my_condition
. Order is arbitrary.my_condition
) and custom classes (MyCondition
)It is based on the following strategy pattern:
classDiagram\n note for MyCondition \"This is a custom condition class\"\n RulesEngine \"1\" -- \"1..*\" Rule\n Rule \"1..*\" -- \"0..*\" BaseCondition\n BaseCondition <|-- StandardCondition\n BaseCondition <|-- SimpleCondition\n BaseCondition <|-- MyCondition\n class RulesEngine{\n +rules\n +apply_rules()\n }\n class Rule {\n #set_id\n #group_id\n #rule_id\n #condition_exprs\n #action\n #action_parameters\n +apply()\n }\n class BaseCondition {\n <<abstract>>\n #condition_id\n #description\n #validation_function\n #validation_function_parameters\n +verify()\n }
Good to know
The class StandardCondition
is the built-in implementation of a condition.
There is one main reason for using Arta and it was the main goal of its development:
Increase business rules maintainability.
In other words, facilitate rules handling in a python app.
"},{"location":"why/#before-arta","title":"Before Arta","text":"Rules in code can rapidly become a headache, kind of spaghetti dish of if
, elif
and else
(or even match/case
since Python 3.10).
Arta increases rules maintainability:
Improve collaboration
Reading python code vs reading YAML.
"},{"location":"assets/js/tarteaucitron/","title":"Index","text":""},{"location":"assets/js/tarteaucitron/#tarteaucitronjs","title":"tarteaucitron.js","text":"Comply to the european cookie law is simple with the french tarte au citron.
"},{"location":"assets/js/tarteaucitron/#what-is-this-script","title":"What is this script?","text":"The european cookie law regulates the management of cookies and you should ask your visitors their consent before exposing them to third party services.
Clearly this script will: - Disable all services by default, - Display a banner on the first page view and a small one on other pages, - Display a panel to allow or deny each services one by one, - Activate services on the second page view if not denied, - Store the consent in a cookie for 365 days.
Bonus: - Load service when user click on Allow (without reload of the page), - Incorporate a fallback system (display a link instead of social button and a static banner instead of advertising).
"},{"location":"assets/js/tarteaucitron/#supported-services","title":"Supported services","text":"vShop
APIs
Typekit (adobe)
Audience measurement
Xiti
Comment
Facebook (commentaire)
Social network
Twitter (timelines)
Support
Zopim
Video
In PHP for example, you can bypass all the script by setting this var tarteaucitron.user.bypass = true;
if the visitor is not in the EU.
Visit opt-out.ferank.eu
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"a_simple_example/","title":"A Simple Example","text":""},{"location":"a_simple_example/#intro","title":"Intro","text":"As we already mentioned: Arta is a simple python rules engine.
But what do we mean by rules engine?
True
or False
(i.e., we say verified or not verified) triggering an action (i.e., any python callable object).Imagine the following use case:
Your are managing a superhero school and you want to use some school rules in your python app.
The rules (intentionally simple) are:
Admission rules
If the applicant has a school authorized power then he is admitted,
Else he is not.
Course selection rules
If he is speaking french and his age is known then he must take the \"french\" course,
Else if his age is unknown (e.g., it's a very old superhero), then he must take the \"senior\" course,
Else if he is not speaking french, then he must take the \"international\" course.
Send favorite meal rules
If he is admitted and has a prefered dish, then we send an email to the school cook with the dish name.
"},{"location":"a_simple_example/#rules","title":"Rules","text":"You can define above rules for Arta in one simple YAML file :
---\nrules:\n default_rule_set:\n admission:\n ADMITTED:\n simple_condition: input.power==\"strength\" or input.power==\"fly\"\n action: set_admission\n action_parameters:\n value: true \n NOT_ADMITTED:\n simple_condition: null\n action: set_admission\n action_parameters:\n value: false\n course:\n FRENCH:\n simple_condition: input.language==\"french\" and input.age!=None\n action: set_course\n action_parameters:\n value: french\n SENIOR:\n simple_condition: input.age==None\n action: set_course\n action_parameters:\n value: senior\n INTERNATIONAL:\n simple_condition: input.language!=\"french\"\n action: set_course\n action_parameters:\n value: international\n favorite_meal:\n EMAIL:\n simple_condition: input.favorite_meal!=None\n action: send_email\n action_parameters:\n mail_to: cook@super-heroes.test\n mail_content: \"Thanks for preparing once a month the following dish:\"\n meal: input.favorite_meal\n\nactions_source_modules:\n - my_folder.actions\n
Simple Conditions
This configuration uses what we called simple conditions, you can find out more here.
"},{"location":"a_simple_example/#actions","title":"Actions","text":"An action is triggered when the conditions are verified (i.e., True
).
Actions are defined by the following keys in the previous YAML file:
action: set_admission # (1)\n action_parameters: # (2)\n value: true \n
The action function's implementation has to be located in the configured module:
actions_source_modules:\n - my_folder.actions\n
And could be for example (intentionally simple) in actions.py
:
def set_admission(value: bool, **kwargs: Any) -> dict[str, bool]:\n \"\"\"Return a dictionary containing the admission result.\"\"\"\n return {\"is_admitted\": value}\n\n\ndef set_course(course_id: str, **kwargs: Any) -> dict[str, str]:\n \"\"\"Return the course id as a dictionary.\"\"\"\n return {\"course_id\": course_id}\n\n\ndef send_email(mail_to: str, mail_content: str, meal: str, **kwargs: Any) -> bool:\n \"\"\"Send an email.\"\"\"\n result: str | None = None\n\n if meal is not None:\n # API call here\n result = \"sent\"\n\n return result\n
**kwargs
**kwargs is mandatory in action functions.
"},{"location":"a_simple_example/#engine","title":"Engine","text":"The rules engine is responsible for evaluating the configured rules against some data (usually named \"input data\").
In our use case, the input data could be a list of applicants:
applicants = [\n {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"english\",\n \"power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n },\n {\n \"id\": 2,\n \"name\": \"Batman\",\n \"civilian_name\": \"Bruce Wayne\",\n \"age\": 33,\n \"city\": \"Gotham City\",\n \"language\": \"english\",\n \"power\": \"strength\",\n \"favorite_meal\": None,\n \"secret_weakness\": \"Feel alone\",\n \"weapons\": [\"Hands\", \"Batarang\"],\n },\n {\n \"id\": 3,\n \"name\": \"Wonder Woman\",\n \"civilian_name\": \"Diana Prince\",\n \"age\": 5000,\n \"city\": \"Island of Themyscira\",\n \"language\": \"french\",\n \"power\": \"strength\",\n \"favorite_meal\": None,\n \"secret_weakness\": \"Lost faith in humanity\",\n \"weapons\": [\"Magic lasso\", \"Bulletproof bracelets\", \"Sword\", \"Shield\"],\n },\n]\n
Now, let's apply the rules on a single applicant:
from arta import RulesEngine\n\neng = RulesEngine(config_path=\"/to/my/config/dir\") # (1)\n\nresult = eng.apply_rules(input_data=applicants[0])\n\nprint(result) # (2)\n# {\n# \"admission\": {\"is_admitted\": True},\n# \"course\": {\"course_id\": \"senior\"},\n# \"favorite_meal\": \"sent\"\n# }\n
In the rules engine result, we have 3 outputs:
\"admission\": {\"is_admitted\": True},
\"course\": {\"course_id\": \"senior\"},
\"favorite_meal\": \"sent\"
Each corresponds to one of these rules.
Here, we can apply the rules to all the data set (3 applicants) with a simple dictionary comprehension:
from arta import RulesEngine\n\nresults = {applicant[\"name\"]: eng.apply_rules(applicant) for applicant in applicants}\n\nprint(results)\n# {\n# \"Superman\": {\n# \"admission\": {\"is_admitted\": True}, \n# \"course\": {\"course_id\": \"senior\"}, \n# \"favorite_meal\": \"sent\"},\n# \"Batman\": {\n# \"admission\": {\"is_admitted\": True},\n# \"course\": {\"course_id\": \"international\"},\n# \"favorite_meal\": None,\n# },\n# \"Wonder Woman\": {\n# \"admission\": {\"is_admitted\": True},\n# \"course\": {\"course_id\": \"french\"},\n# \"favorite_meal\": None,\n# }\n# }\n
It is the end of this Arta's overview. If you want now to go deeper in how to use Arta, click here.
"},{"location":"api_reference/","title":"API Reference","text":""},{"location":"api_reference/#_enginepy","title":"_engine.py","text":"Module implementing the rules engine.
Class: RulesEngine
"},{"location":"api_reference/#arta._engine.RulesEngine","title":"RulesEngine
","text":"The Rules Engine is in charge of executing different groups of rules of a given rule set on user input data.
Attributes:
Name Type Descriptionrules
dict[str, dict[str, list[Rule]]]
A dictionary of rules with k: rule set, v: (k: rule group, v: list of rule instances).
Source code inarta/_engine.py
class RulesEngine:\n \"\"\"The Rules Engine is in charge of executing different groups of rules of a given rule set on user input data.\n\n Attributes:\n rules: A dictionary of rules with k: rule set, v: (k: rule group, v: list of rule instances).\n \"\"\"\n\n # ==== Class constants ====\n\n # Rule related config keys\n CONST_RULE_SETS_CONF_KEY: str = \"rules\"\n CONST_DFLT_RULE_SET_ID: str = \"default_rule_set\"\n CONST_STD_RULE_CONDITION_CONF_KEY: str = \"condition\"\n CONST_ACTION_CONF_KEY: str = \"action\"\n CONST_ACTION_PARAMETERS_CONF_KEY: str = \"action_parameters\"\n\n # Condition related config keys\n CONST_STD_CONDITIONS_CONF_KEY: str = \"conditions\"\n CONST_CONDITION_VALIDATION_FUNCTION_CONF_KEY: str = \"validation_function\"\n CONST_CONDITION_DESCRIPTION_CONF_KEY: str = \"description\"\n CONST_CONDITION_VALIDATION_PARAMETERS_CONF_KEY: str = \"condition_parameters\"\n CONST_USER_CONDITION_STRING: str = \"USER_CONDITION\"\n\n # Built-in factory mapping\n BUILTIN_FACTORY_MAPPING: dict[str, type[BaseCondition]] = {\n \"condition\": StandardCondition,\n \"simple_condition\": SimpleCondition,\n }\n\n def __init__(\n self,\n *,\n rules_dict: dict[str, dict[str, Any]] | None = None,\n config_path: str | None = None,\n ) -> None:\n \"\"\"Initialize the rules.\n\n 2 possibilities: either 'rules_dict', or 'config_path', not both.\n\n Args:\n rules_dict: A dictionary containing the rules' definitions.\n config_path: Path of a directory containing the YAML files.\n\n Raises:\n KeyError: Key not found.\n TypeError: Wrong type.\n \"\"\"\n # Var init.\n factory_mapping_classes: dict[str, type[BaseCondition]] = {}\n std_condition_instances: dict[str, StandardCondition] = {}\n\n if config_path is not None and rules_dict is not None:\n raise ValueError(\"RulesEngine takes only one parameter: 'rules_dict' or 'config_path', not both.\")\n\n # Init. default parsing_error_strategy (probably not needed because already defined elsewhere)\n self._parsing_error_strategy: ParsingErrorStrategy = ParsingErrorStrategy.RAISE\n\n # Initialize directly with a rules dict\n if rules_dict is not None:\n # Data validation\n RulesDict.parse_obj(rules_dict)\n\n # Edge cases data validation\n if not isinstance(rules_dict, dict):\n raise TypeError(f\"'rules_dict' must be dict type, not '{type(rules_dict)}'\")\n elif len(rules_dict) == 0:\n raise KeyError(\"'rules_dict' couldn't be empty.\")\n\n # Attribute definition\n self.rules: dict[str, dict[str, list[Rule]]] = self._adapt_user_rules_dict(rules_dict)\n\n # Initialize with a config_path\n elif config_path is not None:\n # Load config in attribute\n config_dict: dict[str, Any] = load_config(config_path)\n\n # Data validation\n config: Configuration = Configuration(**config_dict)\n\n if config.parsing_error_strategy is not None:\n # Set parsing error handling strategy from config\n self._parsing_error_strategy = ParsingErrorStrategy(config.parsing_error_strategy)\n\n # dict of available action functions (k: function name, v: function object)\n action_modules: list[str] = config.actions_source_modules\n action_functions: dict[str, Callable] = self._get_object_from_source_modules(action_modules)\n\n # dict of available standard condition functions (k: function name, v: function object)\n condition_modules: list[str] = (\n config.conditions_source_modules if config.conditions_source_modules is not None else []\n )\n std_condition_functions: dict[str, Callable] = self._get_object_from_source_modules(condition_modules)\n\n # Dictionary of condition instances (k: condition id, v: instance), built from config data\n if len(std_condition_functions) > 0:\n std_condition_instances = self._build_std_conditions(\n config=config.dict(), condition_functions_dict=std_condition_functions\n )\n\n # User-defined/custom conditions\n if config.condition_factory_mapping is not None and config.custom_classes_source_modules is not None:\n # dict of custom condition classes (k: classe name, v: class object)\n custom_condition_classes: dict[str, type[BaseCondition]] = self._get_object_from_source_modules(\n config.custom_classes_source_modules\n )\n\n # Build a factory mapping dictionary (k: conf key, v:class object)\n factory_mapping_classes.update(\n {\n conf_key: custom_condition_classes[class_name]\n for conf_key, class_name in config.condition_factory_mapping.items()\n }\n )\n\n # Arta built-in conditions\n factory_mapping_classes.update(self.BUILTIN_FACTORY_MAPPING)\n\n # Attribute definition\n self.rules = self._build_rules(\n std_condition_instances=std_condition_instances,\n action_functions=action_functions,\n config=config.dict(),\n factory_mapping_classes=factory_mapping_classes,\n )\n else:\n raise ValueError(\"RulesEngine needs a parameter: 'rule_dict' or 'config_path'.\")\n\n def apply_rules(\n self, input_data: dict[str, Any], *, rule_set: str | None = None, verbose: bool = False, **kwargs: Any\n ) -> dict[str, Any]:\n \"\"\"Apply the rules and return results.\n\n For each rule group of a given rule set, rules are applied sequentially,\n The loop is broken when a rule is applied (an action is triggered).\n Then, the next rule group is evaluated.\n And so on...\n\n This means that the order of the rules in the configuration file\n (e.g., rules.yaml) is meaningful.\n\n Args:\n input_data: Input data to apply rules on.\n rule_set: Apply rules associated with the specified rule set.\n verbose: If True, add extra ids (group_id, rule_id) for result explicability.\n **kwargs: For user extra arguments.\n\n Returns:\n A dictionary containing the rule groups' results (k: group id, v: action result).\n\n Raises:\n TypeError: Wrong type.\n KeyError: Key not found.\n \"\"\"\n # Input_data validation\n if not isinstance(input_data, dict):\n raise TypeError(f\"'input_data' must be dict type, not '{type(input_data)}'\")\n elif len(input_data) == 0:\n raise KeyError(\"'input_data' couldn't be empty.\")\n\n # Var init.\n input_data_copy: dict[str, Any] = copy.deepcopy(input_data)\n\n # Prepare the result key\n input_data_copy[\"output\"] = {}\n\n # If there is no given rule set param. and there is only one rule set in self.rules\n # and its value is 'default_rule_set', look for this one (rule_set='default_rule_set')\n if rule_set is None and len(self.rules) == 1 and self.rules.get(self.CONST_DFLT_RULE_SET_ID) is not None:\n rule_set = self.CONST_DFLT_RULE_SET_ID\n\n # Check if given rule set is in self.rules?\n if rule_set not in self.rules:\n raise KeyError(\n f\"Rule set '{rule_set}' not found in the rules, available rule sets are : {list(self.rules.keys())}.\"\n )\n\n # Var init.\n results_dict: dict[str, Any] = {\"verbosity\": {\"rule_set\": rule_set, \"results\": []}}\n\n # Groups' loop\n for group_id, rules_list in self.rules[rule_set].items():\n # Initialize result of the rule group with None\n results_dict[group_id] = None\n\n # Rules' loop (inside a group)\n for rule in rules_list:\n # Apply rules\n action_result, rule_details = rule.apply(\n input_data_copy, parsing_error_strategy=self._parsing_error_strategy, **kwargs\n )\n\n # Check if the rule has been applied (= action activated)\n if \"action_result\" in rule_details:\n # Save result and details\n results_dict[group_id] = action_result\n results_dict[\"verbosity\"][\"results\"].append(rule_details)\n\n # Update input data with current result with key 'output' (can be used in next rules)\n input_data_copy[\"output\"][group_id] = copy.deepcopy(results_dict[group_id])\n\n # We can only have one result per group => break when \"action_result\" in rule_details\n break\n\n # Handling non-verbose mode\n if not verbose:\n results_dict.pop(\"verbosity\")\n\n return results_dict\n\n @staticmethod\n def _get_object_from_source_modules(module_list: list[str]) -> dict[str, Any]:\n \"\"\"(Protected)\n Collect all functions defined in the list of modules.\n\n Args:\n module_list: List of source module names.\n\n Returns:\n Dictionary with objects found in the modules.\n \"\"\"\n object_dict: dict[str, Any] = {}\n\n for module_name in module_list:\n # Import module\n mod: ModuleType = importlib.import_module(module_name)\n\n # Collect functions\n module_functions: dict[str, Any] = {key: val for key, val in getmembers(mod, isfunction)}\n object_dict.update(module_functions)\n\n # Collect classes\n module_classes: dict[str, Any] = {key: val for key, val in getmembers(mod, isclass)}\n object_dict.update(module_classes)\n\n return object_dict\n\n def _build_rules(\n self,\n std_condition_instances: dict[str, StandardCondition],\n action_functions: dict[str, Callable],\n config: dict[str, Any],\n factory_mapping_classes: dict[str, type[BaseCondition]],\n ) -> dict[str, dict[str, list[Any]]]:\n \"\"\"(Protected)\n Return a dictionary of Rule instances built from the configuration.\n\n Args:\n rule_sets: Sets of rules to be loaded in the Rules Engine (as needed by further uses).\n std_condition_instances: Dictionary of condition instances (k: condition id, v: StandardCondition instance)\n actions_dict: Dictionary of action functions (k: action name, v: Callable)\n config: Dictionary of the imported configuration from yaml files.\n factory_mapping_classes: A mapping dictionary (k: condition conf. key, v: custom class object)\n\n Returns:\n A dictionary of rules.\n \"\"\"\n # Var init.\n rules_dict: dict[str, dict[str, list[Any]]] = {}\n\n # Retrieve rule set ids from config\n rule_set_ids: list[str] = list(config[self.CONST_RULE_SETS_CONF_KEY].keys())\n\n # Going all way down to the rules (rule set > rule group > rule)\n for set_id in rule_set_ids:\n rules_conf: dict[str, Any] = config[self.CONST_RULE_SETS_CONF_KEY][set_id]\n rules_dict[set_id] = {}\n rule_set_dict: dict[str, list[Any]] = rules_dict[set_id]\n\n # Looping throught groups\n for group_id, group_rules in rules_conf.items():\n # Initialize list or rules in the group\n rule_set_dict[group_id] = []\n\n # Looping through rules (inside a group)\n for rule_id, rule_dict in group_rules.items():\n # Get action function\n action_function_name: str = rule_dict[self.CONST_ACTION_CONF_KEY]\n\n if action_function_name not in action_functions:\n raise KeyError(f\"Unknwown action function : {action_function_name}\")\n\n action: Callable = action_functions[action_function_name]\n\n # Look for condition conf. keys inside the rule\n condition_conf_keys: set[str] = set(rule_dict.keys()) - {\n self.CONST_ACTION_CONF_KEY,\n self.CONST_ACTION_PARAMETERS_CONF_KEY,\n }\n\n # Store the cond. expressions with the same order as in the configuration file (very important)\n condition_exprs: dict[str, str | None] = {\n key: value for key, value in rule_dict.items() if key in condition_conf_keys\n }\n\n # Create the corresponding Rule instance\n rule: Rule = Rule(\n set_id=set_id,\n group_id=group_id,\n rule_id=rule_id,\n action=action,\n action_parameters=rule_dict[self.CONST_ACTION_PARAMETERS_CONF_KEY],\n condition_exprs=condition_exprs,\n std_condition_instances=std_condition_instances,\n condition_factory_mapping=factory_mapping_classes,\n )\n rule_set_dict[group_id].append(rule)\n\n return rules_dict\n\n def _build_std_conditions(\n self, config: dict[str, Any], condition_functions_dict: dict[str, Callable]\n ) -> dict[str, StandardCondition]:\n \"\"\"(Protected)\n Return a dictionary of Condition instances built from the configuration file.\n\n Args:\n config: Dictionary of the imported configuration from yaml files.\n condition_functions_dict: A dictionary where k:condition id, v:Callable (validation function).\n\n Returns:\n A dictionary of StandardCondition instances (k: condition id, v: StandardCondition instance).\n \"\"\"\n # Var init.\n conditions_dict: dict[str, StandardCondition] = {}\n\n # Condition configuration (under conditions' key)\n conditions_conf: dict[str, dict[str, Any]] = config[self.CONST_STD_CONDITIONS_CONF_KEY]\n\n # Looping through conditions (inside a group)\n for condition_id, condition_params in conditions_conf.items():\n # Get condition validation function\n validation_function_name: str = condition_params[self.CONST_CONDITION_VALIDATION_FUNCTION_CONF_KEY]\n\n if validation_function_name not in condition_functions_dict:\n raise KeyError(f\"Unknwown validation function : {validation_function_name}\")\n\n # Get Callable from function name\n validation_function: Callable = condition_functions_dict[validation_function_name]\n\n # Create Condition instance\n condition_instance: StandardCondition = StandardCondition(\n condition_id=condition_id,\n description=condition_params[self.CONST_CONDITION_DESCRIPTION_CONF_KEY],\n validation_function=validation_function,\n validation_function_parameters=condition_params[self.CONST_CONDITION_VALIDATION_PARAMETERS_CONF_KEY],\n )\n conditions_dict[condition_id] = condition_instance\n\n return conditions_dict\n\n def _adapt_user_rules_dict(self, rules_dict: dict[str, dict[str, Any]]) -> dict[str, dict[str, list[Any]]]:\n \"\"\"(Protected)\n Return a dictionary of Rule's instances built from user's rules dictionary.\n\n Args:\n rules_dict: User raw rules dictionary.\n\n Returns:\n A rules dictionary made from the user input rules.\n \"\"\"\n # Var init.\n rules_dict_formatted: dict[str, list[Any]] = {}\n\n # Looping throught groups\n for group_id, group_rules in rules_dict.items():\n # Initialize list or rules in the group\n rules_dict_formatted[group_id] = []\n\n # Looping through rules (inside a group)\n for rule_id, rule_dict in group_rules.items():\n # Get action function\n action = rule_dict[\"action\"]\n\n # Trigger if not **kwargs\n if \"kwargs\" not in inspect.signature(action).parameters:\n raise KeyError(f\"The action function {action} must have a '**kwargs' parameter.\")\n\n # Create Rule instance\n rule = Rule(\n set_id=self.CONST_DFLT_RULE_SET_ID,\n group_id=group_id,\n rule_id=rule_id,\n action=action,\n action_parameters=rule_dict.get(self.CONST_ACTION_PARAMETERS_CONF_KEY),\n condition_exprs={self.CONST_STD_RULE_CONDITION_CONF_KEY: self.CONST_USER_CONDITION_STRING}\n if self.CONST_STD_RULE_CONDITION_CONF_KEY in rule_dict\n and rule_dict.get(self.CONST_STD_RULE_CONDITION_CONF_KEY) is not None\n else {self.CONST_STD_RULE_CONDITION_CONF_KEY: None},\n std_condition_instances={\n self.CONST_USER_CONDITION_STRING: StandardCondition(\n condition_id=self.CONST_USER_CONDITION_STRING,\n description=\"Automatic description\",\n validation_function=rule_dict.get(self.CONST_STD_RULE_CONDITION_CONF_KEY),\n validation_function_parameters=rule_dict.get(\n self.CONST_CONDITION_VALIDATION_PARAMETERS_CONF_KEY\n ),\n )\n },\n condition_factory_mapping=self.BUILTIN_FACTORY_MAPPING,\n )\n rules_dict_formatted[group_id].append(rule)\n\n return {self.CONST_DFLT_RULE_SET_ID: rules_dict_formatted}\n\n def __str__(self) -> str:\n \"\"\"Object human string representation (called by str()).\n\n Returns:\n A string representation of the instance.\n \"\"\"\n # Vars init.\n attrs_str: str = \"\"\n\n # Get some instance attributes infos\n class_name: str = self.__class__.__name__\n attrs: list[tuple[str, Any]] = [\n attr\n for attr in inspect.getmembers(self)\n if not (\n attr[0].startswith(\"_\")\n or attr[0].startswith(\"CONST_\")\n or isinstance(attr[1], (FunctionType, MethodType))\n )\n ]\n\n # Build string representation\n for attr, val in attrs:\n attrs_str += f\"{attr}={str(val)}, \"\n\n return f\"{class_name}({attrs_str})\"\n
"},{"location":"api_reference/#arta._engine.RulesEngine.__init__","title":"__init__(*, rules_dict=None, config_path=None)
","text":"Initialize the rules.
2 possibilities: either 'rules_dict', or 'config_path', not both.
Parameters:
Name Type Description Defaultrules_dict
dict[str, dict[str, Any]] | None
A dictionary containing the rules' definitions.
None
config_path
str | None
Path of a directory containing the YAML files.
None
Raises:
Type DescriptionKeyError
Key not found.
TypeError
Wrong type.
Source code inarta/_engine.py
def __init__(\n self,\n *,\n rules_dict: dict[str, dict[str, Any]] | None = None,\n config_path: str | None = None,\n) -> None:\n \"\"\"Initialize the rules.\n\n 2 possibilities: either 'rules_dict', or 'config_path', not both.\n\n Args:\n rules_dict: A dictionary containing the rules' definitions.\n config_path: Path of a directory containing the YAML files.\n\n Raises:\n KeyError: Key not found.\n TypeError: Wrong type.\n \"\"\"\n # Var init.\n factory_mapping_classes: dict[str, type[BaseCondition]] = {}\n std_condition_instances: dict[str, StandardCondition] = {}\n\n if config_path is not None and rules_dict is not None:\n raise ValueError(\"RulesEngine takes only one parameter: 'rules_dict' or 'config_path', not both.\")\n\n # Init. default parsing_error_strategy (probably not needed because already defined elsewhere)\n self._parsing_error_strategy: ParsingErrorStrategy = ParsingErrorStrategy.RAISE\n\n # Initialize directly with a rules dict\n if rules_dict is not None:\n # Data validation\n RulesDict.parse_obj(rules_dict)\n\n # Edge cases data validation\n if not isinstance(rules_dict, dict):\n raise TypeError(f\"'rules_dict' must be dict type, not '{type(rules_dict)}'\")\n elif len(rules_dict) == 0:\n raise KeyError(\"'rules_dict' couldn't be empty.\")\n\n # Attribute definition\n self.rules: dict[str, dict[str, list[Rule]]] = self._adapt_user_rules_dict(rules_dict)\n\n # Initialize with a config_path\n elif config_path is not None:\n # Load config in attribute\n config_dict: dict[str, Any] = load_config(config_path)\n\n # Data validation\n config: Configuration = Configuration(**config_dict)\n\n if config.parsing_error_strategy is not None:\n # Set parsing error handling strategy from config\n self._parsing_error_strategy = ParsingErrorStrategy(config.parsing_error_strategy)\n\n # dict of available action functions (k: function name, v: function object)\n action_modules: list[str] = config.actions_source_modules\n action_functions: dict[str, Callable] = self._get_object_from_source_modules(action_modules)\n\n # dict of available standard condition functions (k: function name, v: function object)\n condition_modules: list[str] = (\n config.conditions_source_modules if config.conditions_source_modules is not None else []\n )\n std_condition_functions: dict[str, Callable] = self._get_object_from_source_modules(condition_modules)\n\n # Dictionary of condition instances (k: condition id, v: instance), built from config data\n if len(std_condition_functions) > 0:\n std_condition_instances = self._build_std_conditions(\n config=config.dict(), condition_functions_dict=std_condition_functions\n )\n\n # User-defined/custom conditions\n if config.condition_factory_mapping is not None and config.custom_classes_source_modules is not None:\n # dict of custom condition classes (k: classe name, v: class object)\n custom_condition_classes: dict[str, type[BaseCondition]] = self._get_object_from_source_modules(\n config.custom_classes_source_modules\n )\n\n # Build a factory mapping dictionary (k: conf key, v:class object)\n factory_mapping_classes.update(\n {\n conf_key: custom_condition_classes[class_name]\n for conf_key, class_name in config.condition_factory_mapping.items()\n }\n )\n\n # Arta built-in conditions\n factory_mapping_classes.update(self.BUILTIN_FACTORY_MAPPING)\n\n # Attribute definition\n self.rules = self._build_rules(\n std_condition_instances=std_condition_instances,\n action_functions=action_functions,\n config=config.dict(),\n factory_mapping_classes=factory_mapping_classes,\n )\n else:\n raise ValueError(\"RulesEngine needs a parameter: 'rule_dict' or 'config_path'.\")\n
"},{"location":"api_reference/#arta._engine.RulesEngine.__str__","title":"__str__()
","text":"Object human string representation (called by str()).
Returns:
Type Descriptionstr
A string representation of the instance.
Source code inarta/_engine.py
def __str__(self) -> str:\n \"\"\"Object human string representation (called by str()).\n\n Returns:\n A string representation of the instance.\n \"\"\"\n # Vars init.\n attrs_str: str = \"\"\n\n # Get some instance attributes infos\n class_name: str = self.__class__.__name__\n attrs: list[tuple[str, Any]] = [\n attr\n for attr in inspect.getmembers(self)\n if not (\n attr[0].startswith(\"_\")\n or attr[0].startswith(\"CONST_\")\n or isinstance(attr[1], (FunctionType, MethodType))\n )\n ]\n\n # Build string representation\n for attr, val in attrs:\n attrs_str += f\"{attr}={str(val)}, \"\n\n return f\"{class_name}({attrs_str})\"\n
"},{"location":"api_reference/#arta._engine.RulesEngine.apply_rules","title":"apply_rules(input_data, *, rule_set=None, verbose=False, **kwargs)
","text":"Apply the rules and return results.
For each rule group of a given rule set, rules are applied sequentially, The loop is broken when a rule is applied (an action is triggered). Then, the next rule group is evaluated. And so on...
This means that the order of the rules in the configuration file (e.g., rules.yaml) is meaningful.
Parameters:
Name Type Description Defaultinput_data
dict[str, Any]
Input data to apply rules on.
requiredrule_set
str | None
Apply rules associated with the specified rule set.
None
verbose
bool
If True, add extra ids (group_id, rule_id) for result explicability.
False
**kwargs
Any
For user extra arguments.
{}
Returns:
Type Descriptiondict[str, Any]
A dictionary containing the rule groups' results (k: group id, v: action result).
Raises:
Type DescriptionTypeError
Wrong type.
KeyError
Key not found.
Source code inarta/_engine.py
def apply_rules(\n self, input_data: dict[str, Any], *, rule_set: str | None = None, verbose: bool = False, **kwargs: Any\n) -> dict[str, Any]:\n \"\"\"Apply the rules and return results.\n\n For each rule group of a given rule set, rules are applied sequentially,\n The loop is broken when a rule is applied (an action is triggered).\n Then, the next rule group is evaluated.\n And so on...\n\n This means that the order of the rules in the configuration file\n (e.g., rules.yaml) is meaningful.\n\n Args:\n input_data: Input data to apply rules on.\n rule_set: Apply rules associated with the specified rule set.\n verbose: If True, add extra ids (group_id, rule_id) for result explicability.\n **kwargs: For user extra arguments.\n\n Returns:\n A dictionary containing the rule groups' results (k: group id, v: action result).\n\n Raises:\n TypeError: Wrong type.\n KeyError: Key not found.\n \"\"\"\n # Input_data validation\n if not isinstance(input_data, dict):\n raise TypeError(f\"'input_data' must be dict type, not '{type(input_data)}'\")\n elif len(input_data) == 0:\n raise KeyError(\"'input_data' couldn't be empty.\")\n\n # Var init.\n input_data_copy: dict[str, Any] = copy.deepcopy(input_data)\n\n # Prepare the result key\n input_data_copy[\"output\"] = {}\n\n # If there is no given rule set param. and there is only one rule set in self.rules\n # and its value is 'default_rule_set', look for this one (rule_set='default_rule_set')\n if rule_set is None and len(self.rules) == 1 and self.rules.get(self.CONST_DFLT_RULE_SET_ID) is not None:\n rule_set = self.CONST_DFLT_RULE_SET_ID\n\n # Check if given rule set is in self.rules?\n if rule_set not in self.rules:\n raise KeyError(\n f\"Rule set '{rule_set}' not found in the rules, available rule sets are : {list(self.rules.keys())}.\"\n )\n\n # Var init.\n results_dict: dict[str, Any] = {\"verbosity\": {\"rule_set\": rule_set, \"results\": []}}\n\n # Groups' loop\n for group_id, rules_list in self.rules[rule_set].items():\n # Initialize result of the rule group with None\n results_dict[group_id] = None\n\n # Rules' loop (inside a group)\n for rule in rules_list:\n # Apply rules\n action_result, rule_details = rule.apply(\n input_data_copy, parsing_error_strategy=self._parsing_error_strategy, **kwargs\n )\n\n # Check if the rule has been applied (= action activated)\n if \"action_result\" in rule_details:\n # Save result and details\n results_dict[group_id] = action_result\n results_dict[\"verbosity\"][\"results\"].append(rule_details)\n\n # Update input data with current result with key 'output' (can be used in next rules)\n input_data_copy[\"output\"][group_id] = copy.deepcopy(results_dict[group_id])\n\n # We can only have one result per group => break when \"action_result\" in rule_details\n break\n\n # Handling non-verbose mode\n if not verbose:\n results_dict.pop(\"verbosity\")\n\n return results_dict\n
"},{"location":"api_reference/#conditionpy","title":"condition.py","text":"Condition implementation.
Classes: BaseCondition, StandardCondition, SimpleCondition
"},{"location":"api_reference/#arta.condition.BaseCondition","title":"BaseCondition
","text":" Bases: ABC
Base class of a Condition object (Strategy Pattern).
Is an abstract class and can't be instantiated.
Attributes:
Name Type Descriptioncondition_id
Id of a condition.
description
Description of a condition.
validation_function
Validation function of a condition.
validation_function_parameters
Arguments of the validation function.
Source code inarta/condition.py
class BaseCondition(ABC):\n \"\"\"Base class of a Condition object (Strategy Pattern).\n\n Is an abstract class and can't be instantiated.\n\n Attributes:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n\n # Class constants\n CONST_CONDITION_DATA_LABEL: str = \"Custom condition data (not needed)\"\n CONDITION_ID_PATTERN: str = UPPERCASE_WORD_PATTERN\n\n def __init__(\n self,\n condition_id: str,\n description: str,\n validation_function: Callable | None = None,\n validation_function_parameters: dict[str, Any] | None = None,\n ) -> None:\n \"\"\"\n Initialize attributes.\n\n Args:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n self._condition_id = condition_id # NOSONAR\n self._description = description # NOSONAR\n self._validation_function = validation_function # NOSONAR\n self._validation_function_parameters = validation_function_parameters # NOSONAR\n\n @classmethod\n def extract_condition_ids_from_expression(cls, condition_expr: str | None = None) -> set[str]:\n \"\"\"Get the condition ids from a string (e.g., UPPERCASE words).\n\n E.g., CONDITION_1 and not CONDITION_2\n\n Warning: implementation is based on the current class constant CONDITION_SPLIT_PATTERN.\n\n Args:\n condition_expr: A boolean expression (string).\n\n Returns:\n A set of extracted condition ids.\n \"\"\"\n cond_ids: set[str] = set()\n\n if condition_expr is not None:\n cond_ids = set(re.findall(cls.CONDITION_ID_PATTERN, condition_expr))\n\n return cond_ids\n\n @abstractmethod\n def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"(Abstract)\n Return True if the condition is verified.\n\n Args:\n input_data: Input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n \"\"\"\n raise NotImplementedError\n
"},{"location":"api_reference/#arta.condition.BaseCondition.__init__","title":"__init__(condition_id, description, validation_function=None, validation_function_parameters=None)
","text":"Initialize attributes.
Parameters:
Name Type Description Defaultcondition_id
str
Id of a condition.
requireddescription
str
Description of a condition.
requiredvalidation_function
Callable | None
Validation function of a condition.
None
validation_function_parameters
dict[str, Any] | None
Arguments of the validation function.
None
Source code in arta/condition.py
def __init__(\n self,\n condition_id: str,\n description: str,\n validation_function: Callable | None = None,\n validation_function_parameters: dict[str, Any] | None = None,\n) -> None:\n \"\"\"\n Initialize attributes.\n\n Args:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n self._condition_id = condition_id # NOSONAR\n self._description = description # NOSONAR\n self._validation_function = validation_function # NOSONAR\n self._validation_function_parameters = validation_function_parameters # NOSONAR\n
"},{"location":"api_reference/#arta.condition.BaseCondition.extract_condition_ids_from_expression","title":"extract_condition_ids_from_expression(condition_expr=None)
classmethod
","text":"Get the condition ids from a string (e.g., UPPERCASE words).
E.g., CONDITION_1 and not CONDITION_2
Warning: implementation is based on the current class constant CONDITION_SPLIT_PATTERN.
Parameters:
Name Type Description Defaultcondition_expr
str | None
A boolean expression (string).
None
Returns:
Type Descriptionset[str]
A set of extracted condition ids.
Source code inarta/condition.py
@classmethod\ndef extract_condition_ids_from_expression(cls, condition_expr: str | None = None) -> set[str]:\n \"\"\"Get the condition ids from a string (e.g., UPPERCASE words).\n\n E.g., CONDITION_1 and not CONDITION_2\n\n Warning: implementation is based on the current class constant CONDITION_SPLIT_PATTERN.\n\n Args:\n condition_expr: A boolean expression (string).\n\n Returns:\n A set of extracted condition ids.\n \"\"\"\n cond_ids: set[str] = set()\n\n if condition_expr is not None:\n cond_ids = set(re.findall(cls.CONDITION_ID_PATTERN, condition_expr))\n\n return cond_ids\n
"},{"location":"api_reference/#arta.condition.BaseCondition.verify","title":"verify(input_data, parsing_error_strategy, **kwargs)
abstractmethod
","text":"(Abstract) Return True if the condition is verified.
Parameters:
Name Type Description Defaultinput_data
dict[str, Any]
Input data to apply rules on.
requiredparsing_error_strategy
ParsingErrorStrategy
Error handling strategy for parameter parsing.
required**kwargs
Any
For user extra arguments.
{}
Returns:
Type Descriptionbool
True if the condition is verified, otherwise False.
Source code inarta/condition.py
@abstractmethod\ndef verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"(Abstract)\n Return True if the condition is verified.\n\n Args:\n input_data: Input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n \"\"\"\n raise NotImplementedError\n
"},{"location":"api_reference/#arta.condition.SimpleCondition","title":"SimpleCondition
","text":" Bases: BaseCondition
Class implementing a built-in simple condition.
Attributes:
Name Type Descriptioncondition_id
Id of a condition.
description
Description of a condition.
validation_function
Validation function of a condition.
validation_function_parameters
Arguments of the validation function.
Source code inarta/condition.py
class SimpleCondition(BaseCondition):\n \"\"\"Class implementing a built-in simple condition.\n\n Attributes:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n\n # Class constants\n CONST_CUSTOM_CONDITION_DATA_LABEL: str = \"Simple condition data (not needed)\"\n CONDITION_ID_PATTERN: str = r\"(?:input\\.|output\\.)(?:[a-z_\\-0-9!=<>\\\"NTF\\.]*)\"\n\n def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"Return True if the condition is verified.\n\n Example of a unitary simple condition to be verified: 'input.age>=100'\n\n Args:\n input_data: Request or input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n\n Raises:\n AttributeError: Check the validation function or its parameters.\n \"\"\"\n bool_var: bool = False\n unitary_expr: str = self._condition_id\n\n data_path_patt: str = r\"(?:input\\.|output\\.)(?:[a-z_\\-\\.]*)\"\n\n # Retrieve only the data path\n path_matches: list[str] = re.findall(data_path_patt, unitary_expr)\n\n if len(path_matches) == 1:\n # Regular case: we have a data_path\n data_path: str = path_matches[0]\n\n # Read data from its path\n data = parse_dynamic_parameter( # noqa\n parameter=data_path, input_data=input_data, parsing_error_strategy=parsing_error_strategy\n )\n\n # Replace with the variable name in the expression\n eval_expr: str = unitary_expr.replace(data_path, \"data\")\n\n # Evaluate the expression\n try:\n bool_var = eval(eval_expr) # noqa\n except TypeError:\n # Ignore evaluation --> False\n pass\n\n elif parsing_error_strategy == ParsingErrorStrategy.RAISE:\n # Raise an error because of no match for a data path\n raise ConditionExecutionError(f\"Error when verifying simple condition: '{unitary_expr}'\")\n\n else:\n # Other case: ignore, default value => return False\n pass\n\n return bool_var\n
"},{"location":"api_reference/#arta.condition.SimpleCondition.verify","title":"verify(input_data, parsing_error_strategy, **kwargs)
","text":"Return True if the condition is verified.
Example of a unitary simple condition to be verified: 'input.age>=100'
Parameters:
Name Type Description Defaultinput_data
dict[str, Any]
Request or input data to apply rules on.
requiredparsing_error_strategy
ParsingErrorStrategy
Error handling strategy for parameter parsing.
required**kwargs
Any
For user extra arguments.
{}
Returns:
Type Descriptionbool
True if the condition is verified, otherwise False.
Raises:
Type DescriptionAttributeError
Check the validation function or its parameters.
Source code inarta/condition.py
def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"Return True if the condition is verified.\n\n Example of a unitary simple condition to be verified: 'input.age>=100'\n\n Args:\n input_data: Request or input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n\n Raises:\n AttributeError: Check the validation function or its parameters.\n \"\"\"\n bool_var: bool = False\n unitary_expr: str = self._condition_id\n\n data_path_patt: str = r\"(?:input\\.|output\\.)(?:[a-z_\\-\\.]*)\"\n\n # Retrieve only the data path\n path_matches: list[str] = re.findall(data_path_patt, unitary_expr)\n\n if len(path_matches) == 1:\n # Regular case: we have a data_path\n data_path: str = path_matches[0]\n\n # Read data from its path\n data = parse_dynamic_parameter( # noqa\n parameter=data_path, input_data=input_data, parsing_error_strategy=parsing_error_strategy\n )\n\n # Replace with the variable name in the expression\n eval_expr: str = unitary_expr.replace(data_path, \"data\")\n\n # Evaluate the expression\n try:\n bool_var = eval(eval_expr) # noqa\n except TypeError:\n # Ignore evaluation --> False\n pass\n\n elif parsing_error_strategy == ParsingErrorStrategy.RAISE:\n # Raise an error because of no match for a data path\n raise ConditionExecutionError(f\"Error when verifying simple condition: '{unitary_expr}'\")\n\n else:\n # Other case: ignore, default value => return False\n pass\n\n return bool_var\n
"},{"location":"api_reference/#arta.condition.StandardCondition","title":"StandardCondition
","text":" Bases: BaseCondition
Class implementing a built-in condition, named standard condition.
Attributes:
Name Type Descriptioncondition_id
Id of a condition.
description
Description of a condition.
validation_function
Validation function of a condition.
validation_function_parameters
Arguments of the validation function.
Source code inarta/condition.py
class StandardCondition(BaseCondition):\n \"\"\"Class implementing a built-in condition, named standard condition.\n\n Attributes:\n condition_id: Id of a condition.\n description: Description of a condition.\n validation_function: Validation function of a condition.\n validation_function_parameters: Arguments of the validation function.\n \"\"\"\n\n def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"Return True if the condition is verified.\n\n Example of a unitary standard condition: CONDITION_1\n\n Args:\n input_data: Request or input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n\n Raises:\n AttributeError: Check the validation function or its parameters.\n \"\"\"\n if self._validation_function is None:\n raise AttributeError(\"Validation function should not be None\")\n\n if self._validation_function_parameters is None:\n raise AttributeError(\"Validation function parameters should not be None\")\n\n # Parse dynamic parameters\n parameters: dict[str, Any] = {}\n\n for key, value in self._validation_function_parameters.items():\n parameters[key] = parse_dynamic_parameter(\n parameter=value, input_data=input_data, parsing_error_strategy=parsing_error_strategy\n )\n\n # Run validation_function\n return self._validation_function(**parameters)\n
"},{"location":"api_reference/#arta.condition.StandardCondition.verify","title":"verify(input_data, parsing_error_strategy, **kwargs)
","text":"Return True if the condition is verified.
Example of a unitary standard condition: CONDITION_1
Parameters:
Name Type Description Defaultinput_data
dict[str, Any]
Request or input data to apply rules on.
requiredparsing_error_strategy
ParsingErrorStrategy
Error handling strategy for parameter parsing.
required**kwargs
Any
For user extra arguments.
{}
Returns:
Type Descriptionbool
True if the condition is verified, otherwise False.
Raises:
Type DescriptionAttributeError
Check the validation function or its parameters.
Source code inarta/condition.py
def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErrorStrategy, **kwargs: Any) -> bool:\n \"\"\"Return True if the condition is verified.\n\n Example of a unitary standard condition: CONDITION_1\n\n Args:\n input_data: Request or input data to apply rules on.\n parsing_error_strategy: Error handling strategy for parameter parsing.\n **kwargs: For user extra arguments.\n\n Returns:\n True if the condition is verified, otherwise False.\n\n Raises:\n AttributeError: Check the validation function or its parameters.\n \"\"\"\n if self._validation_function is None:\n raise AttributeError(\"Validation function should not be None\")\n\n if self._validation_function_parameters is None:\n raise AttributeError(\"Validation function parameters should not be None\")\n\n # Parse dynamic parameters\n parameters: dict[str, Any] = {}\n\n for key, value in self._validation_function_parameters.items():\n parameters[key] = parse_dynamic_parameter(\n parameter=value, input_data=input_data, parsing_error_strategy=parsing_error_strategy\n )\n\n # Run validation_function\n return self._validation_function(**parameters)\n
"},{"location":"glossary/","title":"Glossary","text":"Concept Definition action A task which is executed when conditions are verified. action function A callable object called to execute the action. action parameter Parameter of an action function. condition A condition to be verified before executing an action. condition id Identifier of a single condition (must be in CAPITAL LETTER). condition expression A boolean expression combining several conditions (meaning several condition id). condition function A callable object called to be verified therefore it returns a boolean. condition parameter Parameter of a condition/validation function. custom condition A user-defined condition. rule A set of conditions combined to one action. rule group A group of rules (usually sharing a common context). rule id Identifier of a single rule. rule set A set of rule groups (mostly one: default_rule_set
). simple condition A built-in very simple condition. standard condition The regular built-in condition. validation function Same thing as a condition function."},{"location":"home/","title":"Home","text":"An Open Source Rules Engine - Make rule handling simple
"},{"location":"home/#welcome-to-the-documentation","title":"Welcome to the documentation","text":"
New feature
Check out the new and very convenient feature called the simple condition. A new and lightweight way of configuring your rules' conditions.
Arta is automatically tested with:
Releases
Want to see last updates, check the Release notes
Pydantic 2
Arta is now working with Pydantic 2! And of course, Pydantic 1 as well.
"},{"location":"how_to/","title":"How to","text":"Ensure that you have correctly installed Arta before, check the Installation page
"},{"location":"how_to/#simple-condition","title":"Simple condition","text":"Beta feature
Simple condition is still a beta feature, some cases could not work as designed.
Simple conditions are a new and straightforward way of configuring your conditions.
It simplifies a lot your rules by:
conditions.py
module (no validation functions needed).conditions:
configuration key in your YAML files.Note
With the simple conditions you use straight boolean expressions directly in your configuration.
It is easyer to read and maintain
The configuration key here is:
simple_condition:
Example :
---\nrules:\n default_rule_set:\n admission:\n ADM_OK:\n simple_condition: input.power==\"strength\" or input.power==\"fly\"\n action: set_admission\n action_parameters:\n value: OK \n ADM_TO_BE_CHECKED:\n simple_condition: input.age>=150 and input.age!=None\n action: set_admission\n action_parameters:\n value: TO_CHECK \n ADM_KO:\n simple_condition: null\n action: set_admission\n action_parameters:\n value: KO\n\nactions_source_modules:\n - my_folder.actions # (1)\n
conditions_source_modules
here.How to write a simple condition like:
input.power==\"strength\" or input.power==\"fly\"\n
input
(for input data)output
(for previous rule's result)input.powers.main_power
.==, <, >, <=, >=, !=
)str, int, None
).Warning
is
or in
, as an operator (yet).float
as right operand (it's a bug, will be fixed).\"
.Security concern
Python code injection:
Because Arta is using the eval()
built-in function to evaluate simple conditions:
simple_condition:
conf. key).It is the first implemented way of using Arta and probably the most powerful.
The configuration key here is:
condition:
YAML
The built-in file format used by Arta for configuration is YAML.
Enhancement proposal
We are thinking on a design that will allow user-implemented loading of the configuration (e.g., if you prefer using a JSON format). Stay tuned.
"},{"location":"how_to/#yaml-file","title":"YAML file","text":"Simple Conditions
The following YAML example illustrates how to configure usual standard conditions but there is another and simpler way to do it by using a special feature: the simple condition.
Create a YAML file and define your rules like this:
---\nrules:\n default_rule_set: # (1)\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER # (2)\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions: # (3)\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n\nconditions_source_modules: # (4)\n - my_folder.conditions\nactions_source_modules: # (5)\n - my_folder.actions\n
default_rule_set
is by default).Warning
Condition ids must be in capital letters here, it is mandatory (e.g., HAS_SCHOOL_AUTHORIZED_POWER
).
Tip
You can split your configuration in multiple YAML files seamlessly in order to keep things clear. Example:
It's very convenient when you have a lot of different rules and conditions in your app.
"},{"location":"how_to/#condition-expression","title":"Condition expression","text":"In the above YAML, the following condition expression is intentionally very simple:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n
The key condition:
can take one condition id but also a condition expression (i.e., a boolean expression of condition ids) combining several conditions:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: (HAS_SCHOOL_AUTHORIZED_POWER or SPEAKS_FRENCH) and not(IS_EVIL)\n action: set_admission\n action_parameters:\n value: true\n
Warning
In that example, you must define the 3 conditions in the configuration:
Tip
Use the condition expressions to keep things simple. Put your conditions in one expression as you can rather than creating several rules
"},{"location":"how_to/#functions","title":"Functions","text":"We must create 2 modules:
conditions.py
-> implements the needed validation functions.actions.py
-> implements the needed action functions.Note
Module names are arbitrary, you can choose what you want.
And implement our 2 needed validation and action functions (the one defined in the configuration file):
conditions.py:
def has_authorized_super_power(power):\n return power in [\"strength\", \"fly\", \"immortality\"]\n
actions.py:
def set_admission(value, **kwargs): # (1)\n return {\"is_admitted\": value}\n
**kwargs
is mandatory here.Warning
Function name and parameters must be the same as the one configured in the YAML file.
"},{"location":"how_to/#usage","title":"Usage","text":"Once your configuration file and your functions are ready, you can use it very simply:
from arta import RulesEngine\n\ninput_data = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"super_power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n\neng = RulesEngine(config_path=\"path/to/conf/dir\")\n\nresult = eng.apply_rules(input_data)\n\nprint(result)\n
You should get:
{'check_admission': {'is_admitted': True}}
API Documentation
You can get details on the RulesEngine
parameters in the API Reference.
Let's go deeper into the concepts.
"},{"location":"how_to/#rule-set-and-rule-group","title":"Rule set and rule group","text":"A rule set is composed of rule groups which are themselves composed of rules. We can find this tree structure in the following YAML:
---\nrules:\n default_rule_set: # (1)\n check_admission: # (2)\n ADMITTED_RULE: # (3)\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n
Rule definitions are identified by an id (e.g., ADMITTED_RULE
):
ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n
Tip
Rule ids are in capital letters for readability only: it is an advised practice.
Rules are made of 2 different things:
ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n
ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n
"},{"location":"how_to/#condition-and-action","title":"Condition and Action","text":"Conditions and actions are quite similar in terms of implementation but their goals are different.
Both are made of a callable object and some parameters:
Condition keys:
validation_function
: name of a callable python object that returns a bool
, we called this function the validation function (or condition function*).
condition_parameters
: the validation function's arguments.
Action keys:
action
: name of a callable python object that returns what you want (or does what you want such as: requesting an api, sending an email, etc.), we called this function the action function.
action_parameters
: the action function's arguments.
Parameter's special syntax
The action and condition arguments can have a special syntax:
condition_parameters:\n power: input.super_power\n
The string input.super_power
is evaluated by the rules engine and it means \"fetch the key super_power
in the input data\".
Rules can be configured in a YAML file but they can also be defined by a regular dictionary:
Without type hintsWith type hints (>=3.9)from arta import RulesEngine\n\nset_admission = lambda value, **kwargs: {\"is_admitted\": value}\n\nrules = {\n \"check_admission\": {\n \"ADMITTED_RULE\": {\n \"condition\": lambda power: power in [\"strength\", \"fly\", \"immortality\"],\n \"condition_parameters\": {\"power\": \"input.super_power\"}, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": True},\n },\n \"DEFAULT_RULE\": {\n \"condition\": None,\n \"condition_parameters\": None, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": False},\n },\n }\n}\n\ninput_data = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"super_power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n\neng = RulesEngine(rules_dict=rules)\n\nresult = eng.apply_rules(input_data)\n\nprint(result)\n
from typing import Any, Callable\n\nfrom arta import RulesEngine\n\nset_admission: Callable = lambda value, **kwargs: {\"is_admitted\": value}\n\nrules: dict[str, Any] = {\n \"check_admission\": {\n \"ADMITTED_RULE\": {\n \"condition\": lambda power: power in [\"strength\", \"fly\", \"immortality\"],\n \"condition_parameters\": {\"power\": \"input.super_power\"}, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": True},\n },\n \"DEFAULT_RULE\": {\n \"condition\": None,\n \"condition_parameters\": None, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": False},\n },\n }\n}\n\ninput_data: dict[str, Any] = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"super_power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n\neng = RulesEngine(rules_dict=rules)\n\nresult: dict[str, Any] = eng.apply_rules(input_data)\n\nprint(result)\n
You should get:
{'check_admission': {'is_admitted': True}}\n
Success
Superman is admitted to the superhero school!
Well done! By executing this code you have:
set_admission
)rules
)input_data
)RulesEngine
).apply_rules()
)Note
In the code example we used some anonymous/lambda function for simplicity but it could be regular python functions as well.
YAML vs Dictionary
How to choose between dictionary and configuration?
In most cases, you must choose the configuration way of defining your rules.
You will improve your rules' maintainability a lot. In some cases like proof-of-concepts or Jupyter notebook works, you will probably be happy with straightforward dictionaries.
Arta has plenty more features to discover. If you want to learn more, go to the next chapter: Advanced User Guide.
"},{"location":"installation/","title":"Installation","text":""},{"location":"installation/#python","title":"Python","text":"Compatible with:
"},{"location":"installation/#pip","title":"pip","text":"In your python environment:
"},{"location":"installation/#regular-use","title":"Regular use","text":"pip install arta\n
"},{"location":"installation/#development","title":"Development","text":"pip install arta[all]\n
"},{"location":"parameters/","title":"Parameters","text":""},{"location":"parameters/#parsing-prefix-keywords","title":"Parsing prefix keywords","text":"There is 2 allowed parsing prefix keywords:
input
: corresponding to the input_data
.output
: corresponding to the result output data (returned by the apply_rules()
method).Here are examples:
input.name
: maps to input_data[\"name\"]
.output.check_admission.is_admitted
: maps to result[\"check_admission\"][\"is_admitted\"]
.They both can be used in condition and action parameters.
Info
A value without any prefix keyword is a constant.
"},{"location":"parameters/#parsing-error","title":"Parsing error","text":""},{"location":"parameters/#raise-by-default","title":"Raise by default","text":"By default, errors during condition and action parameters parsing are raised.
If we refer to the dictionary example:
rules = {\n \"check_admission\": {\n \"ADMITTED_RULE\": {\n \"condition\": lambda power: power in [\"strength\", \"fly\", \"immortality\"],\n \"condition_parameters\": {\"power\": \"input.super_power\"}, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": True},\n },\n \"DEFAULT_RULE\": {\n \"condition\": None,\n \"condition_parameters\": None, \n \"action\": set_admission,\n \"action_parameters\": {\"value\": False},\n },\n }\n}\n
With modified data like:
input_data = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n
By default we will get a KeyError
exception during the execution of the apply_rules()
method because of power
vs super_power
.
You can change the by default raising behavior of the parameter's parsing.
Two ways are possible:
You just have to add the following key somewhere in your configuration:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules: \n - my_folder.actions\n\nparsing_error_strategy: ignore # (1)\n
parsing_error_strategy
has two possible values: raise
and ignore
.It will affect all the parameters.
"},{"location":"parameters/#parameter-level","title":"Parameter level","text":"Quick Sum Up
input.super_power?
: set the value to None
input.super_power?no_power
: set the value to no_power
input.super_power!
: force raise exception (case when ignore is set by default)You can also handle more precisely that aspect at parameter's level:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power? # (1)\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules: \n - my_folder.actions\n
KeyError
when reading, power
will be set to None
rather than raising the exception.Info
You can enforce raising exceptions at parameter's level with !
.
power: input.super_power!\n
"},{"location":"parameters/#default-value-parameter-level","title":"Default value (parameter level)","text":"Finally, you can set a default value at parameter's level. This value will be used if there is an exception during parsing:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power?no_power # (1)\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules: \n - my_folder.actions\n
power
will be set to \"no_power\"
.Good to know
Parameter's level is overriding configuration level.
"},{"location":"rule_sets/","title":"Rule sets","text":"Rule sets are a convenient way to separate your business rules into different collections.
Doing so increases the rules' maintainability because of a better organization and fully uncoupled rules.
Tip
Rule sets are very usefull when you have a lot of rules.
Info
Most of the time, you won't need to handle different rule sets and will only use the default one: default_rule_set
.
The good news is that different rule sets can be used seamlessly with the same rules engine instance
Let's take the following example:
Based on that example, imagine that you need to add some rules about something totally different than the superhero school. Let's say rules for a dinosaur school.
"},{"location":"rule_sets/#configuration","title":"Configuration","text":"Update your configuration by adding a new rule set: dinosaur_school_set
---\nrules:\n superhero_school_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n dinosaur_school_set: # (1)\n food_habit:\n HERBIVOROUS:\n condition: not(IS_EATING_MEAT)\n action: send_mail_to_cook\n action_parameters:\n meal: \"plant\"\n CARNIVOROUS:\n condition: null\n action: send_mail_to_cook\n action_parameters:\n meal: \"meat\"\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n IS_EATING_MEAT: # (2)\n description: \"Is dinosaur eating meat?\"\n validation_function: is_eating_meat\n condition_parameters:\n power: input.diet.regular_food\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules:\n - my_folder.actions\n
rules
keyGood to know
You can define your rule sets into different YAML files (under the rules
key in each).
Now that your rule sets are defined (and assuming that your condition and action functions are implemented in the right modules), you can easily use them:
from arta import RulesEngine\n\ninput_data_1 = {\n \"id\": 1,\n \"name\": \"Superman\",\n \"civilian_name\": \"Clark Kent\",\n \"age\": None,\n \"city\": \"Metropolis\",\n \"language\": \"french\",\n \"super_power\": \"fly\",\n \"favorite_meal\": \"Spinach\",\n \"secret_weakness\": \"Kryptonite\",\n \"weapons\": [],\n}\n\ninput_data_2 = {\n \"id\": 1,\n \"name\": \"Diplodocus\",\n \"age\": 152000000,\n \"length\": 31,\n \"area\": \"north_america\",\n \"diet\": {\n \"regular_food\": \"plants\",\n },\n}\n\neng = RulesEngine(config_path=\"path/to/conf/dir\")\n\nsuperhero_result = eng.apply_rules(input_data_1, rule_set=\"superhero_school_set\") # (1)\n\ndinosaur_result = eng.apply_rules(input_data_2, rule_set=\"dinosaur_school_set\")\n
Good to know
Input data can be different or the same among the rule sets. It depends on the use case.
"},{"location":"rule_sets/#object-oriented-model","title":"Object-Oriented Model","text":"classDiagram\n rule_set \"1\" -- \"1..*\" rule_group\n rule_group \"1\" -- \"1..*\" rule\n rule \"1..*\" -- \"0..*\" condition\n rule \"1..*\" -- \"1\" action
"},{"location":"special_conditions/","title":"Special conditions","text":""},{"location":"special_conditions/#custom-condition","title":"Custom condition","text":"Custom conditions are user-defined conditions.
A custom condition will impact the atomic evaluation of each conditions (i.e., condition ids).
Vocabulary
To be more precise, a condition expression is something like:
CONDITION_1 and CONDITION_2\n
In that example, the condition expression is made of 2 conditions whose condition ids are:
With the built-in condition (also named standard condition), condition ids map to validation functions and condition parameters but we can change that with a brand new custom condition.
A custom condition example:
my_condition: NAME_JOHN and AGE_42\n
Remember
condition ids have to be in CAPITAL LETTERS.
Imagine you want it to be interpreted as (pseudo-code):
if input.name == \"john\" and input.age == \"42\":\n # Do something\n ...\n
With the custom conditions it's quite simple to implement.
Why using a custom condition?
The main goal is to simplify handling of recurrent conditions (e.i., \"recurrent\" meaning very similar conditions).
"},{"location":"special_conditions/#class-implementation","title":"Class implementation","text":"First, create a class inheriting from BaseCondtion
and implement the verify()
method as you want/need:
from typing import Any\n\nfrom arta.condition import BaseCondition\nfrom arta.utils import ParsingErrorStrategy\n\n\nclass MyCondition(BaseCondition):\n def verify(\n self,\n input_data: dict[str, Any],\n parsing_error_strategy: ParsingErrorStrategy,\n **kwargs: Any\n ) -> bool:\n\n field, value = tuple(self.condition_id.split(\"_\"))\n\n return input_data[field.lower()] == value.lower()\n
self.condition_id
self.condition_id
will be NAME_JOHN
for the first condition and AGE_42
for the second.
Good to know
The parsing_error_strategy
can be used by the developer to adapt exception handling behavior. Possible values:
ParsingErrorStrategy.RAISE\nParsingErrorStrategy.IGNORE\nParsingErrorStrategy.DEFAULT_VALUE\n
"},{"location":"special_conditions/#configuration","title":"Configuration","text":"Last thing to do is to add your new custom condition in the configuration:
---\nrules:\n default_rule_set:\n check_admission:\n ADMITTED_RULE:\n condition: HAS_SCHOOL_AUTHORIZED_POWER\n my_condition: NAME_JOHN and AGE_42 # (1)\n action: set_admission\n action_parameters:\n value: true\n DEFAULT_RULE:\n condition: null\n action: set_admission\n action_parameters:\n value: false\n\nconditions:\n HAS_SCHOOL_AUTHORIZED_POWER:\n description: \"Does applicant have a school authorized power?\"\n validation_function: has_authorized_super_power\n condition_parameters:\n power: input.super_power\n\nconditions_source_modules:\n - my_folder.conditions\nactions_source_modules: \n - my_folder.actions\n\ncustom_classes_source_modules:\n - dir.to.my_module # (2)\ncondition_factory_mapping:\n my_condition: MyCondition # (3)\n
condition
then my_condition
. Order is arbitrary.my_condition
) and custom classes (MyCondition
)It is based on the following strategy pattern:
classDiagram\n note for MyCondition \"This is a custom condition class\"\n RulesEngine \"1\" -- \"1..*\" Rule\n Rule \"1..*\" -- \"0..*\" BaseCondition\n BaseCondition <|-- StandardCondition\n BaseCondition <|-- SimpleCondition\n BaseCondition <|-- MyCondition\n class RulesEngine{\n +rules\n +apply_rules()\n }\n class Rule {\n #set_id\n #group_id\n #rule_id\n #condition_exprs\n #action\n #action_parameters\n +apply()\n }\n class BaseCondition {\n <<abstract>>\n #condition_id\n #description\n #validation_function\n #validation_function_parameters\n +verify()\n }
Good to know
The class StandardCondition
is the built-in implementation of a condition.
There is one main reason for using Arta and it was the main goal of its development:
Increase business rules maintainability.
In other words, facilitate rules handling in a python app.
"},{"location":"why/#before-arta","title":"Before Arta","text":"Rules in code can rapidly become a headache, kind of spaghetti dish of if
, elif
and else
(or even match/case
since Python 3.10).
Arta increases rules maintainability:
Improve collaboration
Reading python code vs reading YAML.
"},{"location":"assets/js/tarteaucitron/","title":"Index","text":""},{"location":"assets/js/tarteaucitron/#tarteaucitronjs","title":"tarteaucitron.js","text":"Comply to the european cookie law is simple with the french tarte au citron.
"},{"location":"assets/js/tarteaucitron/#what-is-this-script","title":"What is this script?","text":"The european cookie law regulates the management of cookies and you should ask your visitors their consent before exposing them to third party services.
Clearly this script will: - Disable all services by default, - Display a banner on the first page view and a small one on other pages, - Display a panel to allow or deny each services one by one, - Activate services on the second page view if not denied, - Store the consent in a cookie for 365 days.
Bonus: - Load service when user click on Allow (without reload of the page), - Incorporate a fallback system (display a link instead of social button and a static banner instead of advertising).
"},{"location":"assets/js/tarteaucitron/#supported-services","title":"Supported services","text":"vShop
APIs
Typekit (adobe)
Audience measurement
Xiti
Comment
Facebook (commentaire)
Social network
Twitter (timelines)
Support
Zopim
Video
In PHP for example, you can bypass all the script by setting this var tarteaucitron.user.bypass = true;
if the visitor is not in the EU.
Visit opt-out.ferank.eu
"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 4b6e771..9f2bc91 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,57 +2,57 @@