Créer et déployer un package Python avec Poetry et Pypiserver

Chez Anybox nous sommes orientés majoritairement sur les solutions de développement utilisant le langage Python. Nous allons voir comment créer des paquets Python et les déployer chez nos clients sans avoir à les héberger sur PyPI.

Quel outil pour les packages Python ?

Une des difficultés principales dans la création d'un package Python,  c’est la diversité des solutions proposées. Pour l’installeur, on peut choisir entre  distribute, setuptools, distutils ou Distutils2, rien que ça. Ils ont tous été à un moment la façon recommandée de créer son paquet Python.

Avec la PEP-518 la Python Packaging Authority a proposé l'introduction d'un nouveau fichier standardisé nommé pyproject.toml. Ce dernier permet de remplacer l'ensemble des fichiers setup.py, requirements.txt, setup.cfg, MANIFEST.in et Pipfile auparavant nécessaires pour configurer son paquet. Nous allons nous pencher sur le fichier pyproject.toml.

Poetry est un outil qui va nous permettre de gérer le cycle de vie de notre projet ainsi que nos différentes dépendances. Il va également gérer pour nous la création et la gestion du fichier pyproject.toml facilement.

Installation de Poetry

Nous allons dans un premier temps installer Poetry

$ curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python

Cela va installer l'outil dans le dossier $HOME/.poetry/bin/poetry et l'ajouter à notre PATH.

Création de notre paquet d'exemple

Notre programme d'exemple va aller récupérer les informations concernant les dernières offres d'emploi publiées sur le site de l'AFPY afin d'afficher les titres et les résumés de ces dernières.

Pour créer notre paquet nous allons utiliser la commande poetry new afin qu'il nous génère le squelette de notre application.

$ poetry new test_lib
$ tree test_lib
├── README.rst
├── pyproject.toml
├── test_lib
│   └── __init__.py
└── tests
    ├── __init__.py
    └── test_test_lib.py

Poetry a généré l'arborescence de notre application en y ajoutant les tests unitaires et le fichier pyproject.toml attendu.

[tool.poetry]
name = "test_lib"
version = "0.1.0"
description = ""
authors = ["TROUVERIE Joachim <bin@anybox.fr>"]

[tool.poetry.dependencies]
python = "^3.6"

[tool.poetry.dev-dependencies]
pytest = "^3.0"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

Nous allons modifier un peu ce fichier pour lui indiquer la licence de notre bibliothèque, la description du projet, une liste de mots-clés et un lien vers notre README. Je vous renvoie à la documentation de poetry pour plus de détails.

[tool.poetry]
name = "test_lib"
version = "0.1.0"
description = "A lib to get the last AFPY job offers"
authors = ["TROUVERIE Joachim <bin@anybox.fr>"]
license = "GPL-3.0+"
keywords = ["afpy", "job offers"]
readme = "README.rst"

[tool.poetry.dependencies]
python = "^3.6"

[tool.poetry.dev-dependencies]
pytest = "^3.0"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

Ajout des dépendances avec Poetry

Poetry va nous permettre de gérer l'ensemble de nos dépendances de production et de développement grâce à sa sous-commande add.

Si vous êtes familier avec les Pipfile, poetry utilise une façon analogue de gestion des dépendances.

Dans notre exemple, nous aurons besoin de la bibliothèque BeautilfulSoup afin de naviguer au sein du HTML de la page des offres d'emploi de l'AFPY.

$ poetry add bs4

Poetry va télécharger puis mettre à jour notre environnement virtuel de développement et les dépendances de notre projet. Une nouvelle ligne a été ajoutée à notre fichier pyproject.toml dans la section [tool.poetry.dependencies] : bs4 = "^0.0.1 qui permettra à poetry de gérer ces dépendances lors de la construction du paquet.

Le code

Nous allons maintenant passer au code de notre bibliothèque. Rien de bien compliqué ici, le code va juste récupérer l'ensemble des offres d'emploi de l'AFPY à l'adresse suivante : https://www.afpy.org/posts/emplois. Puis nous effectuerons un traitement sur la page afin de pouvoir afficher ces informations dans un terminal.

Pour cela nous allons ajouter un fichier jobs.py dans notre dossier test_lib

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from bs4 import BeautifulSoup, NavigableString
from urllib import request


AFPY_URL = "https://www.afpy.org/posts/emplois"


