Metadata-Version: 2.4
Name: simplibs-cli
Version: 0.1.0
Summary: The small, repeatable parts of a command-line program that talks to a person: prompts that ask again until the answer is usable, a check whether anybody can answer, option lists, failure summaries and verbosity flags resolved to a logging level.
Author-email: "Dalibor Sova (Sudip2708)" <daliborsova@seznam.cz>
License-Expression: MIT
Project-URL: Homepage, https://github.com/simplibs/simplibs-cli
Project-URL: Repository, https://github.com/simplibs/simplibs-cli
Project-URL: Issues, https://github.com/simplibs/simplibs-cli/issues
Project-URL: Changelog, https://github.com/simplibs/simplibs-cli/blob/main/CHANGELOG.md
Keywords: cli,command-line,prompt,prompts,input,interactive,terminal,verbosity,argparse,simplibs,simplibs-cli,developer-tools
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: User Interfaces
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: simplibs-exception>=1.1.4
Requires-Dist: simplibs-validate>=0.3.2
Requires-Dist: simplibs-rules>=0.4.0
Requires-Dist: simplibs-path>=0.1.1
Requires-Dist: simplibs-style>=0.1.0
Provides-Extra: dev
Requires-Dist: pytest>=9.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Dynamic: license-file

# 💬 `simplibs-cli`

