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!