jueves, 24 de diciembre de 2020

¿Cómo generar backups Django desde archivo Python?


 A continuación el código para generar nuestros backups en Django. Lo incluimos en una tarea automatizada para que el código se ejecute automáticamente cada cierto tiempo. Es necesario que el código sea ejecutado en nuestro entorno virtual.

Para ello hacemos uso de la librería pathlib, útil para trabajar con rutas de archivos y directorios. Ofrece un sin número de método adecuados a la plataforma. Según su documentación, este módulo ofrece clases que representan rutas del sistema de archivos con semántica apropiada para diferentes sistemas operativos. Las clases de ruta se dividen entre rutas puras , que proporcionan operaciones puramente computacionales sin E/S, y rutas concretas , que heredan de rutas puras pero también proporcionan operaciones de E/S.

Y también de hacemos uso de la librería zipfile para comprimir nuestros backups en archivos .zip.


import os
import datetime


from pathlib import Path # https://docs.python.org/3/library/pathlib.html
from zipfile import ZipFile # https://docs.python.org/3/library/zipfile.html


# Importamos los settings de nuestro proyecto.
from app.settings import (setting1, setting2, setting3)

SETTINGS = (setting1, setting2, setting3)

# Directorio donde serán almacenados las copias de seguridad.
BACKUP_DIR_NAME = Path(__file__).resolve().parent.parent / "backups"

# Sufijo que tendrá el nombre del archivo generado Ej. nombre_20201224.zip
FILE_SUFFIX_DATE_FORMAT = "%Y%m%d"

# Cantidad de días que durará una copia de seguridad
# almacenada antes de proceder a borrarla automáticamente.
DAYS_TO_KEEP_BACKUP = 3

# Usaremos esta fecha para comparar los archivos antiguos que serán eliminados.
back_date = datetime.datetime.now() - datetime.timedelta(days=DAYS_TO_KEEP_BACKUP)
back_date = back_date.strftime(FILE_SUFFIX_DATE_FORMAT)


# Eliminamos primero para asegurarnos de que casi siempre haya
# espacio disponible para los nuevos backups que serán creados.

for setting in SETTINGS:
path = BACKUP_DIR_NAME

    # Suponiendo hemos creado una variable NAME en cada uno de nuestros setting
prefix = setting.NAME + "_"

# Listamos los archivos dentro del directorio donde se guardan los backups.
list_files = path.iterdir()

for f in list_files:
if f.suffix.lower() == ".zip":

try:
suffix = f.name.split("_")[1].split(".")[0]
except (IndexError):
# Si el archivo no tiene la estructura 'name_%Y%m%d.zip'
# entonces no es un archivo que se ha generado aquí.
continue
# Comparamos las fechas de tipo string y funciona ya que:
# '20201221' es menor que '20201224.
if suffix < back_date:
print(f"Eliminando: {f}")
f.unlink(missing_ok=True)



# Ahora procedemos a generar nuestros backups.

for setting in SETTINGS:

# Construimos el nombre que tendrá el archivo de salida, de acuerdo
# a este formato: 'filename_%Y%m%d.json' -> 'ininstancia_20201224.json'.
timestamp = datetime.datetime.now().strftime(FILE_SUFFIX_DATE_FORMAT)
backup_filename = BACKUP_DIR_NAME / f"{setting.NAME}_{timestamp}.json"

# Ejecutamos el comando python manage.py dumpdata...
print(f"dumpdata para {setting.NAME}")
os.system(f"python manage.py dumpdata > {backup_filename}")

# Creamos el archivo .zip y eliminamos el .json.
zip_filename = backup_filename.with_suffix(".zip")
with ZipFile(zip_filename, 'w') as zip:
zip.write(backup_filename, backup_filename.name)

# Eliminamos el archivos .json.
backup_filename.unlink(missing_ok=True)

print(zip_filename, "OK")

lunes, 14 de diciembre de 2020

Borrar los archivos de migraciones en Django

Script para eliminar todos los archivos de migraciones generados en un proyecto Django.


En la mayoría de los casos no es recomendable borrar los archivos de migraciones generados por Django al ejecutar el comando python manage.py makemigrations, ya que esto guarda un historial completo de los cambios realizados en la base de datos, y así Django, al ejecutar el comando python manage.py migrate determina que migraciones se van aplicar en la base de datos en cuestión. Puedes leer más acerca de las migraciones en Django en el siguiente enlace. Pero, en algunos casos, especialmente para proyectos nuevos que aún no han sido implementados en producción, nos resulta útil borrar estas migraciones. 

Por ejemplo:

Cuando estamos creando un nuevo proyecto Django, es frecuente que realicemos cambios en nuestros modelos, cambiando parámetros de nuestros campos, ya sea corrigiendo errores ortográficos en los textos de parámetro help_text o verbose_name, etc., los cuales resultarán en un nuevo archivo de migración creado por Django, aunque estos cambios no sean tan relevantes. Entonces al momento de implementar nuestro proyecto en producción, tendríamos un sin número de migraciones prácticamente innecesarias, ya que bien podrían haberse incluido en las migraciones iniciales. 

Podemos manualmente eliminar cada una de estas migraciones entrando a cada aplicación y eliminar los archivos uno a uno, pero por comodidad siempre preferimos automatizar todo.


"""
Elimina los archivos dentro las carpetas 'migrations'
"""

import os

opt = str(input("Si continua se borrarán todas las migraciones de todas las "
"aplicaciones que se encuentran en el directorio base. Especificamente dentro "
"de los subdirectorios 'migrations'. ¿Desea continuar? yes/no: "))

