Skip to content

Commit

Permalink
Merge pull request #3 from FragileTech/text-effects
Browse files Browse the repository at this point in the history
Improve terminal effects
  • Loading branch information
Guillemdb authored Sep 15, 2024
2 parents 4874140 + 575c77b commit 768243b
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 9 deletions.
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

0 comments on commit 768243b

Please sign in to comment.