Beispiel #1
0
def _find_db(
    search_path,
    maxdepth=6,
    filename="collection.anki2",
    break_on_first=False,
    user: Optional[str] = None,
) -> DefaultDict[str, List[Path]]:
    """
    Like find_database but only for one search path at a time. Also doesn't
    raise any error, even if the search path doesn't exist.

    Args:
        search_path:
        maxdepth: Maximum depth relative to search_path
        filename:
        break_on_first: Break on first search result
        user: Only search for this user

    Returns:
        collection.defaultdict({user: [list of results]})
    """
    search_path = Path(search_path)
    if not search_path.exists():
        log.debug("_find_db: Search path %r does not exist.", str(search_path))
        return collections.defaultdict(list)
    if search_path.is_file():
        if search_path.name == filename:
            return collections.defaultdict(
                list, {search_path.parent.name: [search_path]}
            )
        else:
            log.warning(
                "_find_db: Search path %r is a file, but filename does not "
                "match that of %r.",
                str(search_path),
                filename,
            )
            return collections.defaultdict(list)
    found = collections.defaultdict(list)  # type: DefaultDict[str, List[Path]]
    for root, dirs, files in os.walk(str(search_path)):
        if filename in files:
            _user = os.path.basename(root)
            if user and not _user == user:
                continue
            found[_user].append(Path(root) / filename)
            if break_on_first:
                log.debug("_find_db: Breaking after first hit.")
                break
        depth = len(Path(root).relative_to(search_path).parts)
        if maxdepth and depth >= maxdepth:
            # log.debug(
            #     "_find_db: Abort search at %r. "
            #     "Max depth exceeded.",
            #     str(root)
            # )
            del dirs[:]
    return found
Beispiel #2
0
def _consolidate_tables(
    df: pd.DataFrame, df_old: pd.DataFrame, mode: str, id_column="id"
):

    if not list(df.columns) == list(df_old.columns):
        raise ValueError(
            "Columns do not match: Old: {}, New: {}".format(
                ", ".join(df_old.columns), ", ".join(df.columns)
            )
        )

    old_indices = set(df_old[id_column])
    new_indices = set(df[id_column])

    # Get indices
    # -----------

    if mode == "update":
        indices = set(old_indices)
    elif mode == "append":
        indices = set(new_indices) - set(old_indices)
        if not indices:
            log.warning(
                "Was told to append to table, but there do not seem to be any"
                " new entries. "
            )
    elif mode == "replace":
        indices = set(new_indices)
    else:
        raise ValueError(f"Unknown mode '{mode}'.")

    df = df[df[id_column].isin(indices)]

    # Apply
    # -----

    if mode == "update":
        df_new = df_old.copy()
        df_new.update(df)
    elif mode == "append":
        df_new = df_old.append(df, verify_integrity=True)
    elif mode == "replace":
        df_new = df.copy()
    else:
        raise ValueError(f"Unknown mode '{mode}'.")

    return df_new
Beispiel #3
0
def _find_db(
    search_path,
    maxdepth=6,
    filename="collection.anki2",
    break_on_first=False,
    user=None,
):
    """
    Like find_database but only for one search path at a time. Also doesn't
    raise any error, even if the search path doesn't exist.

    Returns:
        collection.defaultdict({user: [list of results]})
    """
    search_path = pathlib.Path(search_path)
    if not search_path.exists():
        log.debug("_find_db: Search path '{}' does not "
                  "exist.".format(str(search_path)))
        return collections.defaultdict(list)
    if search_path.is_file():
        if search_path.name == filename:
            return collections.defaultdict(
                list, {search_path.parent.name: [search_path]})
        else:
            log.warning(
                "_find_db: Search path '{}' is a file, but filename does not "
                "match that of '{}'.".format(str(search_path), filename))
            return collections.defaultdict(list)
    found = collections.defaultdict(list)
    for root, dirs, files in os.walk(str(search_path)):
        if filename in files:
            _user = os.path.basename(root)
            if user and not _user == user:
                continue
            found[_user].append(pathlib.Path(root) / filename)
            if break_on_first:
                log.debug("_find_db: Breaking after first hit.")
                break
        if maxdepth and root.count(os.sep) >= maxdepth:
            log.debug("_find_db: Abort search at '{}'. "
                      "Max depth exceeded.".format(str(root)))
            del dirs[:]
    return found
