Mostrando entradas con la etiqueta psycopg2-binary. Mostrar todas las entradas
Mostrando entradas con la etiqueta psycopg2-binary. Mostrar todas las entradas

viernes, 24 de noviembre de 2023

17.3 Base de datos PostgreSQL. Busquedas, stemming y ranking de resultados.

En las aplicaciones web es una práctica común la realización de búsquedas en la base de datos a partir de entradas proporcionadas por el usuario. A continuación vamos a ver como introducir estas capacidades de búsqueda en la aplicación Blog.

El ORM de Django nos permite realizar búsquedas sencillas en la base de datos usando el filtro "contains" que no distingue por ejemplo entre mayúsculas o minúsculas. Por ejemplo podemos utilizarlo para ver los post que contienen la palabra "funciona" dentro del cuerpo del post.

from blog.models import Post
Post.objects.filter(contenido__contains='funciona')
Sin embargo, si queremos realizar búsquedas más complejas necesitamos un motor de búsqueda más complejo. Django proporciona una poderosa funcionalidad de búsqueda pero basada en la base de datos PostgreSQL. El módulo django.contrib.postgres proporciona funcionalidades ofrecidas por PostgreSQL que no son compartidas por las otras bases de datos que admite Django. Puedes obtener información sobre la búsqueda de texto completo de PostgreSQL en https://www.postgresql.org/docs/14/textsearch.html.


Instalando PostgreSQL.


Hasta el momento en nuestro proyecto estamos usando la base de datos SQLlite. Sin embargo, PostgreSQL es mucho mejor a la hora de realizar búsquedas de texto complejas en la base de datos. Vamos a migrar nuestra base de datos de SQlite a PostgreSQL para beneficiarnos de sus mejores características.

SQlite está muy bien para entornos de desarrollo, sin embargo para un entorno de producción, necesitarás una base de datos más poderosa como PostgreSQL, MariaDB, MySQL etc.

Ya habíamos comentado anteriormente como instalar PostgreSQL, puedes encontrar en enlace aquí.
Vamos a resumirlo. Como yo trabajo con una distribución Debian, los comandos de instalación son los siguiente:
 
$ sudo apt-get install postgresql postgresql-contrib

# Entramos en la consola de postgresql
$ sudo su - postgres
postgres@lenovo:~$ psql
psql (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1))
Type "help" for help.

# Creamos un usuario y su contraseña. 
# En el ejemplo usuario = "tu_usuario" y contraseña="xxxxxxxx"
postgres=# CREATE USER tu_usuario WITH PASSWORD 'xxxxxxxx';
Salida: CREATE ROLE # Creamos una base de datos de ejemplo. Se pude poner el nombre # que se quiera. # En el ejemplo aplicacion_db postgres=# CREATE DATABASE aplicacion_db OWNER tu_usuario ENCODING 'UTF8';
Salida: CREATE DATABASE # Para salir del terminal de postgresql y volver al terminal. postgres-# \q postgres@lenovo:~$ exit Salida: logout

También necesitamos instalar el adaptador psycop2 PostgreSQL para Python. Ejecuta el siguiente comando en el shell para instalarlo:

pip install psycopg2-binary


Realizando una Copia de los datos existentes.


Antes de cambiar de base de datos en nuestra aplicación, necesitamos realizar una copia de los datos existentes en la base de datos SQlite. Exportaremos esos datos, cambiaremos la base de datos a PostgreSQL e importaremos los datos otra vez a la nueva base.

Django viene con una forma muy sencilla de cargar y grabar datos de una base a un archivo. Se puede hacer en formado JSON, XML o YAML. Vamos a guardar todos los registros de nuestra base de datos en un archivo. Ejecuta el siguiente comando desde el shell:

python manage.py dumpdata --indent=2 --output=mysite_data.json

La salida del comando será parecido a esto:

[................................................................................................................]

Todos los datos existentes han sido exportados en formato JSON a un nuevo archivo llamado "mysite_data.json". Si tienes un error mientras ejecutas el comando, usa el siguiente comando para activar el modo UTF-8 de Python:

python -Xutf8 manage.py dumpdata --indent=2 --output=mysite_data.json

Ahora cambiaremos la base de datos en el proyecto de Django y volveremos a importar los datos a la nueva base.


Reemplazando la base de datos en el proyecto.


Edita el archivo settings.py del proyecto y modifica el apartado DATABASES de la siguiente forma:

PracticaDjango/PracticaDjango/settings.py

  
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'aplicacion_db', # nombre de la base de datos.
        'USER': 'tu_usuario', # utiliza el usuario que hayas escogido.
        'PASSWORD': 'xxxxxxxx', # Cambia las xxxxxxxx por la clave que hayas puesto.
        'HOST': '127.0.0.1',
        'DATABASE_PORT': '5432',
    }
}


Reemplaza las 'xxxxxxxx' con la contraseña que usaste cuando creaste el usuario de la base de datos PostgreSQL. La nueva base de datos está vacia.