[![PyPI](https://img.shields.io/pypi/v/simplibs-cli)](https://pypi.org/project/simplibs-cli/)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
[![Licence](https://img.shields.io/badge/licence-MIT-green)](https://github.com/simplibs/simplibs-cli/blob/main/LICENSE)
[![tests](https://github.com/simplibs/simplibs-cli/actions/workflows/tests.yml/badge.svg)](https://github.com/simplibs/simplibs-cli/actions)

**The conversation with the person at the terminal — asked once, asked right.**

A small Python library that replaces the `while True: input()` loop every command-line program
writes by hand. A prompt **asks again until the answer is acceptable**, so what it returns needs no
second check; a typo is answered with one warning line, a wrong call with a card, and a program with
nobody to ask with a clear message instead of a hang. It starts where `argparse`, `click` or `typer`
hand over the values, and it does nothing but the conversation.

```python
from simplibs.cli import prompt_int, prompt_confirm

workers = prompt_int("Workers", default=4, minimum=1, maximum=64)
# Workers [4]: 100
# Please enter a whole number from 1 to 64.        <- asked again, nothing raised
# Workers [4]:                                     <- Enter: the default

if prompt_confirm("Start?", default=False):        # a hurried Enter means "no"
    print(f"Starting {workers} workers")
```

> Colors come from [`simplibs-style`](https://pypi.org/project/simplibs-style/) and disappear on their
> own when the output is a pipe or a file.

---

## 🧭 The Core Philosophy

Every command-line program that asks a question ends up with the same loop: read a line, strip it,
parse it, check the range, print a complaint, repeat. Each copy forgets something — the empty Enter,
the default that is not an option, the input that has ended and turns into a bare `EOFError` in CI.
`simplibs-cli` writes the loop once, and writes it carefully.

* **Ask until it is usable.** A prompt returns only an acceptable answer — the right type, within the
  limits, one of the options, satisfying the rule. What comes back needs no second check.
* **A typo is not an exception.** A refused answer is explained in one warning line and the question
  is repeated. A *wrong call* — an empty option list, a default that is not an option, limits that
  exclude everything — is the programmer's mistake and raises a card before the first question.
* **A default is a real answer.** Pressing Enter returns it, and it goes through the same checks as
  anything typed, so the result always satisfies what the caller asked for.
* **The command line first.** `resolve_or_prompt` uses a value from the arguments when it is good and
  asks only when it is missing or refused — the program reads the same either way.
* **Nobody there is an answer too.** `is_interactive()` and `require_interactive()` turn "the program
  hangs in CI" into a card that names the option to pass.
* **It starts where the parser stops.** No argument parsing, no logging setup — `resolve_verbosity`
  only returns the level. There are no classes, rules or actions: a prompt is an interaction, not a
  step of a data pipeline.
* **Layers that only look left.** `interaction`, `reporting` and `verbosity` stand alone ← `prompts`
  — every layer imports only from those before it.

```python
prompt_choice("Format", {})                                    # -> raises: nothing to choose from
prompt_int("Workers", minimum=10, maximum=1)                   # -> raises: the limits exclude everything
prompt_choice("Format", {"json": "…"}, default="xml")          # -> raises: the default is not an option
```

---

## 📦 Installation

```bash
pip install simplibs-cli
```

Requires Python 3.11+. The runtime dependencies (installed automatically) are other simplibs
libraries: [`simplibs-exception`](https://pypi.org/project/simplibs-exception/) for the structured
error cards, [`simplibs-validate`](https://pypi.org/project/simplibs-validate/) for `@validate_call` on
the public functions, [`simplibs-rules`](https://pypi.org/project/simplibs-rules/) for the message of
a refusing rule, [`simplibs-path`](https://pypi.org/project/simplibs-path/) for `prompt_path` and
[`simplibs-style`](https://pypi.org/project/simplibs-style/) for colors that disappear outside a
terminal.

---

## 🚀 Quick Start in 60 Seconds

### Level 1: Ask and get a usable answer

```python
from simplibs.cli import prompt_text, prompt_int, prompt_confirm

name    = prompt_text("Project name", default="demo")
workers = prompt_int("Workers", default=4, minimum=1, maximum=64)

if prompt_confirm("Create the project?", default=True):
    print(f"Creating {name} with {workers} workers")
```

Every question shows its default in brackets, and Enter takes it. An answer that is not accepted is
explained in one line and the question comes again.

### Level 2: Choices and paths

```python
from simplibs.cli import prompt_choice, prompt_multi_choice, prompt_path

fmt    = prompt_choice("Format", {"json": "Machine-readable", "text": "For people"}, default="text")
extras = prompt_multi_choice("Extras", {"tests": "Test folder", "docs": "Docs folder"}, allow_empty=True)
target = prompt_path("Output folder", kind="dir", create_if_missing=True)
```

```text
  json - Machine-readable
  text - For people

Format [text]: xml
'xml' is not a valid option. Please try again.
Format [text]:
```

`fmt` is always one of the keys, `extras` is a tuple (possibly empty), and `target` is a normalized
`Path` of a folder that exists — created on the spot when it did not.

### Level 3: The command line first, the question second

```python
import argparse
from pathlib import Path
from simplibs.cli import prompt_path, require_interactive, resolve_or_prompt

parser = argparse.ArgumentParser()
parser.add_argument("--folder")
args = parser.parse_args()

if args.folder is None:
    require_interactive("--folder is missing")     # in CI: a card that names the option, not a hang

folder = resolve_or_prompt(
    args.folder,
    is_valid=lambda value: Path(value).is_dir(),
    prompt=lambda: prompt_path("Folder", kind="dir", must_exist=True),
    invalid_message=f"'{args.folder}' is not a folder.",
)
```

A good `--folder` is used as it is. A bad one is mentioned and replaced by a question. A missing one
is asked for — or, without a terminal, refused with a card.

### Level 4: A whole program — verbosity, a batch and an exit code

```python
import argparse
import logging
from simplibs.cli import report_failures, resolve_verbosity

def process(name: str) -> bool:
    return name.endswith(".txt")                   # stands in for the real work

parser = argparse.ArgumentParser()
parser.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument("files", nargs="*")
args = parser.parse_args()

logging.basicConfig(level=resolve_verbosity(quiet=args.quiet, verbose=args.verbose))

results = [(name, process(name)) for name in args.files]          # (item, succeeded) pairs
ok = report_failures(results, describe=lambda name: f"{name} could not be processed")
raise SystemExit(0 if ok else 1)
```

`-q` shows only errors, `-v` adds progress, `-vv` shows everything. The batch runs to the end, the
failures are listed once, and the exit code follows.

---

## 🛠️ Architecture & Package Structure

```
src/simplibs/cli/
├── interaction/   ◄── Is there somebody to ask? (is_interactive, require_interactive)
├── reporting/     ◄── Output that is not a question (print_choices, report_failures)
├── verbosity/     ◄── Flags to a logging level (resolve_verbosity)
└── prompts/       ◄── The questions (seven prompts and resolve_or_prompt, with the helpers
                       and the diagnostic raisers behind them)
```

The root of the package exports every function, so one import line is all a program needs. The
folders are plain packages for the code's own organization; nothing has to be imported from them.

```python
from simplibs.cli import prompt_text, prompt_path, resolve_or_prompt     # the questions
from simplibs.cli import is_interactive, report_failures, resolve_verbosity
```

---

## 🙋 Prompts — Ask a Person

Seven prompts that ask at a terminal and ask again until the answer is acceptable, plus
`resolve_or_prompt`, which puts one of them behind a value from the command line. They share four
rules: an empty answer takes the default (or is refused), a refused answer is explained in one line
and asked again, a wrong call raises a `ParamError` before the first question, and an input that has
ended raises a `StateError`.

```python
name = prompt_text("Project name", default="demo")
port = prompt_int("Port", minimum=1024, maximum=65535)
ok   = prompt_confirm("Overwrite?", default=False)
```

➡️ [README_CLI_PROMPTS](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_PROMPTS.md)

---

## 🚦 Interaction — Is Anybody There?

Two functions that answer one question before anything is asked: can this program talk to a person?
A person needs a way to answer and a way to read the question, so both standard input and standard
output must be terminals. `is_interactive()` returns the answer; `require_interactive()` turns it
into a card that says why an answer was needed.

```python
if not is_interactive():
    print("Pass --name, there is nobody to ask.")
```

➡️ [README_CLI_INTERACTION](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_INTERACTION.md)

---

## 📋 Reporting — Say Something That Is Not a Question

Two functions for output that is not a question: `print_choices` prints an aligned
`key - description` list (the prompts use it too, so a list looks the same everywhere), and
`report_failures` prints the failed items of a batch and returns whether everything succeeded — ready
for `SystemExit`.

```python
print_choices({"json": "Machine-readable", "text": "For people"}, title="Formats:")
# Formats:
#   json - Machine-readable
#   text - For people
```

➡️ [README_CLI_REPORTING](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_REPORTING.md)

---

## 🔊 Verbosity — Flags to a Level

One function that turns `--quiet` and a counted `-v` / `-vv` into one `logging` level. `quiet` wins
over `verbose`, and a single `-v` never makes the output quieter than the default. It only returns
the level; the program's own logging applies it.

| Flags                | Level                       |
|----------------------|-----------------------------|
| none                 | `WARNING` (the default)     |
| `-v`                 | `INFO`                      |
| `-vv`                | `DEBUG`                     |
| `-q` (with or without `-v`) | `ERROR`              |

➡️ [README_CLI_VERBOSITY](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_VERBOSITY.md)

---

## 📖 Quick Reference

### 🙋 `prompts/` — Ask a Person

| Function              | Description / Parameters                                                                              |
|-----------------------|-------------------------------------------------------------------------------------------------------|
| `prompt_text`         | A line of text, optionally judged by a rule (`message`, `default`, `allow_empty=False`, `rule`, `invalid_message`). |
| `prompt_int`          | A whole number, optionally within limits (`message`, `default`, `minimum`, `maximum`).                |
| `prompt_secret`       | A line typed without being shown (`message`, `allow_empty=False`).                                    |
| `prompt_confirm`      | A yes/no question; always returns a `bool` (`message`, `default`).                                    |
| `prompt_choice`       | One key from a described set (`message`, `options`, `default`).                                       |
| `prompt_multi_choice` | One or more comma-separated keys, in the order typed, as a tuple (`message`, `options`, `default=()`, `allow_empty=False`). |
| `prompt_path`         | A normalized `Path` of a given kind, existing or created (`message`, `kind="any"`, `must_exist=False`, `create_if_missing=False`, `default`, `rule`, `invalid_message`). |
| `resolve_or_prompt`   | Uses a value from the command line, asks only when it is missing or refused (`value`, `is_valid`, `prompt`, `invalid_message`). |

➡️ [README_CLI_PROMPTS](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_PROMPTS.md)

---

### 🚦 `interaction/` — Is Anybody There?

| Function              | Description / Parameters                                                       |
|-----------------------|--------------------------------------------------------------------------------|
| `is_interactive`      | `True` when both standard input and standard output are terminals.             |
| `require_interactive` | Raises `StateError` when there is nobody to ask (`reason=None`).               |

➡️ [README_CLI_INTERACTION](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_INTERACTION.md)

---

### 📋 `reporting/` — Say Something That Is Not a Question

| Function          | Description / Parameters                                                                 |
|-------------------|------------------------------------------------------------------------------------------|
| `print_choices`   | Prints an aligned `key - description` list (`options`, `title=None`).                    |
| `report_failures` | Prints the failed items of a batch, returns whether all succeeded (`results`, `describe`, `title`). |

➡️ [README_CLI_REPORTING](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_REPORTING.md)

---

### 🔊 `verbosity/` — Flags to a Level

| Function            | Description / Parameters                                                                  |
|---------------------|-------------------------------------------------------------------------------------------|
| `resolve_verbosity` | Resolves `--quiet` and `-v` / `-vv` into a logging level (`quiet=False`, `verbose=0`, `default_level=logging.WARNING`). |

➡️ [README_CLI_VERBOSITY](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_VERBOSITY.md)

---

## 🔌 Pipes, CI and Ctrl+C

How the library behaves where there is no person at the keyboard — or where the person gives up:

* **Piped answers work.** `echo y | tool` drives the prompts, and so do tests. The prompts do not check
  for a terminal themselves; whether a *missing* answer is an error is the program's decision, made
  with `require_interactive()`.
* **A dead input is a card.** When the input has ended (`tool < /dev/null`, a CI job), the prompt
  raises `CLI_NO_ANSWER_AVAILABLE` — what was asked, and what to do — instead of a bare `EOFError`.
* **Secrets are not echoed, but they can be piped.** Without a terminal `prompt_secret` falls back to
  standard input with a warning, unhidden. A program that must never read a secret from a pipe checks
  `is_interactive()` first.
* **`Ctrl+C` is never swallowed.** `KeyboardInterrupt` goes up unchanged — it is the user saying
  "stop", and a prompt that caught it would be a trap.
* **Everything goes to standard output.** Questions, warnings, lists and reports share one stream, so
  colors are decided once and never leak into a redirected file.

---

## ⚠️ Exceptions

Every error is a structured [`simplibs.exception`](https://pypi.org/project/simplibs-exception/) card,
and every error name starts with `CLI_`. A wrong call is a `ParamError`, raised before the first
question; a missing terminal or a dead input is a `StateError`. Each error is documented next to the
function that raises it.

| Error name                   | Card         | Wraps          | Raised when                                                        |
|------------------------------|--------------|----------------|--------------------------------------------------------------------|
| `CLI_OPTIONS_EMPTY`          | `ParamError` | `ValueError`   | `prompt_choice` / `prompt_multi_choice` got no options.            |
| `CLI_DEFAULT_NOT_IN_OPTIONS` | `ParamError` | `ValueError`   | A default (or a key of it) is not one of the options.              |
| `CLI_RANGE_EMPTY`            | `ParamError` | `ValueError`   | `prompt_int` got a `minimum` greater than the `maximum`.           |
| `CLI_CREATE_NEEDS_KIND`      | `ParamError` | `ValueError`   | `prompt_path` got `create_if_missing=True` with `kind="any"`.      |
| `CLI_NOT_INTERACTIVE`        | `StateError` | `RuntimeError` | `require_interactive` found input or output that is not a terminal. |
| `CLI_NO_ANSWER_AVAILABLE`    | `StateError` | `EOFError`     | The input ended before a prompt got its answer.                    |

A mistake in the *type* of an argument is refused by `@validate_call` (from
[`simplibs-validate`](https://pypi.org/project/simplibs-validate/)) before the function runs.

---

## 🔗 Related libraries

* **[`simplibs-exception`](https://pypi.org/project/simplibs-exception/)** — the structured cards behind every error here.
* **[`simplibs-validate`](https://pypi.org/project/simplibs-validate/)** — `@validate_call`, which checks the arguments of every public function.
* **[`simplibs-rules`](https://pypi.org/project/simplibs-rules/)** — the `Rule` objects that `prompt_text` and `prompt_path` accept, and whose `describe()` becomes the refusal message.
* **[`simplibs-path`](https://pypi.org/project/simplibs-path/)** — normalizing and creating the path in `prompt_path`, and the path rules to hand it.
* **[`simplibs-style`](https://pypi.org/project/simplibs-style/)** — the colors of warnings, headings and keys, plain outside a terminal.

---

## ☯️ About simplibs

All libraries in the **simplibs** (Simple Libraries) ecosystem share a common
engineering philosophy:

* **Dyslexia-friendly:**
We actively minimize cognitive load. Code is atomized into small, self-contained units,
files are named directly after the logical task they perform, and explanations describe
*why* something is designed, not just *what* it is.
* **Programmer's Zen:**
Nothing should be missing, and nothing should be superfluous. We value clean execution
paths and robust, understandable code architectures over rushed, messy feature sets.
* **Defensive Style:**
We actively anticipate edge cases and failure modes so that only safe operational paths
remain. Our code is built to degrade gracefully rather than crash unexpectedly.
* **Minimalism:**
Find the most direct path to the goal in as few operational steps as possible without
taking shortcuts on safety, readability, or completeness.
* **Code as Craft:**
Code should be pleasant to look at, readable at a glance, and evoke structural harmony.
We treat software engineering as a precision trade.

---

### 🤝 Contributing & Community

This is an **open-source project** built with love and care. We strongly believe in
community collaboration and welcome any feedback, bug reports, or feature ideas!

* **Want to contribute?** Feel free to open an Issue or submit a Pull Request.
* **Want to get in touch?** If you'd like to discuss the project further, collaborate,
  or just say hello, feel free to open a GitHub Issue or start a Discussion.

---

### 📝 License

This library is released under the **MIT License**. Build great things!

---
