Compare commits

..

No commits in common. "573aec8ad757f51ff370c90e39260d866ec9bf36" and "d00a533dd96e5f406658d307311bce0942174a8e" have entirely different histories.

6 changed files with 81 additions and 214 deletions

View File

@ -5,7 +5,6 @@
} }
], ],
"settings": { "settings": {
"python.terminal.activateEnvInCurrentTerminal": true, "python.terminal.activateEnvInCurrentTerminal": true
"pylint.path" : ["${interpreter}", "-m", "pylint"]
} }
} }

View File

@ -1,4 +1,2 @@
#!/usr/bin/env python
from .main import app from .main import app
app(prog_name="bookbuild") app(prog_name="bookbuild")

View File

@ -1,43 +1,40 @@
from pathlib import Path from pathlib import Path
from logging import Logger
import typer import typer
from .defaults import LOGGER, OUTPUT_DIR
# instantiate typer app
clean_app = typer.Typer() clean_app = typer.Typer()
# ################## def _clean_dir(path: Path, logger: Logger, start_dir = None) -> None:
# INTERNAL FUNCTIONS
# ##################
def _clean_dir(path: Path) -> None:
""" """
Recursively delete all files and directories in `path`. Recursively delete all files and directories in `path`.
""" """
# remember start dir (not to be deleted)
if not start_dir:
start_dir = path
for file in path.iterdir(): for file in path.iterdir():
# if file: delete # if file: delete
if file.is_file(): if file.is_file():
LOGGER.debug(f"unlinking file {file}") logger.debug(f"unlinking file {file}")
file.unlink() file.unlink()
# if dir: clean recursively # if dir: clean recursively
else: else:
_clean_dir(file) _clean_dir(file, logger, start_dir)
# delete empty dir unless it's start_dir # delete empty dir unless it's start_dir
if file != OUTPUT_DIR: if file != start_dir:
LOGGER.debug(f"removing dir {file}") logger.debug(f"removing dir {file}")
file.rmdir() file.rmdir()
# ##############
# TYPER COMMANDS
# ##############
@clean_app.command() @clean_app.command()
def clean(ctx: typer.Context): def clean(ctx: typer.Context):
""" """
Remove all files from output directory. Remove all files from output directory.
""" """
LOGGER.info(f"Cleaning directory {OUTPUT_DIR}.") logger, output_dir = ctx.obj["logger"], ctx.obj["paths"]["output"]
_clean_dir(OUTPUT_DIR) logger.info(f"Cleaning directory {output_dir}.")
_clean_dir(output_dir, logger)

View File

@ -1,122 +0,0 @@
import logging
import subprocess
from enum import Enum
from pathlib import Path
from shutil import which
from typing import Annotated
import typer
# start logger
LOGGER = logging.getLogger(__name__)
# helper class for available output formats
class Format(str, Enum):
PDF = "pdf"
PNG = "png"
# directory defaults
BASE_DIR = Path.cwd()
CONFIG_DIR = BASE_DIR / "config"
CONTENT_DIR = BASE_DIR / "content"
COVER_DIR = BASE_DIR / "cover"
MINIZINE_DIR = BASE_DIR / "promo" / "minizine"
OUTPUT_DIR = BASE_DIR / "output"
# logging defaults
LOG_LEVEL = "INFO"
# default executables and options
EXECUTABLES = {
"pandoc": {"executable": "", "options": []},
"typst": {"executable": "", "options": ["compile", "--root", "."]}
}
# ##################
# INTERNAL FUNCTIONS
# ##################
def run_command(executable: str, infiles: list, ex_opts: list = [], outfile: str = ""):
"""
Runs a given executable as a shell command using the given options, list of input filenames, and optional output filename.
"""
# put command together
outfile = [outfile] if outfile else []
command = [executable] + ex_opts + infiles + outfile
LOGGER.debug(f"Executing command {command}")
# run shell command
try:
# stdout = subprocess.check_output(command, stderr=subprocess.STDOUT)
pass #DEBUG
except subprocess.CalledProcessError as e:
LOGGER.critical(f"Command failed with return code {e.returncode}.")
LOGGER.critical(f"Command output: {e.output}")
raise typer.Exit(code=e.returncode)
else:
LOGGER.debug(f"Command finished with return code 0.")
# LOGGER.debug(f"Command output: {stdout}")
def get_executable(cmd: str) -> str:
"""
Returns the path to shell command `cmd` or exits.
"""
exe = which(cmd)
if not exe:
LOGGER.critical(f"{cmd} is not installed!")
typer.Exit(1)
return exe
def absolutify(path: str) -> Path:
"""
Takes a path in string form and returns a Path object which is either the original path (if it was absolute) or relative to the current working directory.
"""
p = Path(path).expanduser()
return p if p.is_absolute() else BASE_DIR / p
# ##############
# TYPER CALLBACK
# ##############
# callback function for the main typer app
def callback(
ctx: typer.Context,
log_level: Annotated[
str,
typer.Option("--log-level", "-l", help="Set log level.")
] = LOG_LEVEL,
output_dir: Annotated[
str,
typer.Option("--output-dir", "-o", help=f"Specify output directory (either absolute or relative to {str(BASE_DIR)}).")
] = str(OUTPUT_DIR)
):
"""
Typer callback function for the main app. Configures the logger and handles CLI options that do not belong to a subcommand.
"""
# validate log_level
if not getattr(logging, log_level.upper()):
LOGGER.error(f"{log_level} is not a recognized log_level, using default '{LOG_LEVEL}' instead.")
log_level = LOG_LEVEL
# configure logger
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="{asctime} {filename} {levelname}: {message}",
style="{",
datefmt="%Y-%m-%d %H:%M:%S"
)
LOGGER.debug("Logger configured.")
# set output directory and make sure it exists
OUTPUT_DIR = absolutify(output_dir)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
LOGGER.debug(f"output_dir is {OUTPUT_DIR}")

