Skip to main content

💬 simplibs-cli

PyPI Python Licence tests

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-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.
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)

➡️ README_CLI_PROMPTS


🚦 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.")

➡️ README_CLI_INTERACTION


📋 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

➡️ README_CLI_REPORTING


🔊 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


📖 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


🚦 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


📋 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


🔊 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


🔌 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 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.


  • simplibs-exception — the structured cards behind every error here.
  • simplibs-validate — @validate_call, which checks the arguments of every public function.
  • simplibs-rules — the Rule objects that prompt_text and prompt_path accept, and whose describe() becomes the refusal message.
  • simplibs-path — normalizing and creating the path in prompt_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)

Source distribution for simplibs-cli 0.1.0
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)

Table of built distributions (wheels) for simplibs-cli 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page