def get_last_job_offers():
    """Get last job offers from AFPY website and print it in
    Terminal
    """
    content = request.urlopen(AFPY_URL).read()
    html_content = BeautifulSoup(content)
    # each job offer is in an article
    for article in html_content.find_all("article"):
        # job title is in a h2 tag
        title = article.find_next("h2").text
        # get article inner text
    	inner_text = [
            element.strip() for element in article
            if isinstance(element, NavigableString)
            and element
        ]
        print(title)
        print("-" * len(title))
        print("\n".join(inner_text))

La création du paquet

Le code est loin d'être complet, il lui manque notamment les tests unitaires. Il a pour but de vous montrer à quel point il est simple d'ajouter de nouvelles dépendances à votre projet. Une fois tout cela effectué, il ne vous reste qu'à construire votre paquet. Et là encore, poetry est là pour vous mâcher le travail.

En effet alors qu'auparavant il vous fallait ajouter wheel à vos dépendances puis lancer un

$ python setup.py sdist
$ python setup.py bdist_wheel

Poetry se charge de tout ça pour vous avec un simple

$ poetry build
Building test_lib (0.1.0)
 - Building sdist
 - Built test_lib-0.1.0.tar.gz

 - Building wheel
 - Built test_lib-0.1.0-py3-none-any.whl

Créer un dépôt à la PyPI

PyPI est le dépôt officiel des packets Python. Il vous permet de rechercher, d'installer et de partager vos paquets Python depuis son interface ou via la commande pip. Si, comme dans cet exemple vous souhaitez pouvoir déployer vos propres paquets sans avoir à les publier sur le dépôt officiel, il vous est possible de monter votre propre serveur de dépôts.

Pour mettre en place ce dernier nous allons nous appuyer sur le paquet pypiserver. Ce dernier, déployé à l'aide un docker-compose, créera une arborescence pour rendre disponibles nos paquets Python à l'installation.

Nous nous baserons sur le docker-compose d'exemple présent dans le dépôt github du projet pour monter notre instance.

version: '3'

services:

  pypi:
    image: pypiserver/pypiserver:latest
    volumes:
      - ./data:/data/packages/
      - ./auth/:/data/auth/
    command: -P /data/auth/.htpasswd -a update,download,list /data/packages
    ports:
      - 8080:8080

Créons les volumes nécessaires

  • data qui contiendra l'ensemble de nos paquets présents sur le serveur
  • auth qui gérera l'authentification des utilisateurs sur notre dépôt et restreindra à ces derniers le déploiement et l'installation des paquets via un fichier htpasswd
$ mkdir data auth
$ htpasswd -sc auth/.htpasswd pypi

Après un docker-compose up -d notre serveur est disponible à l'adresse suivante http://localhost:8080.

Déployer notre paquet

Il est temps de déployer notre paquet sur notre instance pypiserver. La commande historique python setup.py upload sera remplacée ici par l'utilitaire poetry publish qui se chargera pour nous de gérer l'authentification avec notre dépôt. Poetry permet de déployer nos paquets sur PyPI ou sur un dépôt privé en lui spécifiant une clé de configuration pointant vers l'adresse de notre dépôt.

$ poetry config repositories.local http://localhost:8080/

Une fois paramétré il ne nous reste plus qu'à téléverser notre paquet grâce à la commande publish.

$ poetry publish --repository local \
	--username <username_defini_dans_htpasswd> \
	--password <password_defini_dans_htpasswd>
    
 - Uploading test-0.1.0-py3-none-any.whl 100%
 - Uploading test-0.1.0.tar.gz 100%

En vous rendant à l'adresse suivante http://localhost:8080/packages, vous allez pouvoir constater que notre bibliothèque a bien été prise en charge sur notre dépôt.

C'est fini, nous pouvons maintenant installer notre paquet grâce à pip

$ pip install --extra-index-url http://localhost:8080/ test_lib

Le mot de la fin

Vous avez pu voir dans cet article comment créer votre projet, de la conception au déploiement via l’utilitaire Poetry et comment gérer votre propre dépôt via pypiserver.

Concernant l’utilisation du fichier pyproject.toml je vous invite à lire avec attention la PEP-518 qui indique le choix de la Pypa de créer un nouveau type de fichier pour gérer nos paquets Python (et ainsi se passer de notre valeureux setup.cfg). Ce choix n’est pas franchement au goût de tout le monde comme par exemple : https://news.ycombinator.com/item?id=18614654.

Certains  auraient en effet préféré une uniformisation ainsi qu'une amélioration du fichier setup.cfg qui était déjà là depuis tout de même 2016. Il est à noter que vous pouvez tout de même toujours l’utiliser pour construire vos paquets Python : https://docs.python.org/3.7/distutils/configfile.html.