Metadata-Version: 2.4
Name: climax-prompt
Version: 0.7.80
Summary: Prompt for input via the CLI.
License-Expression: MIT
License-File: LICENSE.txt
Keywords: cli,prompt,command-line,command-line-interface,user-input,terminal
Author: Snap
Author-email: XxSnapperGeexX@gmail.com
Requires-Python: >=3.13
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Project-URL: Documentation, https://snap-climax.gitlab.io/python-prompt/
Project-URL: Homepage, https://gitlab.com/snap-climax/python-prompt
Project-URL: Repository, https://gitlab.com/snap-climax/python-prompt
Project-URL: issues, https://gitlab.com/snap-climax/python-prompt/-/issues
Description-Content-Type: text/markdown

# Climax Prompt

Python package for creating cli prompts.

[![python badge]][python website]
[![pytest badge]][pytest website]
[![ruff badge]][ruff website]
[![sphinx badge]][sphinx website]
[![poetry badge]][poetry website]
[![pypi badge]][pypi website]
[![git badge]][git website]
[![gitlab badge]][gitlab repo]
[![codeberg badge]][codeberg repo]
[![github badge]][github repo]
[![mit shield]][mit license]
[![pipeline status shield]][pipeline url]

This package exports 2 classes that contain the majority of its primary
functionality. The `Prompt` and `StringPrompt` classes. The main difference
between the 2 is that `StringPrompt`s only processes strings while `Prompt`s
can process any arbitrary type.

The `StringPrompt.exec_string_input_loop` and `Prompt.exec_input_loop` methods
of each class create a loop that prompts a user for input and will reprompt
until either valid input is inputted or an exception is raised when converting
the input `string`. If valid input is inputted then the inputted string or value
(if the string is converted to a non string value) is returned.

## Usage Examples

### `StringPrompt` Usage Example

```python
from climax.prompt import StringPrompt, StringPromptInput


# string validator gets passed a tuple containing the formatted and original
# unformatted string input
def is_palindrome(string: StringPromptInput) -> str | None:
    return (
        f'Provided input is not a palindrome: "{string.original}"\n'
        if len(string.formatted) == 0 or string.formatted != string.formatted[::-1]
        else None
    )


palindrome_prompt = StringPrompt(
    "Input a palindrome...\n",
    string_validator=is_palindrome,
    formatter=lambda a_string: a_string.strip().lower(),
    ps1=">>> ",
)

# once validation passes the formatted and original unformatted string input
# gets returned
formatted_palindrome_string_input, original_palindrome_string_input = palindrome_prompt.exec_string_input_loop()

print(f'You inputted the palindrome: "{formatted_palindrome_string_input}"')
```

Calling the `StringPrompt.exec_string_input_loop` method will result in the
following process:

1. The `StringPrompt.message` to be printed to stdout followed by the
   `StringPrompt.ps1` if set.

1. The string inputted to stdin then gets passed to the
   `StringPrompt.formatter(str)`.

1. The formatted and raw unformatted original input string then gets passed to
   the `StringPrompt.string_validator`.

1. If validation passes (the `StringPrompt.string_validator` returns `None`)
   then both the formatted and original unformatted string input gets returned.

1. If validation fails (the `StringPrompt.string_validator` returns a
   `string` error message) then the `string` error message gets printed to
   stderr and the process is repeated.

When the method in the example above is called this will result in the following
prompt in the terminal:

```text
Input a palindrome...
>>> slITher                                   # simulated user input
Provided input is not a palindrome: "slITher" # original unformatted string used in error message
Input a palindrome...
>>> levEl                                     # simulated user input
You inputted the palindrome: "level"          # formatted input used for validation and in this message
```

### `Prompt` Usage Example

