Tag: Python

Comprobar permisos de Administrador en una ejecución (estilo 🐍 Python)

Habitualmente nos encontramos con la situación en la que debemos ejecutar ciertos programas o flujos de ejecución y necesitamos permisos de Administrador. Estas situaciones se nos presentan cuando tenemos que manejar carpetas y ficheros en acciones que impliquen lectura, escritura o ejecución en directorios protegidos del sistema. Para ello se requiere una elevación de permisos que deberemos comprobar si existe antes de lanzar nuestra ejecución.

Aprovechando que seguramente tengamos que hacer esto varias veces, creo que la mejor opción de hacerlo es con el mejor lenguaje pegamento que existe hasta la fecha, que no es ni más ni menos que Python. Por ello la solución final que vamos a plantear es excelente tanto para administradores de sistemas como para desarrolladores, siendo además multiplataforma GNU/Linux y Windows.

Tenemos que tener en cuenta que GNU/Linux y Windows no funcionan igual para la gestión de permisos. En Windows preguntaremos desde la API disponible si el usuario es administrador, mientras que en GNU/Linux preguntaremos por si el usuario tiene permisos root.

import ctypes, os
from sys import exit


def is_admin():
    is_admin = False
    try:
        is_admin = os.getuid() == 0
    except AttributeError:
        is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0

    print ("Admin privileges: {}".format(is_admin))
    return is_admin

Para mirar los permisos en Windows usaremos la librería ctypes y para los permisos en GNU/Linux usaremos la librería os. Ambas están incluida en Python. Por una razón arbitraría se ha elegido comprobar primero GNU/Linux y después Windows.

Cuando se comprueba en GNU/Linux invocamos a la función os.getuid que nos devolverá 0 en el caso de ser root. Si llamamos a la función desde Windows con Python nos elevará una excepción del tipo AttributeError, que aprovecharemos a capturar para hacer la comprobación en Windows invocando a la función ctypes.windll.shell32.IsUserAnAdmin. En ambos casos guardaremos el booleano de la evaluación y lo devolveremos al final de la función.

Hasta aquí sencillo, pero esto es código Python y es necesario tener el interprete de Python, con el que obliga al sistema a tener instalado el intérprete de Python. Para solucionarlo, deberíamos convertir nuestro código en un programa añadiendo un punto de entrada y devolviendo un código de retorno tras la ejecución.

import ctypes, os
from sys import exit


def is_admin():
    is_admin = False
    is_win = False
    try:
        is_admin = os.getuid() == 0
    except AttributeError:
        is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
        is_win = True

    print ("Admin privileges: {}".format(is_admin))
    return is_admin, is_win

Antes modificaremos un poco la función is_admin()añadiendo un valor is_win que usaremos para saber que estamos comprobando en GNU/Linux o Windows los permisos de administrador, devolviendo el valor como un segundo valor en el retorno de la función.

Recordad que Python es posible recoger múltiples valores de retorno en una variable como una tupla, en variables separadas o incluso tirar los valores que no nos interesen.

def my_function():
    a = 2
    b = 3
    return a, b

a = my_function()
print(a)  # out: (2, 3)

a, b = my_function()
print(a)  # out: 2
print(b)  # out: 3

a, _ = my_function()  #  b es ignorado
print(a)  # out: 2

Además añadimos un punto de entrada debajo de la función is_admin().

if __name__ == "__main__":
    is_admin, is_win = is_admin()
    # Converting boolean to integer 
    ret = int(is_admin == False)
    if is_win:
        exit(ret)
    else:
        exit(ret * -1)

Comprobar si __name__ es __main__, es una excelente forma de hacer que cada unidad de fichero Python sea ejecutable si se quiere. El nombre de __main__ sólo es dado al fichero Python que es dado al intérprete para ser ejecutado y en caso de no serlo, siempre puede ser importado en otro fichero Python, sin que el punto de entrada afecte.