Beispiel #4
0
def find_db(
    search_paths=None,
    maxdepth=8,
    filename="collection.anki2",
    user=None,
    break_on_first=True,
) -> pathlib.Path:
    """
    Find path to anki2 database.

    Args:
        search_paths: Search path as string or pathlib object or list/iterable
            thereof. If None, some search paths are set by default.
        maxdepth: Maximal search depth.
        filename: Filename of the collection (default: ``collections.anki2``)
        user: Username to which the collection belongs. If None, search for
            databases of any user.
        break_on_first: Stop searching once a database is found. This is
            obviously faster, but you will not get any errors if there are
            multiple databases matching your criteria.

    Raises:
        If none ore more than one result is found: :class:`ValueError`

    Returns:
        pathlib.Path to the anki2 database
    """
    if not search_paths:
        log.info("Searching for database. This might take some time. "
                 "You can speed this up by specifying a search path or "
                 "directly entering the path to your database.")
        search_paths = [
            "~/.local/share/Anki2/",
            "~/Documents/Anki2",
            pathlib.Path(os.getenv("APPDATA", "~") + "/Anki2/"),
            "~/.local/share/Anki2",
            pathlib.Path.home(),
        ]
        search_paths = [
            pathlib.Path(sp).expanduser().resolve() for sp in search_paths
        ]
    if break_on_first:
        log.warning(
            "The search will stop at the first hit, so please verify that "
            "the result is correct (for example in case there might be more "
            "than one Anki installation)")
    if isinstance(search_paths, (str, pathlib.PurePath)):
        search_paths = [search_paths]
    found = {}
    for search_path in search_paths:
        found = {
            **found,
            **_find_db(
                search_path,
                maxdepth=maxdepth,
                filename=filename,
                user=user,
                break_on_first=break_on_first,
            ),
        }
        if break_on_first:
            if user is not None:
                if user in found:
                    break
            else:
                if found:
                    break

    if user:
        if user not in found:
            raise ValueError(
                f"Could not find database belonging to user {user}")
        found = found[user]
    else:
        if len(found) >= 2:
            raise ValueError(
                "Found databases for more than one user: {}. Please specify "
                "the user.".format(", ".join(found)))
        elif not found:
            raise ValueError(
                "No database found. You might increase the search depth or "
                "specify search paths to find more.")
        else:
            found = found.popitem()[1]
    if len(found) >= 2:
        raise ValueError(
            "Found more than one database belonging to user {} at {}".format(
                user, ", ".join(map(str, found))))
    found = found[0]
    log.debug("Database found at %r.", found)
    return found
Beispiel #5
0
    def write(
        self,
        modify=False,
        add=False,
        delete=False,
        backup_folder: Union[PurePath, str] = None,
    ):
        """ Creates a backup of the database and then writes back the new
        data.

        .. danger::

            The switches ``modify``, ``add`` and ``delete`` will run additional
            cross-checks, but do not rely on them to 100%!

        .. warning::

            It is recommended to run :meth:`summarize_changes` before to check
            whether the changes match your expectation.

        .. note::

            Please make sure to thoroughly check your collection in Anki after
            every write process!

        Args:
            modify: Allow modification of existing items (notes, cards, etc.)
            add: Allow adding of new items (notes, cards, etc.)
            delete: Allow deletion of items (notes, cards, etc.)
            backup_folder: Path to backup folder. If None is given, the backup
                is created in the Anki backup directory (if found).

        Returns:
            None
        """
        if not modify and not add and not delete:
            log.warning(
                "Please set modify=True, add=True or delete=True, you're"
                " literally not allowing me any modification at all."
            )
            return None

        try:
            prepared = self._prepare_write_data(
                modify=modify, add=add, delete=delete
            )

            info = self._get_and_update_info()
        except Exception as e:
            log.critical(
                "Something went wrong preparing the data for writing. "
                "However, no data has been written out, so your"
                "database is save!"
            )
            raise e
        else:
            log.debug("Successfully prepared data for writing.")

        backup_path = ankipandas.paths.backup_db(
            self.path, backup_folder=backup_folder
        )
        log.info("Backup created at {}.".format(backup_path.resolve()))

        # Actually setting values here, after all conversion tasks have been
        # carried out. That way if any of them fails, we don't have a
        # partially written collection.
        log.debug("Now actually writing to database.")
        try:
            for table, values in prepared.items():
                raw.set_table(
                    self.db, values["raw"], table=table, mode=values["mode"]
                )
            # Actually only needed if we actually modify the info.
            # This will trigger a complete re-upload, so we want to avoid this
            # if possible.
            # raw.set_info(self.db, info)
        except Exception as e:
            log.critical(
                "Error while writing data to database at {path}"
                "This means that your database might have become corrupted. "
                "It's STRONGLY adviced that you manually restore the database "
                "by replacing it with the backup from {backup_path} and restart"
                " from scratch. "
                "Please also open a bug report at "
                "https://github.com/klieret/AnkiPandas/issues/, as errors "
                "during the actual writing process should never occurr!".format(
                    path=self.path.resolve(), backup_path=backup_path.resolve()
                )
            )
            raise e
