log vs config vs app (DIRTY)

This commit is contained in:
2026-08-11 10:10:52 -03:00
parent 2167e9b801
commit 067319f7ef
5 changed files with 209 additions and 192 deletions
+2 -5
View File
@@ -13,11 +13,8 @@ except PackageNotFoundError:
# package is not installed # package is not installed
pass pass
from typing import Union
from pathlib import Path
PathLike = Union[str, Path]
from .logger import logger, loglevel_map from .logger import logger, loglevel_map, get_logger
from .utils import DataDict from .utils import DataDict, PathLike, path
from .app import App from .app import App
+9 -184
View File
@@ -15,6 +15,7 @@ The industry-standard hierarchy for application configuration is:
from argparse import ArgumentParser from argparse import ArgumentParser
from collections import UserDict from collections import UserDict
from pathlib import Path from pathlib import Path
from typing import Optional
import logging import logging
import os import os
import re import re
@@ -30,13 +31,10 @@ from . import tts_aedocw, tts_generic, tts_kokoro
from . import logger, PathLike, loglevel_map from . import logger, PathLike, loglevel_map
from . import DataDict from . import DataDict
class App(DataDict): class App():
# Globals/Defaults
DEFAULT_ENV_FILE=".env"
ENV_FILE_OVERRIDE=False
WHICH = ["ffmpeg"] # Needed in $PATH WHICH = ["ffmpeg"] # Needed in $PATH
DEBUG = False
LOGLEVEL = "WARNING"
BACKENDS = { BACKENDS = {
# keys are the backend names, values are the corresponding TTS classes # keys are the backend names, values are the corresponding TTS classes
"default": tts_generic.GenericTTSBackend, "default": tts_generic.GenericTTSBackend,
@@ -52,33 +50,12 @@ class App(DataDict):
"kokoro": tts_kokoro.KokoroBackend, "kokoro": tts_kokoro.KokoroBackend,
} }
def __init__(self, *args, **kwargs): def __init__(self,
super().__init__(*args,**kwargs) input_str: Optional[str|Path|list[str|Path]],
self._argv = sys.argv output_str: Optional[str|Path],
self._args_list = args if args is not None else self._argv backend: str,
):
self.setup_logger()
if "env_file" not in self._data:
self._data["env_file"] = App.DEFAULT_ENV_FILE
self.logger.debug(self._data["env_file"])
# for path_flag in ["env_file"]:
# if not Path(self[path_flag]).exists():
# self.logger.warning(f"Could not read %s. Ignoring '%s' and using '%s'", path_flag, self[path_flag], App.DEFAULT_ENV_FILE)
# self[path_flag] = App["DEFAULT_" + path_flag.upper()]
# load .env/env_file
load_dotenv(self.env_file, override=os.environ.get("ENV_FILE_OVERRIDE",App.ENV_FILE_OVERRIDE))
# Now we can call argparser with correct defaults and logger/loglevel
# Get ArgumentParser and pased *args or read from sys.argv
self._parsed_args = self.get_parser().parse_args(args or sys.argv[1:])
for k,v in self._parsed_args.__dict__.items():
self[k] = v
# Validate and sanitize output # Validate and sanitize output
self.output_path = App.path(self.output) self.output_path = App.path(self.output)
@@ -126,76 +103,6 @@ class App(DataDict):
self.engine = App.BACKENDS[self.backend](self) self.engine = App.BACKENDS[self.backend](self)
def setup_logger(self, verbose=None, debug_level=None, loglevel: str|int = None, logfile:PathLike=None):
"""Initialize the package logger from CLI flags and defaults.
Resolve the effective logging level from the class args (or sys.argv).
--debug (or debug_level=True) takes priority, then --verbose (or verbose=True)
is compared to --loglevel (or loglevel=) and the lower one is proritized.
"""
# Setup logger
self.logger = logger
# Find lowest priority loglevel
if debug_level or App.DEBUG or "--debug" in sys.argv or "--debug" in self._args_list:
print("DEBUG!")
self.debug_level = True
self.loglevel = "DEBUG"
else: # compare verbose to loglevel
if "--verbose" in self._args_list or verbose == True:
self.verbose = True
for i, a in enumerate(self._args_list):
if a == "--loglevel":
self.loglevel = self._args_list[i+1]
elif a.startswith("--loglevel="):
self.loglevel = a.replace("--loglevel=", "")
if "loglevel" in self and self.loglevel is None:
if loglevel is not None:
self.logger.debug("self.loglevel is None, but setup_logger(loglevel) is not None")
self.loglevel = loglevel
else:
self.logger.debug("self.loglevel = App.LOGLEVEL (%s)", App.LOGLEVEL)
self.loglevel = App.LOGLEVEL
# lower = more priority
if loglevel_map[self.loglevel] > loglevel_map["INFO"]:
self.logger.debug("self.loglevel='%s'(%d) > INFO", self.loglevel, loglevel_map[self.loglevel])
self.loglevel = "INFO"
self.logger.info("Setting loglevel to %s",self.loglevel)
self.logger.setLevel(loglevel_map[self.loglevel])
# Get logfile
for i, a in enumerate(self._args_list):
if a == "--logfile":
self.logfile = self._args_list[i+1]
elif a.startswith("--logfile="):
self.logfile = a.replace("--logfile=", "")
if "logfile" in self and self.logfile:
if logfile:
self.logger.warning("Overriding --logfile='%s' from setup_logger(logfile='%s')", self.logfile, logfile)
self.logfile = logfile
fh = logging.FileHandler(logfile)
fh.setLevel(self.loglevel)
fh.setFormatter(logger.formatter)
self.logger.addHandler(fh)
# if self.debug_level:
# logger.info("Loglevel: DEBUG")
# if self.loglevel:
# logger.warning("Ignoring '--loglevel' flag because DEBUG/'--debug' was also set.")
# if "--verbose" in self._args_list:
# logger.warning("Ignoring '--verbose' flag because '--debug' was also set.")
# self.verbose = False
# if self.loglevel and self.verbose:
# self.warning("Conflict: trying to --loglevel=%s (%d) and --verbose.", self.loglevel, loglevel_map[self.loglevel])
# if loglevel_map[self.loglevel] < logging.INFO:
# self.logger.info("Setting loglevel to %s (%d), which is more verbose than INFO.", self.loglevel, loglevel_map[self.loglevel])
return self.logger
# Utility methods # Utility methods
def check_env(self, which=None): def check_env(self, which=None):
@@ -218,85 +125,3 @@ class App(DataDict):
else: else:
self.logger.info(f"%s found at: %s", bin, p) self.logger.info(f"%s found at: %s", bin, p)
return True return True
def get_parser(self):
p = ArgumentParser(description="Convert EPUB to audio using TTS")
p.add_argument(
"--version",
help="Show version and exit",
action="version",
version=f"%(prog)s {__import__('epub_tts').__version__}")
# General options
p.add_argument(
"--debug", dest="debug_level", action="store_true",
help="Enable debug logging and verbose diagnostics")
p.add_argument(
"--verbose", action="store_true",
help="Enable info-level logging")
p.add_argument(
"--loglevel",
type=str.upper, default=logging.INFO,
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set logging level, from most verbose to least verbose:")
p.add_argument(
"--logfile", default=None,
help="Write logs to the specified file")
# Input options and processing
p.add_argument(
"-i", "--input",
nargs="+", required=True,
help="Input EPUB file")
p.add_argument(
"-r", "--replace", action="append", nargs=2,
help="Replace text in the intermediate output. "
"Specify pairs of old_text new_text. "
"Can be used multiple times.")
p.add_argument(
"--check-env",action="store_true",
help="Check the runtime environment and exit")
p.add_argument(
"-b", "--backend", default="default",
choices=App.BACKENDS.keys(),
help="Backend to use for TTS")
# Output options
p.add_argument(
"-o", "--output",
help="Output audio file", required=True)
p.add_argument(
"-c", "--cover",
help="Path to cover image to embed or use "
"for output metadata")
# Speech options
p.add_argument(
"-l", "--language",
help="Language to use for TTS (see your "
"backend's documentation for available voices)")
p.add_argument(
"-v", "--voice",
help="Voice to use for TTS (see your backend's "
"documentation for available voices)")
p.add_argument(
"--speed", type=float, default=1.0,
help="Playback speed multiplier (default: 1.0) "
"(not all backends support this)")
p.add_argument(
"--short-pause", type=int, default=None,
help="Short pause duration in milliseconds between "
"phrases or sentences (not all backends support this)")
p.add_argument(
"--long-pause", type=int, default=None,
help="Long pause duration in milliseconds between "
"sections or paragraphs (not all backends support this)")
p.add_argument(
"--notitles", action="store_true",
help="Do not read chapter titles")
return p
@staticmethod
def path(fpath: PathLike = None):
return Path(fpath).resolve()
+112
View File
@@ -31,6 +31,118 @@ from argparse import ArgumentParser
# Local imports # Local imports
from . import App from . import App
from . import logger, get_logger
# Globals/Defaults
DEFAULT_ENV_FILE=".env"
ENV_FILE_OVERRIDE=False
DEBUG = False
LOGLEVEL = "WARNING"
def get_parser():
p = ArgumentParser(description="Convert EPUB to audio using TTS")
p.add_argument(
"--version",
help="Show version and exit",
action="version",
version=f"%(prog)s {__import__('epub_tts').__version__}")
# General options
p.add_argument(
"--debug", dest="debug_level", action="store_true",
help="Enable debug logging and verbose diagnostics")
p.add_argument(
"--verbose", action="store_true",
help="Enable info-level logging")
p.add_argument(
"--loglevel",
type=str.upper, default=logging.INFO,
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set logging level, from most verbose to least verbose:")
p.add_argument(
"--logfile", default=None,
help="Write logs to the specified file")
# Input options and processing
p.add_argument(
"-i", "--input",
nargs="+", required=True,
help="Input EPUB file")
p.add_argument(
"-r", "--replace", action="append", nargs=2,
help="Replace text in the intermediate output. "
"Specify pairs of old_text new_text. "
"Can be used multiple times.")
p.add_argument(
"--check-env",action="store_true",
help="Check the runtime environment and exit")
p.add_argument(
"-b", "--backend", default="default",
choices=App.BACKENDS.keys(),
help="Backend to use for TTS")
# Output options
p.add_argument(
"-o", "--output",
help="Output audio file", required=True)
p.add_argument(
"-c", "--cover",
help="Path to cover image to embed or use "
"for output metadata")
# Speech options
p.add_argument(
"-l", "--language",
help="Language to use for TTS (see your "
"backend's documentation for available voices)")
p.add_argument(
"-v", "--voice",
help="Voice to use for TTS (see your backend's "
"documentation for available voices)")
p.add_argument(
"--speed", type=float, default=1.0,
help="Playback speed multiplier (default: 1.0) "
"(not all backends support this)")
p.add_argument(
"--short-pause", type=int, default=None,
help="Short pause duration in milliseconds between "
"phrases or sentences (not all backends support this)")
p.add_argument(
"--long-pause", type=int, default=None,
help="Long pause duration in milliseconds between "
"sections or paragraphs (not all backends support this)")
p.add_argument(
"--notitles", action="store_true",
help="Do not read chapter titles")
return p
def setup_app(cmdline:str|list[str]=None):
logger = get_logger()
parser = get_parser()
parsed_args = p.parse_args(cmdline)
if "env_file" not in self._data:
self._data["env_file"] = App.DEFAULT_ENV_FILE
self.logger.debug(self._data["env_file"])
# for path_flag in ["env_file"]:
# if not Path(self[path_flag]).exists():
# self.logger.warning(f"Could not read %s. Ignoring '%s' and using '%s'", path_flag, self[path_flag], App.DEFAULT_ENV_FILE)
# self[path_flag] = App["DEFAULT_" + path_flag.upper()]
# load .env/env_file
load_dotenv(self.env_file, override=os.environ.get("ENV_FILE_OVERRIDE",App.ENV_FILE_OVERRIDE))
# Now we can call argparser with correct defaults and logger/loglevel
# Get ArgumentParser and pased *args or read from sys.argv
self._parsed_args = self.get_parser().parse_args(args or sys.argv[1:])
for k,v in self._parsed_args.__dict__.items():
self[k] = v
def cli(args=None): def cli(args=None):
app = App() app = App()
+81 -1
View File
@@ -5,7 +5,7 @@
# ============================================================ # ============================================================
# Logging # Logging
# ============================================================ # ============================================================
from pathlib import Path
import enum import enum
import logging import logging
import os import os
@@ -67,3 +67,83 @@ def print(message):
print(formatted.replace("', '", "',\n'")) print(formatted.replace("', '", "',\n'"))
else: else:
pp.pprint(message) pp.pprint(message)
def get_level(
cmdline: str|list[str] = None,
verbose: bool = False,
debug: bool = False,
quiet: bool = False,
default: int = logging.INFO,
) -> int:
# Find lowest priority loglevel
if "--debug" in cmdline:
debug = True
if "--verbose" in cmdline:
verbose = True
if "--quiet" in cmdline:
quiet = True
if debug:
return logging.DEBUG
for i, a in enumerate(cmdline):
if a == "--loglevel":
loglevel = loglevel_map.get(cmdline[i+1], None)
elif a.startswith("--loglevel="):
loglevel = loglevel_map.get(a.replace("--loglevel=", ""), None)
# lower = more priority
if loglevel_map[loglevel] > loglevel_map["INFO"]:
config.logger.debug("self.loglevel='%s'(%d) > INFO", config.loglevel, loglevel_map[config.loglevel])
config.loglevel = "INFO"
def get_logger(
verbose:bool = False,
debug:bool = False,
quiet: bool = False,
loglevel: str|int = logging.INFO,
logfile: str|Path = None,
):
"""Initialize the package logger from CLI flags and defaults.
Resolve the effective logging level from the class args (or sys.argv).
--debug (or debug_level=True) takes priority, then --verbose (or verbose=True)
is compared to --loglevel (or loglevel=) and the lower one is proritized.
"""
config.logger.info("Setting loglevel to %s",config.loglevel)
config.logger.setLevel(loglevel_map[config.loglevel])
# Get logfile
for i, a in enumerate(config._args_list):
if a == "--logfile":
config.logfile = config._args_list[i+1]
elif a.startswith("--logfile="):
config.logfile = a.replace("--logfile=", "")
if "logfile" in config and config.logfile:
if logfile:
config.logger.warning("Overriding --logfile='%s' from setup_logger(logfile='%s')", config.logfile, logfile)
config.logfile = logfile
fh = logging.FileHandler(logfile)
fh.setLevel(config.loglevel)
fh.setFormatter(logger.formatter)
config.logger.addHandler(fh)
# if self.debug_level:
# logger.info("Loglevel: DEBUG")
# if self.loglevel:
# logger.warning("Ignoring '--loglevel' flag because DEBUG/'--debug' was also set.")
# if "--verbose" in self._args_list:
# logger.warning("Ignoring '--verbose' flag because '--debug' was also set.")
# self.verbose = False
# if self.loglevel and self.verbose:
# self.warning("Conflict: trying to --loglevel=%s (%d) and --verbose.", self.loglevel, loglevel_map[self.loglevel])
# if loglevel_map[self.loglevel] < logging.INFO:
# self.logger.info("Setting loglevel to %s (%d), which is more verbose than INFO.", self.loglevel, loglevel_map[self.loglevel])
return config.logger
+4 -1
View File
@@ -10,12 +10,15 @@ import shutil
import sys import sys
import venv import venv
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional, Union
from collections.abc import MutableMapping from collections.abc import MutableMapping
from . import logger, PathLike from . import logger, PathLike
def path(fpath: str|Path = None):
return Path(fpath).resolve()
class DataDict(dict): class DataDict(dict):
"""Wrapper around dictionary objects for easier dict subclassing """Wrapper around dictionary objects for easier dict subclassing
See module [collections](https://github.com/python/cpython/blob/3.14/Lib/collections/__init__.py#L1133) See module [collections](https://github.com/python/cpython/blob/3.14/Lib/collections/__init__.py#L1133)