import argparse import logging import sys from collections.abc import Callable from omicron.ssg.output import OutputError from omicron.ssg.site import Site from omicron.ssg.config import ConfigError, SiteConfig, DEFAULT_ITEMS_PER_PAGE from pathlib import Path log = logging.getLogger(__name__) 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", ) # clean command subparsers.add_parser("clean", help="remove the output directory") # init command subparsers.add_parser( "init", help="create a new site directory", formatter_class=help_formatter ) return parser def setup_logging(verbose: bool = False, log_file: Path | None = None) -> None: logger = logging.getLogger("omicron.ssg") logger.setLevel(logging.DEBUG) console = logging.StreamHandler(sys.stderr) console.setLevel(logging.DEBUG if verbose else logging.INFO) 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 prompt( label: str, explanation: str | None = None, default: str | None = None, required: bool = True, validate: Callable[[str], bool] | None = None, error: str = "Invalid value.", ) -> str: if explanation: print(explanation) hint = f" [{default}]" if default is not None else "" while True: value = input(f"{label}{hint}: ").strip() if not value and default is not None: value = default if not value and not required: return "" if not value: print("This field is required.") continue if validate and not validate(value): print(error) continue return value def init(args: argparse.Namespace) -> None: try: args.site.mkdir(parents=True, exist_ok=True) except (FileExistsError, NotADirectoryError) as e: raise CliError(f"{args.site} is not a directory") from e if any(args.site.iterdir()): raise CliError(f"{args.site} is not empty") config_path = args.site / "config.yml" name = prompt("Site name") base_url = prompt( "Base URL", "Optional, e.g. https://example.com/blog", required=False ) template = prompt("Template", default="plain") items_per_page = prompt( "Items per page", default=str(DEFAULT_ITEMS_PER_PAGE), validate=lambda v: v.isdigit() and int(v) > 0, error="Items per page must be a positive integer.", ) config = SiteConfig.from_dict( { "name": name, "base_url": base_url, "template": template, "items_per_page": int(items_per_page), } ) config.to_file(config_path) (args.site / "content").mkdir(exist_ok=True) log.info("Created new site at %s", args.site) def cli() -> None: parser = setup_argparse() args = parser.parse_args() default_log_file = None if args.command == "init" else args.site / "build.log" log_file = getattr(args, "log_file", default_log_file) setup_logging(verbose=bool(args.verbose), log_file=log_file) if args.command == "build": build(args) elif args.command == "clean": clean(args) elif args.command == "init": init(args) else: raise NotImplementedError(f"'{args.command}' command not implemented yet") def main() -> None: try: cli() except (ConfigError, OutputError, CliError) as e: print(e) sys.exit(1)