¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Crear software instalable
La diferencia entre un script y un programa, es que es script es un único archivo python que se ejecuta desde su ubicación. Puede ser muy útil, especialmente para pequeñas tareas, pero un programa software es más profesional y tiene muchas más ventajas: ejecutarlo desde cualquier ubicación, manejar información estructurada (JSON, Bases de datos), usar módulos (diferentes archivos de biblioteca).
No es que no pueda hacerse todo esto con un script, pero es más complejo y es preferible estructurarlo.
La mejor forma de construir un programa python es estructutarlo como un paquete Python, con soporte para control de versiones, en nuestro caso GitHub. De este modo podrá instalarse a través del comando pip.
Estructura del paquete Python
Para que nuestro proyecto pueda beneficiarse de ser un paquete Python, debe tener la siguiente estructura:
nombre_proyecto/ │ ├── nombre_programa/ # Paquete principal │ ├── __init__.py │ ├── programa.py # Tus funciones │ └── main.py # Punto de entrada │ ├── setup.py # Script de instalación ├── README.md # (Opcional) ├── LICENSE # Licencia └── .gitignore # Archivos a ignorar en la sincronización con el repositorio
A continuación comento cada uno de estos archivos.
__init__.py
Este archivo indica a Python que esta carpeta es un paquete (podría estar vacía).
Funciona como un “expositor” de las funciones principales del paquete, de modo que puede incluir código que define qué funciones o clases están disponibles al hacer import.
Ejemplo:
from .gendocs import generar_documentos_simples_xlsx, generar_documentos_compuestos_xlsx, cruce_documentos
main.py
Es el punto de entrada al paquete cuando lo ejecutas desde consola.
Es donde se define la interfaz de línea de comandos (CLI): qué comandos, opciones y argumentos puede introducir el usuario, incluidos los –help.
Utiliza la biblioteca argparse para leer argumentos desde línea de comandos.
En main.py se puede:
- Leer argumentos desde la terminal.
- Validar entradas del usuario.
- Ejecutar las funciones de tu módulo según esos argumentos.
- Mostrar mensajes, progreso (por ejemplo con barras con la librería tqdm) o errores.
- Devolver resultados al usuario (PDF generados, logs, rutas, etc.).
En definitiva, realiza el flujo del programa.
A continuación un ejemplo, en el que se define el comando “gendocs” y 3 subcomandos “simples”, “compuestos” y “cruce”, cada uno con sus parámetros de entrada:
# Este archivo es el punto de entrada al paquete cuando lo ejecutas desde consola: Define la función main() que se ejecuta cuando alguien escribe "docgen ..." # Usa argparse para leer argumentos desde línea de comandos. import argparse import os from gendocs.gendocs import ( generar_documentos_simples_xlsx, generar_documentos_compuestos_xlsx, cruce_documentos ) def main(): parser = argparse.ArgumentParser(description="Generador de documentos Word/PDF, a partir de los datos de un documento Excel y plantillas Word") subparsers = parser.add_subparsers(dest="comando", required=True, help="Elige el tipo de generación") # Los argumentos obligatorios deben ir en orden, los opcionales deben llamarse igual que en la función. # Subcomando: Documentos simples parser_simple = subparsers.add_parser( "simple", help="Genera documentos simples a partir de un Excel" ) parser_simple.add_argument("excel", help="Ruta al archivo Excel") parser_simple.add_argument("hoja", help="Nombre de la hoja del Excel") parser_simple.add_argument("plantilla", help="Ruta a la plantilla Word") parser_simple.add_argument("salida", help="Directorio donde se guardarán los documentos") parser_simple.add_argument("--guardar_ruta_pdf", action="store_true", help="Guarda la ruta del PDF en el Excel") parser_simple.add_argument("--normalizar", action="store_true", help="Normaliza nombres de archivo") parser_simple.add_argument("--encabezado_ruta_pdf", default="RutaPDF", help="Nombre del encabezado para ruta PDF") parser_simple.add_argument("--separador", default="_", help="Separador para el nombre del archivo") parser_simple.add_argument("--nombre_archivo", nargs="*", help="Lista de campos para construir el nombre del archivo") # Subcomando: Documentos compuestos parser_compuesto = subparsers.add_parser( "compuesto", help="Genera documentos agrupados a partir de un Excel" ) parser_compuesto.add_argument("excel", help="Ruta al archivo Excel") parser_compuesto.add_argument("hoja", help="Nombre de la hoja del Excel") parser_compuesto.add_argument("plantilla", help="Ruta a la plantilla Word") parser_compuesto.add_argument("salida", help="Directorio donde se guardarán los documentos") parser_compuesto.add_argument("--campos_agrupar", nargs="+", required=True, help="Campos del Excel para agrupar") parser_compuesto.add_argument("--guardar_ruta_pdf", action="store_true", help="Guarda la ruta del PDF en el Excel") parser_compuesto.add_argument("--normalizar", action="store_true", help="Normaliza nombres de archivo") parser_compuesto.add_argument("--encabezado_ruta_pdf", default="RutaPDF", help="Nombre del encabezado para ruta PDF") parser_compuesto.add_argument("--separador", default="_", help="Separador para el nombre del archivo") parser_compuesto.add_argument("--nombre_archivo", nargs="*", help="Lista de campos para construir el nombre del archivo") parser_compuesto.add_argument("--hoja_docs", default="Documentos", help="Nombre de la hoja donde guardar las rutas") # Subcomando: Cruce automático parser_cruce = subparsers.add_parser( "cruce", help="Elige automáticamente entre documento simple o compuesto según agrupación" ) parser_cruce.add_argument("excel", help="Ruta al archivo Excel") parser_cruce.add_argument("hoja", help="Nombre de la hoja del Excel") parser_cruce.add_argument("plantilla_simple", help="Ruta a la plantilla Word para documentos simples") parser_cruce.add_argument("plantilla_resumen", help="Ruta a la plantilla Word para documentos compuestos") parser_cruce.add_argument("salida", help="Directorio donde se guardarán los documentos") parser_cruce.add_argument("--campos_agrupar", nargs="+", required=True, help="Campos del Excel para agrupar") parser_cruce.add_argument("--guardar_ruta_pdf", action="store_true", help="Guarda la ruta del PDF en el Excel") parser_cruce.add_argument("--normalizar", action="store_true", help="Normaliza nombres de archivo") parser_cruce.add_argument("--encabezado_ruta_pdf", default="RutaPDF", help="Nombre del encabezado para ruta PDF") parser_cruce.add_argument("--separador", default="_", help="Separador para el nombre del archivo") parser_cruce.add_argument("--nombre_archivo", nargs="*", help="Lista de campos para construir el nombre del archivo") parser_cruce.add_argument("--hoja_docs_simples", default="Documentos simples", help="Nombre de la hoja para documentos simples") parser_cruce.add_argument("--hoja_docs_compuestos", default="Documentos compuestos", help="Nombre de la hoja para documentos compuestos") args = parser.parse_args() os.makedirs(args.salida, exist_ok=True) # Asegura que la carpeta de salida existe if args.comando == "simple": generar_documentos_simples_xlsx( doc_excel=args.excel, hoja_excel=args.hoja, doc_word=args.plantilla, directorio=args.salida, nombre_archivo=args.nombre_archivo or [], separador=args.separador, normalizar=args.normalizar, guardar_ruta_pdf=args.guardar_ruta_pdf, encabezado_ruta_pdf=args.encabezado_ruta_pdf ) elif args.comando == "compuesto": generar_documentos_compuestos_xlsx( doc_excel=args.excel, hoja_excel=args.hoja, doc_word=args.plantilla, directorio=args.salida, campos_agrupar=args.campos_agrupar, nombre_archivo=args.nombre_archivo or [], separador=args.separador, normalizar=args.normalizar, guardar_ruta_pdf=args.guardar_ruta_pdf, encabezado_ruta_pdf=args.encabezado_ruta_pdf, hoja_docs=args.hoja_docs ) elif args.comando == "cruce": cruce_documentos( doc_excel=args.excel, hoja_excel=args.hoja, plantilla_word_simple=args.plantilla_simple, plantilla_word_resumen=args.plantilla_resumen, directorio=args.salida, campos_agrupar=args.campos_agrupar, nombre_archivo=args.nombre_archivo or [], separador=args.separador, normalizar=args.normalizar, guardar_ruta_pdf=args.guardar_ruta_pdf, encabezado_ruta_pdf=args.encabezado_ruta_pdf, hoja_docs_simples=args.hoja_docs_simples, hoja_docs_compuestos=args.hoja_docs_compuestos ) else: parser.print_help() if __name__ == "__main__": main()
Un ejemplo de comando sería:
gendocs cruce ` "E:\OneDrive CAD\OneDrive - Universidad de Alcala\CURSO 2024-25\Taller 3º Anamnesis e historia clínica\Taller de Anamnesis e Historia Clínica 24_25(1-8).xlsx" ` "Hoja1" ` "E:\OneDrive CAD\OneDrive - Universidad de Alcala\CURSO 2024-25\Taller 3º Anamnesis e historia clínica\Plantilla certificado simple.docx" ` "E:\OneDrive CAD\OneDrive - Universidad de Alcala\CURSO 2024-25\Taller 3º Anamnesis e historia clínica\Plantilla certificado compuesto.docx" ` "E:\OneDrive CAD\OneDrive - Universidad de Alcala\CURSO 2024-25\Taller 3º Anamnesis e historia clínica\Certificados" ` --campos_agrupar "Nombre" "Apellidos" "Correo electrónico" ` --guardar_ruta_pdf ` --normalizar ` --separador _ ` --nombre_archivo Apellidos Nombre "-" Taller "24-25" ` --hoja_docs_simples "Documentos simples" ` --hoja_docs_compuestos "Documentos compuestos"
setup.py
Es el script de instalación. Usa la librería setuptools para convertir este proyecto en un paquete instalable con pip.
Describe los metadatos (nombre, versión, dependencias, etc.), permite que el proyecto se instale localmente o se distribuya y configura comandos que puedas ejecutar desde cualquier terminal.
Crea el comando: “gendocs –help”, que ejecuta la función main() que está en nombre_programa/main.py
Ejemplo:
from setuptools import setup, find_packages setup( name="gendocs", # Nombre del paquete version="1.0.0", # Versión packagez=find_packages(), # Encuentra automáticamente los subpaquetes entry_points={ # Punto de entrada para crear un comando en consola "console_scripts": [ "gendocs=gendocs.main:main", # Conecta el comando docgen a la función main() ], }, install_requires=[ # Dependencias "python-docx", "docx2pdf", "openpyxl" ], python_requires = ">=3.8", # Versión mínima de Python )
readme.md
Es un archivo de texto (usualmente en formato Markdown, de ahí la extensión .md) que explica de qué trata el proyecto.
Sirve como presentación o guía rápida del proyecto. Suele incluir:
- Qué hace el software
- Cómo se instala o se ejecuta
- Ejemplos de uso
- Dependencias o requisitos
- Cómo contribuir al proyecto (si es abierto)
- Créditos y contacto
Ejemplo:
# Mi Proyecto Genial Este programa convierte texto a voz. ## Instalación ```bash pip install mi-proyecto-genial
LICENSE
Archivo que especifica los términos legales de uso del software. Indica:
- Si se puede copiar, modificar y compartir.
- Si hay restricciones (por ejemplo, uso no comercial).
- Quién es el propietario legal del código.
- Qué responsabilidad (o ausencia de ella) tiene el autor.
Algunos ejemplos de licencias:
- MIT: muy permisiva, permite casi todo mientras se dé crédito al autor.
- GPL: código libre, pero si lo modificas y compartes, debe seguir siendo libre.
- Apache 2.0: parecida a MIT pero con más detalles sobre patentes.
No es obligatorio incluirlo, pero sin licencia, legalmente nadie puede usar tu código, aunque esté en GitHub. Pueden encontrarse en sus respectivas páginas web.
Visibilidad de funciones
Puede que no quieras que el usuario al que está destinado el programa tenga acceso a todas las funciones, aunque sean necesarias internamente. Para eso existen 2 opciones:
- Incluir un '_' en el nombre de cada función: No impide su uso técnicamente, pero sí es una convención muy respetada en Python (como decir “esto no lo toques”).
- Usar all en el programa principal para controlar qué se exporta. Ejemplo:
__all__ = [ "generar_documento_simple", "generar_documento_compuesto", ] # De este modo, si alguien importa con from certificados_lib import *, solo se cargan esas funciones visibles.
Instalación y distribución
Local
Para instalar el programa en tu máquina local, desde la consola de comandos debes navegar hasta onde se encuentre el archivo setup.py y ejecutar:
pip install -e . # Sustituye '.' por la ruta, si no estás en el mismo directorio que setup.py
El instalador añade el comando (el nombre que hayas puesto en entry_points) al PATH de tu entorno Python.
Además, la carpeta desde donde haces la instalación en modo editable (pip install -e .) es, literalmente, la carpeta donde está el código que se ejecuta: Python crea un enlace simbólico (acceso directo interno) desde el entorno Python hacia la carpeta donde está tu código (donde está setup.py).
Por eso cualquier cambio en esa carpeta afecta directamente al paquete instalado y ejecutable, sin necesidad de reinstalar.
