diff --git a/omicron/ssg/cli.py b/omicron/ssg/cli.py index 069dd6a..1ce5473 100644 --- a/omicron/ssg/cli.py +++ b/omicron/ssg/cli.py @@ -1,10 +1,56 @@ +import argparse import logging import sys -from omicron.ssg.site import Site +from omicron.ssg.output import OutputError +from omicron.ssg.site import ConfigError, Site from pathlib import Path -def setup_logging(verbose: bool = False, log_file: str | None = None) -> None: +class CliError(RuntimeError): + pass + + +def setup_argparse() -> argparse.ArgumentParser: + # base options + help_formatter = argparse.ArgumentDefaultsHelpFormatter + parser = argparse.ArgumentParser(prog="ossg", formatter_class=help_formatter) + parser.add_argument( + "--site", type=Path, default=Path("."), help="path to site directory" + ) + parser.add_argument( + "--verbose", "-v", action="count", default=0, help="increase log verbosity" + ) + parser.add_argument( + "--log-file", + type=Path, + default=argparse.SUPPRESS, + help="write logs to this file (default: /build.log)", + ) + + subparsers = parser.add_subparsers(dest="command", required=True) + + # build command + build = subparsers.add_parser( + "build", help="build the site", formatter_class=help_formatter + ) + build.add_argument( + "--clean", + action=argparse.BooleanOptionalAction, + default=True, + help="remove output directory before building", + ) + build.add_argument( + "--drafts", + action=argparse.BooleanOptionalAction, + default=False, + help="include draft content", + ) + subparsers.add_parser("clean", help="remove the output directory") + + return parser + + +def setup_logging(verbose: bool = False, log_file: Path | None = None) -> None: logger = logging.getLogger("omicron.ssg") logger.setLevel(logging.DEBUG) @@ -13,8 +59,46 @@ def setup_logging(verbose: bool = False, log_file: str | None = None) -> None: console.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) logger.addHandler(console) + if log_file is not None: + try: + file_handler = logging.FileHandler(log_file) + except FileNotFoundError as e: + raise CliError("log file can't be written") from e + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter( + logging.Formatter("%(asctime)s %(levelname)s: %(message)s") + ) + logger.addHandler(file_handler) + + +def build(args: argparse.Namespace) -> None: + site = Site(args.site, args.drafts) + if args.clean: + site.clean() + site.build() + + +def clean(args: argparse.Namespace) -> None: + site = Site(args.site) + site.clean() + + +def cli() -> None: + parser = setup_argparse() + args = parser.parse_args() + log_file = getattr(args, "log_file", args.site / "build.log") + setup_logging(verbose=bool(args.verbose), log_file=log_file) + if args.command == "build": + build(args) + elif args.command == "clean": + clean(args) + else: + raise NotImplementedError(f"'{args.command}' command not implemented yet") + def main() -> None: - setup_logging(verbose=True) - site = Site(Path(".")) - site.build() + try: + cli() + except (ConfigError, OutputError, CliError) as e: + print(e) + sys.exit(1) diff --git a/omicron/ssg/site.py b/omicron/ssg/site.py index 89d17d8..e7f600b 100644 --- a/omicron/ssg/site.py +++ b/omicron/ssg/site.py @@ -1,4 +1,5 @@ import logging +import shutil from typing import Any, cast from urllib.parse import urlparse @@ -31,7 +32,7 @@ class ConfigError(RuntimeError): class Site: - def __init__(self, site: Path): + def __init__(self, site: Path, drafts: bool = False): self.site_path = site.resolve() self.content: list[Content] = [] self.directories: list[Directory] = [] @@ -54,8 +55,11 @@ class Site: def read_config(self) -> dict[str, Any]: config_path = self.site_path / "config.yml" log.debug("Reading config file %s", config_path) - with open(config_path, "r") as f: - config = yaml.safe_load(f) + try: + with open(config_path, "r") as f: + config = yaml.safe_load(f) + except FileNotFoundError: + raise ConfigError("config file missing, not a valid ossg directory") if not isinstance(config, dict) or not all(isinstance(k, str) for k in config): raise ConfigError("config file must be a YAML mapping with string keys") if "name" not in config: @@ -188,6 +192,12 @@ class Site: self.other_outputs.append(tags_page) log.info("Discovered %d content items", len(self.content)) + def clean(self) -> None: + if not self.output_path.exists(): + return + shutil.rmtree(self.output_path) + log.info("Removed %s", self.output_path) + def build(self) -> None: if not self.origin: log.warning(