Exemplo n.º 1
0
    def build_graph(self, data: Dict[str, Any]) -> None:
        """Adds a process and its in-edges to the graph.

        :param data: A dictionary containing all the data from a CBR
        :type data: dict[str, Any]
        """
        self.clear()
        self.g.graph["name"] = data["info"]["name"]

        for k, v in data["ingredients"].items():
            self.add_ingredient(k, v)

        for k, v in data["appliances"].items():
            self.add_appliance(k, v)

        for k, v in data["preparation"].items():
            self.add_process(k, v)

        self.resolve_pending_processes_edges()

        for in_foodstuff, out_process in self._pending_processes_edges:
            self.g.add_node(in_foodstuff, type="unref_foodstuff")
            self.g.add_edge(in_foodstuff, out_process)
            logger.error("Neither ingredient nor process found with reference "
                         f"'{in_foodstuff}'")
Exemplo n.º 2
0
    def ingredients_used_exactly_once(graph: CBRGraph) -> AppliedRuleResult:
        """Checks that every :ref:`CBR Ingredient <cbr-ingredients>` in a given
        :doc:`CBRGraph <cbrg>` is directly used by a :ref:`CBR Process
        <cbr-preparation>` exactly once.

        The following messages will be logged if the corresponding problems are found:

        - *Error*: A given :ref:`CBR Ingredient <cbr-ingredients>` is used more than
          once in the :code:`preparation` section of the analyzed :ref:`CBR <cbr>`.
        - *Warning*: A given :ref:`CBR Ingredient <cbr-ingredients>` is not used in the
          :code:`preparation` section of the analyzed :ref:`CBR <cbr>`.

        :param graph: The :doc:`CBRGraph <cbrg>` generated from the :ref:`CBR <cbr>` to
          be validated
        :type graph: cookbase.graph.cbrgraph.CBRGraph
        :return: An :class:`AppliedRuleResult` object containing the errors and warnings
          registered during rule application
        :rtype: AppliedRuleResult
        """
        result = AppliedRuleResult()

        for i in graph.get_ingredients():
            r = graph.g.out_degree(i)

            if r == 0:
                w = f"Ingredient '{i}' is not used during preparation"
                result.warnings.append(w)
                logger.warning(w)
            elif r > 1:
                e = f"Ingredient '{i}' is used more than once during preparation"
                result.errors.append(e)
                logger.error(e)

        return result
Exemplo n.º 3
0
    def ingredients_are_valid(ingredients: Dict[str, Any]) -> AppliedRuleResult:
        """Checks whether the :ref:`CBR Ingredients <cbr-ingredients>` present in a
        :ref:`CBR <cbr>` are correct and their respective :ref:`CBIs <cbi>` exist in the
        database.

        The following messages will be logged if the corresponding problems are found:

        - *Error*: A given :ref:`CBI <cbi>` is not found in the database by its
          identifier.
        - *Warning*: A given :ref:`CBR Ingredient <cbr-ingredients>`'s name does not
          match any name available from the its referred :ref:`CBI <cbi>` definition.

        :param ingredients: The dictionary containing the :code:`ingredients` property
          from the :ref:`CBR <cbr>` to be validated, which holds a set of :ref:`CBR
          Ingredients <cbr-ingredients>`
        :type ingredients: dict[str, Any]
        :return: An :class:`AppliedRuleResult` object containing the errors and warnings
          registered during rule application
        :rtype: AppliedRuleResult
        """
        result = AppliedRuleResult()
        db_handler = handler.get_handler()

        for i in ingredients.values():
            cbi = db_handler.get_cbi(i["cbiId"])
            if cbi is None:
                e = f'CBI with id {i["cbiId"]} does not exist in database'
                result.errors.append(e)
                logger.error(e)
            else:
                cbi_names = cbi["name"][i["name"]["language"]]

                if (
                    isinstance(cbi_names, list) and i["name"]["text"] not in cbi_names
                ) or (isinstance(cbi_names, str) and i["name"]["text"] != cbi_names):
                    w = (
                        f'Ingredient name {i["name"]["text"]} does not match any '
                        f'available name for CBI {i["cbiId"]}'
                    )
                    result.warnings.append(w)
                    logger.warning(w)

        return result
