<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def preprocess( self, nb: "NotebookNode", resources: dict ) -> Tuple["NotebookNode", dict]: """Preprocess the entire notebook."""

        if "remove_solution" not in resources:
            raise KeyError("The resources dictionary must have a remove_solution key.")
        if resources["remove_solution"]:
            keep_cells_idx = []
            for index, cell in enumerate(nb.cells):
                if "## solution" in cell.source.lower():
                    keep_cells_idx.append(index)
                # The space at the end of the test string here is important
                elif len(keep_cells_idx) > 0 and cell.source.startswith("### "):
                    keep_cells_idx.append(index)

            keep_cells = nb.cells[: keep_cells_idx[0] + 1]
            for i in keep_cells_idx[1:]:
                keep_cells.append(nb.cells[i])
                if resources["by_hand"]:
                    keep_cells.append(by_hand_cell)
                else:
                    if "sketch" in nb.cells[i].source.lower():
                        keep_cells.append(sketch_cell)
                    else:
                        keep_cells.append(md_expl_cell)
                        keep_cells.append(code_ans_cell)
                        keep_cells.append(md_ans_cell)
            nb.cells = keep_cells

        return nb, resources