Skip to main content

Lightweight helper toolkit for building small CLI (command-line) applications in Python.

Project description

cli_tools_by_oleksa

Python Version License PyPI

A lightweight, zero-dependency toolkit that removes the most annoying boilerplate when writing small interactive console programs in Python.

Perfect for:

  • Students & homework assignments
  • Teachers who are tired of seeing 50-line while True: try: int(input()) loops
  • Anyone building tiny CLI utilities without wanting Click/Typer/Rich

✨ Features

  • valid_input — Base validation function: single-pass flow (RegEx → Validator → Conversion). Raises ValueError on failure with no retry logic.
  • get_valid_input — High-level input handler with automatic retrying, custom if_incorrect messages, and clean, predictable user prompts.
  • choose_from_list — Safe list selection by name or index, case-insensitive, with built-in retry loop and clear error messages.
  • extract_match — Extracts RegEx groups and converts them into the desired types.
  • split — Flexible string splitting using either str or re.Pattern, with optional conversion of each element.
  • print_iterable / print_table — Utilities for clean, formatted output of iterables, pairs and structured data.
  • safe_int / safe_float — Safe numeric converters returning None instead of raising exceptions.
  • Predefined Patterns — Frequently used RegEx patterns: INT, FLOAT, EMAIL, DATE_DMY, NUMBER, and more.
  • Validator Factories — Generator functions for common validations: is_in_range, is_in_list, is_list_of, more, less_or_equal, and other composable checks.
  • safe_run — Context manager for safe execution: catches exceptions, handles Ctrl+C, and ensures a clean exit.
  • try_until_ok — General retry mechanism: repeatedly executes an operation until it succeeds.

Requires Python 3.10+ and has zero external dependencies.


📦 Installation

pip install --upgrade cli_tools_by_oleksa

🛠️ Usage Example

Basic example

New functions used:

  • print_header
  • valid_input
  • get_valid_input

Code:

from cli_tools import print_header,valid_input, get_valid_input
from cli_tools.patterns import INT              # Check if input is integer number
from cli_tools.validators import is_in_range    # Check if input is a number in chosen range

print_header('Basic example', char='=', space=2)

# Without a pattern/validator, accepts any input and applies a converter to it
name = valid_input('Enter your name: ', converter= lambda x: x.strip().capitalize())

# Accepts only integer age, 1 <= age <= 120. Convert input to int
age = get_valid_input('Enter your age: ',
                      pattern=INT,
                      validator=is_in_range(1, 120),
                      converter=int,
                      if_incorrect='Be serious :)')

print(f'Hello, {name}! Your age is {age}. {'You are so young!' if age < 18 else 'How do you like being an adult?'}')

Interactive choice example

New functions used:

  • print_iterable
  • print_table
  • choose_from_list
  • split

Code:

from cli_tools import get_valid_input, print_header
from cli_tools import (print_iterable,          # format and print iterable object
                       print_table,   # format and print iterable object
                       choose_from_list,        # ask for choose an option from the list
                       split)                   # split using str.split or re.split, optionally converts all elements

from cli_tools.patterns import NUMBER       # pattern for a number (int or float)
from cli_tools.validators import is_list_of # check if input is a list of numbers

print_header('| CLI Interactive Choice Demo |')

options = ['Football', 'Music', 'Coding']
# print options using pattern
# there is also a format_iterable function that return string instead of print it
print_iterable(options, '- {}', '\n',
               start='Imagine that you could only pursue one hobby for the next year. What would you choose?\n')
# return entered string if it is in options
hobby = choose_from_list(options,
                         case_sensitive=False,
                         prompt='What is your choice? ',
                         if_incorrect="Sorry, but your input is incorrect. Chose from the list!")

options = ['My hobby','Secret tip']
# print options using pattern
# there is also a format_table function that return string instead of print it
print_table(enumerate(options, start=1), '{}. {}', '\n', start='What is next?\n')
# return index of chosen option
choice = choose_from_list(options, by_number=True, prompt='Enter a number: ',)

match choice:
    case 1: print(f'Your hobby is {hobby}.')
    case 2: print("Secret tip: if you dont have enough money - just find a job.")