Como vemos, ahora estamos recogiendo en el retorno is_admin e is_win. Necesitamos el segundo valor para decidir el valor de retorno, ya que los códigos de error en Windows son enteros positivos y en GNU/Linux son enteros negativos. El único punto en común es que devolver un 0 es señal de que la ejecución fue correcta. Sabiendo esto debemos adaptar el retorno de los errores dependiendo el sistema.

💾 Binarizando nuestra aplicación Python

Para convertirlo en un ejecutable usaremos PyInstaller, el cual podéis instalar con un simple pip install pyinstaller. Para binarizar nuestra aplicación en un ejecutable autocontenido con el interprete de Python ejecutaremos:

pyinstaller -F is_admin.py

De esta forma resultará, mucho mas sencilla la distribución de nuestra aplicación.

👔 Puesta en Producción

Ahora vamos a plantear los dos casos de uso para Windows y GNU/Linux. Como aclaración, decir que los ejemplos expuestos se pueden usar tanto con la versión binarizada o con la de código invocando al intérprete de Python.

Windows

@echo off

cd
cd /D "%~dp0"
cd

#REM python is_admin.py
is_admin.exe

if errorlevel 1 (
   echo Exit Code is %errorlevel%
   echo.
   echo Admin privileges required
   echo Run it again as Administrator
   echo.
   pause
   exit /b %errorlevel%
)

#REM [your_executable].exe

pause

👀 Puedes descargar la versión binarizada de is_admin.exe

GNU/Linux

#!/bin/bash

pwd
cd `dirname $0`
SCRIPTDIR=`pwd`
pwd

#python is_admin.py
is_admin


if [ $? -eq 0 ]
then
  # [your_executable]
else
  echo Exit Code is $?
  echo.
  echo Admin privileges required
  echo Run it again as Administrator
  echo.
  read -rsp $'Press any key to continue...\n' -n 1 key
  exit $?
fi

read -rsp $'Press any key to continue...\n' -n 1 key

Si no estás tan familiarizado con el Bash te invito a que visites el proyecto ExplainShell para que puedas obtener la explicación de los diferentes comandos y argumentos.

🥅 Conclusiones

La solución presentada, sin ser seguramente perfecta, asegura tanto a administradores de sistemas como desarrolladores, comprobar si existen los permisos de administrador necesarios de manera sencilla; pudiendo hacerlo con una versión binarizada autocontenida que incluya el intérpretete Python, ejecutándolo usando el intérprete de Python del sistema gracias a ser todo dependencias internas, o llamando directamente a la función is_admin() si se quiere integrar en un desarrollo.

El código completo puedes encontrarlo en el siguiente gist de GitHub:

Espero que esta forma de trabajar te resulte útil y puedas extrapolarla y usar para otros casos Python como tu lenguaje pegamento generando código y aplicaciones altamente reutilizables.

Gestión de dependencias y versionado en Python

Cuando programas una de las cosas más importantes para asegurar la compatibilidad y el soporte de una librería, herramienta o programa, es el versionado del mismo.

El versionado sirve para que a nivel interno, el equipo de desarrolladores pueda llevar un control de hitos dentro del desarrollo y a su vez sirve para que otros desarrolladores o usuarios, que usan estas librerías, herramientas o programas, puedan tener un control a la hora de integrarlas o actualizarlas.

Ahora bien, el problema viene a la hora de establecer la significación de dicho número de versión. En Python es normal usar el sistema SemVer, el cual establece tres grupos de números separados por puntos de la forma MAJOR.MINOR.PATCH

  • MAJOR: Se incrementa cuando cuando se realizan cambios incompatibles a nivel de API.
  • MINOR: Se incrementa cuando se añaden funcionalidad de manera retrocompatible.
  • PATCH: Se incrementa cuando se corrigen 🐛 fallos de manera retrocompotible

Es posible que estuvieras familiarizado con este sistema de versionado, pero aún así te recomiendo su lectura completa en https://semver.org.