```python
from typing import assert_never

from climax.prompt import (
    Prompt,
    PromptResultFailure,
    PromptResultSuccess,
    StringPromptInput,
)


# string validator gets passed a tuple containing the formatted and original
# unformatted string input
def string_is_integer(string: StringPromptInput) -> str | None:
    start_index = 1 if string.formatted.startswith("-") or string.formatted.startswith("+") else 0

    return (
        None
        if string.formatted[start_index:].isdecimal()
        else f'Provided input is not an integer: "{string.formatted}"\n'
    )


# validator gets passed whatever the string input gets converted to
def is_positive_even_integer(integer: int) -> str | None:
    if integer <= 0:
        return f"Integer isn't positive: {integer}\n"

    if integer % 2 != 0:
        return f"Integer isn't even: {integer}\n"

    return None


positive_even_integer_prompt = Prompt[int].create(
    "Input a positive even integer: ",
    int,  # can pass any function/lambda that consumes a string and outputs the specified type
    validator=is_positive_even_integer,
    string_validator=string_is_integer,
    formatter=str.strip,
)

positive_even_integer_prompt_result = positive_even_integer_prompt.exec_input_loop()

# The result is a union of two classes. The type checker narrows the type in
# each case, so `value` is only available on the successful conversion and
# `conversion_exception` is only available on the failed conversion.
match positive_even_integer_prompt_result:
    case PromptResultFailure(
        original_input_string=original_input_string,
        conversion_exception=conversion_exception,
    ):
        print(f'Error converting input to an int: "{original_input_string}"\n\n{repr(conversion_exception)}')
    case PromptResultSuccess(value=value):
        print("You inputted the positive even integer:", value)
    case _:
        assert_never(positive_even_integer_prompt_result)
```

Calling the `Prompt.exec_input_loop` method will result in the same process
outlined above when `StringPrompt.exec_string_input_loop` is called, except it
performs additional conversion of the string input and validation on that
converted string input as outlined below ***if*** string input validation passes
(otherwise the reprompt process is identical):

1. The *formatted* validated string input is passed to the
   `Prompt.converter(str)`.

1. If an exception occurs during conversion, then a `PromptResultFailure`
   is returned with the original input string and the `Exception` that was raised.

1. If conversion succeeds, the *converted* string input value is then passed to
   the `Prompt.validator`.

1. If validation passes (the `Prompt.validator` returns `None`) then a
   `PromptResultSuccess` containing the original input string and
   its converted value is returned.

1. If validation fails (the `Prompt.validator` returns a `string` error
   message) then the error message gets printed to stderr and the process is
   repeated.

When the method in the example above is called this will result in the following
prompt in the terminal:

```text
Input a positive even integer:    sliTHer          # simulated user input
Provided input is not an integer: "slither"        # formatted string input is used in this message
Input a positive even integer: -34                 # simulated user input
Integer isn't positive: -34
Input a positive even integer: 7                   # simulated user input
Integer isn't even: 7
Input a positive even integer: 22                  # simulated user input
You inputted the positive even integer: 22
```

## Installing Package

This package is [available on PyPI][pypi website] and can be installed via any of the standard
methods:

```sh
# via poetry
poetry add climax-prompt

# via uv
uv add climax-prompt

# via pip
pip install climax-prompt
```

## Repo

The next sections cover details and info about this git repository. If you're
just importing this package as a dependency in a python project then it probably
doesn't apply to you. If you clone this repo, especially if you intend to make a
pr, then they're relevant.

### Tooling Tasks

The repo for this project uses [ruff][ruff website] and [mypy][mypy website] for
linting, [ruff][ruff website] for formatting, [pytest][pytest website] for
testing, and [sphinx][sphinx website] (with [numpydoc][numpydoc website]) for
generating API documentation.

All repo tasks can be executed via the `make` targets listed below:

- `all` - Run build.
- `bandit` - Scan source code for security issues with bandit.
- `build` - Build distributable artifacts.
- `clean` - Remove build output and generated docs files.
- `clean-all` - Run clean and clean-cache.
- `clean-cache` - Remove tool caches and `__pycache__` directories.
- `format` - Fix lint issues and format code with ruff.
- `lint` - Run all lint and type checks.
- `mypy` - Type check with mypy.
- `poetry-check` - Validates content of `pyproject.toml` and `poetry.lock`.
- `readme` - Generate `source/README.rst` by using [pandoc] to convert the root
  `README.md` to rst.
