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 Version

Lightweight helper toolkit for building robust and interactive CLI (command-line) applications in Python.

The library simplifies common CLI tasks such as:

  • Safe Input & Validation: Handling user input with built-in retry mechanisms, type conversion, and defined validation rules.
  • Error Management: Ensuring graceful execution and clean shutdown using exception handlers and context managers.
  • Data Processing: Splitting, converting, and extracting structured data from raw input strings using focused utilities.
  • Formatted Output: Providing clean, structured, and customizable console displays for lists and tables.

It is good for educational scripts, training exercises, small utilities, and simple CLI apps where readability, robustness, and speed of development matter more than complex frameworks.


✨ 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_zipped_iterable — 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')

# 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?'}')

Output:

~~~~~~~~~~~~~
Basic example
~~~~~~~~~~~~~
Enter your name:   oLeKSa
Enter your age: 0
Be serious :)
Enter your age: 1000
Be serious :)
Enter your age: 19
Hello, Oleksa! Your age is 19. How do you like being an adult?

Process finished with exit code 0

Interactive choice example

New functions used:

  • print_iterable
  • print_zipped_iterable
  • 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_zipped_iterable,   # 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_zipped_iterable function that return string instead of print it
print_zipped_iterable(enumerate(options, start=1), '{}. {}', '\n', start='What next?\n')
# return index of chosen option
choice = choose_from_list(options, by_number=True, prompt='Enter a number: ',)

match choice:
    case 0:
        print(f'Your hobby is {hobby}.')
    case 1:
        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!')

Output:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| CLI Interactive Choice Demo |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imagine that you could only pursue one hobby for the next year. What would you choose?
- Football
- Music
- Coding
What is your choice? IDK
Sorry, but your input is incorrect. Chose from the list!
What is your choice? music
What next?
1. My hobby
2. Secret tip
Enter a number: 2
Secret tip: if you dont have enough money - just find a job.
Enter a numbers separated by spaces: 12 8 23
Sum of these numbers: 43.0
Bye!

Process finished with exit code 0

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!')

Output:

—————————————————————
| Simple calculator |
—————————————————————
Supports simple expressions in format <number> <operator> <number>. Press Ctrl+C to exit.
> 2+2
4.0
> 5/0
Division by zero is not allowed!
> 3     ^  2
9.0
> banana
Wrong format!
> 1.25 * 8
10.0
> 
Process finished with exit code 0

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}")

Output:

~~~~~~~~~~~~~~~~~~~
Safe Execution Demo
~~~~~~~~~~~~~~~~~~~
Press Ctrl+C to test graceful exit, or wait for the error...
Crashing in 3...
Crashing in 2...
Crashing in 1...
Error: Something went wrong inside the app!
App still working.
~~~~~~~~~~~~~~~~
Retry Logic Demo
~~~~~~~~~~~~~~~~
Attempting to connect to server...
Connection failed. Retrying...
Connection failed. Retrying...
Connection failed. Retrying...
Success! Server response: 200 OK

Process finished with exit code 0

📚 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 for a choice from a list of options. Automatically handles validation and retries internally using try_until_ok. options: list[str]

by_number: bool = False

case_sensitive: bool = True

prompt: str = 'Choose option: '

if_incorrect: str = 'Incorrect option!'
If by_number is True, returns the 0-based index (int). Otherwise, returns the chosen option string (str).

🖨️ Output Formatting

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

symbol: str='~'
Useful for creating clean section titles in the console.
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_zipped_iterable() 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_zipped_iterable. 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_zipped_iterable() 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.2.0.tar.gz (14.4 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.2.0-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: cli_tools_by_oleksa-1.2.0.tar.gz
  • Upload date:
  • Size: 14.4 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.2.0.tar.gz
Algorithm Hash digest
SHA256 d80b0565d6d79523a6008adf2cc40ac7466ac911a992428ad71002fc024e786a
MD5 8ad0fbc2cd11087e11827b9169000afa
BLAKE2b-256 4757a042c462d28090f1be870a59b23163376be83594c8a383b6eed97124ba62

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for cli_tools_by_oleksa-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9677c36c386318df0bbb846936a971c7017bcd9365e89e990a0520c1c0123ef5
MD5 13cc2196cd2a9a80a08b917cec2cd1df
BLAKE2b-256 40722df0ba4ebdf3d8563b5dc89d784c3a0e78604613afd506d7aab502c1c382

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