Python, gestion des requêtes HTTP avec les packages requests et httplib2

Introduction

Les fonctionnalités essentielles à aborder lorsqu'on souhaite apprendre et utiliser très rapidement Python :

Dans ce chapître, comment gérer les requêtes HTTP dans un programme Python.

Un programme PHP de démo rpc-articles-indexing.php envoie dans un format JSON les 10 derniers articles à indexer (colonne data_ixgoo est à null) :

rpc-articles-indexing.php
<?php

  $conn=mysqli_connect('localhost','sqlpac_ro','********','sqlpac',40000);
  mysqli_set_charset($conn,"utf8");
  
  if(!$conn) {
    die('Connexion error : ' . mysqli_connect_error());
  }
  
  $sql  = "select filename, id_lang from articles where date_ixgoo is null ";
  $sql .= " order by date_ol desc limit 10 ";
  
  $data = array();
  
  $get_articles = mysqli_query($conn, $sql);
  
  if($get_articles)
  {
     foreach ($get_articles as $row) {
        $data[] = $row;
     }
  }
  
  print json_encode($data);

?>

En interrogeant https://www.sqlpac.com/rpc/rpc-articles-indexing.php, les données produites avec json_encode ont le format suivant :

[
  {"filename":"mariadb-columnstore-1.2.3-installation-standalone-ubuntu-premiers-pas.html","id_lang":"fr"},
  {"filename":"mariadb-columnstore-1.2.3-standalone-installation-ubuntu-getting-started.html","id_lang":"en"},
  {"filename":"influxdb-v2-prise-en-main-installation-preparation-migration-version-1.7.html","id_lang":"fr"},
  {"filename":"influxdb-v2-getting-started-setup-preparing-migration-from-version-1.7.html","id_lang":"en"}
]

Voyons comment réaliser les requêtes HTTP dans un programme Python.

2 packages très utiles sont disponibles : requests et httplib2. Un autre package est disponible: urllib2, mais il nécessite plus de code.

Package requests

Installation

Si il n'est pas installé dans son environnement virtuel Python, installer le package requests avec pip :

pip3 search requests
requests (2.23.0)                   - Python HTTP for Humans.
pip3 install requests
Installing collected packages: urllib3, chardet, certifi, idna, requests
Successfully installed certifi-2020.4.5.1 chardet-3.0.4 idna-2.9 requests-2.23.0 urllib3-1.25.9

Une simple requête GET avec requests

Dans le programme Python, il faut juste importer le package requests et appeler la méthode get :

import requests

r = requests.get('https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php')

print(r.status_code)
print(r.headers)
print(r.text)
200

{'Date': 'Thu, 16 Apr 2020 14:59:04 GMT', 'Content-Type': 'text/html; charset=UTF-8',
  'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive',
  'Server': 'Apache', 'X-Powered-By': 'PHP/7.3',
  'Vary': 'Accept-Encoding', 'Content-Encoding': 'gzip', 'X-IPLB-Instance': '30837',
  'Set-Cookie': 'SERVERID108286=102098|Xpic6|Xpic6; path=/'
}

[
  {"filename":"influxdb-v2-prise-en-main-installation-preparation-migration-version-1.7.html","id_lang":"fr"},
  {"filename":"influxdb-v2-getting-started-setup-preparing-migration-from-version-1.7.html","id_lang":"en"},
  {"filename":"linux-ubuntu-fail2ban-installation-configuration-iptables.html","id_lang":"fr"}
]

Si facile que le code n'a pas besoin de commentaires.

requests - Ajouter des paramètres à la requête GET

Améliorons la requête dans le programme PHP pour y ajouter des critères : https://www.sqlpac.com/rpc/rpc-articles-indexing.php?section=oracle&year=2006

  $sql  = "select filename, id_lang from articles where date_ixgoo is null ";
  if (isset($_GET["section"])) { $sql .= " and filename like '".$_GET["section"]."%'"; }
  if (isset($_GET["year"]))    { $sql .= " and date_ol between '".$_GET["year"]."-01-01' and '".$_GET["year"]."-12-31'"; }
  $sql .= " order by date_ol desc limit 10";

Pour envoyer les critères :

