zmoog / public-notes

Apache License 2.0
0 stars 1 forks source link

Python: read data from stdin #3

Closed zmoog closed 1 year ago

zmoog commented 1 year ago

Sometimes in CLI tools it is conveniente read data from stdin instead of an argument or an option. This allows chaining different CLI tools together, for example:

$ tgl entries list | jinja -d - template.j2 | telegram message send --chat-id 123
zmoog commented 1 year ago

Starting by collecting some info:

From https://www.digitalocean.com/community/tutorials/read-stdin-python:

There are three ways to read data from stdin in Python.

  • sys.stdin
  • input() built-in function
  • fileinput.input() function

After skimming through the article I guess sys.stdin may be the way to go.

zmoog commented 1 year ago

Using:

import sys

if __name__ == "__main__":
    data = sys.stdin.read()
    print(data.upper())

Here' a quick test:

$ echo "hogwarts legacy is cool" | python main.py
HOGWARTS LEGACY IS COOL
zmoog commented 1 year ago

The Click framework has some nice utilities that deals with streams:

https://click.palletsprojects.com/en/8.1.x/utils/#standard-streams

import click

stdin_text = click.get_text_stream('stdin')
stdout_binary = click.get_binary_stream('stdout')
zmoog commented 1 year ago

As mentioned in zmoog/til#15.


I like this approach, it's quite flexible.

Here's one take on this that allows three different input sources:

  1. Directly as a text message using --text
  2. From a file or stdin using --text-file
  3. If I don't set neither, the text_file.read() will wait until the user has typed the message and hit CTRL + D

@message.command(name="send")
@click.option(
    "--text",
    help="Text to send. If not provided, text will be read from a file or stdin.",
    default=None,
)
@click.option(
    "--text-file", 
    help="Text file to read from. If not provided, stdin will be used.",
    type=click.File("r"),
    default=click.get_text_stream("stdin"),
)
@click.option(
    "--chat-id",
    required=True,
)
@click.option(
    '--parse-mode',
    type=click.Choice(
        ['HTML', 'MarkdownV2'],
        case_sensitive=False
    ),
    default=None,
)
@click.pass_context
def send(ctx: click.Context, text: str, text_file: typing.TextIO, chat_id: str, parse_mode: str):
    """Send a text message to a chat."""
    client = telegram.Client.from_envorinment(verbose=ctx.obj["verbose"])

    if text:
        message_text = text
    elif text_file and text_file.readable():
        message_text = text_file.read()
    else:
        raise Exception("No text or text file provided")

    resp = client.send(message_text, chat_id, parse_mode=parse_mode)

    message_id = resp.get("result", {}).get("message_id", "No message id found")
    click.echo(f"message-id: {message_id}")
zmoog commented 1 year ago

Implemented with https://github.com/zmoog/telegram-cli/pull/4