Teniendo esto claro, vamos a pasar a ver la aplicación práctica que que tiene esto en la gestión de dependencias, donde la generación de paquetes para su distribución en Pipy es donde se marca la diferencia. Podemos ajustar que nuestro paquete maneje las dependencias de acuerdo a criterios de versión de Python, debido a casos como que una librería específica deje de dar soporte a ciertas versiones de Python a partir de cierta versión.

En el caso citado, hay dos opciones directas que son, o retirar soporte a ciertas versiones de Python propagando la restricción de alguna de nuestras dependencias, o controlar las versiones de las dependencias dependiendo de la versión de Python a utilizar. Veamos cómo:

colorama==0.3.7; python_version < '3.4.*'
colorama>=0.3.7; python_version >= '3.4.*'

En este caso definimos en nuestro requirements.txt la condición de que para versiones de Python menores a 3.4.* es necesario usar la versión de colorama 0.3.7, ya que dicha dependencia retira el soporte a versiones inferiores desde esa versión. Para versiones iguales o mayores a 3.4.*, simplemente se exige una versión mayor o igual a 0.3.7. Atención a que siempre se citan los tres grupos de números.

Otras veces puede pasar que cierta versión de una dependencia nos dé problemas y queramos convertirla en una excepción al requerimiento

fake_library>1.6.2,!=1.7.2

En este caso de la librería inventada fake_library exigiría una versión superior a 1.6.2, pero que no sea 1.7.2.

Por último es posible instalar dependencias que sean necesarias para ciertos sistemas operativos. Veamos un ejemplo:

pywin32 >=1.0.*; sys_platform == 'win32'
SistemaValor platform
AIX'aix'
GNU/Linux'linux'
Windows'win32'
Windows/Cygwin'cygwin'
macOS'darwin'

https://docs.python.org/3/library/sys.html#sys.platform

Para el ejemplo se especifica una versión superior o igual a 1.0.* de pywin32 en el caso de encontrarnos en un sistema Windows.

Finalmente es posible que en el setup.py de una librería, herramienta o aplicación, se obligue a contar con unas versión especificas de Python y/o evitar otras:

from setuptools import setup
 
setup(
    name='mypackage',
    packages=['mypackage'],
    version='0.1',
    license='LGPL v3',
    description='A random test lib',
    author='RDCH106',
    author_email='contact@rdch106.hol.es',
    url='https://github.com/RDCH106/mypackage',
    download_url='https://github.com/RDCH106/parallel_foreach_submodule/archive/v0.1.tar.gz',
    keywords='test example develop', 
    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', # requerimientos de versión de Python
    classifiers = ['Programming Language :: Python',
                   'Programming Language :: Python :: 2.7',
                   'Programming Language :: Python :: 3.4',
                   'Programming Language :: Python :: 3.5',
                   'Programming Language :: Python :: 3.6',
                   'Programming Language :: Python :: 3.7',
                   'Programming Language :: Python :: 3.8'],
)

En la línea 14 se puede ver que se especifica que se requiere versiones de Python mayores a 2.7, pero que no sean ni 3.0.*, 3.1.*, 3.2.* o 3.3.*.

Todas estas reglas condicionales para la gestión de dependencias quedan reflejadas en las PEP (Python Enhancement Proposal) 345, 440, 496 y 508 principalmente.

Espero que todo esto sirva para evitar caer en el llamado “infierno de las dependencias”, mientras crece tu librería, herramienta o aplicación.

Aprende a programar en Python – Introducción – 0

Llevaba tiempo mascando esta serie de entradas para aprender a programar en Python, para las cuales no quise quedarme en algo del tipo mira, aprende y copia. El curso tiene un enfoque práctico con su parte de teoría y ejercicios donde corresponda. Atendiendo a las actuales necesidades formativas he decidido que el curso y su material podrá ser trabajado de manera online utilizando únicamente un navegador web.

Para ello he creado un repositorio en GitHub con dos herramientas que podréis elegir usar de manera online:

Ambos proyectos usan como base el intérprete de Brython, un proyecto que intenta emular el interprete de Python que puedes instalarte en tu equipo. Este proyecto permite saltarse el problema de no poder instalar el interprete de Python debido a restricciones en el equipo que uses, por ejemplo si dependes del departamento de Sistemas para instalarlo, o simplemente si quieres retomar el curso en cualquier instante sin necesidad de instalar nada.