import requests

q = {'section':'oracle', 'year':2006}
r = requests.get('https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php', params=q )
          
print(r.status_code)
print(r.text)
200
[
  {"filename":"oracle-resultats-procedure-stockee-vers-ms-sql.html","id_lang":"fr"},
  {"filename":"oracle-trigger-systeme-after-logon.html","id_lang":"fr"}
]

Pas besoin d'importer le package json, un décodeur JSON est intégré avec la méthode json :

import requests

q = {'section':'oracle', 'year':2006}
r = requests.get('https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php', params=q )

jresult = r.json()

print(type(jresult))
print(jresult[0]["filename"])
<class 'list'>
oracle-resultats-procedure-stockee-vers-ms-sql.html

requests - La méthode POST

Pour envoyer des données avec la méthode POST, utiliser la méthode post avec l'argument data, aussi simple que la méthode get et son argument params:

Le programme PHP rpc-update-article.php met à jour une table en utilisant les variables POST envoyées par le programme Python et retourne au format JSON les résultats au format JSON (nombre de lignes affectées ou code erreur):

rpc-update-article.php
<?php

  $resp = array();
  
  if (! isset($_POST["filename"]) || ! isset($_POST["datets"])) {
     $resp[0]["returncode"] = -1;
     $resp[0]["reason"] = "Missing parameter, filename or timestamp";
  } else {
     $sql = "update articles set date_ixgoo='".$_POST["datets"]."' where filename='".$_POST["filename"]."'";
     
     $conn=mysqli_connect('localhost','sqlpac_ro','********','sqlpac',40000);
     mysqli_set_charset($conn,"utf8");
     
     if ( ! $conn ) {
        $resp[0]["returncode"] = -2;
        $resp[0]["reason"] = "Connexion to database issue";
     }
     else {
        $sql = "update articles set date_ixgoo='".$_POST["datets"]."' where filename='".$_POST["filename"]."'";
        if ( ! mysqli_query($conn,$sql) ) {
           $resp[0]["returncode"] = -2;
           $resp[0]["errorcode"] = mysqli_errno($conn);
           $resp[0]["reason"] = mysqli_error($conn);      
        } else {
           $resp[0]["returncode"] = mysqli_affected_rows($conn);
           $resp[0]["filename"] = $_POST["filename"];
           $resp[0]["datets"] = $_POST["datets"];
        }
        mysqli_close($conn);
     }
     
  }
  print json_encode($resp);

?>

Les données sont envoyées avec le code suivant :

import requests

formdata = {'filename':'python-http-queries-with-packages-requests-httplib2.html', 'datets':'2020-04-16'}
p = requests.post('https://www.sqlpac.com/sqlpac/rpc-update-article.php', data=formdata)

print(p.status_code)
print(p.json())
200
[{'returncode': 1, 'filename': 'python-http-queries-with-packages-requests-httplib2', 'datets':'2020-04-16'}]

Le package requests est puissant pour l'upload de fichiers avec la méthode POST, il n'y a qu'à utiliser l'argument files :

import requests

formdata = {'filename':'python-http-queries-with-packages-requests-httplib2.html', 'datets':'2020-04-16'}
uploadfiles = {'file': open('file1.txt', 'rb'), 'file': open('file2.txt', 'rb')}

p = requests.post('https://www.sqlpac.com/sqlpac/rpc-update-article.php', data=formdata, files=uploadfiles)

requests - Désactivation de la vérification du certificat SSL

Ajouter l'option verify=False pour désactiver la validation du certificat SSL lors de l'utilisation de la méthode get ou post :

import requests
          
r = requests.get('https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php', verify=False )
InsecureRequestWarning: Unverified HTTPS request is being made to host 'www.sqlpac.com'.
Adding certificate verification is strongly advised.
See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings

requests et l'authentification basique (basic HTTP authentication)

Quand un répertoire web est protégé avec une authentification basique (fichiers .htaccess et .htpasswd), utiliser l'argument auth=HTTPBasicAuth('user','password') en important HTTPBasicAuth

import requests
from requests.auth import HTTPBasicAuth