Exemplo n.º 4
0
    def processes_are_valid(processes: Dict[str, Any]) -> AppliedRuleResult:
        """Checks whether the :ref:`CBR Processes <cbr-preparation>` present in a
        :ref:`CBR <cbr>` are correct and their respective :ref:`CBPs <cbp>` exist in the
        database.

        The following messages will be logged if the corresponding problems are found:

        - *Error*: A given :ref:`CBP <cbp>` is not found in the database by its
          identifier.
        - Messages from the :meth:`process_is_valid` calls.

        .. note::
           Do not call if
           :meth:`processes_and_appliances_are_valid_and_processes_requirements_met` is
           executed, as this test will be redundant.

        :param processes: The dictionary containing the :code:`preparation` property
          from the :ref:`CBR <cbr>` to be validated, which holds a set of :ref:`CBR
          Processes <cbr-preparation>`
        :type processes: dict[str, Any]
        :return: An :class:`AppliedRuleResult` object containing the errors and warnings
          registered during rule application
        :rtype: AppliedRuleResult
        """
        result = AppliedRuleResult
        db_handler = handler.get_handler()

        for i in processes.values():
            cbp = db_handler.get_cbp(i["cbpId"])

            if cbp is None:
                e = f'CBP with id {i["cbpId"]} does not exist in database'
                result.errors.append(e)
                logger.error(e)
            else:
                partial_result = Semantics.process_is_valid(i, cbp)
                result.include_result(partial_result)

        return result
Exemplo n.º 5
0
    def single_final_process(graph: CBRGraph) -> AppliedRuleResult:
        """Checks if there is only one :ref:`CBR Process <cbr-preparation>` in a given
        :doc:`CBRGraph <cbrg>` acting as the end process.

        The following messages will be logged if the corresponding problems are found:

        - *Error*: There are more than one ending :ref:`CBR Process <cbr-preparation>`
          in the analyzed :ref:`CBR <cbr>`.

        :param graph: The :doc:`CBRGraph <cbrg>` generated from the :ref:`CBR <cbr>` to
          be validated
        :type graph: cookbase.graph.cbrgraph.CBRGraph
        :return: An :class:`AppliedRuleResult` object containing the errors and warnings
          registered during rule application
        :rtype: AppliedRuleResult
        """
        result = AppliedRuleResult()

        if len(graph.get_leaf_processes()) > 1:
            e = f"There are more than one ending processes in the recipe"
            result.errors.append(e)
            logger.error(e)

        return result
Exemplo n.º 6
0
    def validate(self,
                 cbr: Dict[str, Any],
                 store: bool = False,
                 strict: bool = True) -> ValidationResult:
        """Main function of the class, it performs the validation of a :ref:`CBR <cbr>`
        and builds the :doc:`CBRGraph <cbrg>`.

        The validation process is implemented in two stages: firstly, a JSON Schema
        validation is performed, and, secondly, validating rules are sequentially
        applied to ensure that the recipe document satisfies the :ref:`CBR <cbr>`
        definition.

        :param cbr: The :ref:`CBR <cbr>` to be validated
        :type cbr: dict[str, Any]
        :param store: A flag indicating whether the validated CBR and :doc:`CBRGraph
          <cbrg>` should be stored in database, defaults to :const:`False`
        :type store: bool, optional
        :param strict: A flag indicating the validation policy, defaults to
          :const:`True`
        :type strict: bool, optional
        :return: The results from applying the set of validation rules
        :rtype: ValidationResult
        """
        try:
            jsonschema.validate(cbr, self.schema)
        except jsonschema.exceptions.SchemaError as e:
            logger.error("Invalid CBR Schema: " + e.message)
            return ValidationResult(schema_validated=False)
        except jsonschema.exceptions.ValidationError as e:
            logger.error("CBR does not satisfy CBR Schema: " + e.message)
            return ValidationResult(schema_validated=False)

        result = self.apply_validation_rules(cbr)

        if not result.is_valid(strict):
            logger.error("CBR does not satisfy CBR validation rules")
        elif store:
            try:
                result.storing_result = self._store(cbr, result.cbrgraph)
            except (CBRInsertionError, CBRGraphInsertionError) as e:
                logger.error(e)
                result.storing_result = e.partial_result

        return result