if opt.lower() != "yes":
exit(0)

def removedir(path):
for name in os.listdir(path):
p = os.path.join(path, name)
if os.path.isdir(p):
removedir(p)
else:
try:
os.remove(p)
except (WindowsError) as e:
print(e)
else:
print("CORRECTO -- ", p)

path1 = os.curdir

# Remover todos los archivos dentro de los
# directorios 'migrations' y __pycache__
for dirname in ["migrations", "__pycache__"]:
for name in os.listdir(path1):
p = os.path.join(path1, name, dirname)
if os.path.isdir(p):
removedir(p)
try:
# Volvemos a crear el __init__.py borrado.
f = open(os.path.join(p, "__init__.py"), "w")
f.close()
except (BaseException) as e:
print(e)


Copias de seguridad periódica en PythonAnywhere

¿Como hacer copias de seguridad  de la base de datos de forma periódica y automática en pythonanywhere.com?

A continuación se muestra el código de forma detallada de como poder realizar nuestros backups de la base de datos mediante la ejecución de tareas.

De manera general lo que haremos es crear un archivo Python dentro de algún directorio de nuestra cuenta en Pythonanywhere, y luego hacer que dicho archivo sea ejecutado de forma periódica mediante la programación de tareas en el mismo Pythonanywhere.

Para empezar creamos nuestro archivo .py importamos los módulos que usaremos:

import os
import datetime
from zipfile import ZipFile

lunes, 14 de septiembre de 2020

Código para mostrar las diferencias entre tus modelos Django y la base de datos

¿Cómo saber si existen diferencias entre tus modelos Django y la base de datos? 

Es posible que, por lo menos una vez en la vida, hayas tenido problemas aplicando las migraciones de tu proyecto Django.
Las migraciones son la forma en que Django propaga los cambios que realiza en sus modelos (agregar un campo, eliminar un modelo, etc.) en el esquema de su base de datos. Están diseñados para ser en su mayoría automáticos, pero necesitará saber cuándo realizar migraciones, cuándo ejecutarlas y los problemas comunes que puede encontrar. https://docs.djangoproject.com/en/3.1/topics/migrations/

¿Cuáles son los problemas más comunes? 

 El sistema de migraciones de Django es un sistema bastante potente. Te permite realizar varias operaciones importantes en tu base de datos, sin tan siquiera tener que escribir SQL. El sistema puede crear, a partir de los modelos que hayas creado en tus aplicaciones, toda la estructura de la base de datos, las modificaciones que realices a tus modelos se verán reflejadas en tu base de datos con tan solo escribir python manage.py makemigrations y python manage.py migrate. También te permite realizar la operación de manera contraria, creando tus modelos a partir de una base de datos ya existente. Pero, como bien lo dice el equipo de Django en su guía:
"no es perfecto y, para cambios complejos, es posible que no detecte lo que esperaba".
De modo que puedas verte en la necesidad de realizar ajustes en tus modelos o en la base de datos de forma manual. El siguiente código te puede ayudar a encontrar las diferencias existentes entre tus modelos y la base de datos:

from django.db import connections
from django.db.models import ForeignKey, ManyToOneRel, ManyToManyRel, ManyToManyField
from django.db.utils import OperationalError
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings



def get_cursor(db_alias="default"):
"""
Obtiene el cursor de la conección a la base de datos.
"""
return connections[db_alias].cursor()


def check_database():
"""
Compruba que la información de los modelos está acorde con la información
en la base de datos. Los campos que estén acordes con la información de la
base de datos, serán marcados con una [X]; una casilla vacia en caso contrario [ ].
"""
from django.contrib.contenttypes.models import ContentType
qs = ContentType.objects.all()
cursor = get_cursor()

models_evals = []
dbname = str(settings.DATABASES["default"]["NAME"])
text = f"=======================\nTest: {dbname}\n=======================\n"
print(text)
for ct in qs:
model = ct.model_class()
if (model is None):
continue
models_evals, text = check_model(model, cursor, models_evals, text)
filename = dbname.replace("$", "_").replace(".", "_").replace(" ", "_")
filename = "test/" + filename.split("/")[-1] + ".txt"
f = open(filename, "w")
f.write(text)
f.close()
print(f"Guardado en: '{filename}'")


def check_model(model, cursor, models_evals=[], text=""):
"""
Compara la información de los campos del modelo indicado, con la base de datos.
"""
if (str(model) in models_evals):
return models_evals, text

print("\n----------------------------------")
print(model, model._meta.verbose_name)
text += f"\n{model} | {model._meta.verbose_name}\n"
models_evals.append(str(model))

try:
fields = model._meta.get_fields()
except (AttributeError) as e:
return models_evals, text

manytomanyfieldsmodels = []

for field in fields:
name = field.name

if isinstance(field, ForeignKey):
name = name + "_id"

elif isinstance(field, ManyToManyField):
manytomanyfieldsmodels.append(field.related_model)
print(f"Relación: {field}")
text += f"Relación: {field}\n"
continue

elif (isinstance(field, (ManyToOneRel, ManyToManyRel))):
print(f"Dependiente: {field}")
text += f"Dependiente: {field}\n"
continue

pri = f" - Field: {field.__class__.__name__}( '{name}' )"
try:
cursor.execute(f"SELECT {name} FROM {model._meta.db_table};")
except (OperationalError) as e:
pri = "[ ] "+ pri + f" | Error: {e}"
else:
pri = "[X] " + pri

print(pri)
text += f"{pri}\n"

for many_model in manytomanyfieldsmodels:
models_evals, text = check_model(many_model, cursor, models_evals, text)

return models_evals, text