Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Double Click - Continue Building Better CLIs

Seb
November 18, 2017

Double Click - Continue Building Better CLIs

Seb

November 18, 2017
Tweet

More Decks by Seb

Other Decks in Technology

Transcript

  1. Double Click
    Continue Building Better CLIs
    Sebastian Vetter
    @elbaschid
    Slides: bit.ly/pyconca-double-click

    View Slide

  2. Check Out Click - Part I
    Click - A Pleasure To Write, A Pleasure To Use (PyCon US 2016)

    View Slide

  3. Seb
    • @elbaschid
    • Freelance Coder at Roadside Software
    • Outdoor Tour Guide
    • Vancouver ➡ Rocky Mountains

    View Slide

  4. Why This Talk?

    View Slide

  5. Terminology

    View Slide

  6. Parameter
    • Argument
    • Option

    View Slide

  7. Argument
    • Mandatory parameter
    • Only values required
    What It Looks Like:
    $ pgcli postgresql://....

    View Slide

  8. Option
    • Optional parameter
    • Name and value required
    • Flags as special options
    What It Looks Like:
    $ heroku --help
    $ heroku logs --app my-heroku-app

    View Slide

  9. (Sub-)Command
    • Nested commands allowed
    • Groups sub-commands
    • Has options & arguments
    What It Looks Like:
    $ pip install django

    View Slide

  10. Introducing Click

    View Slide

  11. click
    • Author: Armin Ronacher
    • Version 6.x
    • http://click.pocoo.org/6/
    • Why Click?

    View Slide

  12. Let's Use It

    View Slide

  13. pip install click

    View Slide

  14. Basic Example
    # cli.py
    import click
    @click.command()
    @click.option('--times', '-t', type=int)
    @click.argument('symbol')
    def main(times, symbol):
    print(f'I {symbol * times} Click!')
    if __name__ == '__main__':
    main()

    View Slide

  15. Running It
    $ python test.py -t 5 ❤
    I
    ❤❤❤❤❤
    Click!

    View Slide

  16. Breaking It
    $ python test.py -t five ❤
    Usage: test.py [OPTIONS] SYMBOL
    Error: Invalid value for "--times" / "-t":
    five is not a valid integer

    View Slide

  17. Predict The Weather

    View Slide

  18. Using A Web API

    View Slide

  19. Github Repos
    • Click Template: bit.ly/click-template
    • Example Code: bit.ly/double-click-example

    View Slide

  20. We Are Building
    • weather config
    • weather forecast LOCATION

    View Slide

  21. weather config
    $ weather config
    Please enter your API key []: mysecretapikey
    $ cat ~/.weather.cfg
    mysecretapikey

    View Slide

  22. weather forecast
    $ weather forecast Montreal
    Time Description Min Temp Max Temp
    ============================================================
    Tue, Nov 14 @ 00h clear sky -2.3 2.1
    Tue, Nov 14 @ 03h clear sky -2.8 0.1
    Tue, Nov 14 @ 06h few clouds -2.7 -1.2
    Tue, Nov 14 @ 09h few clouds -2.9 -2.9
    ... ... ... ...

    View Slide

  23. Multiple Commands

    View Slide

  24. Group
    import click
    @click.command()
    def main():
    pass

    View Slide

  25. Group
    import click
    @click.group()
    def main():
    pass

    View Slide

  26. config Command
    @click.command()
    def config():
    pass

    View Slide

  27. config Command
    @main.command()
    def config():
    pass

    View Slide

  28. forecast Command
    @main.command()
    @click.argument('location')
    def forecast(location):
    pass

    View Slide

  29. Store The API Key

    View Slide

  30. Running It
    $ weather config
    Please enter your API key []: mysecretapikey
    $ cat ~/.weather.cfg
    mysecretapikey

    View Slide

  31. Parameter Types

    View Slide

  32. Basic Types Click Types
    int UUID
    float File
    str Path
    bool Choice
    IntRange

    View Slide

  33. Using The Path Type
    @main.option(
    '--config-file',
    type=click.Path(),
    default=os.path.expanduser('~/.weather.cfg'))
    @click.option('--api-key', envvar='API_KEY', default='')
    def config(config_file, api_key):
    ...

    View Slide

  34. Using The Path Type
    @main.option(
    '--config-file',
    type=click.Path(exists=True, writable=True),
    default=os.path.expanduser('~/.weather.cfg'))
    @click.option('--api-key', envvar='API_KEY', default='')
    def config(config_file, api_key):
    ...

    View Slide

  35. Save The Key
    def config(config_file, api_key):
    api_key = click.prompt(
    'Please enter your API key',
    default=api_key
    )
    with open(config_file, 'w') as cfg:
    cfg.write(api_key)

    View Slide

  36. Look At The Weather

    View Slide

  37. weather forecast
    $ weather forecast Montreal
    Time Description Min Temp Max Temp
    ============================================================
    Tue, Nov 14 @ 00h clear sky -2.3 2.1
    Tue, Nov 14 @ 03h clear sky -2.8 0.1
    Tue, Nov 14 @ 06h few clouds -2.7 -1.2
    Tue, Nov 14 @ 09h few clouds -2.9 -2.9
    ... ... ... ...
    API Call
    https://api.openweathermap.org/data/2.5/forecast?q=Montreal

    View Slide

  38. weather forecast
    $ weather forecast 6077243
    Time Description Min Temp Max Temp
    ============================================================
    Tue, Nov 14 @ 00h clear sky -2.3 2.1
    Tue, Nov 14 @ 03h clear sky -2.8 0.1
    Tue, Nov 14 @ 06h few clouds -2.7 -1.2
    Tue, Nov 14 @ 09h few clouds -2.9 -2.9
    ... ... ... ...
    API Call
    https://api.openweathermap.org/data/2.5/forecast?id=6077243

    View Slide

  39. Custom Parameter Type

    View Slide

  40. The Return Type
    from collections import namedtuple
    Location = namedtuple('Location', ['query', 'value'])

    View Slide

  41. Our Location Type
    class LocationType(click.ParamType):
    name = 'location'
    def convert(self, value, param, ctx):
    try:
    value = int(value)
    except ValueError:
    query = 'q'
    else:
    query = 'id'
    return Location(query, value)

    View Slide

  42. Use It
    @main.command()
    @click.argument('location', type=LocationType())
    def forecast(location):
    ...
    url = 'https://api.openweathermap.org/data/2.5/forecast'
    params = {
    location.query: location.value,
    }
    response = session.get(url, params=params)
    ...

    View Slide

  43. weather forecast
    $ weather forecast Montreal
    Time Description Min Temp Max Temp
    ============================================================
    Tue, Nov 14 @ 00h clear sky -2.3 2.1
    Tue, Nov 14 @ 03h clear sky -2.8 0.1
    Tue, Nov 14 @ 06h few clouds -2.7 -1.2
    Tue, Nov 14 @ 09h few clouds -2.9 -2.9
    ... ... ... ...

    View Slide

  44. Don't Break It

    View Slide

  45. The Test Runner
    from click.testing import CliRunner
    def test_writing_config_file():
    runner = CliRunner()
    runner.invoke(
    main,
    ['config'],
    input='mysecretapikey',
    )
    ...

    View Slide

  46. Filesystem Tests
    with runner.isolated_filesystem() as env:
    api_key = '00ee52f44f3350c73f2684a0f23f2805'
    filename = f'{env}/weather.cfg'
    result = runner.invoke(
    main,
    ['--config-file', filename, 'config'],
    input=api_key,
    )

    View Slide

  47. The Result
    assert result.exit_code == 0
    assert api_key in result.output
    with open(filename) as cfg_file:
    assert cfg_file.read() == api_key

    View Slide

  48. Packaging Your CLI

    View Slide

  49. Learn More
    • PyPA Packaging User Guide
    • Grug make fire! Grug make wheel! - Russell Keith-Magee
    • Shipping Software To Users With Python - Glyph

    View Slide

  50. Choose A License
    It's a minefield, people. All I'm saying is this: the next time
    you release code into the wild, do your fellow developers a
    favor and pick a license – any license.
    — Coding Horror

    View Slide

  51. Start With Here
    # setup.py
    setup(
    name='double-click-weather',
    version='0.0.1',
    license='MIT',
    ...
    )

    View Slide

  52. Create The Executable
    # setup.py
    setup(
    ...
    entry_points={'console_scripts': [
    'weather = forecast.simple:main']},
    ...
    )

    View Slide

  53. pip install double-click-weather

    View Slide

  54. File Location
    ${VENV}/bin/weather.py

    View Slide

  55. Generated Code
    #!${VENV}/bin/python3
    # EASY-INSTALL-ENTRY-SCRIPT: 'double-click-weather','console_scripts','weather'
    __requires__ = 'double-click-weather'
    import re
    import sys
    from pkg_resources import load_entry_point
    if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(
    load_entry_point('double-click-weather', 'console_scripts', 'weather')()
    )

    View Slide

  56. You Have Many New Tools

    View Slide

  57. PyCascades 2018
    Vancouver, BC
    !
    22-23 January, 2018
    !
    Get Your Ticket
    !
    http://www.pycascades.com

    View Slide

  58. View Slide

  59. Need Help Building Cool
    Let's Talk
    [email protected] | www.roadsi.de
    Slides: bit.ly/pyconca-double-click
    Code: bit.ly/double-click-example

    View Slide

  60. Chains & Pipes

    View Slide

  61. Unix-style Pipeline
    $ weather find Montreal | weather forecast
    Time Description Min Temp Max Temp
    ============================================================
    Tue, Nov 14 @ 00h clear sky -2.3 2.1
    Tue, Nov 14 @ 03h clear sky -2.8 0.1
    Tue, Nov 14 @ 06h few clouds -2.7 -1.2
    ... ... ... ...

    View Slide

  62. Chainable Group
    @click.group(chain=True)
    def main():
    pass

    View Slide

  63. Write To stdout
    @main.command()
    @click.argument('location', type=LocationType())
    @click.argument('output', type=click.File('w'), default='-')
    def find(location, output):
    ...
    output.write(f"{location.value}")

    View Slide

  64. Read From stdin
    @main.command()
    @click.argument('location', type=click.File('r'))
    def forecast(location):
    value = location.read()
    ...

    View Slide