El proyecto de la Consola Python Online es una consola que se comporta de manera similar a la que puedes tener cuando lanzas Python desde una consola de comandos. El proyecto IDE Python Online es un entorno de desarrollo integrado donde podrás en su parte izquierda realizar la programación con realce de sintaxis y ayuda de autocompletado predictivo de código; y en su parte derecha podrás ver el resultado del código escrito a la izquierda tras darle al botón "▶ Run" de la parte superior. Con estas dos herramientas tendrás lo que te hace falta para seguir el curso.

Este proyecto es de carácter Open Source, siendo posible contribuir en el propio repositorio a la mejora de los fragmentos de código, ejercicios y herramientas.

Antes de empezar quisiera explicarte qué es Python y ofrecerte una pequeña aproximación de su historia. Python es un lenguaje de programación interpretado, lo que significa que no necesita compilarse y generar un binario para funcionar, se interpreta el código y el encargado de hacerlo (el intérprete), lo traducen a lenguaje máquina. Esto quiere decir también que con vuestro código en Python sólo necesitáis que un intérprete de Python esté instalado en la máquina para ejecutar el código, o usar las herramientas que anteriormente presentaba 😉 .

Python es un excelente lenguaje de programación si quieres aprender una herramienta que potencie o complemente tu trabajo, como es el caso del clásico Excel. O simplemente como tu primer lenguaje de programación, debido a que su curva de aprendizaje es mucho más baja a diferencia de otros lenguajes, haciendo hincapié en una sintaxis que favorece la legibilidad del código. Además Python es multiparadigma, lo que significa que puede acomodarse a distintos enfoques y necesidades de programación, incluyendo el buen diseño por defecto en la propia sintaxis y evolución del lenguaje.

Actualmente Python se encuentra en un proceso de migración de Python 2 a Python 3, debido a que el soporte de Python 2 acaba el 1 de enero de 2020, terminando con un estado de segmentación del lenguaje que llevaba varios años produciéndose. El salto de Python 2 a Python 3 supone el cambio del diseño de cierta parte de la sintaxis que rompe la compatibilidad y evitaba a Python avanzar hacia el Python que hoy conocemos y que tanta penetración tiene en los sectores técnicos y no tan técnicos. No obstante existen herramientas como 2to3 que realizan la adaptación automática de la sintaxis de Python 2 a Python3. Obviamente no es perfecto y queda en manos del programador el terminar de pulir la migración. La versión del lenguaje que aprenderás aquí, por supuesto es Python 3 😉 .

Para ir abriendo boca voy a mostrarte el clásico programa de inicio "Hello World!" o "Hola Mundo!" en español:

Si le das al botón "▶ Run" leerás en la derecha "Hello World" seguido del tiempo que ha tardado en ejecutarse nuestro programa (lo que hay en el panel izquierdo).

Ahora te propongo un simple ejercicio hasta la próxima entrada. Partiendo del siguiente código:

Intenta que imprima en el lado derecho "Hello World". Es fácil y seguro que lo consigues 😉 .

Con esto cerramos el capítulo de introducción donde hemos conocido las herramientas que vamos a usar para el curso, hemos presentado Python y hemos ejecutado nuestro primer programa.

Cómo subir un paquete a PyPI

Recientemente un compañero mío quería distribuir una pequeña librería en Python que había escrito. En lugar de forzar a los desarrolladores a clonar su repositorio, quería que pudieran instalarse la librería con un sólo comando "pip install". Planteamiento muy interesante que se formaliza con el concepto de gestor de paquetes.

Un gestor permite un uso más inteligente, ágil y seguro de las dependencias y ayuda tanto al que distribuye el paquete como al que lo usa, resolviendo problemas como el versionado, la cadena de dependencias dentro del paquete, la instalación...