# ask for list of numbers until get it
numbers = get_valid_input(prompt='Enter a numbers separated by spaces: ',
                          validator=is_list_of(NUMBER))

# split with conversation: return list of float numbers
number_list = split(numbers, converter=float)

print(f'Sum of these numbers: {sum(number_list)}')
print('Bye!')

Project example

New functions used:

  • extract_match

Code:

import re
from cli_tools import (print_header, get_valid_input,
                       extract_match)   # extract list of all matches, optionally convert every match. [] if no matches
from cli_tools.patterns import NUMBER   # Ready-made pattern: accepts both int and float

# Pattern <number> <operator> <number>, spaces ignored
simple_expr_pattern = re.compile(fr' *({NUMBER.pattern}) *([+\-*/^]) *({NUMBER.pattern}) *')
# Accepts either numbers or operators. Converts numbers to float
converter = lambda x: float(x) if x not in '+-*/^' else x

print_header('| Simple calculator |', '—')
print('Supports simple expressions in format <number> <operator> <number>. Press Ctrl+C to exit.')

while True:
    # Guaranteed to pass only input that matches the pattern
    expr = get_valid_input(prompt='> ', pattern=simple_expr_pattern, if_incorrect='Wrong format!')
    # Unpack the input, immediately converting the numbers
    left, operator, right = extract_match(expr, simple_expr_pattern, converter=converter)

    match operator:
        case '+':
            print(left+right)
        case '-':
            print(left-right)
        case '*':
            print(left*right)
        case '/':
            if right == 0:
                print('Division by zero is not allowed!')
                continue
            print(left/right)
        case '^':
            print(left**right)
        case _:
            print('Wrong operator!')

Error handling features

New functions used:

  • safe_run
  • try_until_ok

Code:

import random, time

from cli_tools.exceptions import (CLIError,         # Base class for all raised errors
                                  APIError,         # Error that raised when you pass invalid data to a function.
                                  ValidationError,  # Data not validated
                                  ConversionError)  # The transferred converter caused an error
from cli_tools import safe_run, try_until_ok, print_header

print_header('Safe Execution Demo')

with safe_run(debug=False, exit_on_error=False):
    print("Press Ctrl+C to test graceful exit, or wait for the error...")

    for i in range(3, 0, -1):
        print(f"Crashing in {i}...")
        time.sleep(1)

    raise CLIError("Something went wrong inside the app!")

print("App still working.")

###

print_header('Retry Logic Demo')

def unstable_network_request():
    """Simulates a connection that fails 70% of the time."""
    if random.random() < 0.7:
        raise ConnectionError("Connection timed out")
    return "200 OK"

print("Attempting to connect to server...")

status = try_until_ok(
    unstable_network_request,
    exceptions=ConnectionError,
    on_exception="Connection failed. Retrying..."
)

print(f"Success! Server response: {status}")

📚 API Reference

🏗️ cli.py

This module contains basic functions for handling user input from the console, formatting and outputting data. No separate import required: all functions are available via import cli_tools

⌨️ Input Handling

Function Description Key Arguments Returns
valid_input() Prompts for input and performs single-pass validation (RegEx + custom validator). Throws ValueError if input is incorrect. Enforces the sequence: Pattern Check → Validator Check → Conversion. prompt: str = ''

pattern: str | re.Pattern = ANY

validator: Callable[[str], bool] = lambda x: True

converter: Callable[[str], Any] = str
The converted input value (Any).
get_valid_input() Wraps valid_input inside a retry loop (try_until_ok). Repeatedly prompts the user until input is valid, displaying if_incorrect on failure. prompt: str = ''

pattern: str | re.Pattern = ANY

validator: Callable[[str], bool] = lambda x: True

converter: Callable[[str], Any] = str

if_incorrect: str = 'Incorrect input!'
The converted input value (Any).
choose_from_list() Prompts the user to select an item from a list (by text or by number), with automatic validation and retries. Numbered choice starts from configurable value (default: 1). options: list[str]

by_number: bool = False

case_sensitive: bool = True

prompt: str = 'Choose option: '

if_incorrect: str = 'Incorrect option!'

start: int = 1
Returns the entered number (int) if by_number=True, or the matching option (str) otherwise.

