Double Click - Continue Building Better CLIs

C65d18a43152b199ee94aad2b79b70c4?s=47 Seb
November 18, 2017

Double Click - Continue Building Better CLIs

C65d18a43152b199ee94aad2b79b70c4?s=128

Seb

November 18, 2017
Tweet

Transcript

  1. Double Click Continue Building Better CLIs Sebastian Vetter @elbaschid Slides:

    bit.ly/pyconca-double-click
  2. Check Out Click - Part I Click - A Pleasure

    To Write, A Pleasure To Use (PyCon US 2016)
  3. Seb • @elbaschid • Freelance Coder at Roadside Software •

    Outdoor Tour Guide • Vancouver ➡ Rocky Mountains
  4. Why This Talk?

  5. Terminology

  6. Parameter • Argument • Option

  7. Argument • Mandatory parameter • Only values required What It

    Looks Like: $ pgcli postgresql://....
  8. Option • Optional parameter • Name and value required •

    Flags as special options What It Looks Like: $ heroku --help $ heroku logs --app my-heroku-app
  9. (Sub-)Command • Nested commands allowed • Groups sub-commands • Has

    options & arguments What It Looks Like: $ pip install django
  10. Introducing Click

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

    • Why Click?
  12. Let's Use It

  13. pip install click

  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()
  15. Running It $ python test.py -t 5 ❤ I ❤❤❤❤❤

    Click!
  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
  17. Predict The Weather

  18. Using A Web API

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

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

  21. weather config $ weather config Please enter your API key

    []: mysecretapikey $ cat ~/.weather.cfg mysecretapikey
  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 ... ... ... ...
  23. Multiple Commands

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

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

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

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

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

  29. Store The API Key

  30. Running It $ weather config Please enter your API key

    []: mysecretapikey $ cat ~/.weather.cfg mysecretapikey
  31. Parameter Types

  32. Basic Types Click Types int UUID float File str Path

    bool Choice IntRange
  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): ...
  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): ...
  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)
  36. Look At The Weather

  37. weather forecast <name> $ 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
  38. weather forecast <city ID> $ 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
  39. Custom Parameter Type

  40. The Return Type from collections import namedtuple Location = namedtuple('Location',

    ['query', 'value'])
  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)
  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) ...
  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 ... ... ... ...
  44. Don't Break It

  45. The Test Runner from click.testing import CliRunner def test_writing_config_file(): runner

    = CliRunner() runner.invoke( main, ['config'], input='mysecretapikey', ) ...
  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, )
  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
  48. Packaging Your CLI

  49. Learn More • PyPA Packaging User Guide • Grug make

    fire! Grug make wheel! - Russell Keith-Magee • Shipping Software To Users With Python - Glyph
  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
  51. Start With Here # setup.py setup( name='double-click-weather', version='0.0.1', license='MIT', ...

    )
  52. Create The Executable # setup.py setup( ... entry_points={'console_scripts': [ 'weather

    = forecast.simple:main']}, ... )
  53. pip install double-click-weather

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

  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')() )
  56. You Have Many New Tools

  57. PyCascades 2018 Vancouver, BC ! 22-23 January, 2018 ! Get

    Your Ticket ! http://www.pycascades.com
  58. None
  59. Need Help Building Cool Let's Talk seb@roadsi.de | www.roadsi.de Slides:

    bit.ly/pyconca-double-click Code: bit.ly/double-click-example
  60. Chains & Pipes

  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 ... ... ... ...
  62. Chainable Group @click.group(chain=True) def main(): pass

  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}")
  64. Read From stdin @main.command() @click.argument('location', type=click.File('r')) def forecast(location): value =

    location.read() ...