Ejecuta el siguiente comando para realizar las migraciones de la nueva base de datos PostgreSQL.

python manage.py migrate

En terminal verás un montón de salidas sobre las migraciones que se están aplicando.


Cargando los datos en la nueva base de datos.


Ejecuta el siguiente comando para cargar de nuevo los datos en la base de datos.

python manage.py loaddata mysite_data.json

Deberías ver la siguiente salida en el terminal:

Installed 104 object(s) from 1 fixture(s)

El número de objetos puede diferir en tu proyecto dependiendo del modelo.

SIN EMBARGO PUEDES ENCONTRARTE CON ESTE PROBLEMA (si no te ocurre sigue adelante, no hace falta que leas esto)

django.db.utils.IntegrityError: Problem installing fixture '/home/chema/Projects/PYTHON/DJANGO/PracticaDjango/mysite_data.json': Could not load contenttypes.ContentType(pk=1): llave duplicada viola restricción de unicidad «django_content_type_app_label_model_76bd3d3b_uniq»
DETAIL:  Ya existe la llave (app_label, model)=(admin, logentry).

ContentType es una especie de registro de los modelos de una aplicación, pensado como una interfaz para poder acceder a información sobre un modelo sin tener que saber mucho sobre los detalles del mIismo.
El marco de autenticación de Django utiliza ContentType para asignar permisos a modelos. La aplicación de administración de Django rastrea los cambios en sus objetos a través del modelo LogEntry que a su vez usa ContentTypes (ese es el modelo que está causando el error).

Si se consulta ContentType para un modelo que aún no conoces, se crea un nuevo registro con ese modelo: eso significa que la composición de la tabla ContentType puede diferir de un entorno a otro; depende del orden en el que se solicitan los modelos. 

Viendo el volcado del error para ContentType, y suponiendo que para cualquier modelo que use ContentType, el volcado también contiene las referencias a esa nueva tabla ContentType, deberíamos poder eliminar todas las entradas que se encuentran actualmente en la tabla ContentType local:

Para solucionarlo entraremos en el shell de Django:

python manage.py shell

y tecleamos: 

from django.contrib.contenttypes.models import ContentType
ContentType.objects.all().delete()

Con esto deberíamos poder cargar sin problemas los datos de nuevo en la base nueva. Ejecuta de nuevo:

python manage.py loaddata mysite_data.json

Puede realizar, si quieres una copia de seguridad del contenido de tu ContentType, si quieres estar seguro de no perder nada usando:

python manage.py dumpdata contenttypes.ContentType

Una vez cambiado la base de datos y cargado los datos de la base antigua en la nueva vamos a ver que todo funcione. Ejecutamos el servidor de desarrollo y comprobamos que todo, sobre todo los post, este ahí y todo funcione correctamente.


Realizando búsquedas sencillas.


Edita el archivo settings.py del proyecto y añade django.contrib.postgres al grupo de aplicaciones instaladas:

PracticaDjango/PracticaDjango/settings.py

