"""CLI entry point for the Crossword KDP Book Generator.""" import logging import sys import click from csv_reader import CSVValidationError, read_csv from generator import generate_batch from kdp_format import render_kdp_book from models import TRIM_SIZES, PuzzleBook @click.command() @click.option("--input", "input_path", required=True, type=click.Path(exists=True, dir_okay=False), help="Path to a CSV file with word,clue columns.") @click.option("--output", "output_path", default="output.pdf", show_default=True, help="Output PDF file path.") @click.option("--puzzles", default=20, show_default=True, help="Number of crossword puzzles to generate.") @click.option("--grid-size", default=13, show_default=True, help="Grid dimensions (e.g. 15 for a 15×15 grid).") @click.option("--trim-size", default="8.5x11", show_default=True, type=click.Choice(list(TRIM_SIZES.keys())), help="KDP page trim size.") @click.option("--title", default="Crossword Puzzles", show_default=True, help="Book title for the cover page.") @click.option("--author", default="", help="Author name.") @click.option("--words-per-puzzle", default=20, show_default=True, help="Target number of words per puzzle.") @click.option("--solutions-at-end", is_flag=True, default=False, help="Group all solution pages at the back of the book.") @click.option("--seed", default=None, type=int, help="Random seed for reproducible output.") @click.option("--no-toc", is_flag=True, default=False, help="Omit the table of contents page.") @click.option("--no-instructions", is_flag=True, default=False, help="Omit the how-to-solve instructions page.") @click.option("-v", "--verbose", is_flag=True, default=False, help="Enable verbose logging.") def main(input_path: str, output_path: str, puzzles: int, grid_size: int, trim_size: str, title: str, author: str, words_per_puzzle: int, solutions_at_end: bool, seed: int | None, no_toc: bool, no_instructions: bool, verbose: bool) -> None: """Generate a KDP-ready crossword puzzle book from a CSV word list.""" logging.basicConfig( level=logging.DEBUG if verbose else logging.INFO, format="%(levelname)s: %(message)s", ) # -- read & validate CSV ------------------------------------------------- try: word_clues = read_csv(input_path, min_words=3, max_word_length=grid_size) except (CSVValidationError, FileNotFoundError) as exc: click.echo(f"Error: {exc}", err=True) sys.exit(1) click.echo(f"Loaded {len(word_clues)} words from {input_path}.") # -- generate puzzles ---------------------------------------------------- click.echo(f"Generating {puzzles} puzzle(s) " f"({words_per_puzzle} words each, {grid_size}×{grid_size} grid)…") generated = generate_batch( word_clues, count=puzzles, words_per_puzzle=words_per_puzzle, grid_size=grid_size, seed=seed, ) if not generated: click.echo("Error: could not generate any puzzles. " "Try a larger word list or smaller grid.", err=True) sys.exit(1) if len(generated) < puzzles: click.echo(f"Warning: only {len(generated)}/{puzzles} puzzles met " f"quality thresholds.", err=True) click.echo(f"Generated {len(generated)} puzzle(s).") # -- assemble KDP book --------------------------------------------------- book = PuzzleBook( title=title, author=author, puzzles=generated, trim_size=TRIM_SIZES[trim_size], solutions_at_end=solutions_at_end, ) try: total_pages = render_kdp_book( book, output_path, include_toc=not no_toc, include_instructions=not no_instructions, ) except ValueError as exc: click.echo(f"Error: {exc}", err=True) sys.exit(1) click.echo(f"Wrote {total_pages}-page KDP book to {output_path}.") if __name__ == "__main__": main()