Quisiera a fin de fomentar el uso del gestor de paquetes de Python llamado pip, que usa como repositorio oficial PyPI (Python Package Index), generar una guía lo más completa posible. ⚠️ Me gustaría remarcar que con ello no quiero ni pretendo incentivar que cualquiera se dedique a subir de cualquier forma lo primero que se lo ocurra. Aludo a la responsabilidad de cada uno, para no convertir PyPI en un vertedero de paquetes de dudosa calidad. ⚠️

¿Qué es PyPI?

Desde la web oficial:

PyPI — Índice de Paquetes Python

El Índice de Paquetes Python es un repositorio de software para el lenguaje de programación de Python.

¿Has programado algo genial? ¿Quieres que otros puedan instalarlo con easy_install o pip? Pon tu código en PyPI. Es una gran lista de paquetes de Python donde debes enviar tu paquete para que pueda instalarse fácilmente con uno sólo comando.

La buena noticia es que enviar un paquete a PyPI en la teoría es muy simple: registrarte y cargar tu código, todo de manera gratuita. La mala noticia es que en la práctica es un poco más complicado que eso 😅. La otra buena noticia es que he escrito esta guía 😁 y que, si estás atascado, siempre puedes consultar la documentación oficial 😉.

He escrito esta guía con los siguientes supuestos:

  •      El módulo / biblioteca / paquete que está enviando se llama mypackage.
  •      mypackage está alojado en GitHub.

Crear tu cuenta

Crea una cuenta en PyPI Live y también en PyPI Test. Debes crear una cuenta para poder cargar tu código. Te recomiendo usar el mismo correo electrónico y contraseña para ambas cuentas, sólo para hacerte la vida más fácil cuando llegue el momento de subir tu código. Ambas plataformas son idénticas, siendo la de test una réplica de la oficial para que pruebes a subir tus paquetes.

Crear un archivo de configuración .pypirc

Este archivo contiene su información para la autenticación con PyPI, tanto la versión en vivo como la versión de prueba.

[distutils]
index-servers =
  pypi
  pypitest

[pypi]
repository=https://upload.pypi.org/legacy/
username=your_username
password=your_password

[pypitest]
repository=https://test.pypi.org/legacy/
username=your_username
password=your_password

Esto es sólo para hacer tu vida más fácil, para que cuando llegue el momento de subir el código de de tu paquete no tengas que recordar/escribir tu nombre de usuario y contraseña. Asegúrate de poner este archivo en tu carpeta de inicio de tu sistema GNU/Linux; tu ruta debe ser:

~/.pypirc

Debido a que este archivo contiene tu nombre de usuario y contraseña, es posible que desees cambiar sus permisos para que sólo tú puedas leerlos y escribirlos. Desde la terminal, ejecuta:

chmod 600 ~/.pypirc

Si por el contrario te encuentras en un sistema Windows genera el archivo .pypirc en la carpeta de tu usuario en C:\Users\usuario con el siguiente comando:

type nul > your_file.txt
👁️ Notas sobre usernames / passwords

En Python 3, si tu contraseña incluye un % sin procesar, debes generar una secuencia de escape duplicándolo. El analizador de configuración de .pypirc interpola las cadenas de texto. Por ejemplo, si tu contraseña es hello%world:

[pypi]
repository=https://pypi.python.org/pypi
username=myusername
password=hello%%world

Nunca me he encontrado con este problema, pero si te ocurre, esto podría ayudar. 😉

Este comportamiento de escape ha sido parcheado y ya no es necesario hacerlo, pero si ves un error con un código de respuesta de:

 403: Invalid or non-existent authentication information 

Intenta eliminar la secuencia de escape de los signos de porcentaje en tu contraseña.

Si tu contraseña incluye espacios, asegúrese de no entrecomillarla. Por ejemplo, si tu contraseña es me encanta Mascando Bits:

[pypi]
repository=https://pypi.python.org/pypi
username=myusername
password=me encanta MascandoBits

Preparar el paquete

