martes, 22 de junio de 2021

Los Diccionarios en Python

imagen de un diccionario clave:valor


Un diccionario en Python es un objeto (dict) en el que los elementos se almacenan utilizando un sistema basado en el sistema clave:valor o Key:values en inglés. Es decir se asocia a cada clave un determinado valor o valores. 

Un ejemplo de diccionario podría ser el que asociara los países con sus respectivas capitales. En este caso la clave sería el país y su valor la capital. 

midiccionario = {"país":"capital"} ------> {clave : valor, .........., clave : valor}

>>> diccio = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
# para acceder a los valores de un diccionario usamos las claves
>>> print(diccio["España"])
Madrid

Los datos a partir de la version 3.7 de python son ordenados, se pueden modificar, pero lo que no puede ocurrir nunca en un diccionario es que haya dos claves iguales.

Otra forma de definir un dicionario en Python es utilizando un lista con tuplas de dos elementos en la que la primera será la clave y la segunda el valor.

>>> mi_diccionario = dict([("clave1","valor1"), ("clave2", "valor2"), ("clave3", "valor3")])
>>> mi_diccionario
{'clave1': 'valor1', 'clave2': 'valor2', 'clave3': 'valor3'}
E incluso si las claves son strings, es decir texto, también tenemos otra opción para definir un diccionario:

>>> mi_diccionario = dict(clave1="valor1", clave2="valor2")
>>> mi_diccionario
{'clave1': 'valor1', 'clave2': 'valor2'}


Ver los valores asociados a una clave.


Podemos acceder a los elementos de un diccionario refiriéndonos a los valores de su clave entre corchetes [ ]

print(nombre_diccionario[clave])

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
>>> print(diccionario["España"])
Madrid

aunque también podemos utilizar el método get

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlín"}
>>> print(diccionario.get("España"))
Madrid

La diferencia que existe entre ambos métodos es que si no existe la clave en el diccionario en el primero nos dará un error mientras que con get nos dará un None o si definimos su valor opcional nos lo mostrará.

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
>>> print(diccionario['Rusia'])
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(diccionario['Rusia'])
KeyError: 'Rusia'
>>> print(diccionario.get('Rusia'))
None
>>> print(diccionario.get('Rusia', 'No existe la clave'))
No existe la clave

Otra forma de ver si existe una clave en un diccionario es usando la palabra reservada in. El valor será True si existe o False en caso contrario.

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
>>> print("Alemania" in diccionario)
True

Para añadir un registro a un diccionario se hace de una forma muy parecida. Vamos a añadir al diccionario anterior como país Reino Unido y como capital Londres.

>>> diccionario["Reino Unido"] = "Londres"
>>> print(diccionario)
{'España': 'Madrid', 'Francia': 'Paris', 'Alemania': 'Berlín', 'Reino Unido': 'Londres'}

Y si queremos eliminar un elemento utilizamos la función del, o el método pop() o pop(item):

>>> del diccionario["Francia"]
>>> print(diccionario)
{'España': 'Madrid', 'Alemania': 'Berlín', 'Reino Unido': 'Londres'}

El método pop elimina el elemento con la clave especificada:

>>> diccionario.pop('Francia')
>>> print(diccionario)
{'España': 'Madrid', 'Alemania': 'Berlín', 'Reino Unido': 'Londres'}
El método .popitem() elimina el último elemento insertado. (En versiones anteriores a la 3.7 de Python elimina un elemento de forma aleatoria).

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlín"}
>>> diccionario.popitem()
>>> print(diccionario)
{'España': 'Madrid', 'Francia': 'Paris'}


Longitud de un Diccionario.


Para saber cuantos elementos tiene un diccionario podemos utilizar la función len()


>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlín"}
>>> print(len(diccionario))
3


Elementos de un diccionario. Tipos de datos.


Los valores de los elementos de un diccionario pueden ser de cualquier tipo de datos, incluso otro diccionario:

>>> coches={"marca":"Seat", "electrico":False, "año":1998, "colores":["rojo","blanco","azul"]}
>>> print(coches)
{'marca': 'Seat', 'electrico': False, 'año': 1998, 'colores': ['rojo', 'blanco', 'azul']}

Desde la perspectiva de Python los diccionarios son objetos con el tipo de dato "dict"

<class 'dict'>


Listar las claves, valores o elementos de un diccionario.

El método .keys() nos proporciona una lista con todas las claves del diccionario.

>>> diccionario = {'nombre' : 'Carlos', 'edad' : 22, 'cursos': ['Python','Django','JavaScript'] }
>>> print(diccionario.keys())
dict_keys(['nombre', 'edad', 'cursos'])