Exemplo n.º 7
0
    def processes_and_appliances_are_valid_and_processes_requirements_met(
        appliances: Dict[str, Any], processes: Dict[str, Any]
    ) -> AppliedRuleResult:
        """Checks correctness and consistency on the :ref:`CBR Appliances
        <cbr-appliances>` and :ref:`CBR Processes <cbr-preparation>` present in a
        :ref:`CBR <cbr>`.

        The function checks if:

        1. All :ref:`CBR Appliances <cbr-appliances>` and :ref:`CBR Processes
           <cbr-preparation>` are correct and their respective :ref:`CBAs <cba>` and
           :ref:`CBPs <cbp>` exist in the database.
        2. All :ref:`CBR Processes <cbr-preparation>` find there appliance requirements
           met.

        The following messages will be logged if the corresponding problems are found:

        - *Error*: A given :ref:`CBA <cba>` is not found in the database by its
          identifier.
        - *Error*: A given :ref:`CBP <cbp>` is not found in the database by its
          identifier.
        - *Error*: The appliance requirements of a given :ref:`CBR Process
          <cbr-preparation>` are not met.
        - Messages from the :meth:`appliance_is_valid` and :meth:`process_is_valid`
          calls.

        :param appliances: The dictionary containing the :code:`appliances` property
          from the :ref:`CBR <cbr>` to be validated, which holds a set of :ref:`CBR
          Appliances <cbr-appliances>`
        :type appliances: dict[str, Any]
        :param processes: The dictionary containing the :code:`preparation` property
          from the :ref:`CBR <cbr>` to be validated, which holds a set of :ref:`CBR
          Processes <cbr-preparation>`
        :type processes: dict[str, Any]
        :return: An :class:`AppliedRuleResult` object containing the errors and warnings
          registered during rule application
        :rtype: AppliedRuleResult
        """
        result = AppliedRuleResult()
        db_handler = handler.get_handler()

        for process_reference, p in processes.items():
            # Checking CBP validity
            cbp = db_handler.get_cbp(p["cbpId"])

            if cbp is None:
                e = f'CBP with id {p["cbpId"]} does not exist in database'
                result.errors.append(e)
                logger.error(e)
            else:
                partial_result = Semantics.process_is_valid(p, cbp)
                result.include_result(partial_result)

            # Checking CBAs validity
            cbas = []

            for a in p["appliances"]:
                if "cbaId" in appliances[a["appliance"]]:
                    cba = db_handler.get_cba(appliances[a["appliance"]]["cbaId"])

                    if cba is None:
                        e = (
                            f'CBA with id {appliances[a["appliance"]]["cbaId"]} does '
                            f"not exist in database"
                        )
                        result.errors.append(e)
                        logger.error(e)
                        continue
                    else:
                        partial_result = Semantics.appliance_is_valid(
                            appliances[a["appliance"]], cba
                        )
                        result.include_result(partial_result)
                else:
                    cba = {
                        "id": None,
                        "info": {
                            "familyLevel": 0,
                            "functions": appliances[a["appliance"]]["functions"],
                        },
                    }

                cbas.append(cba)

            # Checking whether process requirements are met
            partial_result = Semantics.cbas_satisfy_cbp(cbas, cbp)
            result.include_result(partial_result)

        return result
