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 vuela 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 --skip-existing es útil para que el 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, sié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 apliciación cliente.
Un saludo!