Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Why This Talk?

Slide 5

Slide 5 text

Terminology

Slide 6

Slide 6 text

Parameter • Argument • Option

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Introducing Click

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Let's Use It

Slide 13

Slide 13 text

pip install click

Slide 14

Slide 14 text

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()

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Predict The Weather

Slide 18

Slide 18 text

Using A Web API

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

We Are Building • weather config • weather forecast LOCATION

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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 ... ... ... ...

Slide 23

Slide 23 text

Multiple Commands

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Store The API Key

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Parameter Types

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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): ...

Slide 34

Slide 34 text

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): ...

Slide 35

Slide 35 text

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)

Slide 36

Slide 36 text

Look At The Weather

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Custom Parameter Type

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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)

Slide 42

Slide 42 text

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) ...

Slide 43

Slide 43 text

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 ... ... ... ...

Slide 44

Slide 44 text

Don't Break It

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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, )

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Packaging Your CLI

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

pip install double-click-weather

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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')() )

Slide 56

Slide 56 text

You Have Many New Tools

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Chains & Pipes

Slide 61

Slide 61 text

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 ... ... ... ...

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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}")

Slide 64

Slide 64 text

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