Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve terminal effects #3

Merged
merged 2 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 148 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,148 @@
# hydraclick
Develop CLI tools with top-notch config management like a boss
# Hydraclick

Hydraclick is an open-source Python package that seamlessly integrates [Hydra](https://hydra.cc/) and [Click](https://click.palletsprojects.com/) to create production-grade command-line interfaces (CLIs). It leverages Hydra's powerful configuration management with Click's user-friendly CLI creation to provide a robust foundation for building complex CLI applications.

## Features

- **Configuration Management**: Utilize Hydra's dynamic configuration capabilities for flexible CLI options.
- **Structured Logging**: Integrate with flogging for structured and efficient logging.
- **Easy Decorators**: Simple decorators to convert functions into CLI commands.
- **Extensible**: Easily extend and customize to fit your project's needs.
- **Shell Completion**: Support for generating shell completion scripts.

## Installation

Install Hydraclick via pip:

```bash
pip install hydraclick
```

## Getting Started

### Basic Usage

Define your function and decorate it with `@hydra_command` to create a CLI command.

```python
from omegaconf import DictConfig
from hydraclick import hydra_command

@hydra_command(config_path="config", config_name="my_config")
def my_function(config: DictConfig):
print(f"Running with config: {config.pretty()}")
```

### Running the CLI

After defining your function, you can run it from the command line:

```bash
python my_script.py --config-path path/to/config --config-name my_config
```

### Example

Here's a complete example of creating a CLI with Hydraclick:

```python
import sys
from omegaconf import DictConfig
from hydraclick import hydra_command

@hydra_command(config_path="configs", config_name="app_config", run_mode="kwargs")
def main(**kwargs):
print(f"Running with config: {kwargs}")

if __name__ == "__main__":
main()
```


## API Reference

### `hydra_command`

Decorator to create CLI commands.

```python
def hydra_command(
config_path: str | Path | None = None,
config_name: str | None = "config",
version_base: str | None = None,
run_mode: str = "config",
preprocess_config: Callable[[DictConfig], DictConfig] | None = None,
print_config: bool = True,
resolve: bool = True,
use_flogging: bool = True,
**flogging_kwargs: Any,
) -> Callable:
```

## Configuration Options

Hydraclick provides several configuration options to customize your CLI:

- `config_path`: Path to the configuration directory. Passed to [`hydra.main()`](https://hydra.cc/docs/tutorials/basic/your_first_app/config_file/)
- `config_name`: Name of the configuration file. Passed to [`hydra.main()`](https://hydra.cc/docs/tutorials/basic/your_first_app/config_file/)
- `version_base`: Base version of the configuration. Passed to [`hydra.main()`](https://hydra.cc/docs/tutorials/basic/your_first_app/config_file/)
- `run_mode`: Mode to run the function (`"config"` or `"kwargs"`).
- `"config"`: Pass the configuration as a single `OmegaConf.DictCondig` object.
- `"kwargs"`: Resolve the `OmegaConf.DictConfig` objet into a python `dict` and pass it as keyword arguments.
- `preprocess_config`: Function to preprocess the configuration. It takes a `DictConfig` object and returns a `DictConfig` object.
- `print_config`: Whether to print the configuration before execution.
- `resolve`: Whether to resolve the configuration.
- `use_flogging`: Whether to use flogging for structured logging.
- `**flogging_kwargs`: Additional keyword arguments for flogging.

## Logging with Flogging

Hydraclick integrates with [flogging](https://github.com/FragileTech/flogging) for structured logging.
To enable flogging, ensure it's installed:

```bash
pip install hydraclick[flogging]
```

```bash
pip install flogging
```

If `flogging` is not available, Hydraclick will log a warning and disable structured logging.

## Shell Completion

Hydraclick supports generating shell completion scripts. Use the `--shell-completion` option
to generate scripts for your preferred shell.

```bash
cli_app command --shell-completion install=bash > my_script_completion.sh
source my_script_completion.sh
```

## Contribution

Contributions are welcome! Please follow these steps:

1. Fork the repository.
2. Create a new branch for your feature or bugfix.
3. Commit your changes with clear messages.
4. Submit a pull request detailing your changes.

For major changes, please open an issue first to discuss your ideas.

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

## Support

If you encounter any issues or have questions, feel free to open an issue on the [GitHub repository](https://github.com/yourusername/hydraclick).

## Acknowledgements

- [Hydra](https://hydra.cc/) for powerful configuration management.
- [Click](https://click.palletsprojects.com/) for creating beautiful CLIs.
- [Flogging](https://github.com/FragileTech/flogging) for structured logging.


4 changes: 3 additions & 1 deletion src/hydraclick/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
import logging

import click
import flogging
Expand All @@ -14,8 +15,9 @@ def cli():

@cli.command(short_help="test_stuff.")
@hydra_command()
def nothing(args, **kwargs): # noqa: ARG001
def nothing(args, **kwargs):
"""Test function that does nothing."""
logging.warning(f"Doing nothing {args, kwargs}")


if __name__ == "__main__":
Expand Down
53 changes: 47 additions & 6 deletions src/hydraclick/terminal_effects.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import shutil
from typing import Optional, Callable
import os
import sys

import click
from click import Context, Option
Expand All @@ -18,20 +20,59 @@ def get_no_terminal_efects() -> bool:
NO_TERMINAL_EFFECTS = get_no_terminal_efects()


def display_terminal_effect(value):
"""Display the terminal effect."""
# from terminaltexteffects.effects.effect_random_sequence import RandomSequence
from terminaltexteffects.effects.effect_print import Print # noqa: PLC0415
def config_effect(effect):
"""Configure the terminal effect."""
from terminaltexteffects.utils.graphics import Color # noqa: PLC0415

effect = Print(value)
# effect.effect_config.speed = 0.025
effect.effect_config.print_speed = 5
effect.effect_config.print_head_return_speed = 3
effect.effect_config.final_gradient_stops = (Color("00ffae"), Color("00D1FF"), Color("FFFFFF"))
return effect


def remove_lines(num_lines: int):
"""Remove the last `num_lines` printed lines from the terminal."""
for _ in range(num_lines):
# Move the cursor up one line
sys.stdout.write("\x1b[1A")
# Clear the entire line
sys.stdout.write("\x1b[2K")
sys.stdout.flush()


def count_wrapped_lines(text: str, terminal_width: int):
"""Count the number of lines that the text will take when wrapped."""
lines = text.splitlines()
total_lines = 0
for line in lines:
if terminal_width > 0:
num_terminal_lines = (len(line) + terminal_width - 1) // terminal_width
else:
num_terminal_lines = 1
total_lines += max(num_terminal_lines, 1)
return total_lines


def display_terminal_effect(value, effect_cls=None):
"""Display the terminal effect."""
from terminaltexteffects.effects.effect_print import Print # noqa: PLC0415

effect_cls = effect_cls or Print
effect = effect_cls(value)
effect = config_effect(effect)
with effect.terminal_output() as terminal:
for frame in effect:
terminal.print(frame)
terminal_width = shutil.get_terminal_size().columns
n_lines_last_rendered_frame = count_wrapped_lines(frame, terminal_width)
remove_lines(n_lines_last_rendered_frame)
last_effect = effect_cls(value)
last_effect = config_effect(last_effect)
last_effect.terminal_config.ignore_terminal_dimensions = True
last_frame = list(last_effect)[-1]
sys.stdout.write(last_frame.lstrip())
sys.stdout.write("\n")
sys.stdout.flush()


_TERMINAL_EFFECT = display_terminal_effect
Expand Down
Loading