Cómo crear un CRUD en python. Parte 2: Creación y listado
Bienvenidos a la segunda parte de este tutorial en el que vamos a aprender muchas funcionalidades chulas de Python. En el tutorial de hoy nos encargaremos de la creación de un contacto, su guardado en un csv y el posterior listado de los contactos.
Lo primero que vamos a hacer, es crear una clase que nos permita validar los datos introducidos por el usuario, para ello crearemos un archivo llamado validations.py dentro de nuestra carpeta classes e introduciremos el siguiente código:
import re
import datetime
regex_email = '^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$'
regex_phone = '^[0-9]{9}$'
class Validations:
def __init__(self):
pass
def validateName(self, name):
if len(name) < 3 or len(name) > 50:
raise ValueError(f'El nombre debe tener como mínimo 3 caractares y un máximo de 50 caracteres, tamaño actual: {len(name)}')
return True
def validateSurname(self, surname):
if len(surname) < 5 or len(surname) > 100:
raise ValueError(f'Los apellidos deben tener como mínimo 5 caractares y un máximo de 100 caracteres, tamaño actual: {len(surname)}')
return True
def validateEmail(self, email):
if not re.search(regex_email, email):
raise ValueError('El formato del email no es válido')
return True
def validatePhone(self, phone):
if not re.search(regex_phone, phone):
raise ValueError('El formato del teléfono no es válido, debe ser un número de 9 cifras sin guiones ni puntos')
return True
def validateBirthday(self, birthday):
try:
datetime.datetime.strptime(birthday, '%Y-%m-%d')
except ValueError:
raise ValueError('El formato de la fecha es incorrecta, debe ser YYYY-MM-DD')
return True
Lo primero que estamos haciendo es importar la librería re, para expresiones regulares, y la librería datetime con la que podremos manipular fechas.
Después creamos dos expresiones regulares para validar tanto el correo electrónico como el número de teléfono introducido por el usuario.
Lo siguiente que haremos será declarar la clase Validations en donde guardaremos una validación por cada campo. Lo que hemos hecho en cada función es añadir una validación, sino la cumple, lanzaremos una excepción de tipo Value error, estas se lanzan de esta forma:
raise ValueError('Mensaje customizado con el error')
Para el nombre y los apellidos, solo estamos validando un tamaño mínimo y máximo del campo.
Para el email, verificamos con nuestra expresión regular si el email introducido es válido.
Con el teléfono validamos que sea un número de nueve cifras.
Para finalizar, con el cumpleaños validamos que el formato de fecha introducido es válido con la librería datetime, si lanza una excepción es que no nos está enviando el formato correcto, aquí lo estamos capturando con un bloque de try catch que en python se usan de esta forma:
try:
datetime.datetime.strptime(birthday, '%Y-%m-%d')
except ValueError:
raise ValueError('El formato de la fecha es incorrecta, debe ser YYYY-MM-DD')
Ahora que ya tenemos creada nuestra clase para las validaciones nos pondremos manos a la obra con la creación, para ello abrimos el archivo main.py y añadimos nuestra clase Validations, de tal forma que ahora el inicio del archivo quedaría así:
import os
import time
from classes.validations import Validations
validator = Validations()
Con from le decimos donde está nuestra clase y con import la clase que queremos importar, si tuvieramos varias clases dentro del archivo, solo tendríamos que añadirlas separadas por comas si también quisieramos importarlas.
Ahora iremos a la función run y en la creación añadiremos la función create_contact, que será la que llamará cuando queramos crear un contacto:
def run():
print_options()
command = input()
command = command.upper()
if command == 'C':
create_contact()
elif command == 'L':
pass
elif command == 'M':
pass
elif command == 'E':
pass
elif command == 'B':
pass
elif command == 'S':
os._exit(1)
else:
print('Comando inválido')
time.sleep(1)
run()
Lo siguiente que haremos será declarar la función create_contact.
def create_contact():
print('CREACIÓN DE CONTACTO')
print('*' * 50)
name = check_name()
surname = check_surname()
email = check_email()
phone = check_phone()
birthday = check_birthday()
Esta función nos mostrará un mensaje y por cada dato de nuestro contacto llamará una función para revisar los datos. Ahora que tenemos esta función vamos a definir los checks.
def check_name():
print('Inserta el nombre:')
name = input()
try:
validator.validateName(name)
return name
except ValueError as err:
print(err)
check_name()
def check_surname():
print('Inserta los apellidos:')
surname = input()
try:
validator.validateSurname(surname)
return surname
except ValueError as err:
print(err)
check_surname()
def check_email():
print('Inserta el email:')
email = input()
try:
validator.validateEmail(email)
return email
except ValueError as err:
print(err)
check_email()
def check_phone():
print('Inserta el teléfono (9 cifras sin guiones ni puntos):')
phone = input()
try:
validator.validatePhone(phone)
return phone
except ValueError as err:
print(err)
check_phone()
def check_birthday():
print('Inserta la fecha de nacimiento (YYYY-MM-DD):')
birthday = input()
try:
validator.validateBirthday(birthday)
return birthday
except ValueError as err:
print(err)
check_birthday()
Aquí lo que hacemos es decirle al usuario el dato que queremos, lo capturamos con input y luego usamos nuestro objeto validator para validar el campo introducido por el usuario, si es válido lo retornamos a nuestra clase create_contact, sino capturamos la expeción, mostramos el error al usuario y llamamos de nuevo a la función para que vuelva a pedir el dato.
Como se ve en el código es bastante similiar para todos los datos y ya que queremos hacer un código más limpio lo que haremos será unificarlo todo en una misma función y que según el dato que pase, realice la validación correspondiente. Para ello crearemos la función check_contact_data que contendrá el siguiente código:
def check_contact_data(message, data_name):
print(message)
input_data = input()
try:
getattr(validator, f'validate{data_name.capitalize()}')(input_data)
return input_data
except ValueError as err:
print(err)
check_contact_data(message, data_name)
Para esta función estamos pasando el mensaje que queremos mostrar al usuario y el nombre del campo. A la hora de llamar a la función del validator para revisar nuestro dato, utilizaremos getattr; en esta función, pasamos el objeto y el nombre de la función en formato de texto y le enviamos el dato a validar, que en este caso es input_data y funciona igual que si hubiesemos llamado a validator.validateName(name).
Ahora lo que haremos será modificar la función create_contact que quedaría así:
def create_contact():
print('CREACIÓN DE CONTACTO')
print('*' * 50)
name = check_contact_data('Inserta el nombre:', 'name')
surname = check_contact_data('Inserta los apellidos:', 'surname')
email = check_contact_data('Inserta el email:', 'email')
phone = check_contact_data('Inserta el teléfono (9 cifras sin guiones ni puntos):', 'phone')
birthday = check_contact_data('Inserta la fecha de nacimiento (YYYY-MM-DD):', 'birthday')
Como veis, pasamos por parámetro el mensaje que queremos mostrar al usuario y el nombre del campo y ya podemos reutilizar la función.
Lo siguiente que haremos será crear dos clases, una para la escritura y lectura del csv y otra que extenderá de esta y que servirá de enlace con nuestra clase principal.
Primero empezaremos con la clase para trabajar con los csvs, se llamará dbcsv.py y estará dentro de la carpeta classes. Una vez creado el archivo, añadiremos el constructor.
import csv
class DBbyCSV:
def __init__(self, schema, filename):
self._filename = f'./{filename}.csv'
try:
# Verificamos si ya existe el archivo
f = open(self._filename)
f.close()
except IOError:
# Si el archivo no existe creamos la cabecera
with open(self._filename, mode='w', encoding='utf-16') as csv_file:
data_writer = csv.writer(csv_file, delimiter=';', quotechar='"', quoting=csv.QUOTE_MINIMAL, lineterminator='\n')
data_writer.writerow(schema.keys())
En el constructor pasamos por parámetro el schema, que más adelante veremos como hacerlo, y el nombre del archivo. Después verificamos si ya existe el archivo, sino es así abriremos el archivo con with open() y por parámetro pasaremos el nombre del archivo, el modo en este caso sera 'w' que es del escritura y la codificación.
Para lo que se pregunten que es la instrucción with, esta se utiliza para obtener una mejor sintaxis y manejo de excepciones, además se encarga de cerrar automáticamente el archivo.
Después utilizaremos la función writer de la libería de csv de python que se encargará de convertir los datos en cadenas delimitadas por la lista dada.
En nuestro caso le estamos diciendo que el delimitador será por ';'.
Cada cadena de texto estará encapsulada dentro de las comillas dobles.
Con quoting=csv.QUOTE_MINIMAL indicaremos que las comillas solo las añada a los textos que tengan caracteres especiales.
Para finalizar, con lineterminator le indicamos que termine la línea con el salto de línea "\n", esto lo indicamos ya que por defecto, las clases del módulo csv usan los terminadores de línea de Windows (\r\n) y esto crea líneas vacías en nuestro archivo.
Una vez creado el writer, lo que hacemos es pasarle las claves de diccionario schema que serán las cabeceras del archivo.
Ahora que ya hemos definido nuestra clase base es hora de crear otra clase que extenderá de esta, creamos un archivo llamado dbcontacts.py dentro de classes y añadimos el siguiente código:
from .contact import Contact
from .dbcsv import DBbyCSV
SCHEMA = {
'ID': {
'type': 'autoincrement',
},
'NAME': {
'type': 'string',
'min_length': 3,
'max_length': 50
},
'SURNAME': {
'type': 'string',
'min_length': 5,
'max_length': 100
},
'EMAIL': {
'type': 'string',
'max_length': 254
},
'PHONE': {
'type': 'int'
},
'BIRTHDAY': {
'type': 'date'
}
}
class DBContacts(DBbyCSV):
def __init__(self):
super().__init__(SCHEMA, 'contacts')
Lo primero que estamos haciendo es importarnos nuestras clases, definimos una constante llamada SCHEMA que contendrá un diccionario con los nombres de nuestros datos y unas reglas definidas que se usarán más tarde en posteriores tutoriales de esta serie y por último creamos la clase DBContacts que extiende de DBbyCSV y llamaremos a la función super que lo que hará es que podamos acceder al constructor padre y pasamos por parámetros tanto el schema como el nombre que le queremos dar al archivo.
Importante
Como veis, hemos añadido el campo ID pero en nuestra clase Contacts no lo tenemos así que lo añadimos en el constructor y definimos el getter y el setter.
classes/contact.py
class Contact:
def __init__(self, id_contact, name, surname, email, phone, birthday):
self._id_contact = id_contact
self._name = name
self._surname = surname
self._email = email
self._phone = phone
self._birthday = birthday
@property
def id_contact(self):
return self._id_contact
@id_contact.setter
def id_contact(self, id_contact):
self._id_contact = id_contact
.
.
.
Ahora que todo está como debería volvemos al archivo dbcsv.py y dentro de la clase, añadimos la funcionalidad para insertar contactos.
def insert(self, data):
id_contact = self.get_last_id() + 1
line = [id_contact] + data
with open(self._filename, mode='a', encoding='utf-16') as csv_file:
data_writer = csv.writer(csv_file, delimiter=';', quotechar='"', quoting=csv.QUOTE_MINIMAL, lineterminator='\n')
data_writer.writerow(line)
return True
def get_last_id(self):
list_ids = []
with open(self._filename, mode='r', encoding='utf-16') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=';')
is_header = True
for row in csv_reader:
if is_header:
is_header = False
continue
if row:
list_ids.append(row[0])
if not list_ids:
return 0
# Ordenamos la lista de mayor a menor y retornamos el elemento de mayor tamaño
list_ids.sort(reverse = True)
return int(list_ids[0])
En esta parte estamos declarando dos funciones, insert y get_last_id.
La función insert obtiene el identificador más alto mediante la función get_last_id, crea una nueva lista con el identificador más alto +1 y los datos del contacto y después mediante el writer los añade en el csv. Observad que esta vez hemos usado el modo 'a' en vez del modo 'w', la diferencia entre estos dos modos es que el modo 'w' sobreescribe el documento, por lo que si lo usamos borraría todo lo anterior escrito en el, el modo 'a' añade los datos que insertamos al final del documento.
La función get_last_id recorre el csv guardando todos los identificadores, después los ordena de mayor a menor y por último retorna el primer elemento de la lista. Si la lista está vacía retornará 0.
Ahora que ya tenemos el insert en la parte de nuestra clase para trabajar con csvs, volvemos a la clase DBContacts y creamos la función save_contact que la utilizaremos para guardar un contacto.
def save_contact(self, contact):
data = [contact.name, contact.surname, contact.email, contact.phone, contact.birthday]
return self.insert(data)
Aquí simplemente obtenemos el objeto contacto y convertimos los datos a una lista que después pasamos a nuestra función de insertado de datos en el csv.
Ya tenemos la parte de inserción creada así que ahora volvemos a nuestro archivo main.py e importamos la clase Contact y DBContacts, por último creamos una instancia de la clase DBContacts que usaremos posteriormente.
import os
import time
from classes.validations import Validations
from classes.contact import Contact
from classes.dbcontacts import DBContacts
validator = Validations()
db = DBContacts()
Volvemos a la función create_contact dentro de main.py y añadimos las líneas para insertar el nuevo contacto en la agenda.
def create_contact():
print('CREACIÓN DE CONTACTO')
print('*' * 50)
name = check_contact_data('Inserta el nombre:', 'name')
surname = check_contact_data('Inserta los apellidos:', 'surname')
email = check_contact_data('Inserta el email:', 'email')
phone = check_contact_data('Inserta el teléfono (9 cifras sin guiones ni puntos):', 'phone')
birthday = check_contact_data('Inserta la fecha de nacimiento (YYYY-MM-DD):', 'birthday')
contact = Contact(None, name, surname, email, phone, birthday)
if db.save_contact(contact):
print('Contacto insertado con éxito')
else:
print('Error al guardar el contacto')
Como veis, estamos creando un objeto de tipo Contact y después lo pasamos a la función save_contact que devolerá True si todo ha ido correctamente.
Para la última parte de este tutorial vamos a crear la funcionalidad para listar los contactos, vamos al archivo dbcsv.py y en la clase añadimos la función para devolver un listado con todos los contactos llamada get_all.
def get_all(self):
list_data = []
list_header = []
with open(self._filename, mode='r', encoding='utf-16') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=';')
is_header = True
for row in csv_reader:
if is_header:
list_header = row
is_header = False
continue
if row:
file = {}
for key, value in enumerate(row):
file[list_header[key]] = value
list_data.append(file)
return list_data
Esta función lo que hace es leer el csv y crear un diccionario por cada contacto usando como clave los textos de la cabecera y posteriormente guardando cada contacto en una lista que retornaremos.
Ahora abrimos el archivo dbcontacts.py y en la clase añadimos la función list_contacts y get_schema.
def list_contacts(self):
list_contacts = self.get_all()
if not list_contacts:
return None
object_contacts = []
# Convertimos los datos a objectos de tipo contact
for contact in list_contacts:
c = Contact(contact['ID'], contact['NAME'], contact['SURNAME'], contact['EMAIL'], contact['PHONE'], contact['BIRTHDAY'])
object_contacts.append(c)
return object_contacts
def get_schema(self):
return SCHEMA
La función list_contacts llamara a la función get_all y recuperará el listado de contactos, después sino está vacía, convertirá cada diccionario en un objecto de tipo Contact, lo guardará en una lista y lo devolverá.
La función get_schema devuelve schema que creamos para la clase, ahora veremos su utilidad.
Ahora vamos a necesitar una librería que no viene por defecto en python por lo que crearemos un entorno virtual en nuestro proyecto, sino sabéis crearlo, en este tutorial explico instalar pip y el entorno virtual.
Una vez hecho esto y hallamos activado el entorno virtual, instalaremos la libería PrettyTable, que la utilizaremos para mostrar el listado de contactos de una forma más amigable, para ello lanzamos el siguiente comando.
pip3 install PrettyTable
Volvemos al archivo main.py e importamos la librería PrettyTable.
import os
import time
from classes.validations import Validations
from classes.contact import Contact
from classes.dbcontacts import DBContacts
from prettytable import PrettyTable
validator = Validations()
db = DBContacts()
Y después creamos la función list_contacts.
def list_contacts():
list_contacts = db.list_contacts()
if not list_contacts:
return print('Todavía no hay contactos guardados')
table = PrettyTable(db.get_schema().keys())
for contact in list_contacts:
table.add_row([
contact.id_contact,
contact.name,
contact.surname,
contact.email,
contact.phone,
contact.birthday
])
print(table)
print('Pulsa intro para salir')
command = input()
Esta función se encarga de recuperar el listado de contactos, sino hay ninguno, muestra un mensaje, si por el contrario si los tenemos, creamos un objecto de tipo PrettyTable, en el constructor le pasamos las cabeceras y después recorremos el listado de contactos para añadirlos a la tabla. Una vez hecho esto mostramos la tabla con un print y detenemos la finalización de la función con input, cuando el usuario quiera volver al menú, simplemente tendrá que pulsar intro.
Muy importante, ya tenemos la funcionalidad para mostrar el listado, ahora solo necesitamos añadirla al run para poder usarla.
def run():
print_options()
command = input()
command = command.upper()
if command == 'C':
create_contact()
elif command == 'L':
list_contacts()
elif command == 'M':
pass
elif command == 'E':
pass
elif command == 'B':
pass
elif command == 'S':
os._exit(1)
else:
print('Comando inválido')
time.sleep(1)
run()
Conclusiones
Sin duda este es el tutorial más largo que he hecho en mi vida, hemos aprendido a leer, crear y añadir datos a un csv, importar clases y extenderlas.
En el siguiente tutorial aprenderemos a como realizar modificaciones, borrado y búsquedas en un csv.
Recordad que cualquier duda podéis escribirla en la caja de comentarios y el código de esta parte está en mi github.
Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.
Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.
Nos leemos 👋.