Source code for ahbicht.expressions.requirement_constraint_expression_evaluation

"""
This module evaluates the parsed condition expression tree regarding the requirement constraints using ConditionNodes.
The RequirementConstraintTransformer defines the rules how the different parts and nodes
of the condition expression tree are handled.

The used terms are defined in the README_conditions.md.
"""

from typing import Literal, Mapping, Optional, Type, Union

from lark import Token, Tree, v_args
from lark.exceptions import VisitError

from ahbicht.condition_node_builder import ConditionNodeBuilder, TRCTransformerArgument
from ahbicht.content_evaluation.ahb_context import AhbContext
from ahbicht.expressions import InvalidExpressionError
from ahbicht.expressions.base_transformer import BaseTransformer
from ahbicht.expressions.condition_expression_parser import parse_condition_expression_to_tree
from ahbicht.expressions.expression_builder import FormatConstraintExpressionBuilder, HintExpressionBuilder
from ahbicht.models.condition_nodes import (
    ConditionFulfilledValue,
    ConditionNode,
    EvaluatedComposition,
    FormatConstraint,
    Hint,
    RequirementConstraint,
    UnevaluatedFormatConstraint,
)
from ahbicht.models.evaluation_results import RequirementConstraintEvaluationResult


[docs] @v_args(inline=True) # Children are provided as *args instead of a list argument # pylint:disable=inherit-non-class class RequirementConstraintTransformer(BaseTransformer[TRCTransformerArgument, EvaluatedComposition]): """ Transformer that evaluates the trees built from the condition expressions regarding the requirement constraints. The input are the conditions as defined in the AHBs in the form of ConditionNodes. In the case of a Bedingung/RequirementConstraint already evaluated for the single condition. Hints are evaluated as neutral element in the boolean logic: and_composition: True, or_composition: False. After two nodes are evaluated in a composition, the resulting node has the node type EvalutatedComposition. The return value is a ConditionNode whose attribute `conditions_fulfilled` describes whether it is a required field, its hints are the gathered hints and its `format_constraint_expression` contains the gathered format constraints for the field in a newly built expression. """
[docs] def and_composition(self, left: TRCTransformerArgument, right: TRCTransformerArgument) -> EvaluatedComposition: """Evaluates logical and_composition""" # if one of the nodes is neutral the condition_fulfilled.value of the other one is the resulting one evaluated_composition = EvaluatedComposition( conditions_fulfilled=left.conditions_fulfilled & right.conditions_fulfilled ) # Hints are added if the branch is true or neutral if evaluated_composition.conditions_fulfilled != ConditionFulfilledValue.UNFULFILLED: evaluated_composition.hint = ( HintExpressionBuilder(getattr(left, "hint", None)).land(getattr(right, "hint", None)).get_expression() ) evaluated_composition.format_constraints_expression = ( FormatConstraintExpressionBuilder(left).land(right).get_expression() ) return evaluated_composition
def _or_xor_composition( self, left: ConditionNode, right: ConditionNode, composition: Literal["or_composition", "xor_composition"] ) -> EvaluatedComposition: """ Determine the condition_fulfilled attribute for or_/xor_compostions. """ # combining a hint with a format constraint in an or_/xor_composition has no useful result if (isinstance(left, Hint) and isinstance(right, UnevaluatedFormatConstraint)) or ( isinstance(right, Hint) and isinstance(left, UnevaluatedFormatConstraint) ): raise InvalidExpressionError( error_message=f"Combining a {left.__class__} and a {right.__class__} in an" f"{composition} is not implemented as it has no useful result." ) # combining a neutral element with a boolean value in an or_/xor_composition has no useful result if ( left.conditions_fulfilled == ConditionFulfilledValue.NEUTRAL and right.conditions_fulfilled != ConditionFulfilledValue.NEUTRAL or ( right.conditions_fulfilled == ConditionFulfilledValue.NEUTRAL and left.conditions_fulfilled != ConditionFulfilledValue.NEUTRAL ) ): neutral_element: ConditionNode boolean_element: ConditionNode if right.conditions_fulfilled == ConditionFulfilledValue.NEUTRAL: neutral_element = right boolean_element = left else: neutral_element = left boolean_element = right error_message = ( f"Combining a neutral element '{neutral_element}' with a boolean value {boolean_element} " f"in an {composition} is not implemented as it has no useful result." ) if isinstance(neutral_element, Hint): # This error is quite popular in AHBs. # The BDEW tends to write stuff like "[123] O [584]" where [123] is a regular RC and [584] is # something like "Verwendung der ID der Markt/Messlokation". Their intention is that the hint on the # right side should be evaluated as if it was a self-referencing Requirement Constraint that # magically evaluates to Fulfilled/Unfulfilled instead of being treated as what it is: A plain hint. # This is similar to what they tried with [93x] (german datetime) format constraints. # But other than the 931-935 FCs this kind of expression is not only a bad idea but also not even # valid according to their own set of rules. # If you encounter such an error in the AHBs, please message the EDI@Energy Arbeitsgruppe to fix it. # The easiest workaround (beyond ignoring it) is probably to convert it to an _xor_ expression: # Instead of "[123] O [584]" write "[983][123] X [984][1]" where # - [1] is an artificial requirement constraint, that should always evaluate to FULFILLED # (Note that the "[1]" does not exist by default; Here it's just a placeholder for this example.) # - [984] is the proper format constraint that has the same meaning as the hint [584] # - [983] is the complementary FC to [984] ("not [984]") error_message += " This is probably an error in the AHB itself." raise InvalidExpressionError(error_message) if composition == "or_composition": resulting_conditions_fulfilled = left.conditions_fulfilled | right.conditions_fulfilled elif composition == "xor_composition": resulting_conditions_fulfilled = left.conditions_fulfilled ^ right.conditions_fulfilled # resulting_conditions_fulfilled is never referenced before assignment when you obey the type hint # pylint:disable=possibly-used-before-assignment evaluated_composition = EvaluatedComposition(conditions_fulfilled=resulting_conditions_fulfilled) return evaluated_composition
[docs] def or_composition(self, left: ConditionNode, right: ConditionNode) -> EvaluatedComposition: """Evaluates logical (inclusive) or_composition""" evaluated_composition = self._or_xor_composition(left, right, "or_composition") evaluated_composition.format_constraints_expression = ( # todo: ask annika why the case of "invalid arguments" never happens here... FormatConstraintExpressionBuilder(left) # type: ignore[arg-type] .lor(right) # type: ignore[arg-type] .get_expression() ) evaluated_composition.hint = ( HintExpressionBuilder(getattr(left, "hint", None)).lor(getattr(right, "hint", None)).get_expression() ) return evaluated_composition
[docs] def xor_composition(self, left: ConditionNode, right: ConditionNode) -> EvaluatedComposition: """Evaluates exclusive xor_composition""" evaluated_composition = self._or_xor_composition(left, right, "xor_composition") evaluated_composition.format_constraints_expression = ( # todo: ask annika why the case of "invalid arguments" never happens here... FormatConstraintExpressionBuilder(left) # type: ignore[arg-type] .xor(right) # type: ignore[arg-type] .get_expression() ) evaluated_composition.hint = ( HintExpressionBuilder(getattr(left, "hint", None)).xor(getattr(right, "hint", None)).get_expression() ) return evaluated_composition
def _then_also( self, format_constraint: UnevaluatedFormatConstraint, other_condition: Type[ConditionNode], ) -> EvaluatedComposition: """ Evaluates a boolean condition with a format constraint. The functions name indicates its behaviour: If the `condition_fulfilled` attribute is true, the format constraint has to be fulfilled, too. But it is not directly evaluated but only attached to the evaluated composition such that we keep a strict separation of the Mussfeldprüfung itself and evaluation of format constraints on fields that have been marked obligatory by the (previously run) Mussfeldprüfung but do not affect the result of the Mussfeldprüfung. (Things that are obligatory, are obligatory regardless of the format constraints.) :param format_constraint: a format constraint :param other_condition: a requirement constraint or hint :return: """ if other_condition.conditions_fulfilled != ConditionFulfilledValue.NEUTRAL: evaluated_composition = EvaluatedComposition(conditions_fulfilled=other_condition.conditions_fulfilled) format_constraint_is_required = other_condition.conditions_fulfilled == ConditionFulfilledValue.FULFILLED elif isinstance(other_condition, Hint): evaluated_composition = EvaluatedComposition( conditions_fulfilled=ConditionFulfilledValue.NEUTRAL, hint=other_condition.hint ) format_constraint_is_required = True else: raise NotImplementedError( f"Combining Conditions of type {other_condition.__class__} with a " f"{format_constraint.__class__} is not implemented." ) if format_constraint_is_required: evaluated_composition.format_constraints_expression = ( FormatConstraintExpressionBuilder(format_constraint).land(other_condition).get_expression() ) return evaluated_composition
[docs] def then_also_composition(self, left: Type[ConditionNode], right: Type[ConditionNode]) -> EvaluatedComposition: """ A "then also" composition is typically used for format constraints. It connects an evaluable expression with a format constraint. If the `conditions_fulfilled` attribute evaluates to true, then also the attached format constraint has to be fulfilled. """ # pylint: disable=protected-access if isinstance(left, UnevaluatedFormatConstraint): return self._then_also(format_constraint=left, other_condition=right) return self._then_also( format_constraint=right, # type: ignore[arg-type] # #because typically format constraints are "attached" to the right side of an expression other_condition=left, ) # this might raise the NotImplementedError
[docs] def evaluate_requirement_constraint_tree( parsed_tree: Tree[Token], input_values: Mapping[str, TRCTransformerArgument] ) -> EvaluatedComposition: """ Evaluates the tree built from the expressions with the help of the ConditionsTransformer. :param parsed_tree: Tree :param input_values: dict(condition_key, ConditionNode) :return: EvaluatedComposition """ if not all(isinstance(input_value, ConditionNode) for input_value in input_values.values()): raise ValueError("Please make sure that the passed values are ConditionNodes.") if not all( isinstance(input_value, (RequirementConstraint, Hint, FormatConstraint)) for input_value in input_values.values() ): raise ValueError("""Please make sure that the passed values are ConditionNodes \ of the type RequirementConstraint, Hint or FormatConstraint.""") try: result = RequirementConstraintTransformer(input_values).transform(parsed_tree) except VisitError as visit_err: raise visit_err.orig_exc return result # type: ignore[no-any-return]
[docs] async def requirement_constraint_evaluation( condition_expression: Union[str, Tree[Token]], ahb_context: AhbContext, ) -> RequirementConstraintEvaluationResult: """ Evaluation of the condition expression in regard to the requirement conditions (rc). The condition expression can either be a string that still needs to be parsed as condition expression or a tree that has already been parsed. :param ahb_context: AhbContext providing all evaluators and data """ if isinstance(condition_expression, str): parsed_tree_rc: Tree[Token] = parse_condition_expression_to_tree(condition_expression) else: parsed_tree_rc = condition_expression # get all condition keys from tree all_condition_keys: list[str] = [t.value for t in parsed_tree_rc.scan_values(lambda v: isinstance(v, Token))] condition_node_builder = ConditionNodeBuilder(all_condition_keys, ahb_context=ahb_context) input_nodes = await condition_node_builder.requirement_content_evaluation_for_all_condition_keys() resulting_condition_node: EvaluatedComposition = evaluate_requirement_constraint_tree(parsed_tree_rc, input_nodes) requirement_constraints_fulfilled: Optional[bool] = ( resulting_condition_node.conditions_fulfilled == ConditionFulfilledValue.FULFILLED ) requirement_is_conditional: Optional[bool] = True if resulting_condition_node.conditions_fulfilled == ConditionFulfilledValue.NEUTRAL: # pylint:disable=no-member requirement_constraints_fulfilled = True requirement_is_conditional = False if resulting_condition_node.conditions_fulfilled == ConditionFulfilledValue.UNKNOWN: # pylint:disable=no-member # unknown_keys = [ # node_key # for node_key, node_value in input_nodes.items() # if isinstance(node_value, RequirementConstraint) # and node_value.conditions_fulfilled == ConditionFulfilledValue.UNKNOWN # ] # a NotImplementedError was raised here in ahbicht<=v0.5.11 # https://github.com/Hochfrequenz/ahbicht/issues/275 requirement_constraints_fulfilled = None requirement_is_conditional = None format_constraints_expression = getattr(resulting_condition_node, "format_constraints_expression", None) if isinstance(resulting_condition_node, UnevaluatedFormatConstraint): format_constraints_expression = f"[{resulting_condition_node.condition_key}]" hints = getattr(resulting_condition_node, "hint", None) requirement_constraint_evaluation_result = RequirementConstraintEvaluationResult( requirement_constraints_fulfilled=requirement_constraints_fulfilled, hints=hints, format_constraints_expression=format_constraints_expression, requirement_is_conditional=requirement_is_conditional, ) return requirement_constraint_evaluation_result