r = requests.get('https://www.sqlpac.com/rpc/send-data.php', auth=HTTPBasicAuth('sqlpac', '*********'))

print(r.status_code)
200

D'autres méthodes d'authentification peuvent bien entendu être utilisées avec requests : Digest, Oauth ….

Package httplib2

Étudions un autre package: httplib2. Le package requests est si puissant et simple que l'on pourrait conclure que nous avons tout ce qu'il nous faut avec celui-ci mais le package httplib2 doit aussi être étudié car les exemples de code pour les API Google utilisent ce package, et il y a un aspect qui est rarement abordé dans les documentations et tutoriaux à propos de requests : le mécanisme de mise en cache (caching), disponible en natif avec httplib2.

Installation

Si il n'est pas installé dans l'environnement virtuel Python, installer le package httplib2 avec pip :

pip3 search httplib2
          httplib2 (0.17.2)                         - A comprehensive HTTP client library.
pip3 install httplib2
          Installing collected packages: httplib2
Successfully installed httplib2-0.17.2

Comparé au package requests, httplib2 est autonome et ne requiert pas de dépendances. Le package requests dépend de chardet, urllib3 et d'autres.

La requête GET avec httplib2

En utilisant les programmes PHP de démo lors de l'exploration du package requests, pour lancer une requête GET :

import httplib2

http = httplib2.Http()

r = http.request("https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php", method="GET")
print(r)
({'date': 'Thu, 16 Apr 2020 16:20:09 GMT', 'content-type': 'text/html; charset=UTF-8', 'transfer-encoding': 'chunked',
'connection': 'keep-alive', 'server': 'Apache', 'x-powered-by': 'PHP/7.3',
'vary': 'Accept-Encoding', 'x-iplb-instance': '30846', 'set-cookie': 'SERVERID108286=102098|XpjaH|XpjaH; path=/',
'status': '200', 'content-length': '904', '-content-encoding': 'gzip',
'content-location': 'https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php'},

b'[{"filename":"influxdb-v2-prise-en-main-installation-preparation-migration-version-1.7.html","id_lang":"fr"}, … ]')

Les résultats sont moins faciles à exploiter que ceux obtenus avec le package requests.

2 objets sont retournés:

  • L'entête (headers), appelé aussi réponse : class 'httplib2.Response'
  • Le contenu de la réponse : class 'bytes'

Les objets de la réponse (headers, contenu) peuvent être séparés avec la syntaxe suivante :

import httplib2

http = httplib2.Http()

(headers, content) = http.request("https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php", method="GET")

print(headers.status)
200

httplib2 ne fournit pas de traduction native en JSON comme le fait le package requests avec la méthode json, le package json doit être importé et utilisé :

import json
import httplib2

http = httplib2.Http()

(headers, content) = http.request("https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php", method="GET")

if (headers.status==200) :
	jdata = json.loads(content)
	
	for elt in jdata:
		print("%s %s" % (elt["filename"], elt["id_lang"]))
ms-sql-server-2016-dbcc-clonedatabase-usage.html fr
ms-sql-server-2016-using-dbcc-clonedatabase.html en

httplib2 - Les requêtes GET avec des paramètres

Les paramètres doivent être donnés dans l'URL, aussi il ne faut pas oublier d'encoder la chaîne de requête (query string) avec urlencode :

import httplib2
import json
from urllib.parse import urlencode

params = { "section": "oracle", "year": 2006 }
(headers, content) = http.request("https://www.sqlpac.com/sqlpac/rpc-articles-indexing.php?" + urlencode(params),
                                    method="GET")

if (headers.status==200) :
  …
oracle-resultats-procedure-stockee-vers-ms-sql.html fr
oracle-trigger-systeme-after-logon.html fr
…

httplib2 - La méthode POST

En utilisant la méthode POST, la méthode est évidemment définie à POST, et 2 autres arguments sont donnés :

  • headers : type de contenu, défini à application/x-www-form-urlencoded pour un formulaire.
  • body : valeurs des données à envoyer, à encoder avec urlencode.
import httplib2
from urllib.parse import urlencode

http = httplib2.Http()

formdata = {'filename':'python-http-queries-with-packages-requests-httplib2.html', 'datets':'2020-04-16'}