y el método .values() nos dará los valores del mismo.

>>> diccionario = {'nombre' : 'Carlos', 'edad' : 22, 'cursos': ['Python','Django','JavaScript'] }
>>> print(diccionario.values())
dict_values(['Carlos', 22, ['Python', 'Django', 'JavaScript']])

Para obtener los elementos de un diccionario podemos realizar una iteración en el diccionario:

>>> diccionario = {'nombre' : 'Carlos', 'edad' : 22, 'cursos': ['Python','Django','JavaScript'] }
# Se podría también iterar sobre:
# for i in diccionario.keys():
# for i in diccionario.values():
# for i in diccionario.items():
>>> for i in diccionario:
      print(i, diccionario[i])

nombre Carlos
edad 22
cursos ['Python', 'Django', 'JavaScript']

Aunque es más eficiente usar el método .items(), que nos devuelve cada el elemento del diccionario, como una tupla dentro de una lista.


Actualizando un diccionario:

El método .update() actualizará un diccionario ya existente con los elementos que le hayamos pasado como argumento. El argumento debe ser un diccionario o un iterable con la estructura clave:valor

>>> diccionario = {'nombre' : 'Carlos', 'edad' : 22, 'cursos': ['Python','Django','JavaScript'] }
>>> diccionario.update({"apellido":"moran"})
>>> print(diccionario["apellido"])
moran

o también podemos añadir un nuevo elemento simplemente usando una nueva clave y su correspondiente valor como vimos anteriormente.


Copiando un diccionario.

No podemos copiar un diccionario simplemente diciendo que diccionario2 = diccionario1 ya que entonces el diccionario2 sería solamente una referencia al diccionario1 y cualquier cambio que hiciéramos en este, automáticamente quedaría hecho en el diccionario2.

Para copiar un diccionario podemos usar el método copy() o también la función de python dict()

>>> diccionario = {"España":"Madrid","Francia":"Paris","Alemania":"Berlin"}
>>> diccionario2 = diccionario.copy()
>>> diccionario3 = dict(diccionario)


Diccionarios Anidados.

Un diccionario puede contener en su interior otros diccionarios, lo cual es bastante habitual. Es lo que se conoce como diccionarios anidados. Veamos un ejemplo:

>>> empleados = {
  "empleado1" : {
    "nombre" : "Antonio",
    "año de incorporación" : 2004
  },
  "empleado2" : {
    "nombre" : "Emilio",
    "año de incorporación" : 2007
  },
  "empleado3" : {
    "nombre" : "Ana",
    "año de incorporación" : 2011
  }
}


Biblioteca Standar Collections y su relación con los diccionarios.


Particularmente  un módulo que me ha resultado muy útil de la biblioteca estándar de Python es "collections".

En el podemos encontrar la clase "Counter". Lo que hace es contar las frecuencias de una colección de elementos que le pasemos como argumento, usualmente strings o listas aunque también le podemos pasar objetos.  Lo mejor es que veamos su funcionamiento con varios ejemplos. 

>>> from collections import Counter
>>> lista = ['pera','manzana','pera','limon','pera','manzana','limon']
>>> print(Counter(lista))
Counter({'pera': 3, 'manzana': 2, 'limon': 2})

Como ves nos devuelve un iterable que contiene un diccionario en el cual están las veces que se repite la fruta en cuestión en la lista que le he pasado. Sirve para contar las veces que se repite un elemento.

>>> from collections import Counter
>>> lista=[1,3,5,4,5,1,2,7,6,7,9,1,0,1]
>>> diccionario_frecuencias = Counter(lista)
>>> print(diccionario_frecuencias)
Counter({1: 4, 5: 2, 7: 2, 3: 1, 4: 1, 2: 1, 6: 1, 9: 1, 0: 1})
>>> diccionario_frecuencias[7]
2
En este otro ejemplo le pasamos una lista de números y luego almacenamos el objeto que nos devuelve Counter a la variable "diccionario_frecuencias" y vemos como podemos consultar cualquier valor escribiendo su clave. En el ejemplo le preguntamos por el número 7 y nos devuelve que se repite 2 veces en la lista.

También podemos encontrar la clase "defaultdict". Básicamente nos sirve para que cuando intentemos acceder a claves que no existen en un diccionario no nos de el error "Keyerror". El argumento para inicializarlo es cualquier objeto que se pueda llamar, ya sea una lista, un entero, un string etc pero sin argumentos.

Veamos un ejemplo del problema.