INSTALLED_APPS = [
    # Nuestras aplicaciones
    'Proyecto_web_app.apps.ProyectoWebAppConfig',
    'Servicios.apps.ServiciosConfig',
    'Blog.apps.BlogConfig',
    'Contacto.apps.ContactoConfig',
    # Aplicaciones de terceros
    'django_bootstrap5',
    # Mapa del Sitio
    'django.contrib.sites', # add sites to installed_apps
    'django.contrib.sitemaps',  # add Django sitemaps to installed app
    # PostgreSQL
    'django.contrib.postgres',
    # Aplicaciones por defecto
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
Abre el shell de Django ejecutando el siguiente comando:

python manage.py shell

Ejecuta los siguientes comandos en el shell:

>>> from Blog.models import Post
>>> Post.objects.filter(titulo__search='ver')
<QuerySet [<Post: quinto post para ver la paginacion>, <Post: Cuarto post para ver como funciona el slug>, <Post: Un tercer post para ver como sigue>]>

Esta búsqueda usa PostgreSQL para crear un vector de búsqueda para el campo título, usando el termino "ver". Lo que se obtiene son todos los post que contienen la palabra "ver" dentro del título del post.

Buscando a través de varios campos.

Puede que quieras realizar una búsqueda a través de varios campos del modelo. En ese caso necesitaras crear un objeto SearchVector. Vamos a crear un vector para buscar en los campos 'titulo' y 'contenido' del modelo Post.

Ejecuta el siguiente código en el shell de Python:

>>> from django.contrib.postgres.search import SearchVector

>>> from Blog.models import Post

>>> Post.objects.annotate(search=SearchVector('titulo','contenido'),).filter(search='sitio')

<QuerySet [<Post: Este es el primer post de prueba>]>

>>> 

Usando 'annotate' y definiendo un SearchVector con ambos campos, puedes realizar una búsqueda contra el campo 'titulo' y 'contenido'.


Construyendo una vista de busqueda.

Vamos a construir una vista personalizada que permita a los usuario realizar búsquedas en los post que estén publicados. Lo primero que necesitaremos es crear un formulario de búsqueda. Edita el archivo forms.py de la aplicación Blog y añade el siguiente formulario:

PracticaDjango/Blog/forms.py

from django import forms
from .models import Comentario

class ComentarioForm(forms.ModelForm):
    class Meta:
        model = Comentario
        
class BusquedaForm(forms.Form):
    consulta = forms.CharField()

Usaremos el campo "consulta" para permitir a los usuarios introducir términos de búsqueda. Edita el archivo views.py de la aplicación Blog y añade el siguiente código:

/PracticaDjango/Blog/views.py

#...
# Importamos el formulario para los comentarios de cada post.
from .forms import ComentarioForm, BusquedaForm
from django.views.decorators.http import require_POST

# Para realizar busquedas en los campos de los post.
from django.contrib.postgres.search import SearchVector

#...
def busqueda_post(request):
    formulario = BusquedaForm()
    consulta = None
    resultados = []

    if "consulta" in request.GET:
        formulario = BusquedaForm(request.GET)
        if formulario.is_valid():
            consulta = formulario.cleaned_data["consulta"]
            resultados = Post.objects.annotate(
                search=SearchVector("titulo", "contenido"),
            ).filter(search=consulta)

    return render(
        request,
        "Blog/busqueda.html",
        {"formulario": formulario, "consulta": consulta, "resultados": resultados},
    )
Lo que hemos hecho es, primero, crear una instancia del formulario BusquedaForm. Para comprobar si el formulario ha sido enviado, buscamos el parámetro "consulta" en el diccionario request.GET. Enviamos el formulario usando el método GET en vez del método POST, asi que la URL resultante incluye le parámetro "consulta".

Por ejemplo http://127.0.0.1:8000/blog/busqueda/?consulta=primero

Cuando se envía el formulario, lo instanciamos con los datos suministrados por le método GET, y verificamos que los datos contenidos son válidos. Si los datos pasan la validación, buscamos en los post la entrada introducida a través de los campos titulo y contenido utilizando un vector SearchVector.

Con esto la vista para la búsqueda está lista. Ahora tenemos que crear una plantilla para mostrar el formulario y los resultados de la búsqueda cuando los usuarios realicen una consulta.

Crea un nuevo archivo dentro del directorio templates/Blog/ de la aplicación Blog y llámalo busqueda.html. Luego añade el siguiente código:

PracticaDjango/Blog/templates/Blog/busqueda.html

<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}

<!-- Cargamos la etiqueta personalizada -->
{% load blog_tags %}

<!-- Establecemos el titulo de la página -->
{% block title %}Busqueda{% endblock %}

{% block content %}

<div class="container-fluid bg-white" style="margin-bottom: 50px;">

{% if consulta %}
    <h1>Post que contienen la consulta "{{ consulta|truncatewords:15 }}"</h1>
    <h3>
    {% with resultados.count as total_results %}
        Encontrados {{ total_results }} resultado{{total_results|pluralize}}
    {% endwith %}
    </h3>
    {% for post in resultados %}
    <h4>
        <a href="{{ post.get_absolute_url }}">
            {{ post.titulo }}
        </a>
    </h4>
    {{ post.contenido|markdown|truncatewords_html:12 }}
    {% empty %}
    <p>No hay resultados para la consulta.</p>
    {% endfor %}

    <p><a href="{% url 'Blog:busqueda_post' %}">Realizar nueva búsqueda</a></p>

{% else %}
    <h1>Busqueda en los Posts</h1>
    <form method="get">
        {{ formulario.as_p }}
        <input type="submit" value="Busqueda">
    </form>
{% endif %}

</div>

{% endblock %}
 Al igual que en la vista, aquí distinguimos también si el formulario ha sido enviado comprobando la presencia del parámetro "consulta". Antes de que la consulta sea enviada, mostramos el formulario y el botón para enviarlo. Cuando el formulario se envía, mostramos los resultados de la consulta, el número total de resultados y la lista de post que coinciden con el resultado de la búsqueda.

Finalmente, edita el archivo urls.py de la aplicación blog y añade el siguiente parámetro de URL:

PracticaDjango/Blog/urls.py

from django.urls import path
from . import views

app_name = 'Blog'

urlpatterns = [
# post views
# path('', views.lista_post, name='lista_post'),
path('', views.lista_post.as_view(), name='lista_post'),
path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.detalle_post, name='detalle_post'),
path('categoria/<int:categoria_id>/', views.categoria, name='categoria'),
path('<int:post_id>/comentario/', views.post_comentario, name='post_comentario'),
path('busqueda/', views.busqueda_post, name='busqueda_post'),
]

Ahora abre la dirección http://127.0.0.1:8000/blog/busqueda/ en tu navegador. Deberías ver el siguiente formulario:

formulario de busqueda


Introduce una consulta y pulsa el botón de búsqueda. Verás el resultado de la consulta de esta forma:

resultados de la busqueda


Con estos pasos que hemos hecho, hemos creado un motor de búsqueda básica para los posts.


Stemming y ordenación de los resultados por su relevancia.

Stemming, en el ámbito de la búsqueda de datos y procesamiento de texto en informática, se refiere a un proceso utilizado en el procesamiento del lenguaje natural (NLP) que busca reducir las palabras a su forma base o raíz, conocida como el "stem" en inglés. El objetivo principal es simplificar las palabras para que diferentes formas de la misma palabra se consideren como iguales, lo que facilita la búsqueda y el análisis de texto.

Por ejemplo, palabras como "correr", "corriendo" y "corrió" se reducirían al mismo stem, que en este caso sería "corr". Al hacer esto, se puede agrupar el contenido que contiene estas palabras en sus formas derivadas, facilitando el análisis y la recuperación de información.

El stemming no siempre produce palabras válidas o legibles, ya que se centra en la reducción de las palabras a su forma base sin considerar su significado contextual. Es una técnica útil en la recuperación de información y el procesamiento de texto, pero puede presentar limitaciones en ciertos contextos lingüísticos debido a las variaciones y excepciones en la estructura de las palabras en diferentes idiomas.

Django proporciona una clase SearchQuery para traducir términos en un objeto de consulta de búsquedas. Por defecto, los términos se pasan a través de algoritmos de stemming, lo que ayuda a obtener mejores coincidencias.

El motor de búsqueda de PostgreSQL también elimina palabras vacías para realizar búsquedas, como "a", "the", "on"  y "of" en inglés. Las "stop words" son un conjunto de palabras de uso común en un idioma. Se eliminarán al crear una consulta de búsqueda porque aparecen con demasiada frecuencia como para ser relevantes en las búsquedas. Puedes encontrar la lista de "stop words" para el idioma inglés en esta dirección https://github.com/postgres/postgres/blob/master/src/backend/snowball/stopwords/english.stop. Sin embargo lo anterior se puede realizar en cualquier lenguaje. Por ejemplo también podemos encontrar una lista de "stop words" en español en https://github.com/postgres/
postgres/blob/master/src/backend/snowball/stopwords/spanish.stop.

También queremos ordenar esos resultados por su relevancia. PostgreSQL proporciona una función de clasificación que ordena los resultados en función de la frecuencia con la que aparecen los términos de consulta y de lo cerca que están entre si.

Edita el archivo views.py de la aplicación Blog y añade el siguiente código:

PracticaDjango/Blog/views.py

#...
# Para realizar busquedas en los campos de los post.
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

#...
def busqueda_post(request):
    formulario = BusquedaForm()
    consulta = None
    resultados = []

    if "consulta" in request.GET:
        formulario = BusquedaForm(request.GET)
        if formulario.is_valid():
            consulta = formulario.cleaned_data["consulta"]
            vector_busqueda = SearchVector("titulo","contenido", config='spanish')
            consulta_busqueda = SearchQuery(consulta, config='spanish')
            resultados = Post.objects.annotate(
                search = vector_busqueda,
                rank = SearchRank(vector_busqueda, consulta_busqueda)
            ).filter(search=consulta_busqueda).order_by('-rank')

    return render(
        request,
        "Blog/busqueda.html",
        {"formulario": formulario, "consulta": consulta, "resultados": resultados},
    )

En el código superior, hemos creado un objeto SearchQuery, filtrando los resultados en base a él, y usando SearchRank para ordenar los resultados por su relevancia. Como verás hemos usado también el vector_busqueda y consulta_busqueda con el atributo config, lo que nos permite establecer diferentes configuraciones. Esto nos va a permitir trabajar con diferentes idiomas. Por ejemplo, en el código usamos el stemming y removemos las "stop words" usando el idioma Español. 


Uso de ponderaciones en las consultas.

Podemos potenciar vectores específicos para que se les atribuya más peso cuando se ordenen los resultados por su relevancia. Por ejemplo, podemos utilizar esto para dar más relevancia a las publicaciones que coinciden por título en lugar de por contenido.

Edita el archivo views.py de la aplicación del blog y modifica la vista busqueda_post de la siguiente manera. El nuevo código está resaltado en negrita:

PracticaDjango/Blog/views.py

#...
def busqueda_post(request):
    formulario = BusquedaForm()
    consulta = None
    resultados = []

    if "consulta" in request.GET:
        formulario = BusquedaForm(request.GET)
        if formulario.is_valid():
            consulta = formulario.cleaned_data["consulta"]
            vector_busqueda = SearchVector(
                "titulo", weight="A", config="spanish"
            ) + SearchVector("contenido", weight="B", config="spanish")
            consulta_busqueda = SearchQuery(consulta, config="spanish")
            resultados = (
                Post.objects.annotate(
                    search=vector_busqueda,
                    rank=SearchRank(vector_busqueda, consulta_busqueda),
                )
                .filter(rank__gte=0.3)
                .order_by("-rank")
            )

    return render(
        request,
        "Blog/busqueda.html",
        {"formulario": formulario, "consulta": consulta, "resultados": resultados},
    )