(headers, content) = http.request("https://www.sqlpac.com/sqlpac/rpc-update-article.php",
								   method="POST",
								   headers={'Content-type': 'application/x-www-form-urlencoded'},
								   body=urlencode(formdata)
								  )
print(content)
b'[{'returncode': 1, 'filename': 'python-http-queries-with-packages-requests-httplib2', 'datets':'2020-04-16'}]'

Comparé au package requests, plus de code est nécessaire pour gérer les uploads de fichiers avec la méthode POST.

httplib2 - Désactivation de la validation du certificat SSL

Définir la propriété disable_ssl_certificate_validation à True avant d'exécuter une requête si la validation du certificat SSL doit être désactivée pour une quelconque raison, aucun message d'avertissement n'est levé comparé au package requests :

import httplib2

http = httplib2.Http()
http.disable_ssl_certificate_validation=True
          
(headers, content) = http.request("url", method="GET")
…

httplib2 et l'authentification basique (basic HTTP authentication)

Lorsqu'une authentification HTTP basique est mise en place, utiliser la méthode add_credentials(user, password) avant d'appeler la méthode request :

import httplib2

http = httplib2.Http()

http.add_credentials('sqlpac','*********')
(headers, content) = http.request("https://www.sqlpac.com/rpc/send-data.php",
                                  method="POST")
print(headers.status)
200

Avantages de httplib2 : usage d'un cache

Le package httplib2 est moins facile que le package requests, mais il a un gros avantage dans certaines circonstances : le cache.

Les résultats des requêtes peuvent être mis en cache dans un répertoire :

import httplib2

http = httplib2.Http("/tmp/.cache")

(headers, content) = http.request("https://www.sqlpac.com/rpc/send-data.php",
								   method="POST")
print(headers.status)
200

Dans l'exemple ci-dessus, les données sont mis en cache dans le répertoire /tmp/.cache, si le répertoire n'existe pas, le programme essaie de le créer.

L'expiration peut être gouvernée par l'entête Expires envoyé par le serveur Web. Avec Apache, pour définir une expiration dans un fichier .htaccess :

.htaccess
<IfModule mod_expires.c>
	ExpiresActive on
	ExpiresDefault     "access plus 4 hours"
</IfModule>

La propriété headers.fromcache (True | False) donne le statut "read from cache | lu depuis le cache" de la réponse.

import httplib2

http = httplib2.Http("/tmp/.cache")


(headers, content) = http.request("https://www.sqlpac.com/rpc/1.html")
print("Expires : %s" % (headers["expires"]))
print(headers.fromcache)

(headers, content) = http.request("https://www.sqlpac.com/rpc/1.html")
print(headers.fromcache)
Expires : Fri, 17 Apr 2020 14:50:13 GMT
False
True

Tous les appels suivants seront lus depuis le cache jusqu'à la date/heure de l'expiration, et ce sera valable également pour les futures autres exécutions du programme.

Ça peut être utile pour certains besoins, par exemple éviter les surcoûts d'accès réseau pour des données relativement statiques :

Expires : Fri, 17 Apr 2020 14:50:13 GMT
True
True

Pour écraser et mettre à jour le cache pour un appel : utiliser l'entête cache-control et appliquer la valeur no-cache :

import httplib2

http = httplib2.Http("/tmp/.cache")


(headers, content) = http.request("https://www.sqlpac.com/rpc/1.html")
print("Expires : %s" % (headers["expires"]))
print(headers.fromcache)

(headers, content) = http.request("https://www.sqlpac.com/rpc/1.html",
                                   headers={'cache-control':'no-cache'})
print(headers.fromcache)
Expires : Fri, 17 Apr 2020 14:50:13 GMT
True
False

Le package requests ne supporte pas en natif le mode cache, mais un package dérivé est disponible: requests-cache.

Conclusion

Selon les besoins, le package requests est le meilleur pour la gestion des requêtes HTTP si le format JSON est utilisé intensivement, ses syntaxes sont les plus faciles.

Pour le mécanisme de mise en cache (caching), httplib2 semble le plus approprié. La mise en cache avec le package requests nécessite un package optionnel (requests-cache), non abordé ici.