Pris par la curiosité de savoir comment peut fonctionner un programme CLI, autant se lancer dans la réalisation d’un projet simple histoire de voir ce qu’il y a derrière !
Pour cela, je vais utiliser le package python Click et simplement connecter une API météo pour afficher le temps qu’il fait dans les jolies villes de France.
Son petit nom: Morning
C’est parti !

mkdir morning
cd morning
python3 -m venv .venv
pip freeze > requirements.txt
source .venv/bin/activate
vim morning.py

Je crée le bon dossier à l’endroit qui va bien suivi d’un environnement virtuel.

morning.py

import click

@click.command()
@click.option('--city', '-c', help='Le nom de votre ville.')
def now(city):
    """La météo de votre ville, tout simplement."""
    click.echo('Coucou de ' + city)

if __name__ == '__main__':
    now()

Le décorateur @click.option permet d’ajouter des arguments à notre commande:

$ python morning.py now --city Paris
Coucou de Paris

L’argument help permet de renseigner les infos qui seront visibles si l’on tape:

$ python morning.py now --help
Usage: morning now [OPTIONS]

  La météo de votre ville, tout simplement.

Options:
  -c, --city TEXT  Le nom de votre ville
  --help           Show this message and exit.

L’idée est là, maintenant on démarre l’écriture du programme.
Première étape: on va setup l’api.

  1. J’ai choisi d’utiliser OpenWeatherMap
    Ils ont un accès gratuit qui suffira amplement.
    Tu t’inscris et tu vas recevoir ta clé API par email.
import json
import requests

import click


API_KEY = 'ta_clé_super_secrète'
OPENWEATHER_API_URL = 'http://api.openweathermap.org/data/2.5/weather?id={}&appid={}'

class RequestedWeather():
	def __init__(self, city_name):
		self.country = 'FR'
		self.city_name = city_name.capitalize()

	def api(self, id=None):
		if not id:
			id = self.city_id()
		url = OPENWEATHER_API_URL.format(id, API_KEY)
		response = requests.get(url)
		if response:
			return response.json()
	
	def city_id(self):
		with open('city.list.json', 'r') as f:
			data = json.load(f)
		city = next(x for x in data
			if x['name'] == self.city_name and x['country'] == self.country)
		if city:
			return city['id']
		else:
			click.echo("La ville {} n'existe pas".format(self.city_name))


@click.command()
@click.option('--city', '-c', help='Le nom de votre ville.')
def now(city):
    	"""La météo de votre ville, tout simplement."""
    	requested_weather = RequestedWeather(city).api()
	click.echo('Coucou de ' + city)
	click.echo(requested_weather)

if __name__ == '__main__':
    now()

Alors, du haut en bas:
J’importe la libraire requests pour gérer l’api et json pour gérer le json, Captain Obvious.
Ensuite deux jolies constantes pour stocker la clé et le call api.
J’ai utilisé ce format d’url car la doc d’OpenWeatherMap conseil de prendre l’id des villes à la place des noms directement.

__init__(), l’idée c’est de seulement avoir les villes françaises alors hop on met FR en dur et on garde city_name en dynamique.

api() effectue le call en fonction de l’id renseigné et retourne un beau json qui contient toutes les infos utilisables par la suite.

city_id() va s’occuper d’aller chercher l’id correspondant à la ville entrée en argument par l’utilisateur dans la liste fournie par OpenWeatherMap.

now(city), on instancie notre objet RequestedWeather() avec la ville renseignée par l’utilisateur et on renvoit ses informations avec click.echo(requested_weather)

On pourrait s’arrêter là en vrai parce qu’on a le comportement souhaité à la base, mais c’est pas jolie, pas lisible, c’est caca quoi. Donc je vais formatter un peu tout ce joyeux bordel.

ABSOLUTE_ZERO = 273.15

@click.command()
@click.option('--city', '-c', help='Le nom de votre ville.')
def now(city):
    	"""La météo de votre ville, tout simplement."""
   	# Init objet
	requested_weather = RequestedWeather(city).api()

	# Retrieve informations
	if requested_weather:
		weather = requested_weather['weather'][0]['main']
		temp = requested_weather['main']['temp'] - ABSOLUTE_ZERO
	else:
		click.echo('Tu dois renseigner une API_KEY')

	# Return
	click.echo('Actuellement dans votre jolie ville de {}:'.format(city))
	click.echo('--------------')
	click.echo('Temps: {}'.format(weather))
	click.echo('--------------')
	click.echo('Temperature: {}°C'.format(int(temp)))
	click.echo('Bonne journée !')

