180 lines
5.1 KiB
Python
180 lines
5.1 KiB
Python
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: <site>/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)
|