🖨️ Output Formatting

Function Description Key Arguments Notes
print_header() Prints a centralized header with decorative lines above and below, using a customizable symbol, adjustable total width and side spacing. header: str

char: str='~'

width: int | None = None

spaces: int = 0
Useful for creating clean section titles and menus in console apps.
print_iterable() Convenient formatted output of any iterable object. iterable: Iterable[Any]

item_pattern: str = '{}'

, join_by: str = '\n'

, start: str = '', end: str = ''
Formats each item using item_pattern.format(item) and then prints start+join_by.join(formated_items)+end.
print_table() Convenient formatted output for iterables containing unpackable pairs (e.g., tuples or lists). iterable: Iterable[Iterable[Any]], item_pattern: str = '{}: {}', join_by: str='\n', start: str = '', end: str = '' Formats each pair using item_pattern.format(*item) and then prints start+join_by.join(formated_items)+end. Ideal for items from zip()ordict.items()`.

utils.py

This module contains essential helper functions for safe execution, safe type conversion, string manipulation, and data extraction using regular expressions. No separate import required: all functions are available via import cli_tools

Safe Execution

Context Manager: safe_run

A context manager designed to wrap sections of code (or the entire application entry point) to prevent program crashes due to unhandled exceptions, and to ensure graceful handling of user interruptions (Ctrl+C).

Parameter Type Default Description
debug bool False If True, the full Python traceback is printed upon an error. If False, only a brief error message is shown.
exit_on_error bool True If True, the program exits immediately (status code 1) when an exception is caught. If False, the execution continues after the with block.
Intercepts Catches all general exceptions (Exception) and KeyboardInterrupt (Ctrl+C), ensuring a graceful exit (status code 0) in the latter case.

Function: try_until_ok

Repeatedly executes a function until it succeeds (completes without raising a specified exception). This is the core retry mechanism used by get_valid_input and choose_from_list.

Parameter Type Default Description
func Callable The function to execute repeatedly.
*args, **kwargs Any Positional and keyword arguments passed directly to func.
exceptions tuple[Type] | Type Exception The exception type(s) to catch. If a function raises one of these, it will retry. If it raises anything else, the program will crash (as intended).
on_exception str | Callable | None None The action to take when a caught error occurs before retrying:
- str: The message to print.
- Callable: A function to call with the exception object (Callable[[BaseException], Any]).
Handles Gracefully intercepts KeyboardInterrupt (Ctrl+C) to exit the program (status code 0).

Safe Converters

These functions safely convert strings to numeric types without raising exceptions (ValueError).

Function Description Returns Notes
safe_int(string) Safely converts a string to an integer. int if conversion succeeds, else None.
safe_float(string) Safely converts a string to a float (number with decimal point). float if conversion succeeds, else None. Used by numeric validators (is_in_range, more, etc.).

String and RegEx Processing

Function Description Key Arguments Returns
split() Splits an input string into a list of elements using a specified delimiter, and optionally converts each element. string: str

split_by: None | str | re.Pattern = None

converter: Callable[[str], Any] | None = None
List of parsed and optionally converted elements.
extract_match() Performs a RegEx search (re.search) and returns a list where each element corresponds to a captured group in the pattern. string: str

pattern: re.Pattern

pos: int = 0

endpos: int = sys.maxsize,

converter: Callable[[str], Any] | None = None`
List of captured groups, optionally converted. Returns [] if no match found.

String Formatting

These functions implement the core formatting logic used by print_iterable and print_table. They return strings instead of printing them, offering flexibility for logging or file writing.

Function Description Key Arguments Returns
format_iterable() Formats elements of a one-dimensional iterable into a single string. iterable: Iterable[Any]

item_pattern: str = '{}'

join_by: str = '\n'

start: str = ''

end: str = ''
The formatted str.
format_table() Formats unpackable elements (pairs, tuples) into a single string using *item unpacking syntax. iterable: Iterable[Any]

item_pattern: str = '{}'

join_by: str = '\n'

start: str = ''

end: str = ''
The formatted str.

patterns.py

This module contains a set of precompiled regular expressions (re.Pattern) for common data formats and helper functions.