View File

@ -1,26 +1,83 @@
#!/usr/bin/env python
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Annotated
import logging
import typer import typer
from .defaults import callback from .clean import clean_app
#from .pdf import pdf as pdf_app
# start logger
logger = logging.getLogger(__name__)
# instantiate typer app
app = typer.Typer( app = typer.Typer(
no_args_is_help=True, no_args_is_help=True,
callback=callback, help="Build script to make a professionally typeset book from markdown source files that can either be read on screen (PDF or EPUB) or be used in a project with Tredition.",
context_settings={"help_option_names": ["-h", "--help"]}, context_settings={"help_option_names": ["-h", "--help"]}
help="Build script to make a professionally typeset book from markdown source files that can either be read on screen (PDF or EPUB) or be used in a project with Tredition."
) )
# add typer apps handling the subcommands #app.add_typer(pdf_app)
from .clean import clean_app
app.add_typer(clean_app) app.add_typer(clean_app)
from .minizine import minizine_app
app.add_typer(minizine_app)
# Make CLI runnable from source tree with python src/package # Make CLI runnable from source tree with python src/package
if not __package__: if not __package__:
package_source_path = Path(__file__).parent package_source_path = Path(__file__).parent
sys.path.insert(0, package_source_path) sys.path.insert(0, package_source_path)
@app.callback()
def main(
ctx: typer.Context,
log_level: Annotated[
str,
typer.Option("--log-level", "-l", help="Set log level.")
] = "INFO",
output_dir: Annotated[
str,
typer.Option("--output-dir", "-o", help="Specify output directory.")
] = "./output/"
):
# prepare context object
ctx.obj = {}
# validate log_level
if not getattr(logging, log_level.upper()):
logger.error(f"{log_level} is not a recognized log_level, using 'info' instead.")
log_level = "info"
# configure logger
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="{asctime} {levelname}: {message}",
style="{",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger.debug("Logger configured.")
ctx.obj["logger"] = logger
# set directories
paths = {}
paths["base"] = Path.cwd()
paths["config"] = paths["base"] / "config"
paths["content"] = paths["base"] / "content"
paths["cover"] = paths["base"] / "cover"
paths["minizine"] = paths["base"] / "promo" / "minizine"
# set output directory
p = Path(output_dir).expanduser()
paths["output"] = p if p.is_absolute() else paths["base"] / p
logger.debug(f"output_dir is {paths['output']}")
# add paths to context
ctx.obj["paths"] = paths
# make sure output dir exists
paths["output"].mkdir(parents=True, exist_ok=True)
#if __name__ == "__main__":
# app()

View File

@ -1,62 +0,0 @@
from logging import Logger
from pathlib import Path
from typing import Annotated
import typer
from .defaults import *
# instantiate typer app
minizine_app = typer.Typer()
@minizine_app.command()
def minizine(
ctx: typer.Context,
format: Annotated[
Format,
typer.Option("--format", "-f", help="Specify output format.")
] = Format.PDF,
input_dir: Annotated[
str,
typer.Option("--input-dir", "-i", help=f"Specify input directory (either absolute or relative to {str(BASE_DIR)}).")
] = str(MINIZINE_DIR)
):
"""
Build the minizine for the book.
"""
LOGGER.info(f"Building minizine as a {format.upper()} file.")
# make sure input_dir exists and is not empty
input_dir = absolutify(input_dir)
if not input_dir.exists():
LOGGER.error(f"Input directory {input_dir} does not exist.")
return
elif not next(input_dir.glob("*"), None):
LOGGER.error(f"Input directory {input_dir} is empty.")
return
# get typst executable
typst = EXECUTABLES["typst"]
# get input files
input_paths = input_dir.glob("*.typ")
# run command on each input file
was_empty = True
while True:
# get next generator item
input_path = next(input_paths, None)
if input_path:
was_empty = False
LOGGER.info(f"Processing file {str(input_path)}")
# get input file name
input_files = [str(input_path)]
# set output file name
output_file = str(OUTPUT_DIR / f"{input_path.stem}.{format.value}")
# run command
run_command(typst["executable"], ex_opts=typst["options"], infiles=input_files, outfile=output_file)
else:
if was_empty:
LOGGER.error(f"No typst source files found in {input_dir}")
break