Esempio n. 1
0
def backup_db(
    db_path: Union[str, pathlib.PurePath],
    backup_folder: Union[str, pathlib.PurePath] = None,
) -> pathlib.Path:
    """
    Back up database file.

    Args:
        db_path: Path to database
        backup_folder: Path to backup folder. If None is given, the backup is
            created in the Anki backup directory.

    Returns:
        Path to newly created backup file as :class:`pathlib.Path`.
    """
    db_path = pathlib.Path(db_path)
    if backup_folder:
        backup_folder = pathlib.Path(backup_folder)
        if not backup_folder.is_dir():
            log.debug("Creating backup directory %s.", backup_folder)
            backup_folder.mkdir(parents=True)
    else:
        backup_folder = get_anki_backup_folder(db_path, nexist="raise")
    if not db_path.is_file():
        raise FileNotFoundError("Database does not seem to exist.")
    backup_path = backup_folder / db_backup_file_name()
    shutil.copy2(str(db_path), str(backup_path))
    return backup_path
Esempio n. 2
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
Esempio n. 3
0
def _sync_metadata(df_ret: pd.DataFrame, df_old: pd.DataFrame) -> None:
    """
    If the df_old has a `_metadata` field, containing a list of attribute
    names that contain metadata, then this is copied from `df_old` to the new
    dataframe `df_ret.

    Args:
        df_ret:
        df_old:

    Returns:
        None
    """
    if hasattr(df_old, "_metadata"):
        for key in df_old._metadata:
            value = getattr(df_old, key)
            log.debug("Setting metadata attribute %s to %s", key, value)
            setattr(df_ret, key, value)
Esempio n. 4
0
    def _prepare_write_data(
        self, modify=False, add=False, delete=False
    ) -> Dict[str, Any]:
        prepared = {}
        for key, value in self.__items.items():
            if value is None:
                log.debug("Write: Skipping {}, because it's None.".format(key))
                continue
            if key in ["notes", "cards", "revs"]:
                if not delete:
                    ndeleted = len(value.was_deleted())
                    if ndeleted:
                        raise ValueError(
                            "You specified delete=False, but {} rows of item "
                            "{} would be deleted.".format(ndeleted, key)
                        )
                if not modify:
                    nmodified = sum(value.was_modified(na=False))
                    if nmodified:
                        raise ValueError(
                            "You specified modify=False, but {} rows of item "
                            "{} would be modified.".format(nmodified, key)
                        )
                if not add:
                    nadded = sum(value.was_added())
                    if nadded:
                        raise ValueError(
                            "You specified add=False, but {} rows of item "
                            "{} would be modified.".format(nadded, key)
                        )

                mode = "replace"
                if modify and not add and not delete:
                    mode = "update"
                if add and not modify and not delete:
                    mode = "append"
                value._check_table_integrity()
                raw_table = value.raw()
                prepared[key] = {"raw": raw_table, "mode": mode}

        return prepared
Esempio n. 5
0
def db_path_input(path: Union[str, PurePath] = None, user: str = None) -> Path:
    """Helper function to interpret user input of path to database.

    1. If no path is given, we search through some default locations
    2. If path points to a file: Take that file
    3. If path points to a directory: Search in that directory

    Args:
        path: Path to database or search path or None
        user: User name of anki collection or None

    Returns:
        Path to anki database as :class:`Path` object

    Raises:
        If path does not exist: :class:`FileNotFoundError`
        In various other cases: :class:`ValueError`
    """
    if path is None:
        result = find_db(user=user)
    else:
        path = Path(path)
        if not path.exists():
            raise FileNotFoundError(
                "db_path_input: File '{}' does not exist.".format(str(path))
            )
        if path.is_file():
            log.debug(
                "db_path_input: Database explicitly set to %r.", str(path)
            )
            result = path
        else:
            result = find_db(
                search_paths=(path,), user=user, break_on_first=False
            )
            log.info("Database found at %r.", str(result))
    if result:
        return result
    else:
        raise ValueError("Database could not be found.")
Esempio n. 6
0
    def _prepare_write_data(self,
                            modify=False,
                            add=False,
                            delete=False) -> Dict[str, Any]:
        prepared = {}
        for key, value in self.__items.items():
            if value is None:
                log.debug("Write: Skipping %s, because it's None.", key)
                continue
            if key in ["notes", "cards", "revs"]:
                ndeleted = len(value.was_deleted())
                nmodified = sum(value.was_modified(na=False))
                nadded = sum(value.was_added())

                if not delete and ndeleted:
                    raise ValueError(
                        "You specified delete=False, but {} rows of item "
                        "{} would be deleted.".format(ndeleted, key))
                if not modify and nmodified:
                    raise ValueError(
                        "You specified modify=False, but {} rows of item "
                        "{} would be modified.".format(nmodified, key))
                if not add and nadded:
                    raise ValueError(
                        "You specified add=False, but {} rows of item "
                        "{} would be modified.".format(nadded, key))

                if not ndeleted and not nmodified and not nadded:
                    log.debug(
                        "Skipping table %s for writing, because nothing "
                        "seemed to have changed",
                        key,
                    )
                    continue

                mode = "replace"
                if modify and not add and not delete:
                    mode = "update"
                if add and not modify and not delete:
                    mode = "append"
                log.debug("Will update table %s with mode %s", key, mode)
                value.check_table_integrity()
                raw_table = value.raw()
                prepared[key] = {"raw": raw_table, "mode": mode}

        return prepared
Esempio n. 7
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
Esempio n. 8
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
Esempio n. 9
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
Esempio n. 10
0
 def __del__(self):
     log.debug("Closing db {db} which was loaded from {path}.".format(
         db=self.db, path=self.path))
     raw.close_db(self.db)
     log.debug("Closing successful")
Esempio n. 11
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
Esempio n. 12
0
 def db(self) -> sqlite3.Connection:
     """Opened Anki database. Make sure to call `db.close()` after you're
     done. Better still, use `contextlib.closing`.
     """
     log.debug(f"Opening Db from {self._path}")
     return raw.load_db(self._path)
Esempio n. 13
0
 def __del__(self):
     log.debug("Closing db %s which was loaded from %s.", self.db,
               self.path)
     raw.close_db(self.db)
     log.debug("Closing successful")