Crear un paquete Python y publicarlo en Azure DevOps con GitHub Actions

En este post, me gustaría compartir una forma de crear un paquete de Python, publicarlo en un feed privado con una GitHub action y consumirlo en una aplicación cliente.

He dicho “una forma” porque hay otras muchas que igualmente serían válidas, basta con darse una vuelta por https://www.pypa.io/en/latest/ y ver que el estado del arte de publicación de paquetes en Python da para un libro.

Estoy usando PowerShell, así que no te extrañes cuando veas cosas como cd .\directorio\, asumo que son todos comandos con fácil traducción al shell de turno.

Para crear el proyecto, lo único que hay que hacer es crear un entorno virtual dentro de la carpeta del proyecto e instalar las dependencias:

mkdir NameFake
cd .\NameFake\
pipenv shell
pipenv install requests

En este momento los únicos ficheros del directorio serán Pipfile y Pipfile.lock.

No es necesario, pero será más fácil seguir la receta si creamos un directorio name\_fake y dentro un fichero _init\_.py vacío y un fichero generator.py con este código:

import requests

def get_name():
    response = requests.get('https://api.namefake.com/random/random/')
    return response.json()['name']

if __name__ == '__main__':
    name = get_name()
    print(name)

Para el tema del versionado de la librería, lo haremos creando el fichero name\_fake\version.py.

VERSION = (0, 0, 1)
__version__ = ".".join([str(x) for x in VERSION])

Y actualizaremos _init_.py con:

from .version import __version__

Podemos testear nuestro código con:

python .\name_fake\generator.py

Como queremos crear una librería (un paquete) y publicarlo en un repositorio, crearemos un fichero setup.py con el siguiente contenido (que he dejado al mínimo porque también da para una serie entera de posts)

from setuptools import setup, find_packages

import toml

from name_fake import __version__


def get_install_requires():    
    data = toml.load("Pipfile")
    return [package + (version if version != "*" else "")
            for package, version in data["packages"].items()]

packages = find_packages()    
install_requires = get_install_requires()

setup(
    name="NameFake-Generator",
    version=__version__,
    packages=packages,
    install_requires=install_requires
)

También necesitaremos instalar la dependencia de toml que estamos usando dentro de setup.py.

    pipenv install toml --dev

La estructura del proyecto ha quedado así:

tree /F
C:.
│   Pipfile
│   Pipfile.lock
│   setup.py
└───name_fake
        generator.py
        version.py
        __init__.py

Ahora ya podemos crear nuestro paquete con el siguiente comando y encontraremos en la carpeta dist el fichero NameFake\_Generator-0.0.1-py3-none-any.whl.

python setup.py clean --all bdist_wheel

Antes de subir a nuestro feed privado, podemos probar la librería creando un nuevo proyecto (y un nuevo entorno virtual) e instalando el paquete localmente:

cd ..
mkdir NameFakeClient
cd .\NameFakeClient\
pipenv shell
pipenv install ..\NameFake\dist\NameFake_Generator-0.0.1-py3-none-any.whl

Creamos un fichero main.py con este código:

from name_fake import generator

print(generator.get_name())

Y lo ejecutamos con:

python .\main.py

Ya tenemos un paquete wheel y hemos probado que funciona correctamente, ahora sí podemos publicarlo en nuestro feed privado, del que, por cierto, voy a asumir que ya está creado en Azure DevOps.

Para publicar el paquete necesitaremos un token de Azure DevOps con permisos de read/write en Packages.

Una vez lo tengamos podremos crear nuestro GitHub action con el siguiente contenido:

name: Upload Python Package
on: [workflow_dispatch]
jobs: 
    deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
        uses: actions/setup-python@v2
        with:
        python-version: '3.7.9'
    - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install setuptools wheel pipenv twine
          pipenv install --dev          
    - name: Build
        run: pipenv run python setup.py bdist_wheel
    - name: Upload package
        run: twine upload dist/* --repository-url https://pkgs.dev.azure.com/<YOUR_ORGANIZATION>/<YOUR_TEAM_PROJECT>/_packaging/<YOUR_FEED>/pypi/upload/ --username ${{ secrets.PRIVATEFEED_USERNAME }} --password ${{ secrets.PRIVATEFEED_PASSWORD }} --skip-existing

A propósito de este workflow, cabe mencionar que asume se han creado secretos para el nombre de usuario y contraseña (nombre del token y token) y que el campo --repository-url requiere que cada uno ponga el suyo, claro. Además --sip-xisting es útil para que workflow no falle si intentamos publicar una versión que ya existe en el feed. Por último, en el paso de establecer la versión de Python,:iéntete libre de poner la que creas oportuna.

Lógicamente, tanto el proyecto de la librería en local (probablemente con pre-commit) como el workflow deberían incluir pasos extrás como flake8, quizás mypy, pruebas automatizadas, etc.

En cualquier caso y teniendo ya nuestro paquete publicado en un feed de Azure DevOps, el último paso es configurar nuestra aplicación cliente para que pueda descargar paquetes no sólo de PyPI.org, sino también de nuestro feed privado.

Para ello, tendremos que crear un nuevo token con permisos read en Packaging y crear el fichero $env:USERPROFILE\pip\pip.ini con este contenido:

[global] 
extra-index-url=https://<YOUR_TOKEN_NAME>:<YOUR_TOKEN>@pkgs.dev.azure.com/<YOUR_ORGANIZATION>/<YOUR_TEAM_PROJECT>/_packaging/<YOUR_FEED>/pypi/simple/

Además, en el fichero Pipfile debemos agregar:

[[source]] 
url = "https://pkgs.dev.azure.com/<YOUR_ORGANIZATION>/<YOUR_TEAM_PROJECT>/_packaging/<YOUR_FEED>/pypi/simple/"
verify_ssl = true
name = "<YOUR_FEED>"

Fíjate que en el fichero pip.ini (y aunque es cierto que este fichero no está dentro del repositorio de código fuente) la autenticación se guarda en plano. Por otro lado, en Pipfile (que sí es parte del repositorio) no tenemos autenticación en la url (sería un problema mayúsculo subir secretos al repositorio). Si no usamos pipenv, podemos confiar en artifacts-keyring para que nos pida usuario y contraseña de forma interactiva al usar pip install, pero con pipenv no la pide, por eso guardamos el token en pip.ini.

Y con esto ya habríamos publicado una librearía de Python en un feed interno y la estaríamos consumiendo en nuestra aplicación cliente.

Un saludo!


Ver también