Constant Description Match Example
ANY Matches any string (including empty ones). "", "text", "123"
INT Integer number (optionally prefixed with + or -). 10, -5, +42
FLOAT Floating-point number (must contain a decimal point). 3.14, -0.01
NUMBER Universal number (matches both INT and FLOAT). 42, -3.14, 0
USERNAME Variable name or identifier... user_01, varName
EMAIL Basic email format. user@example.com
DATE_DMY Date in DD.MM.YYYY format. 31.12.2023
DATE_YMD Date in YYYY-MM-DD format. 2023-12-31
TIME_24H Time in HH:MM format (24-hour clock). 14:30, 09:05, 23:59

Helper Functions

  • in_line(pattern: re.Pattern): Wraps the provided pattern with start (^) and end ($) anchors.
  • in_group(pattern: re.Pattern): Wraps the provided pattern in a capture group (...).

validators.py

This module contains validator factories. These functions return a validator (Callable[[str], bool]) that is passed as the validator argument to valid_input or get_valid_input.

Lists & Collections

  • is_list_of(pattern, split_by=None): Generates a validator that checks if a string is a list of elements with a specific format.
  • is_in_list(options, case_sensitive=True): Generates a validator that checks if the input string exists in a provided list of options.

Numeric Comparisons

Function Logic Description
is_in_range(start, end) start <= x <= end Checks if a number is within the inclusive closed range ([start, end]).
is_between(start, end) start < x < end Checks if a number is within the exclusive open interval ((start, end)).
more(limit) x > limit Checks if the number is strictly greater than the limit (exclusive).
more_or_equal(limit) x >= limit Checks if the number is greater than or equal to the limit (inclusive).
less(limit) x < limit Checks if the number is strictly less than the limit (exclusive).
less_or_equal(limit) x <= limit Checks if the number is less than or equal to the limit (inclusive).

exceptions.py

This module provides the core exception classes used across the library. A clear exception hierarchy ensures that users can precisely handle user input errors (ValidationError) separately from programmer errors (APIError, ConversionError).

Hierarchy

The exceptions are structured with CLIError as the base class, allowing users to catch all library-related errors easily.

Class Inherits From Description Responsibility
CLIError Exception Base class for all custom exceptions raised by the cli_tools library. General Catch-all
APIError CLIError Raised when a function is called with invalid or impossible arguments (e.g., passing an empty list to choose_from_list). Programmer Error (API Usage)
ValidationError CLIError Raised when the user input is invalid (does not match the RegEx pattern or fails the custom validator). User Input Error (Caught for Retries)
ConversionError CLIError Raised when the designated converter function fails (e.g., int() cannot process a string). Crucially: The valid_input function internally catches standard Python exceptions (ValueError, TypeError) and re-raises them as ConversionError. This signals a programmer error in logic, and it is NOT automatically caught for retries by get_valid_input. Programmer Error (Conversion Logic)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cli_tools_by_oleksa-1.3.0.tar.gz (14.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cli_tools_by_oleksa-1.3.0-py3-none-any.whl (16.9 kB view details)

Uploaded Python 3

File details

Details for the file cli_tools_by_oleksa-1.3.0.tar.gz.

File metadata

  • Download URL: cli_tools_by_oleksa-1.3.0.tar.gz
  • Upload date:
  • Size: 14.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for cli_tools_by_oleksa-1.3.0.tar.gz
Algorithm Hash digest
SHA256 a682c5e09d91cce58bdc35c56ebaded3ad13325c04ccb9212d2b5ba7c70c84dc
MD5 0dfb180b3394fadb140f35ba17bf0012
BLAKE2b-256 fbb600356f7590214913cfa8115e4172b4e5824a18d5048f29b1959fbd375faf

See more details on using hashes here.

File details

Details for the file cli_tools_by_oleksa-1.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for cli_tools_by_oleksa-1.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f0d76d501208c681e86d8a32d29eada14bb70d4ca72c414b4fcc4c0fabf0494d
MD5 31d5474932b791c437f7052738a0ef9b
BLAKE2b-256 c68b8f75f7a8cccc9b5b9f127f38e5d09951213e480cd77938f83e2da98ec8df

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page