Cada paquete en PyPI necesita tener un archivo llamado setup.py en la raíz del directorio. Si estás utilizando un archivo README en formato markdown, también necesitará un archivo MANIFEST.in. Además, es recomendable elegir una licencia para tu paquete reflejada en un archivo LICENSE que describa lo que se puede hacer con tu código. Entonces, si por ejemplo he estado trabajando en una biblioteca llamada mypackage, la estructura de mi directorio se vería tal que así:

root-dir/   # nombre de directorio de trabajo aleatorio
  setup.py
  MANIFEST.in
  LICENSE.txt
  README.md
  mypackage/
    __init__.py
    foo.py
    bar.py
    baz.py

A continuación un desglose de lo que va en cada archivo:

setup.py

Son los metadatos de la librería necesarios para generar el paquete.

from setuptools import setup

setup(
    name='mypackage',
    packages=['mypackage'], # Mismo nombre que en la estructura de carpetas de arriba
    version='0.1',
    license='LGPL v3', # La licencia que tenga tu paquete
    description='A random test lib',
    author='RDCH106',
    author_email='contact@rdch106.hol.es',
    url='https://github.com/RDCH106/mypackage', # Usa la URL del repositorio de GitHub
    download_url='https://github.com/RDCH106/parallel_foreach_submodule/archive/v0.1.tar.gz', # Te lo explico a continuación
    keywords='test example develop', # Palabras que definan tu paquete
    classifiers=['Programming Language :: Python',  # Clasificadores de compatibilidad con versiones de Python para tu paquete
                 'Programming Language :: Python :: 2.7',
                 'Programming Language :: Python :: 3.3',
                 'Programming Language :: Python :: 3.4',
                 'Programming Language :: Python :: 3.5',
                 'Programming Language :: Python :: 3.6',
                 'Programming Language :: Python :: 3.7'],
)

El parámetro download_url es un enlace a un archivo alojado con el código de tu repositorio. Github alojará esto para ti, pero solo si creas una etiqueta git tag. En tu repositorio, escribe en la línea de comandos:

git tag v0.1 -m "Agrega una etiqueta para que podamos poner esto en PyPI"

Luego, escribe el siguiente comando para mostrar la lista de etiquetas:

git tag

Deberías ver v0.1 en la lista.

Por último escribe el siguiente comando para actualizar tu código en Github con la información de etiqueta más reciente:

git push --tags origin master

Github creará archivos comprimidos para descargar en https://github.com/{username}/{package_name}/archive/v{tag}.tar.gz.

MANIFEST.in

Incluye archivos que se empaquetarán en al distribución del paquete.

include LICENSE.txt README.md

LICENSE.txt será el archivo que contiene la licencia y README.md será el fichero que contenga la información básica (instalación, uso...) que quieras distribuir con tu paquete.

Subir tu paquete a PyPI Test

Ejecuta:

python setup.py sdist upload -r pypitest

No debería recibir ningún error si seguiste los pasos hasta este punto, y ahora también deberías poder ver tu paquete en el repositorio PyPI de prueba. Si hay algo que no te gusta este es el momento de hacer los cambios que quieras. Eso sí, para subir el paquete de nuevo tendrás que editar el número de versión para que sea distinto al de la anterior vez.

⚠️ PyPI te permite borrar las versiones del paquete y subir todos las versiones que quieras, pero no editar una versión de tu paquete subida. Esto es para mantener la estabilidad de las dependencias. Es posible retirar una versión, creando la consecuente ruptura de dependencias, pero no se deja modificar para una versión de un paquete por no desvirtuar el sentido mismo del versionado.

Es posible que en algunas guía o documentación hayas visto que también se hace previo al upload un comando register:

python setup.py register -r pypitest

Ya no es necesario y no está soportado:

https://packaging.python.org/guides/migrating-to-pypi-org/#registering-package-names-metadata


Subir tu paquete a PyPI Live

Ejecuta:

python setup.py sdist upload -r pypi

Y ya, !lo has hecho! 🎉🎉🎉 Has publicado tu primer paquete en PyPI y podrá ser instalado con el gestor de paquetes pip o cualquier otro como conda.