Compare commits
No commits in common. "573aec8ad757f51ff370c90e39260d866ec9bf36" and "d00a533dd96e5f406658d307311bce0942174a8e" have entirely different histories.
573aec8ad7
...
d00a533dd9
3
.vscode/bookbuild.code-workspace
vendored
3
.vscode/bookbuild.code-workspace
vendored
@ -5,7 +5,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
"python.terminal.activateEnvInCurrentTerminal": true
|
||||||
"pylint.path" : ["${interpreter}", "-m", "pylint"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,2 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from .main import app
|
from .main import app
|
||||||
app(prog_name="bookbuild")
|
app(prog_name="bookbuild")
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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}")
|
|
||||||
|
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
|
||||||
Loading…
Reference in New Issue
Block a user