En el código anterior, aplicamos diferentes pesos a los vectores de búsqueda construidos utilizando los campos de título y cuerpo. Los pesos predeterminados son D, C, B y A, y se refieren a los números 0.1, 0.2, 0.4 y 1.0, respectivamente. Aplicamos un peso de 1.0 al vector de búsqueda del título (A) y un peso de 0.4 al vector del contenido (B). Las coincidencias en el título prevalecerán sobre las coincidencias en el contenido del cuerpo. Filtramos los resultados para mostrar solo aquellos con una clasificación superior a 0.3.

Puedes encontrar el contenido de este capitulo en este enlace de Github.


lunes, 7 de junio de 2021

Flask 21. Desplegar una aplicación Flask en Heroku.

logotipo de heroku


Anteriormente. Apéndice 20. Uso de MYSQL o MARIADB en un proyecto Flask.


En este capítulo vamos a desplegar nuestra aplicación en Heroku, que es un servidor de alojamiento en la nube externo. Muchos proveedores de alojamiento en la nube ofrecen una plataforma administrativa en la que se pueden ejecutar aplicaciones. 

Todo lo que necesitamos para implementar una aplicación de Python en estas plataformas es la aplicación solamente, porque el hardware, el sistema operativo, los interpretes de lenguaje de secuencias de comandos, las bases de datos etc son todos administrados por el servicio. Este tipo de servicio se denomina Plataforma como servicio o PaaS.

Heroku, que es un servicio muy popular, nos facilita además una serie de servicios de forma gratuita en donde podremos implementar nuestra aplicación de Python.


Hosting en Heroku.


La implementación de una aplicación Python en Heroku se realiza a través de la herramienta de control de versiones de git, por lo que deberemos tener nuestra aplicación en un repositorio de git. Heroku busca un archivo llamado Procfile en el directorio raíz de la aplicación para obtener instrucciones sobre como iniciar la aplicación. Para los archivos de Python, Heroku también espera un archivo llamado "requirements.txt" que contendrá todas las dependencias del modulo que deben instalarse. Una vez que nuestra aplicación se cargue en los servidores de Heroku a través de git, solamente tendremos que esperar unos segundos hasta que la aplicación este en línea y funcionando. Así de simple.

Lógicamente al ser un servicio gratuito, que nos facilitan para desarrollar y practicar la programación, la capacidad de computo y almacenamiento es limitada. Si necesitas mayor potencia tendrás que adquirirla a través de lo que Heroku llama "dynos". Pero para lo básico y para practicar, el servicio gratuito nos servirá de sobra.


Crear una cuenta en Heroku.

Antes de poder empezar, lógicamente tenemos que tener una cuenta con ellos. Visita www.heroku.com y crea una cuenta gratuita.

página de inicio de Heroku

Una vez que tengas la cuenta, inicia sesión con tu correo y password (Log in) y tendrás acceso a un panel de administración donde aparecerán nuestros proyectos.

vista de nuevo proyecto en heroku



Instalando el cliente de Heroku en el ordenador.


Heroku nos proporciona una herramienta de línea de comandos para poder interactuar con sus servicios llamada Heroku CLI, que tenemos disponible para Linux, Mac y Windows. En la documentación tenemos las instrucciones de instalación para cada una de las plataformas compatibles. 

página de descargas de Heroku



Como yo esto trabajando con Ubuntu usaré la siguiente:

$sudo snap install --classic heroku

pero también puedes usar la que aparece en su página web más abajo para Debian, que es la que nos funcionará en la Raspberry Pi.

Nota: En ubuntu una instalación alternativa si te diese problemas de actualización (heroku update) de paquetes de snap es curl https://cli-assets.heroku.com/install.sh | sh. En este caso el programa está en /usr/local/bin/heroku

Desinstala previamente el paquete snap con sudo snap remove heroku.

Sigamos.

Lo primero que tenemos que hacer es loguearnos en la cuenta de heroku que creamos antes. Para ello tecleamos en el terminal:

$ heroku login
Heroku CLI abrirá el navegador y nos llevara a una página donde nos loguearemos. Nos pedirá la dirección de correo con la que nos registramos y nuestro password de la cuenta. Una vez hecho esto podemos cerrar la página que se nos ha abierto en el navegador y si vamos al terminal veremos que aparecemos logueados. Esta acción no hace falta volver a hacerla porque se recordará para comandos posteriores.

Preparando la aplicación para subirla a Heroku.


Aunque ser pude hacer de múltiples formas yo voy a usar la siguiente. Primeramente crearemos un esquema de directorios donde poner el proyecto con la siguiente forma:


Esquema de directorios y archivos del proyecto


1.- Creo una carpeta que se llame proyecto y entro en la misma. 

$ mkdir proyecto
$ cd proyecto
proyecto $
2.- Creo en el entorno virtual que usará la aplicación:

