diff --git a/docs/conf.py b/docs/conf.py index 4263ea4..e362445 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,6 +23,7 @@ description = __about__.__summary__ project = __about__.__title__ version = release = __about__.__version__ +github_doc_root = f"{__about__.__uri__}/tree/main/docs/" # -- General configuration --------------------------------------------------- @@ -170,7 +171,7 @@ "deflist", "html_admonition", "html_image", - # "linkify", + "linkify", "replacements", "smartquotes", "strikethrough", diff --git a/geotribu_cli/cli.py b/geotribu_cli/cli.py index a26b0a1..dff08db 100644 --- a/geotribu_cli/cli.py +++ b/geotribu_cli/cli.py @@ -38,6 +38,7 @@ parser_search_image, parser_upgrade, ) +from geotribu_cli.utils.journalizer import configure_logger # ############################################################################# # ########## Globals ############### @@ -144,11 +145,18 @@ def main(args: list[str] = None): action="count", default=1, dest="verbosity", - # metavar="GEOTRIBU_LOGS_LEVEL", help="Niveau de verbosité : None = WARNING, -v = INFO, -vv = DEBUG. Réglable " "avec la variable d'environnement GEOTRIBU_LOGS_LEVEL.", ) + main_parser.add_argument( + "--no-logfile", + default=True, + action="store_false", + dest="opt_logfile_disabled", + help="Désactiver les fichiers de journalisation (logs).", + ) + main_parser.add_argument( "-h", "--help", @@ -351,23 +359,13 @@ def main(args: list[str] = None): # just get passed args args = main_parser.parse_args(args) - # set log level depending on verbosity argument - if 0 < args.verbosity < 4: - args.verbosity = 40 - (10 * args.verbosity) - elif args.verbosity >= 4: - # debug is the limit - args.verbosity = 40 - (10 * 3) + # log configuration + if args.opt_logfile_disabled: + configure_logger( + verbosity=args.verbosity, logfile=f"{__title_clean__}_{__version__}.log" + ) else: - args.verbosity = 0 - - logging.basicConfig( - level=args.verbosity, - format="%(asctime)s||%(levelname)s||%(module)s||%(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - console = logging.StreamHandler() - console.setLevel(args.verbosity) + configure_logger(verbosity=args.verbosity) # add the handler to the root logger logger = logging.getLogger(__title_clean__) diff --git a/geotribu_cli/utils/journalizer.py b/geotribu_cli/utils/journalizer.py new file mode 100644 index 0000000..3347f87 --- /dev/null +++ b/geotribu_cli/utils/journalizer.py @@ -0,0 +1,172 @@ +#! python3 # noqa: E265 + +"""Helper to configure logging depending on CLI options.""" + +# ############################################################################ +# ########## IMPORTS ############# +# ################################ + +# standard library +import logging +from getpass import getuser +from logging.handlers import RotatingFileHandler +from os import environ, getenv +from os.path import expanduser, expandvars +from pathlib import Path +from platform import architecture, platform +from socket import gethostname +from typing import Optional + +# package +from geotribu_cli.__about__ import __title__, __version__ +from geotribu_cli.constants import GeotribuDefaults +from geotribu_cli.utils.check_path import check_path +from geotribu_cli.utils.proxies import get_proxy_settings +from geotribu_cli.utils.str2bool import str2bool + +# ############################################################################ +# ########## GLOBALS ############# +# ################################ + +# logs +logger = logging.getLogger(__name__) +default_settings = GeotribuDefaults() + +# ############################################################################ +# ########## FUNCTIONS ########### +# ################################ + + +def configure_logger(verbosity: int = 1, logfile: Optional[Path] = None): + """Configure logging according to verbosity from CLI. + + Args: + verbosity (int): verbosity level + logfile (Path, optional): file where to store log. Defaults to None. + """ + # handle log level overridden by environment variable + verbosity = getenv("GEOTRIBU_LOGS_LEVEL", verbosity) + try: + verbosity = int(verbosity) + except ValueError as err: + logger.error(f"Bad verbosity value type: {err}. Fallback to 1.") + verbosity = 1 + + # set log level depending on verbosity argument + if 0 < verbosity < 4: + verbosity = 40 - (10 * verbosity) + elif verbosity >= 4: + # debug is the limit + verbosity = 40 - (10 * 3) + else: + verbosity = 0 + + # set console handler + log_console_handler = logging.StreamHandler() + log_console_handler.setLevel(verbosity) + + # set log file + if not logfile: + logging.basicConfig( + level=verbosity, + format="%(asctime)s||%(levelname)s||%(module)s||%(funcName)s||%(lineno)d||%(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[log_console_handler], + ) + + else: + if getenv("GEOTRIBU_LOGS_DIR") and check_path( + input_path=Path(expandvars(expanduser(getenv("GEOTRIBU_LOGS_DIR")))), + must_be_a_file=False, + must_be_a_folder=True, + must_be_writable=True, + raise_error=False, + ): + logs_folder = Path(expandvars(expanduser(getenv("GEOTRIBU_LOGS_DIR")))) + logger.debug( + f"Logs folder set with GEOTRIBU_LOGS_DIR environment variable: {logs_folder}" + ) + else: + logs_folder: Path = default_settings.geotribu_working_folder.joinpath( + "logs" + ) + logger.debug( + "Logs folder specified in GEOTRIBU_LOGS_DIR environment variable " + f"{getenv('GEOTRIBU_LOGS_DIR')} can't be used (see logs above). Fallback on " + f"default folder: {logs_folder}" + ) + + # make sure folder exists + logs_folder.mkdir(exist_ok=True, parents=True) + logs_filepath = Path(logs_folder, logfile) + + log_file_handler = RotatingFileHandler( + backupCount=10, + delay=True, + encoding="UTF-8", + filename=logs_filepath, + maxBytes=3000000, + mode="a", + ) + # force new file by execution + if logs_filepath.is_file(): + log_file_handler.doRollover() + + logging.basicConfig( + level=verbosity, + format="%(asctime)s||%(levelname)s||%(module)s||%(funcName)s||%(lineno)d||%(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[log_console_handler, log_file_handler], + ) + + logger.info(f"Log file: {logs_filepath}") + + headers() + + +def headers(): + """Basic information to log before other message.""" + # initialize the log + logger.info(f"{'='*10} {__title__} - {__version__} {'='*10}") + logger.debug(f"Operating System: {platform()}") + logger.debug(f"Architecture: {architecture()[0]}") + logger.debug(f"Computer: {gethostname()}") + logger.debug(f"Launched by user: {getuser()}") + + if getenv("userdomain"): + logger.debug(f"OS Domain: {getenv('userdomain')}") + + if get_proxy_settings(): + logger.debug(f"Network proxies detected: {get_proxy_settings()}") + else: + logger.debug("No network proxies detected") + + if str2bool(getenv("QDT_SSL_USE_SYSTEM_STORES", False)): + logger.debug("Option to use native system certificates stores is enabled.") + if "REQUESTS_CA_BUNDLE" in environ: + environ.pop("REQUESTS_CA_BUNDLE") + logger.debug( + "Custom path to CA Bundle (REQUESTS_CA_BUNDLE) has been removed from " + "environment variables." + ) + if "CURL_CA_BUNDLE" in environ: + environ.pop("CURL_CA_BUNDLE") + logger.debug( + "Custom path to CA Bundle (CURL_CA_BUNDLE) has been removed from " + "environment variables." + ) + + +def get_logger_filepath() -> Optional[Path]: + """Retrieve log filepath within logger handlers. + + Returns: + Path | None: path to the logfile or None if no handler has baseFilename attr. + """ + if logger.root.hasHandlers(): + for handler in logger.root.handlers: + if hasattr(handler, "baseFilename"): + return Path(handler.baseFilename) + + logger.warning("No file found in ay log handlers.") + return None diff --git a/requirements/base.txt b/requirements/base.txt index c06d3f8..30f99dd 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ markdownify>=0.11,<0.13 Mastodon.py>=1.8.1,<1.9 orjson>=3.8,<3.11 packaging>=20,<25 -rich_argparse>=0.6,<1.5 +rich_argparse>=1,<1.5 python-frontmatter>=1,<2 requests>=2.31,<3 typing-extensions>=4,<5 ; python_version < '3.11'