Skip to content

Commit

Permalink
2.3.0 (#54)
Browse files Browse the repository at this point in the history
* Replaces `os` with `pathlib.Path` for disk level actions.
* Adds `enums.TargetOS`
* Bump testing and development to 3.10
* Add development requirements to `pyproject.toml`
* Replaces `re.match` with `re.search` for `fastresume` discovery when using Regex patterns 
* Adds emojis and cleans up INFO and DEBUG log outputs.
* Adds `-v` and `--version` for version print.
  • Loading branch information
jslay88 authored May 2, 2022
1 parent a5d8dda commit a57b15d
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 107 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: actions/[email protected]
if: steps.filter.outputs.python == 'true'
with:
python-version: 3.9
python-version: "3.10"

- name: Install Tox
if: steps.filter.outputs.python == 'true'
Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:
uses: actions/[email protected]
if: steps.filter.outputs.python == 'true'
with:
python-version: 3.9
python-version: "3.10"

- name: Install Coverage & diff_cover
if: steps.filter.outputs.python == 'true'
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:
uses: actions/[email protected]
if: steps.filter.outputs.python == 'true'
with:
python-version: 3.9
python-version: "3.10"

- name: Install Flit
if: steps.filter.outputs.python == 'true'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/[email protected]
with:
python-version: 3.9
python-version: "3.10"

- name: Install Flit
run: |
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,23 @@ Install from PyPi using `pip`, or jump to [Examples](#Examples) for Docker

Run the script and follow prompts or use CLI arguments with command `qbt_migrate`

usage: qbt_migrate [-h] [-e EXISTING_PATH] [-n NEW_PATH] [-r] [-t {Windows,Linux,Mac}] [-b BT_BACKUP_PATH] [-s] [-l {DEBUG,INFO}]
usage: qbt_migrate [-h] [-e EXISTING_PATH] [-n NEW_PATH] [-r] [-t {Windows,Linux,Mac}] [-b BT_BACKUP_PATH] [-s] [-l {DEBUG,INFO}] [-v]

optional arguments:
options:
-h, --help show this help message and exit
-e EXISTING_PATH, --existing-path EXISTING_PATH
Existing root of path to look for.
-n NEW_PATH, --new-path NEW_PATH
New root path to replace existing root path with.
-r, --regex Existing and New paths are regex patterns with capture groups.
-r, --regex Existing and New paths are regex patterns. (Capture groups recommended).
-t {Windows,Linux,Mac}, --target-os {Windows,Linux,Mac}
Target OS (converts slashes). Default will auto-detect if conversion is needed based on existing vs new.
-b BT_BACKUP_PATH, --bt-backup-path BT_BACKUP_PATH
BT_backup Path Override.
-s, --skip-bad-files Skips bad .fastresume files instead of exiting. Default behavior is to exit.
-l {DEBUG,INFO}, --log-level {DEBUG,INFO}
Log Level, Default is INFO.
-v, --version Prints the current version number and exits.

By default, everything happens in the BT_backup directory defined by the OS the script is running on.
Override `BT_backup` path if needed.
Expand Down
12 changes: 10 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@ dynamic = [
]

[project.optional-dependencies]
test = [
dev = [
"black",
"coverage",
"diff-cover",
"flake8",
"isort",
"pre-commit",
"pytest>=2.7.3",
"tox"
]
test = [
"tox",
]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion qbt_migrate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
from .methods import convert_slashes, discover_bt_backup_path


__version__ = "2.2.1" + os.getenv("VERSION_TAG", "")
__version__ = "2.3.0" + os.getenv("VERSION_TAG", "")

logging.getLogger(__name__).addHandler(logging.NullHandler())
78 changes: 45 additions & 33 deletions qbt_migrate/classes.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging
import os
import re
from datetime import datetime
from pathlib import Path
from threading import Thread
from typing import Optional
from typing import Optional, Union

import bencodepy
from bencodepy.exceptions import BencodeDecodeError

from .enums import TargetOS
from .methods import backup_folder, convert_slashes, discover_bt_backup_path


Expand All @@ -22,15 +23,15 @@ def __init__(self, bt_backup_path: str = None):
if bt_backup_path is None:
bt_backup_path = discover_bt_backup_path()
self.logger.debug(f"BT_backup Path: {bt_backup_path}")
self.bt_backup_path = bt_backup_path
self.bt_backup_path = Path(bt_backup_path)
self.discovered_files = set()

def run(
self,
existing_path: str,
new_path: str,
regex_path: bool = False,
target_os: Optional[str] = None,
target_os: Optional[TargetOS] = None,
create_backup: bool = True,
skip_bad_files: bool = False,
):
Expand All @@ -42,37 +43,40 @@ def run(
:type new_path: str
:param regex_path: Existing and New Paths are regex patterns with capture groups
:type regex_path: bool
:param target_os: If targeting a different OS than the source. Must be Windows, Linux, or Mac.
:type target_os: str
:param target_os: If targeting a different OS than the source.
:type target_os: TargetOS
:param create_backup: Create a backup archive of the BT_backup directory?
:type create_backup: bool
:param skip_bad_files: Skip .fastresume files that cannot be read successfully.
:type skip_bad_files: bool
"""
if not os.path.exists(self.bt_backup_path) or not os.path.isdir(self.bt_backup_path):
if not self.bt_backup_path.is_dir():
raise NotADirectoryError(self.bt_backup_path)
if create_backup:
backup_filename = f'fastresume_backup{datetime.now().strftime("%Y%m%d%H%M%S")}.zip'
backup_folder(self.bt_backup_path, os.path.join(self.bt_backup_path, backup_filename))
backup_folder(self.bt_backup_path, self.bt_backup_path / backup_filename)

self.logger.info(f"Searching for .fastresume files with path {existing_path} ...")
self.logger.info(f"🕵️ Searching for .fastresume files with path {existing_path} ...")
count = 0
for fast_resume in self.discover_relevant_fast_resume(
self.bt_backup_path, existing_path, regex_path, not skip_bad_files
):
count += 1
# Fire and forget
self.discovered_files.add(fast_resume)
Thread(
target=fast_resume.replace_paths, args=[existing_path, new_path, regex_path, target_os, True, False]
).start()
logger.info(f"{'✔️' if count else '⚠️'} Processed {count} relevant fastresume file{'s' if count > 1 else ''}!")

@classmethod
def discover_relevant_fast_resume(
cls, bt_backup_path: str, existing_path: str, regex_path: bool = False, raise_on_error: bool = True
cls, bt_backup_path: Union[str, Path], existing_path: str, regex_path: bool = False, raise_on_error: bool = True
):
"""
Find .fastresume files that contain the existing path.
:param bt_backup_path: Path to BT_backup folder
:type bt_backup_path: str
:type bt_backup_path: str | Path
:param existing_path: The existing path to look for
:type existing_path: str
:param regex_path: Existing Path is a regex pattern with capture groups
Expand All @@ -82,29 +86,37 @@ def discover_relevant_fast_resume(
:return: List of FastResume Objects
:rtype: list[FastResume]
"""
for file in os.listdir(bt_backup_path):
if file.endswith(".fastresume"):
bt_backup_path = Path(bt_backup_path)
for file in bt_backup_path.iterdir():
if file.is_dir():
continue
if file.name.endswith(".fastresume"):
try:
fast_resume = FastResume(os.path.join(bt_backup_path, file))
fast_resume = FastResume(bt_backup_path / file)
except (BencodeDecodeError, FileNotFoundError, ValueError) as e:
if raise_on_error:
cls.logger.critical(f"Unable to parse {file}. Stopping Discovery!")
cls.logger.critical(f"🛑 Unable to parse {file}. Stopping Discovery!")
raise e
cls.logger.warning(f"Unable to parse {file}. Skipping!\n\n{e}")
cls.logger.warning(f"⚠️ Unable to parse {file}. Skipping!\n\n{e}")
continue
if (fast_resume.save_path is not None and existing_path in fast_resume.save_path) or (
fast_resume.qbt_save_path is not None and existing_path in fast_resume.qbt_save_path
):
yield fast_resume
elif regex_path and (
(fast_resume.save_path is not None and re.match(existing_path, fast_resume.save_path))
or (fast_resume.qbt_save_path is not None and re.match(existing_path, fast_resume.qbt_save_path))
(fast_resume.save_path is not None and re.search(existing_path, fast_resume.save_path))
or (fast_resume.qbt_save_path is not None and re.search(existing_path, fast_resume.qbt_save_path))
):
yield fast_resume
else:
logger.debug(
f"FastResume {file} is not relevant, Save Path: {fast_resume.save_path}, "
f"qBt-savePath: {fast_resume.qbt_save_path}"
)
return

@classmethod
def backup_folder(cls, folder_path: str, archive_path: str):
def backup_folder(cls, folder_path: Union[str, Path], archive_path: Union[str, Path]):
return backup_folder(folder_path, archive_path)

@classmethod
Expand All @@ -114,7 +126,7 @@ def update_fastresume(
existing_path: str,
new_path: str,
regex_path: bool = False,
target_os: Optional[str] = None,
target_os: Optional[TargetOS] = None,
save_file: bool = True,
create_backup: bool = True,
):
Expand All @@ -126,21 +138,21 @@ def update_fastresume(
class FastResume(object):
logger = logging.getLogger(__name__ + ".FastResume")

def __init__(self, file_path: str):
self._file_path = os.path.realpath(file_path)
if not os.path.exists(self.file_path) or not os.path.isfile(self.file_path):
def __init__(self, file_path: Union[str, Path]):
self._file_path = Path(file_path)
if not self.file_path.is_file():
raise FileNotFoundError(self.file_path)
self.logger.debug(f"Loading Fast Resume: {self.file_path}")
self._data = bencode.read(self.file_path)
self.logger.debug(f"Fast Resume ({self.file_path}) Init Complete.")

@property
def file_path(self) -> str:
def file_path(self) -> Path:
return self._file_path

@property
def backup_filename(self) -> str:
return f'{self.file_path}.{datetime.now().strftime("%Y%m%d%H%M%S")}.bkup'
def backup_filename(self) -> Path:
return Path(f'{self.file_path}.{datetime.now().strftime("%Y%m%d%H%M%S")}.bkup')

@property
def save_path(self) -> Optional[str]:
Expand All @@ -161,7 +173,7 @@ def set_save_path(
self,
path: str,
key: str = "save_path",
target_os: Optional[str] = None,
target_os: Optional[TargetOS] = None,
save_file: bool = True,
create_backup: bool = True,
):
Expand All @@ -180,7 +192,7 @@ def set_save_paths(
self,
path: str,
qbt_path: Optional[str] = None,
target_os: Optional[str] = None,
target_os: Optional[TargetOS] = None,
save_file: bool = True,
create_backup: bool = True,
):
Expand All @@ -198,22 +210,22 @@ def set_save_paths(
if save_file:
self.save()

def save(self, file_name: Optional[str] = None):
def save(self, file_name: Union[str, Path, None] = None):
if file_name is None:
file_name = self.file_path
self.logger.info(f"Saving File {file_name}...")
self.logger.debug(f"Saving File {file_name}...")
bencode.write(self._data, file_name)

def replace_paths(
self,
existing_path: str,
new_path: str,
regex_path: bool = False,
target_os: Optional[str] = None,
target_os: Optional[TargetOS] = None,
save_file: bool = True,
create_backup: bool = True,
):
self.logger.info(f"Replacing Paths in FastResume {self.file_path}...")
self.logger.debug(f"Replacing Paths in FastResume {self.file_path}...")
if regex_path:
new_save_path = None
new_qbt_save_path = None
Expand Down Expand Up @@ -245,4 +257,4 @@ def replace_paths(
save_file=save_file,
create_backup=create_backup,
)
self.logger.info(f"FastResume ({self.file_path}) Paths Replaced!")
self.logger.debug(f"FastResume ({self.file_path}) Paths Replaced!")
33 changes: 27 additions & 6 deletions qbt_migrate/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import logging
import sys

from . import QBTBatchMove, discover_bt_backup_path
from . import QBTBatchMove, __version__, discover_bt_backup_path
from .enums import TargetOS


logger = logging.getLogger(__name__)
Expand All @@ -15,7 +16,7 @@ def parse_args(args=None):
parser.add_argument(
"-r",
"--regex",
help="Existing and New paths are regex patterns with capture groups.",
help="Existing and New paths are regex patterns. (Capture groups recommended).",
action="store_true",
default=None,
)
Expand All @@ -42,15 +43,33 @@ def parse_args(args=None):
"-l", "--log-level", help="Log Level, Default is INFO.", choices=["DEBUG", "INFO"], default="INFO"
)

parser.add_argument(
"-v",
"--version",
help=f"Prints the current version number and exits. Current qbt_migrate version: {__version__}",
action="store_true",
default=False,
)

return parser.parse_args(args)


def main():
args = parse_args(sys.argv[1:])
logging.basicConfig()
fmt = (
"%(message)s"
if args.log_level == "INFO"
else "%(asctime)s :: %(levelname)s :: %(name)s.%(funcName)s: %(message)s"
)
logging.basicConfig(format=fmt)
logger.setLevel(args.log_level)
logging.getLogger("qbt_migrate").setLevel(args.log_level)
logging.getLogger("qbt_migrate").propagate = True
if args.version:
logger.debug("Version Print requested.")
logger.info(f"{__version__}")
logger.debug("Exiting")
return
qbm = QBTBatchMove()
if args.bt_backup_path is not None:
qbm.bt_backup_path = args.bt_backup_path
Expand Down Expand Up @@ -85,15 +104,17 @@ def main():
):
print("Please answer Windows, Linux, or Mac")
args.target_os = answer.lower().strip()
if args.target_os:
args.target_os = TargetOS.WINDOWS if args.target_os.lower() in TargetOS.WINDOWS.value else TargetOS.POSIX

# Handle Target OS Auto-Detect if not specified
if not args.target_os.strip():
if not args.target_os:
if "/" in args.existing_path and "\\" in args.new_path:
logger.info("Auto detected target OS change. Will convert slashes to Windows.")
args.target_os = "windows"
args.target_os = TargetOS.WINDOWS
elif "\\" in args.existing_path and "/" in args.new_path:
logger.info("Auto detected target OS change. Will convert slashes to Linux/Mac.")
args.target_os = "linux"
args.target_os = TargetOS.POSIX
else:
args.target_os = None

Expand Down
6 changes: 6 additions & 0 deletions qbt_migrate/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import Enum


class TargetOS(Enum):
WINDOWS = ["windows"]
POSIX = ["linux", "mac", "unix"]
Loading

0 comments on commit a57b15d

Please sign in to comment.