proyecto $ python3 -m venv venv

3.- Creamos la carpeta src que contendrá el código fuente del proyecto.

proyecto $ mkdir src

Dentro de esta carpeta colocaré los archivos del programa de Flask que hemos utilizado a lo largo de este tutorial y que puedes encontrar en esta carpeta del proyecto en github..

Para descargar solamente este directorio y no todo el repositorio, entras en el directorio POST 19, copias el enlace del navegador y puedes utilizar alguno de estos 2 links que te dejo:

https://downgit.github.io/#/home

https://download-directory.github.io/

para obtener un archivo zip con el código fuente. Luego extraes los archivos dentro de la carpeta src.

La carpeta src debería quedar tal como se ve a continuación.


directorios y archivos del proyecto en el directorio padre scr


IMPORTANTE: al tratarse de un entorno de producción asegúrate de entrar en la carpeta config, editar el archivo config.py y poner el DEBUG=False.


Heroku necesita unos cuantos archivos para subir el proyecto y que funcione. Estos son:

requirements.txt  => Este archivo contiene todas las librerías o bibliotecas de Python que el programa utiliza y que son necesarias para su funcionamiento. Todos los paquetes que utilices para el proyecto deben estar aquí para que heroku sepa lo que se necesita. Este archivo en su día lo realizamos entrando en el entorno virtual y ejecutando la siguiente instrucción:

env $ pip freeze > requirements.txt

Aquí ya esta hecho y si lo abres verás todas las bibliotecas que se utilizan en el programa.

Nota: Si en alguna ocasión te sale dentro de requirements.txt el paquete pkg-resources==0.0.0 elimínalo que luego al subirlo a heroku da un error.

Como lo voy a necesitar posteriormente para pasar de una base sqlite a una postgresql (un poco más adelante veras porqué) vamos a instalar todos los paquetes o dependencias del proyecto en local. Para  instalarlos en nuestro ordenador tenemos que entrar en el entorno virtual, en el directorio src que contiene requirements.txt y ejecutar la siguiente instrucción. Vamos a hacerlo:

env $ pip install -r requirements.txt


[runtime.txt] => Es un archivo en el que se recoge la versión de Python del programa. Este es un archivo opcional y sino lo creamos el programa se ejecutará con la versión 3.9.5. No obstante podemos elegir otra versión distinta. Mira el manual ya que hay que escribir en el archivo la versión de una forma particular y con los tres dígitos, ya que sino no funcionará. Para saber que versión de Python estamos tenemos simplemente tecleamos:

>>> python3 --version
Python 3.8.5


Procfile => En este fichero se recoge que archivo se debe ejecutar en Heroku cuando la aplicación se inicie.

En Heroku se necesita para que cualquier proyecto de Flask funcione instalar el complemento gunicorn que ejecuta el servidor http. Como este paquete en el proyecto no estaba instalado necesitamos hacerlo. Para ello entraremos en el entorno virtual (desde el directorio src, $ source ../venv/bin/activate) y ejecutaremos la siguiente instrucción:

(env) /proyecto/src $ pip install gunicorn

Luego creamos el archivo Procfile y escribimos:

web: flask db upgrade; gunicorn inicio:app

Guardamos y salimos. 

inicio es el nombre del archivo de python que ejecuta la aplicación (inicio.py) y app hace referencia al módelo que lanza la aplicación dentro del mismo (app = Flask(__name__)). "flask db upgrade" lo utilizaremos para consolidar la migración de nuestra base de datos sql a la que utiliza heroku como veremos más adelante en el capitulo.

Como esta biblioteca no estaba instalada en su día, en el archivo requirements.txt tenemos que añadirla. Para ver la versión que se nos ha instalado tecleamos:

(env) ~/proyecto/src $ pip freeze
gunicorn==20.1.0

Pues tal cual copiamos la salida "gunicorn==20.1.0" y la añadimos al final del archivo requirements.txt. 

paquetes contenidos en el archivo requeriments.txt


Guardamos y salimos.

Es sistema de archivos es temporal.


En Heroku tenemos un problema. En cualquier momento se puede restablecer el servidor virtual en el que se ejecutará nuestro programa a su estado inicial. Esto ocasiona que no se pueda confiar en que los datos que se guarden en el sistema de archivos persistirán, ya que de hecho, Heroku restablece los servidores con bastante frecuencia.

El problema que se genera es que el motor de la base de datos SQlite escribe datos a un archivo en disco, que se borraría al reiniciarse el servidor al igual que todo lo que se almacene en memoria. Con lo que si se hubieran escrito nuevos registros estos se borrarían dejando la base de datos sql a su estado original.

Afortunadamente lo que si nos proporciona Heroku, con un complemento, es una base de datos propia Postgresql lo que nos solucionará el problema de la persistencia de los datos.

Trabajando con la base de datos Postgresql de Heroku.


