diff --git a/PFERD/__init__.py b/PFERD/__init__.py index 87131ce..059f585 100644 --- a/PFERD/__init__.py +++ b/PFERD/__init__.py @@ -5,33 +5,4 @@ This module exports only what you need for a basic configuration. If you want a more complex configuration, you need to import the other submodules manually. """ -import logging - from .pferd import Pferd - -STYLE = "{" -FORMAT = "[{levelname:<7}] {message}" -DATE_FORMAT = "%F %T" - -FORMATTER = logging.Formatter( - fmt=FORMAT, - datefmt=DATE_FORMAT, - style=STYLE, -) - - -def enable_logging(name: str = "PFERD", level: int = logging.INFO) -> None: - """ - Enable and configure logging via the logging module. - """ - - handler = logging.StreamHandler() - handler.setFormatter(FORMATTER) - - logger = logging.getLogger(name) - logger.setLevel(level) - logger.addHandler(handler) - - # This should be logged by our own handler, and not the root logger's - # default handler, so we don't pass it on to the root logger. - logger.propagate = False diff --git a/PFERD/ilias/crawler.py b/PFERD/ilias/crawler.py index c36b7c7..e3e27cb 100644 --- a/PFERD/ilias/crawler.py +++ b/PFERD/ilias/crawler.py @@ -14,7 +14,8 @@ from urllib.parse import (parse_qs, urlencode, urljoin, urlparse, urlsplit, import bs4 import requests -from ..utils import PrettyLogger, soupify +from ..logging import PrettyLogger +from ..utils import soupify from .authenticators import IliasAuthenticator from .date_demangler import demangle_date from .downloader import IliasDownloadInfo diff --git a/PFERD/ilias/downloader.py b/PFERD/ilias/downloader.py index e88ff38..98ad388 100644 --- a/PFERD/ilias/downloader.py +++ b/PFERD/ilias/downloader.py @@ -9,10 +9,11 @@ from typing import Callable, List, Optional import bs4 import requests +from ..logging import PrettyLogger from ..organizer import Organizer from ..tmp_dir import TmpDir from ..transform import Transformable -from ..utils import PrettyLogger, soupify, stream_to_path +from ..utils import soupify, stream_to_path from .authenticators import IliasAuthenticator LOGGER = logging.getLogger(__name__) diff --git a/PFERD/logging.py b/PFERD/logging.py new file mode 100644 index 0000000..65f8f81 --- /dev/null +++ b/PFERD/logging.py @@ -0,0 +1,133 @@ +""" +Contains a few logger utility functions and implementations. +""" + +import logging +from typing import Optional + +import colorama +from colorama import Fore, Style + +from .utils import PathLike, to_path + +STYLE = "{" +FORMAT = "[{levelname:<7}] {message}" +DATE_FORMAT = "%F %T" + +FORMATTER = logging.Formatter( + fmt=FORMAT, + datefmt=DATE_FORMAT, + style=STYLE, +) + + +def enable_logging(name: str = "PFERD", level: int = logging.INFO) -> None: + """ + Enable and configure logging via the logging module. + """ + + handler = logging.StreamHandler() + handler.setFormatter(FORMATTER) + + logger = logging.getLogger(name) + logger.setLevel(level) + logger.addHandler(handler) + + # This should be logged by our own handler, and not the root logger's + # default handler, so we don't pass it on to the root logger. + logger.propagate = False + + colorama.init() + + +class PrettyLogger: + """ + A logger that prints some specially formatted log messages in color. + """ + + def __init__(self, logger: logging.Logger) -> None: + self.logger = logger + + @staticmethod + def _format_path(path: PathLike) -> str: + return repr(str(to_path(path))) + + def error(self, message: str) -> None: + """ + Print an error message indicating some operation fatally failed. + """ + self.logger.error( + f"{Fore.RED}{Style.BRIGHT}{message}{Style.RESET_ALL}" + ) + + def warn(self, message: str) -> None: + """ + Print a warning message indicating some operation failed, but the error can be recovered + or ignored. + """ + self.logger.warning( + f"{Fore.YELLOW}{Style.BRIGHT}{message}{Style.RESET_ALL}" + ) + + def modified_file(self, path: PathLike) -> None: + """ + An existing file has changed. + """ + + self.logger.info( + f"{Fore.MAGENTA}{Style.BRIGHT}Modified {self._format_path(path)}.{Style.RESET_ALL}" + ) + + def new_file(self, path: PathLike) -> None: + """ + A new file has been downloaded. + """ + + self.logger.info( + f"{Fore.GREEN}{Style.BRIGHT}Created {self._format_path(path)}.{Style.RESET_ALL}" + ) + + def ignored_file(self, path: PathLike, reason: str) -> None: + """ + File was not downloaded or modified. + """ + + self.logger.info( + f"{Style.DIM}Ignored {self._format_path(path)} " + f"({Style.NORMAL}{reason}{Style.DIM}).{Style.RESET_ALL}" + ) + + def searching(self, path: PathLike) -> None: + """ + A crawler searches a particular object. + """ + + self.logger.info(f"Searching {self._format_path(path)}") + + def not_searching(self, path: PathLike, reason: str) -> None: + """ + A crawler does not search a particular object. + """ + + self.logger.info( + f"{Style.DIM}Not searching {self._format_path(path)} " + f"({Style.NORMAL}{reason}{Style.DIM}).{Style.RESET_ALL}" + ) + + def starting_synchronizer( + self, + target_directory: PathLike, + synchronizer_name: str, + subject: Optional[str] = None, + ) -> None: + """ + A special message marking that a synchronizer has been started. + """ + + subject_str = f"{subject} " if subject else "" + self.logger.info("") + self.logger.info(( + f"{Fore.CYAN}{Style.BRIGHT}Synchronizing " + f"{subject_str}to {self._format_path(target_directory)} " + f"using the {synchronizer_name} synchronizer.{Style.RESET_ALL}" + )) diff --git a/PFERD/organizer.py b/PFERD/organizer.py index e5b2c10..752697c 100644 --- a/PFERD/organizer.py +++ b/PFERD/organizer.py @@ -10,7 +10,8 @@ from pathlib import Path, PurePath from typing import List, Set from .location import Location -from .utils import PrettyLogger, prompt_yes_no +from .logging import PrettyLogger +from .utils import prompt_yes_no LOGGER = logging.getLogger(__name__) PRETTY = PrettyLogger(LOGGER) diff --git a/PFERD/pferd.py b/PFERD/pferd.py index ebff3b4..327164b 100644 --- a/PFERD/pferd.py +++ b/PFERD/pferd.py @@ -11,10 +11,11 @@ from .ilias import (IliasAuthenticator, IliasCrawler, IliasDirectoryFilter, IliasDownloader, IliasDownloadStrategy, KitShibbolethAuthenticator, download_modified_or_new) from .location import Location +from .logging import PrettyLogger from .organizer import Organizer from .tmp_dir import TmpDir from .transform import TF, Transform, apply_transform -from .utils import PrettyLogger, PathLike, to_path +from .utils import PathLike, to_path # TODO save known-good cookies as soon as possible diff --git a/PFERD/utils.py b/PFERD/utils.py index 8edb2f3..320b0a5 100644 --- a/PFERD/utils.py +++ b/PFERD/utils.py @@ -2,19 +2,20 @@ A few utility bobs and bits. """ -import logging import re from pathlib import Path, PurePath from typing import Optional, Tuple, Union import bs4 import requests -from colorama import Fore, Style PathLike = Union[PurePath, str, Tuple[str, ...]] def to_path(pathlike: PathLike) -> Path: + """ + Convert a given PathLike into a Path. + """ if isinstance(pathlike, tuple): return Path(*pathlike) return Path(pathlike) @@ -24,6 +25,9 @@ Regex = Union[str, re.Pattern] def to_pattern(regex: Regex) -> re.Pattern: + """ + Convert a regex to a re.Pattern. + """ if isinstance(regex, re.Pattern): return regex return re.compile(regex) @@ -74,79 +78,3 @@ def prompt_yes_no(question: str, default: Optional[bool] = None) -> bool: if response == "" and default is not None: return default print(wrong_reply) - - -class PrettyLogger: - """ - A logger that prints some specially formatted log messages in color. - """ - - def __init__(self, logger: logging.Logger) -> None: - self.logger = logger - - @staticmethod - def _format_path(path: PathLike) -> str: - return repr(str(to_path(path))) - - def modified_file(self, path: PathLike) -> None: - """ - An existing file has changed. - """ - - self.logger.info( - f"{Fore.MAGENTA}{Style.BRIGHT}Modified {self._format_path(path)}.{Style.RESET_ALL}" - ) - - def new_file(self, path: PathLike) -> None: - """ - A new file has been downloaded. - """ - - self.logger.info( - f"{Fore.GREEN}{Style.BRIGHT}Created {self._format_path(path)}.{Style.RESET_ALL}" - ) - - def ignored_file(self, path: PathLike, reason: str) -> None: - """ - File was not downloaded or modified. - """ - - self.logger.info( - f"{Style.DIM}Ignored {self._format_path(path)} " - f"({Style.NORMAL}{reason}{Style.DIM}).{Style.RESET_ALL}" - ) - - def searching(self, path: PathLike) -> None: - """ - A crawler searches a particular object. - """ - - self.logger.info(f"Searching {self._format_path(path)}") - - def not_searching(self, path: PathLike, reason: str) -> None: - """ - A crawler does not search a particular object. - """ - - self.logger.info( - f"{Style.DIM}Not searching {self._format_path(path)} " - f"({Style.NORMAL}{reason}{Style.DIM}).{Style.RESET_ALL}" - ) - - def starting_synchronizer( - self, - target_directory: PathLike, - synchronizer_name: str, - subject: Optional[str] = None, - ) -> None: - """ - A special message marking that a synchronizer has been started. - """ - - subject_str = f"{subject} " if subject else "" - self.logger.info("") - self.logger.info(( - f"{Fore.CYAN}{Style.BRIGHT}Synchronizing " - f"{subject_str}to {self._format_path(target_directory)} " - f"using the {synchronizer_name} synchronizer.{Style.RESET_ALL}" - )) diff --git a/example_config.py b/example_config.py index 1fcdae9..cb803f5 100644 --- a/example_config.py +++ b/example_config.py @@ -1,7 +1,8 @@ import argparse from pathlib import Path, PurePath -import PFERD +from PFERD import Pferd +from PFERD.logging import enable_logging from PFERD.transform import (attempt, glob, keep, move, move_dir, optionally, re_move) @@ -45,9 +46,12 @@ tf_ss_2020_pg = attempt( def df_ss_2020_or1(path: PurePath) -> bool: - if glob("Tutorien/")(path): return True - if glob("Tutorien/Tutorium 10, dienstags 15:45 Uhr/")(path): return True - if glob("Tutorien/*")(path): return False + if glob("Tutorien/")(path): + return True + if glob("Tutorien/Tutorium 10, dienstags 15:45 Uhr/")(path): + return True + if glob("Tutorien/*")(path): + return False return True @@ -64,8 +68,8 @@ def main() -> None: parser.add_argument("synchronizers", nargs="*") args = parser.parse_args() - PFERD.enable_logging() - pferd = PFERD.Pferd(Path(__file__).parent, test_run=args.test_run) + enable_logging() + pferd = Pferd(Path(__file__).parent, test_run=args.test_run) if not args.synchronizers or "numerik" in args.synchronizers: pferd.ilias_kit(