La constante ABSOLUTE_ZERO fait son entrée pour convertir la température donnée en Kelvin dans le json en Celsius.

Ok, le truc est sympa. Mais le nom des villes est sensible à la casse… Je vais pas trop m’embêter et simplement ajouter une commande en plus de now pour faire une recherche des villes disponibles.

import json
import requests

import click


ABSOLUTE_ZERO = 273.15
API_KEY = 'ta_clé_super_secrète'
CITIES_DATA = None
OPENWEATHER_API_URL = 'http://api.openweathermap.org/data/2.5/weather?id={}&appid={}'

class RequestedWeather():
	def __init__(self, city_name):
		self.country = 'FR'
		self.city_name = city_name.capitalize()

	def api(self, id=None):
		if not id:
			id = self.city_id()
		url = OPENWEATHER_API_URL.format(id, API_KEY)
		response = requests.get(url)
		if response:
			return response.json()
	
	def city_id(self):
		city = next(x for x in CITIES_DATA
			if x['name'] == self.city_name and x['country'] == self.country)
		if city:
			return city['id']
		else:
			click.echo("La ville {} n'existe pas".format(self.city_name))

@click.group()
def cli():
	global CITIES_DATA
	with open('city.list.json', 'r') as f:
		CITIES_DATA = json.load(f)

@cli.command()
def all():
	"""Liste des villes disponibles."""
	user = input('Le nom de votre ville ? \n')
	for city in CITIES_DATA:
		my_city = city['name'].lower()
		if my_city.find(user.lower()) >= 0 and city['country'] == 'FR':
			click.echo(my_city.capitalize())


@cli.command()
@click.option('--city', '-c', help='Le nom de votre ville.')
def now(city):
    	"""La météo de votre ville, tout simplement."""
   	# Init objet
	requested_weather = RequestedWeather(city).api()

	# Retrieve informations
	if requested_weather:
		weather = requested_weather['weather'][0]['main']
		temp = requested_weather['main']['temp'] - ABSOLUTE_ZERO
	else:
		click.echo('Tu dois renseigner une API_KEY')

	# Return
	click.echo('Actuellement dans votre jolie ville de {}:'.format(city))
	click.echo('--------------')
	click.echo('Temps: {}'.format(weather))
	click.echo('--------------')
	click.echo('Temperature: {}°C'.format(int(temp)))
	click.echo('Bonne journée !')


if __name__ == '__main__':
    cli()

Alors que pasa a la izquierda… Click permet la création de groupe de commandes, d’où l’arrivé de cli().
Maintenant les décorateurs de nos commandes changent pour @cli.command() et on appelle à la toute fin cli().

Les commandes python morning.py now et python morning.py all sont dispo !

$ python morning.py now -c Nice
Actuellement dans votre jolie ville de Nice:
------------------
Temps: Clouds
------------------
Temperature: 13°C
------------------
Bonne journée !

$ python morning.py all
Le nom de votre ville ? 
Bordeau

Arrondissement de bordeaux
Bordeaux
Bordeaux
Saint-caprais-de-bordeaux
Saint-caprais-de-bordeaux
Artigues-près-bordeaux
Artigues-pres-bordeaux
Lignan-de-bordeaux
Lignan-de-bordeaux
Carignan-de-bordeaux

Afin d’eviter de laisser la clé api en dur dans le fichier, tu peux setup une variable d’environnement et utiliser os.environ pour la récupérer.

$ export API_KEY='votre_clé_magique'

et

import os

API_KEY = os.environ.get('API_KEY')

Pour avoir une commande qui claque du genre morning now -c Lille, regarde du côté de la création d’un package avec setuptools.

Voili voilou pour la découverte de click, si t’as une remarque ou besoin d’aide n’hésites pas à venir sur le bon twitter des familles.

Le projet complet est diposnible sur mon github.