En este punto nos encontramos con un segundo problema. Tenemos la base de datos de los usuarios en formato sqlite, la cual no es soportada por Heroku por lo que hemos comentado anteriormente. Por lo cual tenemos que usar una base con un motor de datos diferente. Heroku tiene una oferta de base de datos propia, basada en la base de datos Postgresql que vamos a utilizar. Escogeremos la que nos ofrece de forma gratuita que tiene un tamaño máximo de 8 MB y 20 conexiones simultáneas lo cual es más que suficiente para nuestro proyecto.

Para hacer esto, lo primero que tenemos que hacer es crear nuestra aplicación de Heroku. Aunque se puede hacer desde la pagina web, lo haremos desde el terminal del sistema con la instrucción:

$ heroku create nombre_proyecto_heroku
* Si el nombre_proyecto_heroku ya esta cogido te dará un error y tendrás que ponerle otro nombre distinto, el que más te guste. Yo lo he llamado novatillo.

y para añadirle una base de datos gratuita teclearemos:


heroku addons:create heroku-postgresql:hobby-dev --app nombre_proyecto_heroku


En mi caso la instrucción es como sigue:

$ heroku addons:create heroku-postgresql:hobby-dev --app novatillo
Creating heroku-postgresql:hobby-dev on ⬢ novatillo... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-cylindrical-56001 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation

La url de la base de datos se almacena en heroku en una variable de entorno llamada DATABASE_URL que estará disponible cuando se ejecute la aplicación. Pero aun que ya tenemos la base de datos tenemos que adaptar el conector en nuestro programa para poder usarla.


Conectando la base de datos de Heroku a nuestra aplicación Python.

Para usar PostgreSQL como nuestra base de datos en una aplicación de Python lo primero que tenemos que hacer es instalar el paquete psycopg2 en el entorno virtual, que es como el driver para que sqlalchemy pueda relacionarse con la la base postgrepsql que utiliza heroku:

(venv) ~/proyecto/src $ pip install psycopg2-binary

y como es un paquete nuevo que también es necesario para que nos funcione el proyecto en heroku tenemos que añadirlo al final del archivo requirements.txt

paquetes necesarios para instalar heroku, requeriments.txt


Ahora lo que tenemos que hacer es modificar el conector en nuestro programa para que SQLAlchemy se pueda comunicar con la base de datos de heroku una vez que lo hayamos subido a su servidor . Este conector es una variable del sistema llamada DATABASE_URL por lo que tenemos que capturarla y usarla en nuestro código tal como dice el manual de Heroku. 

Para saber cual es el conector con la base de datos de heroku podemos hacerlo desde la pagina web de administración de proyectos de heroku o desde el terminal usando la siguiente instrucción:

heroku config:get DATABASE_URL -a nombre_aplicación


# Como mi aplicación se llama novatillo  
(venv) ~/proyecto/scr $ heroku config:get DATABASE_URL -a novatillo
salida por pantalla del resultado de conectarnos a la base de datos de heroku

Copiamos todo esto que nos sale, que es la uri del conector.

Ahora tendríamos que entrar en config.py dentro de la carpeta config y modificar la línea 17 para capturar la variable de entorno en donde esta la base de datos en Heroku. Le tendríamos que decir al programa, que el conector  se encuentra primero en la variable del sistema DATABASE_URL y si no es así que use la antigua de sqlite.

Sin embargo por la nota siguiente voy a usar la uri que acabamos de copiar, y la pegaremos directamente. Ya que es un string la pegaremos dentro de un par de comillas " ".  Como ves la cadena comienza por "postgres:// ", pues bien cambia la palabra "postgres" por "postgresql". Quedarías así:


código del archivo config.py


¿Por qué? Lee lo siguiente:


Nota importante a la fecha en la que estoy escribiendo esto.


 El dialecto predeterminado que utiliza heroku en su base de datos es postgres y así nos lo pasan en DATABASE_URL. Sin embargo en un cambio reciente en la líbrería SQLAlchemy en la intrucción SQLALCHEMY_DATABASE_URI se espera encontrar un conector que comience por 'postgresql' para manejar este tipo de base de datos. De ahí que utilice todo el contenido de la variable y la modifique, y no capture solamente el valor de las misma. Es necesario para que la cosa funcione. Esto no quiere decir que en un futuro heroku pueda actualizar su complemento de postgres a postgresql ya no siendo necesario realizar este cambio. Si lo dejáramos tal cual tenemos este bonito error:
sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres
Otra posible solución es utilizar una libreria de SQLALchemy inferior a la versión 1.4.0  (1.3.23 por ejemplo debería funcionar)


Nos queda otra cuestión por resolver. Hasta ahora la base de datos que teníamos era sqlite y ahora necesitamos pasar su estructura a otro sistema: PostgreSql. Para ello tenemos que migrar la base de datos. Esto lo puedes ver en más detalle en el capitulo 14, así que aquí no lo explicaré y solo pondré los pasos:

Empezamos instalando el paquete flask-migrate en nuestro entorno virtual:

(venv) ~/proyecto/src $ pip install flask-migrate

y añadimos el paquete al archivo requirements.txt (Flask-Migrate==3.0.1)

Ahora en el archivo principal de la aplicación inicio.py tenemos que importar Migrate desde flask_migrate:

código del archivo inicio.py

y declararla después de db=SQLAlchemy(app)

insertar migrate en el archivo de inicio de Python

El proceso de migración es sencillo. Desde el terminal del entorno virtual inicializamos la migración. Entramos en el directorio src, si no lo estamos ya. Lo primero le decimos a Flask cual es la aplicación principal, más tarde haremos lo mismo en heroku. Lo hacemos creando una variable de entorno.


(venv) ~/proyecto/src $ export FLASK_APP=inicio.py
Seguidamente iniciamos la migración con:

(venv) ~/proyecto/src $ flask db init

iniciando migración en Python con flask db init


y realizamos la primera migración:

(venv) ~/proyecto/src $ flask db migrate -m "preparando para heroku"

primera migración

Si te fijas dentro del directorio src que tenemos en local se ha creado un nuevo directorio llamado "migrations". 

El último paso de la migración se realizará directamente cuando se ejecute la aplicación en heroku ya que si recuerdas al principio del capitulo creamos un archivo llamado Procfile, en el que usamos la última instrucción que necesitamos "flask db upgrade". Esta creará las tablas en la base de datos de heroku y pasará automáticamente los datos que tengamos en ella.

web: flask db upgrade; gunicorn inicio:app


Creación del repositorio git, configuración y subida del proyecto a heroku.


Para subir nuestro programa a Heroku hay que crear un repositorio de git. 

Claro esta que si no lo tienes instalado lo primero es descargarlo y configurarlo. Si no lo tienes ve al paso 1) y si ya lo tienes pasa al 2)

1) Ve a la página de github y create una cuenta (sign up). Te pedirá un usuario y una cuenta de correo.

Para instalar el cliente git en el ordenador, en una pi que usa Debian, o en ubuntu como estoy yo usamos:

$ sudo apt-get install git

Una vez instalado el cliente de git tenemos que configurar nuestro nombre de usuario y dirección de correo electrónico.
$ git config --global user.name "nombre_usuario"
$ git config --global user.email tucorreo@correo.com

Si quieres ver tu configuración puedes usar la siguiente orden en el terminal:

git config --list


2) Nos metemos en la carpeta src, si no lo estamos ya y tecleamos. No hace falta estar dentro del entorno virtual pero si es importante estar en la carpeta src.

$ git init

Inicia el repositorio.

$ git status 

Vemos los archivos preparados para subir (en rojo)

$ git add .

Le decimos a git que añada todo lo que hay en el directorio en el que estamos (src) al repositorio.

$ git commit -m "paquete git para subir a heroku"

y con esto creamos el repositorio. Si te fijas se ha creado un directorio llamado .git oculto dentro del directorio src. Ahí esta el repositorio. Si más adelante quieres borrar el repositorio solo tienes que borrarlo.

Ahora enlazamos la aplicación que hemos creado en Heroku (novatillo en mi caso) con la aplicación o repositorio de flask que tenemos en el ordenador.

# heroku git:remote nombre_aplicación
$ heroku git:remote novatillo

Todo esto lo hacemos dentro de la carpeta src.

Ahora que tenemos el proyecto enlazado lo subimos a heroku (¡por fin!)

$ git push heroku master

y si todo va bien, comprimirá la aplicación, subirá los archivos, instalará los paquetes y ya tendremos la aplicación subida y el link de nuestra aplicación en Heroku:

subida del proyecto a Heroku


Pero antes de acceder y como el primero de los dos subcomandos que usamos en el archivo Procfile

Procfile: Heroku Procfile.

web: flask db upgrade; gunicorn inicio:app

está basado en un comando de flask, al igual que hicimos cuando iniciamos la migración en local, necesitamos añadir la variable FLASK_APP al entorno de Heroku 

$ heroku config:set FLASK_APP=inicio.py

y ya esta todo. 

Si navegamos a la página que nos ha facilitado heroku https://(nombre_proyecto).herokuapp.com veremos nuestro proyecto funcionando en el servidor:

proyecto funcionando en Heroku



Anexo.

Otros comandos de heroku cli que nos pueden ser útiles son:

Establecer una variable de entorno en Heroku

$ heroku config:set FLASK_APP=inicio.py

Ejecutar un comando en la consola de la aplicación en heroku. En este caso una que nos muestre el historial de 
migraciones

# heroku run -a nombre_aplicación (orden a ejecutar)
$ heroku run -a novatillo flask db history

Podríamos haber hecho el upgrade de la migración no poniendo el comando automáticamente en el archivo Procfiles sino usando la consola de esta forma:

Upgrade de la migración

$ heroku run -a novatillo flask db upgrade


Próximo Capítulo. Flask 22. Desplegar una aplicación de Flask en un contenedor de Docker.