- `ruff-check` - Lint with ruff.
- `ruff-format-check` - Check formatting with ruff.
- `serve-docs` - Build and serve html API docs on `127.0.0.1:8000`.
- `serve-tests` - Build and serve html test results and coverage reports on
  `127.0.0.1:8000` and `127.0.0.1:8001`. Depends on [parallel].
- `setup` - Install dependencies and pre-commit hooks. This should be run right
  after cloning the repo.
- `smoke` - Install the built wheel and sdist in clean venvs and run smoke
  tests. The wheel and sdist artifacts must be built before running this target.
- `test` - Run tests with terminal results and coverage report.
- `test-html` - Run tests with html results and coverage reports.
- `test-xml` - Run test with xml results and coverage reports.

In addition to the `make` targets listed above, all sphinx `make` targets are
available and valid as well.

### Remotes

The primary remote repo for this package is hosted on [gitlab][gitlab repo], but
is also mirrored to [codeberg][codeberg repo] and [github][github repo].

## MIT License

This package is licensed under the MIT license and can be found in
[LICENSE.txt][mit license].

[pandoc]: https://pandoc.org/ "pandoc"
[parallel]: https://www.gnu.org/software/parallel/ "GNU Parallel"
[python badge]: https://img.shields.io/badge/python-%3E%3D3.13-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54&labelColor=3670A0&color=939393 "Python"
[python website]: https://www.python.org/ "Python"
[pytest badge]: https://img.shields.io/badge/pytest-%23ffffff.svg?style=for-the-badge&logo=pytest&logoColor=2f9fe3 "PyTest"
[pytest website]: https://docs.pytest.org/en/stable/ "PyTest"
[ruff badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=for-the-badge "Ruff"
[ruff website]: https://docs.astral.sh/ruff/ "Ruff"
[sphinx website]: https://www.sphinx-doc.org/ "Sphinx"
[sphinx badge]: https://img.shields.io/badge/Made%20with-Sphinx-1f425f.svg?style=for-the-badge "Sphinx"
[numpydoc website]: https://numpydoc.readthedocs.io/en/latest/ "numpydoc"
[mypy website]: https://mypy-lang.org/ "mypy"
[poetry badge]: https://img.shields.io/badge/Poetry-%233B82F6.svg?style=for-the-badge&logo=poetry&logoColor=0B3D8D "Poetry"
[poetry website]: https://python-poetry.org/ "Poetry"
[pypi badge]: https://img.shields.io/badge/pypi-%23ececec.svg?style=for-the-badge&logo=pypi&logoColor=1f73b7 "PyPI"
[pypi website]: https://pypi.org/project/climax-prompt/ "PyPI"
[git badge]: https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white "git"
[git website]: https://git-scm.com/ "git"
[gitlab badge]: https://img.shields.io/badge/gitlab-%23181717.svg?style=for-the-badge&logo=gitlab&logoColor=white "GitLab"
[gitlab repo]: https://gitlab.com/snap-climax/python-prompt "GitLab repo"
[codeberg badge]: https://img.shields.io/badge/Codeberg-%232185D0.svg?style=for-the-badge&logo=Codeberg&logoColor=white "Codeberg"
[codeberg repo]: https://codeberg.org/SnapperGee/climax-prompt-python "Codeberg repo"
[github badge]: https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white "GitHub repo"
[github repo]: https://github.com/SnapperGee/climax-prompt-python "GitHub repo"
[mit shield]: https://img.shields.io/badge/license-MIT-green?style=for-the-badge "MIT"
[mit license]: https://gitlab.com/snap-climax/python-prompt/-/blob/main/LICENSE.txt "MIT"
[pipeline status shield]: https://img.shields.io/gitlab/pipeline-status/snap-climax/python-prompt?branch=main&style=for-the-badge "Pipeline status"
[pipeline url]: https://gitlab.com/snap-climax/python-prompt/-/pipelines/main/latest "Pipeline"