Beispiel #6
0
 def test_log(self):
     log.info("Test info")
     log.warning("Test warning")
Beispiel #7
0
    def write(
        self,
        modify=False,
        add=False,
        delete=False,
        backup_folder: Union[PurePath, str] = None,
    ):
        """ Creates a backup of the database and then writes back the new
        data.

        .. danger::

            The switches ``modify``, ``add`` and ``delete`` will run additional
            cross-checks, but do not rely on them to 100%!

        .. warning::

            It is recommended to run :meth:`summarize_changes` before to check
            whether the changes match your expectation.

        .. note::

            Please make sure to thoroughly check your collection in Anki after
            every write process!

        Args:
            modify: Allow modification of existing items (notes, cards, etc.)
            add: Allow adding of new items (notes, cards, etc.)
            delete: Allow deletion of items (notes, cards, etc.)
            backup_folder: Path to backup folder. If None is given, the backup
                is created in the Anki backup directory (if found).

        Returns:
            None
        """
        if not modify and not add and not delete:
            log.warning(
                "Please set modify=True, add=True or delete=True, you're"
                " literally not allowing me any modification at all.")
            return None

        try:
            prepared = self._prepare_write_data(modify=modify,
                                                add=add,
                                                delete=delete)
            log.debug("Now getting & updating info.")
            info = self._get_and_update_info()
        except Exception as e:
            log.critical(
                "Something went wrong preparing the data for writing. "
                "However, no data has been written out, so your "
                "database is save!")
            raise e
        else:
            log.debug("Successfully prepared data for writing.")

        if prepared == {}:
            log.warning(
                "Nothing seems to have been changed. Will not do anything!")
            return None

        backup_path = ankipandas.paths.backup_db(self.path,
                                                 backup_folder=backup_folder)
        log.info(f"Backup created at {backup_path.resolve()}.")
        log.warning(
            "Currently AnkiPandas might not be able to tell Anki to"
            " sync its database. "
            "You might have to manually tell Anki to sync everything "
            "to AnkiDroid.\n"
            "Furthermore, if you run into issues with tag searches not working"
            "anymore, please first do Notes > Clear unused notes and then "
            "Tools > Check Database (from the main menu). This should get them"
            " to work (sorry about this issue).")

        # Actually setting values here, after all conversion tasks have been
        # carried out. That way if any of them fails, we don't have a
        # partially written collection.
        log.debug("Now actually writing to database.")
        try:
            for table, values in prepared.items():
                log.debug(f"Now setting table {table}.")
                raw.set_table(self.db,
                              values["raw"],
                              table=table,
                              mode=values["mode"])
                log.debug(f"Setting table {table} successful.")
            # log.debug("Now setting info")
            # raw.set_info(self.db, info)
            # log.debug("Setting info successful.")
        except Exception as e:
            log.critical(
                "Error while writing data to database at {path}"
                "This means that your database might have become corrupted. "
                "It's STRONGLY advised that you manually restore the database "
                "by replacing it with the backup from {backup_path} and restart"
                " from scratch. "
                "Please also open a bug report at "
                "https://github.com/klieret/AnkiPandas/issues/, as errors "
                "during the actual writing process should never occur!".format(
                    path=self.path.resolve(),
                    backup_path=backup_path.resolve()))
            raise e