# cli_tools_by_oleksa

[![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-1.1.0-orange.svg)](https://pypi.org/project/cli_tools_by_oleksa/)

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

---

## 🔗 References

- **📘 Documentation**  
  Complete API reference and module descriptions:  
  → [API Reference](#-api-reference)  
  → [Core Input/Output](#%EF%B8%8F-module-core-inputoutput-cli_toolsiov)  
  → [Utilities](#%EF%B8%8F-module-utilities-cli_toolsutils)  
  → [Patterns](#-module-patterns-cli_toolspatterns)

- **🧪 Examples**  
  Ready-to-run usage demonstrations:  
  → [Basic example](#basic-example)  
  → [Interactive choice example](#interactive-choice-example)  
  → [Project example](#project-example)  
  → [Error handling features](#error-handling-features)

---

## 📦 Installation
```bash
pip install --upgrade cli_tools_by_oleksa
```

---

## 🛠️ Usage Example
### Basic example
#### New functions used:
- print_header
- valid_input

Code:
```Python
from cli_tools import print_header, valid_input, get_valid_input
from cli_tools.validators import is_in_range

print_header('Basic example')

name = valid_input('Enter your name: ', converter= lambda x: x.strip().capitalize())
age = get_valid_input('Enter your age: ',
                      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:
```commandline
~~~~~~~~~~~~~
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:
```Python
from cli_tools import get_valid_input, print_header
from cli_tools import print_iterable, print_zipped_iterable, choose_from_list, split

from cli_tools.patterns import NUMBER
from cli_tools.validators import is_list_of

print_header('| CLI Interactive Choice Demo |')

options = ['Football', 'Music', 'Coding']
print_iterable(options, '- {}', '\n',
               start='Imagine that you could only pursue one hobby for the next year. What would you choose?\n')
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_zipped_iterable(enumerate(options, start=1), '{}. {}', '\n', start='What next?\n')
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.")

numbers = get_valid_input(
    prompt='Enter a numbers separated by spaces: ',
    validator=is_list_of(NUMBER))
number_list = split(numbers, converter=float)
print(f'Sum of these numbers: {sum(number_list)}')

print('Bye!')
```

Output:
```commandline
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 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:
```Python
import re
from cli_tools import print_header, extract_match, get_valid_input
from cli_tools.patterns import NUMBER

simple_expr_pattern = re.compile(fr' *({NUMBER.pattern}) *([+\-*/^]) *({NUMBER.pattern}) *')
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:
    expr = get_valid_input(prompt='> ', pattern=simple_expr_pattern, if_incorrect='Wrong format!')
    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:
```commandline
—————————————————————
| 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:
```Python
import random, time
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 RuntimeError("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:
```commandline
~~~~~~~~~~~~~~~~~~~
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

## 🏗️ Module: Core Input/Output (`cli_tools.io`)

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 = ''`<br/><br/>`pattern: str \| re.Pattern = ANY`<br/><br/>`validator: Callable[[str], bool] = lambda x: True`<br/><br/>`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 = ''`<br/><br/>`pattern: str \| re.Pattern = ANY`<br/><br/>`validator: Callable[[str], bool] = lambda x: True`<br/><br/>`converter: Callable[[str], Any] = str`<br/>`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]`<br/><br/>`by_number: bool = False`<br/><br/>`case_sensitive: bool = True`<br/><br/>`prompt: str = 'Choose option: '`<br/><br/>`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`<br/><br/>`symbol: str='~'` | Useful for creating clean section titles in the console.                                                      |
| `print_iterable()`       | Convenient formatted output of any iterable object. | `iterable: Iterable[Any]`<br/><br/>`item_pattern: str = '{}'`<br/><br/>, `join_by: str = '\n'`<br/><br/>, `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()` or `dict.items()`.        |


---

## ⚙️ Module: Utilities (`cli_tools.utils`)

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

### 🔄 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`<br/><br/> `split_by: None \| str \| re.Pattern = None`<br/><br/>`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`<br/><br/> `pattern: re.Pattern`<br/><br/>`pos: int = 0`<br/><br/>`endpos: int = sys.maxsize,`<br/><br/>converter: Callable[[str], Any] \| None = None`                                                                     | List of captured groups, optionally converted. Returns `[]` if no match found. |


---

## 🧩 Module: Patterns (`cli_tools.patterns`)

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

---

### ✅ Module: Validators (`cli_tools.validators`)

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 Ranges
| Function | Logic | Description |
| :--- | :--- | :--- |
| **`is_in_range(start, end)`** | `start <= x <= end` | Checks if a number is within the **inclusive** range. |
| **`is_between(start, end)`** | `start < x < end` | Checks if a number is within the **exclusive** (open) interval. |

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


---

## 🛡️ Module: Error Handling and Guards (`cli_tools.exceptions`)

This module provides core utilities for safe execution, graceful shutdown, and retry logic.

### 🛑 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: <br/> **- `str`**: The message to print. <br/> **- `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). |