Exemplo n.º 8
0
    def cbas_satisfy_cbp(
        cbas: List[Dict[str, Any]], cbp: Dict[str, Any]
    ) -> AppliedRuleResult:
        """Checks if a set of :ref:`CBAs <cba>` satisfy at least one of the condition
        clauses provided by the :code:`data.validation.conditions.requiredAppliances`
        property of a given :ref:`CBP <cbp>`.

        The provided :ref:`CBAs <cba>` are assumed to exist in the database.

        :param cbas: A list containing the :ref:`CBAs <cba>` to be verified
        :type cbas: list[dict[str, Any]]
        :param cbp: The dictionary containing the :ref:`CBP <cbp>` whose conditions
          clauses are to be checked for satisfaction
        :type cbp: dict[str, Any]
        :return: An :class:`AppliedRuleResult` object containing the errors and warnings
          registered during rule application
        :rtype: AppliedRuleResult
        """
        from cookbase.validation.cba import unroll

        unrolled_cbas = [unroll(cba) for cba in cbas]

        for clause in cbp["info"]["validation"]["conditions"]["requiredAppliances"]:
            unsatisfied_clause = False
            satisfied_literals = [False] * len(clause)
            satisfying_cbas = [False] * len(unrolled_cbas)

            # First iteration searching for exact cbaIds
            # in order to minimize the search space
            for i in range(len(clause)):
                literal = clause[i]
                if "cbaId" in literal:
                    for j in range(len(unrolled_cbas)):
                        if satisfying_cbas[j]:
                            continue

                        if (
                            isinstance(unrolled_cbas[j]["id"], int)
                            and unrolled_cbas[j]["id"] == literal["cbaId"]
                        ) or (
                            isinstance(unrolled_cbas[j]["id"], list)
                            and literal["cbaId"] in unrolled_cbas[j]["id"]
                        ):
                            satisfied_literals[i] = True
                            satisfying_cbas[j] = True
                            break

                    if not satisfied_literals[i]:
                        unsatisfied_clause = True
                        break

            if unsatisfied_clause:
                continue

            # Second iteration searching for functions

            # TODO: search combinatorial space exhaustively
            ###############################################
            # This implementation may throw false negatives
            # as it associates a CBA to a CBP literal function
            # sequentially, not considering any rearrangement
            # that may lead us to a solution.
            ############################
            for i in range(len(clause)):
                if satisfied_literals[i]:
                    continue

                literal = clause[i]
                if "function" in literal:
                    for j in range(len(unrolled_cbas)):
                        if satisfying_cbas[j]:
                            continue

                        if literal["function"] in unrolled_cbas[j]["info"]["functions"]:
                            satisfied_literals[i] = True
                            satisfying_cbas[j] = True
                            break

                    if not satisfied_literals[i]:
                        unsatisfied_clause = True
                        break

            if not unsatisfied_clause:
                return AppliedRuleResult()

        e = f'Appliance requirements of CBP {cbp["id"]} are not satisfied'
        logger.error(e)
        return AppliedRuleResult(errors=[e])