>>> # Creamos un diccionario vacio.
>>> mi_diccionario = {}
>>> # Añadimos una clave.
>>> mi_diccionario["uno"] = 1
>>> # Si intentamos acceder a una clave que no existe entonces:
>>> mi_diccionario["missing"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'missing'
>>> # Obtenemos un error. Lo intentamos con otra clave.
>>> mi_diccionario["another_missing"].append(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'another_missing'
Ahora vamos a ver como usando "defaultdict" se nos resuelve el problema.

>>> from collections import defaultdict
>>> mi_diccionario = defaultdict(list) # el argumento sin parentesis 
>>> mi_diccionario["uno"] = 1
>>> mi_diccionario["missing"]
[]
>>> mi_diccionario["another_missing"] = 4
>>> print(mi_diccionario)
defaultdict(<class 'list'>, {'uno': 1, 'missing': [], 'another_missing': 4})
>>> 

Encontrar la clave para el valor máximo en un diccionario.


Imagina que tienes un almacén de fruta. En ese almacén las existencias de cada una de las frutas está almacenadas en un diccionario con la siguiente estructura.

diccionario {"peras":2,"manzanas":5,"naranjas":1}
¿Cómo saber de cual de las frutas es de la que tenemos más existencias? Es sencillo simplemente tenemos que usar la función "max" sobre el diccionario de las siguiente forma:

manzanas


Ordenar un diccionario por clave o por valor.


Usando el ejemplo anterior podemos ordenar un diccionario por sus claves.

diccionario {"peras":2,"manzanas":5,"naranjas":1}
dicionario_ordenado = sorted(diccionario, key = diccionario.get) >>> ['naranjas', 'peras', 'manzanas']
o en base a sus valores. Si queremos en orden descendente usamos el parámetro reverse=True.

diccionario = {"peras":2,"manzanas":5,"naranjas":1}

diccionario_ordenado = sorted(diccionario, key = diccionario.get, reverse=True)
for w in diccionario_ordenado:
    print(w, diccionario[w])
>>>
manzanas 5
peras 2
naranjas 1

Invertir un diccionario. Intercambiar claves por valores y viceversa.

# Diccionario original
dic = {'a':1, 'b':2, 'c':3}
# Diccionario con claves intercambiadas con valores
dic_invertido = {v: k for k,v in dic.items()}
print(dic_invertido)

SALIDA
{1:'a', 2:'b', 3:'c'}


Usar un diccionario para realizar bifurcaciones:

Imagina que quieres construir un menú de opciones en el que dependiendo de lo que seleccione el usuario se haga una cosa u otra. Tendrías que construir un código similar a esto:

# Bifurcaciones usando if.
'''
if acción == 1:
    print("Pulsaste uno")
elif accion == 2:
    print("Pulsaste dos")
elif accion == 3:
    print("Pulsaste tres")
elif accion == 4:
    print("Pulsaste cuatro")
'''

# Bifurcaciones usando un diccionario.

opciones = {1:'uno', 2:'dos', 3:'tres', 4:'cuatro'}
x = int(input("introduzca un valor: "))
print(f'Pulsaste {opciones[x]}')

Pues bien usando diccionarios se nos simplifica bastante el código de la siguiente manera:


Puedes encontrar más información y ejemplos sobre diccionarios en la página web de w3schools.com



domingo, 20 de junio de 2021

Flask 22. Desplegando una aplicación de Flask mediante un contenedor de Docker.


Esquema de funcionamiento de Docker



Desplegando una aplicación de Flask mediante un contenedor de Docker.


Docker nos permite utilizar imágenes para crear contenedores o procesos. Estos se basan en una tecnología de virtualización bastante ligera en el uso de recursos, que nos permite que una aplicación junto con sus dependencias y configuración, se ejecuten en un completo aislamiento, pero sin la necesidad de utilizar una solución de virtualización completa, del tipo máquina virtual, que necesitan muchos más recursos, y a veces puede tener una degradación significativa en el rendimiento de la máquina que hace de host.

En una máquina que hace de host de los containers, se pueden ejecutar muchos de ellos, todos compartiendo el kernel del host y su hardware. Esto contrasta con las máquinas virtuales que deben emular un sistema completo, con su cpu, su disco, otra hardware, kernel etc.

A pesar de tener que compartir el Kernel es aislamiento de un contenedor es bastante alto. Un contenedor tiene su propio sistema de archivos y puede basarse en un sistema de archivos distinto al que se ejecuta en la máquina host del contenedor. Por ejemplo pueden usarse contenedores en Ubuntu en un host que ejecute Fedora, o viceversa. Aunque el origen es de linux, gracias a la virtualización también se pueden ejecutar contenedores de linux en un host de Windows o Mac


Instalando Docker. 


Para poder trabajar con contenedores de Docker lo primero es tenerlo instalado en nuestro ordenador. Hay instaladores para  Windows, Mac o Linux disponibles en su página web. Es escoger el instalador que necesites y seguir los pasos que te dicen en el manual para la instalación. Una cosa a puntualizar es que si instalas el motor de docker sin más, para ejecutar los comandos en linux necesitaras ejecutar los comandos con privilegio de superusuario (sudo). Si te incordia tener que estar ejecutando sudo para cualquier comando y quieres ejecutarlo de forma normal entonces tienes que seguir en las instrucciones el paso que pone "post-instalation step in linux"

Una vez que tenemos instalado Docker en nuestro ordenador podemos verificar que todo ha ido correctamente tecleando la siguiente instrucción en una ventana de terminal:

$ sudo docker version
Client: Docker Engine - Community
 Version:           20.10.7
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        f0df350
 Built:             Wed Jun  2 11:56:38 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.7
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       b0f5bc3
  Built:            Wed Jun  2 11:54:50 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.6
  GitCommit:        d71fcd7d8303cbf684402823e425e9dd2e99285d
 runc:
  Version:          1.0.0-rc95
  GitCommit:        b9ee9c6314599f1b4a7f497e1f1f856fe433d3b7
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


Construyendo una imagen de contenedor para nuestra aplicación.


Como en el capítulo anterior usaremos el código de la aplicación del capítulo 22. El primer paso para crear un contendor para la aplicación es crear una imagen para él. Una imagen de contenedor es como una plantilla que se usa para crear un contenedor. Contiene una representación completa del sistema de archivos del contenedor, junto con varias configuraciones relacionadas con la red, las opciones de inicio etc. 

Un muy buen sitio para empezar a ver como funciona Docker la encontrarás en este sitio de youtube, "FATZ, Docker, Curso Práctico para principiantes (desde linux)"

https://www.youtube.com/watch?v=NVvZNmfqg6M

La forma más sencilla de crear una imagen de contenedor para cualquier aplicación es iniciar un contenedor ya existente del sistema operativo deseamos utilizar (ubuntu, Debian, Fedora, etc), conectarnos a ese contenedor usando un shell de bash, copiar los archivos fuente de la aplicación e instalar todas las dependencias. Después de instalar todo, podemos tomar una instantánea del contenedor y esa instantánea se convertirá en la imagen que queremos. Sin embargo esto no es muy práctico ya que no es conveniente tener que instalar manualmente la aplicación cada vez que necesitemos crear una nueva imagen.

En su lugar generaremos la imagen de la aplicación a través de un script. El comando que crea imágenes de un contenedor a través de un script es "sudo docker build". Este comando lee y ejecuta las instrucciones de compilación de un archivo llamado Dockerfile, que tendremos que crear. Este archivo, Dockerfile, es básicamente una especie de script de instalación que ejecuta los pasos de instalación para implementar la aplicación, además de algunas configuraciones especificas del contenedor.

Empecemos.

Lo que tenemos que tener claro es que una cosa es la imagen y otra el contenedor. Lo primero que vamos a crear es la imagen de la aplicación y luego veremos como ejecutar el contenedor. 

Creamos un directorio donde copiamos todos los archivos del proyecto. Yo le llamaré src.


directorio de trabajo de la aplicación





Entramos dentro del mismo y creamos un archivo llamado Dockerfile. (En el código fuente de la aplicación este ya existe)

Cada línea de Dockerfile es una instrucción.

código del archivo Dockerfile



En la primera línea el comando FROM escribimos el nombre del módulo que queremos utilizar en este caso el nombre del módulo de Python. La idea es que comencemos con una imagen que ya exista, agreguemos o cambiemos algunas cosas y terminemos con la imagen que permita ejecutar nuestra aplicación. Si visitamos la página de Docker Hub y en su buscador tecleamos Python, y entramos en él 


imagen de Python en la página de Docker Hub



veremos que existen múltiples versiones en base principalmente al sistema operativo base utilizado por la imagen. Nosotros usaremos la que está señalada abajo.


distintas versiones de Python en Docker Hub

La razón de utilizar la distribución Alpine Linux, en lugar de otras distribuciones más populares como Debian o Ubuntu, es por su pequeño tamaño. Al final en esta instrucción lo que le estamos diciendo a Docker es que queremos utilizar el módulo de Python y exactamente queremos utilizar la versión 3.9-alpine (para utilizar cualquier otra es copiar y pegar el nombre)

El comando WORKDIR le va a decir en que directorio va a estar mi proyecto dentro del contenedor. Puede ser uno que ya exista y sino existe como es el caso de este ejemplo lo creará. Le diremos que cree la carpeta /code y es allí donde se van a ejecutar los siguientes comandos.

Para que nuestra aplicación se ejecute es necesario que le pasemos una variable de entorno que le diga al contenedor cual es el archivo que tiene que arrancar. Para establecer variables de entorno dentro de Dockerfile simplemente utilizamos el comando ENV y establecemos el nombre y el valor de la variable que queramos crear. De esta forma utilizamos la variable de entorno:

ENV FLASK_APP inicio.py

para crear una variable de entorno que le diga al Docker que el programa que ejecute al comenzar sea inicio.py, que es el archivo principal de la aplicación.

A mayores definimos también la variable de entorno:

ENV FLASK_RUN_HOST 0.0.0.0

para decirle al servidor de Flask que nuestro proyecto sea visible y accesible a través de nuestra red. Ya que sino solo estaría accesible a través del localhost.

La siguiente instrucción 

RUN apk add --no-cache gcc musl-dev linux-headers

nos servirá para instalar algunos módulos para que Python funcione más rápidamente en la versión Alpine de Linux. (apk es el administrador de paquetes de Alpine)


Nota:

RUN apk add build-base

Está puesta porque existe una incompatibilidad entre cython y python 3.7 que provoca un error al tratar de instalar el modulo "Error greenlet docker build". Esto se podría solucionar utilizando una imagen de python inferior como FROM python:3.6-stretch que funciona bien en distribuciones más pesadas, pero como yo quiero utilizar la versión alpine que es más ligera este comando soluciona el error. Para más información ver este link.

Necesitamos instalar todos los módulos o bibliotecas que se necesitan para que funcione el proyecto y que se encuentran dentro del archivo requirements.txt. Para ello le decimos en el Dockerfile, que queremos que copie el archivo requierements.txt que está el el directorio scr, donde también está el archivo Dockerfile, al contenedor, dentro del directorio de trabajo y queremos que también lo llame requirements.txt. Para ello es que añadimos:

COPY requirements.txt requirements.txt

Es como copiar de origen a destino, pero no añadimos rutas porque el origen está dentro del directorio donde también está el Dockerfile y el destino sino especificamos nada, por defecto entiende que es el directorio de trabajo que hemos especificado antes (/code)

Una vez copiado queremos que ejecute el comando necesario para que el lea el archivo requirements.txt (que ahora lo tiene en el directorio /code) e instale los módulos necesarios para que funcione el proyecto, uno a uno:

RUN pip install -r requirements.txt

Una vez que haya finalizado la instalación vamos a copiar lo más importante. Todo el código con los archivos que conforman el proyecto y que están están todos dentro de la carpeta src. Para ello le decimos a Docker que copie todo lo que esta dentro del directorio src en donde estamos, a la carpeta de trabajo.

COPY . .

Para finalizar tenemos que ejecutar el servidor gunicorn que servirá nuestra aplicación de Flask. Lo hubiésemos podido ejecutar con el comando:

CMD [ "gunicorn", "-b", "0.0.0.0:5000", "inicio:app" ]

En donde "0.0.0.0:5000" es el puerto donde se servirá la aplicación dentro del contenedor y "inicio:app" en donde inicio hace referencia a nuestra aplicación principal de python que es el archivo inicio.py y app es el nombre del WSGI que ejecutamos [app=Flask(__name__)]

Pero yo he escogido la opción de hacerlo a través de un ENTRYPOINT que es como un pequeño archivo o script ejecutable que le dirá a Docker lo que quiero ejecutar. Se puede poner el nombre que se quiera al archivo, yo le llamo boot.sh. Al estar en linux para asegurarme de que se pueda ejecutar le doy permisos de ejecución con:

RUN chmod +x boot.sh

El comando:

EXPOSE 5000

configura el puerto que este contenedor usará para su servidor.  Este es necesario para que Docker pueda configurar la red en el contenedor de la manera más adecuada. Elegí el puerto 5000 que es el standar de Flask pero podría haber sido cualquier otro.

Y para finalizar le digo cual va a ser el ENTRYPOINT:

ENTRYPOINT ["./boot.sh"]

que es el comando o archivo predeterminado que debe ejecutarse cuando se inicia el contenedor. Este es el comando que iniciará el servidor web de nuestra aplicación usando gunicorn para ello. En este ejemplo sencillo preferí usar una secuencia de comandos separada para esto aunque como ya comente tambien se podría haber hecho sin este script y usando un comando CMD.


boot.sh: Docker container start-up script.

#!/bin/sh

# Se podrían haber usado otros comandos a ejecutar como
# flask db upgrade u otros.

exec gunicorn -b :5000 inicio:app


En resumen:

Lo que va hace lo anterior es instalar Python en un contendor, luego creará una carpeta llamada /code, configurará las variables que necesita Flask para funcionar, como cual es el archivo principal y cual es el host, y luego ejecutará los módulos para que podamos ejecutar nuestro proyecto. Finalmente copiamos el código y ejecutamos la aplicación.

Con todo esto ya estamos listos para crear la imagen de nuestro proyecto. (Es como las isos de las instalaciones en windows). Luego a partir de esa imagen podremos ejecutar múltiples instancias de ese contenedor con nuestra aplicación en cualquier ordenador.

Para generar una imagen la instrucción a teclear es:

src $ sudo Docker build -t flask22 .

Tenemos que estar dentro del directorio que contiene el archivo Dockerfile. Si utilizamos el parámetro -t (de tag, etiqueta en ingles) podemos especificar el nombre de nuestra imagen. Si no la utilizamos nos asignará un nombre de imagen por defecto.

Después de que realice todo los pasos acabará creando una imagen de nuestro proyecto. Si tecleamos

src $ sudo Docker images

imagenes de Docker instaladas localmente, flask22 y Python

vemos que tenemos dos imágenes en nuestro equipo. Una que es la de nuestra aplicación y otra python que la ha tenido que descargar en local porque era necesaria para crear la nuestra.


Ejecutando un contenedor.


Con una imagen ya creada, ahora ya podemos ejecutar la versión de contenedor para la aplicación. Para ver que todo funciona bien vamos a ejecutar el contenedor a partir de la imagen creada. Tecleamos

src $ sudo docker run -d -p 8000:5000 --name prueba --rm flask22:latest
Que le está diciendo a Docker que ejecute un contenedor, al que hemos llamado prueba usando el parámetro --name, que lo haga en segundo plano con el parámetro -d ya que sino nos bloquearía el prompt del terminal. La opción -p asigna puertos del contendor a puertos del host. El primer valor, a la izquierda de los dos puntos, es el puerto donde se verá el programa en nuestro ordenador host y el de la derecha es el puerto dentro del contenedor. En el ejemplo anterior se asigna el puerto 5000 que es el que usamos en gunicorn y que se ejecuta dentro del contenedor al puerto 8000 en el host, por lo que para ver la aplicación tendremos que acceder al puerto 8000 aunque internamente el contenedor lo esté ejecutando en el puerto 5000. La opción --rm borrará el contenedor una vez que este terminado. Si bien esta opción no suele ser necesaria, los contenedores que terminan o que se interrumpen generalmente ya no son necesarios, por lo que se pueden eliminar automáticamente. El último argumento es el nombre de la imagen en la que se basa el contenedor y (opcional) después de 2 puntos la etiqueta que se usará para el contenedor .

Si ahora abres el navegador y vas al localhost:8000 verás como funciona el proyecto:

página inicial del proyecto mostrada en un navegador



Para ver los contenedores que se están ejecutando podemos teclear:

src $ sudo docker ps

salida por pantalla de una imagen de docker



Para parar el contenedor se usa:

src $ sudo docker stop prueba

o también se puede usar los 3 primeros dígitos del id del container. Por ejemplo para volver a ejecutarlo tecleamos (en tu caso los 3 primeros dígitos de identifiquen a tu container, sino hubiéramos usado la opción --rm):

src $ sudo docker start 416

que iniciará el contenedor con las mismas opciones con la que lo creamos.

Cada vez que realicemos cambios en la aplicación podemos actualizar la imagen del contenedor ejecutando nuevamente el comando de compilación.

Si hubiésemos querido añadir variables de entorno pero no metiéndolas en el archivo Dockerfile al compilar la imagen lo cual las haría accesibles para todos los contenedores, sino solo para un contenedor específico, en tiempo de ejecución, se puede hacer usando el parámetro u opción -e. Un ejemplo imaginario podría ser por ejemplo:

sudo docker run -d -p 8000:5000 --name prueba --rm -e SECRET_KEY=mi clave secreta \
-e MAIL_SERVER=smtp.gogle.com flask22:latest


Usando servicios de contenedores de Terceros.


La versión de contenedor de nuestra aplicación funciona bien, pero hasta ahora estamos usando para guardar los datos una base de datos sqlite que se apoya en un archivo que se guarda en el disco. ¿Qué crees que le sucederá a ese archivo cuando se detenga y elimine el contenedor? Pues que como el archivo está dentro del contenedor, el archivo de sqlite desaparecerá. Podríamos usar la opción -v para crear un enlace o volumen entre un directorio del host y el directorio del contenedor que contiene el archivo de la base de datos pero vamos a hacerlo de otro modo.

El sistema de archivos de un contenedor es efímero, lo que significa que desaparece cuando lo hace el contenedor. Para arreglar esto vamos a utilizar un contendor a mayores, una base de datos Mariadb 
y luego la línea de comandos que inicia el contenedor de nuestra aplicación será un poco más larga con opciones que nos permitan acceder a este nuevo contenedor.


Añadiendo un contenedor de Mariadb


Como otros muchos productos y servicios, Mariadb o también Mysql tienen imágenes de contenedores públicos disponibles en el registro de Docker. Al igual que el contenedor de nuestra aplicación, Mariadb se basa en variables de entorno que deben pasarse al comando que se encarga de su ejecución en Docker (contraseña, nombre de la base de datos, etc). Utilizaremos la imagen oficial que podemos encontrar en esta dirección:

https://hub.docker.com/_/mariadb


Si recordáis el proceso de configurar Mariadb para nuestro proyecto que se encuentra en el apéndice del capítulo 20 es bastante laborioso, que no complicado. Sin embargo utilizando los contenedores de Docker es un juego de niños en comparación. Aquí el comando de Docker que inicia un servidor de Mariadb con una persistencia de datos es:

$ docker run -d \
--name mariadbc \
-p 3306:3306 \
--rm \
-e MYSQL_RANDOM_ROOT_PASSWORD=yes \
-e MYSQL_DATABASE=mibase \
-e MYSQL_USER=usuario \
-e MYSQL_PASSWORD=password \
-v /home/usuario/src/base_de_mariadb:/var/lib/mysql \
mariadb
¡Y ya está ! En cualquier máquina que tenga Docker instalado, podemos ejecutar el comando anterior y tendremos un servidor Mariadb completamente instalado, con una contraseña de root generada aleatoriamente, una nueva base de datos llamada "mibase" y un usuario nuevo "usuario" que está configurado con permisos completos para acceder a la base de datos. Ten en cuenta que debemos poner un valor adecuado para la contraseña en la variable de entorno MYSQL_PASSWORD.

Lo interesante, además, es que hemos creado una vinculación (--volume o -v) que no es ni más ni menos que se creará una copia de lo que en el contenedor está en /var/lib/mysql (donde se guardan las bases de datos en mysql o mariadb) en nuestro directorio de trabajo /home/usuario/scr/base_de_mariadb y ambos directorios estarán sincronizados (tienes que poner la ruta de la carpeta de tu proyecto. No importa que no exista la carpeta aún porque el comando la crea. Puedes obtener la ruta con el comando pwd en linux)

Y lo más interesante es que cualquier modificación que hagas en un archivo dentro de cualquiera de las dos carpetas queda automáticamente reflejado en la otra. De esta forma si cerramos el contenedor se guardarán los datos para la próxima vez en el disco local del host.

Y ahora ya podemos ejecutar nuestra aplicación de nuevo, pero esta vez con un enlace al contenedor de la base de datos para que ambos puedan comunicarse a través de la red.

src $ docker run --name test1 \
-d -p 8000:5000 \
--rm \
--link mariadbc \
-e DATABASE_URL=mariadb+pymysql://usuario:password@mariadbc/mibase \
flask22
El argumento -d es para que el programa se ejecute en segundo plano.

El argumento -p 8000:5000 le dice a Docker que lo que internamente esta funcionando en el puerto 5000 nosotros podamos usarlo a través del puerto 8000 del host.

El argumento --rm es para que el contedor se borre al cerrarlo.

El argumento --link le dice a Docker que haga que otro contenedor, en este caso el de mariadbc, este disponible para este. Con el enlace entre los dos contenedores establecido puedo configurar la variable de entorno DATABASE_URL que es el conector entre mariadb y el paquete SQLAlchemy de Python.


Con esto ya tenemos una base de datos funcionando y una aplicación de flask que está conectada a esa base de datos. Sin embargo nuestra base de datos de mariadb, "mibase", está vacía, no tiene ninguna tabla porque en los otros capítulos estaba funcionando con una base sqlite. Sin embargo en el código ya está preparada con la librería de python flask-migrate para realizar la migración. Lo primero que tenemos que hacer es entrar en el shell del contenedor test1 para realizar unas pocas instrucciones. (También hubieramos podido usar las próximas dos instrucciones en local y la tercera meterla en el ENTRYPOINT). 

1º Ejecutamos el shell del contenedor test1:

src $ docker exec -it test1 sh

y una vez dentro inicializamos la migración:

/code # flask db init
  Creating directory /code/migrations ...  done
  Creating directory /code/migrations/versions ...  done
  Generating /code/migrations/alembic.ini ...  done
  Generating /code/migrations/README ...  done
  Generating /code/migrations/env.py ...  done
  Generating /code/migrations/script.py.mako ...  done
  Please edit configuration/connection/logging settings in '/code/migrations/alembic.ini' before proceeding.


2º Realizamos la primera migración:

/code # flask db migrate -m "docker mariadb"
INFO  [alembic.runtime.migration] Context impl MariaDBImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'usuario'
  Generating /code/migrations/versions/a9939dd69617_docker_mariadb.py ...  done


3 Finalizamos aplicando los cambios.

/code # flask db upgrade
INFO  [alembic.runtime.migration] Context impl MariaDBImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> a9939dd69617, docker mariadb

Podemos salir del shell tecleando: exit

Ya está todo. Como nuestro contenedor tiene persistencia todo esto quedará ya guardado en el directorio local (acuérdate que funciona en ambos lados como un espejo: ordenador local <-> Contenedor).

Si quieres, para seguir probando puedes usar un contenedor de phpmyadmin para ver y gestionar la base de datos: 

src $ docker run -d --rm \
--name phpmyadminc \
--link mariadbc \
-e PMA_HOST=mariadbc \
-p 8080:80 \
phpmyadmin/phpmyadmin

y entrando en el navegador en el local host a traves del puerto 8080 y usando el usuario y contraseña con la que creasteis el contenedor de mariadb podemos gestionar la base de datos:


página de inicio de PhpmyAdmin




Así de sencillo.

Ahora solo queda probar el probar nuestra aplicación varias veces introduciendo varios usuarios, cerrando el contenedor y volviéndolo a ejecutar de nuevo para ver que no se ha perdido la información.


El registro de contenedores de Docker.


Ahora tenemos la aplicación completa en funcionamiento en Docker, usando dos contenedores, de los cuales uno proviene de una imagen de terceros que es de acceso público (mariadb). Si quieres que las imágenes que tu crees estén disponibles para otros, tenemos que enviarlas al registro de Docker desde donde serán accesibles para todo el mundo.

Para acceder al registro de Docker tenemos que ir a la siguiente dirección https://hub.docker.com y crearnos una cuenta. Asegúrate de elegir un nombre de usuario que te guste ya que se utilizará en todas las imágenes que publiques.

Una vez que estés registrado, para acceder a tu cuenta desde la línea de comandos tenemos que iniciar sesión usando el siguiente comando:

$ docker login
Si has seguido las instrucciones de este capítulo, seguramente tendrás una imagen llamada flask22 que está guardada físicamente en el ordenador. Para poder subir esta imagen al archivo de docker es necesario cambiarle el nombre para que incluya nuestra cuenta de usuario. Esto se hace con el comando de Docker, tag:

$ docker tag flask22:latest <nombre_usuario_en_docker>/flask22:latest
* la etiqueta es opcional si no la pones por defecto te asigna la etiqueta "latest"

Otra forma de hacer esto es, cuando creamos originalmente la imagen (docker build) llamarla directamente usando esa sintaxis nombre_usuario/nombre_imagen:[etiqueta]

Una vez que la tenemos renombrada para que nuestro nombre de usuario de Docker aparezca antes del nombre de la imagen, la subimos a Docker Hug con:

$ docker push <nombre_usuario_en_docker>/flask22:latest
Ahora la imagen está disponible públicamente y podemos documentar como instalarla y ejecutarla desde el registro de Docker  de la misma manera que lo hace la imagen de Mariadb y otras muchas. 


Despliegue de aplicaciones que estén en contenedores.


Una de las mejores cosas de tener tu aplicación ejecutándose en contenedores de Docker, es que una vez que los hayamos probado en un entorno local, podemos llevarlo a cualquier plataforma que ofrezca soporte para Docker (Digital Ocean, Linode, Amazon lightsail, Google Clouds, Microsoft Azure, IBM cloud y otros). Incluso la oferta más barata de cualquiera de ellos es suficiente para ejecutar varios contenedores de Docker.






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.