💬 simplibs-cli
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.
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-styleand 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_promptuses 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()andrequire_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_verbosityonly 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,reportingandverbositystand alone ←prompts— every layer imports only from those before it.
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
pip install simplibs-cli
Requires Python 3.11+. The runtime dependencies (installed automatically) are other simplibs
libraries: simplibs-exception for the structured
error cards, simplibs-validate for @validate_call on
the public functions, simplibs-rules for the message of
a refusing rule, simplibs-path for prompt_path and
simplibs-style for colors that disappear outside a
terminal.
🚀 Quick Start in 60 Seconds
Level 1: Ask and get a usable answer
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
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)
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
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
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.
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.
name = prompt_text("Project name", default="demo")
port = prompt_int("Port", minimum=1024, maximum=65535)
ok = prompt_confirm("Overwrite?", default=False)
🚦 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.
if not is_interactive():
print("Pass --name, there is nobody to ask.")
📋 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.
print_choices({"json": "Machine-readable", "text": "For people"}, title="Formats:")
# Formats:
# json - Machine-readable
# text - For people
🔊 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 |
📖 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). |
🚦 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). |
📋 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). |
🔊 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). |
🔌 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 | tooldrives 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 withrequire_interactive(). - A dead input is a card. When the input has ended (
tool < /dev/null, a CI job), the prompt raisesCLI_NO_ANSWER_AVAILABLE— what was asked, and what to do — instead of a bareEOFError. - Secrets are not echoed, but they can be piped. Without a terminal
prompt_secretfalls back to standard input with a warning, unhidden. A program that must never read a secret from a pipe checksis_interactive()first. Ctrl+Cis never swallowed.KeyboardInterruptgoes 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 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) before the function runs.
🔗 Related libraries
simplibs-exception— the structured cards behind every error here.simplibs-validate—@validate_call, which checks the arguments of every public function.simplibs-rules— theRuleobjects thatprompt_textandprompt_pathaccept, and whosedescribe()becomes the refusal message.simplibs-path— normalizing and creating the path inprompt_path, and the path rules to hand it.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!
Metadata
Release files for simplibs-cli 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| simplibs_cli-0.1.0.tar.gz | 36.8 kB | Without provenance | Details for simplibs_cli-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| simplibs_cli-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for simplibs_cli-0.1.0-py3-none-any.whl |
Total release size: 80.9 kB
Release files / simplibs_cli-0.1.0.tar.gz
| Download URL | simplibs_cli-0.1.0.tar.gz |
|---|---|
| Size | 36.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b2c6889aefd4c8e89e18ebacaf088e047851211c9cac7ec4957ef95bea297a18
|
|
BLAKE2b-256 checksum How to use checksums |
aa808e71582db47b88ab4aa6cac4dcb40799bbb9b1693a20947d519569f11e22
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / simplibs_cli-0.1.0-py3-none-any.whl
| Download URL | simplibs_cli-0.1.0-py3-none-any.whl |
|---|---|
| Size | 44.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
869f6a1bcaaa2277027cb9d20553b23d0a3de672b07ac39ab9e1a11d15e119a3
|
|
BLAKE2b-256 checksum How to use checksums |
20e4a9618e269645e6fe5b6a3785644df1c6e4528b19e9844f524c5c0d4a92c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Security for simplibs-cli 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.