Exemplo n.º 9
0
    def foodstuff_and_appliance_references_are_consistent(
        ingredients: Dict[str, Any],
        appliances: Dict[str, Any],
        processes: Dict[str, Any],
    ) -> AppliedRuleResult:
        """Checks for the consistency of a :ref:`CBR <cbr>` on the scope of its
        :ref:`CBR Ingredient <cbr-ingredients>`, :ref:`CBR Appliance <cbr-appliances>`
        and :ref:`CBR Process <cbr-preparation>` references.

        The function checks if:

        1. All foodstuff and appliance references appearing in the :ref:`CBR Processes
           <cbr-preparation>` exist in the context of the given :ref:`CBR <cbr>` (which
           may either be references to :ref:`CBR Ingredients <cbr-ingredients>`,
           :ref:`CBR Appliances <cbr-appliances>` or :ref:`CBR Processes
           <cbr-preparation>`, as explained in the :doc:`Cookbase Data Model (CBDM)
           documentation <cbdm>` on the :code:`preparation` section of a :ref:`CBR
           <cbr>`).
        2. There are no unreferenced :ref:`CBR Ingredients <cbr-ingredients>` or
           :ref:`CBR Appliances <cbr-appliances>` in the given :ref:`CBR <cbr>`.

        The following messages will be logged if the corresponding problems are found:

        - *Error*: There is no :ref:`CBR Ingredient <cbr-ingredients>` nor :ref:`CBR
          Process <cbr-preparation>` matching a foodstuff reference from a :ref:`CBR
          Process <cbr-preparation>` in the given :ref:`CBR <cbr>`.
        - *Error*: There is no :ref:`CBR Appliance <cbr-appliances>` matching an
          appliance reference from a :ref:`CBR Process <cbr-preparation>` in the given
          :ref:`CBR <cbr>`.
        - *Warning*: A :ref:`CBR Ingredient <cbr-ingredients>` is not referenced by any
          :ref:`CBR Process <cbr-preparation>` in the given :ref:`CBR <cbr>`.
        - *Warning*: A :ref:`CBR Appliance <cbr-appliances>` is not referenced by any
          :ref:`CBR Process <cbr-preparation>` in the given :ref:`CBR <cbr>`.

        :param ingredients: The dictionary containing the :code:`ingredients` property
          from the :ref:`CBR <cbr>` to be validated, which holds a set of :ref:`CBR
          Ingredients <cbr-ingredients>`
        :type ingredients: dict[str, Any]
        :param appliances: The dictionary containing the :code:`appliances` property
          from the :ref:`CBR <cbr>` to be validated, which holds a set of :ref:`CBR
          Appliances <cbr-appliances>`
        :type ingredients: dict[str, Any]
        :param processes: The dictionary containing the :code:`preparation` property
          from the :ref:`CBR <cbr>` to be validated, which holds a set of :ref:`CBR
          Processes <cbr-preparation>`
        :type processes: dict[str, Any]
        :return: An :class:`AppliedRuleResult` object containing the errors and warnings
          registered during rule application
        :rtype: AppliedRuleResult
        """
        result = AppliedRuleResult()
        used_ingredients = set()
        used_appliances = set()

        for i in processes.values():
            # Checking foodstuffs references
            for j in [v for v in Definitions.foodstuff_keywords if v in i.keys()]:
                r = i[j]

                if isinstance(r, str):
                    if r not in ingredients.keys():
                        if r not in processes.keys():
                            e = (
                                f"Foodstuff reference '{r}' appears neither in "
                                f"'ingredients' nor in 'preparation' section"
                            )
                            result.errors.append(e)
                            logger.error(e)
                    else:
                        used_ingredients.add(r)
                else:
                    for q in r:
                        if q not in ingredients.keys():
                            if q not in processes.keys():
                                e = (
                                    f"Foodstuff reference '{q}' appears neither in "
                                    f"'ingredients' nor in 'preparation' section"
                                )
                        else:
                            used_ingredients.add(q)

            # Checking appliances references
            for a in i["appliances"]:
                if a["appliance"] not in appliances.keys():
                    e = (
                        f'Appliance reference \'{a["appliance"]}\' does not appear in '
                        f"'appliances' section"
                    )
                    result.errors.append(e)
                    logger.error(e)
                else:
                    used_appliances.add(a["appliance"])

        # Checking unused ingredients
        diff = ingredients.keys() - used_ingredients

        for d in diff:
            w = f"Ingredient '{d}' is not used in 'preparation' section"
            result.warnings.append(w)
            logger.warning(w)

        # Checking unused appliances
        diff = appliances.keys() - used_appliances

        for d in diff:
            w = f"Appliance '{d}' is not used in 'preparation' section"
            result.warnings.append(w)
            logger.warning(w)

        return result